[转]用汇编分析 .NET 执行 - 简易版

http://www.rainsts.net/article.asp?id=859

前些日子博客园有过一次热闹的 IL 和 汇编之争,我非常无聊地当了回事后围观群众。双方都是博客园的牛人,我作为 "境外人士" 就不站队掺和了。

就我个人而言,C#、IL、ASM 在 "挖坟" 时通常会组合起来使用。IL 有助于理解虚拟平台的 "实现",在不脱离抽象思想的情况下去了解 "过程"。而 ASM 则专注于现实的本质,一个最终的结果,让我们用最原始的 "数据" 和 "指针" 验证计算结果。我比较反感用 ASM 来推导 "思想",因为 ASM 是一种最终实现,它一定程度上忽略了 "思想过程" (抽象变成了指针) 和 "行为规则" (被编译器优化掉)。.NET、Java 以及现下流行的动态语言,它们共同遵循的是同一种 (或相似) "思想",一种基于堆栈式虚拟机的设计,只是实现上各有差异。用某个平台上反汇编结果来反推 "思想" 似乎在培养一种恶劣的习惯,一种因果倒置的逻辑。我有些羡慕 Java 和动态语言社区,他们更多地讨论思想和架构层面上的东西,而 WIN32/.NET 社区无论大牛小鸟大都执着于某个技巧以及汽车挡泥板的钢铁成份。ASM 对我来说只是一个 "显微镜",用于 "局部" 验证思想的正确性而已。

以上废话,纯粹凑字,可跳过。(起点大牛们常用的行文方式,沾点书卷气 [lol])

------------ 分隔线 ------------

以下的例子也许过于简单,但也仅仅是为了展示一个过程。鉴于本人 N 久没有折腾过汇编了,所以对可能存在的错误不予负责,仅供参考。

注意:以下分析代码基于 Debug 版本,如果编译成 Release 的话,很多东东都看不到了。如果要研究 CSC 和 JIT 优化,可另起炉灶。

class Program
{
    static void Main()
    {
        Test(); // Breakpoint

        Console.WriteLine("Press any key to exit..."); // Breakpoint
        Console.ReadKey(true);
        Environment.Exit(0);
    }

    private static void Test()
    {
        int a = 1;
        int b = 2;
        int c = a + b;
        string d = "Hello, World!";
    }
}

我们可以直接借助于 Visual Studio + SOS.dll 来完成反汇编分析。相关内容,可参考以前写的文章。

F5 运行 (Debug),运行到断点中断,快捷键 "CTRL + ALT + D" 打开 Disassembly 反汇编窗口,"CTRL + D, R" 打开 Registers 寄存器查看窗口,以及 "CTRL + D, Y" 打开 Memory 1 内存查看窗口。

!clrstack

ESP EIP 
0012f43c 00e70092 Learn.CUI.Program.Main()
0012f69c 79e71b4c [GCFrame: 0012f69c] 

ESP 保存了堆栈指针地址,EIP 指向方法当前执行指令地址。

!u 00e70092

Normal JIT generated code
Learn.CUI.Program.Main()
Begin 00e70070, size 53

00E70070 55 push ebp
00E70071 8BEC mov ebp,esp
00E70073 57 push edi
00E70074 56 push esi
00E70075 53 push ebx
00E70076 83EC38 sub esp,38h
00E70079 33C0 xor eax,eax
00E7007B 8945F0 mov dword ptr [ebp-10h],eax
00E7007E 33C0 xor eax,eax
00E70080 8945E4 mov dword ptr [ebp-1Ch],eax
00E70083 833D142E990000 cmp dword ptr ds:[00992E14h],0
00E7008A 7405 je 00E70091
00E7008C E8F0A32579 call 7A0CA481 (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
00E70091 90 nop
>>> 00E70092 E881BFB2FF call 0099C018 (Learn.CUI.Program.Test(), mdToken: 06000002)
00E70097 90 nop
00E70098 8B0D30203102 mov ecx,dword ptr ds:[02312030h] ("Press any key to exit...")
00E7009E E835379278 call 797937D8 (System.Console.WriteLine(System.String), mdToken: 060007c8)
00E700A3 90 nop
00E700A4 8D4DBC lea ecx,[ebp-44h]
00E700A7 BA01000000 mov edx,1
00E700AC E8C3379278 call 79793874 (System.Console.ReadKey(Boolean), mdToken: 060007a6)
00E700B1 90 nop
00E700B2 33C9 xor ecx,ecx
00E700B4 E8E7298D78 call 79742AA0 (System.Environment.Exit(Int32), mdToken: 06000a60)
00E700B9 90 nop
00E700BA 90 nop
00E700BB 8D65F4 lea esp,[ebp-0Ch]
00E700BE 5B pop ebx
00E700BF 5E pop esi
00E700C0 5F pop edi
00E700C1 5D pop ebp
00E700C2 C3 ret

我们可以记录下方法跳转前的相关寄存器信息,用于后续执行过程对比。
EAX = 00000000 EBX = 0012F4AC ECX = 0012F560 EDX = 00000000 ESI = 00196200 
EDI = 00000000 EIP = 00E70092 ESP = 0012F43C EBP = 0012F480 EFL = 00000246 

CS = 001B DS = 0023 ES = 0023 SS = 0023 FS = 003B GS = 0000 

需要关注的几个寄存器:
  • EAX: 除了当做通用存储器,通常还用于保存方法返回值。
  • EIP: 存储下一条执行指令地址。
  • ESP: 存储堆栈指针。
  • EBP: 存储堆栈帧基地址,相关代码指令会用它减去偏移量来确定指针位置。
回到汇编代码上来,call 指令后的操作数是执行目标代码的地址,如果目标方法还没有被 JIT 编译,那么你会看到如下情形。

!u 0099C018

Unmanaged code
0099C018 E89D5D4D79 call 79E71DBA
0099C01D 5E pop esi
0099C01E 0401 add al,1
0099C020 E8955D4D79 call 79E71DBA
0099C025 5E pop esi
0099C026 0800 or byte ptr [eax],al
0099C028 F02F lock das
0099C02A 99 cdq
0099C02B 0000 add byte ptr [eax],al
0099C02D 0000 add byte ptr [eax],al

这段代码会执行 JIT 内部方法完成目标 IL 代码编译。我们 F11 进入 Test 方法入口,使其完成编译过程。再次反汇编该地址的代码,我们就会看到 Test 代码的实际位置了。

!u 0099C018

Unmanaged code
0099C018 E9BB404D00 jmp 00E700D8
0099C01D 5F pop edi
0099C01E 0401 add al,1
0099C020 E8955D4D79 call 79E71DBA
0099C025 5E pop esi
0099C026 0800 or byte ptr [eax],al
0099C028 F02F lock das
0099C02A 99 cdq
0099C02B 0000 add byte ptr [eax],al
0099C02D 0000 add byte ptr [eax],al

"jmp 00E700D8" 给出了 Test 执行代码地址,同时也告诉我们 JIT 不会编译第二次。

!u 00E700D8

Normal JIT generated code
Learn.CUI.Program.Test()
Begin 00e700d8, size 66

>>> 00E700D8 55 push ebp
00E700D9 8BEC mov ebp,esp
00E700DB 57 push edi
00E700DC 56 push esi
00E700DD 53 push ebx
00E700DE 83EC3C sub esp,3Ch
00E700E1 8D7DC8 lea edi,[ebp-38h]
00E700E4 B90B000000 mov ecx,0Bh
00E700E9 33C0 xor eax,eax
00E700EB F3AB rep stos dword ptr es:[edi]
... ...

这段代码值得好好研究一下,VS 支持我们单步执行汇编代码,只要在 Disassembly 窗口按 F10 就行。Disassembly 反汇编代码比 "sos !u " 要少些信息,不过两项结合着看就是了,而且还有 C# 源代码显示,很牛叉。

>>> 00E700D8 55 push ebp ; 将前一个栈帧地址压入堆栈,做现场保护
00E700D9 8BEC mov ebp,esp ; 修改当前堆栈帧的起始地址
00E700DB 57 push edi
00E700DC 56 push esi
00E700DD 53 push ebx

00E700DE 83EC3C sub esp,3Ch
00E700E1 8D7DC8 lea edi,[ebp-38h]
00E700E4 B90B000000 mov ecx,0Bh
00E700E9 33C0 xor eax,eax
00E700EB F3AB rep stos dword ptr es:[edi]
00E700ED 33C0 xor eax,eax
00E700EF 8945E4 mov dword ptr [ebp-1Ch],eax
00E700F2 833D142E990000 cmp dword ptr ds:[00992E14h],0
00E700F9 7405 je 00E70100
00E700FB E881A32579 call 7A0CA481 (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE) ; 插入了相关的调试代码

00E70100 33D2 xor edx,edx
00E70102 8955C0 mov dword ptr [ebp-40h],edx ; 初始化变量 b
00E70105 33D2 xor edx,edx
00E70107 8955C4 mov dword ptr [ebp-3Ch],edx ; 初始化变量 a
00E7010A 33D2 xor edx,edx
00E7010C 8955B8 mov dword ptr [ebp-48h],edx ; 初始化变量 d
00E7010F 33D2 xor edx,edx
00E70111 8955BC mov dword ptr [ebp-44h],edx ; 初始化变量 c
00E70114 90 nop

00E70115 C745C401000000 mov dword ptr [ebp-3Ch],1 ; a = 1
00E7011C C745C002000000 mov dword ptr [ebp-40h],2 ; b = 2
00E70123 8B45C4 mov eax,dword ptr [ebp-3Ch]
00E70126 0345C0 add eax,dword ptr [ebp-40h] ; (eax) = a + b
00E70129 8945BC mov dword ptr [ebp-44h],eax ; c = (eax)
00E7012C 8B0534203102 mov eax,dword ptr ds:[02312034h] ("Hello, World!") ; (eax) = 02312034h 存储了字符串地址
00E70132 8945B8 mov dword ptr [ebp-48h],eax ; d = (eax)
00E70135 90 nop

00E70136 8D65F4 lea esp,[ebp-0Ch] ; 开始恢复堆栈现场
00E70139 5B pop ebx
00E7013A 5E pop esi
00E7013B 5F pop edi
00E7013C 5D pop ebp ; 恢复上一个栈帧的起始地址
00E7013D C3 ret

从反汇编代码中,我们可以发现,方法内部变量都是事先初始化好的,然后通过堆栈指针进行访问。在我们单步执行的时候,可以观察寄存器存储内容的变化。

执行到 "00E7013D C3 ret" 时寄存器状态。
EAX = 01312DC8 EBX = 0012F4AC ECX = 00000000 EDX = 00000000 ESI = 00196200 
EDI = 00000000 EIP = 00E7013D ESP = 0012F438 EBP = 0012F480 EFL = 00000206 

CS = 001B DS = 0023 ES = 0023 SS = 0023 FS = 003B GS = 0000 

和前面记录的调用前寄存器数据对比一下。
EAX = 00000000 EBX = 0012F4AC ECX = 0012F560 EDX = 00000000 ESI = 00196200 
EDI = 00000000 EIP = 00E70092 ESP = 0012F43C EBP = 0012F480 EFL = 00000246 

CS = 001B DS = 0023 ES = 0023 SS = 0023 FS = 003B GS = 0000 

EBP 内容被恢复。

!clrstack

ESP EIP 
0012f438 00e7013d Learn.CUI.Program.Test()
0012f43c 00e70097 Learn.CUI.Program.Main()
0012f69c 79e71b4c [GCFrame: 0012f69c] 

ret 后将从 00e70097 处恢复 Main 方法执行。

Learn.CUI.Program.Main()
Begin 00e70070, size 53

>>> 00E70092 E881BFB2FF call 0099C018 (Learn.CUI.Program.Test(), mdToken: 06000002)
00E70097 90 nop
00E70098 8B0D30203102 mov ecx,dword ptr ds:[02312030h] ("Press any key to exit...")
00E7009E E835379278 call 797937D8 (System.Console.WriteLine(System.String), mdToken: 060007c8)

---------------------- 第二次提溜出来的分隔线 -------------------

作为简易版写到这就算对得起观众了,赶紧闪人……
posted @ 2011-03-15 09:44  愤怒的熊猫  阅读(235)  评论(0编辑  收藏  举报