- .NET安全攻防指南(上冊)
- 李寅 莫書棋
- 6字
- 2025-06-11 10:22:26
2.2 .NET基礎知識
2.2.1 基本概念
1.公共類型系統
公共類型系統(Common Type System,CTS)是一個正式的規范,完整地描述了CLR所支持的所有數據類型和編程結構,指定了這些實體之間如何交互,并規定了它們在.NET元數據中如何表示。通常只有那些設計.NET平臺工具或者開發編譯器的人員才對CTS的內部工作非常關心,但是.NET編程人員必須了解CTS定義的常用類型,具體如表2-2所示。
表2-2 CTS定義的5種常用類型

另外,不同的語言用于聲明內建CTS數據類型的關鍵字一般是不同的,但是所有語言的關鍵字最終都將解釋成定義在mscorlib.dll程序集中的相同類型。
2.公共語言運行時
公共語言運行時(CLR)專為.NET Framework提供托管運行環境。我們開發的.NET程序都是基于CLR的類庫實現的,并運行在CLR的引擎之上,因此通常所說的.NET框架就是CLR。其主要作用是系統調用、內存管理、程序編譯啟動或停止、線程管理等,可以被支持.NET的所有語言和平臺共享。
(1)CLR版本
CLR是.NET Framework的子集,但是兩者的版本策略不同。截至2019年,微軟發布了4個版本的CLR,兩者對應關系如表2-3所示。
表2-3 CLR和.NET Framework兩者對應關系

因此,使用ASP.NET Web Form開發的應用程序,在部署到IIS服務器時,不同的CLR版本需要選擇不同的托管管道模式,如圖2-5所示。

圖2-5 選擇IIS應用程序池中不同的CLR版本
(2)CLR初始化加載
由于CLR是托管環境,因此運行時中的多個組件需要在執行任何代碼之前進行初始化。初始化時有60件以上的事情需要CLR幫助我們完成,60只是一個粗略的統計,具體的事件數量取決于當前系統使用的.NET運行時版本以及啟用了哪些功能。
以最簡單的控制臺程序為例,將“Hello World!”在控制臺窗口進行打印,代碼如下所示。

將上述.NET代碼生成可執行文件,運行時控制權會交由EE執行引擎,這個引擎由ceemain.cpp文件進行編譯,最終會通過EEStartupHelper()方法啟動執行。為了使它們更容易理解,我們將它們分為5個不同的階段,下面分別進行說明。
1)第一個階段主要對CLR加載時做基礎設施設置,如表2-4所示。
表2-4 CLR加載基礎設施設置

2)第二個階段主要對CLR核心和底層的組件做初始化配置,如表2-5所示。
表2-5 CLR加載核心組件

3)第三個階段CLR開始啟動錯誤處理、分析API等功能,如表2-6所示。
表2-6 CLR啟動錯誤處理等功能

(續)

4)第四個階段CLR開始啟動垃圾回收、AppDomain等功能,如表2-7所示。
表2-7 CLR啟動AppDomian和垃圾回收等功能

5)第五個階段EE啟動后開啟通知其他組件等功能,如表2-8所示。
表2-8 EE啟動后通知其他組件

3.基礎類庫
基礎類庫(BCL)是由.NET平臺提供的,適用于全部.NET程序語言,封裝了線程、文件I/O、圖形繪制、硬件交互及其他應用服務等。比如,常見的命名空間System.*也屬于BCL。
BCL定義了一些可以創建任意類型應用軟件的基礎能力,如使用ASP.NET創建Web應用,使用WCF創建網絡通信服務,使用Windows Form/WPF創建桌面GUI應用,使用ADO.NET與關系數據庫交互、XML操作、文件系統交互等。
4.公共語言規范
公共語言規范(Common Language Specification,CLS)是一套規則,描述了.NET的編譯器必須支持的最小的和完全的特征集,以生成可由CLR承載的代碼,同時可以被所有.NET語言用統一的方式進行訪問。CLS和.NET語言之間的關系如圖2-6所示。

圖2-6 CLS和.NET語言之間的關系
CLS可以看成CTS所定義完整功能的一個子集。.NET中可以使用特性來讓編譯器檢查代碼是否遵循CLS規則,代碼如下所示。

Add方法不遵循CLS,聲明時使用了無符號數unit,不符合CLS約束,因為某些.NET語言不支持無符號數unit。而第二個Sub方法遵循CLS,只在方法內部使用了無符號數(uint)x,并未在方法聲明時使用unit。
5.通用中間語言
通用中間語言(Common Intermediate Language,CIL)簡稱MSIL或IL,它運行于CLR之上,支持C#、Visual Basic.NET等托管的編程語言,當編譯器構建.NET程序集時就會把源碼翻譯成CIL,這樣可有效地轉換為本機代碼且獨立于CPU的指令。
CIL由一組CIL指令、CIL特性、CIL操作碼構成,下面分別對它們展開詳細介紹。
(1)CIL指令
CIL指令是用于描述.NET程序集總體結構的標記,并且通知CIL編譯器如何定義在程序集中用到的命名空間、類、成員。
以一個點號開頭,如.namespace、.class、.property、.method、.assembly等,具體說明如表2-9所示。
表2-9 CIL指令

(2)CIL特性
CIL特性是在CIL指令并不能完全說明.NET成員和類的特性的情況下,對CIL指令進行補充說明的。比如一個自定義類聲明是公共的,繼承自某個父類,這時就需要用public、extends或implements特性對類的.class指令進行補充說明。圖2-7所示是一個.class指令的特性。

圖2-7 .class指令特性
對于.class指令使用的特性,其使用說明如表2-10所示。
表2-10 .class指令

(3)CIL操作碼
CIL操作碼是對類或方法的內部邏輯進行描述和操作的代碼,比如Add操作碼表示將兩個值相加并將結果推送到堆棧中。Main方法操作碼如圖2-8所示。

圖2-8 Main方法操作碼
CIL操作碼有很多,圖2-8中使用到的操作碼也是常見的,關于更多操作碼的詳細說明如表2-11所示。
表2-11 CIL操作碼

(續)

6.Emit動態生成
.NET可以由VB、C#等語言進行編寫,這些語言會被不同的編譯器解釋為IL代碼并執行,而Emit類庫的作用就是用這些語言來編寫生成IL,并交給CLR進行執行。
.NET編譯后的每個.dll或.exe文件稱為程序集(Assembly),而在一個程序集中內部包含和定義了許多命名空間,這些命名空間被稱為模塊(Module),而模塊正是由一個個類型(Type)組成,如圖2-9所示。

圖2-9 程序集的組成
所以我們必須先定義Assembly、Module、Type才能進行下一步工作,在Emit中所有創建類型均以Builder結尾,如表2-12所示。
表2-12 程序集和Emit對比

(1)AssemblyBuilder和ModuleBuilder
由于創建程序集需要從Assembly開始創建,所以入口是AssemblyBuilder,而ModuleBuilder用于創建程序集中的模塊。通過這兩者可以動態生成包含類型和方法的程序集,代碼如下所示。

(2)TypeBuilder
TypeBuilder用于創建動態類型。可以使用DefineType方法來定義類型,并指定其名稱、基類、接口等信息,代碼如下所示。

(3)MethodBuilder
MethodBuilder用于創建動態方法。通過DefineMethod方法定義方法,然后使用GetIL-Generator獲取IL生成器,向方法中插入如下IL代碼。

下面是一段通過Emit動態技術實現控制臺輸出Hello World的完整示例,代碼如下:

首先需要引入類庫的命名空間System.Reflection.Emit,接著向IL生成器插入相應的操作碼,這些操作碼代表MSIL中的不同指令。比如使用OpCodes.Ldstr向堆棧壓入字符串Hello World!,OpCodes.Call調用Console.WriteLine方法,最后通過調用CreateType和CreateDelegate,可以將動態生成的類型轉換為委托,從而在運行時執行動態生成的代碼,啟動后控制臺輸出Hello World!,如圖2-10所示。

圖2-10 Emit動態編譯
7.即時編譯
即時(JIT)編譯是一種執行計算機代碼的方式,在程序運行時而不是在執行之前進行編譯,JIT也是CLR的一部分,編譯器負責加快代碼執行速度,并提供對多平臺的支持,工作原理如圖2-11所示。

圖2-11 即時編譯的工作原理
從圖2-11中可知,基于C#、VB.NET、F#開發的托管文件或.dll文件都不是本地代碼,不能像C或C++編寫的代碼那樣直接運行在CPU平臺上,因此啟動托管PE文件都會被JIT編譯成本地代碼運行。
由于即時編譯面臨性能損耗的問題,于是微軟又提供了預編譯方式,簡稱Pre-JIT,在.NET Framework中使用本機圖像生成器Ngen.exe將整個源代碼直接轉換為本地代碼,這樣就可以從緩存中使用本機代碼,而不是調用JIT編譯器。預編譯的工作原理如圖2-12所示。

圖2-12 預編譯的工作原理
這些方法在第一次調用時被編譯后存儲在緩存中,當再次調用相同的方法時,將使用緩存中的編譯代碼來執行,因此加快了執行速度。
與.NET Framework不同的是,.NET Core提供了一個叫作ReadToRun的功能,它可以預先將IL代碼編譯成本地代碼。要使用這個功能,只需在程序發布的時候執行CIL命令:dotnet publish -c Release -r win-x64 -p:PublishReadToRun=true,本質上ReadToRun也是AOT的一種形式。
另一種方式是使用.NET 5新增的AOT編譯功能,AOT編譯也是提前將IL代碼編譯成本地代碼,不同的是在發布時生成的單個文件還包含一個精簡版的本地運行時。
JIT編譯器的優點在于使用的內存較少,因為JIT編譯器僅將運行時所需的方法編譯為機器代碼。缺點是對性能的損耗,當龐大的應用程序最初執行時,JIT編譯器需要更多的啟動時間。