一个简单的C++程序反汇编解析
本系列主要从汇编角度研究c++语言机制和汇编的对应关系。第一篇自然应该从最简单的开始。c++的源代码如下:
class my_class { public : my_class() { m_member = 1; } void method(int n) { m_member = n; } ~my_class() { m_member = 0; } private : int m_member; }; int _tmain(int argc, _tchar* argv[]) { my_class a_class; a_class.method(10); return 0; }
可以直接debug的时候看到assembly代码,不过这样获得的代码注释比较少。比较理想的方法是利用vc编译器的一个选项/fas来生成对应的汇编 代码。/fas还会在汇编代码中加入注释注明和c++代码的对应关系,十分有助于分析。build代码便可以在输出目录下发现对应的.asm文件。本文将 逐句分析汇编代码和c++的对应关系。首先是winmain:
_text segment _wmain proc push ebp ; 保存旧的ebp mov ebp, esp ; ebp保存当前栈的位置 push -1 ; 建立seh(structured exception handler)链 ; -1表示表头,没有prev push __ehhandler$_wmain ; seh异常处理程序的地址 mov eax, dword ptr fs:0 ; fs:0指向teb的内容,头4个字节是当前seh链的地址 push eax ; 保存起来 sub esp, d8h ; 分配d8h字节的空间 push ebx push esi push edi lea edi, dword ptr [ebp-e4h] ; e4h = d8h + 4 * 3,跳过中间ebx, esi, edi mov ecx, 36h ; 36h*4h=d8h,也就是用36h个cccccccch填满刚才分配的d8h字节空间 mov eax, cccccccch rep stosd mov eax, dword ptr ___security_cookie xor eax, ebp push eax ; ebp ^ __security_cookie压栈保存 lea eax, dword ptr [ebp-0ch] ; ebp-0ch是新的seh链的结构地址(刚压入栈中的栈地址) mov dword ptr fs:0, eax ; 设置到teb中作为当前active的seh链表末尾
到此为止栈的内容是这样的:低地址
security cookie after xor edi esi ebx local stack: d8h old fs:0 __ehhandler$_wmain ffffffffh old ebp
高地址main接着后面调用my_class的构造函数
lea ecx, dword ptr [ebp-14h] call ??0my_class@@qae@xz ; 调用my_class::my_class, ??my_class@@qae@xz是经过name mangling后的名字 mov dword ptr [ebp-4], 0 ; 进入__try块,在main中有一个隐式的__try/__except块
接着调用my_class::method
push 10 ; 参数入栈 lea ecx, dword ptr [ebp-14h] ; 遵循thiscall调用协定,ecx存放的是this指针 call ?method@my_class@@qaexh@z ; 调用子程序my_class:method(10)
之后是析构:
mov dword ptr [ebp-e0h], 0 ; 用来放置返回值 mov dword ptr [ebp-4], -1 ; 标记try的正常结束 lea ecx, dword ptr [ebp-14h] ; a_class的地址作为this存入ecx call ??1my_class@@qae@xz ; my_class::~my_class mov eax, dword ptr [ebp-e0h] ; 返回值按照约定放入eax中
main函数退出代码如下:
push edx mov ecx, ebp push eax lea edx, dword ptr $ln7@wmain call @_rtc_checkstackvars@8 ; 检查栈 pop eax pop edx mov ecx, dword ptr [ebp-0ch] ; 取出之前保存的旧的fs:0,并恢复 mov dword ptr fs:0, ecx pop ecx pop edi pop esi pop ebx add esp, e4h ; 退掉分配的d8h + 建立seh链所需的0ch字节 cmp ebp, esp call __rtc_checkesp ; 检查esp值,这个时候esp应该和ebp匹配,否则说明出现了栈不平衡的情况,这种情况下调用子程序报错 mov esp, ebp ; 恢复ebp到esp pop ebp ; 恢复原来的ebp值 ret 0 _wmain endp
专门用于seh的子程序。__unwindfunclet$_wmain$0当异常发生的时候被调,负责进行栈展开,主要是调用析构函数。__ehhandler$_wmain则是在exception被抛出的时候调用。
text$x segment __unwindfunclet$_wmain$0: ; 当seh发生的时候会调用该函数,析购a_class lea ecx, dword ptr [ebp-14h] ; ecx = [ebp – 14h],也就是a_class的地址 jmp ??1my_class@@qae@xz ; 调用my_class::~my_class __ehhandler$_wmain: mov edx, dword ptr [esp+8] ; esp = 当前的fs:0, [esp + 8] = 之前的seh结构,也就是main中建立的 lea eax, dword ptr [edx+0ch] ; edx + 0ch = 当前的ebp,也就是main的ebp,此时不能直接使用ebp因为可能会从任意函数调过来,此时ebp是该函数的ebp,而不是main的ebp mov ecx, dword ptr [edx-e0h] ; 之前存下去的__security_cookie ^ ebp xor ecx, eax ; 再次和ebp相异或 call @__security_check_cookie@4 ; 此时ecx应该等于__security_cookie,否则说明栈的内容被恶意改动(或者编程错误) mov eax, offset __ehfuncinfo$_wmain jmp ___cxxframehandler3 text$x ends
my_class::my_class构造函数如下。构造函数本质上就是一个全局函数,名字是经过打乱的(name mangling),这样可以和同一class和其他class的同名方法区别开来。不同编译器有不同规则,因此不必过于深究。
_text segment
??0my_class@@qae@xz proc push ebp ; 保存旧的ebp mov ebp, esp ; ebp保存当前栈的位置 sub esp, cch ; 给栈分配cch个字节 push ebx ; 保存常用寄存器 push esi push edi push ecx lea edi, dword ptr [ebp-cch] ; 从分配的位置开始 mov ecx, 33h ; 写33h个cccccccch mov eax, cccccccch ; 也就是33h*4h=cch,正好是分配的大小 rep stosd ; 从而把整个栈上当前分配的空间用cch填满 pop ecx mov dword ptr [ebp-8], ecx ; 按照约定,一般用ecx保存this指针 ; 把this存入到ebp-8,并不是很必要,因为这是debug版本 ; 10 : { ; 11 : m_member = 1; mov eax, dword ptr [ebp-8] ; eax中存放this mov dword ptr [eax], 1 ; this的头四个byte是m_member的内容 ; 12 : } mov eax, dword ptr [ebp-8] ; 多余的一句话,可以优化掉 pop edi pop esi pop ebx mov esp, ebp ; 恢复esp,因此就算是中间栈运算出错,最后也不会导致灾难性的结果,只要ebp还是正确的 pop ebp ret 0 ??0my_class@@qae@xz endp
my_class::method的实现如下:
_text segment ?method@my_class@@qaexh@z proc ; my_class::method ; 15 : { push ebp mov ebp, esp sub esp, cch push ebx push esi push edi push ecx lea edi, dword ptr [ebp-cch] mov ecx, 33h mov eax, cccccccch rep stosd pop ecx mov dword ptr [ebp-8], ecx ; 16 : m_member = n; mov eax, dword ptr [ebp-8] ; eax中存放this mov ecx, dword ptr [ebp+8] ; ebp -> ebp ; ebp + 4 -> ip ; ebp + 8 -> n ; 把n存入ecx中 mov dword ptr [eax], ecx ; this头四个字节是m_member, 因此这句话就是m_member = n ; 17 : } pop edi pop esi pop ebx mov esp, ebp pop ebp ret 4 ; 等价于 ; ret 恢复eip,返回调用地址 ; add esp, 4 -> 把n从栈上pop掉 ?method@my_class@@qaexh@z endp
最后的析构函数,和前面的代码并无区别。
_text segment ??1my_class@@qae@xz proc ; my_class::~my_class ; 20 : { push ebp mov ebp, esp sub esp, 204 push ebx push esi push edi push ecx lea edi, dword ptr [ebp-204] mov ecx, 33h mov eax, cccccccch rep stosd pop ecx mov dword ptr _this$[ebp], ecx ; 21 : m_member = 0; mov eax, dword ptr [ebp-8] mov dword ptr [eax], 0 ; 22 : } pop edi pop esi pop ebx mov esp, ebp pop ebp ret 0 ??1my_class@@qae@xz endp ; my_class::~my_class _text ends
(完)