老钟古

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

一、前言

这篇文章主要是来自CodeProject的一篇文章,当然我也推荐这个网址给读者。文章包括自己在学习这篇文章过程的一些想法和心得体会。另外想说的是在学习过程中,如果碰到自己有哪些概念不懂的,我们可以从汇编角度来进行理解可能会有更好的收获,当然这个前提需要你具备一定的汇编能力,最好有反汇编的经验。很多知识需要深入的去学习,要有死磕的精神。

 

二、正文内容

   一般来说,我们都有见过在函数前面有时候会有一些奇怪的说明符,比如__cdecl __stdcall __fastcall WINAPI等等,当然还有PascalC++成员函数调用thiscall。从MSDN或者其它的参考资料中,你可能会发现那些说明符指的就是函数的调用约定。在这篇文章中我会解释不同的调用约定在VC++6.0中表现形式。上面的前四个调用约定都是Microsoft制定的,如果你想编写可移植的代码就最好不要在函数前面使用这些说明符。

    函数的调用约定究竟是什么呢?当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。对于编写良好的程序是否要一定知道函数的调用约定呢?这并不完全是,但是了解函数的调用约定对于调试程序却很有帮助,当然在反汇编和逆向工程这是必须要掌握的。因为程序一般是由很多函数或例程组合起来的,对函数的调用在反汇编中就显得尤为重要了。所以明白函数的调用约定还是有必要的。

 

阅读这篇文章之前,如果你有一定的汇编知识将会很有帮助。当然汇编知识也是很基础的。相信看懂是没问题的。

无论哪个函数调用约定被使用,在函数调用的时候下面的几件事情都会发生。

1、所有的参数都被扩展为4个字节(在Win32环境下,其实等下下面看到汇编代码的时候我会解释一下),这些参数会被放在适当的内存地址,一般他们都是放在栈中的,但也有可能是在寄存器中,这就由函数的调用约定来指定了。

2、函数的执行流程会跳转到被调用函数的地址处。

3、在函数内部,ESIEDIEBXEBP寄存器都会被保存在栈中,用来保存这些寄存器的代码被叫做函数的开端(function prolog),这些代码都是由编译器自动生成的。

4、函数块内的代码会被执行,如果有会返回值的话,返回值是有EAX寄存器返回的。

5、寄存器ESIEDIEBXEBP在栈中被恢复(即从栈中弹出),执行这个操作的代码被叫做函数收尾(function epilog),与函数的开端一样,是由编译器自动生成的。

6、参数从栈中移除,这个叫做栈的清理,即平衡堆栈,这个操作有可能是由调用函数或者是被调用的函数内部进行处理,当然这些都还是由函数的调用约定来指定的。

接下来我们就对逐个函数的调用约定进行分析,从VC++6.0中的反汇编代码结合来理解这样就对调用约定有更加深刻的理解,当然如果我们如果在语言上的某些概念有不清楚的地方,我们都可以从汇编的角度来进行分析,这样可能会收到更好的收获。

前面四个的函数调用约定都是采用下面的源程序来进行说明的,

#include <stdio.h>

 

int sumExample(int a, int b)

{

       return (a + b);

}

 

int main()

{

       int c = 0;

       c = sumExample(2, 3);

 

       return 0;

}

具体的调用约定会在函数面前加上相应的调用约定说明符。

 

1__cdecl调用约定

__cdecl调用约定的主要特征是:

1、参数是从右向左传递的,也是放在堆栈中。

2、堆栈平衡是由调用函数来执行的(在call B,之后会有add esp xx表示参数的字节数)。

3、函数的前面会加一个前缀__sumExample

下面来看看具体的反汇编代码,这是从VC反汇编的代码截取的一部分代码。

 

10:       int c = 0;

00401088 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0

11:       c = sumExample(2, 3);

0040108F 6A 03                push        3

00401091 6A 02                push        2

从上面的两个push操作我们就可以知道参数是从右向左传递的了。另外这里也回答前面的问题为什么参数会被扩展为4个字节,因为堆栈的操作都是对一个字进行操作的,所以参数都是4个字节的。

 

00401093 E8 7C FF FF FF         call        @ILT+15(_Max) (00401014)

这里就是调用函数操作了。在进行call操作之后,会自动将call的下一条语句作为函数的返回地址保存在栈中,也就是下面的(00401098)在地址00401014处我们可以看到这样的一小段代码

@ILT+0(_Max):

00401005 E9 26 00 00 00       jmp         _sumExample (00401030)

这里就可以知道程序在编译之后会在函数前面加上前缀_

00401098 83 C4 08             add         esp,8  

这里就是平衡堆栈操作了。可以看出是在调用者进行的。

0040109B 89 45 FC             mov         dword ptr [ebp-4],eax

保存值是由eax寄存器返回的,从这里就可以看出了。

12:

13:       return 0;

0040109E 33 C0                xor         eax,eax

14:   }

 

3:    int __cdecl sumExample(int a, int b)

4:    {

00401030 55                   push        ebp

00401031 8B EC                mov         ebp,esp

00401033 83 EC 40              sub         esp,40h 为局部变量预留空间

00401036 53                   push        ebx

00401037 56                   push        esi

00401038 57                   push        edi

00401039 8D 7D C0             lea         edi,[ebp-40h]

0040103C B9 10 00 00 00        mov         ecx,10h

00401041 B8 CC CC CC CC      mov         eax,0CCCCCCCCh

00401046 F3 AB                rep stos    dword ptr [edi]

这上面的一段代码就是函数的开端了。也就是function prolog。通过将一些寄存器来对它们进行保存,也就像中断发生后,需要保护现场一样

 

5:        return (a + b);

00401048 8B 45 08              mov         eax,dword ptr [ebp+8]

0040104B 03 45 0C              add         eax,dword ptr [ebp+0Ch]

6:    }

0040104E 5F                   pop         edi

0040104F 5E                   pop         esi

00401050 5B                   pop         ebx

00401051 8B E5                mov         esp,ebp

00401053 5D                   pop         ebp

00401054 C3                   ret

这里就是函数收尾,也就是function epilog

 

经过上面的分析,相信你对__cdecl调用约定有了比较清晰的认识了。但是这里我们应该想想为什么不在被调函数内进行堆栈平衡呢?在这里我们应该要考虑类似于像scanfprintf这样的函数,这里我们应该明白这两个函数的参数都是可变的,如果参数不固定的话,在被调用函数内就无法知道参数究竟使用了多少个字节,所以为了实现可变参数,我们必须要在被调函数执行之后我们才知道参数究竟用了多少字节,所以我们在调用者来进行堆栈平衡操作。在后面我们将要对printf函数内部是怎么实现做一些探究。

 

2__stdcall调用约定

Win32 API函数绝大部分都是采用__stdcall调用约定的。WINAPI其实也只是__stacall的一个别名而已。

#define WINAPI __stdcall

还是与上面一样,我们在函数的面前用__stdcall作为修饰符。此时函数将会采用__stdcall调用约定

int __stdcall sumExample (int a, int b);

__stdcall调用约定的主要特征是:

1、参数是从右往左传递的,也是放在堆栈中。

2、函数的堆栈平衡操作是由被调用函数执行的。

3、在函数名的前面用下划线修饰,在函数名的后面由@来修饰并加上栈需要的字节数的空间(_sumExample@8)。

main函数

push 3

push 2

这两个push可以说明函数的参数是由右向左传递的。

 

call _sumExample@8   //调用函数

mov dword ptr [c], eax //eax寄存器保存函数的返回值,此时将返回值赋值给局部变量c

 

再来看看函数的代码。

函数的开端与__cdecl调用约定是相同的

mov eax, dword ptr [a]

add   eax, dword ptr [b]

函数的收尾也是和__cdecl调用约定是相同的

另外在最后面将对堆栈进行平衡操作。

ret 8   //两个4字节的参数

 

上面的是文章本来的说明,但在VC中却好像有点区别。

main函数

0040108F 6A 03                push        3

00401091 6A 02                push        2

00401093 E8 81 FF FF FF      call        @ILT+20(_sumExample) (00401019)

00401098 89 45 FC             mov         dword ptr [ebp-4],eax

 

sumExample函数

5:        return (a + b);

00401048 8B 45 08             mov         eax,dword ptr [ebp+8]

0040104B 03 45 0C             add         eax,dword ptr [ebp+0Ch]

 

00401054 C2 08 00             ret         8    //堆栈平衡操作

 

因为栈的清理(堆栈平衡操作)是由被调用函数执行的。所以使用__stdcall调用约定生成的可执行文件要比__cdecl的要小,因为在每次的函数调用都要产生堆栈清理的代码。函数具有可变参数像我wsprintf这个函数,与前面的prinf一样,都必须使用__cdecl调用约定,因为只有调用者才知道参数的数量在每一次的函数调用,因此也只有调用者才能够执行堆栈清理操作。

 

3__fastcall调用约定

__fastcall见名知其意,其特点就是快。__fastcall函数调用约定表明了参数应该放在寄存器中,而不是在栈中,VC编译器采用调用约定传递参数时,最左边的两个不大于4个字节(DWORD)的参数分别放在ecxedx寄存器。当寄存器用完的时候,其余参数仍然从右到左的顺序压入堆栈。像浮点值、远指针和__int64类型总是通过堆栈来传递的。

 

下面来看看使用测试的源代码

#include <stdio.h>

 

int __fastcall sumExample(int a, int b, int c)

{

       return (a + b + c);

}

 

double __fastcall sumExampled(double a, double b)

{

       return (a + b);

}

 

int main()

{

       int c = 0;

       double d = 0.0;

       c = sumExample(2, 3, 5);

 

       d = sumExampled(2.3, 2.5);

 

       return 0;

}

 

15:       int c = 0;

004010C8 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0

16:       double d = 0.0;

004010CF C7 45 F4 00 00 00 00 mov         dword ptr [ebp-0Ch],0

004010D6 C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0

17:       c = sumExample(2, 3, 5);

004010DD 6A 05                push        5

004010DF BA 03 00 00 00       mov         edx,3

004010E4 B9 02 00 00 00       mov         ecx,2

004010E9 E8 26 FF FF FF       call        @ILT+15(@sumExample@8) (00401014)

004010EE 89 45 FC             mov         dword ptr [ebp-4],eax

18:

19:       d = sumExampled(2.3, 2.5);

004010F1 68 00 00 04 40       push        40040000h

004010F6 6A 00                push        0

004010F8 68 66 66 02 40       push        40026666h

004010FD 68 66 66 66 66       push        66666666h

00401102 E8 FE FE FF FF       call        @ILT+0(@sumExampled@16) (00401005)

00401107 DD 5D F4             fstp        qword ptr [ebp-0Ch]

 

 

 

 

 

 

三、学习总结

 从这篇文章的学习,自己对函数的调用约定有了较深刻的理解,对于一些特殊函数比如printfwsprintf这些能够实现不定参数调用的函数,我们不仅需要知道他们的函数调用约定,重要的是能够明白其内部是怎么实现的,这就是深入学习了,能够做到举一反三。如果不进行深入的学习,应该为自己使用STLsort排序,却不懂得快速排序或者写不出冒泡算法而悲哀,使用map关联容器却没有了解或者实现过红黑树而悲哀。

 

四、参考文献

《加密解密第三版》   段钢编著 推荐

http://www.codeproject.com/KB/cpp/calling_conventions_demystified.aspx

http://www.cppblog.com/xyjzsh/archive/2010/11/11/133333.html

http://blogold.chinaunix.net/u2/79914/showart_2002034.html

posted on 2011-03-06 22:16  老钟古  阅读(1185)  评论(0编辑  收藏  举报