對於 .NET 開發者,IL 碼只是中間產物,真正在 CPU 上跑的是即時編譯器(JIT)產出的原生機器碼。本文實戰演示如何使用 WinDbg 搭配 SOS 擴充套件,穿透 CLR 抽象層,在執行期捕獲特定托管方法(TestMethod)的 x86 組合語言指令,並見證 JIT 編譯前後方法入口點的動態變化。
TestAsm.exe(Debug 模式編譯,並以 [MethodImpl(MethodImplOptions.NoInlining)] 防止內聯)。SOS.dll(對應 .NET Framework 版本)。Console.ReadLine() 讓程式暫停,提供附加偵錯的時間窗口。0:003> .load C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll
0:003> !name2ee *!TestAsm.TestClass
... MethodTable: 0094306c
0:003> !dumpmt -md 0094306c
... MethodDesc Table
Entry MethodDesc JIT Name
0094c040 00943060 NONE TestAsm.TestClass.TestMethod(Int32) ; 尚未 JIT
此時 !u 0094c040 顯示的是導向 JIT 編譯器的 Precode Stub(call mscorwks!PrecodeFixupThunk),而非真正的方法機器碼。
在 WinDbg 中按 F5 讓程式繼續,於主控台按 Enter 觸發第一次呼叫,再按 Ctrl+Break 中斷回偵錯器。
0:003> !dumpmt -md 0094306c
... 01a100d8 00943060 JIT TestAsm.TestClass.TestMethod(Int32) ; 已 JIT,入口點變更
0:003> !u 01a100d8
Normal JIT generated code
TestAsm.TestClass.TestMethod(Int32)
Begin 01a100d8, size 2d
01a100d8 55 push ebp
01a100d9 8bec mov ebp,esp
01a100db 83ec08 sub esp,8
01a100de 894dfc mov dword ptr [ebp-4],ecx ; 參數 i 存入區域變數
...
01a100f5 8b45fc mov eax,dword ptr [ebp-4] ; 返回值放入 EAX
01a10103 5d pop ebp
01a10104 c3 ret
註:Debug 模式下包含堆疊框架建立、JustMyCode 檢查等除錯輔助指令,Release 模式會精簡許多。
檢視呼叫端 Main 方法的組合語言碼發現:
01a1009d ff1568309400 call dword ptr ds:[943068h] ; 間接呼叫
指令並非直接 call 01a100d8,而是 call [943068h]。查詢該記憶體位址:
0:003> dd 943068h
00943068 01a100d8 ... ; 儲存著 TestMethod 當前的實際入口點
結論:CLR 使用「間接呼叫 + 入口點表」機制。JIT 編譯完成後,CLR 更新表格中對應的槽位,所有呼叫端無需修改自身機器碼,下次呼叫自動導向新編譯的原生碼。這就是 JIT 編譯能透明運作的關鍵。
ngen.exe install TestAsm.exe 後,方法呈現 PreJIT 狀態,偵錯時可直接檢視原生碼,無需等待執行期 JIT。