常用32位编程调用规范简介

< 上一页访问堆栈参数 局部变量应用下一页 >
本节将给出 Windows 环境中两种最常用的 32 位编程调用规范。首先是 C 语言发布的 C 调用规范,该语言用于 Unix 和 Windows。然后是 STDCALL 调用规范,它描述了调用 Windows API 函数的协议。这两种规范都很重要,因为在 C 和 C++ 程序中会调用汇编函数, 同时汇编语言程序也会调用大量的 Windows API 函数。

C 调用规范

C 调用规范用于 C 和 C++ 语言。子程序的参数按逆序入栈,因此,C 程序在调用如下函数时,先将 B 入栈,再将 A 入栈:

AddTwo(A, B)

C 调用规范用一种简单的方法解决了清除运行时堆栈的问题:程序调用子程序时,在 CALL 指令的后面紧跟一条语句使堆栈指针(ESP)加上一个数,该数的值即为子程序参数所占堆栈空间的总和。下面的例子在执行 CALL 指令之前,将两个参数(5 和 6)入栈:
Example1 PROC
    push 6
    push 5
    call AddTwo
    add esp, 8        ;从堆栈移除参数
    ret
Example1 ENDP
因此,用 C/C++ 编写的程序在从子程序返回后,总是能把参数从堆栈中删除。

STDCALL 调用规范

另一种从堆栈删除参数的常用方法是使用名为 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 的下方,如下图所示。

< 上一页访问堆栈参数 局部变量应用下一页 >