6.2 各种调用方式的考察
6.1 节介绍了栈结构的相关知识,进入函数时会打开栈空间,退出函数时会还原栈空间。在VC++中,通常使用栈来传递函数参数,因此传递函数的栈也属于被调用函数栈空间中的一部分。那么它又是如何平衡的呢?汇编过程中通常使用“ret xxxx”来平衡参数所使用的栈空间,当函数的参数为不定参数时,函数自身无法确定参数所使用的栈空间的大小,因此无法由函数自身执行平衡操作,需要此函数的调用者执行平衡操作。为了确定参数的平衡者,以及参数的传递方式,于是有了函数的调用约定。VC++环境下的调用约定有三种:_cdecl、_stdcall、_fastcall。这3种调用约定的解释如下:
_cdecl:C\C++默认的调用方式,调用方平衡栈,不定参数的函数可以使用。
_stdcall:被调方平衡栈,不定参数的函数无法使用。
_fastcall:寄存器方式传参,被调方平衡栈,不定参数的函数无法使用。
当函数参数个数为0时,无需区分调用方式,使用_cdecl和_stdcall都一样。而大部分函数都是有参数的,那么该如何分析出它们的调用方式呢?通过查看平衡栈即可还原对应的调用方式。那么_cdecl与_stdcall这两种调用方式又有什么区别呢?我们通过代码清单6-3对二者进行分析对比,找出其中的差别。
代码清单6-3_cdecl与_stdcall的对比—Debug版
//C++源码说明:_cdecl、_stdcall两种调用方式的区别
void_stdcall ShowStd(int nNumber){//使用_stdcall调用方式,被调方平衡栈
printf("%d\r\n",nNumber);
}
void_cdecl ShowCde(int nNumber){//使用_cdecl调用方式,调用方平衡栈
printf("%d\r\n",nNumber);
}
void main(){
ShowStd(5);//不会有平衡栈操作
ShowCde(5);//函数调用结束后,对esp平衡4
}
//C++源码于对应汇编代码讲解
//C++源码对比,_stdcall调用方式
void_stdcall ShowStd(int nNumber)
;略去部分代码
{
//printf函数实现略
printf("%d\r\n",nNumber);
}
;略去部分代码
00401059 ret 4;结束后平衡栈顶4,等价esp+=4
//C++源码对比,_cdecl调用方式
void_cdecl ShowCde(int nNumber)
;略去部分代码
{
//printf函数实现略
printf("%d\r\n",nNumber);
}
//printf函数实现略
004010A9 ret;没有平衡操作
//C++源码对比,使用_stdcall方式调用函数ShowStd
ShowStd(5);
0040B7C8 push 5;函数传参,使用push指令esp-4
0040B7CA call@ILT+10(show)(0040100f);没有对esp操作的指令
//C++源码对比,使用_cdecl方式调用函数ShowCde
ShowCde(5);
0040B7CF push 5;函数传参,使用push指令esp-4
0040B7D1 call@ILT+15(ShowCde)(00401014)
0040B7D6 add esp,4;esp+=4,平衡栈顶
通过对代码清单6-3的分析可以得知,_cdecl调用方式在函数内没有任何平衡参数操作,而在退出函数后对esp执行了加4操作,从而实现栈平衡,_stdcall调用方式则与之相反。那么,是不是只要检查ret处是否有平衡操作即可得知函数的调用方式呢?由于汇编语言灵活多变,这种方法无法保证分析结构的正确性。在函数的结尾处,很有可能会有其他汇编指令间接地对esp做加法,如pop ecx这样的指令也可达到栈平衡效果,而且指令周期较短。因此,还需要结合函数在执行过程中使用的栈空间,与函数调用结束时的栈平衡数进行对比,以判断是否实现参数平衡。
C语言中经常使用的printf函数就是典型的_cdecl调用方式,由于printf的参数可以有多个,所以只能以_cdecl方式调用。那么,当printf函数被多次使用后,会在每次调用结束后进行栈平衡操作吗?在Debug版下,为了匹配源码会这样做。而经过O2选项的优化后,会采取复写传播优化,将每次参数平衡的操作进行归并,一次性平衡栈顶指针esp。示例如代码清单6-4所示。
代码清单6-4_cdecl参数平衡代码的复写传播优化—Release版
//C++源码说明:复写传播
void main(){
printf("Hello");//函数调用结束后,执行eps+4平衡参数
printf("World");//同上
printf("C++");//同上
printf("\r\n");//同上,经过优化后,会将4次平衡归并为1次
}
;Release版的反汇编代码信息
push offset Format;"Hello"
call_printf;调用结束后没有平衡栈
push offset aWorld;"World"
call_printf;调用结束后没有平衡栈
push offset aC;"C++"
call_printf;调用结束后没有平衡栈
push offset asc_406030;"\r\n"
call_printf
add esp,10h;一次性对esp加16,正好平衡了之前的4个参数
retn
通过以上分析发现,_cdecl与_stdcall只在参数平衡上有所不同,其余部分都一样。但经过优化后,_cdecl调用方式的函数在同一作用域内多次使用,会在效率上比_stdcall高一点,这是因为_cdecl可以使用复写传播,而_stdcall都在函数内平衡参数,无法使用复写传播这种优化方式。在这三种调用方式中,_fastcall调用方式的效率最高,其他两种调用方式都是通过栈传递参数,唯独_fastcall可以利用寄存器传递参数。但由于寄存器数目很少,而参数相比可以很多,只能量力而行,故_fastcall调用方式只使用了ecx和edx,分别传递第一个参数和第二个参数,其余参数传递则转换成栈传参方式。示例如代码清单6-5所示。
代码清单6-5_fastcall调用方式示例
//C++源码说明:_fastcall调用方式
void__fastcall ShowFast(int nOne, int nTwo, int nThree, int nFour){
printf("%d%d%d%d\r\n",nOne, nTwo, nThree, nFour);
}
void main(){
ShowFast(1,2,3,4);
}
//C++源码与对应汇编代码讲解
//C++源码对比,函数调用
ShowFast(1,2,3,4);
004012A8 push 4;使用栈方式传递参数
004012AA push 3;使用栈方式传递参数
004012AC mov edx,2;使用edx传递第二个参数2
004012B1 mov ecx,1;使用ecx传递第一个参数1
004012B6 call@ILT+15(ShowFast)(00401014)
//C++源码对比,函数说明
void_fastcall ShowFast(int nOne, int nTwo, int nThree, int nFour){
004010F0 push ebp
004010F1 mov ebp, esp
004010F3 sub esp,48h
004010F6 push ebx
004010F7 push esi
004010F8 push edi
;由于ecx即将被赋值作为循环计数器使用,在此将ecx原值保存
004010F9 push ecx
004010FA lea edi,[ebp-48h]
004010FD mov ecx,12h
00401102 mov eax,0CCCCCCCCh
00401107 rep stos dword ptr[edi]
00401109 pop ecx;还原ecx
;使用临时变量保存edx(参数2)
0040110A mov dword ptr[ebp-8],edx
;使用临时变量保存ecx(参数1)
0040110D mov dword ptr[ebp-4],ecx
//C++源码对比,printf函数调用
printf("%d%d%d%d\r\n",nOne, nTwo, nThree, nFour);
;使用ebp相对寻址取得参数4
00401110 mov eax, dword ptr[ebp+0Ch]
00401113 push eax;将eax压栈,作为参数
;使用ebp相对寻址取得参数3
00401114 mov ecx, dword ptr[ebp+8]
00401117 push ecx;将ecx压栈,作为参数
;在ebp-8中保存edx,即参数2
00401118 mov edx, dword ptr[ebp-8]
0040111B push edx;将edx压栈,作为参数
;在ebp-4中保存ecx,即参数1
0040111C mov eax, dword ptr[ebp-4]
0040111F push eax;将eax压栈,作为参数
00401120 push offset string"%d%d%d%d\r\n"(00422024)
00401125 call printf(004012e0);调用printf函数
0040112A add esp,14h;平衡pirntf使用的5个参数
}
;Debug还原环境,栈检测部分略
0040113D ret 8;此函数有4个参数,ret指令对其平衡
此段代码的Release版将更加简洁明了,这里就不再讲解了。