深入剖析.NET运行机制
深入剖析.NET運行機制
比較認同一個專業的說法,稱對象之間的調用為 “消息傳遞”,正如其描述“通過發送和接受消息”。為什么說其專業?因為對于單機開發,調用者(用戶)和被調用者(服務)處于同一臺計算機內存之中,CPU執行指令僅僅是根據地址取出內存數據進行處理。但實質上仍然是“消息傳遞”,而分布式應用通過socket實現不同協議間的遠程服務調用,更加直觀。
.NET的程序集相關知識
有一個知識點十分重要,這被一些新手所忽視,導致他們有時很難理解一些技術書籍的內存原理分析。就是程序從編譯到運行的過程。借此文,我來解釋一下,如有錯誤,望大家指出,一定要指出。
當我們依賴google或者CTRL+C、CTRL+V編寫完成代碼之后,點擊發布或生成,本機安裝的編譯器會對代碼文件進行分析編譯(C/C++針對特定CPU編譯過程發生在宿主服務器上)。針對與.NET,機器上安裝的CLR虛擬機,會讓其內置的對應語言的編譯器將代碼編譯為版本相符的MSIL語言,并且保存文件格式為dll,這個dll稱之為托管程序集,也是一個PE文件。
.PE文件一般包含以下幾個部分:
PE頭
Data Directory(數據目錄)
.text segment
.data segment
.rdata segment
.idata segment
.edata segment
....其他部分不再列出。“那么CLR的托管程序集是怎么樣的?我聽說會有一個CLR頭?還有元數據以及IL代碼?(CLR via C#)” 當然,這是準確無誤的,《CLR via C#》圣經讀物,怎會輕易出錯?
在windows XP之前的操作系統,這些section都被保存在 PE 文件中的.text segment中,并且.text 塊中還保存了“非托管的存根函數”。
什么意思呢?你可以理解為是一段在.net編譯時(非.net編譯可能是其他的存根代碼或者沒有存根代碼)加入的非托管代碼。而PE頭中的某一塊域(其實是 AddressOfEntryPoint 域)指向這段代碼,當windows加載器加載PE文件時(托管程序集),便會執行這段代碼,這段代碼會調用_CoreExeMain或_CoreDllMain的代碼(前者針對exe文件,后者針對dll文件),由于這兩個函數都是導入函數(依賴別的程序集),則windows加載包含此函數的mscoree.dll,再去找到 data directory (數據目錄,這個目錄也是在.net編譯時生成的)。data directory包含了CLR頭的位置和大小,在根據CLR頭的位置在.text塊中找到CLR頭,CLR頭包含了CLR的版本信息、程序集簽名等很多信息,根據這些信息,非托管存根代碼調用對應的CLR,并啟動CLR,注意:如果CLR已經啟動,則CLR直接加載程序集到內存。
但是XP以及之后的操作的系統,對windows加載器進行了升級,使其能夠自動識別.net程序集,無需通過“非托管存根函數”來調用“_CoreExeMain”或“_CoreDllMain”,而是直接根據CLR頭啟動CLR。那么,至此,.NET 的PE文件的編譯和加載啟動過程已經清晰了。接下來總結一下:
XP 之前的.net程序集PE文件:
| PE Header | 存儲了PE文件格式(exe還是dll),創建信息,entry point(入口點) data directory 位置信息等 | ? |
| data directory(數據目錄) | 包含了CLR頭的位置和大小 | 注意,這個是.net編譯時添加的塊 |
| .text | 包含了CLR頭,元數據,IL代碼以及 非托管存根函數 | 注意,這個非托管存根函數也是.net編譯時加入的 |
| .idata | 導入函數,即此PE運行依賴的其他程序集的函數 _CoreExeMain和_CoreDllMain函數便是從mscoree.dll導入的函數 | ? |
| .edata | 導出函數,即此PE公開的可對外服務的函數 | ? |
| .rdata | 常量以及只讀數據 | ? |
| ..... | 其他塊,不贅述 | ? |
XP以及之后操作系統.NET的PE文件:
| PE???Header | 存儲了PE文件格式(exe還是dll),創建信息等。如果程序集包含非托管代碼, PE頭還存儲非托管代碼的一些信息(entry point) | ? |
| CLR Header | 記錄了CLR版本,Main方法(如果有),還記錄了元數據、IL代碼 的位置和大小 | 注意:不在生成 data directory部分 |
| .text | 元數據,IL代碼 | 注意: 1.不在生成專門用于調用_CoreExeMain和_CoreDllMain函數 的非托管存根函數 2.CLR頭不在.text中,單獨被放置為一個部分。 3.元數據,IL代碼還是在.text 段中的,只是CLR via c#書中 為了更好說明,將其提取出來了。 |
| .idata | 導入函數,即此PE運行依賴的其他程序集的函數 _CoreExeMain和_CoreDllMain函數便是從mscoree.dll導入的函數 | ? |
| .edata | 導出函數,即此PE公開的可對外服務的函數 | ? |
| .rdata | 常量以及只讀數據 | ? |
| ..... | 其他塊,不贅述 | ? |
CLR頭是用來啟動CLR,那么元數據和IL代碼是用來干什么的呢?這里不再詳述,比較麻煩,建議閱讀《CLR via C#》。我這里只提供書中的一幅圖:
從這幅圖中可以看出,元數據其中之一用途就是當IL代碼再被JIT(即時編譯)時,如果IL引用到了一個類型則會根據元數據去正確的創建類型對象。 當然,還有十分重要的一個作用就是:為開發人員提供反射機制。
應用程序域 應用程序域的作用,我想大家都應該知曉,主要是為了隔離代碼影響。比如IIS中啟動多個web應用,每個應用的錯誤不會影響到其他站點。這就是應用程序域的典型案例。 當CLR被啟動后,首先會創建一個系統級別應用程序域。該程序域的創建由CLR主導,對開發者完全透明,系統應用程序域的主要功能:
- 1)創建其他兩個應用程序域(共享應用程序域和默認應用程序域)。
- 2)將mscorlib.dll加載到共享應用程序域中(在下面將進一步討論)。
- 3)記錄進程中所有其他的應用程序域,包括提供加載/卸載應用程序域等功能。
- 4)記錄字符串池中的字符串常量,因此允許任意字符串在每個進程中都存在一個副本。
- 5)初始化特定類型的異常,例如內存耗盡異常,棧溢出異常以及執行引擎異常等。
我們注意到,系統應用程序域會自動創建共享應用程序域和默認應用程序域,這對于開發人員也是透明的,也就是說一個.net程序一旦啟動,就會有三個應用程序域。 系統應用程序域的作用已經說明了,而共享應用程序域主要是用于加載一些與 默認應用程序域無關 的代碼,即“非用戶代碼”,比如mscoree.dll,一些system命名空間的類型等,由其名稱可知,其內部加載的成程序集可被所有應用程序域訪問。 默認應用程序域就是我們代碼(用戶代碼)執行的地方,開發者還可以通過系統應用程序域創建多個應用程序域,但往往很少有這種需求。
內存工作方式
上面說完了程序集的編譯以及加載原理,下面來主要說一下.net程序內存的工作方式。
現在假設我們的程序已經編譯OK,并且被保存再磁盤上的一處位置。我們準備雙擊運行它,首先這必須是一個exe文件,否則,無法直接運行。隨后,這個exe文件被加載到內存的代碼區,(這里也說明一下,程序集盡量實現單一職責,否則加載了一個很大的程序集卻只用小部分功能,簡直是浪費內存!)當exe文件被加載到內存代碼區后,windows加載器開始加載CLR,而后,CLR啟動后根據exe文件的CLR頭信息中的Main方法入口,從而進入用戶代碼。從Main方法開始,IL代碼就被不斷取出交給JIT即時編譯成機器指令,從而達到功能實現。
OK,有人會問,所有的代碼都會一次性交給IL進行即時編譯嗎?No!面向對象只是我們進行業務設計時更易建模領域模型,對于計算機,它永遠只知道,你讓它做什么,他就去做什么。絕不會多做任何一點兒。所以,一切都是面向函數!函數即指令!一個dll,加載到內存,沒有被調用,沒有main方法,它則永不會被JIT!當然我們只是舉例,沒有被調用,也根本不可能被加載到內存! 而Main方法就是所有程序的入口函數,Main方法其內部的所有代碼都會被JIT(Main方法所在線程即主線程)。 如下文件:
class Program{static void main(string[] args){Console.WeiteLine("Test");int a=1;int b=2;int c=add(a,b);Console.WriteLine(c);} static int add(int a,int b){return a+b;}}當CLR調用Main方法時,首先將Main方法的IL指令交給JIT翻譯為機器指令,此時,發現Main方法屬于Program類型,且是靜態方法,便會在內存堆上分配空間,根據元數據中的TypeDef創建類型內部結構,即Type對象。注意:非program對象,因為并沒有new指令。然后將Main方法的IL指令翻譯為CPU指令,緊接著進入Main方法內部的其他IL代碼,從而正式開始應用程序主線程的運行。內存分配圖如下:
1.CLR調用Main方法;2.JIT到代碼區查找元數據和IL代碼3.JIT根據找到的信息編譯出CPU指令,static構造函數和main函數4.編譯后的Main函數開始執行,進入應用程序主線程5.調用add方法;(不應該指向 JIT?待思考)6.JIT將add方法的IL代碼翻譯為CPU指令并執行。注意:JIT有非常強大的緩存功能,也就是說相同的代碼在同一應用程序域不會被JIT第二次,而是使用之前JIT出的CPU指令
總結
本文主要針對.NET的運行原理進行了分析,但運行在虛擬機之上的語言的編一個運行過程大致相同,可能由于一些語言的性質不同,會做一些編譯優化,比如F#函數式語言,scala多范式語言等。特別強調:本文為作者個人多年工作經驗和書籍閱讀帶來的個人理解,并通過博文進行分享,有一個目的是希望有更加清晰明白的人能指出其中的不足或錯誤,以免誤人子弟,同時學習進步。
有什么疑問,請留言,我們一起討論,謝謝!
參考:
《CLR via c#》
《.NET高級調試》
?
轉載于:https://www.cnblogs.com/phoozyan/p/4998727.html
總結
以上是生活随笔為你收集整理的深入剖析.NET运行机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 缺陷漏测分析:测试过程改进
- 下一篇: 程序员有趣的面试智力题(转)