函数调用约定

2018-11-27  本文已影响0人  Prothean

最近在研究反汇编相关的东西,看到了一些汇编中函数调用的约定,顺便记录下来。


cdecl
x86体系结构的许多C编译器使用的默认调用约定叫做C调用约定(c declaration)。cdecl调用约定规定:调用方按从右到左的顺序将函数参数放入 栈中,在被调用的函数完成其操作时,调用方(而不是被调用方)负责从栈中清除参数。从右到左在栈中放入参数的一个结果是,如果函数被调用,最左边的(第一个)参数将始终 位于栈顶。这样,无论该函数需要多少个参数,我们都可轻易找到第一个参数。因此,cdecl调用约定非常适用于那些参数数量可变的函数(如printf)。
要求调用函数从栈中删除参数,意味着你将经常看到:指令在由被调用的函数返回后,会立即对程序栈指针进行调整。如果函数能够接受数量可变的参数,则调用方非常适于进行这种调整, 因为它清楚地知道,它向函数传递了多少个参数,因而能够轻松做出正确的调整。而被调用的函数事先无法知道自己会收到多少个参数,因而很难对栈做出必要的调整。
例如我们调用一个拥有以下原型的函数:

void demo_cdecl(int w, int x, int y, int z);

默认情况下,这个函数将使用cdecl调用约定,并希望你按从右到左的顺序压入4个参数,同 时要求调用方清除栈中的参数。编译器可能会为这个函数的调用生成以下代码:

; demo_cdecl(1, 2, 3, 4);
push     4                    ;  push parameter z
push     3                    ;  push parameter y
push     2                    ;  push parameter x
push     1                    ;  push parameter w
call     demo_cdecl           ;  call the function
add      esp, 16              ;  改变 esp,因为栈是从高地址到低地址增长的

在调用函数时,栈指针都会指向最左边的参数。

stdcall
这种约定在函 数声明中使用了修饰符_stdcall,如下所示:

void _stdcall demo_stdcall(int w, int x, int y);

和cdecl调用约定一样,stdcall调用约定按从右到左的顺序将函数参数放在程序栈上。使用stdcall调用约定的区别在于:函数结束执行时,应由被调用的函数负责删除栈中的函数参数。对被调用的函数而言,要完成这个任务,它必须清楚知道栈中有多少个参数,这只有在函数接受的参数数量固定不变时才有可能。因此,printf这种接受数量可变的参数的函数不能使 用stdcall调用约定。例如,demo_stdcall函数需要3个整数参数,在栈上共占用12个字节(在 32位体系结构上为3*sizeof(int))的空间。x86编译器能够使用RET指令的一种特殊形式,同时 从栈顶提取返回地址,并给栈指针加上12,以清除函数参数。demo_stdcall可能会使用以下指 令返回到调用方:

ret 12  ;  return and clear 12 bytes from the stack

使用stdcall的主要优点在于,在每次函数调用之后,不需要通过代码从栈中清除参数,因而能够生成体积稍小、速度稍快的程序。

fastcall
fastcall约定是stdcall约定的一个变体,它向CPU寄存器(而非程序栈)最多传递两个参数。 Microsoft Visual C/C++ 和GNU gcc/g++(3.4及更低版本)编译器能够识别函数声明中的fastcall 修饰符。如果指定使用fastcall约定,则传递给函数的前两个参数将分别位于ECX和EDX寄存器 中。剩余的其他参数则以类似于stdcall约定的方式从右到左放入栈上。同样与stdcall约定类似 的是,在返回其调用方时,fastcall函数负责从栈中删除参数。下面的声明中即使用了fastcall 修饰符:

void fastcall demo_fastcall(int w, int x, int y, int z);

为调用demo_fastcall,编译器可能会生成以下代码:

; demo_fastcall(1, 2, 3, 4);
push     4                    ;  move parameter z to the second position on stack
push     3                    ;  move parameter y to the top position on stack
mov      edx, 2               ;  move parameter x to edx
mov      ecx, 1               ;  move parameter w to ecx
call     demo_fastcall        ;  call the function

注意,调用demo_fastcall返回后,并不需要调整栈,因为demo_fastcall负责在返回到调用 方时从栈中清除参数y和z。由于有两个参数被传递到寄存器中,被调用的函数仅仅需要从栈中清除8字节,即使该函数拥有4个参数也是如此,理解这一点很重要。

上一篇 下一篇

猜你喜欢

热点阅读