Assembly 调用约定
当调用了一个子程序,调用的代码和子程序(被调用的代码)必须协商好在它们之间如何传递数据。高级语言有标准传递数据的方法称为调用约定。要让高级语言接口于汇编语言,汇编语言代码就一定要使用与高级语言一样的约定。不同的编译器有不同的调用约定或者说不同的约定可能取决于代码如何被编译。(例如:是否进行了优化)。一个广泛的约定是:使用一条CALL指令来调用代码再通过RET指令返回。
所有PC的C编译器支持的调用约定将在本章的后续部分阶段进行描述。这些约定允许你创建可重入的子程序。一个可重入的子程序可以在程序中的任意一点被安全调用(甚至在子程序内部)。
在堆栈上传递参数
给子程序的参数需要在堆栈中传递。它们在CALL指令之前就已经被压入栈中了。和在C中是一样的,如果参数被子程序改变了,那么需要传递的是数据的地址,而不是值。如果参数的大小小于双字,它就需要在压入栈之
前转换成双字。
前转换成双字。
在堆栈里的参数并没有由子程序弹出,取而代之的是:它们自己从堆栈中访问本身。为什么?
1、因为它们在CALL指令之前被压入栈中,所以返回时首先弹出的是返回地址(然后修改堆栈指针使其指向参数入栈以前的值)。
2、参数往往将会使用在子程序中几个的地方。通常在整个程序中,它们不可以保存在一个寄存器中,而应该储存在内存中。把它们留在堆栈里就相当于把数据复制到了内存中,这样就可以在子程序的任意一点访问数据。
考虑通过堆栈传递了一个参数的子程序。当子程序被调用了,堆栈状态如图4.1。这个参数可以通过间接寻址访问到。([ESP+4])。
如果在子程序内部使用了堆栈储存数据,那么与ESP相加的数将要改变。例如:图4.2展示了如果一个双字压入栈中后堆栈的状态。现在参数在ESP + 8中,而不在ESP + 4中。因此,引用参数时若使用ESP就很容易犯错了。为了解决这个问题,80386提供使用另外一个寄存器:EBP。这个寄存器的唯一目的就是引用堆栈中的数据。C调用约定要求子程序首先把EBP的值保存到堆栈中,然后再使EBP的值等于ESP。当数据压入或弹出堆栈时,这就允许ESP值被改变的同时EBP不会被改变。在子程序的结束处,EBP的原始值必须恢复出来(这就是为什么它在子程序的开始处被保存的缘故。图4.3展示了遵循这些约定的子程序的一般格式。
图4.3中的第2行和第3行组成了一个子程序的大体上的开始部分。第5行和第6行组成了结束部分。图4.4展示了刚执行完开始部分之后堆栈的状态。现在参数可以在子程序中的任何地方通过[EBP + 8]来访问,而不用担心在子程序中有什么数据压入到堆栈中了。
执行完子程序之后,压入栈中的参数必须移除掉。C调用约定规定调用的代码必须做这件事。其它约定可能不同。例如:Pascal 调用约定规定子程序必须移除参数。(RET指令的另一种格式可以很容易做这件事。)一些C编译器同样支持这种约定。关键字pascal用在函数的原型和定义中来告诉编译器使用这种约定。事实上,MS Windows API的C函数使用的stdcall调用约定同样以这种方式运作。这种方式有什么优点?它比C调用约定更有效一点。那为什么所有的C函数都使用C调用约定呢?一般说来,C允许一个函数的参数为变化的个数(例如:printf和scanf函数)。对于这种类型的函数,将参数移除出栈的操作在这次函数调用中和下次函数调用中是不同的。C调用约定能使指令简单地执行这种不同的操作。Pascal和stdcall调用约定执行这种操作是非常困难的。因此,Pascal调用约定(和Pascal语言一样)不允许有这种类型的函数。MS Windows只有当它的API函数不可以携带变化个数的参数时才可以使用这种约定。
图4.5展示了一个将被调用的子程序如何使用C调用约定。第3行通过直接操作堆栈指针将参数移除出栈。同样可以使用POP指令来做这件事,但是常常使用在要求将无用的结果储存到一个寄存器的情况下。实际上,对于这种情况,许多编译器常常使用一条POP ECX来移除参数。编译器会使用POP指令来代替ADD指令,因为ADD指令需要更多的字节。但是,POP会改变ECX的值。下面是一个有两个子程序的例子,它们使用了上面讨论的C调用约定。54行(和其它行)展示了多个数据和文本段可以在同一个源文件中声明。进行连接处理时,它们将会组合成单一的数据段和文本段。把数据和文本段分成单独的几段就允许数据定义在子程序代码附近,这也是子程序
经常做的。
堆栈上的局部变量
堆栈可以方便地用来储存局部变量。这实际上也是C储存普通变量(或C lingo中的自动变量)的地方。如果你希望子程序是可重入的,那么使用堆栈存储变量是非常重要的。一个可重入的子程序不管在任何地方被调用都能正常运行,包括子程序本身。换句话说,可重入子程序可以嵌套调用。储存变量的堆栈同样在内存中。不储存在堆栈里的数据从程序开始到程序结束都使用内存(C称这种类型的变量为全局变量或静态变量)。储存在堆栈里的数据只有当定义它们的子程序是活动的时候才使用内存。
在堆栈中,局部变量恰好储存在保存的EBP值之后。它们通过在子程序的开始部分用ESP减去一定的字节数来分配存储空间。图4.6展示了子程序新的骨架。EBP用来访问局部变量。考虑图4.7中的C函数。图4.8 展示如何用汇编语言编写等价的子程序。
图4.9展示了执行完图4.8中程序的开始部分后的堆栈状态。这一节的堆栈包含了参数,返回信息和局部变量,这样堆栈称为一个堆栈帧。C函数的每一次调用都会在堆栈上创建一个新的堆栈帧。
可以使用两条专门的指令来简化一个子程序的开始部分和结束部分,它们是为这个目的而专门设计的。ENTER指令执行开始部分的代码,而LEAVE指令执行结束部分。ENTER指令携带两个立即数。对于C调用约定, 第二个操作数总是为0。第一个操作数是局部变量所需要的字节数。LEAVE指令没有操作数。图4.10展示了如何使用这些指令。注意程序skeleton同样使用了ENTER和LEAVE指令。