接上篇:你的C#代碼是怎么跑起來的(一)
通過上篇文章知道了EXE文件的結構,現在來看看雙擊后是怎樣運行的:

雙擊文件后OS Loader加載PE文件並解析,在PE Optional Header里找到基地址和RVA,通過這兩個確定了程序的入口地址,這個地址指向MsCorEE.dll的_CorExeMain(),執行它。_CorExeMain()開始執行,選擇加載合適版本的CLR,CLR開始運行,CLR運行時會分配一個連續的地址空間用作托管堆,並用一個指針NextObjPtr指到開始位置,下次分配內存時就從指針指的位置開始。
CLR運行后從CLR頭里找到應用程序入口標識,也就是Main()方法的MethodDefToken,通過這個標識在元數據表MethodDef里找到Main方法的偏移位置,這樣就可以找到Main()的IL代碼。
CLR檢查Main方法里面是否有沒加載的類型,沒有的話就加載進來並在托管堆上建一個類型對象,類型對象包含靜態字段,方法,基類的引用。然后給類型的方法表里每個方法一個存根,存根是用於標識是否被JIT編譯過。
JIT: just-in-time Compiler,即時編譯器。
JIT編譯之前CLR會對Main方法的代碼進行驗證,確保類型安全且元數據正確,一切沒問題后先檢查類型方法表里這個方法的存根,不為空的話表示已經編譯過就不需要再次編譯,沒有的話JIT把這段IL代碼編譯成本地代碼保存到內存中並方法表的存根做上標記,然后JIT返回編譯前的位置並把原來CLR指向JIT的地址修改為指向本地代碼的地址,這樣函數的本地代碼開始執行。程序執行到哪里就編譯到哪里,沒有執行到的就不會加載和編譯,同樣的代碼再次執行的話就直接在內存里拿了,這也是為什么第一次運行C#時比較慢而后面就快的原因。這樣就開始陸續執行所有的代碼,程序也就跑起來了。
在內存上,運行線程會把函數的參數和局部變量壓入線程棧上,棧上的空間默認是1M,方法的參數和局部變量都會壓到函數的棧幀上,方法里的對象在托管堆NextObjPtr指向的位置分配內存並把內存地址存到棧上的局部變量里。CLR會給托管堆上的每個對象包括對象類型都添加兩個字段,一個對象類型指針,一個同步塊索引。
說起棧幀,大家在調試代碼時應該都喜歡用CallStack吧,這可以通過看調用棧很方便來定位出問題的具體原因,這個CallStack也就是方法的棧幀的具體顯示,一級一級的。
對象類型指針從字面上就很容易知道跟類型有關。CLR剛開始運行時就分配了一個Type的對象類型,他的對象類型指針指向自己,后面創建的對象類型的對象類型指針指針就指向這個Type,而new出來的對象的對象類型指針就指向它的類型,這樣所有對象都能找到自己的類型使CLR在運行時能確保類型安全。
同步塊索引的格式是前6個標志位加后面26位內容(32位系統),作用則有好幾個。
1. 調用對象的gethashcode()后標志位改變一位,后26位會存儲對象的hashcode,保證對象生命周期內hashcode的唯一;
2. lock時用到,CLR會維護一個同步塊數組,每項由一個指向同步塊的指針和對象指針組成,lock時同樣改變標識位,然后去同步塊數組找一個閑置項,后26則變成這項在數組中的索引,有人要問了,剛才hashcode不是用了這26位嗎,現在變了,hashcode豈不是丟了。確實,hashcode在lock之后不能直接存到索引了,不過同步塊中專門准備了一個字段用來存hashcode,所以可以轉移到同步塊中,這樣設計是為了節省內存,因為大部分情況下是不用lock的,也就不需要增加多余的同步塊。
另外為什么是索引而不是地址呢,因為同步塊數組的大小不是固定的,隨着對象的增多而變大,在內存上的位置可能會發生變化,所以用索引就不用管數組在哪個位置了。
當線程進入lock后檢查同步塊的m_motion,發現沒有標識則進入lock區域並把標識改變,如果已經有同一個線程進去則把計數器加1,如果已經有其他線程則等待。
3. 垃圾回收時的標識,GC觸發時首先認為所有的對象都是垃圾,由局部變量,寄存器,靜態變量這些根向上找,凡是包含的對象都認為還有引用,在同步塊索引上修改一位標識,當所有對象都遍歷過后沒有標識的對象就會被清掉,然后再是整理內存、修改引用地址等。
看個簡單的例子,只用於演示,不考慮合理性:
1 using System; 2 3 namespace Test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int height = 170; 10 int weight = 60; 11 People.Find(); 12 People developer = new Developer()(height, weight); 13 bool isHealthyWeight = developer.IsHealthyWeight(); 14 bool isRich = developer.IsRich(); 15 } 16 } 17 18 class People 19 { 20 int _height; 21 int _weight; 22 23 public People(int height, int weight) 24 { 25 _height = height; 26 _weight = weight; 27 } 28 29 public virtual bool IsRich(); 30 31 public bool IsHealthyWeight() 32 { 33 var healthyWeight = (Height - 80) * 0.7; 34 return Weight <= healthyWeight * 1.1 && Weight >= healthyWeight * 0.9; 35 } 36 37 public static string Find(string id) { return ""; } 38 } 39 40 class Developer : People 41 { 42 public Developer(int height, int weight) : base(height, weight) 43 { } 44 45 public override bool IsRich() 46 { 47 return false; 48 } 49 } 50 51 }

*圖片不清楚可以放大看
首先判斷類型是否都加載,用到了int,bool,string,這些是在mscorlib.dll程序集的system命名空間下,所以先加載mscorlib.dll程序集,再把int,bool,string加到類型對象里。另外還有我們自己定義的Developer和People,也把類型對象創建好,另外也別忘了基類object,也要加載進來。(實際上還有double啊,這里就沒畫了)另外繼承類的類型對象里面都有個字段指向基類,所以才能往上執行到基類方法表里的方法。
局部變量都在線程棧上,Find()方法是靜態方法,直接去People類型對象的方法表里去找,找到后看是否有存根標識,沒有的話做JIT編譯,有的話直接運行。
developer的實例化雖然是用People定義的,但實例還是Developer,所以developer的類型對象指針指向Developer,對象里除了類型對象指針還有實例字段,包括基類的。內存分配在托管堆上,並把地址給到線程棧上的變量中。
虛函數也一樣,在運行時已經確定是Developer,所以會調用Developer方法表里的IsRich方法,一樣先JIT,再運行。
以上就是一個簡單的C#程序的運行過程和在內存上的表現,本篇主要內容來自CLR via C#這本書,小弟算是總結一下,謝謝觀看。
