汇编语言执行call与ret的过程
没有参数的情况:当执行段内转移时,不将cs压入栈,只压入ip;当执行段间转移时,先将cs压栈,再将ip压栈。返回时先将ip弹出,再根据有无压入cs弹出cs。
有参数的情况:先将cs、ip依次压入栈中,然后将参数从右向左依次压栈,再push bp,mov bp, sp。当返回时,pop bp, pop ip, pop cs;然后让栈顶指针sp+参数大小,即ret num。
给个考试题及答案:
函数调用类型比较
先写一个测试程序,该程序使用编译器为DevC++:
#include<stdio.h>
int __cdecl fun_cdecl(int a, int b){
int c = a + b;
return c;
}
int __stdcall fun_stdcall(int a, int b){
int c = a + b;
return c;
}
int __fastcall fun_fastcall(int a, int b){
int c = a + b;
return c;
}
int main(){
printf("here");
__asm__("nop");
fun_cdecl(1, 2);
__asm__("nop");
fun_stdcall(1, 2);
__asm__("nop");
fun_fastcall(1, 2);
__asm__("nop");
return 0;
}
其中printf(“here”);的作用在于快速找到下面三个函数的位置。使用x64dbg进行动态调试,右键搜索字符串,点击here跳转到三个函数位置,如下:
由于不同软件对于反汇编的精细程度不同,因此,同时使用了OllyDbg进行观察,如下:
下面来分析每种调用类型的区别。
注:
指令leave等价于
mov esp, ebp
由于起始部分有:
push ebp
mov ebp, esp
的操作,因此leave完成了对局部变量的回收工作
__cdecl调用
__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。
观察到,对于__cdecl调用方式,使用ret形式,要求调用者自行回收参数;且x64Dbg认为进行了压栈,而OD认为并没用执行参数压栈操作。
__stdcall调用
被这个关键字修饰的函数,其参数都是从右向左通过堆栈传递的, 函数调用参数在返回前要由被调用者清理堆栈。
观察到,对于__stdcall调用方式,使用retn 0x8形式,由被调用者回收参数;且x64Dbg认为进行了压栈,而OD认为并没用执行参数压栈操作。
__fastcall调用
规定将前两个参数由寄存器ecx和edx来传递,其余参数从右到左通过堆栈传递,函数调用参数在返回前要由被调用者清理堆栈,这与__stdcall相同。
观察到,对于__fastcall调用方式,使用retn形式,这与编译器的优化方式有关;由被调用者回收参数;且x64Dbg认为进行了压栈,而OD认为并没用执行参数压栈操作;前两个参数由ecx和edx传递,符合设计。
总结
三种不同的调用方式在各自目录下已经进行了阐述,不再赘述。由于编译环境和反汇编环境的不同,观察到反汇编结果也有很大区别;就ret而言,OD反汇编更加精细,而就参数压栈而言,x64Dbg反汇编更加精细,可见二者各有所长。
另外:x64Dbg可以调试64位程序;而OD虽然既可以在32位系统下运行,也可以在64位系统下运行,但是却只能对32位程序进行调试,因此,两者结合使用效果更佳。期待OD更新。