C#筆記 – CLR的執行模型(二) 程序集
程序集

-
程序集是一個或多個模塊的邏輯性分組,在CLR中,程序集相當於組件
- 托管模塊和資源文件被編譯器所處理,並生成一個代表文件邏輯分組的PE32/PE32+文件
-
文件包含一個manifest數據塊,為「元數據表的集合」,描述了:
-
構成程序集的文件
-
程序集中的文件所實現的public類型
-
與程序集關聯的資源或數據文件
-
- 托管模塊和資源文件被編譯器所處理,並生成一個代表文件邏輯分組的PE32/PE32+文件
-
編譯器默認將編譯源代碼後生成的托管模塊合并轉換成程序集
-
C#編譯器生成的是含有清單(manifest)的托管模塊
-
如果只有一個托管模塊,且沒有資源文件,程序集就等於托管模塊
-
兩種程序集
-
弱命名程序集、強命名程序集
-
弱命名 vs 強命名
-
在程序集結構上,弱命名和強命名的結構完全一致,兩者的區別在於:
- 強命名程序集使用發布者的公鑰/私鑰進行了簽名。這對密鑰允許對程序集進行唯一性的標識、保護和版本控制,并允許程序集部署在用戶機器的任何地方,甚至是internet上。
-
加載CLR
-
生成的每個程序集由CLR管理程序集中的代碼的執行,機器上必須安裝好.NET Framework
-
如果%SystemRoot%System32目錄中存有mscoree.dll文件,就代表.NET Framework已經安裝

- 而在%SystemRoot%Microsoft.NETFramework和%SystemRoot%Microsoft.NETFramework64的子目錄中,則可了解到安裝了哪些版本的.NET Framework

- .NET Framework SDK提供了CLRVer.exe的命令行實用程序,可列出機器上安裝的所有CLR版本;也可以通過-all命令列出機器中正在運行的進程使用的CLR版本號

IL代碼
-
程序集中包含托管模塊,托管模塊裡包含元數據和IL
-
IL是與CPU無關的機器語言,比大多數CPU機器語言高級
-
IL能執行訪問和操作對象類型、創建/初始化對象、調用虛方法、操作數組元素、異常處理等工作
- 一種「面向對象的機器語言」
-
-
IL一般是開發人員編寫高級語言代碼(如C#)後,被編譯器所生成
-
一般高級語言只公開了CLR的部分功能
-
IL匯編語言允許開發人員訪問CLR的全部功能
-
IL可以使用匯編語言編寫
-
IL基於棧,因此所有指令都要將操作數Push進一個執行棧,再從棧Pop出結果
-
IL指令是「無類型」(typeless)的,它判斷棧中的操作數的類型,並執行恰當的操作
-
-
IL的驗證
-
在JIT把IL編譯成本機CPU指令時,CLR會檢查IL代碼所做的一切是否安全,如參數、類型、返回值的正確使用
-
托管模塊的元數據包含驗證過程要用到的所有方法及類型信息
-
-
在Visual Studio中,可以使用ILAsm.exe的IL匯編器
- 如:進行打包DLL/EXE的工作
-

- 還可以使用ILDasm.exe的IL反匯編器來查看代碼被編譯成怎樣的IL代碼

程序集代碼的執行過程
-
為了執行托管程序集裡面的方法,首先要由CLR的「JIT(Just-In-Time)編譯器」把方法的IL代碼轉換成本機的CPU指令
-
以該代碼為例:
-
static void Main(){ Console.WriteLine("Hello"); Console.WriteLine("Goodbye");}
-
-
以下是Console.WriteLine方法被首次調用時的流程(Console.WriteLine(“Hello”)):
-
第一步:在源碼被編譯器編譯成托管模塊後,在執行這個程序集中的Main方法前。
- CLR會檢測出方法體中的代碼所引用的所有類型。在本例中,類型為「Console」
-
第二步:CLR會分配內部數據結構來管理對引用類型的訪問。
- 在該數據結構中,Console類型定義的每個方法都有一個對應的記錄項(entry)
- 每個記錄項都含有一個地址,指向方法的實現
- 該數據結構初始化時,CLR將每個記錄項都指向「包含在CLR內部的一個未編檔函數」(JITCompiler)

-
第三步:Main中的WriteLine被執行時。
-
JITCompiler函數會被調用,該函數負責將方法的IL代碼編譯成本機CPU指令
-
由於這個步驟對IL的編譯是「即時」(Just - In - Time)進行的,因此這個組件才被稱為「JIT編譯器」或 「JITter」
-
-
第四步:JITCompiler函數被調用時。
-
在實現該方法(WriteLine)的類型(Console)元數據中找到該方法(WriteLine)
-
在定義該類的程序集元數據中獲取該方法的IL
-
JITCompiler驗證其IL代碼,并編譯成本機CPU指令
-
把該指令分配到一個動應分配的內存塊中
-
JITCompiler返回CLR為類型創建的內部數據結構中,把對應方法的記錄項地址指向該內存塊(已包含本機CPU指令)
-
JITCompiler跳轉到該內存塊,執行代碼
-
執行完成後返回托管程序集中,執行下一行代碼
-
-
-

- 現在代碼執行到了下一行(Console.WriteLine(“Goodbye”)):
-
由於同樣是使用Console.WriteLine(string)方法,因此,WriteLine的代碼已經經過驗證和編譯
-
內部數據結構中的WriteLine(string)記錄項指向不再是JITCompiler,而是包括了Console.WriteLine(string)具體本機CPU指令的內存塊,因此,CLR會直接執行內存塊中的代碼,完全跳過JITCompiler函數
-

-
JIT將方法的IL編譯成本機代碼時,會利用其元數據中的TypeRef和AssemblyRef確定定義了引用的類型的程序集
- JIT獲取AssemblyRef元數據表記錄項中,構成程序集強名稱的各個部分(名稱、版本、語言文化、公鑰標記),並把它們連接成一個字符串
- 然後根據該字符串標識,嘗試把程序集加載到AppDomain
CLR解析類型引用
-
運行應用程序時,CLR會加載並初始化自身,讀取程序集的CLR頭,查找入口方法的MethodDefToken,然後檢索MethodDef元數據表找到方法的IL,將IL JIT編譯成機器碼,最後執行本機代碼。
-
對IL進行JIT編譯時,CLR會檢測所有類型和成員引用,加載它們的定義程序集
-
Program.cs中有以下代碼,其中有一個Main方法
-
public sealed class Program{ public static void Main(string[] args) { System.Console.WriteLine("Hello World!"); System.Console.ReadLine(); } }
-
-
Main方法編譯後的IL代碼
-
.method public hidebysig static void Main(string[] args) cil managed // SIG: 00 01 01 1D 0E { .entrypoint // 方法開始於 RVA 0x2050 // 程式碼大小 19 (0x13) .maxstack 8 IL_0000: /* 00 | */ nop IL_0001: /* 72 | (70)000001 */ ldstr "Hello World!" IL_0006: /* 28 | (0A)000004 */ call void [mscorlib]System.Console::WriteLine(string) IL_000b: /* 00 | */ nop IL_000c: /* 28 | (0A)000005 */ call string [mscorlib]System.Console::ReadLine() IL_0011: /* 26 | */ pop IL_0012: /* 2A | */ ret } // end of method Program::Mai -
從IL代碼中看出,Main方法包含了System.Console.WriteLine的引用
-
具體來說,IL call指令引用了元數據token:(0A)000004。
-
0A = 0x0a,在CorHdr.h中的CorTokenType枚舉中指出0x0a標識的元數據表為「MemberRef」。因此(0A)000004等價於MemberRef的第4個記錄項
-
// Token tags. typedef enum CorTokenType { ... mdtMemberRef = 0x0a000000, // ... } CorTokenType;
-
-
-
根據這個信息,CLR接下來會在Program.cs生成的程序集元數據中檢查MemberRef第4個記錄項(token:0a000004)的位置。然後發現它位於TypeRef的一個記錄項下面。
-
TypeRef #6 (01000006) ------------------------------------------------------- Token: 0x01000006 ResolutionScope: 0x23000001 TypeRefName: System.Console MemberRef #1 (0a000004) ------------------------------------------------------- Member: (0a000004) WriteLine: CallCnvntn: [DEFAULT] ReturnType: Void 1 Arguments Argument #1: String
-
-
這個記錄項中的ResolutionScope代表了引用類型的實現位置。
-
如果類型在另一個類型中實現,引用指向一個TypeRef記錄項,ResolutionScope = 0x01
-
如果類型在同一個模塊中,引用指向一個ModuleDef記錄項,ResolutionScope = 0x00
-
如果類型在同一個程序集的另一個模塊中,引用指向一個ModuleRef記錄項,ResolutionScope = 0x1a
-
如果類型在不同程序集的其他模塊中,引用指向一個AssemblyRef記錄項,ResolutionScope = 0x23
-
-
在本例中,按照TypeRef的Resolution又把CLR引導至AssemblyRef的第一個記錄項(0x23000001)。在Program.cs生成的程序集元數據文件中找到AssemblyRef的第一個記錄項,得知它需要的是哪個程序集
-
AssemblyRef #1 (23000001) ------------------------------------------------------- Token: 0x23000001 Public Key or Token: b7 7a 5c 56 19 34 e0 89 Name: mscorlib Version: 4.0.0.0 Major Version: 0x00000004 Minor Version: 0x00000000 Build Number: 0x00000000 Revision Number: 0x00000000 Locale: HashValue Blob: Flags: [none] (00000000) -
“mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”
-
-
CLR定位並加載該程序集
-
解析引用的類型時,CLR可能在以下三個地方找到類型:
-
在同一個程序集的同一個文件中
- 「編譯時」就能發現對相同文件中的類型的訪問,稱為「早期綁定」(early binding),直接從文件中加載
-
在同一個程序集的另一個文件中
- 「運行時」確保被引用的文件在當前程序集元數據的FileDef表<中,檢查加載程序集清單文件的目錄,加載被引用的文件,檢查哈希值以確保文件完整性,發現類型的成員
-
在另一個程序集的另一個文件中
- 「運行時」加載被引用程序集的清單文件。如果所需類型不在該文件中,就繼續加載包含了類型的文件,發現類型的成員
-
-
-
-
在上例中,CLR發現System.Console在和調用者不同的程序集中實現
-
CLR須找到那個程序集,加載包含清單元數據的PE文件
-
掃瞄清單,判斷具體是哪個PE文件實現了類型
-
如果類型在程序集的另一個文件中(不包含清單元數據的文件)
-
CLR加載那個文件,掃瞄其元數據來定位類型
-
然後CLR創建內部數據結構來表示該類型
-
方法首次調用時,JIT編譯完成對Main方法的編譯
-
-
-
整體解析流程:
-

托管vs非托管
-
在托管環境下,代碼的編譯分兩階段完成
-
編譯器遍歷源碼,生成IL
-
IL在運行的時候即時編譯成本機CPU指令
-
會比非托管花費較多的內存和CPU時間
- 非托管直接是針對一種具體的CPU平台編譯
-
-
托管代碼的優勢
-
JITCompiler能判斷運行機器的CPU並生成相應的本機代碼,使用提升性能的特殊指令;非托管應用程序通常是指對具有最小功能集合的CPU編譯,不會使用這些有用的特殊指令
-
JITCompiler能進行邏輯判斷,決定某些代碼是否需要生成出CPU指令。使得本機代碼可針對主機進行優化
-
應用程序運行時,CLR可評估代碼的執行,將IL重新編譯成本機CPU代碼。
-
在托管代碼中,由於IL的驗證過程,可確保代碼不會不正確地訪問內存,因此可以把多個托管應用程序放在一個Windows虛擬地址空間中運行
-
從而減少Windows開啟的進程數量,增強性能
-
每個托管EXE默認在自己的獨立地址空間中運行,這個地址空間只有一個AppDomain。但CLR的宿主進程可決定在一個進程中運行多個AppDomain
-
-
與非托管代碼的互操作性
-
CLR允許在應用程序中同時包含托管和非托管代碼,並支持3種互操作情形
-
托管代碼調用DLL中的非托管函數
- 使用P/Invoke(Platform Invoke)機制
-
托管代碼可使用現有COM組件(服務器)
-
參考:.NET Framework SDK提供的 Tlbimp.exe (類型程式庫匯入工具)
-
-
非托管代碼可使用托管類型(服務器)
-
參考:.NET Framework SDK提供的 TlbExp.exe(類型程序庫導出工具)
-
參考:.NET Framework SDK提供的 RegAsm.exe(組件登錄工具)
-
-
不安全的代碼
-
C#編譯器允許開發人員寫「不安全」(unsafe)的代碼
- 允許直接操作內存地址以及這些地址處的字節
-
風險大,C#編譯設有多重保護措施,都通過後才能編譯unsafe代碼
-
使用unsafe關鍵字標記不安全代碼
-
打開/unsafe編譯器開關
-
JIT編譯unsafe方法時:
-
檢查方法是否有System.Security.Permissions.SecurityPermission權限
-
System.Security.Permissions.SecurityPermissionFlag的SkipVerification標志是否設置
-
-
不通過時拋出異常:System.InvalidProgramException或System.Security.VerificationException
-
-
檢查程序集中的不安全代碼
-
從本地計算機或「網絡共享」加載的程序集默認被授與完全信任,包括執行不安全代碼
-
但通過Internet執行的程序集默認不會被授與執行不安全代碼的權限,如果含有不安全的代碼,則會拋出異常(System.InvalidProgramException或System.Security.VerificationException)
-
當然,管理員和最終用戶可以修改這些默認設置
-
-
PEVerify.exe可檢查一個程序集的所有方法是否包含不安全的方法
-
該驗證需要訪問所有依賴程序集中包含的元數據,因此使用CLR來定位這些程序集
-
因此會採用和平時執行程序集一樣的綁定(binding)和探測(probing)規則來定位程序
-
-
參考書目
- 《CLR via C#》(第4版) Jeffrey Richter