首先,C假定子程序保存了下面这几个寄存器的值:EBX,ESI,EDI,EBP,CS,DS,SS,ES。这并不意味着不能在子程序内部修改他们。相反,它表示如果子程序改变了它们的值,那么子程序返回之前必须恢复它们的原始值。EBX,ESI和EDI的值不能被改变,因为C将这些寄存器用于寄存器变量。通常都是使用堆栈来保存这些寄存器的原始值。
2.函数名
大多数C编译器都在函数名和全局或静态变量前附加一个下划线字符。例如,函数名f将指定为_f,而不是f。Linux gcc编译器并不附加任何字符。在可执行的Linux ELF下,对于C函数f,你只需要简单使用函数名f即可。但是,DJGPP的gcc却附加了一个下划线。
3.传递参数
按照C调用约定,一个函数的参数将以一定顺序压入栈中,这个顺序与他们出现在函数里的顺序相反。
考虑这条C语句:printf("x=%d\n",x);下图上展示了如何编译这条语句(用等价的NASM格式)。下图下展示了执行完printf函数的开始部分后,堆栈的状态。printf函数是一个可以携带任意个参数的C语言库函数。C调用约定的规则就是专门为允许这些类型的函数而规定的。因为format字符串最后压入堆栈,所以不管有多少参数传递到函数,它在堆栈里的位置将总是EBP+8。然后printf代码就可以通过看format字符串的位置来决定需要传递多少参数和堆栈上如何找到它们。
当然,如果有错误发生,printf("x=%d\n"),printf代码仍然会将[EBP+12]中的双字输出,而这并不是x的值。
segment .data
x dd 0
format db " x=%d\n " , 0
segment .text
push dword [x] ; 将x的值压入栈中
push dword format ; 将format字符串的地址压入栈中
call _printf ; 注意下划线!
add esp, 8 ; 从栈中移除参数
x dd 0
format db " x=%d\n " , 0
segment .text
push dword [x] ; 将x的值压入栈中
push dword format ; 将format字符串的地址压入栈中
call _printf ; 注意下划线!
add esp, 8 ; 从栈中移除参数
4.计算局部变量的地址
找到定义在data或bss段的变量地址是非常容易的。基本上,连接程序做的就是这件事情。但是,要计算出在堆栈上的一个局部变量(或参数)的地址就不简单了。可是,当调用子程序时,这种需求是非常普通的。考虑传递一个变量(让我们称它为x)的地址到一个函数(让我们称它为foo)的情况。如果x处在堆栈的EBP-8的位置,你不可以这样使用:
mov eax,ebp-8
为什么?因为指令MOV储存到EAX里的值必须能有汇编器计算出来(也就是说,它最后必须是一个常量)。但是,有一条指令能做这种需求。它就是LEA(Load Effective Address)。下面的代码就能计算出x的地址并将它储存到EAX中:
lead eax,[ebp-8]
现在EAX中有了x的地址,而且当调用函数foo的时候,就可以将其压入到栈中。
5.返回值
返回值不为空的C函数执行完后会返回一个值。C调用约定规定了这个要如何去做。返回值需要通过寄存器传递。所有的整形类型(char,int,enum等)通过EAX寄存器返回。如果他们小于32位,那么储存到EAX的时候,他们将被扩展成32位。64位的值通过EDX:EAX寄存器对返回。浮点数出巡在数学协处理器的ST0寄存器中。
6.其它调用约定
所有的80x86 C编译器中都支持上面描述的标准C调用约定的规则。通常编译器也支持其它调用约定。挡雨汇编语言进行接口时,知道编译器调用你的函数时使用的是什么调用约定是非常重要的。通常,缺省时,使用的是标准的调用约定;但是,并不总是这一种情况。使用多种约定的编译器都拥有可以用来改变缺省约定的命令行开关。他们同样提供扩展的C语法来为单个函数指定调用约定。但是,各个编译器的这些扩展标准可以是不一样的。
GCC编译器允许不同的调用约定。一个函数的调用约定可以通过扩展语法__attribute__明确指定。例如,要声明一个返回值为空的函数f,它带有一个int参数,使用标准调用约定,需要使用下面的语法来声明它的圆形:
void f(int) __attribute__((cdecl));
GCC同样支持标准call调用约定。通过把cdecl替换成stdcall,上面的函数可以指定为使用这种约定。stdcall约定和cdecl约定的不同点是stdcall要求子程序将参数移除栈(和Pascal调用约定一样)。因此,stdcall调用约定只能使用在带有固定参数的函数上。
Borland和Microsoft使用一样语法来声明调用约定。他们在C代码中加上关键字_cdecl和_stdcall。这些关键字来修饰函数。在原型声明中,他们出现在函数名的前面:
void _cdecl f(int);
每种调用约定都有自己的优缺点。cdecl调用约定的主要有点是它非常简单而且灵活。它可以用于任何类型的C函数和C编译器。使用其它约定会限制子程序的可移植性。他的主要缺点是与其他约定相比它的执行较慢而且使用更多的内存。
stdcall调用约定的主要优点是相比于cdecl它使用较少的内存。在CALL指令之后,不需要清理堆栈。它的主要缺点是它不能用于可变参数的函数。