如果想要了解C++
内部的实现原理,没有什么比观察C++代码对应的汇编代码来的更直接了。本系列主要从汇编角度研究C++代码和汇编的对应关系,揭示C++
内部的机制和原理。在第一篇文章中我将从一个简单的C++程序着手快速解释一下C++反汇编代码的基本的结构和内容,相当于一个简单的Preview。而在后续的文章中,我将根据不同的Topic,详细解释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++代码的对应关系,十分有助于分析。在VS2005中可以这样打开/FAs:
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
,跳过之前建立SEH链所用的3个Push指令所占用的栈的空间,以及sub esp, d8h为局部变量分配的d8H字节空间
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
是之前
main的起始代码中在堆栈中建立的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
|