文档章节

函数调用约定 (cdecl stdcall)

傅易
 傅易
发布于 08/18 16:15
字数 2463
阅读 24
收藏 1

函数调用约定 (cdecl stdcall)

在 C 语言里,我们通过阅读函数声明,就知道怎么携带参数去调用函数,也能在函数体定义内使用这些参数。但是 CPU 并不直接完成函数调用的传参操作,这需要人为的约定。这些约定被编译器识别和使用,生成所需的代码。

一般每个线程都维护着一个堆栈,称为线程栈。调用方想进行函数调用并传递参数,就往栈里压入参数。被调函数从栈顶取出一定数量的参数使用,这就完成了参数的传递。这个过程需要一些约定,使调用方和被调函数都能正确地识别对应参数的位置等。

顺便一提,函数返回值是怎么实现的呢?有一种最为常用的方法是往调用栈里压入了一个空值占位,被调函数往这个位置写入返回值,调用方再取出来使用,这就完成了返回值的传递。

函数调用约定主要包括两方面的内容:

  1. 参数传递使用哪些寄存器,使用栈时的压栈顺序(是从左至右还是从右至左)
  2. 调用完成后堆栈由谁清理(是调用方还是被调函数)

函数调用约定与编程环境是息息相关的,不同编译器的实现也不尽相同。

调用约定、类型表示和名称修饰三者合起来,即是所谓的 ABI (应用程序二进制接口)。

x86 编程环境

32 位 C/C++ 编程中,主要有 cdecl stdcall 两种不同的约定。

cdecl

cdecl (c declaration) 是 C 语言的事实标准,它表示:

  1. 实参从右至左压入线程栈
  2. 返回值保存在寄存器 EAX/AX/AL 中,如果是浮点值保存在寄存器 ST0 中
  3. 调用方负责清栈
  4. 告知 C 编译器,输出的函数名应该前加 _,即修饰为 _<func-name>
  5. 8 位或 16 位长度的整形实参,隐式提升为 32 位长
  6. 易失寄存器有:EAX, ECX, EDX, ST0 - ST7, ES, GS
  7. 非易失寄存器有:EBX, EBP, ESP, EDI, ESI, CS, DS
  8. 使用 RET 指令返回(实质上是读取 EBP 指向的线程栈处所保存的函数返回地址,将其加载到 IP 中)

易失寄存器(volatile register)是指在函数调用时不需要被保护和恢复的;非易失寄存器(non-volatile register)是指需要保护和恢复的,需要为此生成额外的代码。

cdecl 带来了两件事:

  1. 被调函数虽然不知道有多少参数入了栈,但会自上而下按需取走需要的参数。你可能想到了,使用可变参数(vararg/stdarg)的函数(如 printf)就只能使用 cdecl 约定。
  2. 每次函数调用后都要生成一段清栈代码,生成的目标代码会比较大

不同的编译器对标准的执行情况不同。

Visual C++ 规定,返回值如果是 POD 且长度不超过 4B 则用寄存器 EAX;长度不超过 8B,用寄存器 EAX:EDX 传递;长度超过 8B 或者不是 POD,则调用者为其预先分配一个空间,把该空间的地址作为第一个参数传递给被调函数。

GCC 的返回值则不作判断,都由调用者分配空间并把其地址作为第一个参数传递给被调函数。从 GCC 4.5 起,调用函数时,线程栈上的数据必须 16B 对齐。

cdecl 是 C/C++ 的默认函数调用约定。

在使用时,用特定的关键字来指定使用 cdecl,Visual C++ Compiler 使用 __cdecl 关键字;GCC 使用 __attribute__((cdecl)) 修饰。

stdcall

stdcall 是微软建立的约定,用于 Windows API。这是 Pascal 约定与 cdecl 的折中方案。

stdcall 表示:

  1. 以 cdecl 为基准
  2. 被调函数负责清栈
  3. 告知 C 编译器,输出的函数名应该前加 _ 后跟 @ 和参数占用的栈空间的长度(记为 <param-bytes>),即修饰为 _<func-name>@<param-bytes>
  4. 使用 RETN <param-bytes> 指令返回(实质上为了完成清栈操作)

这里的第三点,举例说明:在 win32 环境下,函数 int __stdcall foo(void * p) 就可以在外部使用 _foo@4 来引用。

在使用时,vc++ 下使用 __stdcall;gcc 下使用 __attribute__((stdcall))

事实上,vc++ 规定 PASCAL WINAPI APIENTRY FORTRAN CALLBACK STDCALL __far __pascal __fortran __stdcall 均是指 stdcall。

因为 stdcall 严格控制了参数的字节数,所以不能实现可变参数。

fastcall

该约定还未被标准化,但 vc++ 和 gcc 的实现是相同的,在这两者下,fastcall 表示:

  1. 以 stdcall 为基准
  2. 从左至右数,第一个不超过 32 位的参数通过寄存器 ECX/CX/CL 传递
  3. 从左至右数,第二个不超过 32 位的参数通过寄存器 EDX/DX/DL 传递
  4. C 编译器输出的函数名修饰为 @<func-name>@<param-bytes>

在使用时,vc++ 下使用 __fastcall;gcc 下使用 __attribute__((fastcall))

thiscall

thiscall 因 c++ 而引入,这是针对 c++ 的类成员函数需要 this 指针而定的。

该约定以 cdecl 为基准,vc++ 和 gcc 的实现是不同的。vc++ 使用寄存器 ECX 传递 this;gcc 将 this 视为左起第一个参数,即在最后将 this 压栈。

在使用时,vc++ 下使用 __thiscall;gcc 下使用 __attribute__((thiscall))

naked call

表示函数无需保护现场(prolog)和恢复现场(epilog)的代码。

这在 vc++ 下不是通过关键字实现的,而是通过 __declspec(naked) 函数声明;gcc 下通过 __attribute__((naked))

naked 标志跟 __inline 内联函数是不同的机制,不能一起使用。

x86-64 编程环境

有两种主流规则。

微软 x64 调用约定

参数传递

  • 从左至右的头 4 个参数使用寄存器传递。
  • 标量(scalar)寄存器依次是 RCX, RDX, R8, R9。标量寄存器中的值是右对齐的,这样可以允许访问 RCX/ECX/CX/CL 来得到特定位数的值。
  • 非标量(non-scalar)寄存器依次是 XMM0, XMM1, XMM2, XMM3。
  • 这 8 个寄存器中只用到 4 个,其他未用到的寄存器槽被忽略(见例子 1)。
  • 长度在 64b 内的整型、指针、数组、struct、union、__m64,使用标量寄存器。浮点数使用非标量寄存器。
  • 长度超过 64b 的,由调用方开辟空间(需要 16B 对齐)并传递指针(见例子 2)。
  • 如果超过 4 个变量,这些更多的参数从右至左入栈。
  • 无论有多少个参数,在入栈过程结束后,在栈上分配 32B 的影子空间(shadow space),预留给 4 个寄存器的值用于调试时存储其值。
// 例子 1
void func(int a, double b, int c, float d);
// a in RCX, b in XMM1, c in R8, d in XMM3, other registers are unused

// 例子 2
void func(__m64 a, _m128 b, struct c, float d);  
// a in RCX, ptr to b in RDX, ptr to c in R8, d in XMM3  

返回值传递

  • 标量结果放在 RAX 中,非标量结果放在 XMM0 中。
  • 长度在 64b 内的整型、指针、数组、struct、union、__m64,视为标量。浮点数和 __m128 视为非标量。
  • 长度超过 64b 的,由调用方开辟空间,并将其指针视为第一个参数传递在 RCX 中。被调函数必须在 RAX 中返回相同的指针。其他正常的参数顺延。
__int64 func(int a, double b, int c, float d);
// a in RCX, b in XMM1, c in R8, d in XMM3, others are unused
// retval in RAX

__m128 func(float a, double b, int c, __m64 d);
// a in XMM0, b in XMM1, c in R8, d in R9
// retval in XMM0, __m128 fits in float register

struct S1 {
   int j, k;        // fits in 64b
};

S1 func(int a, double b, int c, float d);
// a in RCX, b in XMM1, c in R8, d in XMM3
// retval in RAX, S1 fits in 64b

struct S2 {
   int j, k, l;     // exceeds 64b
};

void func(__m64 a, _m128 b, struct S2 c, float d);
// Caller allocates memory for retval and passes ptr to retval in RCX
// a in RDX, ptr to b in R8, ptr to c in R9, d pushed on the stack

S2 func(__m64 a, _m128 b, struct S2 c, float d);
// Caller allocates memory for retval and passes ptr to retval in RCX
// a in RDX, ptr to b in R8, ptr to c in R9, d pushed on the stack
// ptr to retval in RAX

保护现场和恢复现场

  • 调用方负责清栈
  • 易失寄存器有:RAX, RCX, RDX, R8, R9, R10, R11
  • 非易失寄存器有:RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15

System V AMD64 ABI

  • 以微软的 x64 约定为基准。
  • 头六个参数里,整型放在寄存器 RDI, RSI, RDX, RCX, R8, R9;浮点数放在寄存器 XMM0 - XMM7。
  • 栈是 16b 对齐的。
  • 其他参数从右至左入栈。
  • 不在栈上创建影子空间。

关于函数名修饰规则

编译器输出的函数名,在其他目标文件中引用该函数时使用。

C 语言采用上述规则,而 C++ 则采用另一套规则,该规则详尽描述了函数原型,因此显得有些复杂。

但是,在 C++ 中可以使用 extern "C" 来要求以 C 编译器的方式来编译和链接,这也会以 C 的方式来修饰函数名。

在编写可移植的代码时,常用以下代码段来实现这一目标:

#ifdef  __cplusplus 
extern "C" {
#endif

// code

#ifdef  __cplusplus
}
#endif  /* end of __cplusplus */

注意,64 位环境下,C 语言的函数名不做修饰。

拓展阅读

© 著作权归作者所有

共有 人打赏支持
傅易
粉丝 25
博文 102
码字总数 61690
作品 0
海淀
后端工程师
带你玩转Visual Studio——调用约定与(动态)库

上一篇文章带你玩转Visual Studio——调用约定cdecl、stdcall和fastcall中已经讲述了cdecl、stdcall和fastcall几种调用约定的主要区别。这一章将进一步深入了解不同调用约定对编译后函数修饰...

spencer.luo
2017/12/25
0
0
_cdecl、_stdcall、_fastcall和_thiscall整理

cdecl、stdcall、fastcall和thiscall整理 1._cdecl是C Declaration的缩写,表示C语言默认的函数调用方法:所有参数 从右到左依次入栈,这些参数由调用者清除,称为手动清栈(由调用者把参数弹...

西昆仑
2011/11/17
0
1
关于windows dll的生成

关于windows dll的生成 今天上午看到VC板上有人提到dll调用约定的问题,发现自己一直以来只是网上说的文章去做的,具体的实验还真的没有做过。中午闲来无聊,写了几个小例子,测试一下windo...

KavenSu
2014/01/16
0
0
带你玩转Visual Studio——调用约定__cdecl、__stdcall和__fastcall

有一定C++开发经验的人一定对"cdecl、stdcall、fastcall"肯定不陌生吧!但你真正理解了吗?是的,我曾在这采了无数个坑,栽了无数个跟头,终于忍无可忍要把它总结一下(虽然我已经有能力解决大...

spencer.luo
2017/12/25
0
0
__cdecl __stdcall

1.如果函数func是cdecl(默认调用方式),调用时情况如下 int main() { //参数从右到左压栈 push 4 push 3 push 2 push 1 call func add esp 0x10 //调用者恢复堆栈指针esp,4个参数的大小是0...

ryany
2011/03/21
0
0

没有更多内容

加载失败,请刷新页面

加载更多

00.编译OpenJDK-8u40的整个过程

前言 历经2天的折腾总算把OpenJDK给编译成功了,要说为啥搞这个,还得从面试说起,最近出去面试经常被问到JVM的相关东西,总感觉自己以前学的太浅薄,所以回来就打算深入学习,目标把《深入理...

凌晨一点
今天
2
0
python: 一些关于元组的碎碎念

初始化元组的时候,尤其是元组里面只有一个元素的时候,会出现一些很蛋疼的情况: def checkContentAndType(obj): print(obj) print(type(obj))if __name__=="__main__": tu...

Oh_really
昨天
6
2
jvm crash分析工具

介绍一款非常好用的jvm crash分析工具,当jvm挂掉时,会产生hs_err_pid.log。里面记录了jvm当时的运行状态以及错误信息,但是内容量比较庞大,不好分析。所以我们要借助工具来帮我们。 Cras...

xpbob
昨天
112
0
Qt编写自定义控件属性设计器

以前做.NET开发中,.NET直接就集成了属性设计器,VS不愧是宇宙第一IDE,你能够想到的都给你封装好了,用起来不要太爽!因为项目需要自从全面转Qt开发已经6年有余,在工业控制领域,有一些应用...

飞扬青云
昨天
4
0
我为什么用GO语言来做区块链?

Go语言现在常常被用来做去中心化系统(decentralised system)。其他类型的公司也都把Go用在产品的核心模块中,并且它在网站开发中也占据了一席之地。 我们在决定做Karachain的时候,考量(b...

HiBlock
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部