.NET 之路 | 007 详解 .NET 程序集
上一篇我們介紹了 Roslyn 編譯器,我們知道,我們編寫的 C#/VB 代碼經(jīng)過 Roslyn 編譯器編譯后會(huì)生成程序集文件。按照之前講的 .NET 執(zhí)行模型的順序,這一篇我具體講講程序集。
什么是程序集
我們編寫的 C# 代碼經(jīng)過編譯會(huì)生成 .dll 或 .exe 文件,這些文件就是 .NET 的程序集(Assembly)。
盡管 .NET 的程序集文件與非托管的 Windows 二進(jìn)制文件采用相同的文件擴(kuò)展名(*.dll),但它們的內(nèi)部完全不同。具體來說,.NET Core 程序集文件不包含平臺(tái)(泛指操作系統(tǒng)和 CPU 架構(gòu)的組合)特定的指令,而是平臺(tái)無關(guān)的中間語言(IL)和類型元數(shù)據(jù)。
你可能在一些 /.NET Core 的文檔中看到過 IL 的另外兩種縮寫:MSIL(Microsoft Intermediate Language,微軟中間語言) 和 CIL(Common Intermediate Language,通用中間語言)。IL、MSIL 和 CIL 都是一個(gè)概念,其中 MSIL 是早期的叫法,現(xiàn)在已經(jīng)很少有人用了。
但 .NET Core 與 .NET Framework 不一樣,.NET Core 始終只會(huì)生成 *.dll 格式的程序集文件,即使是像控制臺(tái)應(yīng)用這樣的可執(zhí)行項(xiàng)目也不再會(huì)生成 *.exe 格式的程序集文件。
那我們在 .NET Core 項(xiàng)目的 bin 目錄中看到和項(xiàng)目同名的 *.exe 文件是怎么回事呢?這個(gè)文件并不是一個(gè)程序集文件,而是專門為 Windows 平臺(tái)生成的一個(gè)可執(zhí)行的快捷方式。在 Windows 平臺(tái)雙擊這個(gè)文件等同于執(zhí)行 dotnet?.dll 命令。在我們安裝的 .NET Core 目錄中有個(gè) dotnet.exe 命令文件(如 Windows 系統(tǒng)默認(rèn)位置是C:\Program Files\dotnet\dotnet.exe),在編譯時(shí),該文件會(huì)被復(fù)制到構(gòu)建目錄,并重命名為與項(xiàng)目名稱同名的?.exe 文件。
程序集的組成
總的來說,每個(gè)程序集文件主要由 IL 代碼、元數(shù)據(jù)(Metadata)、清單(Manifest) 和資源文件(如 jpg、html、txt 等)組成。其中,IL 代碼和元數(shù)據(jù)會(huì)先被編譯為一個(gè)或多個(gè)托管模塊,然后托管模塊和資源文件會(huì)被合并成程序集。
托管模塊,或者叫托管資源或托管代碼,顧名思義,這種資源是由 .NET Core 的 CLR 運(yùn)行時(shí)來管理運(yùn)行的,它包含 IL 代碼和元數(shù)據(jù)。比如對象的回收是由 CLR 中垃圾回收器(GC)自動(dòng)執(zhí)行的,不需要手動(dòng)管理。
程序集文件中占比最大的一般是 IL 代碼。IL 代碼和 Java 字節(jié)碼相似,它不包含平臺(tái)特定的指令,它只在必要的時(shí)候被 .NET Core 運(yùn)行時(shí)中的 JIT 編譯器編譯成本機(jī)代碼(機(jī)器碼)。
程序集文件中的元數(shù)據(jù)詳細(xì)地描述了程序集文件中每個(gè)類型的特征。比如有一個(gè)名為 Product 的類,類型元數(shù)據(jù)描述了 Product 的基類、實(shí)現(xiàn)的接口(如果有的話)和每個(gè)成員的完整描述等細(xì)節(jié)。元數(shù)據(jù)由語言編譯器(Roslyn)自動(dòng)生成。
除了托管模塊,程序集文件還可以嵌入資源文件,如 jpg、gif、html 等格式的靜態(tài)文件,這些文件是非托管資源。
當(dāng)托管模塊和資源文件合并成程序集時(shí),會(huì)生成一份清單,它是專門用來描述程序集本身的元數(shù)據(jù)。清單包含程序集的當(dāng)前版本信息、本地化信息(用于本地化字符串等),以及正確執(zhí)行所需的所有外部引用程序集列表等。
在第 5 篇文章中我們講了 .NET 的兩種執(zhí)行模型,其中,當(dāng)基于本地運(yùn)行時(shí)執(zhí)行模型發(fā)布時(shí),雖然你的應(yīng)用程序可以發(fā)布為可直接執(zhí)行的單一文件,但這個(gè)單一的文件其實(shí)是多個(gè)文件的包裝。它包含了由 IL 代碼編譯成的本地代碼和 Native AOT 本地運(yùn)行時(shí)。你的代碼仍然在一個(gè)托管的容器中運(yùn)行,運(yùn)行時(shí)它的資源的管理和它作為多個(gè)文件發(fā)布是一樣的。
下面讓我們更詳細(xì)地了解一下 IL 代碼、元數(shù)據(jù)和程序集清單。
IL 代碼
我們先來看看下面這樣一段簡單的 C# 代碼被編譯成 IL 代碼會(huì)是什么樣子。C# 代碼如下:
class Calculator
{
public int Add(int num1,int num2)
{
return num1 + num2;
}
}
經(jīng)過編譯后,在項(xiàng)目的 bin\Debug 目錄會(huì)生成一個(gè)與項(xiàng)目名稱同名的 dll 程序集文件。我們使用 ildasm.exe 工具打開這個(gè)文件,定位到 Calculator 的 Add 方法,可以看到 Add 方法的 IL 代碼如下:
.method public hidebysig
instance int32 Add (
int32 num1,
int32 num2
) cil managed
{
// Code size 9 (0x9)
.maxstack 2
.locals init (
[0] int32
)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: add
IL_0004: stloc.0
IL_0005: br.s IL_0007
IL_0007: ldloc.0
IL_0008: ret
}
以我的安裝環(huán)境為例,你可以在這個(gè)位置找到 ildasm.exe 工具:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\ildasm.exe。為了使用方便,你可以把該工具配置到 Visual Studio 的外部工具中。
這就是 IL 代碼的樣子,如果使用 VB 或 F# 編寫相同的 Add 方法,它生成的 IL 代碼會(huì)是一樣的。關(guān)于 IL 代碼語法后面有機(jī)會(huì)再講,這里我們暫且不關(guān)心。
由于程序集中的 IL 代碼不是平臺(tái)特定的指令,所以 IL 代碼必須在使用前調(diào)用 JIT 編譯器進(jìn)行即時(shí)編譯,將其編譯成特定平臺(tái)(特定的操作系統(tǒng)和 CUP 架構(gòu),如 Linux x64)的本地代碼,才能在該平臺(tái)運(yùn)行起來。
.NET Core 運(yùn)行時(shí)會(huì)在 JIT 編譯過程中針對特定平臺(tái)再次進(jìn)行底層優(yōu)化。比如將 IL 代碼編譯成特定于某平臺(tái)的本地代碼時(shí),它會(huì)把平臺(tái)無關(guān)的代碼剔除。并且,它會(huì)以適合目標(biāo)操作系統(tǒng)的方式將編譯好的本地代碼緩存在內(nèi)存中,供以后使用,下次不需要重新編譯 IL 代碼。
元數(shù)據(jù)
除了 CIL 代碼外,.NET Core 程序集還包含完整、全面、細(xì)致的元數(shù)據(jù),它描述了程序集中定義的每個(gè)類型(如類、結(jié)構(gòu)、枚舉),以及每個(gè)類型的成員(如屬性、方法、事件),這些信息生成都由編譯器自動(dòng)完成的。
我們繼續(xù)使用 ildasm.exe 來看看 .NET Core 元數(shù)據(jù)具體長什么樣。以前面的代碼為例,選擇該程序集,依次點(diǎn)擊“視圖->元信息->顯示”,可以看到當(dāng)前程序集的所有元數(shù)據(jù)信息。我們可以在元數(shù)據(jù)信息中找到 Calculator 類的 Add 方法,它的元數(shù)據(jù)是這樣的:
TypeDef #2 (02000003)
-------------------------------------------------------
TypDefName: ConsoleApp.Calculator (02000003)
Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100000)
Extends : 0100000C [TypeRef] System.Object
Method #1 (06000003)
-------------------------------------------------------
MethodName: Add (06000003)
Flags : [Public] [HideBySig] [ReuseSlot] (00000086)
RVA : 0x00002090
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: I4
2 Arguments
Argument #1: I4
Argument #2: I4
2 Parameters
(1) ParamToken : (08000002) Name : num1 flags: [none] (00000000)
(2) ParamToken : (08000003) Name : num2 flags: [none] (00000000)
元數(shù)據(jù)會(huì)被 .NET Core 運(yùn)行時(shí)以及各種開發(fā)工具所使用。例如,Visual Studio 等工具所提供的智能提示功能就是通過讀取程序集的元數(shù)據(jù)而實(shí)現(xiàn)的。元數(shù)據(jù)也被各種對象瀏覽工具、調(diào)試工具和 C# 編譯器本身所使用。元數(shù)據(jù)是眾多 .NET Core 技術(shù)的支柱,比如反射、對象序列化等。
程序集清單
.NET Core 程序集還包含描述程序集本身的元數(shù)據(jù),我們稱之為清單。清單記錄了當(dāng)前程序集正常運(yùn)行所需的所有外部程序集、程序集的版本號(hào)、版權(quán)信息等等。與類型元數(shù)據(jù)一樣,生成程序集清單也是由編譯器的工作。
同樣地,還是以上面 Calculator 類所在項(xiàng)目為例,我們也來看看程序集清單長什么樣子。在 ildasm.exe 工具打開的程序集的目錄樹中,雙擊 MAINFEST 即可查看程序集的清單內(nèi)容:
.assembly extern Systemtime
{
lickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 5:0:0:0
}
.assembly extern System.Console
{
lickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 5:0:0:0
}
.assembly ConsoleApp
{
...
.custom instance void ... TargetFrameworkAttribute ...
.custom instance void ... AssemblyCompanyAttribute ...
...
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module ConsoleApp.dll
agebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00000001 // ILONLY
可以看到,程序集清單首先通過 .assembly extern 指令記錄了它所引用的外部程序集。接著是當(dāng)前程序集本身的信息,記錄了程序集本身的各種特征,如版本號(hào)、模塊名稱等。
提前編譯 IL 代碼
前面提到,IL 代碼需要先通過 JIT 編譯器編譯成特定平臺(tái)的本地代碼,才能在該平臺(tái)運(yùn)行。你可能會(huì)問,.NET 為什么要將源代碼編譯成 IL 代碼,而不直接編譯成特定平臺(tái)的本地代碼呢?
這樣做主要有兩個(gè)好處:一是語言整合,一套運(yùn)行時(shí)環(huán)境可以運(yùn)行多種語言編寫的程序,.NET 團(tuán)隊(duì)不用開發(fā)和維護(hù)多套運(yùn)行時(shí);二是平臺(tái)無關(guān),方便程序和庫的移植,編譯后的程序集可以發(fā)布到多個(gè)平臺(tái),而不用為不同的平臺(tái)發(fā)布特定的程序文件。雖然 IL 代碼帶來了可移植性等的好處,但需要以犧牲一點(diǎn)點(diǎn)啟動(dòng)時(shí)的性能作為代價(jià)。
一般我們的 Web 應(yīng)用程序最終只會(huì)部署在一種平臺(tái)(如 Linux x64),為了更快的啟動(dòng)性能,在啟動(dòng)時(shí),我們確實(shí)可以不需要中間語言編譯這個(gè)環(huán)節(jié),省去啟動(dòng)時(shí)的 JIT 編譯的時(shí)間。.NET Core 為我們提供了兩種方式把 IL 代碼提前編譯成特定平臺(tái)的本地代碼。
一種方式是使用 ReadyToRun 功能。.NET Core 運(yùn)行時(shí)(CoreCLR)中的一個(gè)叫做 CrossGen 的工具,它可以預(yù)先將 IL 代碼編譯成本地代碼。要使用這個(gè)功能,只需在程序發(fā)布的時(shí)候,選擇特定平臺(tái),在發(fā)布選項(xiàng)中勾選 Enable ReadyToRun compilation 即可。不過 ReadyToRun 功能目前只適用于 Windows 系統(tǒng)。
另一種方式是使用 .NET 5 新增加的 AOT 編譯功能。發(fā)布時(shí)選擇 Self-Contained 模式,發(fā)布后生成單個(gè)文件。AOT 編譯也是提前將 IL 代碼編譯成本地代碼,不同的是,它在發(fā)布時(shí)生成的單個(gè)文件還包含一個(gè)精簡版的本地運(yùn)行時(shí)。這點(diǎn)在第 5 篇文章講過,不再累述了。
這兩種方式都有弊端,第一種目前只適用于 Windows 系統(tǒng),第二種 Self-Contained 單個(gè)文件發(fā)布要比多文件發(fā)布大幾十 M。不過對于第一次啟動(dòng)慢那么一點(diǎn)點(diǎn)(可能甚至不到一秒的時(shí)間),大部分的 Web 應(yīng)用程序都是完全可以接受的。如果實(shí)在對啟動(dòng)時(shí)性能有嚴(yán)格的要求,也可以使用預(yù)熱的方案。
小結(jié)
本文介紹了程序集以及它的內(nèi)部組成:IL 代碼、元數(shù)據(jù)、資源文件和程序集清單。總的來說,程序集就是 .NET Core 在編譯后生成的 *.dll 文件,它包含托管模塊、資源文件和程序集清單,其中托管模塊由 IL 代碼和元數(shù)據(jù)組成。
需要強(qiáng)調(diào)的是,IL 代碼不包含特定平臺(tái)的指令,它只在需要的時(shí)候才會(huì)被 CoreCLR 運(yùn)行時(shí)中的 JIT 編譯器編譯成特定于平臺(tái)的本地代碼。
通過本文,相信大家對 .NET Core 程序集和它的內(nèi)部組成已經(jīng)有了一個(gè)整體的認(rèn)識(shí)。
?
總結(jié)
以上是生活随笔為你收集整理的.NET 之路 | 007 详解 .NET 程序集的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 1年将30PB数据迁移到Spark,eB
- 下一篇: .NET EFCore之增删改查