元數據概述:元數據是一種二進制信息,用以對存儲在公共語言運行庫可移植可執行文件 (PE) 文件或存儲在內存中的程序進行描述。將您的代碼編譯為 PE 文件時,便會將元數據插入到該文件的一部分中,而將代碼轉換為 Microsoft 中間語言 (MSIL) 並將其插入到該文件的另一部分中。在模塊或程序集中定義和引用的每個類型和成員都將在元數據中進行說明。當執行代碼時,運行庫將元數據加載到內存中,並引用它來發現有關代碼的類、成員、繼承等信息。
元數據以非特定語言的方式描述在代碼中定義的每一類型和成員。元數據存儲以下信息:
-
程序集的說明。
-
標識(名稱、版本、區域性、公鑰)。
-
導出的類型。
-
該程序集所依賴的其他程序集。
-
運行所需的安全權限。
-
-
類型的說明。
-
名稱、可見性、基類和實現的接口。
-
成員(方法、字段、屬性、事件、嵌套的類型)。
-
-
屬性。
-
修飾類型和成員的其他說明性元素。
-
元數據的優點
對於一種更簡單的編程模型來說,元數據是關鍵,該模型不再需要接口定義語言 (IDL) 文件、頭文件或任何外部組件引用方法。元數據允許 .NET 語言自動以非特定語言的方式對其自身進行描述,而這是開發人員和用戶都無法看見的。另外,通過使用屬性,可以對元數據進行擴展。元數據具有以下主要優點:
-
自描述文件
公共語言運行庫模塊和程序集是自描述的。模塊的元數據包含與另一個模塊進行交互所需的全部信息。元數據自動提供 COM 中 IDL 的功能,允許將一個文件同時用於定義和實現。運行庫模塊和程序集甚至不需要向操作系統注冊。結果,運行庫使用的說明始終反映編譯文件中的實際代碼,從而提高應用程序的可靠性。
-
語言互用性和更簡單的基於組件的設計
元數據提供所有必需的有關已編譯代碼的信息,以供您從用不同語言編寫的 PE 文件中繼承類。您可以創建用任何托管語言(任何面向公共語言運行庫的語言)編寫的任何類的實例,而不用擔心顯式封送處理或使用自定義的互用代碼。
-
屬性
.NET Framework 允許您在編譯文件中聲明特定種類的元數據(稱為屬性)。在整個 .NET Framework 中到處都可以發現屬性的存在,屬性用於更精確地控制運行時您的程序如何工作。另外,您可以通過用戶定義的自定義屬性向 .NET Framework 文件發出您自己的自定義元數據。有關更多信息,請參見利用屬性擴展。
元數據和PE文件結構:
元數據存儲在 .NET Framework 可移植可執行文件 (PE) 文件的一個部分中,而 Microsoft 中間語言 (MSIL) 則存儲在 PE 文件的另一部分中。文件的元數據部分包含一系列的表和堆數據結構。MSIL 部分包含 MSIL 和引用 PE 文件元數據部分的元數據標記。當使用工具(例如,使用 MSIL 反匯編程序 (Ildasm.exe) 來查看代碼的 MSIL 或使用運行庫調試器 (Cordbg.exe) 來執行內存轉儲)時,您可能會遇到元數據標記。
元數據表和堆
每個元數據表都保留有關程序元素的信息。例如,一個元數據表說明代碼中的類,另一個元數據表說明字段等。如果您的代碼中有 10 個類,類表將有 10 行,每行一類。元數據表引用其他的表和堆。例如,類的元數據表引用方法表。
元數據還以四種堆結構存儲信息:字符串、Blob、用戶字符串和 GUID。所有用於對類型和成員進行命名的字符串都存儲在字符串堆中。例如,方法表不直接存儲特定方法的名稱,而是指向存儲在字符串堆中的方法的名稱。
元數據標記
元數據標記在 PE 文件的 MSIL 部分中唯一確定每個元數據表的每一行。元數據標記在概念上和指針相似,永久駐留在 MSIL 中,引用特定的元數據表。
元數據標記是一個四個字節的數字。最高位字節表示特定標記(方法、類型等)引用的元數據表。剩下的三個字節指定與所說明的編程元素對應的元數據表中的行。如果您用 C# 定義一個方法並將其編譯到 PE 文件,下面的元數據標記可能存在於 PE 文件的 MSIL 部分:
0x06000004 |
其中最高位字節 (0x06) 表示這是一個 MethodDef 標記。低位的三個字節 (000004) 指示公共語言運行庫在MethodDef 表的第四行查找對該方法定義進行描述的信息。
PE 文件中的元數據
當為公共語言運行庫編譯程序時,該程序轉換為由三部分組成的 PE 文件。下表說明了每部分的內容。
PE部分 |
PE 部分的內容 |
---|---|
PE 標頭 |
PE 文件主要部分的索引和入口點的地址。 運行庫使用該信息確定該文件為 PE 文件並確定當將程序加載到內存時執行從何處開始。 |
MSIL 指令 |
組成代碼的 Microsoft 中間語言指令 (MSIL)。許多 MSIL 指令帶有元數據標記。 |
元數據 |
元數據表和堆。運行庫使用該部分記錄您的代碼中每個類型和成員的信息。本部分還包括自定義屬性和安全性信息。 |
元數據在運行時的作用:
要更好地理解元數據和它在公共語言運行庫中的作用,構造一個簡單的程序並說明元數據如何影響它的運行時情況可能很有幫助。下面的代碼示例顯示名為 MyApp 的類中的兩種方法。Main 方法是程序入口點,而 Add 方法只返回兩個整數參數的和。
- using System;
- public class MyApp
- {
- public static int Main()
- {
- int ValueOne = 10;
- int ValueTwo = 20;
- Console.WriteLine("The Value is: {0}", Add(ValueOne, ValueTwo));
- return 0;
- }
- public static int Add(int One, int Two)
- {
- return (One + Two);
- }
- }
當代碼運行時,運行庫將模塊加載到內存並向元數據咨詢該類的信息。加載后,運行庫對方法的 Microsoft 中間語言 (MSIL) 流執行廣泛的分析,將其轉換為快速本機指令。運行庫根據需要使用實時 (JIT) 編譯器將 MSIL 指令轉換為本機代碼,每次轉換一個方法。
下面的示例顯示了從以前代碼的 Main 功能生成的部分 MSIL。您可以使用 MSIL 反匯編程序 (Ildasm.exe) 從任何 .NET Framework 應用程序中查看 MSIL 和元數據。
- .entrypoint
- .maxstack 3
- .locals ([0] int32 ValueOne,
- [1] int32 ValueTwo,
- [2] int32 V_2,
- [3] int32 V_3)
- IL_0000: ldc.i4.s 10
- IL_0002: stloc.0
- IL_0003: ldc.i4.s 20
- IL_0005: stloc.1
- IL_0006: ldstr "The Value is: {0}"
- IL_000b: ldloc.0
- IL_000c: ldloc.1
- IL_000d: call int32 ConsoleApplication.MyApp::Add(int32,int32) /* 06000003 */
JIT 編譯器讀取整個方法的 MSIL,對其進行徹底地分析,然后為該方法生成有效的本機指令。在 IL_000d 遇到Add 方法 (/*06000003 */) 的元數據標記,運行庫使用該標記參考 MethodDef 表的第三行。
下表顯示了說明 Add 方法的元數據標記所引用的 MethodDef 表的一部分。雖然程序集中存在其他元數據表並具有它們自己唯一的值,但這里只討論該表。
Row |
相對虛擬地址 (RVA) |
ImplFlags |
Flags |
Name (指向字符串堆。) |
Signature(指向 Blob 堆) |
---|---|---|---|---|---|
1 |
0x00002050 |
IL Managed |
Public ReuseSlot SpecialName RTSpecialName .ctor |
.ctor(構造函數) |
|
2 |
0x00002058 |
IL Managed |
Public Static ReuseSlot |
Main |
String |
3 |
0x0000208c |
IL Managed |
Public Static ReuseSlot |
Add |
int, int, int |
該表的每一列都包含有關代碼的重要信息。RVA 列允許運行庫計算定義該方法的 MSIL 的起始內存地址。ImplFlags 和 Flags 列包含說明該方法的位屏蔽(例如,該方法是公共的還是私有的)。Name 列對來自字符串堆的方法的名稱進行了索引。Signature 列對在 Blob 堆中的方法簽名的定義進行了索引。
運行庫在第三行的 RVA 列計算所需的偏移量地址並將該地址返回到 JIT 編譯器,然后,JIT 編譯器進入新地址。JIT 編譯器繼續在新地址處理 MSIL,直到它遇到另一個元數據標記,之后,重復該過程。
使用元數據,運行庫可以訪問加載代碼並將其處理為本機指令所需的所有信息。以這種方式,元數據使自描述文件、公共類型系統和跨語言繼承成為可能。