程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 匯編語言 >> 使用WinDbg獲得托管方法的匯編代碼

使用WinDbg獲得托管方法的匯編代碼

編輯:匯編語言

這是一個沒有多大價值的小實驗,對於大家了解.NET編程等方面幾乎沒有任何 好處,盡管老趙一直強調“基礎”,例如扎實的算法和數據結構能力,並且對一 些必要的支持,例如操作系統,計算機體系結構,計算機網絡有足夠的了解,擁 有“常識”,在需要的時候有足夠的能力去深入了解便可;但是對於還有一些科 目,例如“編譯原理”,它雖然可以加強對於一個人對程序的理解,但是我也並 不覺得這是一條“必經之路”。了解黑盒內部肯定是有好處的,但是是否值得學 習還要進行權衡,至少要考慮(1)了解這些對於一個人究竟好處有多大,是否真 那麼關鍵;(2)同樣了解這些知識,需要了解到多深,是否我們走的是了解這些 的“必經之路”。同樣,對於那種動辄一個問題就深入“IL”,“系統底層”的 做法,老趙對此持保留態度1。當然,對於親手進行一番嘗試和探索 的做法,我總是支持的,這表明了一種嚴謹的治學態度——但是,前提是我們並 不是“以此為榮”而去搞這些(老趙也一直強調,誰說搞應用層的技術含量就比 搞所謂“底層”要差了),在搞這些之前也已經有必要的根基。我們是為了探索 而去研究,不是為了研究而去研究。

有時候,我們需要查看一個托管方法的匯編指令是怎麼樣的。記得在大學的時 候,我們使用gcc -s和objdump來獲得一個c程序代碼的匯編指令。但是對於.NET 程序來說,我們肯定無法輕松地獲得這些內容。因為所有的.NET程序都是編譯成 IL代碼的,而只有在運行時才會被JIT編譯成本機代碼。因此,我們必須要在程序 運行之後,再使用某種方式去“探得”匯編指令為何——除非我們可以讓JIT在不 運行程序的時候編譯IL代碼,老趙不知道該怎麼做,可能需要朋友的提點。

為了進行這個實驗,我們先來寫一些簡單的示例代碼:

namespace TestAsm
{
  public static class  TestClass
  {
    public static int TestMethod(int i)
    {
      return i;
    }
  }

   class Program
  {
    static void Main(string[]  args)
    {
      Console.WriteLine("Before  JIT.");
      Console.ReadLine();

       TestClass.TestMethod(1);

      Console.WriteLine("After  JIT");
      Console.ReadLine();

       TestClass.TestMethod(1);
    }
  }
}

大家可以新建一個TestAsm項目,將以上代碼復制粘貼,並使用Debug模式編譯 (避免TestMethod方法被內聯,這會導致TestMethod永遠不會被JIT) 2,便可以得到一個TestAsm.exe,這就是我們的試驗目標。可以看到 代碼中調用了兩遍TestClass.TestMethod方法,並且分別在調用前使用 Console.ReadLine中斷,這使我們有了有機會使用WinDbg來進行一番探索。我們 先進行一番准備工作:

運行TestAsm.exe,看到Before JIT字樣(最好不要在VS裡調試運行,因為這 會加入VS的的調試模塊——雖然這並不影響試驗)。

打開WinDbg(假設您已經設好了Symbol Path),按F6(或File - Attach to a Process),選擇TestAsm.exe並確定。

加載SOS(例如.load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727 \sos.dll)。

現在我們就已經做好了准備。那麼我們第一步是什麼呢?自然是要找出 TestClass.TestMethod方法的“位置”,於是先使用!name2ee命令獲得TestClass 類的信息:

0:003> !name2ee *!TestAsm.TestClass
Module: 70ca1000  (mscorlib.dll)
--------------------------------------
Module:  00942c5c (TestAsm.exe)
Token: 0x02000002
MethodTable:  0094306c
EEClass: 0094133c
Name: TestAsm.TestClass

“!name2ee *!TestAsm.TestClass”命令的含義是“遍歷所有已加載模塊,查 找TestAsm.TestClass類型”。如果需要的話,您也可以使用“!name2ee modulename typename”的方式來查找指定模塊中的指定類型。從輸出中我們可以 看到MethodTable的地址是0028306c。於是我們使用!dumpmt -md <address>命令來查看TestClass類型的方法描述符(Method Descriptor) :

0:003> !dumpmt -md 0094306c
EEClass:  0094133c
Module: 00942c5c
Name: TestAsm.TestClass
mdToken: 02000002 (C:\...\TestAsm\bin\Debug\TestAsm.exe)
BaseSize:  0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap:  0
Slots in VTable: 5
--------------------------------------
MethodDesc Table
  Entry MethodDesc   JIT  Name
70e66a70  70ce4934  PreJIT System.Object.ToString()
70e66a90  70ce493c  PreJIT System.Object.Equals (System.Object)
70e66b00  70ce496c  PreJIT  System.Object.GetHashCode()
70ed72f0  70ce4990  PreJIT  System.Object.Finalize()
0094c040  00943060   NONE  TestAsm.TestClass.TestMethod(Int32)

且看TestMethod的JIT欄的狀態:“NONE”,這意味著這個方法還沒有經過JIT 的編譯,如果我們此時通過!u <address>命令來查看方法的匯編指令就會 看到:

0:003> !u 0094c040
Unmanaged code
0094c040  e8755d9571   call  mscorwks!PrecodeFixupThunk (722a1dba)
0094c045 5e       pop   esi
0094c046 0000       add   byte ptr [eax],al
0094c048 60        pushad
0094c049 30940000000000 xor   byte ptr  [eax+eax],dl
0094c050 0000      add   byte ptr  [eax],al
0094c052 0000      add   byte ptr  [eax],al
0094c054 0000      add   byte ptr  [eax],al
0094c056 0000      add   byte ptr  [eax],al
0094c058 0000      add   byte ptr  [eax],al

這段代碼的目的是將方法執行過程導向到JIT進行編譯,再執行編譯後的本機 代碼。由於JIT還沒有發生,因此我們還無法獲得TestMethod方法的匯編指令。

於是我們在WinDbg裡按F5(或Debug - Go)讓程序繼續執行。此時您可以去控 制台按下回車,這樣就會執行TestMethod方法,接著控制台上會顯示After JIT字 樣,並再一次中斷。這樣我們可以回到WinDbg按下Ctrl+Break(或Debug - Break )重新進入調試。我們重新查看TestClass的Descriptor,就會發現:

0:003> !dumpmt -md 0094306c
EEClass:  0094133c
Module: 00942c5c
Name: TestAsm.TestClass
mdToken: 02000002 (C:\...\TestAsm\bin\Debug\TestAsm.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in  IFaceMap: 0
Slots in VTable: 5
----------------------------- ---------
MethodDesc Table
  Entry MethodDesc   JIT  Name
70e66a70  70ce4934  PreJIT System.Object.ToString()
70e66a90  70ce493c  PreJIT System.Object.Equals (System.Object)
70e66b00  70ce496c  PreJIT  System.Object.GetHashCode()
70ed72f0  70ce4990  PreJIT  System.Object.Finalize()
01a100d8  00943060   JIT  TestAsm.TestClass.TestMethod(Int32)

從JIT欄中可以看出,TestMethod方法的已經經過了JIT,而它的Entry地址也 與剛才不同,因為再次調用方法時,已經不需要經過JIT了。現在我們便可繼續!u 來查看TestMethod的匯編指令:

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
01a100e1 833d142e940000 cmp   dword ptr ds: [942E14h],0
01a100e8 7405      je   01a100ef
01a100ea  e892a3ae70   call  mscorwks!JIT_DbgIsJustMyCode (724fa481)
01a100ef 33d2      xor   edx,edx
01a100f1 8955f8      mov   dword ptr [ebp-8],edx
01a100f4 90        nop
01a100f5 8b45fc     mov   eax,dword ptr [ebp-4]
01a100f8 8945f8     mov   dword ptr [ebp- 8],eax
01a100fb 90       nop
01a100fc eb00       jmp   01a100fe
01a100fe 8b45f8     mov   eax,dword ptr  [ebp-8]
01a10101 8be5      mov   esp,ebp
01a10103 5d        pop   ebp
01a10104 c3       ret

關於上面的這段匯編代碼,大家可以不去深究,因為這是使用Debug模式編譯 下的結果,其中的指令會包含一些調試信息(如call mscorwks! JIT_DbgIsJustMyCode)。現在我們也可以看出在JIT前後,一個方法入口點的變 化。那麼您是否會思考,那麼TestMethod在被調用的時候,它的入口點的改變, 是如何讓調用方得知的呢?難道JIT之後,所有調用TestMethod的方法,其匯編指 令還要有所變化嗎?為此,我們可以再關注一下Program.Main方法的匯編指令:

0:003> !name2ee *!TestAsm.Program
Module: 70ca1000  (mscorlib.dll)
--------------------------------------
Module:  00942c5c (TestAsm.exe)
Token: 0x02000003
MethodTable:  0094300c
EEClass: 009412d8
Name: TestAsm.Program
0:003>  !dumpmt -md 0094300c
EEClass: 009412d8
Module:  00942c5c
Name: TestAsm.Program
mdToken: 02000003 (C:\...\TestAsm\bin\Debug\TestAsm.exe)
BaseSize:  0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap:  0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
  Entry MethodDesc   JIT  Name
70e66a70  70ce4934  PreJIT System.Object.ToString()
70e66a90  70ce493c  PreJIT System.Object.Equals (System.Object)
70e66b00  70ce496c  PreJIT  System.Object.GetHashCode()
70ed72f0  70ce4990  PreJIT  System.Object.Finalize()
0094c015  00943004   NONE  TestAsm.Program..ctor()
01a10070  00942ff8   JIT  TestAsm.Program.Main(System.String[])
0:003> !u  01a10070
Normal JIT generated code
TestAsm.Program.Main (System.String[])
Begin 01a10070, size 57
>>>  01a10070 55       push  ebp
01a10071 8bec      mov    ebp,esp
01a10073 50       push  eax
01a10074  894dfc     mov   dword ptr [ebp-4],ecx
01a10077  833d142e940000 cmp   dword ptr ds:[942E14h],0
01a1007e 7405       je   01a10085
01a10080 e8fca3ae70   call   mscorwks!JIT_DbgIsJustMyCode (724fa481)
01a10085 90        nop
01a10086 8b0d3020bd02  mov   ecx,dword ptr ds:[2BD2030h]  ("Before JIT.")
*** WARNING: Unable to verify checksum for  C:\Windows\assembly\...\mscorlib.ni.dll
01a1008c e84737966f    call  mscorlib_ni+0x6d37d8 (713737d8) (...)
01a10091 90        nop
01a10092 e8f141966f   call  mscorlib_ni+0x6d4288  (71374288) (...)
01a10097 90       nop
01a10098  b901000000   mov   ecx,1
01a1009d ff1568309400  call   dword ptr ds:[943068h] (...TestMethod(Int32), ...)
01a100a3 90        nop
01a100a4 8b0d3420bd02  mov   ecx,dword ptr  ds:[2BD2034h] ("After JIT")
01a100aa e82937966f   call   mscorlib_ni+0x6d37d8 (713737d8) (...)
01a100af 90        nop
01a100b0 e8d341966f   call  mscorlib_ni+0x6d4288  (71374288) (...)
01a100b5 90       nop
01a100b6  b901000000   mov   ecx,1
01a100bb ff1568309400  call   dword ptr ds:[943068h] (...TestMethod(Int32), ...)
01a100c1 90        nop
01a100c2 90       nop
01a100c3 8be5       mov   esp,ebp
01a100c5 5d       pop    ebp
01a100c6 c3       ret

請注意最後標紅的兩個地址“943068h”,它並不是call指令的目標,而是表 示call指令的目標是“該地址所存dword的址”。於是我們通過dd <address>命令查看該地址的值:

0:003> dd 943068h
00943068 01a100d8 00000000  0000000c 00040011
00943078 00000004 70f10508 00942c5c  009430a4
00943088 0094133c 00000000 00000000  70e66a70
00943098 70e66a90 70e66b00 70ed72f0 00000080

還記得01a100d8這個地址嗎?向上翻翻,您會發現這就是JIT之後TestMethod 方法的入口點。可以料得,在JIT之前,dd 943068h的結果是0094c040,因為這就 是TestMethod在JIT之前的入口點。在TestMethod第一次被調用時,call指令會進 入JIT,而第二次調用以後,call指令便可以直接訪問方法的匯編指令了。

其實,如果要查看匯編指令,更簡單的方法可能是在VS中設置斷點,然後通過 “Go to Disassmbly”來查看匯編代碼。不過有時候我們卻無法借助VS,例如在 《淺談尾遞歸的優化方式》一文中,我們的試驗目標是通過IL編譯得來的(因為 C#編譯器不會生成IL指令tail.)。這時候,我們就需要出動WinDbg了。當然,您 也可以對進程進行dump之後,使用WinDbg來“Open Crash Dump”再進行分析—— 不過如果你要查看某個方法的匯編指令,還是要確保它已經經過了JIT。而本文沒 有使用dump的方式進行調試,也是因為想要演示一下JIT前後的改變。

注1:事實上經老趙觀察發現,動辄喜歡用IL解釋的人,大都是因為他理解得 不夠;而對問題充分理解之後,往往也就不需要用IL,或長篇IL代碼了。就像CLR via C#,中間有多少是用IL說明問題的呢?而且真正的好書,好的教學方式,都 是盡可能避免用低抽象的內容來說明問題的,因為重在“分析”,而不是使用的 手段本身。手段本身應該盡可能的簡化。因此MIT已經使用Python替代Scheme進行 教學了,而很多大學操作系統課程也用了Java。

注2:使用[MethodImpl(MethodImplOptions.NoInlining)]對方法進行標記之 後,JIT時應該也不會被內聯,可以一試。

注3:如果事先使用ngen.exe對程序集進行處理,則托管方法就會變成PreJIT 狀態,在調試時便可以直接查看其匯編指令。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved