重溫CLR(二)生成、部署以及程式集
將型別生成到模組中
class Program { static void Main(string[] args) { Console.WriteLine("Hi"); } }
該應用程式定義了program型別,其中有名為Main的public static方法。Main中引用了另一個型別System.Console。System.console是Microsoft實現好的型別,用於實現這個型別的各個方法的IL程式碼儲存在MSCorLib.dll檔案中。總之,應用程式定義了一個型別,還使用了其他公司提供的型別。
為了生存這個示例應用程式,請將上述程式碼放到一個原始碼檔案中(假定為Program.cs),然後再命令列執行以下命令
csc.exe /out:program.exe /t:exe /r:MSCorLib.dll Program.cs
這個命令列指示C#編譯器生成名為Program.exe的可執行檔案(/out : Program.exe)。生成的檔案是Win32控制檯應用程式型別(/t[arget]:exe)。
C#編譯器處理原始檔時,發現程式碼引用了System.Console型別的WriteLine方法。此時,編譯器要核實該型別確實存在,它確實有WriteLine方法,而且傳遞的實參與方法形參匹配。由於該型別在c#原始碼中沒有定義,所以要順利通過編譯,必須向c#編譯器提供一組程式集,使它能解析對外部型別的引用。MSCorLib.dll是特殊檔案,它包含所有核心型別,包括Byte,Char,String,Int32等等。事實上,由於這些型別使用得如此頻繁,以至於C#編譯器會自動引用MSCorLib.dll程式集。
此外,/out:program.exe /t:exe開關是C#編譯器的預設設定,所以能簡化命令為
csc.exe program.exe
C#編譯器生成的Program.exe檔案,它是標準PE(可移植執行體,Portable Executable)檔案
元資料概述
Program.exe檔案中到底有什麼?託管PE檔案由4部分構成:PE32(+)頭、CLR頭、元資料以及IL。PE32(+)頭是windows要求的標準資訊。
CLR頭
元資料是由幾個表構成的二進位制資料塊。有三中表,分別是定義表(definition table)、引用表(reference table)和清單表(manifest table)。下表總結了模組元資料塊中常用的定義表。
編譯器編譯原始碼時,程式碼定義的任何東西都導致在上表列出的某個表中建立一個記錄項。此外,編譯器還會檢測原始碼引用的型別、欄位、方法、屬性和事件,並建立相應的元資料表記錄項。在建立的元資料中包含一組引用表,他們記錄了所引用的內容。表2-2總結了常用的引用元資料表。
可以用反編譯工具檢視託管pe檔案中的元資料。
Program.exe包含名為Program的TypeDef。program是公共密封類,從System.Object派生。program型別還定義了兩個方法:main和.ctor(構造器)。
Main是公共靜態方法,用IL程式碼實現
將模組合併成程式集
上一節討論的Program.exe並非只是含有元資料的PE檔案,他還是程式集(assembly)。程式集是一個或多個型別定義檔案及資原始檔的集合。在程式及的所有檔案中,有一個檔案容納了清單(manifest)。清單也是一個元資料表集合,表中主要包含作為程式集組成部分的那些檔案的名稱。此外,還描述了程式集的版本、語言文化、釋出者、公共匯出的型別以及構成程式集的所有檔案。
CLR操作的是程式集。換而言之,CLR總是首先載入包含清單元資料表的檔案,再根據清單來獲取程式集中的其他檔案的名稱。下面列出了程式集的重要特點。
1 程式集定義了可重用的型別
2 程式集用一個版本號標記
3 程式集可以管理安全資訊
除了包含清單元資料表的檔案,程式集其他單獨的檔案並不具備上述特點。
型別為了順利地進行打包、版本控制、安全保護以及使用,必須放在座位程式集一部分的模組中。程式集大多數時候只有一個檔案,就像前面的Program.exe那樣。然後,程式集還可以由多個檔案構成:一些事含有元資料的pe檔案,另一些是.gif或.jpg這樣的資原始檔。為便於理解,可將程式集視為一個邏輯EXE或DLL。
Microsoft為什麼引入程式集的概念?這是因為使用程式集,可重用型別的邏輯表示與物理表示就可以分開。例如,程式集可能包含多個型別。可以將常用型別放到一個檔案中,不常用型別放到另一個檔案中。如果程式集要從internet下載並部署,那麼對於含有不常用型別的檔案,加入客戶端不實用那些型別,該檔案就永遠不會下載到客戶端。
使用多檔案程式集的額外三個理由
1 不同的型別用不同的檔案,使檔案能以“增量”方式下載(就像前面在internet下載的例子描述的那樣)。另外,將型別劃分到不同的檔案中,可以對購買和安裝的應用程式進行部分或分批打包/部署。
2 可在程式集中新增資源或資料檔案。例如,假定一個型別的作用是計算保險資訊,需要訪問精算表才能完成計算。在這種情況下,不必在自己的原始碼中籤入精算表。相反,可以使用有一個工具,使資料檔案稱為程式集的一部分。資料檔案可為任意格式——只要應用程式知道如何解析。
3 程式集包含的各個型別可以用不同的編碼程式語言來實現。然後 可以用工具將所有模組合併成單個程式集。
總之,程式集是進行重用、版本控制和應用安全性設定的基本單元。它允許將型別和資原始檔劃分到單獨的檔案中。這樣一來,無論你自己還是使用者,都可以決定打包和部署那些檔案,一旦CLR載入還有清單的檔案,就可確定在程式集的其他檔案中。
共享程式集和強命名程式集
CLR支援兩種程式集:弱命名程式集(weakly named assembly)和強命名程式集(strongly named assembly)。兩種程式集結構完全相同。也就是說他們都使用前面討論的PE檔案格式頭、PE32(+)頭、CLR頭、元資料、清單表以及IL。生成工具也相同,都是C#編譯器或者AL.EXE。兩者的區別在於,強命名程式集使用釋出者的公鑰/私鑰進行了簽名。這一對金鑰允許對程式集進行唯一性的標識、保護和版本控制,並允許程式集部署到使用者機器的任何地方,甚至可以部署到internet上。
程式集可採用兩種方式部署:私有或全域性。私有部署的程式集是指部署到應用程式基目錄或者某個子目錄的程式集。弱命名程式集只能以私有方式部署。全域性部署的程式集是指部署到一些公認公認位置的程式集。CLR在查詢程式集時,會檢查這些位置。強命名程式集既可私有部署,也可全域性部署。
全域性程式集快取
由多個應用程式訪問的程式集必須放到公認的目錄,而且CLR在檢測到對該程式集的引用時,必須知道檢查該目錄。這個公認位置就是全域性程式集快取(Global Assembly Cache,GAC)。GAC的具體位置是一種實現細節,不同版本會有所變化,一般在
%systemroot%\microsoft.net\assembly
GAC目錄是結構化的:其中包含許多子目錄,子目錄名稱用演算法生成。永遠不要將程式集檔案手動複製到GAC目錄;相反,要用鞏固完成這項任務。工具知道GAC內部結構,並指導如何生成正確的子目錄名。
強命名程式集能放篡改
用私鑰對程式集進行簽名,並將公鑰和簽名簽入程式集,CLR就可驗證程式集未被修改或破壞。程式集安裝到GAC時,系統對包含清單的那個檔案的內容進行雜湊處理,將雜湊值與PE檔案中籤入的RSA簽名進行比較。此外,系統還對程式集的其他檔案的內容進行雜湊處理,並將雜湊值與清單檔案的FileDef表中儲存的雜湊值進行比較。任何一個雜湊值不匹配,表明程式集至少有一個檔案唄篡改,程式集將無法安裝到GAC。
應用程式需要繫結到程式集時,clr根據被引用程式集的屬性(名稱、版本、公鑰等)在Gac中定位該程式集。找到被引用程式集,就返回包含他的子目錄,並加裝清單所在的檔案。如果被引用程式集不在GAC中,CLR會查詢應用程式的基目錄,然後查詢應用程式配置檔案中標註的任何私有路徑。然後,如果應用程式有MSI安裝,CLR要求MSI定位程式集,如果任何位置都找不到程式集,那麼繫結失敗,丟擲FileNotFoundException。
如果強命名程式集檔案從GAC之外的位置載入,CLR會在程式集載入後比較雜湊值。也就是說,每次應用程式執行並載入程式集時,都會對檔案進行雜湊處理,以犧牲效能為代價,保證程式集檔案內容沒有被篡改。
“執行時”如何解析型別引用
class Program { static void Main(string[] args) { Console.WriteLine("Hi"); } }
編譯以上程式碼並生成程式集(假定名為Program.exe)。執行應用程式,CLR會加重並初始化自身,讀取程式集CLR頭,查詢表示了應用程式入口方法(main)的MethodDefToken,檢索MethodDefToken元資料表找到方法的IL程式碼再檔案中的偏移量,將IL程式碼JIT編譯成本機程式碼,最後執行本機程式碼。
對這些程式碼進行JIT編譯,CLR會檢測所有型別和成員的引用,載入他們的定義程式集(如果尚未載入)。具體地說,IL call指令應用了元資料toekn 0A000003。該token表示memberRef元資料表中的記錄項3。CLR檢查該memberRef記錄項,發現它的欄位引用了TypeRef表中的記錄項(system.Console型別)。按照
TypeRef記錄項,CLR被引導至一個AssemblyRef記錄項
這時CLR知道了它需要的是哪個程式集。接著,CLR必須定位並載入該程式集。
高階管理控制
以下是一個示例XML配置檔案
<?xml version="1.0" encoding="utf-8"?> <configuration> <appSettings> <add key="webpages:Version" value="3.0.0.0"/> <add key="webpages:Enabled" value="false"/> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5"/> <httpRuntime targetFramework="4.5"/> </system.web> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35"/> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35"/> <bindingRedirect oldVersion="1.0.0.0-5.2.4.0" newVersion="5.2.4.0"/> </dependentAssembly> </assemblyBinding> </runtime> <system.codedom> <compilers> <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.8.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701"/> </compilers> </system.codedom> </configuration>
這個XML檔案為CLR提供了豐富的資訊,具體如下所示。
probing元素
查詢弱命名程式集時,檢查應用程式基目錄下的AuxFiles和bin\subdir子目錄。對於強命名程式集,clr檢查GAC或者由codeBase元素指定的url。只有在未指定codeBase元素時,CLR才會在用用程式的私有路徑中檢查強命名程式集。
第一個dependentAssembly,assemblyIdentity和bindingRedirect元素
查詢由控制著公鑰標記31bf3856ad364e35的組織釋出的、語言文化為中性的SomeClassLibrary程式集的1.0.0.0版本時,改為定位同一個程式集的3.0.0.0版本。
codeBase元素
查詢由控制著公鑰標記31bf3856ad364e35的組織釋出的、語言文化為中性的SomeClassLibrary程式集的2.0.0.0版本時,嘗試在標記URL處發現他
編譯方法時,CLR判斷它引用了哪些型別和成員。根據這些資訊,“執行時”檢查進行引用的程式集的AssemblyRef表,判斷程式集生成時引用了哪些程式集。然後,clr在引用程式配置檔案中檢查程式集/版本,進行制定的版本號重定向操作。隨後,clr查詢新的、重定向的程式集/版本。最後,clr在機器的machine.config檔案中檢查新的程式集、版本並進行制定的版本號重定向操作。
利用這些配置檔案,管理員可以實際地控制CLR載入的程式集。