该系列文章是依据本人平时对反汇编的学习,归纳总结,所做的学习笔记。如有错误或待改善之处,请留下您宝贵的意见或建议。
调用约定是指调用方放置函数调用所需的参数的具体位置。具体的位置可以是特定的寄存器、程序栈、亦或者寄存器和栈中。调用约定还有一个重要的任务:函数调用完成后(被调用函数结束后),是谁(调用方还是被调用方)完成栈平衡工作(也就是清理栈中的参数)。有的调用约定由调用方完成栈平衡工作(如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. 其他调用约定