__cdecl、__stdcall、__fastcall 与 __pascal 浅析

短命女 2023-06-07 08:05 109阅读 0赞
  • X86调用约定 calling convention:https://www.cnblogs.com/shangdawei/p/3323252.html
  • __cdecl、__stdcall、__fastcall 与 __pascal 浅析:https://www.cnblogs.com/yenyuloong/p/9626658.html
  • 王爽 汇编语言第三版 第9章 转移指令的原理:https://blog.csdn.net/freeking101/article/details/100581181
  • 调用约定(Calling Conventions) (__cdecl、__stdcall、__fastcall) C++函数名修饰 ( Name Mangling ):http://www.3scard.com/index.php?m=blog&f=view&id=10
  • 深入体会__cdecl与__stdcall:https://www.cnblogs.com/sober/archive/2009/09/01/1558178.html
  • __stdcall 详解:https://www.cnblogs.com/songfeixiang/p/3733661.html
  • 用od分析_cdecl和_stdcall调用惯例的差异:https://blog.csdn.net/tch3430493902/article/details/101366850
  • X86调用约定:https://zh.wikipedia.org/wiki/X86调用约定

    1. C语言中,假设我们有这样的一个函数:int function(int a,int b);调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。
    2. 栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改栈,使堆栈恢复原装。
    3. **在参数传递中,有两个很重要的问题必须得到明确说明:当参数个数多于一个时,按照什么顺序把参数压入堆栈函数调用后,由谁来把堆栈恢复原装。在高级语言中,通过函数调用约定来说明这两个问题。**常见的调用约定有:\_\_stdcall\_\_cdecl\_\_fastcall\_\_thiscallnaked call

extern “C” 的含义 和 __stdcall/__cdecl的区别

  extern “C” 和 __stdcall、__cdecl 这两个概念都是C和C++语言混用时需要关注的。extern “C”是代码段的修饰, 既可以单独对函数进行修饰也可以放在代码片段前对整段代码进行修饰;是告知编译器接下来的代码中所有的函数名要以C语言的方式进行解析;_stdcall和_cdecl则是对函数名进行修饰,告知编译器函数名应该按何种方式进行解析。

为什么要加extern C

  工程中经常会遇到C和C++混合编程的情况,有时是C++的工程中需要使用C语言编写的库,有时是C的工程需要使用C++;但如果不加任何修饰,直接调用的话,就会遇到链接问题,提示找不到函数名称。
  这是由于C++在编译时候,会将函数名做一些修饰,在函数名前加上函数名的长度,在函数名后面加上参数类型。链接的时候也使用相同的策略。C++这么做是由于C++语言支持函数重载,在函数名相同参数不同的几个函数也可以共存。C语言不支持重载,所以编译和链接时对函数名不会加修饰。加extern ”C“就是为了让编译器以C语言的函数名处理方式来编译。

使用场景主要有两种:

1.C++ 的模块调用 C语言写的库

C语言 test.h、test.c 创建的动态库libtest.so

#### test.h文件如下

  1. #include "stdio.h"
  2. void test_add(int a, int b);

#### test.c文件如下

  1. #include "test.h"
  2. void test_add(int a, int b)
  3. {
  4. int num = a + b;
  5. printf("%d + %d = %d\n", a, b, num);
  6. }

#### 编译命令

  1. gcc test.c -fPIC -shared -o libtest.so

#### C++ 的文件 caller_cplusplus.cpp 调用

  1. #ifdef __cplusplus
  2. extern "C"
  3. {
  4. #endif
  5. #ifdef __cplusplus
  6. #include "test.h"
  7. }
  8. #endif
  9. int main()
  10. {
  11. test_add(3, 4);
  12. return 0;
  13. }

#### 编译命令

  1. g++ -o callerCpp caller_cplusplus.cpp -L. -ltest

2. C++头文件声明接口函数

  在C中引用C++语言中的函数和变量时,C++的头文件需添加extern “C”,但是在C语言中不能直接引用声明了extern “C”的该头文件,应该仅将C文件中将C++中定义的extern “C”函数声明为extern类型。

//C++头文件 cppExample.h

  1. #ifndef CPP_EXAMPLE_H
  2. #define CPP_EXAMPLE_H
  3. extern "C" int add( int x, int y );
  4. #endif

//C++实现文件 cppExample.cpp

  1. #include "cppExample.h"
  2. int add( int x, int y )
  3. {
  4. return x + y;
  5. }

/* C实现文件 cFile.c*/
/* 这样会编译出错:#include “cExample.h” */

  1. extern int add( int x, int y );
  2. int main( int argc, char* argv[] )
  3. {
  4. add( 2, 3 );
  5. return 0;
  6. }

  上面介绍的是extern “C”的含义和使用方法,主要是跨C和C++的库调用时需要注意的地方。

CRT链接选项(C运行时库的链接选择):一个原则,调用者和库的CRT链接选项要相同,尽量都使用/MD选项 (多线程动态链接)。

__cdecl/__stdcall 是规定了函数名应该如何修饰,以及参数压栈方式,栈由谁清理

函数名修饰规则:

  • __stdcall :约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number(number即参数栈长度)
  • __cdecl :约定仅在输出函数名前加上一个下划线前缀,格式为_functionname

参数栈的管理:两种方式定义的参数传递方式都是从右至左压,区别在于清理栈的角色不同。

  • __stdcall:是被调用函数清理(即函数自己清理)
  • __cdecl:是调用者清理;

  所以__stdcall修饰函数名时加上参数栈的大小可以让函数自己进行栈的清理;对于可变参数的函数,无法在函数定义时确认参数长度,只能让调用者去清理参数栈,所以对于可变参数的函数应该用__cdecl修饰(可变参数的函数exp:printf)

调用约定(或者 调用协议) __cdecl、__stdcall 和 __fastcall 的区别

https://blog.csdn.net/a3192048/article/details/82084374

__stdcall 和 __cdecl 的区别浅析( C 代码 和 生成对应汇编代码分析 ):https://blog.csdn.net/qinrenzhi/article/details/94403385

函数的 调用约定,顾名思义就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的。它决定以下内容:

  • (1) 函数参数的压栈顺序。
  • (2) 由调用者还是被调用者把参数弹出栈。
  • (3) 以及产生函数修饰名的方法。

  • 1) __stdcall 的全称是 standard call。是 C++ 的标准调用方式。函数所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是this指针。函数返回时使用 retn X 指令,其中 X 为调整堆栈的字节数。这些堆栈中的参数由被调用的函数在返回后清除,使用的 指令是 retn X,X 表示参数占用的字节数,CPU在 ret 之后自动弹出 X 个字节的堆栈空间,称为自动清栈,这种方式 叫做自动清栈 ( 被调用的函数在返回前清理传送参数的内存栈 )即 函数在编译的时候就必须确定参数个数,并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错

  • 2) __cdecl 的全称是 C Declaration(declaration,声明),即 C语言默认的函数调用方式。函数参数的入栈顺序为从右到左依次入栈。函数返回时作用 ret 指令。被调用的函数支持 可变参数,即 被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。调用者根据调用时传入参数的个数,手动平衡堆栈,即 这些参数由调用者清除,称为手动清栈。
  • 3) __fastcall 是编译器指定的快速调用方式。由于大多数的函数参数个数很少,使用堆栈传递比较费时。因此 __fastcall 通常规定将前两个(或若干个)参数由寄存器传递,其余参数还是通过堆栈传递。不同编译器编译的程序规定的寄存器不同。返回方式和 __stdcall 相当。(实际上,它用 ECX 和 EDX 传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。
  • 4) __thiscall 是为了解决类成员调用中 this指针传递而规定的。_thiscall 要求把 this 指针放在特定寄存器中,该寄存器由编译器决定。VC 使用 ecx,Borland 的 C++ 编译器使用 eax。返回方式 和 __stdcall 相当。
  • 5) nakedcall 采用 1 - 4 的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec 共同使用。

注意 :__fastcall 和 __thiscall 涉及的寄存器由编译器决定,因此不能用作跨编译器的接口。所以 Windows 上的 COM 对象接口都定义为 __stdcall 调用方式。

用 __stdcall 定义的函数在结束时由该函数自己负责把参数弹出堆栈,windows API 都是采用 __stdcall 压栈/调用方式。
用 __cdecl 定义的函数在结束时不管参数,而由调用函数负责把参数弹出堆栈,c 的库函数都是采用 __cdecl 压栈/调用方式,在c 程序中定义的函数默认都是 __cdecl 方式。

关键字 __stdcall、__cdecl 和 __fastcall 可以直接加在要输出的函数前,也可以在编译环境的Setting…\C/C++ \Code Generation项选择。当加在输出函数前的关键字 与 编译环境中的选择不同时直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd 和 /Gr。缺省状态为 /Gd,即__cdecl

VC++ 对函数的省缺声明是 “__cedcl” ,将只能被 C/C++ 调用 。

示例代码:

  1. #include<stdio.h>
  2. #include<iostream>
  3. // 使用 __stdcall
  4. void __stdcall output_1(int x, int y)
  5. {
  6. printf("%d %d", x, y);
  7. }
  8. // VC++对函数的省缺声明是 "__cedcl", 将只能被C/C++调用.
  9. void output_2(int x, int y)
  10. {
  11. printf("%d %d", x, y);
  12. }
  13. void test_1()
  14. {
  15. __asm
  16. {
  17. mov eax, 10
  18. mov ebx, 20
  19. push eax
  20. push ebx
  21. call output_1
  22. }
  23. printf("\n");
  24. }
  25. void test_2()
  26. {
  27. __asm
  28. {
  29. mov eax, 10
  30. mov ebx, 20
  31. push eax
  32. push ebx
  33. call output_2
  34. add esp, 8; 调用者负责平衡堆栈
  35. }
  36. printf("\n");
  37. }
  38. int main(int argc, char* argv[])
  39. {
  40. test_1();
  41. test_2();
  42. return 0;
  43. }

运行结果:

20191013231239363.png

示例代码:

  1. #define _AFXDLL // 如果不定义这,需要改成单线程版本
  2. #include <afx.h> // CString 头文件
  3. #include <afxwin.h> // AfxMessageBox的头文件
  4. TCHAR appname[] = TEXT("API Test");
  5. void main()
  6. {
  7. int a = 5; //变量a
  8. _asm
  9. {
  10. mov eax, a; // 将变量a的值放入寄存器eax
  11. add eax, eax; // 相当于a=a+a
  12. mov a, eax; // 将 a+a 的结果赋给a
  13. }
  14. //查看结果,注意a的初值为5
  15. CString rst;
  16. rst.Format(_T("a=%d"), a);
  17. AfxMessageBox(rst);
  18. }

使用规则 和 设置方法

1、修饰名 ( Decoration name ),即编译之后的函数名。“C” 或者 “C++” 函数 在 内部(编译和链接)通过 修饰名 识别。修饰名是编译器 在 编译 函数定义 或者 原型 时生成的 字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出 “C++” 重载函数、构造函数、析构函数,又如在汇编代码里调用 “C” 或 “C++” 函数等。修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。

2、函数名修饰约定编译种类调用约定 的不同而不同,下面分别说明。

C语言编译器 的 函数名修饰约定规则:

  1. __stdcall 调用约定在输出函数名前加上一个下划线前缀,后面加上一个 "@" 符号和其参数的字节数,
  2. 格式为: _functionname@number
  3. 例如: function(int a, int b),其修饰名为:_function@8
  4. __cdecl 调用约定仅在输出函数名前加上一个下划线前缀,
  5. 格式为: _functionname
  6. __fastcall 调用约定在输出函数名前加上一个 "@" 符号,后面也是一个 "@" 符号 其参数的字节数,
  7. 格式为: @functionname@number

C++语言编译器 的 函数名修饰约定规则:

  1. __stdcall调用约定:
  2. 1、以 "?" 标识函数名的开始,后跟函数名;
  3. 2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
  4. 3、参数表以代号表示:
  5. X--void
  6. D--char
  7. E--unsigned char
  8. F--short
  9. H--int
  10. I--unsigned int
  11. J--long
  12. K--unsigned long
  13. M--float
  14. N--double
  15. _N--bool
  16. ....
  17. PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以 "0" 代替,一个 "0" 代表一次重复;
  18. 4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
  19. 5、参数表后以 "@Z" 标识整个名字的结束,如果该函数无参数,则以 "Z" 标识结束。
  20. 其格式为 ?functionname@@YG*****@Z ?functionname@@YG*XZ
  21. 例如
  22. int Test1(char *var1, unsigned long) ----- ?Test1@@YGHPADK@Z
  23. void Test2() ----- ?Test2@@YGXXZ
  24. __cdecl调用约定:
  25. 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。
  26. __fastcall调用约定:
  27. 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。

__stdcall 和 __cdecl 相同点 和 不同点

相同点:参数入栈顺序相同:从右到左

不同点:

  1. 堆栈平衡方式不同:__stdcall 自动清栈,__cdecl 手动清栈。
  2. 返回指令不同:_stdcall 使用 retn x, __cdecl 使用 ret
  3. 编译后函数的修饰名不同: 假设有函数 int foo(int a, int b),采用__stdcall 编译后的函数名为 _foo@8,而采用__cdecl 编译后的函数名为_foo。

支持可变参数的函数必须定义为__cdecl,如:

int printf(char *fmt, …);

在 windef.h 中对 __stdcall 和 __cdecl 的定义

  1. #define CALLBACK __stdcall
  2. #define WINAPI __stdcall
  3. #define WINAPIV __cdecl
  4. #define APIENTRY WINAPI
  5. #define APIPRIVATE __stdcall
  6. #define PASCAL __stdcall
  7. #define cdecl _cdecl
  8. #ifndef CDECL
  9. #define CDECL _cdecl
  10. #endif

特别说明

    1. 在默认情况下,采用 __cdecl 方式,因此可以省略.
    1. WINAPI 一般用于修饰动态链接库中导出函数
    1. CALLBACK 仅用于修饰回调函数

便于更好理解, 看下面例子(函数调用的过程以汇编代码表示):

  1. //函数定义
  2. void cdecl fun1(int x,int y);
  3. void stdcall fun2(int x,int y);
  4. void pascal fun3(int x,int y);
  5. // 对应调用函数时的汇编代码
  6. ****************************************
  7. void cdecl fun1(int x,int y);
  8. fun1(x,y);
  9. 调用 fun1 的汇编代码
  10. push y
  11. push x
  12. call fun1
  13. add sp,sizeof(x)+sizeof(y) ;跳过参数区(xy
  14. fun1 的汇编代码:
  15. fun1 proc
  16. push bp
  17. mov bp,sp
  18. ……
  19. pop bp
  20. ret ;返回,但不跳过参数区
  21. fun1 endp
  22. ****************************************
  23. void stdcall fun2(int x,int y);
  24. fun2(x,y);
  25. 调用 fun2 的汇编代码
  26. push y
  27. push x
  28. call fun2
  29. fun2 的汇编代码:
  30. fun2 proc
  31. push bp
  32. mov bp,sp
  33. ……
  34. pop bp
  35. ret sizeof(x)+sizeof(y) ;返回并跳过参数区(xy
  36. fun2 endp
  37. *****************************************
  38. void pascal fun3(int x,int y);
  39. fun3(x,y);
  40. 调用 fun3 的汇编代码
  41. push x
  42. push y
  43. call fun3
  44. fun3 的汇编代码:
  45. fun3 proc
  46. push bp
  47. mov bp,sp
  48. ……
  49. pop bp
  50. ret sizeof(x)+sizeof(y) ;返回并跳过参数区(xy
  51. fun3 endp

_stdcall 与 _cdecl 两者之间的区别:

  1. WINDOWS 的函数调用时需要用到栈(STACK,一种先入后出的存储结构)。当函数调用完成后,栈需要清除,这里就是问题的关键,如何清除??
  2. 如果我们的函数使用了 \_cdecl,那么栈的清除工作是由调用者,用 COM 的术语来讲就是客户来完成的。这样带来了一个棘手的问题,不同的编译器产生栈的方式不尽相同,那么调用者能否正常的完成清除工作呢?答案是不能。
  3. 如果使用 \_\_stdcall,上面的问题就解决了,函数自己解决清除工作。**所以,在跨(开发)平台的调用中,我们都使用\_\_stdcall 虽然有时是以 WINAPI 的样子出现 )。**
  4. 那么为什么还需要\_cdecl ?当我们遇到这样的函数如 fprintf() 它的参数是可变的,不定长的,被调用者事先无法知道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用\_cdecl
  5. 到这里我们有一个结论:**如果程序中没有涉及可变参数,最好使用\_\_stdcall 关键字**

cdecl

cdecl( C declaration,即C声明 ) 是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:

  1. 函数实参在线程栈上按照从右至左的顺序依次压栈。
  2. 函数结果保存在寄存器EAX/AX/AL中
  3. 浮点型结果存放在寄存器ST0中
  4. 编译后的函数名前缀以一个下划线字符
  5. 调用者负责从线程栈中弹出实参(即清栈)
  6. 8比特或者16比特长的整形实参提升为32比特长。
  7. 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS
  8. 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS
  9. RET 指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)

    1. Visual C++ 规定 函数返回值如果是POD值且长度如果不超过32比特,用寄存器EAX传递;长度在33-64比特范围内,用寄存器EAX:EDX传递;长度超过64比特或者非POD值,则调用者为函数返回值预先分配一个空间,把该空间的地址作为隐式参数传递给被调函数。
    2. GCC的函数返回值都是由调用者分配空间,并把该空间的地址作为隐式参数传递给被调函数,而不使用寄存器EAXGCC4.5版本开始,调用函数时,堆栈上的数据必须以16B对齐(之前的版本只需要4B对齐即可)。

考虑下面的C代码片段:

  1. int callee(int, int, int);
  2. int caller(void)
  3. {
  4. register int ret;
  5. ret = callee(1, 2, 3);
  6. ret += 5;
  7. return ret;
  8. }

在x86上, 会产生如下汇编代码(AT&T 语法):

  1. .globl caller
  2. caller:
  3. pushl %ebp
  4. movl %esp,%ebp
  5. pushl $3
  6. pushl $2
  7. pushl $1
  8. call callee
  9. addl $12,%esp
  10. addl $5,%eax
  11. leave
  12. ret

在函数返回后,调用的函数清理了堆栈。 在cdecl的理解上存在一些不同,尤其是在如何返回值的问题上。结果,x86程序经过不同OS平台的不同编译器编译后,会有不兼容的情况,即使它们使用的都是“cdecl”规则并且不会使用系统调用。某些编译器返回简单的数据结构,长度大致占用两个寄存器,放在寄存器对EAX:EDX中;大点的结构和类对象需要异常处理器的一些特殊处理(如一个定义的构造函数,析构函数或赋值),存放在内存上。为了放置在内存上,调用者需要分配一些内存,并且让一个指针指向这块内存,这个指针就作为隐藏的第一个参数;被调用者使用这块内存并返回指针——返回时弹出隐藏的指针。 在Linux/GCC,浮点数值通过x87伪栈被推入堆栈。像这样:

  1. sub esp, 8 ; double值一点空间
  2. fld [ebp + x] ; 加载double值到浮点堆栈上
  3. fstp [esp] ; 推入堆栈
  4. call funct
  5. add esp, 8

使用这种方法确保能以正确的格式推入堆栈。 cdecl调用约定通常作为x86 C编译器的默认调用规则,许多编译器也提供了自动切换调用约定的选项。如果需要手动指定调用规则为cdecl,编译器可能会支持如下语法:

  1. return_type _cdecl funct();

其中_cdecl修饰符需要在函数原型中给出,在函数声明中会覆盖掉其他的设置。

call 指令与 retn 指令

  1. 首先我们得了解 CALL RETN 指令的作用,才能更好地理解调用规则,这也是先决条件。
  2. 实际上,CALL 指令就是先将下一条指令的 EIP 压栈,然后 JMP 跳转到对应的函数的首地址,当执行完函数体后,通过 RETN 指令从堆栈中弹出 EIP,程序就可以继续执行 CALL 的下一条指令。

__cdecl 与 __stdcall 调用规则

  1. C/C++ 中不同的函数调用规则会生成不同的机器代码,产生不同的微观效果,接下来让我们一起来浅析四种调用规则的原理和它们各自的异同。首先我们通过一段 C 语言代码来引导我们的浅析过程。

这里我们编写了三个函数,它们的功能都是返回两个参数的相加结果,只是每个函数都有不一样的调用规则。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZyZWVraW5nMTAx_size_16_color_FFFFFF_t_70

  1. 我们使用 printf 函数主要是为了在 OllyDBG 中能够快速下断点,以确定后边调用三个函数的位置,便于分析。在这里我给每个函数都用了内联的 NOP 指令来分隔开,图中也用红框标明,这样可以便于区分每个函数的调用过程。通过一些简单的步骤,我们用 OllyDBG 查看了编译后代码的“真面目”。代码中有 4 CALL,第一个是 printf,我们不关心这个。后面三个分别是具有 \_\_cdecl\_\_stdcall\_\_fastcall 调用规则的函数 CALL(这里我已经做了注释)。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZyZWVraW5nMTAx_size_16_color_FFFFFF_t_70 1

  1. 在这里为了循序渐进,我们先介绍 \_\_cdecl \_\_stdcall 调用规则,后面我们会接着浅析 \_\_fastcall 调用规则。
  2. 首先,我们得明白一个教条(其实也是自己概括的),那就是 —— 调用规则的区别产生其实就是由于调用者与被调用者之间的“责任分配”问题。
  3. 代码段中的第 2 个就是 \_\_cdecl 调用规则的 CALL。**\_\_cdecl C/C++、MFC 默认的调用规则**。我们可以看到,在执行 CALL 之前,程序会将参数按照从右到左的方式压栈,这里是两个整型参数,每压栈一个 ESP 都会减 4,这样下来 ESP 会减少 8,然后 CALL 这个函数。常规地,我们可以看到,这个 CALL 里面参数的处理和通常情况下一致,先将 EBP 压栈保存现场,然后使 EBP 重合于 ESP,再通过 EBP + 偏移地址来取得两个参数值,赋值再累加到 EAX 中,EAX 将作为返回值给调用者使用,还原 EBP 现场,调用 RETN 返回到调用者。最后,使得 ESP 8。哎!这刚好和开头对称嘛!为了堆栈平衡,ESP 最终又被拉回到了 CALL 之前的位置。我们暂且可以小结一下,实际上 **在 \_\_cdecl 调用规则中,需要调用者来负责清栈操作(由调用者将 ESP 拉高以维持堆栈平衡)。**

20191013181129556.png

  1. 代码段中的第 3 个是 \_\_stdcall 调用规则的 CALL\_\_stdcall 调用规则在 Win32 API 函数中用的比较多。跟 \_\_cdecl 一样,在执行 CALL 之前,程序会先将参数从右到左依次压栈,我们跟进 CALL 里面,可以看到以下的反汇编代码,我们很容易发现,除了最后一条指令,其他的指令与 \_\_cdecl 调用规则是基本一样的。最后一条指令是“RETN 0x8”,这是什么意思呢?实际上呢,就相当于先执行“ADD ESP, 0x8”再执行“POP EIP 。换言之,就是将 ESP 8,然后正常 RETN 返回到调用者。

20191013181157975.png

  1. 我们不难发现**,\_\_stdcall 调用规则使得被调用者来执行清栈操作(由被调用者函数自身将 ESP 拉高以维持堆栈平衡)**,这也是 \_\_stdcall \_\_cdecl 调用规则的最根本的区别。
  2. \_\_cdecl 偏向于把责任分配给调用者,动脑筋想想,我们的程序在 CALL \_\_cdecl 调用规则的函数之前,把参数从右到左依次压栈,CALL 返回后,剩下的清栈操作都交给调用者处理,调用者负责拉高 ESP。再回来想想 \_\_stdcall,在 CALL 中将调用者的 EBP 压栈以保存现场,然后使 EBP 对齐于 ESP,然后通过 EBP + 偏移地址取得参数,并且经过加法得到 EAX 返回值,从堆栈弹出 EBP 恢复现场,但是最后不一样的地方,程序将执行 RETN 0x8 ESP 拉回之前的 ESP + 8 的位置,换言之,被调用者将负责清栈操作。这就是之前所谓的“责任分配”的区别。

__fastcall 调用规则

  1. 不难揣测 fastcall 的英文意思貌似是“快速调用”,这一点与它的调用规则息息相关,它的快速是有原因的,让我们继续来看看之前那张反汇编的截图,代码段中的第 4 个就是 \_\_fastcall 调用规则的 CALL。进 CALL 前,出乎意料地,程序将两个参数从右到左分别传给了 EDXECX 寄存器,讲到这里,学过计算机系统相关知识的人很容易理解为什么这叫“快速调用”了,寄存器比内存快很多很多倍,可以认为传参给寄存器,要比在内存中更快得多,效率更高。

20191013181446764.png

  1. 由于参数是直接传递给了寄存器,堆栈并未发生改变,在 CALL 中,EBP 压栈,EBP ESP 对齐之后,ESP 8,这个操作有点像对局部变量分配堆栈空间(**[这里][Link 6]**有我之前一篇博客,对局部变量的存放规则做了浅析),然后程序将 EDXECX 分别赋值给 EBP 8 EBP 4 这两个地址,这个过程相当于用寄存器给局部变量赋值,接下来运算结果将保存在 EAX 中,ESP 归位,EBP 恢复现场,最后 RETN 返回调用者领空。
  2. 本例只传送了两个整数型参数。其实呢,对于 \_\_fastcall 调用规则,左边开始的两个不大于4字节(int)的参数分别放在ECXEDX寄存器,其余的参数仍旧自右向左压栈传送。并且,**\_\_fastcall 调用规则使得被调用者负责清理栈的操作(由被调用者函数自身将 ESP 拉高以维持堆栈平衡)**,这一点和 \_\_stdcall 一样。

__pascal 调用规则

  1. \_\_pascal 是用于 Pascal / Delphi 编程语言的调用规则,C/C++ 中也可以使用这种调用规则。简单地说,\_\_pascal 调用规则与 \_\_stdcall 不同的地方就是压栈顺序恰恰相反,前面讲到的三种调用规则的压栈顺序都是从右到左依次入栈**,\_\_pascal 则是从左到右依次入栈。**并且,**被调用者(函数自身)将自行完成清栈操作**,这和 \_\_stdcall\_\_fastcall 一样。由于比较简单,我就没有做出示例。

小结

  1. 做个表格来小结一下,很直观就能看出这四种调用规则的异同:




























调用规则

入栈顺序

清栈责任

cdecl

从右到左

调用者

stdcall

从右到左

被调用者

fastcall

从右到左(先 EDX、ECX,再到堆栈)

被调用者

pascal

从左到右

被调用者

通过分析反汇编还原 C 语言 if…else 结构

From:https://www.cnblogs.com/yenyuloong/p/9629749.html

让我们从反汇编的角度去分析并还原 C 语言的 if … else 结构,首先我们不看源代码,我们用 OllyDBG 载入 PE 文件,定位到 main 函数领空,如下图所示。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZyZWVraW5nMTAx_size_16_color_FFFFFF_t_70 2

以下的 C 语言代码段。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZyZWVraW5nMTAx_size_16_color_FFFFFF_t_70 3

函数调用方式 __stdcall 与 __cdecl

  1. 在程序执行过程中发生函数调用时,系统会作如下操作:首先把参数压入堆栈,然后把ip寄存器的值压入堆栈(作为函数的返回地址),然后把堆栈指针(ESP)的值赋给EBP并把EBP压入堆栈,最后为本地变量留出一定空间,即把ESP减去一定的值。
  1. 用 __stdcall 定义的函数在结束时由该函数自己负责把参数弹出堆栈,windows API都是采用__stdcall压栈/调用方式。
  2. 用 __cdecl 定义的函数在结束时不管参数,而由调用函数负责把参数弹出堆栈,C的库函数都是采用 __cdecl 压栈/调用方式,在C 程序中定义的函数默认都是 __cdecl 方式。

举个例子说明一下:
在 VC 中建一个 Win32 Console Application的空项目,输入以下代码:

  1. #include<stdio.h>
  2. int test(int a, int b)
  3. {
  4. return a + b;
  5. }
  6. void main(int argc, char** argv)
  7. {
  8. test(10, 20);
  9. }

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZyZWVraW5nMTAx_size_16_color_FFFFFF_t_70 4

在 main 函数的下一行设置断点,编译,调试。按 Alt + F9、 A 切换到汇编窗口,可以看到

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZyZWVraW5nMTAx_size_16_color_FFFFFF_t_70 5

源代码对应汇编代码:

  1. 3 int test(int a, int b) //test函数的定义
  2. 4 {
  3. 007416E0 push ebp //ebp入栈
  4. 007416E1 mov ebp,esp //esp-->ebp
  5. 007416E3 sub esp,0C0h
  6. 007416E9 push ebx
  7. 007416EA push esi
  8. 007416EB push edi
  9. 007416EC lea edi,[ebp-0C0h]
  10. 007416F2 mov ecx,30h
  11. 007416F7 mov eax,0CCCCCCCCh
  12. 007416FC rep stos dword ptr es:[edi]
  13. 007416FE mov ecx,offset _8A1C4DC9_C++_console@cpp (074C003h)
  14. 00741703 call @__CheckForDebuggerJustMyCode@4 (07411FEh)
  15. 5 return a + b;
  16. 00741708 mov eax,dword ptr [a]
  17. 0074170B add eax,dword ptr [b]
  18. 6 }
  19. 0074170E pop edi
  20. 0074170F pop esi
  21. 00741710 pop ebx
  22. 00741711 add esp,0C0h
  23. 00741717 cmp ebp,esp
  24. 00741719 call __RTC_CheckEsp (0741208h)
  25. 0074171E mov esp,ebp
  26. 00741720 pop ebp
  27. 00741721 ret
  28. ......
  29. ......
  30. 7
  31. 8 void main(int argc, char** argv)
  32. 9 {
  33. 00741750 push ebp
  34. 00741751 mov ebp,esp
  35. 00741753 sub esp,0C0h
  36. 00741759 push ebx
  37. 0074175A push esi
  38. 0074175B push edi
  39. 0074175C lea edi,[ebp-0C0h]
  40. 00741762 mov ecx,30h
  41. 00741767 mov eax,0CCCCCCCCh
  42. 0074176C rep stos dword ptr es:[edi]
  43. 0074176E mov ecx,offset _8A1C4DC9_C++_console@cpp (074C003h)
  44. 00741773 call @__CheckForDebuggerJustMyCode@4 (07411FEh)
  45. 10 test(10, 20);
  46. 00741778 push 14h //将参数压入堆栈,14h转换成十进制是20
  47. 0074177A push 0Ah //将参数压入堆栈,0Ah转换成十进制是10
  48. 0074177C call test (0741230h) //调用test函数
  49. 00741781 add esp,8 //恢复堆栈,这是__cdecl的工作方式
  50. 11 }
  51. 00741784 xor eax,eax
  52. 00741786 pop edi
  53. 00741787 pop esi
  54. 00741788 pop ebx
  55. 00741789 add esp,0C0h
  56. 0074178F cmp ebp,esp
  57. 00741791 call __RTC_CheckEsp (0741208h)
  58. 00741796 mov esp,ebp
  59. 00741798 pop ebp
  60. 00741799 ret

现在把上面的 C 语言代码第一行改为:int __stdcall test(int a,int b)

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZyZWVraW5nMTAx_size_16_color_FFFFFF_t_70 6

对应汇编代码:

  1. int __stdcall test(int a, int b)
  2. {
  3. 00A716E0 push ebp
  4. 00A716E1 mov ebp,esp
  5. 00A716E3 sub esp,0C0h
  6. 00A716E9 push ebx
  7. 00A716EA push esi
  8. 00A716EB push edi
  9. 00A716EC lea edi,[ebp-0C0h]
  10. 00A716F2 mov ecx,30h
  11. 00A716F7 mov eax,0CCCCCCCCh
  12. 00A716FC rep stos dword ptr es:[edi]
  13. 00A716FE mov ecx,offset _8A1C4DC9_C++_console@cpp (0A7C003h)
  14. 00A71703 call @__CheckForDebuggerJustMyCode@4 (0A711FEh)
  15. return a + b;
  16. 00A71708 mov eax,dword ptr [a]
  17. 00A7170B add eax,dword ptr [b]
  18. }
  19. 00A7170E pop edi
  20. 00A7170F pop esi
  21. 00A71710 pop ebx
  22. 00A71711 add esp,0C0h
  23. 00A71717 cmp ebp,esp
  24. 00A71719 call __RTC_CheckEsp (0A71208h)
  25. 00A7171E mov esp,ebp
  26. 00A71720 pop ebp
  27. 00A71721 ret 8 //与__cdecl的不同之处,由被调用函数自己恢复堆栈
  28. void main(int argc, char** argv)
  29. {
  30. 00A71750 push ebp
  31. 00A71751 mov ebp,esp
  32. 00A71753 sub esp,0C0h
  33. 00A71759 push ebx
  34. 00A7175A push esi
  35. 00A7175B push edi
  36. 00A7175C lea edi,[ebp-0C0h]
  37. 00A71762 mov ecx,30h
  38. 00A71767 mov eax,0CCCCCCCCh
  39. 00A7176C rep stos dword ptr es:[edi]
  40. 00A7176E mov ecx,offset _8A1C4DC9_C++_console@cpp (0A7C003h)
  41. 00A71773 call @__CheckForDebuggerJustMyCode@4 (0A711FEh)
  42. test(10, 20);
  43. 00A71778 push 14h
  44. 00A7177A push 0Ah
  45. 00A7177C call test (0A7137Ah)
  46. //调用过程不负责恢复堆栈
  47. }
  48. 00A71781 xor eax,eax
  49. 00A71783 pop edi
  50. 00A71784 pop esi
  51. 00A71785 pop ebx
  52. 00A71786 add esp,0C0h
  53. 00A7178C cmp ebp,esp
  54. 00A7178E call __RTC_CheckEsp (0A71208h)
  55. 00A71793 mov esp,ebp
  56. 00A71795 pop ebp
  57. 00A71796 ret

发表评论

表情:
评论列表 (有 0 条评论,109人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Pascal正式转C++

    NOIP2016结束了 。之前一直想转语言但没坚持下来。这次看到了自己与神犇的差距,也看到了pascal语言在某些功能上的缺陷。 begin writel

    相关 Pascal的旅行

    【问题描述】        一块的nxn游戏板上填充着整数,每个方格上为一个非负整数。目标是沿着从左上角到右下角的任何合法路径行进,方格中的整数决定离开该位置的距离有多大,所