1.call
这个命令是访问子程序的一个汇编基本指令。也许你说,这个我早就知道了!别急请继续看完。
call真正的意义是什么呢?我们可以这样来理解:1.向堆栈中压入下一行程序的地址;2.JMP到call的子程序地址处。例如:
00401029 . E8 DA240A00 call 004A3508
0040102E . 5A pop edx
在执行了00401029以后,程序会将0040102E压入堆栈,然后JMP到004A3508地址处
2.RET
与call对应的就是RET了。对于RET我们可以这样来理解:1.将当前的ESP中指向的地址出栈;2.JMP到这个地址。
创建堆栈框架
- 如果要调用的函数有参数,将参数压入堆栈;
- 用call指令调用子程序;
- 此时子程序开始,将ebp寄存器压入栈:push ebp;
- 将ebp的值设为esp(只是为了方便以后访问参数和局部变量):mov ebp, esp;
- 若有局部变量,将esp的值减去相应的值。假设我们有3个DWORD类型的局部变量,则:sub esp, 12;
- 若有需要保存的寄存器,将要保存的寄存器压栈。
访问堆栈参数
- mov eax, [ebp + 8]
因为call指令会自动将返回地址压入堆栈,因此紧邻ebp上方的堆栈值不是参数,所以最近的参数地址是ebp + 8,而不是ebp + 4。访问其他参数类似,如[ebp + 12]等等。
清理堆栈
- 将之前保存的寄存器值以相反的顺序弹出堆栈;
- 将esp的值设为ebp的值,以销毁局部变量:mov esp, ebp;
- 将原ebp的值弹出堆栈:pop ebp;
- 一个简单的方法是在call指令后面紧跟一条add指令,将esp的值指向一个正确的地址。例如,如果我们之前压入了3个参数,那么应运行:add esp, 12;
- 另一个更好的方法是是使用STDCALL调用规定,即修改子程序代码的ret指令。如对于上面的例子,应改写ret为:ret 12。
函数调用约定
_stdcall调用约定
_cdecl调用约定(The C default calling convention,C调用规定)
_fastcall调用约定
总结
- push ebp ;保存ebp
- mov ebp, esp ;将ebp设为当前esp值
- sub esp, 4*局部变量个数 ;局部变量
- push eax ;将寄存器压栈
- push esi
- ……
- mov eax, [ebp + 8] ;得到第一个参数
- mov edi, [ebp + 12] ;得到第二个参数
- ……
- pop esi ;恢复寄存器值
- pop eax
- mov esp, ebp ;销毁局部变量
- pop ebp ;恢复ebp的值
- ret 4*被压入参数个数
对于 c 语言,函数有好几种调用规则。最常见的是两种,cdecl 方式和 stdcall 方式。
Cdecl 方式
(1)使用堆栈传递参数
(2)主程序按从右向左的顺序将参数逐个压栈,最后一个参数先入栈。每一个参数压栈一次。
(3)在子程序中,使用 [EBP+X] 的方式来访问参数。X=8 代表第 1 个参数;X=12 代表第二个参数,依次类推
(4)子程序用 RET 指令返回。
(5)由主程序执行“ADD ESP, n”指令调整 ESP,达到堆栈平衡。
(6)一般返回值放在 EAX 中
Stdcall 方式
与Cdecl的不同是,堆栈的平衡不是由主程序完成,而是由子程序通过调用“RET n”指令主动平衡。