程序集是 .NET Framework 應用程序的構造塊;程序集構成了部署、版本控制、重復使用、激活范圍控制和安全權限的基本單元。最終由CLR管理這些程序集中代碼的執行。這意味着必須在目標機器上安裝好 .NET Framework 。
公共語言運行時(Common Language Runtime,CLR)是一個可由多種編程語言使用的“運行時”,CLR的核心功能(比如內存管理、程序集加載、安全性、異常處理和線程同步)可由面向CLR的所有語言使用。
高級語言通常只公開了CLR的所有功能的一個子集。然而,IL匯編器語言允許開發人員訪問CLR的所有功能。CLR允許在不同編程語言之間方便切換,同時又保持緊密集成,所以當你使用的高級語言隱藏了你需要的CLR功能,那么可以使用“混合語言編程”或使用IL匯編語言實現。
1. 程序集執行以下功能:
a) 包含公共語言運行時執行的代碼。如果可遷移可執行 (PE) 文件沒有相關聯的程序集清單,則將不執行該文件中的 Microsoft 中間語言 (MSIL) 代碼。
b) 程序集形成安全邊界。程序集就是在其中請求和授予權限的單元。
c) 程序集形成類型邊界。每一類型的標識均包括該類型所駐留的程序集的名稱。在一個程序集范圍內加載的 MyType 類型不同於在其他程序集范圍內加載的 MyType 類型。
d) 程序集形成引用范圍邊界。程序集的清單包含用於解析類型和滿足資源請求的程序集元數據。它指定在該程序集之外公開的類型和資源。該清單還枚舉它所依賴的其他程序集。
e) 程序集形成版本邊界。程序集是公共語言運行時中最小的可版本化單元,同一程序集中的所有類型和資源均會被版本化為一個單元。程序集的清單描述您為任何依賴項程序集所指定的版本依賴性。
f) 程序集形成部署單元。當一個應用程序啟動時,只有該應用程序最初調用的程序集必須存在。其他程序集(例如本地化資源和包含實用工具類的程序集)可以按需檢索。這就使應用程序在第一次下載時保持精簡。
g) 程序集提供允許同時運行多個版本的軟件組件(稱作並行執行)的基本結構。
h) 程序集可以是靜態的或動態的。靜態程序集可以包括 .NET Framework 類型(接口和類),以及該程序集的資源(位圖、JPEG 文件、資源文件等)。靜態程序集存儲在磁盤上的可遷移可執行 (PE) 文件中。您還可以使用 .NET Framework 來創建動態程序集,動態程序集直接從內存運行並且在執行前不存儲到磁盤上。您可以在執行動態程序集后將它們保存在磁盤上。
2. 程序集的優點
程序集旨在簡化應用程序部署並解決在基於組件的應用程序中可能出現的版本控制問題。
最終用戶和開發人員比較熟悉當今基於組件的系統所產生的版本控制和部署問題。一些最終用戶曾經歷過在計算機上安裝新應用程序失敗的事情,發現已有應用程序突然停止工作。許多開發人員花費了大量的時間來使所有必需的注冊表項保持一致,以便激活 COM 類。
通過在 .NET Framework 中使用程序集,許多開發問題得以解決。因為程序集是不依賴於注冊表項的自述組件,所以程序集使無相互影響的應用程序安裝成為可能。程序集還使應用程序的卸載和復制得以簡化。
3. 強名稱的程序集
強名稱是由程序集的標識加上公鑰和數字簽名組成的。其中,程序集的標識包括簡單文本名稱、版本號和區域性信息(如果提供的話)。
強名稱是使用相應的私鑰,通過程序集文件(包含程序集清單的文件,並因而也包含構成該程序集的所有文件的名稱和散列)生成的。
1) 強名稱程序集顯示名稱的語法:
<程序集名稱>, <版本號>, <區域性>, <公鑰標記>
( 如果沒有區域性值,請使用 Culture=neutral )
2) 程序集版本號:
<主版本>.<次版本>.<內部版本號>.<修訂號>
eg:2.5.719.2
a) 前兩個編號:構成了公眾對一個版本的理解。如上例為程序集的2.5版本
b) 第三個編號:是程序集的build號。如果公司每天都要生成程序集,那么每天都應該遞增這個build號
c) 最后一個編號:指出當前build的修訂次數。如因為重大bug當天生成了兩次程序集,那么revision號就應該遞增。
3) 強名稱提供如下好處:
a) 強名稱依賴於唯一的密鑰對來確保名稱的唯一性。任何人都不會生成與您生成的相同的程序集名稱,因為用一個私鑰生成的程序集的名稱與用其他私鑰生成的程序集的名稱不相同。
b) 強名稱保護程序集的版本沿襲。強名稱可以確保沒有人能夠生成您的程序集的后續版本。用戶可以確信,他們所加載的程序集的版本出自創建該版本(應用程序是用該版本生成的)的同一個發行者。
c) 強名稱提供可靠的完整性檢查。通過 .NET Framework 安全檢查后,即可確信程序集的內容在生成后未被更改過。注意:強名稱中或強名稱本身並不暗含信任級別,例如由數字簽名和支持證書提供的信任。
d) 在.NET中,只有強名稱簽名的程序集才能放到全局程序集緩存中。
在引用具有強名稱的程序集時,您應該能夠從中受益,例如版本控制和命名保護。如果此具有強名稱的程序集以后引用了具有簡單名稱的程序集(后者沒有這些好處),則您將失去使用具有強名稱的程序集所帶來的好處,並依舊會產生 DLL 沖突。因此,具有強名稱的程序集“應當只”引用其他具有強名稱的程序集。
更多關於強名稱簽名請參見 《(2)強名稱程序集與數字證書》
4. 延遲為程序集簽名(部分簽名)
一個單位可以具有開發人員在日常使用中無法訪問的嚴密保護的密鑰對。公鑰通常是可用的,但對私鑰的訪問權僅限於少數個人。開發強名稱程序集時,每個引用具有強名稱的目標程序集的程序集中都包含了用於為目標程序集指定強名稱的公鑰的標記。這要求公鑰在開發過程中可用。
您可以在生成時使用延遲簽名(部分簽名),在可遷移可執行 (PE) 文件中為強名稱簽名保留空間,將實際簽名延遲至后面某些階段(通常就在傳送程序集之前)。
下面的步驟說明了延時對程序集簽名的過程:
使用 Windows 軟件開發包 (SDK) 提供的 強名稱工具 (Sn.exe) 從將執行最終簽名的單位獲取密鑰對的公鑰部分。此密鑰通常是 .snk 文件的形式。
使用 System.Reflection 中的兩種自定義特性來批注程序集的源代碼:
1) AssemblyKeyFileAttribute,它將包含公鑰的文件的名稱作為參數傳遞給其構造函數。
2) AssemblyDelaySignAttribute,它通過將 true 作為參數傳遞給其構造函數,表明正在使用延遲簽名。例如:
a) [assembly:AssemblyKeyFileAttribute("myKey.snk")]
b) [assembly:AssemblyDelaySignAttribute(true)]
3) 編譯器將公鑰插入程序集清單,並在 PE 文件中為完整的強名稱簽名保留空間。真正的公鑰必須在生成程序集時存儲,以便引用此程序集的其他程序集可獲取密鑰以存儲在它們自已的程序集引用中。
4) 由於程序集沒有有效的強名稱簽名,所以必須關閉該簽名的驗證。通過將“強名稱”工具與–Vr 選項一起使用來執行此操作。
下面的示例關閉名為 myAssembly.dll 的程序集的驗證。
sn –Vr myAssembly.dll
-Vr選項會將程序集的身份添加到以下注冊表:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\StrongName\Verification
為了確保這些密鑰的安全性,密鑰值絕對不能持久存儲在一個磁盤文件中。“加密服務提供程序”(Cryptographic Service Provder,CSP)提供了對這些密鑰的位置進行抽象的容器。以Microsoft使用的CSP為例,一旦訪問它提供的容器,就會自動從一個硬件設備中獲取私鑰。
5) 以后,通常是在即將交付前,重新開啟強名稱簽名驗證。通過將“強名稱”工具與–R 選項來實際進行強名稱簽名 (這步不能省,否則會產生安全漏洞) 。
下面的示例使用 sgKey.snk 密鑰對為名為 myAssembly.dll 的程序集簽署強名稱。
sn -R myAssembly.dll sgKey.snk
5. 附屬程序集
標記了具體的語言文化的程序集稱為附屬程序集。附屬程序集通常只包含語言文化特有的資源,不包含任何代碼,為附屬程序集指定的語言文化應准確反映程序集中包含的資源的語言文化。
部署一個附屬程序集時,應該把它存到一個專門的子目錄中,子目錄的名稱應該與語言文化的文本相匹配。
在運行時,可以用System.Resources.ResourceManager類來訪問一個附屬程序集的資源。
通常使用AL.exe工具來生成附屬程序集。之所以不用編譯器,是因為附屬程序集中不應包含任何代碼。
一般不應生成引用了附屬程序集的一個程序集。換言之,程序集的AssemblyRef記錄項只應應用語言文化中性的程序集。想訪問包含在一個附屬程序集中的類型或成員,應使用“反射技術”。
6. 全局程序集緩存
安裝有公共語言運行時的每台計算機都具有稱為全局程序集緩存的計算機范圍內的代碼緩存。全局程序集緩存中存儲了專門指定給由計算機中若干應用程序共享的程序集。
1) 一個程序集可以采用兩種方式來部署:私有和全局。
a) “私有部署的程序集”是指部署到應用程序基目錄或者一個子目錄中的程序集。弱命名程序集只能以私有方式部署。
b) “全局部署的程序集”是指部署到一些已知位置,即全局程序集緩存(Global Assembly Cache,GAC);CLR在查找程序集時,會檢查這些位置。強名稱程序集既可以私有部署,也可以全局部署。
應當僅在需要時才將程序集安裝到全局程序集緩存中以進行共享。一般原則是:程序集依賴項保持專用,並在應用程序目錄中定位程序集,除非明確要求共享程序集。另外,不必為了使 COM 互操作或非托管代碼可以訪問程序集而將程序集安裝到全局程序集緩存。
因為將程序集安裝到GAC中,會破壞我們的一些基本目標,即:簡單地安裝、備份、還原、移動和卸載應用程序。所以,建議程序員盡量避免全局部署,盡量使用私有部署。
2) 要將程序集安裝到全局程序集緩存中的原因有以下幾點:
a) 共享位置。
可將應用程序公共使用的程序集放在全局程序集緩存中。
b) 文件安全性。
管理員通常使用訪問控制列表 (ACL) 來保護 systemroot 目錄,以控制寫入和執行訪問。因為全局程序集緩存安裝在 systemroot 目錄中,它繼承了該目錄的 ACL。建議只允許具有“管理員”權限的用戶從全局程序集緩存中刪除文件。
c) 並行版本控制。
可在全局程序集緩存中維護程序集的多個副本(名稱相同但版本信息不同)。
d) 其他搜索位置。
在探測或使用配置文件中的基本代碼信息之前,公共語言運行時會先檢查全局程序集緩存中符合程序集請求的程序集。
3) 可采用三種方法將程序集安裝到全局程序集緩存中:
a) 使用全局程序集緩存工具 (Gacutil.exe)。
您可以使用 Gacutil.exe 將強名稱程序集添加到全局程序集緩存,並查看全局程序集緩存的內容。
在命令提示符處,鍵入下列命令將強名稱程序集安裝到全局程序集緩存中:
gacutil –I <assembly name>
注意:Gacutil.exe 只用於開發,不應用於將產品程序集安裝到全局程序集緩存中。
b) 使用 Microsoft Windows Installer 。
這是將程序集添加到全局程序集緩存的最常用方法,建議采用。此安裝程序可提供全局程序集緩存中程序集的引用計數,還具有其他優點。
c) 使用 Mscorcfg.msc(.NET Framework 配置工具) (.NET 4.0 已刪)
Microsoft 管理控制台 (MMC) 單元,可以查看全局程序集緩存並將新的程序集添加到該緩存。
在全局程序集緩存中部署的程序集必須具有強名稱。將一個程序集添加到全局程序集緩存時,必須對構成該程序集的所有文件執行完整性檢查。緩存執行這些完整性檢查以確保程序集未被篡改。
4) 使自己的程序集出現在“.NET”選項卡的列表中
在vs中引用GAC程序集時,通過在 “.NET” 選項卡列表中選擇;對於自己的程序集目錄若需要他出現在“.NET”選項卡列表中,可在注冊表中配置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders
(或HKEY_LOCAL_MACHINE)
7. 程序集啟動並運行
托管模塊是一個標准的32位Microsoft Windows可移植執行體(PE32)文件(Portable Executable),或者是一個標准的64位Windows可移植執行體(PE32+)文件,它們都需要CLR才能執行。
1) 一個托管PE文件由4個部分構成:PE32(+)頭、CLR頭、元數據以及IL。
元數據是一個二進制數據塊,由幾個表構成。分為三個類別:定義表、引用表和清單表。
通過工具:IL反匯編器可查看元素據。“視圖|元數據|顯示”(或Ctrl+M)
a) 定義表:ModuleDef、TypeDef、MethodDef、FieldDef、ParamDef、PropertyDef、EventDef。
b) 引用表:AssemblyRef、ModuleRef、TypeRef、MemberRef。
c) 清單也是一組元素據表的集合(AssemblyDef、FileDef、ManifestResourceDef、ExportedTypesDef),表中主要包含了作為程序集的組成部分的那些文件的名稱,此外,它們還描述了程序集的版本、語言文化、發布者、公開導出的類型以及構成程序集的所有文件。
CLR總是首先加載包含“清單”元素據表的文件,再根據這個“清單”來獲取程序集中的其他文件的名稱。
2) CLR托管代碼相對於非托管代碼的一個優勢:
Windows進程需要使用大量操作系統資源,所以進程數量太多,會損害性能並制約可用的資源。
CLR提供了在一個操作系統進程中執行多個托管應用程序的能力。每個托管的應用程序都在一個AppDomain中執行。默認情況下,每個托管的EXE文件都在它自己的獨立空間中運行,這個地址空間只有一個AppDomain。然而,CLR的宿主進程(比如IIS或者Microsoft SQL Server)可決定在單個操作系統進程中運行多個AppDomain。
3) /platform選項:Any CPU、x86、x64、intel Itanium
-------/platform開關選項對生成的模塊的影響以及在運行時的影響
/platform開關 |
生成托管托管模塊 |
X86 Windows |
X64 Windows |
IA64 Windows |
anycpu(默認) |
PE32/不明確指定 |
作為32位應用程序運行 |
作為64位應用程序運行 |
作為64位應用程序運行 |
x86 |
PE32/x86 |
作為32位應用程序運行 |
作為WoW64位應用程序運行 |
作為WoW64位應用程序運行 |
x64 |
PE32+/x64 |
不運行 |
作為64位應用程序運行 |
不運行 |
Itanium (Intel安騰處理器) |
PE32+/Itanium |
不運行 |
不運行 |
作為64位應用程序運行 |
Windows的【64位版本】提供一種名為WoW64(Windows on Windowss64)的技術,允許運行32位Windows應用程序。該技術甚至允許使用x86本機代碼的32位應用程序在Itanium機器上運行。這是因為WoW64技術能模擬x86指令集,雖然這樣做會顯著影響性能。
Windows檢查好EXE文件頭,決定是創建32位、64位還是WoW64進程之后,會在進程的地址空間中加載MSCorEE.dll的x86,x64或IA64版本。然后,進程的主線程調用MSCorEE.dll中定義的一個方法。這個方法初始化CLR,加載EXE程序集,然后調用其入口方法(Main)。
更多C#編譯器選項請參加: 《csc.exe(C# 編譯器)》
4) 方法調用過程:
為了執行一個方法,首先必須把它的IL轉換成本地CPU指令,由CLR中的JITCompiler函數負責。由於IL是“即時“(just in time)編譯的,所以通常CLR的這個組件稱為JITter或JIT編譯器。
JIT會在定義程序集的MethodDef元數據表中查找被調用的方法的IL。接着,JIT驗證IL代碼,並將IL代碼編譯成本地CPU指令並【存儲到動態內存】中,再次調用的方法以本地代碼的形式全速運行。
一旦應用程序終止,編譯好的代碼也會被丟棄。所以將來再次運行應用程序,或者同時啟動應用程序的兩個實例(實例是使用不同的操作系統進程—eg:x86和x64),JIT編譯器必須再次將IL編譯為本地指令。
《反射機制》系列:
參考書籍: CLR via C#(第3版)
參考資源: