技术开发 频道

CLR内部有太多太多IL看不到的东西

 IL代码多容易懂呀,这段IL代码基本上就和我们的C#一样。没错,这就是IL的作用。IL和C#一样,都是用于表现程序逻辑。C#使用if...else、while、for等等丰富语法,而在IL中就会变成判断+跳转语句。但是,您从一段几十行的IL语句中,看出一句十几行的while逻辑——收获在哪里?除此之外,C#分配一个变量,IL也分配一个。C#调用一个方法,IL就call或callvirt一下。C#里new一个,IL中就newobj一下(自然也会有一些特殊,例如可以使用jmp或tail call一个方法——是为尾递归,但也只是及其特殊的情况)。可以发现IL的功能大部分就是C#可以表现的功能。而C#隐藏掉的一些细节,在IL这里同样没有显示出来!

 那么我们又该如何发现一些细节呢?例如“书本”告诉我们的JIT的工作方式:方法第一次调用之后才会生成机器码。

 这段程序会打印三行文字,在打印出Before JITed和After JITed字样之后都会有一次停止,需要用户按回车之后才能继续。在进行试验的时候,您可以在程序暂停的时候使用WinDbg的File - Attach to Process命令附加到TestConsole.exe进程中,或者在两次暂停时各生成一个dump文件,这样便可不断地重现一些过程。否则的话,应用程序两次启动所生成的地址很可能会完全不同——因为JIT的工作是动态的,有时候很难提前把握。

 好,我们已经进入了第一个Console.ReadLine暂停,在点击回车继续下去之前。我们先使用WinDbg进行调试。以下是Main方法的汇编代码:

 0:000> !name2ee *!TestConsole.Program

 Module: 70f61000 (mscorlib.dll)

 --------------------------------------

 Module: 00172c5c (TestConsole.exe)

 Token: 0x02000002

 MethodTable: 00173010

 EEClass: 001712d0

 Name: TestConsole.Program

 0:000> !dumpmt -md 00173010

 EEClass: 001712d0

 Module: 00172c5c

 Name: TestConsole.Program

 mdToken: 02000002  (...\bin\Release\TestConsole.exe)

 BaseSize: 0xc

 ComponentSize: 0x0

 Number of IFaces in IFaceMap: 0

 Slots in VTable: 7

 --------------------------------------

 MethodDesc Table

 Entry MethodDesc      JIT Name

 71126ab0   70fa4944   PreJIT System.Object.ToString()

 71126ad0   70fa494c   PreJIT System.Object.Equals(System.Object)

 71126b40   70fa497c   PreJIT System.Object.GetHashCode()

 71197540   70fa49a0   PreJIT System.Object.Finalize()

 0017c019   00173008     NONE TestConsole.Program..ctor()

 0017c011   00172ff0     NONE TestConsole.Program.SomeMethod()

 003e0070   00172ffc      JIT TestConsole.Program.Main(System.String[])

 0:000> !u 003e0070

 Normal JIT generated code

 TestConsole.Program.Main(System.String[])

 Begin 003e0070, size 4d

 >>> 003e0070 55              push    ebp

 003e0071 8bec            mov     ebp,esp

 *** WARNING: Unable to verify checksum for mscorlib.ni.dll

 003e0073 e8a8d3da70      call    mscorlib_ni+0x22d420 (7118d420) (System.Console.get_Out(), ...)

 003e0078 8bc8            mov     ecx,eax

 003e007a 8b153020d102    mov     edx,dword ptr ds:[2D12030h] ("Before JITed.")

 003e0080 8b01            mov     eax,dword ptr [ecx]

 003e0082 ff90d8000000    call    dword ptr [eax+0D8h]

 003e0088 e8971b2571      call    mscorlib_ni+0x6d1c24 (71631c24) (System.Console.get_In(), ...)

 003e008d 8bc8            mov     ecx,eax

 003e008f 8b01            mov     eax,dword ptr [ecx]

 003e0091 ff5064          call    dword ptr [eax+64h]

 003e0094 ff15f82f1700    call    dword ptr ds:[172FF8h] (TestConsole.Program.SomeMethod(), ...)

 003e009a e881d3da70      call    mscorlib_ni+0x22d420 (7118d420) (System.Console.get_Out(), ...)

 003e009f 8bc8            mov     ecx,eax

 003e00a1 8b153420d102    mov     edx,dword ptr ds:[2D12034h] ("After JITed")

 003e00a7 8b01            mov     eax,dword ptr [ecx]

 003e00a9 ff90d8000000    call    dword ptr [eax+0D8h]

 003e00af e8701b2571      call    mscorlib_ni+0x6d1c24 (71631c24) (System.Console.get_In(), ...)

 003e00b4 8bc8            mov     ecx,eax

 003e00b6 8b01            mov     eax,dword ptr [ecx]

 003e00b8 ff5064          call    dword ptr [eax+64h]

 003e00bb 5d              pop     ebp

 003e00bc c3              ret

 请关注上面那个被标红的call语句,它的含义是:

 先从读取172FF8地址中的值,这才是方法调用的目标地址(即SomeMethod方法)。

 使用call指令调用刚才读取到的目标地址

 那么在第一次调用SomeMethod方法之前,目标地址的指令是什么呢?

0:000> dd 172FF8

 00172ff8  0017c011 71030002 00200006 003e0070

 00173008  00060003 00000004 00000000 0000000c

 00173018  00050011 00000004 711d0770 00172c5c

 00173028  0017304c 001712d0 00000000 00000000

 00173038  71126ab0 71126ad0 71126b40 71197540

 00173048  0017c019 00000080 00000000 00000000

 00173058  00000000 00000000 00000000 00000000

 00173068  00000000 00000000 00000000 00000000

 0:000> !u 0017c011

 Unmanaged code

 0017c011 b000            mov     al,0

 0017c013 eb08            jmp     0017c01d

 0017c015 b003            mov     al,3

 0017c017 eb04            jmp     0017c01d

 0017c019 b006            mov     al,6

 0017c01b eb00            jmp     0017c01d

 0017c01d 0fb6c0          movzx   eax,al

 0017c020 c1e002          shl     eax,2

 0017c023 05f02f1700      add     eax,172FF0h

 0017c028 e9d7478c00      jmp     00a40804

 这是什么,不像是SomeMethod的内容阿,SomeMethod是会调用Console.WriteLine方法的,怎么变成了一些跳转了呢?于是我们想起书本(例如《CLR via C#》)中的话来,在方法第一次调用时,将会跳转到JIT的指令处,对方法的IL代码进行编译。再想想书中的示意图,于是恍然大悟,原来这段代码的作用是“让JIT编译IL”啊。那么在JIT后,同样的调用会产生什么结果呢?

 我们在WinDbg中Debug - Detach Debuggee,让程序继续运行。单击回车,您会发现屏幕上出现了Hello Word和After JIT的字样。于是我们继续Attach to Process,重复上面的命令。由于Main方法已经被编译好了,它的汇编代码不会改变,因此在调用SomeMethod方法时的步骤还是不变:先去内存172FF8中读取目标地址,再call至目标地址。

0:000> dd 172FF8

 00172ff8  003e00d0 71030002 00200006 003e0070

 00173008  00060003 00000004 00000000 0000000c

 00173018  00050011 00000004 711d0770 00172c5c

 00173028  0017304c 001712d0 00000000 00000000

 00173038  71126ab0 71126ad0 71126b40 71197540

 00173048  0017c019 00000080 00000000 00000000

 00173058  00000000 00000000 00000000 00000000

 00173068  00000000 00000000 00000000 00000000

 0:000> !u 003e00d0

 Normal JIT generated code

 TestConsole.Program.SomeMethod()

 Begin 003e00d0, size 1a

 >>> 003e00d0 55              push    ebp

 003e00d1 8bec            mov     ebp,esp

 *** WARNING: Unable to verify checksum for mscorlib.ni.dll

 003e00d3 e848d3da70      call    mscorlib_ni+0x22d420 (7118d420) (System.Console.get_Out(), mdToken: 06000772)

 003e00d8 8bc8            mov     ecx,eax

 003e00da 8b153820d102    mov     edx,dword ptr ds:[2D12038h] ("Hello World!")

 003e00e0 8b01            mov     eax,dword ptr [ecx]

 003e00e2 ff90d8000000    call    dword ptr [eax+0D8h]

 003e00e8 5d              pop     ebp

 003e00e9 c3              ret

0
相关文章