文/玄魂
前言
程序集是.NET應用程序的基本單位,包含了程序的資源、類型元數據和MSIL代碼。根據程序集生成方式的不同,可分為靜態程序集和動態程序集。程序集又可分為單文件程序集和多文件程序集,多文件程序集將程序集中的文件按類型組織到多個文件中。每個程序集,無論是靜態的還是動態的,均包含描述該程序集中各元素彼此如何關聯的數據集合,程序集清單就包含這些程序集元數據。程序集還可以進行版本控制和強名稱等安全設置。
反射提供了封裝程序集、模塊和類型的對象(Type 類型)。可以使用反射動態創建類型的實例,將類型綁定到現有對象,從現有對象獲取類型並調用其方法或訪問其字段和屬性。如果代碼中使用了屬性,可以利用反射對它們進行訪問。
2.1 程序集
CLR運行程序總是首先加載程序集,然后根據程序集中的清單去加載並初始化其他內容。程序集由一個或者若干個模塊組成,每個模塊就是一個PE文件。開發人員可以通過命令行調用編譯器,合並多個模塊到程序集。程序集共享和強名稱是代碼訪問安全里兩個重要的概念,通過程序集可以實施代碼級別的安全策略。
2.1.1 模塊的操作
程序集由模塊組成,每個程序集可以包含至少一個模塊。每一個模塊就是一個標准的PE文件(見第1章)。接下來,我們學習使用編譯器來進行模塊的生成、設置等基本操作。
首先創建一個hello.cs文件,如代碼清單2-1所示。
代碼清單2-1 hello.cs代碼
using System; public class Hello { public Hello() { } public static int void Main(string[] args) { Console.WriteLine("hello world!"); Console.Read(); } }
文件創建完畢之后,從命令行啟動C#的編譯器,輸入如下命令:
csc /out:hello.exe /t:exe /r:MSCorLib.dll hello.cs
如圖2-1所示,編譯器首先打印的是版本信息。
圖2-1 命令行調用C#編譯器
然后啟動生成的hello.exe,見圖2-2。可以看到程序輸出的“hello world”。
圖2-2 運行hello.exe
上面的代碼雖然很簡單,但仍然有必要做細致的分析。首先,創建一個名為hello的類型、一個hello()方法、一個Main(string[]args)靜態方法。然后添加類型引用,引用MSCorLib.dll中的Console.WriteLine(string)、Console.Read()方法。當編譯器把上面的C#代碼編譯成MSIL代碼時,遇到Console.WriteLine(string)和Console.Read()的時候,會在指定的程序集中(MSCorLib.dll)中尋找Console類型並判斷相應方法的調用是否正確。
說明 對於C#編譯器,默認會自動引用MSCorLib.dll,並設置/out:hello.exe /t:exe命令,所以該生成exe文件的命令可直接為:csc hello.cs。
這個hello.exe文件究竟是什么呢?首先肯定它是一個托管PE文件,是一個模塊,同時它還是一個程序集。第1章已經介紹過PE文件了,這里不再重復。通過csc的/t:exe、/t:winexe或者/t:library命令行開關得到文件,然后通過ILDasm查看都會發現程序集清單(也就是該模塊同時是一個程序集),那么可不可以生成不包含程序集清單的模塊呢?答案是csc的/t:module命令行開關,通過該命令將生成一個.netmodule文件。新建一個hh.cs文件,如代碼清單2-2所示。
代碼清單2-2 hh.cs源碼
public class Hh { public string Hello() { return "hello"; } }
對hh.cs使用如下命令:
csc /t:module hh.cs
該命令生成的hello.netmodule文件是一個標准的DLL PE文件,但是它不能被CLR加載,使用的時候必須將它嵌入程序集中。后文將介紹如何把模塊添加到程序集中。
2.1.2 程序集概念
簡單地說,程序集是.NET應用程序的基本單位,是CLR運行托管程序的最基本單位。它通常的表現形式是PE文件,區分PE文件是不是程序集或者說模塊和程序集的根本區別是程序集清單,一個PE文件如果包含了程序集清單那么它就是程序集。下面簡要說明程序集的基本功能。
注意 每個程序集只能包含一個入口點,控制台程序的Main方法,dll文件的DllMain方法或者Windows程序的WinMain方法。
一個非程序集的PE文件中的IL代碼是不能被執行的。
(1) 形成程序運行的安全邊界
程序集就是在其中請求和授予權限的單元。關於安全邊界的問題會在2.1.3節詳細論述。
(2) 形成程序的引用范圍邊界
程序集的清單包含用於解析類型和滿足資源請求的程序集元數據。它指定在該程序集之外公開的類型和資源。該清單還枚舉它所依賴的其他程序集。
(3) 形成程序的版本邊界
程序集是公共語言運行庫中最小的可版本化單元,同一程序集中的所有類型和資源均會被版本化為一個單元。
(4) 形成程序的部署單元
當一個應用程序啟動時,只有該應用程序最初調用的程序集必須存在,其他程序集(例如本地化資源和包含實用工具類的程序集)可以按需檢索。這就使應用程序在第一次下載時保持精簡。
(5) 支持並行執行
同一台計算機上可以同時有運行庫的多個版本,並且可以有使用其中某個運行庫版本的應用程序和組件的多個版本。並行執行使您能夠更多地控制應用程序綁定到的組件版本和應用程序使用的運行庫版本。
程序集從創建方式上可分為動態程序集和靜態程序集。動態程序集是程序執行時創建的程序集,創建后可以保存在磁盤上。靜態程序集包括類型、資源等,多被包含在托管PE文件中。
程序集又可分為單文件程序集和多文件程序集。一個程序集通常由程序集清單、類型元數據、MSIL代碼和資源組成。對於程序集來說,程序集清單是必需項。單文件程序集把上述所有內容包含在一個PE文件中(圖2-3),而對於多文件程序集則可能分割在不同的文件中(圖2-4),比如編譯代碼的模塊 (.netmodule)、資源(例如 .bmp 或 .jpg 文件)或應用程序所需的其他文件。如果您希望組合以不同語言編寫的模塊並優化應用程序的下載過程,可創建一個多文件程序集,優化下載過程的方法是將很少使用的類型放入只在需要時才下載的模塊中。
圖2-3 單文件程序集
圖2-4 多文件程序集
注意 構成多文件程序集的那些文件實際上並非由文件系統來鏈接,而是通過程序集清單進行鏈接,公共語言運行庫將這些文件作為一個單元來管理。
本節介紹的最后一個概念,也是程序集的標志性概念,即程序集清單。每一個程序集,無論是靜態的還是動態的,均包含描述該程序集中各元素彼此如何關聯的數據集合。程序集清單就包含這些程序集元數據。程序集清單包含指定該程序集的版本要求和安全標識所需的所有元數據,以及定義該程序集的范圍和解析對資源和類的引用所需的全部元數據。程序集清單可以存儲在具有 Microsoft 中間語言 (MSIL) 代碼的 PE 文件(.exe 或 .dll)中,也可存儲在只包含程序集清單信息的獨立 PE 文件中。
程序集清單的內容如表2-1所示。
表2-1 程序集清單內容
信息 |
說明 |
程序集名稱 |
指定程序集名稱的文本字符串 |
版本號 |
主版本號和次版本號,以及修訂號和內部版本號。公共語言運行庫使用這些編號來強制實施版本策略 |
區域性 |
有關該程序集支持的區域性或語言的信息。此信息只應用於將一個程序集指定為包含特定區域性或特定語言信息的附屬程序集(具有區域性信息的程序集被自動假定為附屬程序集) |
強名稱信息 |
如果已經為程序集提供了一個強名稱,則為來自發行者的公鑰 |
程序集中所有文件的列表 |
在程序集中包含的每一文件的散列及文件名。請注意,構成程序集的所有文件所在的目錄必須是包含該程序集清單的文件所在的目錄 |
類型引用信息 |
運行庫用來將類型引用映射到包含其聲明和實現的文件的信息。該信息用於從程序集導出的類型 |
有關被引用程序集的信息 |
該程序集靜態引用的其他程序集的列表。如果依賴的程序集具有強名稱,則每一引用均包括該依賴程序集的名稱、程序集元數據(版本、區域性、操作系統等)和公鑰 |
通過ILDasm的MANIFEST選項可以查看程序集清單,如圖2-5所示。查看Hello.exe的程序集清單,可以看到外部引用,程序集的版本號、名稱,散列等信息。
圖2-5 hello.exe的程序集清單
2.1.3 強名稱程序集
強名稱是由程序集的標識加上公鑰和數字簽名組成的。其中,程序集的標識包括簡單文本名稱、版本號和區域性信息(如果提供的話)。強名稱是使用相應的私鑰,通過程序集文件(包含程序集清單的文件,因而也包含構成該程序集的所有文件的名稱和散列)生成的。強名稱相同的程序集應該是相同的。
通過簽發具有強名稱的程序集,可以確保名稱的全局唯一性,保護程序集的版本,提供可靠的完整性檢查。
說明 如果一個具有強名稱的程序集以后引用了具有簡單名稱的程序集(后者沒有這些好處),則將失去使用具有強名稱的程序集所帶來的好處,並依舊會產生 DLL 沖突。因此,具有強名稱的程序集只能引用其他具有強名稱的程序集。
想要給一個程序集添加強名稱,可以使用程序集鏈接器(AL.exe)或者強名稱屬性。
在為程序集創建強名稱之前必須先創建一個公鑰/私鑰對。這一對加密公鑰/私鑰用於在編譯過程中創建強名稱程序集。您可以使用強名稱工具 (Sn.exe) 來創建密鑰對。密鑰對文件通常具有snk 擴展名。圖2-6為創建一個名為hello.snk的密鑰文件。
圖2-6 生成密鑰文件
我們還可以從已經創建的密鑰對文件中提取公鑰,放到單獨的文件中,如圖2-7所示:
圖2-7 提取公鑰
在密鑰文件創建完成之后,下一步就是利用密鑰創建強名稱程序集,這里我們使用程序集鏈接器(AL.exe)對前面生成的hh.netmodule模塊使用強名稱,如圖2-8所示:
圖2-8 為程序集添加強名稱
圖2-8中的開關/out:hh.dll hh.netmodule是把hh.netmodule模塊輸出為hh.dll文件,/keyf:hello.snk指定強名稱的密鑰文件。可以通過ILDasm查看hh.dll的強名稱簽名,如圖2-9所示。
圖2-9 hh.dll強名稱簽名的公鑰
一個程序集具有了強名稱之后,接下來的問題就是如何引用這個具有強名稱的程序集。通常有兩種方式可供選擇:編譯時引用和運行時引用。
用C#編譯器引用hello.dll的方式如下所示:
csc /t:library showhello.cs /reference:hello.dll
運行時引用實際是反射的方式加載程序集,反射將會在2.2節介紹。下面給出運行時引用的簡單示例:
Assembly.Load("hello,Version=4.0.0.0,Culture=neutral,PublicKeyToken= B77A5C561934E089");
關於強名稱程序集暫時介紹這些,后面的章節還有很多內容會提及強名稱程序集,下一節會介紹與此相關的共享程序集。
2.1.4 共享程序集
共享程序集的一個前提條件就是該程序集必須要有強名稱。那么什么是共享程序集呢?要了解共享程序集,先從私有程序集開始。
通常建立的exe或者dll程序集都是私有程序集,當在其他客戶應用程序中使用這類程序集時,只需要添加引用。當程序集被多個應用程序域使用時,每個應用程序域需要復制該程序集,進程中也將存在該程序集的多個副本。
相對於私有程序集的是共享程序集,它使多個應用程序域能夠訪問同一個程序集。特別地,內存中只存在該程序集的同一份副本,這種非特定於域的代碼共享極大地節省了內存 資源占用。在大多數情況下,共享程序集安裝在全局程序集高速緩沖存儲器(Global Assembly Cache,GAC)中,而不存在於應用程序相關目錄下,對它的引用不會產生文件復制,因此也不會產生額外的副本。下面了解共享程序集的創建、安裝及使用。
創建共享程序集的第一步是為該程序集添加強名稱(詳見2.1.3節),然后該做的就是在GAC中安裝共享程序集。
.NET提供的命令行工具gacutil.exe可以將具有強名稱的程序集添至全局程序集緩存。命令格式為:
gacutil -I <程序集名稱>
其中,"程序集名稱"是要在全局程序集緩存中安裝的程序集的名稱。
下面的示例語句將文件名為 hello.dll 的程序集安裝到全局程序集緩存:
gacutil -i hello.dll
在客戶應用程序中使用共享程序集的方法與私有程序集一樣簡單。創建客戶應用程序后,以與引用私有程序集相同的方式引用共享程序集,在應用程序代碼中包含共享程序集命名空間(using語句),然后,就可以像使用本地對象一樣使用共享程序集的公共對象了。
注意 安裝全局程序集破壞了.NET簡化應用程序安裝、部署、移動的策略,因為它本身是注冊式安裝。
2.1.5 創建多文件程序集
創建一個多模塊程序集可划分為兩種情況,一是由同一語言編譯器創建的不同模塊的合並,二是由不同語言編譯器創建的模塊的合並。下面分別討論這兩種情況。
首先創建一個Module1.cs文件,如代碼清單2-3所示。
代碼清單2-3 Module1.cs源碼
public class Module1 { public Module1() { } public int Add(int m,int n) { return m + n; } }
使用C#編譯器csc.exe編譯輸出文件Module1.netmodule。如圖2-10所示。
圖2-10 編譯生成Module1.netmodule
然后創建一個MainModule.cs文件,如代碼清單2-4所示。
代碼清單2-4 MainModule.cs源碼
public class MainModule { public int Mul(int m,int n) { return m *n; } }
接下來把這段代碼編譯成dll類型的程序集文件,把Module1.netmodule模塊添加到該程序集中。如圖2-11所示。
圖2-11 生成MainModule.dll
圖2-11中的命令行開關/addmodule:Module1.netmodule把模塊Module1.netmodule添加到程序集MainModule.dll。下面通過ILDasm來查看相關信息,如圖2-12所示。
圖2-12 關於Module1.netmodule的信息
如果某個程序想引用MainModule.dll文件,則必須保證Module1.netmodule文件的存在和訪問權限,否則編譯器會報錯,如圖2-13所示。
圖2-13 模塊丟失的錯誤信息
對於不同編譯器生成的模塊,而使用的編譯器又不支持類似C#編譯器中類似於/addmodule的開關,只能選用程序集鏈接器(AL.exe)來合並各模塊。下面創建含有3個托管模塊的程序集,如圖2-14所示。
圖2-14 包含3個托管模塊的程序集簡易示例
首先使用語言編譯器生成模塊1為M1.netmodule,模塊2為M2.netmodule,然后使用AL.exe 生成包含程序集清單的Main.dll文件,命令如下:
al /out:Main.dll /t:M1.netmodule M2.netmodule
該程序集包含了3個文件,AL.exe不能將多個文件合並成一個文件。
--------------------------------------------------注:本文改編自《.NET 安全揭秘》第2章