旌磅箱 发表于 2025-5-30 01:11:04

使用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类的信息:
<strong>0:003> !name2ee *!TestAsm.TestClass</strong>
Module: 70ca1000 (mscorlib.dll)
--------------------------------------
Module: 00942c5c (TestAsm.exe)
Token: 0x02000002
MethodTable: <font color="#ff0000">0094306c</font>
EEClass: 0094133c
Name: TestAsm.TestClass“!name2ee *!TestAsm.TestClass”命令的含义是“遍历所有已加载模块,查找TestAsm.TestClass类型”。如果需要的话,您也可以使用“!name2ee modulename typename”的方式来查找指定模块中的指定类型。从输出中我们可以看到MethodTable的地址是0094306c。于是我们使用!dumpmt -md 命令来查看TestClass类型的方法描述符(Method Descriptor):
<strong>0:003> !dumpmt -md 0094306c</strong>
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()
<font color="#ff0000">0094c040</font>   00943060   <font color="#0000ff">NONE</font> TestAsm.TestClass.TestMethod(Int32)且看TestMethod的JIT栏的状态:“NONE”,这意味着这个方法还没有经过JIT的编译,如果我们此时通过!u 命令来查看方法的汇编指令就会看到:
<strong>0:003> !u 0094c040</strong>
Unmanaged code
0094c040 e8755d9571      call    mscorwks!PrecodeFixupThunk (722a1dba)
0094c045 5e            pop   esi
0094c046 0000            add   byte ptr ,al
0094c048 60            pushad
0094c049 30940000000000xor   byte ptr ,dl
0094c050 0000            add   byte ptr ,al
0094c052 0000            add   byte ptr ,al
0094c054 0000            add   byte ptr ,al
0094c056 0000            add   byte ptr ,al
0094c058 0000            add   byte ptr ,al这段代码的目的是将方法执行过程导向到JIT进行编译,再执行编译后的本机代码。由于JIT还没有发生,因此我们还无法获得TestMethod方法的汇编指令。
于是我们在WinDbg里按F5(或Debug - Go)让程序继续执行。此时您可以去控制台按下回车,这样就会执行TestMethod方法,接着控制台上会显示After JIT字样,并再一次中断。这样我们可以回到WinDbg按下Ctrl+Break(或Debug - Break)重新进入调试。我们重新查看TestClass的Descriptor,就会发现:
<strong>0:003> !dumpmt -md 0094306c</strong>
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()
<font color="#ff0000">01a100d8</font>   00943060      <font color="#0000ff">JIT</font> TestAsm.TestClass.TestMethod(Int32)从JIT栏中可以看出,TestMethod方法的已经经过了JIT,而它的Entry地址也与刚才不同,因为再次调用方法时,已经不需要经过JIT了。现在我们便可继续!u来查看TestMethod的汇编指令:
<strong>0:003> !u 01a100d8</strong>
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 ,ecx
01a100e1 833d142e940000cmp   dword ptr ds:,0
01a100e8 7405            je      01a100ef
01a100ea e892a3ae70      call    mscorwks!JIT_DbgIsJustMyCode (724fa481)
01a100ef 33d2            xor   edx,edx
01a100f1 8955f8          mov   dword ptr ,edx
01a100f4 90            nop
01a100f5 8b45fc          mov   eax,dword ptr
01a100f8 8945f8          mov   dword ptr ,eax
01a100fb 90            nop
01a100fc eb00            jmp   01a100fe
01a100fe 8b45f8          mov   eax,dword ptr
01a10101 8be5            mov   esp,ebp
01a10103 5d            pop   ebp
01a10104 c3            ret关于上面的这段汇编代码,大家可以不去深究,因为这是使用Debug模式编译下的结果,其中的指令会包含一些调试信息(如call mscorwks!JIT_DbgIsJustMyCode)。现在我们也可以看出在JIT前后,一个方法入口点的变化。那么您是否会思考,那么TestMethod在被调用的时候,它的入口点的改变,是如何让调用方得知的呢?难道JIT之后,所有调用TestMethod的方法,其汇编指令还要有所变化吗?为此,我们可以再关注一下Program.Main方法的汇编指令:
<strong>0:003> !name2ee *!TestAsm.Program</strong>
Module: 70ca1000 (mscorlib.dll)
--------------------------------------
Module: 00942c5c (TestAsm.exe)
Token: 0x02000003
MethodTable: <font color="#ff0000">0094300c</font>
EEClass: 009412d8
Name: TestAsm.Program
<strong>0:003> !dumpmt -md 0094300c</strong>
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()
<font color="#ff0000">01a10070</font>   00942ff8      JIT TestAsm.Program.Main(System.String[])
<strong>0:003> !u 01a10070</strong>
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 ,ecx
01a10077 833d142e940000cmp   dword ptr ds:,0
01a1007e 7405            je      01a10085
01a10080 e8fca3ae70      call    mscorwks!JIT_DbgIsJustMyCode (724fa481)
01a10085 90            nop
01a10086 8b0d3020bd02    mov   ecx,dword ptr ds: ("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:[<font color="#ff0000">943068h</font>] (...TestMethod(Int32), ...)
01a100a3 90            nop
01a100a4 8b0d3420bd02    mov   ecx,dword ptr ds: ("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:[<font color="#ff0000">943068h</font>] (...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 命令查看该地址的值:
<strong>0:003> dd 943068h</strong>
00943068<font color="#ff0000">01a100d8</font> 00000000 0000000c 00040011
0094307800000004 70f10508 00942c5c 009430a4
009430880094133c 00000000 00000000 70e66a70
0094309870e66a90 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前后的改变3。
 
注1:事实上经老赵观察发现,动辄喜欢用IL解释的人,大都是因为他理解得不够;而对问题充分理解之后,往往也就不需要用IL,或长篇IL代码了。就像CLR via C#,中间有多少是用IL说明问题的呢?而且真正的好书,好的教学方式,都是尽可能避免用低抽象的内容来说明问题的,因为重在“分析”,而不是使用的手段本身。手段本身应该尽可能的简化。因此MIT已经使用Python替代Scheme进行教学了,而很多大学操作系统课程也用了Java。
注2:使用对方法进行标记之后,JIT时应该也不会被内联,可以一试。
注3:如果事先使用ngen.exe对程序集进行处理,则托管方法就会变成PreJIT状态,在调试时便可以直接查看其汇编指令。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 使用WinDbg获得托管方法的汇编代码