Bootstrap

UEFI开发探索63 – C与C++、汇编的调用约定

(请保留-> 作者: 罗冰   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。

使用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

其他杂谈

如果想在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++支持。

;