(请保留-> 作者: 罗冰 https://blog.csdn.net/luobing4365)
在编写代码的时候,如果发现函数不如自己预期的运作,我的第一个反应并不是添加调试代码,或者使用调试工具。我反而更习惯于去读函数的汇编代码,查看逻辑上是否有漏洞,这都是以前开发oprom/bios汇编代码时留下的习惯。
这个习惯我认为很好,有助于让我专注在代码本身。不过,由此带来了一些问题,C与汇编的函数约定,参数入栈顺序、堆栈平衡等,很难记忆。我实际上更习惯于16位时代的汇编语言,现在32/64位的接口约定,总是需要思考一会才能想起来。
最近一直在把UEFI的代码转到C++上,又遇到调用约定的问题。正好趁这个机会整理下相关的知识,以备后用。
以下的记录,都是基于VS2015,Windows平台的。少部分内容基于Linux平台,使用Gcc的,会特别说明。
1 C/C++的调用约定
Visual C/C++提供了多个不同的调用约定,以供内部或者外部函数使用。为了加快调用速度,针对64位的调用,编译器又做了额外的规定,微软的网站上有详细介绍:https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=vs-2019
常用的调用约定,有__cdecl、__stdcall、__fastcall、 __clrcall、__thiscall、__vectorcall ,前三个比较常用,其相关信息如下。
(1)__cdecl
__cdecl是缺省的调用约定(对微软的C/C++程序而言),由调用者平衡堆栈,因此可以使用可变参数。 编译器使用“/Gd”强制使用__cdecl调用。关于__cdecl的信息如下:
元素 | 执行 |
参数入栈顺序 | 从右往左 |
堆栈平衡处理方 | 由调用方弹出堆栈的参数 |
名称修饰的约定 | 对C语言,加下划线前缀。foo变为_foo |
对C++语言,其名称修饰的约定规则为:
>> 在原有函数名之前加前缀“?”;
>> 函数名后以“@@YA”标识参数表的开始,参数表第一项为该函数的返回值类型,其后依次为各入口参数的数据类型。如果入口参数是指针类型,则会在入口参数的数据类型前加上“PA”标志;
>> 参数表后面以“@Z”标志整个名字的结束,如果该函数参数为void,则以“Z”标志结束。
>> 参数的数据类型表示:
X void
D char
E unsigned char
F short
H int
I unsigned int
J long
K unsigned long
M float
N double
_N bool
PA 标志指针,后面跟数据类型,比如“PAD”表示unsigned char*。如果相同类型的指针连续出现,以“0”替代,一个“0”代表一次重复。
举例:C++函数 void _cdecl cppPointFun(int *p1, int *p2, int a, int *p3, bool b);
在汇编代码中,其名称为:?cppPointFun@@YAXPAH0H0_N@Z
(2) __stdcall
Win32 API的常用调用约定,编译器使用“/Gz”选项指定函数使用__stdcall调用。
元素 | 执行 |
参数入栈顺序 | 从右往左 |
堆栈平衡处理方 | 由函数本身清理堆栈的参数 |
名称修饰的约定 | 对C语言,加下划线前缀,并在名称后加上“@<number>”,number表示参数个数。foo()变为_foo@0 |
对C++语言,其规则与__cdecl规则相同,只是开始标志改为“@@YG”。
(3)__fastcall
这种调用方式只能在x86架构上使用,一般用于对性能要求非常高的场合,编译器使用“/Gr”选项指定函数使用__ fastcall调用。
元素 | 执行 |
参数入栈顺序 | 从左边开始的两个大小不大于4字节(DWORD)的参数,分别放在ECX和EDX寄存器中,其余参数仍旧从右往左压栈传送 |
堆栈平衡处理方 | 由函数本身清理堆栈的参数 |
名称修饰的约定 | 对C语言,加“@”前缀,并在名称后加上“@<number>”,number表示参数个数。foo()变为@foo@0 |
对C++语言,其规则与__cdecl规则相同,只是开始标志改为“@@YI”。
至于剩下的调用约定,包括__clrcall、__thiscall、__vectorcall,在开发UEFI的时候基本用不到,就不详细讨论了,可以参考微软的网页:
https://docs.microsoft.com/en-us/cpp/cpp/argument-passing-and-naming-conventions?view=vs-2019。
2 使用Visual Studio/Gcc查看汇编代码
在第26篇博客中,谈到了如何在UEFI下观察汇编代码,其中详细讲述了如何打开编译开关,输出汇编文件。这次要做的事情是一样的,只不过是直接使用Visual Stdio集成开发环境来执行。
(1)指定调用约定
调用约定的关键字,可置于返回类型和函数名之间,比如:
// Example of the __cdecl keyword on function
int __cdecl system(const char *);
// Example of the __cdecl keyword on function pointer
typedef BOOL (__cdecl *funcname_ptr)(void * arg1, const char * arg2, DWORD flags, …);
(2)Visual Studio输出汇编代码
图1 打开输出汇编文件的开关
参照图1,打开输出汇编文件的开关。编译示例程序的时候,会同时输出汇编代码。可以写个示例程序,对照上一节的内容,实际体会下C/C++的函数调用约定。
(3)Gcc输出汇编代码
Gcc输出汇编代码比较简单,类似于Borland C的命令:
$ gcc -S hello.c
这条命令将hello.c翻译为汇编语言,并存储在hello.s文件中。
如果想把C语言变量的名称作为汇编语言中的注释,还可以加上选项:
$ gcc -S -fverbose-asm hello.c
3 其他杂谈
如果想在C++代码中使用C语言的函数,可以直接使用extern “C” 声明C语言函数,以解决命名不一致的问题。比如:
extern “C”
{
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/DebugLib.h>
}
在编写UEFI的C++代码时,深入去了解了MSVC CRT的运作,以实现对全局类等特性的支持。不过,虽然把代码出来了,总感觉还有一层薄纱存在,理解得不够透彻。
旁边有一本《程序员的自我修养:链接、装载与库》,很早以前买的一本技术书籍,没事会翻两页。其中对CRT的运作介绍得比较详细,甚至实现了一个小型的CRT。应该找个时间深入研究下,尽可能完整地实现UEFI下C++支持。