很多人学完汇编,去看C/C++的反汇编就会很懵,发现单独看一条指令看的明明白白,但连在多条指令连在一起就不知道有什么作用了,如
push ebp
mov ebp,esp
sub esp,40h
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
这里用VC6.0是最好的,更能看出本质,VS系列也可以,VS的话这里要把优化给关掉。在反汇编指令方面会有一定的差别,但其执行的逻辑还是一致的,我这里用的是VC6.0
下一篇:【逆向工程】C/C++的反汇编表示详解(2) 整数,字符串,分支语句
函数的调用
源代码
一个简单的加法函数
#include <stdio.h>
int Add(int a,int b)
{
return a,b;
}
int main()
{
Add(1,2);
return 0;
}
堆栈图
反汇编
开始调用函数
C/C++以及MFC默认使用_cdecl函数调用约定,即调用者负责清理线程栈(外平衡),参数从右往左入栈,寄存器EAX, ECX和EDX被指定在函数中使用,返回值放置在EAX中。WIN32API调用约定为_stdcall,两种调用约定主要区别是_stdcall采用被调用者负责清理线程栈(内平衡),后续内容中会介绍。
/*
main函数中调用Add函数
*/
00401068 push 2 //将参数压栈
0040106A push 1
0040106C call @ILT+0(Add) (00401005) //调用一条JMP指令
00401071 add esp,8 //函数调用结束后恢复堆栈 使其堆栈平衡
//后面会说明该命令作用
VS和VC编译器在调用函数时,并不会直接跳转到函数所在地址而是会在函数调用后生成一条JMP指令,用来跳转到函数所在的地址,JMP指令不改变堆栈操作
/*
0040106C call @ILT+0(Add) (00401005)
指令跳转的一条JMP指令
*/
00401005 jmp Add (00401020)
开始执行函数
/*
提升堆栈 为函数的执行创造内存
其中提升堆栈之前ESP与提升后ESP的差值所对应的栈空间 即40H叫做缓冲区
用来存放局部变量的值
*/
00401020 push ebp //保存栈底 为函数执行后恢复堆栈做准备
00401021 mov ebp,esp
00401023 sub esp,40h //缓冲区的大小
//在启用编译器优化的情况下可能会用
// push reg指令来开辟栈空间
/*
保存寄存器的值,确保函数执行完毕后,
使后面的程序可以再使用寄存器执行该函数之前的值
*/
00401026 push ebx
00401027 push esi
00401028 push edi
/*
将缓冲区中的内容初始化为CCCCCCCC即int3中断 防止缓冲区溢出
*/
00401029 lea edi,[ebp-40h] //初始化edi的值 即将堆栈提升后的ESP赋给EDI
0040102C mov ecx,10h //重复执行的次数为30H
00401031 mov eax,0CCCCCCCCh //初始化EAX也就是缓冲区要填充的内容
00401036 rep stos dword ptr [edi] //将缓冲区全部填充为int3中断 防止缓冲区溢出
/*
执行return中的内容a+b 并根据cdecl调用约定将返回值保存在EAX中
若返回结果超过EAX寄存器的容量,则其高32位就会放到EDX寄存器中
*/
00401038 mov eax,dword ptr [ebp+8] //[ebp+8]即参数a
0040103B add eax,dword ptr [ebp+0Ch] //[ebp+0Ch]即参数b
/*
恢复堆栈,使ESP,EBP回到函数刚开始也就是提升栈空间前的值
注意这里并不是指堆栈平衡
*/
0040103E pop edi //取回保存的寄存器的值
0040103F pop esi
00401040 pop ebx
00401041 mov esp,ebp //恢复ESP的值
00401043 pop ebp //恢复EBP的值
00401044 ret //等于将EIP的值变为0x00401071 函数返回
函数执行完毕
/*
函数调用结束后恢复堆栈 使其堆栈平衡 回到函数调用之前的堆栈状态
这里属于外平衡 因为_cdecl函数调用约定栈平衡的方式为外平衡
_stdcall内平衡的话会在返回时调用 ret 8 这个指令
*/
00401071 add esp,8 //此时堆栈中只剩下参数A和B,
//因为A,B一共占八个字节所以加8
为什么需要栈平衡?
在程序运行过程中,栈内空间会被函数重复利用,如果函数调用只申请栈空间而不释放它,那么随着函数调用次数的增加,栈内存会很快就会耗光,程序会因此无法正常运行。平衡栈的操作,目的是保证函数调用会栈顶位置和函数调用前位置一致,这样就可以重复利用栈的内存空间了。
需要注意的是,在X64环境下,某些汇编指令对栈顶的对齐值有要求,因此VS编译器在申请栈内存时,会尽量保证栈地址的对齐值可以被16整除。
这里说一下rep 和 stos指令
rep是重复执行指令,其重复执行次数与ECX寄存器的值有关,等同于
START:
DEC ECX
JNZ START
stos是串储存指令,是将EAX中得数据传送到目的地址默认为ES:[EDI]寄存器,等同于
MOV ES:[EDI],EAX //ES附加段地址寄存器
ADD EDI,4 //或者 SUB EDI,4