C#學習筆記——運行時的相互聯絡

來源:互聯網
上載者:User

目錄

一 載入.NET 程式集

二 應用程式定義域

三 解析類型引用

四 類型

五 記憶體配置

六 類型、對象、線程棧、託管堆在運行時的相互聯絡

  本文將解釋 PE、Windows 載入器、應用程式定義域、資訊清單、中繼資料、類型、對象、線程棧、託管堆等,與運行時的相互關係。因此,我首先寫了一個簡單 Demo 用於調試,其代碼如下:

using System;

namespace CLRTest
{
public class Circle
{
public double Radius { get; set; }

public Circle() { }

public Circle(double r)
{
this.Radius = r;
}

public double GetCircumference()
{
return 2 * Math.PI * Radius;
}

public double GetArea()
{
return Math.PI * Math.Pow(this.Radius, 2.0);
}

public override string ToString()
{
return string.Format("半徑:{0} 周長:{1} 面積:{2}", this.Radius, this.GetCircumference(), this.GetArea());
}
}
}
using System;

namespace CLRTest
{
class Program
{
static void Main(string[] args)
{
Circle circle = new Circle(4.0);
Console.WriteLine(circle.ToString());
Console.ReadKey();
}
}
}

一 載入.NET 程式集

  在Windows上啟動並執行程式可以通過多種不同的方式進行啟動。Windows 負責處理所有的相關工作,包括設定進程地址空間、載入可執行程式,以及指示處理器開始執行等。當處理器開始執行程式指令時,它將一直執行下去,直到進程退出。

  現在擴充我們對 可攜式執行檔的認識,PE 格式是 Windows 可執行程式的檔案格式,可執行程式包括:*.exe、*.dll、*.obj、*.sys 等。為了支援.NET,在 可攜式執行檔格式中增加了對程式集的支援,PE檔案格式如下:

  為了支援PE映像的執行,在PE的頭包含了一個域叫做 AddressOfEntryPoint。這個域表示 可攜式執行檔的進入點(EntryPoint)的位置。在.NET程式集中,這個值指向.text 段中的一小段存根(stub)代碼(“JMP _CorExeMain”)。當.NET 編譯器產生程式集時,它會在 可攜式執行檔中增加一個資料目錄項。具體來說,這個資料目錄項的索引為 15,其中包含了 CLR 頭的位置和大小。然後,根據這個位置在 可攜式執行檔中找到位於.text 段中的 CLR 頭。在 CLR 頭中包含了一個結構 IMAGE_COR20_HEADER。在這個結構中包含了許多資訊,例如Managed 程式碼應用程式進入點,目標 CLR 的主要版本號和從版本號碼,以及程式集的強式名稱簽名等。根據這個結構中包含的資訊,Windows 可以知道要載入哪個版本的 CLR 以及關於程式集本身的一些資訊。在.text 段中還包含了程式集的中繼資料表,IL以及非託管啟動存根碼。非託管啟動存根碼包含了由 Windows 載入器執行以啟動 可攜式執行檔執行的代碼。

  當 Windows 載入一個.NET 程式集時,mscoree.dll 的_CorExeMain(或者是_CorDllMain,取決於載入的是可執行檔還是庫) 函數被第一個調用,以啟動 CLR。 mscoree.dll 在啟動 CLR 時將執行一系列操作:

  (1) 通過查看 可攜式執行檔中的中繼資料(具體來說是 CLR 頭中的 MajorRuntimeVersion 和 MinorRuntimeVersion)找出.NET 程式集是基於哪個版本的 CLR 構建的。

  (2) 找出 OS 中正確版本 CLR 的路徑。

  (3) 載入並初始化 CLR。

  在 CLR 被初始化之後,在 可攜式執行檔的 CLR 頭中就可以找到程式集的進入點(Main())。然後,JIT 開始編譯並執行進入點。

  綜上所述,.NET 程式集的載入步驟如下:

  (1) 執行一個 .NET 程式集。

  (2) Windows 載入器查看 AddressOfEntryPoint 域,並找到 可攜式執行檔中的.text 段。

  (3) 位於 AddressOfEntryPoint 位置上的位元組是一個 JMP 指令,用於跳轉到 mscoree.dll 中的一個匯入函數。

  (4) 將執行控制轉移到 mscoree.dll 中的函數 _CorExeMain 中,這個函數將啟動 CLR 並把執行控制轉移到程式集的進入點。

   注意,在 Windows XP 及以後版本中,對載入器進行了最佳化,使其能夠識別出一個 可攜式執行檔,是否是.NET 程式集。這樣,在載入一個.NET 程式集時,就不再需要通過存根函數調用 mscoree.dll的匯入函數了,而是變為自動載入 CLR。

二 應用程式定義域

  Windows 使用進程來隔離應用程式,.NET 在此基礎上進一步引人了另一種邏輯隔離層,即應用程式定義域。構造和管理進程的開銷是非常高的,應用程式定義域極大地降低在建立與銷毀隔離層時所需的開銷。

  進程與應用程式定義域的關係如下:

  在任何啟動了 CLR 的 Windows 進程中都會定義一個或多個應用程式定義域,在這些域中包含了可執行代碼、資料、中繼資料結構以及資源等。除了進程本身的保護機制外,應用程式定義域還進一步引人了以下保護機制:

  • 一個應用程式定義域中的錯誤碼不會影響到同一個進程中另一個應用程式定義域中啟動並執行代碼。
  • 一個應用程式定義域中的代碼不能直接存取另一個應用程式定義域中的資源。
  • 每個應用程式定義域中都可以配置與代碼特定的資訊,如安全設定。

  對於沒有顯式建立應用程式定義域的應用程式來說,CLR 會建立三個應用程式定義域:系統應用程式定義域、共用應用程式定義域、預設應用程式定義域。

(一) 系統應用程式定義域

  系統應用程式定義域主要功能如下:

  • 建立其它兩個應用程式定義域(共用應用程式定義域、預設應用程式定義域)。
  • 將 mscoree.dll載入到共用應用程式定義域中。
  • 記錄進程中所有其它的應用程式定義域,包括提供載入、卸載應用程式定義域等功能。
  • 記錄字串池中的字串常量,因此允許任一字元串在每個進程中都存在一個副本。
  • 初始化特定類型的異常。

(二) 共用應用程式定義域

  在共用應用程式定義域中包含的是與應用程式定義域無關的代碼。mscoree.dll 將被載入到這個應用程式定義域中,此外還包括在 System 命名空間中的一些基本類型(eg.String、Array等)。在大多數情況下,非使用者代碼將被載入到共用應用程式定義域中。啟用了 CLR 的應用程式定義域可以通過載入器的最佳化屬性來注入使用者代碼。

(三) 預設應用程式定義域

  通常,.NET 程式在預設應用程式定義域中運行。位於預設應用程式定義域中的所有代碼都只有在這個域中才是有效。由於應用程式定義域實現了一種邏輯並且可靠的邊界,因此任何跨越應用程式定義域的訪問操作都必須通過.NET 遠程對象來進行。

  顯示了本文開頭建立的 Demo 的應用程式定義域資訊:

三 解析類型引用

  運行應用程式時,CLR 會載入並初始化它。然後 CLR 讀取程式集的 CLR 頭,尋找標識了應用程式入口的方法(Main())的 MethodDefToken。然後,CLR 會搜尋 MethodDef 中繼資料表,找到該方法的 IL 代碼在檔案中的位移量,把這些 IL 代碼 JIT 編譯為本地代碼。編譯時間會對代碼進行驗證以確保型別安全。最後,將執行本地代碼。在 JIT 編譯時,CLR 會檢查對類型和成員的所有引用,並載入定義了它們的程式集(如果尚未載入),CLR 必須定位並載入程式集。解析一個引用的類型時,CLR 可能在以下三個地方找到類型:

  • 同一個檔案 
  • 不同檔案,相同程式集
  • 不同檔案,不同程式集

  解析一個類型引用時如果發送任何錯誤,如找不到檔案、檔案無法載入、雜湊值不匹配等,就會拋出異常。示範了類型綁定的過程:

  (注意 ModuleDef、ModuleRef、FileDef 中繼資料表使用檔案名稱及其副檔名來引用檔案。而 AssemblyRef 中繼資料表使用不帶副檔名的檔案名稱來引用程式集。要和一個程式集綁定時,系統通過探測目錄嘗試定位檔案。)

  對於 CLR 來說,所有程式集都是根據名稱、版本、語言文化、公開金鑰來標識的。但是,GAC 根據名稱、版本、語言文化、公開金鑰和 CPU 架構來標識程式集。在 GAC 中搜尋程式集時,CLR 判斷應用程當前在什麼類型的進程中運行(32位、64位)。然後,CLR 首先搜尋程式集的這種 CPU 架構專用版本,如果沒有找到,就搜尋不區分 CPU 的版本。

四 類型

  類型是.NET 程式中的基本編程單元。在.NET 應用程式中,要麼使用自訂的類型,要麼使用現有的類型。類型分為兩類:實值型別和參考型別。實值型別是指儲存線上程棧上的類型,包括:枚舉、結構以及簡單類型(如 int、bool、char等)。通常,實值型別是一些佔據記憶體空間較小的類型。另一種類型叫做參考型別,它是在堆上分配的,並由記憶體回收行程(GC)負責管理。在參考型別中也可以包含實值型別,在這種情況下,實值型別將同樣位於堆上並且由垃圾收集器來管理。

  託管堆上對象的結構如下:

  在託管堆上的每個對象執行個體中都包含了以下資訊:

  • 同步塊(sync block):同步塊可以是一個位元遮罩,也可以是由 CLR 維持的同步塊表中的索引,其中包含了關於對象本身的輔助資訊。
  • 類型控制代碼(type handle):類型控制代碼是 CLR 類型系統的基礎單元,可以用來對託管堆上的類型進行完整描述。
  • 對象執行個體:在同步塊索引和類型控制代碼之後緊接著是實際的對象資料。

  顯示了 Demo 的 Circle 對象的內容:

(一) 同步塊表

   在託管堆上每個對象的前面都有一個同步塊索引,它指向 CLR 中私人堆上的同步塊表。在同步塊表中包含的是指向各個同步塊的指標,在同步塊中包含了許多資訊,如對象的鎖、互用性資料、應用程式定義域索引、對象的散列碼(hash code)等。當然,在對象中也可能不包含任何同步塊資料,此時的同步塊索引值為0。需要注意的是,在同步塊中並不一定只包含簡單的索引,也可以包含對象的其它輔助資訊。

  (在使用索引時要注意,CLR 可以自由移動/增長同步塊表,同時卻不一定對所有包含同步塊的對象頭進行調整。)

(二) 類型控制代碼

  參考型別的所有執行個體都被放在託管堆上,這個堆是由 GC 來控制。在所有的執行個體中都包含了一個類型控制代碼。簡單地說,類型控制代碼指向的是某個類型的方法表。在方法表中包含了各種中繼資料,它們完整地描述了這個類型。說明了方法表的整體記憶體布局:

  類型控制代碼是 CLR 類型系統中的粘合劑,它把對象執行個體及其所有的相互關聯類型資料關聯起來。對象執行個體的類型控制代碼儲存在託管堆上,它是一個指標,指向類型的方法表。在方法表中包含了關於物件類型的大量資訊,包括指向其它關鍵 CLR 資料結構(如 EEClass)的指標。在類型控制代碼指向的第一類資料中包含了關於類型本身的一些資訊(如標誌、大小、方法數量、父方法表等)。下一個要注意的域是一個指標,指向一個 EEClass。方法表的下一部分也是一個指標,指向與類型相關的模組資訊。在剩餘的域中包含了類型的虛方法表。需要注意的是,在方法表中的一些方法指標可能會指向Unmanaged 程式碼。出現這種情況的原因是,一些方法可能還沒有被 JIT 編譯器編譯。事實上,啟動編譯過程的 JIT 存根代碼是一段Unmanaged 程式碼,當方法沒有被 JIT 編譯器編譯時間,它會指向這段Unmanaged 程式碼,在編譯之後會把執行控制權轉移到新編譯產生的程式碼。

(三) 方法描述符

  在方法表中包含了虛方法表,裡麵包含了一些指向隱藏在類型方法背後的代碼的指標。虛方法表中包含了指向代碼的指標,這些方法本身可以自行描述,這都歸功於方法描述符。在方法描述符中包含了關於方法的詳細資料,如方法的文本表示、它所在的模組、標記以及實現方法的代碼地址。

  顯示了 Demo 的 Circle 對象的方法表及方法描述符:

  查看 GetCircumference 方法的 IL:

  進一步擷取方法的資訊:

(四) 模組

  查看類型 Circle 所在模組的資訊:

(五) 中繼資料標記

  CLR 的中繼資料以表格的形式儲存在運行時引擎中,中繼資料標記是一個4位元組的值,其布局如下:

  查看 Circle 的方法表可以看到中繼資料標記:

  值為 02000004 的中繼資料標記可以解釋為:指向類型定義表中的第4個索引。

(六)EEClass

  EEClass 資料結構可以看成是方法表的一個邏輯等價物,因此它可以作為實現 CLR 類型系統自描述性的一種機制。本質上,EEClass 和方法表是兩種截然不同的結構,但從邏輯來看,它們都表示相同的概念。之所以分成這兩種資料結構,是因為 CLR 使用類型域的頻繁程度不同。頻繁使用的域被儲存到方法表中,而不頻繁使用的域被儲存到 EEClass 中。EEClass 的大體結構如下:

  C# 中的階層在 EEClass 中同樣適用。當 CLR 載入類型時,會建立一個類型的 EEClass 節點階層,其中包含了指向父節點和兄弟節點的指標,這樣就可以遍曆整個階層。EEClass 中的方法描述塊域,包含了一個指標,指向類型中的第一組方法描述符,這樣就能遍曆任意類型中的方法描述符。在每組方法描述符中又包含指向鏈表中下一組方法描述符的指標。

  查看 Circle 的 EEClass:

五 記憶體配置

  CLR管理的記憶體主要分為3部分,如下:

  • 線程棧 用於分配實值型別執行個體。線程棧主要由作業系統管理,而不受垃圾收集器的控制,當實值型別執行個體所在方法結束時,其儲存單位自動釋放。棧的執行效率高,但儲存容量有限。
  • 小型對象堆(SOH) 用於分配小對象執行個體。如果參考型別對象的執行個體大小小於85000位元組,執行個體將被分配在SOH堆上,當有記憶體配置或者回收時,垃圾收集器可能會對SOH堆進行壓縮。
  • 大型物件堆(LOH) 用於分配大對象執行個體。如果參考型別對象的執行個體大小不小於85000位元組時,該執行個體將被分配到LOH堆上,不同於SOH堆,垃圾收集器不會對LOH堆進行壓縮。

六 類型、對象、線程棧、託管堆在運行時的相互聯絡

  運行 Demo 時,會啟動一個進程,因為程式本身是單線程的所有只有一個線程。一個線程被建立時會分配到 1MB 大小的棧。這個棧的空間用於向方法傳遞實參,並用於方法內部定義的局部變數。

  現在,Windows 進程已經啟動,CLR 已經載入到其中,託管堆已初始化,而且已建立一個線程(連同它的 1MB 棧空間)。現在已經進入 Main() 方法,馬上就要執行 Main 中的語句,所以棧和堆的狀態如所示(為了簡化,我只畫出了自訂的類型):

  當 JIT 編譯器將 Main() 方法的 IL 代碼轉換成本地 CPU 指令時,會注意到其內部引用的所有類型。這個時候,CLR 要確保定義了這些類型的所有程式集都已載入。然後利用程式集的中繼資料,CLR 提取與這些類型有關的資訊,並建立一些資料結構來表示類型本身。線上程執行本地代碼前,會建立所需的所有對象。顯示了在 Main 被調用時,建立類型對象後的狀態:

  當 CLR 確定方法需要的所有類型對象都已建立,而且 Main 的代碼已經編譯之後,就允許線程開始執行編譯好的本地代碼。首先執行的是 “Circle circle = new Circle(4.0);”,這會建立一個 Circle 類型的局部變數,並為其賦值。當調用建構函式時,會在託管堆中建立 Circle 的執行個體。任何時候在堆上建立一個對象 CLR 都會自動初始化內部類型對象指標成員,將它引用與對象對應的類型對象。此外,CLR 會先初始化同步塊索引,將對象的所有執行個體欄位設定為 null 或 0,再調用類型的構造器。new 操作符會返回 Circle 對象的記憶體位址,該地址將儲存在局部變數 circle 中(線上程棧上)。此時的狀態如:

  接著執行“Console.WriteLine(circle.ToString());”。ToString() 方法是一個虛方法,在調用虛方法時,JIT 編譯器要在方法中產生一些額外的代碼,方法每次調用時都會執行這些代碼。這些代碼首先檢查發出調用的變數,然後跟隨地址來到發生調用的對象。在本例中,變數 circle 引用的是 Circle 類型的一個對象。然後,代碼檢查對象內部的“類型控制代碼”成員,這個成員指向對象的實際類型。然後,代碼在類型對象的方法表中尋找引用了被呼叫者法的記錄項,對方法進行 JIT 編譯(如果需要),再調用 JIT 編譯過的代碼。就本例來說,調用的是 Circle 類型的 ToString 實現。(在調用非虛方法時,JIT 編譯器會找到調用對象的類型對應的類型對象。如果該類型沒有定義那個方法,JIT 編譯器就會回溯類階層,一直回溯到 Object,並在沿途的每個類型中尋找該方法。)

  WriteLine(string) 是靜態方法。調用一個靜態方法時,CLR 會定位與靜態方法的類型對應的類型對象。然後,JIT 編譯器在類型對象的方法表中尋找與被調用的方法對應的記錄項,對方法進行 JIT 編譯(如果需要),再調用 JIT 編譯的代碼。綜上所述,“Console.WriteLine(circle.ToString());”的操作結果如所示:

  最後,執行“Console.ReadKey();”,與WriteLine(string) 類似,這裡就不再贅述。我們可以看到,Circle 類型對象也包含“類型控制代碼”成員。這是因為類型對象本質上也是對象。CLR 建立類型對象時,必須初始化這些成員。CLR  開始在一個進程中運行時,會立即為 mscorlib.dll 中定義的 System.Type 類型建立一個特殊的類型對象。Circle 類型對象是該類型的執行個體。因此,在初始化時,Circle 類型對象的類型控制代碼會初始化為對 System.Type 類型對象的引用。如所示:

  System.Type 類型對象本身也是一個對象,內部的類型控制代碼指向它本身。System.Object 的 GetType 方法返回的是儲存在指定對象的類型控制代碼(是一個指標)。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.