• 结合VC6讲下对调用约定的理解


      调用约定(Calling Convertions)定义了程序中调用函数的方式,决定了在函数调用时数据在堆栈中的组织方式。如参数的压栈顺序和堆栈清理工作。这里结合VC 6.0,根据具体的小程序讲解三种调用约定:cdecl,stdcall,fastcall。(还有一些其他的调用约定,俺就浅尝辄止了)

      首先在VC6.0中创建一个项目,程序代码如下:

    View Code
    1#include <stdio.h>
    2#include <string.h>
    3
    4void
    5age_print(
    6 char* name,
    7 int age
    8)
    9{
    10 printf("%s is %d years old.\n", name, age);
    11}
    12
    13int main(void)
    14{
    15 char name[128];
    16 int age;
    17
    18 strcpy(name, "joe");
    19 age = 100;
    20
    21 age_print(name, age);
    22
    23 return 0;
    24}

      如何在VC6中设置调用约定呢?Project->Setting->C/C++,在category中选择Code Generation。可以看到在下面出现一个下拉框,名称是Calling convention,即可选择调用约定的方式,支持上面讲的三种方式:cdecl,stdcall,fastcall。

          我们通过VC的调试方式看每种调用约定时堆栈的情况。

    1、cdecl

    此种方式下的汇编代码如下所示:

    View Code
    118: strcpy(name, "joe");
    20040108E push offset string "joe" (00422038)
    300401093 lea eax,[ebp-80h]
    400401096 push eax
    500401097 call strcpy (004011b0)
    60040109C add esp,8
    719: age = 100;
    80040109F mov dword ptr [ebp-84h],64h
    920:
    1021: age_print(name, age);
    11004010A9 mov ecx,dword ptr [ebp-84h]
    12004010AF push ecx
    13004010B0 lea edx,[ebp-80h]
    14004010B3 push edx
    15004010B4 call @ILT+0(_age_print) (00401005)
    16004010B9 add esp,8

      由strcpy(name, "joe");对应的汇编代码,可见name的值为ebp-80h;

      由age=100;对应的汇编代码可见age的地址为ebp-84h。

      执行到age_print(name,age)时,对应的汇编代码是首先push age,然后push name,由此可见cdecl方式下,参数是从右到左进行进栈的。最后通过call调用age_print,age_print对应的汇编语言具体如下:

    View Code
    14: void
    25: age_print(
    36: char* name,
    47: int age
    58: )
    69: {
    700401020 push ebp
    800401021 mov ebp,esp
    900401023 sub esp,40h
    1000401026 push ebx
    1100401027 push esi
    1200401028 push edi
    1300401029 lea edi,[ebp-40h]
    140040102C mov ecx,10h
    1500401031 mov eax,0CCCCCCCCh
    1600401036 rep stos dword ptr [edi]
    1710: printf("%s is %d years old.\n", name, age);
    1800401038 mov eax,dword ptr [ebp+0Ch]
    190040103B push eax
    200040103C mov ecx,dword ptr [ebp+8]
    210040103F push ecx
    2200401040 push offset string "%s is %d years old.\n" (0042201c)
    2300401045 call printf (004010f0)
    240040104A add esp,0Ch
    2511: }
    260040104D pop edi
    270040104E pop esi
    280040104F pop ebx
    2900401050 add esp,40h
    3000401053 cmp ebp,esp
    3100401055 call __chkesp (00401170)
    320040105A mov esp,ebp
    330040105C pop ebp
    340040105D ret

      由最后可以看到直接执行了ret,这说明age_print函数本身没有进行堆栈的清理工作,同时,我们可以在main的汇编代码中看到,在执行了call之后,执行了add  esp,8指令,这说明清理了两个参数的空间,由此可见cdecl的堆栈清理工作是由调用函数进行的。

      由此我们总结出cdecl调用约定的规则:参数进栈顺序是从右到左的,堆栈的清理工作是由调用者进行清理的。

      对于参数的进栈顺序,在《Reversing Secrets of Reverse Engineering》中作者是在APPEND C中这么描述的,

          The first parameter is pushed onto the stack first, and the last parameter is pushed last.

      但是结果和作者所说相反,查了下wiki:

      The cdecl calling convention is used by many C systems for the x86 architecture.[1] In cdecl, function parameters are pushed on the stack in a right-to-left order.

    http://en.wikipedia.org/wiki/X86_calling_conventions#cdecl

      好吧,这个问题到此结束。

    2、fastcall

      fastcall顾名思义是快速调用的意思。为什么叫这个名字?让我们来看相应的汇编代码就了然了。直接给出main函数调用age_print对应的汇编代码:

    View Code
    1 21: age_print(name, age);
    2  004010A9 mov edx,dword ptr [ebp-84h]
    3  004010AF lea ecx,[ebp-80h]
    4  004010B2 call @ILT+10(_age_print) (0040100f)

      由此可见,参数并没有进行压栈操作,而只是传递给了edx和ecx寄存器,直接使用寄存器进行存储参数,大家应该明白为什么fast了吧,fastcall的约定通常是使用ecx和edx分别接受第一个和第二个参数。

       对于stdcall,有兴趣的可以自己验证下,呵呵,就写这么多吧。

  • 相关阅读:
    Iaas、Paas、Saas对比分析
    一个不一样的Python教程
    传销的那些年
    availableProcessors is already set to [8], rejecting [8]
    脚本是个好东西
    《设计模式之禅》之桥梁模式
    博文视点之传奇程序员修炼之道观后感
    《设计模式之禅》之享元模式
    《设计模式之禅》之解释器模式
    Git修改commit提交信息
  • 原文地址:https://www.cnblogs.com/nocode/p/2005799.html
Copyright © 2020-2023  润新知