http://www.computertoday.com.hk/computing.php?gsel=6&cid=91
整理如下
基本觀念註解
1) CLR的定位
.NET應用程式的執行時期環境(Run-time)
提供.NET應用程式和底層作業系統的中間層
2) Windows上的可執行檔(PE/COFF)
A> .NET 程式都是標準 PE 格式的 exe 檔(這裏面存的不是 Native Code!!)。
B> Windows上的可執行檔都必須遵循PE/COFF格式(Portable Excutable/Microsoft Common Object File Format)。
標準的PE可執行檔分成二大部份
* headers
* native image sections(.data,.rdata,.rsrc,.text)
C> .NET的可執行檔(.NET PE)
為了可擴充性,Microsoft允許除了這些區段之外,在PE中自定新的section。
安裝.NET時,會將OS的loader換掉,換成認得.NET PE的loader。
.NET正是利用擴充section的方式,.NET的windows loader認得.NET PE,並將這些
sections交由CLR處理。
這就是為何.NET應用程式也可以以.exe方式存在並直接執行的原因。
D> 觀察.NET的可執行檔
利用dumpbin.exe可以dump出PE檔案。
3) .NET中deploy是以assembly為最小單位。
4) .NET的可執行檔中含MSIL及metadata。
Microsoft的作品為了體貼使用者,隱藏了許多細節,對Developer來說,反而較不直觀。
也是觀念不清的來源。
5) .NET Deployment 特色
A> Side by side excution
Any versions of the same shared assembly to execute at the same time,on
the same system,even in the same process。
(在同一個Process或系統中,不同版本的同名Dll檔案可以同時被存在並被執行)。
B> Xcopy deployment
Assembly具有自我描述功能,所以免註冊。
為何具有自我描述功能?以assembly中的metadata來取代registry功能。
6) Managed Compiler
指將.NET語言compile成「abstract intermediate form」的工具。
如VB.NET的vbc.exe, VC#.NET的csc.exe等。
abstract intermediate form包括二部份
﹡Metadata(描述程式之結構及相關資訊)
﹡Managed code(MSIL)
Managed指由CLR來管理及執行的程式碼。
Managed Code有三個部份由CLR來管理
1> Type control:檢查型別、轉換型別。
2> Exception Handling
3> GC(記憶體回收)
7) Assembly & Module 觀念
Assembly
.NET中Deploy及Versioning的最小單位。
指一個功能完整的.NET應用程式。(managed)
Module
泛指一個可.NET執行檔(.exe)、.NET library(.dll)或其它資源。(必須為managed code)
藉由metadata描述和其它modules的關係。
一個Assembly可以只包含一個或多個Modules。
Assembly程式進入點所在稱為Prime Module。
8) Manifest
Manifest就是Assembly的metadata。
Manifest內含資訊
﹡assembly相依程式碼的位置
﹡組成assembly的各檔案檔名
﹡Assembly相依程式碼的metadata
﹡assembly的版本資訊
﹡型別資訊
9) Assembly Identities
在COM時代,使用GUID來做為Component的唯一識別碼。但一長串的數字實在不容易記。
.NET的private assembly引進namespace的觀念來做唯一識別(類似Java的Package)。但
仍不能區分同一支程式的不同版本。
因此,若一個assembly要由其它的.NET程式共享時,必須由Strong Name來做識別。
10) Strong Name
A> .NET中的唯一識別採用 PKC (public key cryptography)
B> Strong name 的認証過程
1> Application creator利用assembly中的「manifest」做出一個hash code,我們稱之「original hash」。
2> 利用private key 將「original hash」 加密成「encrypted hash」。
3> 「Encrypted hash」及Application creator的「public key 」隨著assembly的manifest發行。
4> 執行者以Application creator的「public key 」對「Encrypted hash」做解密,得到「original hash」 。
5> 執行者以assembly中「manifest」做出一個「original hash」
6> 執行者比對這二個「original hash」一不一致。
11) Shared Assembly
GAC(Global Assembly Cache)
放置重要的系統共享檔案
有可能帶來和傳統registry一樣的問題。
建議只在二種情況之下將assembly設為shared assembly
這個assembly一定要被好幾個Application 所共用。
這個assembly需要高安全性。(只有administrator可以動GAC中的檔案)
12) VEE結構
Class Loader
OS loader認出.NET PE後,將控制權交給CLR。此時ClassLoader會起來,找到並載入含進入點Main()的Class。
其它的Classes在第一次被參考到時也會被載入。
ClassLoader如何找到Classes: 目前目錄之.config、GAC及metadata(manifest)。
13) Method stub
Class Loader將classes放入記憶體中後,會在每個class的method加上一個stub,這個stub有二個作用:
﹡用來辨別這個method是否已被JIT Compilation。
﹡標明這個是managed/unmanaged code
14) JIT Compiler
.NET PE中所包含的不是native code,而是metadata及IL。
直到method第一次被執行前才會由JIT將IL轉成managed native code。(如何得知第一次被執行?利用method stub)
Compile完後將method的native code之位址存回method stub中。
這些編譯後的native code何時被消除? 直到這個process shutdown ,並由GC程序將process所reference到的memory都回收。
15) Pre-JITing
使用ngen.exe:安裝時期先行編譯。
16) Verifier
Verifier是JIT Compiler的一部份。
Class Loader將Class load進來之後,verifier會進行檢查。
﹡metadata是不是well-formed
﹡IL Code是否Type Safe
17) Execution Support & Management
GC
Security management
Code management(控制程式碼的執行)
Exception management
debug/profiling(提供debug/profile的API)
marshaling management
thread management
18) .NET與COM/Native Dll的互通性
呼叫.NET元件
﹡Regasm.exe:將.NET Assembly註冊到系統的registry中。
﹡tlbexp:產生該Assembly的type library
﹡然後VB/VC就可以用呼叫一般COM元件的方式來使用這個.NET元件了。
呼叫COM元件
﹡tlbimp.exe:為com 元件產生一個.NET PE的stub。(會產生一個dll檔)
﹡對這個.NET stub的呼叫都會被轉成對COM的呼叫。
19) Summary
﹡.NET和Java執行方式並不相同,CLR及JVM安裝在平台上的方式也不相同。安裝CLR後對整個作業系統影響較大。(ie.把OS的loader給換掉了)。
﹡.NET的程式利用metadata取代傳統的registry方式來描述自己。從而減少了dll hell的發生機會。
﹡所有.NET的程式經過managed compiler後都會變成IL,因此可達到跨語言的功能。但是變成XX.NET的語言都要符合CTS才行。
﹡CLS的標準只有部份被ECMA接受。
﹡所有.NET程式語言都使用相同一組library,所以只要學習一種.NET語言,其它語言都相當容易上手。
﹡在windows平台上無論是易用性及效能.NET都有出色的表現。移植到其它OS上表現能否一樣出色則尚待觀察。
---------------------------------------------------------
.NET程式的啟動與載入
.NET PE檔如何將控制權交給.NET CLR?
在進入Main()之前,.NET CLR做了哪些事?
.NET組件(assembly)與模組(module)是如何被載入記憶體的?
何謂類別載入器(class loader)?
JIT編譯器的運作原理為何?
本文章以一個實際的例子,幫助讀者理解.NET執行引擎(execution engine)內部的運作,在閱讀完本文章之後,這一切的困惑都將消失。
範例程式
本範例需要三個原始碼,分別是Main.cs、A.cs、BC.cs。下面是BC.cs的原始碼(包含兩個類別):
// BC.cs
public class ClassB {
public static string GetString() {
return "Class B";
}
}
public class ClassC {
public static string GetString() {
return "Class C";
}
}
下面是A.cs的原始碼:
// A.cs
public class ClassA {
public static string GetString() {
return "Class A";
}
}
下面是Main.cs的原始碼:
// Main.cs
using System;
public class MainClass {
public static void Main(string[] args) {
for (int i=0; i< args.Length; i++) {
switch (args[i]) {
case "A" :
ShowA(); break;
case "B" :
ShowB(); break;
case "C" :
ShowC(); break;
}
}
}
private static void ShowA() {
Console.WriteLine(ClassA.GetString());
}
private static void ShowB() {
Console.WriteLine(ClassB.GetString());
}
private static void ShowC() {
Console.WriteLine(ClassC.GetString());
}
}
我們首先將BC.cs編譯成一個模組(module),編譯完畢之後,可以得到一個名為BC.netmodule的模組檔案,其內部的格式也是.NET PE。編譯方式如下:
csc /t:module BC.cs
再將A.cs編譯成一個DLL組件,同時將BC.netmodule加入此組件。編譯完畢之後,可以得到一個多模組(multi-module),也是多檔案(multi-file)的組件,名為A.dll。作法如下:
csc /t:library /addmodule:BC.netmodule A.cs
最後,將Main.cs編譯成一個EXE組件。編譯完畢之後,可以得到一個名為Main.exe的組件。編譯方式如下:
csc /r:A.dll Main.cs
這些組件與模組的關係如圖1所示:
進入Main()之前
執行Main.exe時,.NET CLR啟動的方式如下:
1. Windows所提供的PE Loader將Main.exe載入記憶體
2. Windows所提供的PE Loader將MsCorEE.dll載入記憶體(MsCorEE.dll位於C:\WINDOWS\System32或C:\WINNT\System32)
3. 程式跳到MsCorEE.dll內的 _CorExeMain()
4. _CorExeMain()會載入「適當版本的」.NET CLR。如果你的.NET是1.0.3705版,則.NET CLR位於C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705或者C:\WINNT\Microsoft.NET\Framework\v1.0.3705
5. 程式跳到.NET CLR,進行一些初始化工作(initialization)
6. 載入assembly「MsCorLib」,載入module「MsCorLib.dll」,載入MsCorLib內的某些class
7. 產生主執行緒(main thread)
8. 載入「Main」組件,載入「Main.exe」模組,載入「MainClass」類別
9. 讓主執行緒開始執行MainClass.Main()
上述的1~3點,也就是在一開始載入Main.exe與MsCorEE.dll,並跳到_CorExeMain()的這部分,會因為Windows作業系統版本的不同,而有所差異。Windows XP與Windows Server 2003的loader因應.NET做了修改,更能符合 managed PE的需求。而Windows 2000/me/98的loader則未針對.NET做出修改。下面分別解釋它們的作法。
如果作業系統是Windows XP,當Windows XP的loader將Main.exe載入之後,會檢查PE header的data directory table (請參考圖 2)。如果COM_Header (亦稱為CLR_Header)內的值不為0,就表示是.NET PE,loader會立刻載入 MsCorEE.dll,並直接執行MsCorEE.dll內的_CorExeMain()。
如果作業系統是Windows 2000,當Windows 2000的loader將ain.exe載入之後,會檢查PE header的data directory table (請參考圖 2),將Import_Table所記錄的檔案都載入記憶體,以本例來說,就是MsCorEE.dll。接著找出PE header內所記錄的程式進入點(請參考圖 3),並執行此處的程式,這是x86機器碼,由編譯器自動產生,只有一道指令(6 bytes),為「FF 25 00 20 40 00」,翻譯成 x86組合語言就是「JMP DWORD PTR [402000]」,其中 0x00400000 是 Main.exe 的 image base,而0x2000是import address table的RVA(此處記錄著_CorExeMain() 的記憶體位址),所以執行「JMP DWORD PTR[402000]」的結果會跳到MsCorEE.dll的_CorExeMain()。
目前遊戲光碟所使用的防盜技術多達10數種,其中以SafeDisc、SecuRom、StarForce與實體壞軌保護等方式較為常見,下面就為用戶說明這幾種防拷方式的特色。
綜觀1~3點的作法,可以和Java作一個比較。Java執行方式如下所示:
java Hello
Java的作法是先執行VM(由java.exe進入),再請VM去執行Java bytecode。.NET的作法是,先執行.NET PE,立刻將控制權轉給.NET CLR,再由.NET CLR來執行.NET PE。.NET的作法固然使用上比Java方便,但其實.NET需要OS的loader配合,而Java不需要。
上述第4點提及_CorExeMain()會載入「適當版本的」.NET CLR。其實MsCorEE.dll 不是CLR,只是一個填隙程式(shim),負責搭起.NET PE和.NETCLR之間橋樑。MsCorEE會匯集足夠的資料,然後據以判斷要載入Workstation版(MsCorWks.dll)或者Server版(MsCorSvr.dll)的.NET CLR。以本例來說,被載入的是Workstation版的MsCorWks.dll。在載入MsCorWks.dll之後,還會陸續載入fusion.dll、MsCorSn.dll、MsCorJIT.dll等DLL檔。
事實上java.exe的角色和MsCorEE.dll的角色一樣,負責喚起適當版本的JVM。如果你安裝的是1.4.x的J2SE SDK而非1.4.x的JRE,你的JDK內會同時具備client和server兩個JVM,分別在JDK目錄下的jre\bin\client\jvm.dll與jre\bin\server\jvm.dll。第5點提到,程式跳到.NET CLR,進行一些初始化工作(initialization)。這些初始化的動作包括了:
* 準備一塊記憶體,以為managed heap之用。
* 準備好一個thread pool,以便CLR以及.NET程式庫稍後使用。
* 準備好一個application domain (簡稱為AppDomain)。
AppDomain是一個很複雜的主題,在此筆者只簡略地說明。基本上,AppDomain可以被視為sub-process,也就是對process所切割出來的小單位。AppDomain用來隔離不同的應用,任何被載入process的assembly,都歸屬於某個AppDomain 的管轄。一個process內可以有多個AppDomain,第一個建立的AppDomain是由.NET CLR自動建立的,稱為default AppDomain。default AppDomain剛開始時被命名為「DefaultDomain」,稍後會被更名。
第6點,載入「MsCorLib」組件。對於.NET來說,MsCorLib一定是第一個被載入的組件。MsCorLib是很特殊的組件,從某種角度來說,它可被視為是.NET CLR的一部份。MsCorLib只能屬於default AppDomain,不能屬於其他的AppDomain,因為它一定是被process內所有AppDomain共用的。
接著要載入「MsCorLib.dll」模組。對於MsCorLib這種單模組(single-module)的組件來說,雖然模組和組件是同一個檔案,但是邏輯上還是有所區隔。所以載入「MsCorLib」組件之後,還是需要載入「MsCorLib.dll」模組。接著要請class loader來載入MsCorLib內相關的class。利用工具觀察,此時載入的class依序如下:
System.Object
System.ICloneable
System.Collections.IEnumerable
…為節省篇幅,故略去中間一部份。
System.AppDomain
System.LoaderOptimization
System.Runtime.Remoting.Proxies.__TransparentProxy
請注意,此時並未載入MsCorLib內全部的class,只有載入目前需要的class。第7點,產生主執行緒(main thread),也因此觸發了對某些class的需求,於是又載入下列的這些 class:
System.Threading.Monitor
System.IAppDomainSetup
System.AppDomainSetup
System.Char
System.Runtime.InteropServices.RuntimeEnvironment
System.RuntimeFieldHandle
System.Runtime.CompilerServices.RuntimeHelpers
$$struct0x6000136-1
System.Environment
第8點,載入「Main」組件,載入「Main.exe」模組,載入「MainClass」類別。請注意:「Main」組件被載入到」DefaultDomain」,「DefaultDomain」也隨即被改名為「Main」。(Default AppDomain在載入主程式的組件之後,會自動以組件名稱為AppDomain的名稱。)第9點,讓主執行緒真正進入程式,開始執行MainClass.Main()
進入Main()之後
class loader的作用是:將TypeDef相關的metadata從PE檔中讀進來,建立一個內部的資料結構(CORINFO_CLASS_STRUCT),此資料結構內有一個Non-Virtual Method Table和一個Virtual Method Table,紀錄著此class的每個method記憶體位址。一開始,method的記憶體都指向JIT編譯器。這造成一種現象:任何method第一次被執行時都會跳到JIT編譯器。JIT編譯器會進行下面的動作:
到metadata中查出此method的IL位址,將此method的IL編譯成x86的機器碼(Machine Code)將CORINFO_CLASS_STRUCT內的Method Table中此method的位址指向此機器碼的位址,如此一來,當下次再次執行相同的method時,就不會再通過JIT編譯器了。開始執行此x86機器碼。當程式開始執行MainClass.Main(),由於是第一次執行,所以先會通過JIT編譯器再執行。
實驗一
如果我們在執行Main.exe時,未指定任何命令列參數(common line argument),那麼,Main()就不會進入for迴圈,程式直接結束。整個執行過程中,A.dll與BC.netmodule都不會被載入到記憶體。
實驗二
如果我們在執行Main.exe時,指定一個命令列參數「A」,這會使得,Main() 進入for迴圈,並呼叫ShowA()。由於ShowA()尚未被呼叫過,所以會先進行JIT編譯。
JIT編譯時,有可能會觸發class loader。當Main()被JIT編譯時,由於Main()的IL完全不涉及其他class,所以不會觸發class loader,但是ShowA()就不同於Main()了,ShowA()的定義牽涉到Console和ClassA,這兩個class都尚未被載入記憶體 (也就是說它們都尚未具備CORINFO_CLASS_STRUCT資料結構),所以JIT編譯器會請求class loader的協助,將此二class載入之後,JIT編譯方能繼續。class loader先載入ClassA,再載入Console (此乃依據IL中兩者出現的前後次序,ClassA出現在前,Console出現在後)。
現ClassA所處的模組(也就是A.dll)並未被載入,於是請求module loader的協助;module loader試圖載入A.dll時,發現A.dll所處的組件(也就是A)並未被載入,於是請求assembly loader的協助;於是assembly loader載入「A」組件;module loader載入「A.dll」模組(注意:「BC.netmodule」並不會被載入),然後class loader就可以將ClassA載入記憶體。
class loader不一定會觸發module loader。當class loader試圖載入Console時,發現Console所處的module(也就是MsCorLib.dll)已經被載入,於是class loader逕自將ClassA載入記憶體,不需要assembly/module loader的協助。
實驗三
如果我們在執行Main.exe時,指定一個命令列參數「B」,這會使得,Main()進入for迴圈,並呼叫ShowB()。由於ShowB()尚未被呼叫過,所以會先進行JIT編譯;由於ClassB尚未被載入,所以JIT會請求class loader協助;由於ClassB所處的模組(也就是BC.netmodule)尚未被載入,所以class loader會請求module loader的協助;由於BC.netmodule所處的組件(也就是A)尚未被載入,所以module loader會請求assembly loader的協助。
於是,assembly loader將A載入記憶體,且由於.dll是prime module,所以必須緊接著組件之後被載入(儘管不需要用到ClassA),然後,BC.netmodule才被載入。class loader終於可以載入ClassB。
觀念整理
關於這三個實驗,目的在於讓讀者瞭解下面幾點:
* JIT編譯以method為單位,只有被呼叫到的method才會被編譯。
* JIT編譯器可能會觸發class loader。
* class loader可能會觸發module loader。
* module loader可能會觸發assembly loader。
[jocosn 在 2005-4-16 04:46 PM 作了最後編輯]