從編程開發的角度來簡單來說,CLR就相當於“執行/運行”我們所編寫程式的“環境/服務”。這就好比如我們組裝了一個賽車,我們的賽車需要依賴“跑道”作為一個環境,賽車才能進行飛馳。而這個“跑道”就類似於CLR。在Java平臺中程式員要向一臺電腦部署軟體時,要確保軟體運行,電腦上就要按照JVM(Java虛 ...
從編程開發的角度來簡單來說,CLR就相當於“執行/運行”我們所編寫程式的“環境/服務”。這就好比如我們組裝了一個賽車,我們的賽車需要依賴“跑道”作為一個環境,賽車才能進行飛馳。而這個“跑道”就類似於CLR。在Java平臺中程式員要向一臺電腦部署軟體時,要確保軟體運行,電腦上就要按照JVM(Java虛擬機),那麼這裡的“Java虛擬機”就相當於我們.NET平臺的CLR(Common Language Runtime,公共語言運行時)。
對CLR整體概況來說,CLR是一個軟體層面的代理服務,它負責管理.NET程式集的執行/運行,其中主要包括:
- 管理應用程式域
- 載入和運行程式集
- 安全檢查
- 線程管理
- 將CIL代碼即時編譯為機器代碼
- 異常處理機制
- 對象析構
- 垃圾回收
註意,這些執行項目並非發生在編譯時,而是發生在程式運行的過程中,也因此名稱中包含了“運行時”一說。
CLR是.NET框架中最核心的部分,從上述的講解中我們只能對CLR有個簡單的印象,通過幾句話或者某些概念也並非能夠對CLR有一個深刻的理解,如果你想要牢記它,就必須知道它的作用以及它的工作流程,接下來開始隨著本文開始更進一步的學習。
1.讀取程式集
CLR在對CIL語言代碼進行編譯前,需要先將編譯的環境運行起來並對程式集進行一個讀取過程,這個讀取過程其實也就描述出了程式集中的一個基本構造。
讀取過程如下:
1.1.文件格式
操作系統首先檢查文件是否是一個可執行文件,對於Windows操作系統而言可執行文件就是:“PE/COFF(windows可移植可執行/通用對象文件格式)”。在檢查完文件是否是可執行文件後,操作系統會在檢查文件格式中是否包含“CLR頭”,如果存在“CLR頭”,那麼操作系統就知道該文件是一個.NET程式集,需要區別於其他類型的可執行程式。
1.2.清單
在讀取到“CLR頭”確認文件是一個程式集之後,會讀取程式集中包含的清單(manifest)。這個清單相當於一個目錄,描述了程式集自身的基本信息。這其中包括:程式集的標識(名稱和版本)、描述構成程式集的所有文件、程式集所有外部依賴項的列表等。
1.3.元數據
清單之後就是元數據,在.NET中查看元數據的過程也叫做反射。如果說清單描述了程式集自身的信息,那麼元數據則描述了程式集包含了哪些內容項。這些內容項包括:程式集包含的模塊、類型、類型的成員、類型和類型成員的可見性等。另外要強調的是,元數據並不包含其中內容項的具體實現。
1.4.CIL和資源
在接下來就是:已經被編譯器轉換的CIL代碼和一個資源文件。CIL代碼也就是元數據中所有類型的具體實現,例如包括:方法體、欄位等。資源文件代表程式集所嵌套包含的文件,其中會包含一些如:jpg、xml、txt等文件。
2.非托管的CLR
托管代碼之所以被稱為托管代碼,是因為它是由CLR進行托管的,所以間接的可以猜測到CLR肯定不是托管代碼編寫的。這就是好比如警察負責捉小偷,總不能找個小偷去捉小偷吧。因為CLR本身是用於管理托管代碼的,因此它是由非托管代碼C++編寫的。另外我們可以通過命令行工具在本機上查看對應CLR運行時的版本,如圖:
3.CLR運行工作流程
操作系統首先會檢查程式集文件中是否包含PE頭和CLR頭,如果存在則會載入mscoree.dll組件,mscoree.dll是CLR中最重要的一個組件,又稱公共對象運行庫執行引擎。它在載入之後,會調用它其中的_CorExeMain函數,該函數會根據程式集而載入合適版本的CLR。
在CLR運行之後,程式的執行權就叫給了CLR。CLR會找到程式入口點,通常是Main()方法,然後執行它。這個執行過程包含以下內容:
3.1.載入類型 Class loader(類載入程式)
在執行“程式入口點”之前,CLR首先要找到“程式入口點”的所在的類型並且載入這個類型。
CLR中一個名為Class loader(類載入程式)的組件負責這項工作。它會從GAC、配置文件、程式集元數據中尋找這個類型,然後將它的類型信息載入到記憶體中。在Class loader找到並載入完這個類型之後,它的類型信息會被緩存起來,這樣就無需再次進行相同的過程。在載入這個類以後,還會為它的每個函數插入一個存根。
另外,在類型載入是時候,CLR不光會載入我們自定義的程式集,這其中還會與.NET的基礎類庫中的類型進行交互。也就是對mscorlib.dll這個程式集進行類型載入,這個程式集封裝了各種我們常用的編程類庫和核心的數據類型。
3.2.驗證
在CLR中,還存在一個驗證程式(verfier),該驗證程式的工作是運行時確保代碼是類型安全的。註意驗證兩個方面,一個是元數據是否正確,另一個是CIL代碼必須是類型安全的,類型的簽名必須正確。例如,代碼不允許以允許記憶體位置溢出的方式訪問對象的欄位、驗證檢查代碼以確定是否已正確生成 MSIL等等。
3.3.即時編譯JIT
運行CIL (公共中間語言/中間代碼) 前,必須通過CLR(公共語言運行時)將其編譯為目標電腦基礎結構的本機代碼,這個編譯就是由CLR中的即時編譯器(JIT)來完成的。
即時編譯只有在函數的第一次調用時發生。因為在Class loader(類載入程式)執行的時候會為每個函數插入一個存根。那麼在調用函數時,CLR會檢查函數對應的存根,如果沒有相應的存根,則JIT才會執行編譯,並將該函數被編譯後的本地機器代碼地址寫入到函數對應的存根中。當第二次對同一函數進行調用時,會再次檢查這個存根,如果發現其保存了本地機器代碼的地址,則直接跳轉到本地機器代碼進行執行,而無需再次進行JIT編譯。
另外基於這一現象,我門也從側面知道函數存根中存儲的就是,函數對應的本地機器代碼的地址。
通過結合上面文字描述後,我們將其整理繪製出,CLR運行整體工作流程圖:
總結
在實際的生活當中,往往工作了幾年的程式員都沒對.NET框架有一個基本的認識,有的時候還會在面試中被這個方面的問題“卡過喉嚨”。而我個人認為作為一個合格的.Net程式員,理解.NET框架是最基本的素質之一,這也是為了走向更遠的奠基石。
如果要深入瞭解更多關於CLR部分的內容,可以瀏覽下方官方地址:
https://docs.microsoft.com/zh-cn/dotnet/standard/clr
知識改變命運