天天看点

[C++反汇编] 调用约定 cdecl stdcall fastcall 等

该系列文章是依据本人平时对反汇编的学习,归纳总结,所做的学习笔记。如有错误或待改善之处,请留下您宝贵的意见或建议。

调用约定是指调用方放置函数调用所需的参数的具体位置。具体的位置可以是特定的寄存器、程序栈、亦或者寄存器和栈中。调用约定还有一个重要的任务:函数调用完成后(被调用函数结束后),是谁(调用方还是被调用方)完成栈平衡工作(也就是清理栈中的参数)。有的调用约定由调用方完成栈平衡工作(如C调用约定、C++调用约定(g++)),而有的调用由被调用方完成栈的平衡工作(如stdcall调用约定、fastcall调用约定、C++调用约定(MS Visual C/C++))。遵守指定的调用约定对于维护栈指针的平衡与完整性有重要的作用。下面我们就一一介绍这几种调用约定。其中的例子代码来源于《IDA pro权威指南(第2版)》一书。

1.     C调用约定(cdecl调用约定)

这是X86体系结构中,许多C编译器默认的调用约定。在C/C++程序中,常用_cdecl修饰符迫使编译器使用C调用约定。

cdecl调用约定的规则是:调用方按从右到左的顺序将参数压入栈中,在被调用方完成操作后,由调用方负责完成栈平衡工作。

参数从右到左入栈的一个好处是:如果函数被调用,最左边的(第一个)参数将始终位于栈顶,这样,无论函数需要多少个参数,都能轻易的取到第一个参数。因此,cdecl调用约定很适合那些参数不定的函数,如printf。

由于需要调用方进行栈平衡,所以在函数调用返回后,有立即对栈指针进行调整的操作。如果函数的参数是可变的,那么由调用方完成栈平衡工作看起来更为合适,因为调用方清楚传递了多少个参数,可以轻松的做出调整;而被调用方无法事先知道接受了多少个参数,很难进行调整。

下面一个例子,用于说明cdecl调用约定。

函数的原型为:

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

该函数默认情况下使用cdecl调用约定,要求从右到左传递参数,并且由调用方完成栈平衡工作。可能生成的汇编代码如下:

;demo_cdecl(1,2,3,4);
push 4               ;pushz
push 3               ;pushy
push 2               ;pushx
push 1               ;pushw
call demo_cdecl      ;callthe function
add esp, 16      ;adjustesp to its former value
           

首先在2-5行,参数从右到左入栈,栈指针(esp)变化了16个字节(在32位机上,4*sizeof(int) =16)函数返回后,第7行对esp进行了调整。

还有一种方式就是,在函数调用之前,编译器在栈顶预先分配16个字节的空间,在参数入栈后,并不需要调整栈指针,在函数返回后也不需要调整栈指针。

这正是GUN编译器(gcc/g++)使用的函数入栈方式,但无论是哪一种方式,栈指针都会指向第一个参数。汇编代码如下:

;demo_cdecl(1,2,3,4)
mov [esp+12], 4	;mov z to the stack
mov [esp+8], 3		;mov y to the stack
mov [esp+4], 2		;mov x to the stack
mov [esp], 1		;mov w to the stack
call demo_cdecl	;call function
           

2.     “标准”调用约定(stdcall调用约定)

这里的标准,只是微软为自己的调用约定所起的名称,而不是传统意义上的标准。stdcall调用约定使用修饰符:_stdcall,如下:

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

与cdecl调用约定一样的是,stdcall的调用约定也按从右到左的顺序传递参数;而区别在于,函数执行结束时,由被调用的函数负责完成函数栈的平衡工作,但是对于被调用的函数而言,要想在执行结束时完成这项工作,必须清楚的知道栈中有多少个参数,所以这只有在函数接收的参数固定不变时,被调用函数才能完成这项工作。因此,想printf这样的参数可变的函数,不能使用stdcall调用约定。

如上,demo_stdcall函数需要4个参数,在栈上共占用了16个字节(这是在32位机上4*sizeof(int) =16)。x86编译器能够使用RET指令的一种特殊形式,同时从栈顶取出返回地址,并给栈指针加上16,已完成栈的平衡工作,可能的RET这令为:

ret 16        ;returnand clear 16bytes from the stack
           

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

根据惯例,微软对所有由DLL文件输出的参数数量固定的函数使用stdcall调用约定。

3.     X86 fastcall调用约定

fastcall约定是stdcall的一种变体。它向CPU寄存器(而非函数栈)传递最多两个参数。MicrosoftVisual C/C++和GNU gcc/g++(3.4及更低版本)编译器能够识别函数声明中的fastcall修饰符。如果指定使用fastcall调用约定,则传递给函数的前两个参数分别位于ECX和EDX寄存器中,剩余的参数则以类似于stdcall调用约定的方式从右到左入栈。同样,fastcall是由被调用函数完成栈平衡工作,如下例子:

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

编译器可能产生的代码如下:

;demo_fastcall(1,2,3,4)
push	4
push	3
mov	edx, 2
mov	ecx, 1
call	demo_fastcall
           

所以,虽然函数有4个参数,在由被调用函数清理参数时,是需要清理最后两个参数即可。

4.     C++调用约定

C++类中的非静态成员函数与标准函数不同,它们需要使用this指针,该指针指向用于调用函数的对象。由于调用函数的对象的地址必须由调用方提供,所以,在调用非静态成员函数时,this指针必须作为参数传递给被调用函数。C++语言标准并没有规定具体的传递细节,所以,不同的编译器采用不同的技巧来传递this指针。

MicrosoftVisual C/C++提供thiscall调用约定,它将this指针传递到ECX寄存器中。并且与stdcall一样,由非静态成员函数清除参数。GNU g++编译器将this看成是任何非静态成员函数的第一个隐含参数,其他方面则与cdecl调用约定相同。

5.     其他调用约定

继续阅读