本节将给出 Windows 环境中两种最常用的 32 位编程调用规范。首先是 C 语言发布的 C 调用规范,该语言用于 Unix 和 Windows。然后是 STDCALL 调用规范,它描述了调用 Windows API 函数的协议。这两种规范都很重要,因为在 C 和 C++ 程序中会调用汇编函数, 同时汇编语言程序也会调用大量的 Windows API 函数。
C 调用规范用于 C 和 C++ 语言。子程序的参数按逆序入栈,因此,C 程序在调用如下函数时,先将 B 入栈,再将 A 入栈:
C 调用规范用一种简单的方法解决了清除运行时堆栈的问题:程序调用子程序时,在 CALL 指令的后面紧跟一条语句使堆栈指针(ESP)加上一个数,该数的值即为子程序参数所占堆栈空间的总和。下面的例子在执行 CALL 指令之前,将两个参数(5 和 6)入栈:
Example1 PROC
push 6
push 5
call AddTwo
add esp, 8 ;从堆栈移除参数
ret
Example1 ENDP
因此,用 C/C++ 编写的程序在从子程序返回后,总是能把参数从堆栈中删除。
另一种从堆栈删除参数的常用方法是使用名为 STDCALL 的规范。如下所示的 AddTwo 过程给 RET 指令添加了一个整数参数,这使得程序在返回到调用过程时,ESP 会加上数值 8。这个添加的整数必须与被调用过程参数占用的堆栈空间字节数相等:
AddTwo PROC
push ebp
mov ebp,esp ;堆栈帧基址
mov eax, [ebp + 12 ] ;第二个参数
add eax, [ebp + 8 ] ;第一个参数
pop ebp
ret 8 ;清除堆栈
AddTwo ENDP
要说明的是,STDCALL 与 C 相似,参数是按逆序入栈的。通过在 RET 指令中添加参数,STDCALL 不仅减少了子程序调用产生的代码量(减少了一条指令),还保证了调用程序永远不会忘记清除堆栈。
另一方面,C 调用规范则允许子程序声明不同数量的参数,主调程序可以决定传递多少个参数。C 语言的 printf 函数就是一个例子,它的参数数量取决于初始字符串参数中的格式说明符的个数:
int x = 5;
float y = 3.2;
char z = 'Z';
printf("Printing values: %d, %f, %c", xz y, z);
C 编译器按逆序将参数入栈,被调用的函数负责确定要传递的实际参数的个数,然后依次访问参数。这种函数实现没有像给 RET 指令添加一个常数那样简便的方法来清除堆栈,因此,这个责任就留给了主调程序。
调用 32 位 Windows API 函数时,Irvine32 链接库使用的是 STDCALL 调用规范。Irvine64 链接库使用的是 x64 调用规范。
通常,子程序在修改寄存器之前要将它们的当前值保存到堆栈。这是一个很好的做法,因为可以在子程序返回之前恢复寄存器的原始值。理想情况下,相关寄存器入栈应在设置 EBP 等于 ESP 之后,在为局部变量保留空间之前。这有利于避免修改当前堆栈参数的偏移量。
例如,假设如下过程 MySub 有一个堆栈参数。在 EBP 被设置为堆栈帧基址后,ECX 和 EDX 入栈,然后堆栈参数加载到 EAX:
MySub PROC
push ebp ;保存基址指针
mov ebp,esp ;堆栈帧基址
push ecx
push edx ;保存 EDX
mov eax,[ebp+8] ;取堆栈参数
.
.
pop edx ;恢复被保存的寄存器
pop ecx
pop ebp ;恢复基址指针
ret ;清除堆栈
MySub ENDP
EBP 被初始化后,在整个过程期间它的值将保持不变。ECX 和 EDX 的入栈不会影响到已入栈参数与 EBP 之间的位移量,因为堆栈的增长位于 EBP 的下方,如下图所示。