因为c语言遵循的调用约定是cdecl,咱们也自然要遵守cdecl约定了。不过为了起到对比的作用,除了介绍cdecl外,也会介绍下stdcall。
既然咱们用的是调用约定是cdecl,那对它的介绍最好让它离下一节的内容近一些,所以先说一下咱们不用的stdcall吧^_^,其实这两个差别就在于由谁来回收栈空间。
stdcall的调用约定意味着:
这两点在上文的表中大家已有所了解,下面咱们将理论实践一下,还是拿上面说过的函数举例:
1 int subtract(int a, int b); //被调用者
2 int sub = subtract (3,2); //主调用者
第1行是个函数声明,其实现已经在前面看到了,就是“return a-b”。
第2行进行函数调用,实参分别是3和2。在实际调用中,参数按照从右向左的顺序,参数b会先被压入栈,然后是参数a压入栈。在stdcall调用约定下,这个c代码被编译后的汇编语句是:
主调用者:
; 从右到左将参数入栈
1 push 2 ;压入参数b
2 push 3 ;压入参数a
3 call subtract ;调用函数subtract
以上是主调函数,现在看下被调函数subtract中做了什么。
被调用者:
1 push ebp ;压入ebp备份。
2 mov ebp,esp ;将esp赋值给ebp,
;用ebp做为基址来访问栈中参数。
3 mov eax,[ebp+0x8] ;偏移8字节处为第1个参数a。
4 add eax,[ebp+0xc] ;偏移0xc字节处是第2个参数b,
;参数a和b相加后存入eax。
5 mov esp,ebp ;为防止中间有入栈操作,用ebp恢复esp。
;本句在此例子中可有可无,属于通用代码。
6 pop ebp ;将ebp恢复
7 ret 8 ;数字8表示返回后使esp+8。
;函数返回时由被调函数清理了栈中参数。
当执行流进入到subtract后,在它的内部为了用ebp做为基址引用栈中参数,先执行了push ebp来备份ebp,再将栈指针赋给了ebp。目前栈中布局如图
大家根据上图很容易地看出ebp偏移为8字节是参数a,偏移12字节是参数b。以上代码值得说一下的是ret 8这句。stdcall是被调用者负责清理栈空间,这里的被调用者是函数subtract。也就是说,subtract需要在返回前或返回时完成。在返回前清理栈相对困难一些,清理栈是指将栈顶回退到参数之前。因为返回地址在参数之下,ret指令执行时必须保证当前栈顶是返回地址。所以通常在返回时“顺便”完成。于是ret指令便有了这样的变体,其格式为:
ret 16位立即数
这是允许在返回时顺便再将栈指针esp修改的指令。顺便说一句,由于32位下push指令不是压入字就是压入双字,所以ret的参数必须是偶数。在ret 8执行之前,当前栈顶必须是返回地址,即使没有第5行的代码,当前esp也是等同于ebp,因为之前没有任何push压栈操作,这是编译器为了通用性而加进去的,所以我们在注释中写到,此句可有可无。在经过第6行将栈顶(当前esp指向的内存)弹出到ebp之后,ebp被恢复,此时esp指向了+4字节的位置。即当前栈顶为主调函数的返回地址。结合上图,ret指令将栈顶的数据弹出到寄存器eip后,栈指针esp自加4,由于还有个参数8,所以esp又被加了8,从而跳过了参数a和b,顺利地完成了被调用者清理栈的任务。
stdcall是调用者在栈中压入参数,由被调用者回收栈空间。貌似分工很明确,配合很默契。因为被调用者知道自己需要几个参数,所以知道要回收多少栈空间。 但转念一想,凡事都要自己亲力亲为才放心,调用者压入的参数,万一被调用者忘记回收栈空间该怎么办(这一点由高级语言编译器保证,一般不会,大伙儿放心,本段这么写是为了表述下一种调用约定方式的特点),参数多了栈会溢出的。下面咱们就要介绍这种“亲力亲为”的调用约定,即:调用者自己向栈中压入参数,还是由调用者自己回收栈空间。
好啦,stdcall调用约定就到此为止。大爷再来玩哦。