3.3 高级语言界面
高级语言的编译器为了允许混合语言编程而使用一些约定,以便一个程序的某些子程序可以用某一种语言编写,而另一些子程序则可以用另一种不同的语言编写,而且所有这些子程序都被链接到同一个程序中。这一序列的约定关系到栈框架的建立方式以及调用子程序的调用约定。
3.3.1 栈框架
栈标记包含有调用者里的返回地址和框架指针。根据对被调用者的调用是近调用还是远调用,返回地址的尺寸是不同的。近调用是在相同的段里面,因此可以通过一个当前段基地址的相对偏移量引用。远调用是在不同的段里面,所以两个段和被调用者的相对偏移都被储存。对于机器字长是两字节的体系结构来说,近调用以两字节储存调用者的相对偏移,而远调用以四字节储存段和调用者的相对偏移。寄存器 bp 被用作为框架指针,而调用者的框架指针内容被推入栈中,以便可以在子程序结束时恢复它。
进入子程序
先把寄存器 bp 的值推入栈顶 (即,把调用者的框架指针储存在栈上),然后把当前堆栈指针寄存器(sp) 复制给 bp,这样 bp 就被设置为框架指针了。下面的代码在 i80286 体系结构中被使用:
push bp | ; save old copy of bp |
mov bp, sp | ; bp == frame pointer |
分配局部数据
子程序可能在栈上给局部变量预留空间。其做法是,把栈寄存器 sp 的值减去一个恰好的字节数。例如,为了给两个整型变量分配空间,在栈上预留 4 字节:
sub sp, 4 |
保存寄存器值
对于 DOS 编译器,调用约定普遍要求子程序应该总是保存一些寄存器 si、di、ss、ds 和 bp 的数值。即使其中任何一个寄存器在被调用者子程序中被使用,因为它们的值都已被入栈了,所以能够在子程序返回之前恢复它们。举例说明,假如一个子程序使用了 si 和 di,下面的代码会在局部数据分配之后出现:
push si |
push di |
存取参数
参数是位于相对框架指针寄存器 bp 所指位置的正偏移。若要存取某参数 n,那么它相对 bp 所指位置的偏移就等于是将栈标记的尺寸加上在 bp 所指位置和参数 n 之间那些参数的尺寸和,再加上参数 n 的尺寸即可。
返回数值
使用寄存器返回数值的函数根据返回值的尺寸使用不同的寄存器。一字节的数据值在 al 寄存器中返回,两字节的在 ax 寄存器中返回,而四字节的在 dx:ax 寄存器中返回,如图 3-6 所示。
数据大小(字节) | 寄存器 |
1 | AL |
2 | AX |
4 | DX = high byte, AX = low byte |
图 3-6: 用于返回数值的寄存器约定
更大的数据数值使用下面的约定返回:
l 被 C 语言调用的函数:被调用者必须从堆中为返回值分配空间,并且将其地址放入 dx:ax。
l 被 Pascal、Fortran 或 Basic 调用的函数:调用者在栈段中为返回数值预留空间,并且把所分配空间的偏移地址入栈作为最后一个参数。因此,对于远调用,返回值的偏移地址是在 bp+6,而对于近调用则是在 bp+4,如图 3-7 所示。
退出子程序
恢复栈框架即,弹出那些在子程序入口处保存的任何寄存器、释放为局部变量预留的任何空间、恢复旧框架指针(bp),并且依照使用的约定返回。
l C 语言约定:由调用者为入栈的任何参数平衡栈。结束子程序只须一条 ret 指令即可。
l Pascal、Fortran、Basic 约定:由被调用者平衡栈,即从栈上削减参数所需字节数。使用一条 ret n 指令即可,其中 n 是参数的字节数。
图 3-7: 返回数值约定
例如,下面的代码从栈中恢复寄存器 di 和 si,通过复制 bp 给 sp 来释放局部变量的空间,通过从栈中弹出 bp 来恢复框架指针,并且使用 C 语言约定返回:
pop di | ; restore registers |
pop si | |
mov sp, bp | ; deallocate local variables |
pop bp | ; restore bp |
ret |
3.3.2 参数传递
在 Intel 体系结构上的 DOS 操作系统下,传递参数有三个不同的方法:C 语言调用约定、Pascal 语言调用约定和寄存器调用约定。在其它操作系统和体系结构中可能混合使用这些调用约定。例如,在 OS/2 里,标准调用是按照 C 语言的参数传递次序,但是由被调用者从系统调用的栈上削减参数。
C 语言调用约定
调用者负责将参数入栈,并且负责在被调用者返回以后恢复它们。参数是按照从右到左的次序入栈,以便向被调用者传递可变数目的参数。例如,对于一个 C 语言函数原型 void procX (int, char, long),以及一个调用 procX() 子过程的调用者子过程:
procN() { int i; /* bp - 8 */ char c; /* bp - 6 */ long l; /* bp - 4 */ procX (i, c, l); } |
将会产生下面的汇编代码:
push word ptr [bp-2] | ; high word of l |
push word ptr [bp-4] | ; low word of l |
push [bp-6] | ; c |
push word ptr [bp-8] | ; i |
call procX | ; call function |
add sp, 8 | ; restore the stack |
注意,由于字对齐,尽管字符 c 的尺寸只有一字节,储存它还是占了栈上的两个字节。
Pascal 语言调用约定
调用者负责把参数入栈,而被调用者在返回之前平衡栈。参数按照从左到右的次序入栈,因此在这个约定里使用固定数目的参数。对于前面的例子,在 Pascal 语言约定里,procX (i, c, l) 的调用产生如下汇编代码:
push word ptr [bp-8] | ; i |
push [bp-6] | ; c |
push word ptr [bp-2] | ; high word of l |
push word ptr [bp-4] | ; low word of l |
call procX | ; call procX (procX restores stack) |
寄存器调用约定
这个约定不把参数入栈,而是用寄存器传递它们,因此所生成的代码执行速度比较快。在子程序之间使用预定的寄存器来传递参数,而按参数的尺寸不同使用不同的寄存器。图 3-8 是 Borland Turbo C++ [Bor92] 所使用的寄存器集;最多三个参数可以用寄存器传递。远指针、联合(union)、结构和实数则被入栈。
参数类型 | 寄存器 |
字符型 | al, dl, bl |
整型 | ax, dx, bx |
长整型 | dx:ax |
近指针 | ax, dx, bx |
图 3-8: 寄存器参数传递约定
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论