一、CLR的執行模塊
1.1 將源代碼編譯成託管模塊
非託管C/C++可對系統進行低級控制,按自己的想法管理內存、VB可以快速生成UI應用程序,並控制COM對象和數據庫。
公共語言運行時(Common Language Runtime, CLR)是一個由多種編程語言使用的“運行時”。CLR的核心功能(如內存管理、程序集加載、安全性、異常處理和線程同步)可由面向CLR的所有語言使用。
事實上在運行時,CLR根本不關心開發人員用哪一種語言寫源代碼,這意味着在選擇編程語言時,應選擇最容易表達自己意圖的語言。
面向CLR的語言編譯器:C++/CLI、C#、VB、F#、Iron Python、Iron Ruby以及一個“中間語言”(Intermediate Language, IL)彙編器。除了Microsoft,另一些公司、大學和學院也創建了自己的編譯器,也能向CLR生成代碼。例如:Ada, APL, Caml, COBOL, Eiffel, Forth, Fortran, Haskell, Lexico, Lisp, LOGO, Lua, Mercury, ML, Mondrian, Oberon, Pascal, Perl, PHP, Prolog, RPG, Scheme, Smalltalk, Tcl/Tk
無論選擇哪個編譯器,結果都是託管模塊(Managed Module)。它們都需要CLR才能執行。
表1 託管模塊各個部分
組成部分 | 說明 |
---|---|
PE32或PE32+頭 | 標識了文件類型,包括GUI、CUI或者DLL;包含時間標記,如果包含本機CPU代碼的模塊,會包含本機CPU代碼有關的信息 |
CLR頭 | 頭中包含要求的CLR版本,一些標誌(Flag),託管模塊入口方法的MethodDef元數據Token以及模塊的元數據、資源、強名稱、一些標識以及其他不太重要的數據項的位置及大小 |
元數據 | 主要有兩種表:描述源代碼中定義的類型和成員,描述源代碼引用的類型和成員 |
IL代碼 | 編譯器編譯源代碼時生成的代碼,在運行時,CLR將IL編譯成本機的CPU指令 |
IL代碼有時被稱爲“託管代碼(Maneged Code)”,因爲CLR管理它的執行。
1.1-1 元數據:
面向CLR的編譯器要在每個託管模塊中生成完整的元數據(metadata)。一些數據表描述了模塊中定義了什麼(比如類型以及其成員),另一些描述了模塊引用了什麼(比如導入的類型及其成員)元數據是一些老技術的超集,這些老技術包括COM的“類型庫”(Type Library)和“接口定義語言”(Interface Definition Language, IDL)文件,但CLR元數據遠比他們全面。
元數據有多種用途,下面僅舉例一部分
- 避免了編譯時對原生C/C++頭和庫文件的需求,編譯器直接從託管模塊讀取元數據。
- “智能感知”(IntelliSense)技術會解析元數據,告訴你一個類型提供了哪些方法、屬性、事件和字段、乃至方法需要的參數。
- CLR的代碼驗證過程使用元數據確保代碼只執行“類型安全”的操作。
- 元數據允許將對象的字段序列化到內存塊,將其發送給另一臺機器,然後反序列化,在遠程機器上重建對象狀態。
- 元數據允許垃圾回收器跟蹤對象生存期。垃圾回收器能夠判斷任何對象的類型,並從源數據知道那個對象中的那些字段引用了其他對象。
爲了執行包含託管代碼以及/或者託管數據的模塊,最終用戶必須在自己的計算機上安裝好CLR(目前作爲.NET Framework的一部分提供)。這類似於爲了運行MFC或者VB6.0應用程序,用戶必須安裝MFC庫或者VB DLL。
1.1-2 C++/CLI:
Microsoft C++編譯器默認生成包含非託管代碼的EXE/DLL模塊,並在運行時操縱非託管數據(native內存)。這些模塊不需要CLR即可執行。通過指定/CLR命令行開關,C++編譯器就能生成包含託管代碼的模塊。在前面提到的所有Microsoft編譯器中,C++編譯器是獨一無二的,只有它才允許開發人員同時寫非託管代碼和託管代碼,並生成到同一個模塊中。他也是唯一允許開發人員在源代碼中同時定義託管和非託管數據類型的Microsoft編譯器。
1.2 將託管模塊合併成程序集
CLR實際不和模塊工作,他和程序集工作。程序集(Assembly)是一個抽象概念。首先程序集是一個或多個模塊/資源文件的邏輯性分組。其次,程序集是重用、安全性以及版本控制的最小單元。取決於你所選擇的編譯器或工具,即可生成單文件程序集也可生成多文件程序集。在CLR的世界中,程序集相當於“組件”。
圖1有助於理解程序集,圖中一些託管模塊和資源(或數據)文件準備交由一個工具處理。工具生成代表文件邏輯分組的一個PE32(+)文件。這個PE32(+)文件包含一個名爲清單(manifest)的數據模塊。清單也是元數據表的集合。這些表描述了構成程序集的文件、程序集中的文件所實現的公開導出的類型
①譯者注:所謂公開導出的類型,就是程序集中定義的Public類型,它們在程序集內部外部均可見
C#編譯器生成的是含有清單的託管模塊。對於只有一個託管模塊而沒有資源或數據文件的項目,程序集就是託管模塊,生成過程中無需執行額外步驟,但是如果希望將一組文件合併到程序集中,就必須掌握更多的工具(比如程序集鏈接器AL.exe)以及其他命令行選項。
在程序集的模塊中,還包含與引用的程序集有關的信息(包括他們的版本號)。這些信息使程序既能夠自描述(self-describing)。CLR能判斷爲了執行程序集中的代碼,程序集的直接依賴對象(Immediate dependency)是什麼。相比COM而言不需要註冊表或Active Directory Domain Services(ADDS)
1.3 加載公共語言運行時
C#編譯器提供了一個/platform命令行開關選項,這個開關允許指定最終生成的程序集只能運行在32位Windows版本的x86的機器上使用。或者在32位windows RT的ARM機器上使用,不指定具體平臺的話,默認選項就是anycpu。VS用戶想要設置目標平臺,可以打開項目的屬性頁,從“生成”選項卡的“目標平臺”列表選擇一個選項,如圖2
取決於/platform開關選項,C#編譯器生成的程序集包含的要麼是PE32頭,要麼是PE32+頭。PE32文件在32位或64位地址空間中均可運行,PE32+文件則需要64位地址空間。Windows的64位版本通過WoW64(Windows on Windows64)技術運行32位windows應用程序。
表2 /plantform開關選項對生成的模塊的影響以及在運行時的影響
/plantform開關 | 生成的託管模塊 | X86 Windows | X64 Windows | ARM Windows RT |
---|---|---|---|---|
anycpu(默認) | PE32/任意cpu架構 | 作爲32位應用程序運行 | 作爲64位應用程序運行 | 作爲32位應用程序運行 |
Anycpu 32bit preferred | PE32/任意cpu架構 | 作爲32位應用程序運行 | 作爲WoW64位應用程序運行 | 作爲32位應用程序運行 |
X86 | PE32/x86 | 作爲32位應用程序運行 | 作爲WoW64應用程序運行 | 不運行 |
X64 | PE32+/x64 | 不運行 | 作爲64位應用程序運行 | 不運行 |
ARM | PE32/ARM | 不運行 | 不運行 | 作爲32位應用程序運行 |
Windows檢查EXE文件頭,決定進程爲64或32位後,加載相應的MSCroEE.dll文件,然後進程的主線程調用該dll中的一個方法。這個方法初始化CLR,加載EXE程序集,在調用其入口方法(Main)。
如果非託管程序調用LoadLibrary加載託管程序集,Windows會自動加載並初始化CLR,以處理程序集中的代碼,但由於此時進程已經啓動並運行,則可能影響程序集可用性,如:64位進程無法加載使用/plantform:x86開關編譯的託管程序集,但該託管程序集確實可以在64位內存空間中以WoW64方式運行。
1.4 執行程序集的代碼
IL是與CPU無關的機器語言,IL比大多數CPU語言都高級,IL也能使用彙編語言編寫,Microsoft甚至專門提供了名爲ILAsm.exe的IL彙編器和名爲IDLasm.exe的IL反彙編器。
高級語言通常只公開了CLR全部功能的一個子集,IL彙編語言允許開發人員訪問CLR的全部功能。
1.4-1 JIT(just-in-time 運行時編譯技術):
爲了執行方法,首先必須將方法的IL轉換成本機(natice)CPU指令。這是CLR的JIT編譯器的職責。
在Main方法執行之前,CLR會檢測出Main的代碼引用的所有類型。這導致CLR分配一個內部數據結構來管理對引用類型的訪問。如圖3,Main方法引用了一個Console類型,在這個內部數據結構中,Console類型定義每個方法都有一個對應的記錄項(entry)。每一個記錄項都有一個地址。這個地址最初指向JITCompiler,因此在Main首次調用WriteLine()方法時,JITCompiler會被調用並將該方法的IL代碼編譯成本機的CPU指令,隨後本機CPU指令會被保存到動態分配的內存塊中,然後JITCompiler回到Console結構中,修改最初對JITConpiler的引用,並使其指向剛剛開闢的內存塊。
現在,Main要第二次調用WriteLine時,就會直接調用之前由JITCompiler生成的本機CPU代碼,直到進程終止釋放該內存塊。
JIT將本機CPU指令保存到動態內存中,這意味着一旦應用程序終止,編譯好的代碼也會被丟棄,因此再次運行應用程序,或者同時啓動應用程序的兩個實例,JIT編譯器都必須再次執行編譯。
另一方面JIT會對本機代碼進行優化,可能花較多時間生成優化代碼,但和沒有優化時相比,代碼優化後性能更佳。
編譯器開關設置 | C# LI代碼質量 | JIT本機代碼質量 |
---|---|---|
/optimize-/debug- | 未優化 | 經優化 |
/optimize-/debug(+/full/pdbonly) | 未優化 | 未優化 |
/optimize+/debug(-/+/full/pdbonly) | 經優化 | 經優化 |
/debug:full開關告訴編譯器你打算調試程序,JIT編譯器會記錄每一條IL指令所生成的本機代碼
/debug-下則使得JIT運行的稍快,並且用的內存也稍少。
在Visual Studio中新建C#項目時,”調試“(Debug)配置指定的是/optimize-/debug:full,而”發佈“(release)配置指定的是/optimize+/debug:pdbonly。
雖然你可能很難相信,但許多人(包括本書作者)都認爲託管應用程序的性能實際上超越了非託管應用程序。
託管應用程序較非託管應用程序的幾大優勢
- JIT能夠針對本機CPU爲IL代碼生成指令,以利用本機指定CPU的任何特殊指令進行編譯。相反,非託管應用程序通常是針對具有最小功能集合的CPU編譯的。
- JIT編譯器能夠判斷一個特定的測試在它運行的機器上是否總是失敗,例如,假定有一個方法包含以下代碼
if(numberofCPUs>1){
...//Do something
}
如果主機只有一個CPU,JIT編譯器不會爲上述代碼生成任何CPU指令。
1.4-2 IL和驗證
作者認爲IL最大的優勢在於其產出的應用程序具有出色的健壯性和安全性,將IL編譯成本機CPU指令時,CLR執行一個名爲驗證(verification)的過程,確認代碼的行爲是安全的,確認傳給每個方法的每個參數都有正確的類型,每個方法的返回值都得到了正確的使用。
1.4-3 不安全的代碼
MS C#編譯器默認生成安全(safe)代碼,這種代碼的安全性可以得到驗證。C#也允許開發人員寫不安全(unsafe)的代碼。C#編譯器要求包含不安全代碼的所有方法都用unsafe關鍵字標記,並使用/unsafe編譯器開關來編譯源代碼。
(以博主C++轉來的淺薄的見識看來,如果可能的話,unsafe代碼可能還是單獨隔離出來寫到C/C++中比較好,一方面感覺C#中的指針操作和內存模型不如C++好用,另一方面可能分開用也方便管理(看着順心)一些,畢竟微軟也提供了很方便的工具,C+/CLI也好,COM訪問也好。不知這個想法對不對)(笑)
1.4-4 IL和知識產權保護
IL反彙編器可以較爲輕鬆的對CLR託管模塊進行逆向工程,如果擔心分發出去的程序集,可以從第三方廠商購買“混淆器”(obfuscator)實用程序。它能夠打亂程序集元數據中的所有私有符號的名稱,但它們能提供的保護是有限的。如果覺得混淆器不能提供自己需要的知識產權保護等級,可以考慮在非託管模塊中實現你想保密的算法。
1.5 本機代碼生成器:NGen.exe
NGen.exe能將IL代碼便宜成本及代碼,使用它的好處主要有以下幾點:
- 提高應用程序啓動速度:因爲代碼已經編譯成本機代碼,運行時不再花費時間便宜。
- 減小應用程序的工具集
同時,它生成的文件也具有以下缺點
- 沒有知識產權保護
- NGen生成的文件可能失去同步:當前執行環境與生成文件時有任何特徵不匹配,NGen生成的文件將無法使用
- CLR版本
- CPU類型
- Windows操作系統版本
- 程序集的標誌模塊版本ID:重新編譯後改變
- 引用程序集的版本ID:重新編譯被引用模塊後改變
- 安全性:包括聲明性繼承,聲明性鏈接時等
- 較差的執行時性能:NGen無法像JIT編譯器那樣對執行環境進行許多假定,不能優化地使用特定CPU指令,靜態字段只能間接訪問,還會到處插入代碼調用類構造器,因爲它不知道運行時的代碼執行順序。相較於JIT編譯的版本,NGen生成的某些應用程序在執行時反而要慢5%左右。
所以使用NGen的時候應當謹慎。
1.6 Framework類庫
.NET Framework包括Framework類庫(Framework Class Library FCL)。FCL是一組DLL程序集的總稱,其中含有數千種類型的定義,Microsoft還公開了其他的庫,如:Windows Azure SDK和DirectX SDK
Visual Studio允許創建“可移植類庫”項目,能用於多種應用程序類型,包括.NET Framework, Silverlight, Windows Phone, Windows Store應用和Xbox 360。
表3 部分常規FCL命名空間
命名空間 | 內容說明 |
---|---|
System | 包含每個應用程序都要用的的所有基本類型 |
System.Data | 包含用於和數據庫通信及處理數據的類型 |
System.IO | 包含用於執行流I/O以及瀏覽目錄/文件的類型 |
System.Net | 包含進行低級網絡通信,並與一些常用Internet協議協作的的類型 |
System.Runtime.InteropServices | 包含允許託管代碼訪問非託管操作系統平臺功能的類型 |
System.Security | 包含用於保護數據和資源的類型 |
System.Text | 包含處理各種編碼文本的類型 |
System.Threading | 包含用於異步操作和同步資源訪問的類型 |
System.Xml | 包含用於處理XML架構(XML Schema)和數據的類型 |
1.7 通用類型系統
CLR的一切都圍繞類型展開,由於類型是CLR的根本,所以Microsoft制訂了一個正式的規範來描述類型的定義和行爲,這就是“通用類型系統”(Common Type System, CTS)。
Microsoft事實上已經將CTS和.NET Framework的其他組件——包括文件格式、元數據、中間語言以及對底層平臺的方位P/invoke——提交給ECMA已完成標準化的工作。最後形成的標準稱爲“公共語言基礎結構”(Common Language Infrastructure, CLI)
我們最好區別對待“代碼的語言”和“代碼的行爲”,使用的語言不通,固然有不同的類型定義語法,但無論使用哪一種語言,類型的行爲都完全一致,因爲最終是由CLR的CTS來定義類型的行爲。
下面是另一條CTS規則:所有類型最終必須從預定義的System.Object類型繼承。該類型允許做下面這些事情:
namespace testspace
{
class test
{
void testfun()
{
object obj = 0, tar = 0;
if (obj.Equals(tar)) ; //比較兩個實例的相等性
int hash = obj.GetHashCode(); //獲取實例的哈希碼
obj.GetType(); //查詢一個實例的真正類型
object other = this.MemberwiseClone(); //執行實例的淺(按位/bitwise)拷貝
string str = obj.ToString(); //獲取實例對象當前狀態的字符串表示
}
}
}
1.8公共語言規範
不同語言創建的對象可以通過COM互相通信。CLR則集成了所有語言,用一種語言創建對象在另一種語言中也具有完全一致的行爲和特徵。
語言集成是一件非常棘手的事情,要創建很容易從不同語言訪問的類型,只能從各個語言中挑選其他語言都支持的功能。Microsoft定義了“公共語言規範”(Common Language Specification CLS),它詳細定義了一個最小功能集。任何編譯器只有支持這個功能集,生成的類型才能兼由其他符合CLS、面向CLR的語言生成的組件。
CLR/CTS支持的功能比CLS定義的多得多。具體的說,如果是開放式(對外可見)的類型,就必須遵守CLS定義的規則。但如果是封閉式的,或者說只需要在某一程序集內部訪問,CLS規則就不再適用。
關於符合CLS的類型,以下給出原書中的示例代碼以作補充:
using System
//告訴編譯器檢查CLS相容性
[assembly:CLSCompliant(true)]
namespace SomeLibrary
{
//因爲是public類,所以會顯示警告
public sealed class SomeLibrarytype
{
//警告:SomeLibrary.SomeLibraryType.Abc()的返回類型不符合CLS
public UInt32 Abc(){return 0;}
//警告:僅大小寫不同的標識符SomeLibrary.SomeLibraryType.abc()不符合CLS
public void abc(){ }
//不顯示警告:該方法是私有的
private UInt32 ABC(){return 0;}
}
}
第一個警告是因爲Abc方法返回了無符號整數,一些語言是不能操作無符號整數值的
第二個警告是因爲該類型公開了兩個public方法,這兩個方法只是大小寫和返回類型有別,VB和其他一些語言無法區分這兩個方法。
1.9 與非託管代碼的互操作性
- 託管代碼能調用DLL中的非託管函數:託管代碼通過P/invoke機制調用DLL中的函數
- 託管代碼可以使用現有的COM組件:詳情可參考.NET Framework SDK提供的TlbImp.exe
- 非託管代碼可以使用託管類型:可用C#創建ActiveX控件或Shell擴展。詳情可以參考.NET Framework SDK提供的TlbExp.exe和RegAsm.exe工具。
Microsoft隨同Windows 8引入了稱爲Windows Runtime(WinRT)的新Windows API。該API內部通過COM組件來實現。但通過.NET Framework團隊創建的元數據ECMA標準描述其API,好處是用一種.NET語言寫的代碼能與WinRT API無縫對接。第25章將對此詳細敘述。