走进C/C++函数的名字改编
现在的编程语言中,不同的变量或函数可以用相同的标识符命名,只要它们占据不同的命名空间(通常由模块、类或作用域定义)或有不同的签名(如在函数重载中)时,就可能会出现标识符重名的情况。另外,根据编译器和平台的不同,函数的调用方式在编译为机器代码后也可能使用不同的、专门的调用约定。
名字改编(Name Mangling,或Name Decoration)就是为了解决许多现代编程语言中因需要解决编程实体的唯一名称而引起的各种问题。它提供了一种在函数、结构、类或其他数据类型的名称中编码额外信息的方法,以便从编译器向链接器传递更多的语义信息。例如,我们可能会在编译后链接时遇到类似
“undefined reference to `__Z18create_message_pubNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_S4_i'”
这样的情况,这一长串其实就是函数“void* create_message_pub(std::string a, std::string b, std::string c, int d)”经过Name Mangling处理后的符号。链接器会根据这个符号在目标文件或库文件中找到对应的函数实现,并使得对它的调用能够跳转到正确的地址处,同时检查两边的定义签名和调用约定等是否一致,以在链接时就能发现并避免潜在的错误。
在早期的C语言中,只通过函数名来区分代码片段,而忽略了任何其他信息,如参数类型或返回类型。后来的编程语言,如C++,对被认为是同级的代码片段定义了更严格的要求,如参数类型、返回类型和函数的调用约定。这样使得可以使用函数重载,以及检测各种错误(如使用不同的函数定义来编译不同的源文件)。
02.C语言的规则
所以在C语言下函数名字改编的规则较为简单,通常只包括了函数名和调用约定。函数调用约定是当一个函数被调用时,如何从调用处接受参数以及返回结果的方法的约定,不同调用约定的区别在于:参数和返回值放置的位置(在寄存器中/在调用栈中/两者混合);参数传递的顺序(或者单个参数不同部分的顺序);调用前设置和调用后清理的工作(在调用者和被调用者之间如何分配)等。
常见的调用约定有cdecl(C Declaration,表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈);
stdcall(Standard Call,是Windows API默认的函数调用,所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是this指针。这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是retnX,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间,称为自动清栈);
fastcall(编译器指定的快速调用方式,由于大多数的函数参数个数很少,使用堆栈传递比较费时。因此_fastcall通常规定将前两个(或若干个)参数由寄存器传递,因为寄存器速度远快于内存,其余参数还是通过堆栈传递。不同编译器编译的程序规定的寄存器不同)等。
例如下列函数:
int _cdecl f (int x) { return 0; } int _stdcall g (int y) { return 0; } int _fastcall h (int z) { return 0; } 在Name Mangling后变成了: _f _g@4 @h@4int _cdecl f (int x) { return 0; } int _stdcall g (int y) { return 0; } int _fastcall h (int z) { return 0; } 在Name Mangling后变成了: _f _g@4 @h@4
函数调用中cdecl情况下,符号只是函数名前面加了下划线前缀,stdcall则还要在其后加上@和参数列表中参数的字节数,而fastcall在此基础上将前缀由下划线改为了@符号。
03.C++的规则
而C++下,由于函数重载等特性的需要,改编后的符号中还加入了参数和返回值等信息,并且具体规则也取决于编译器的实现。
在Visual Studio的MSVC编译器中,一个C++函数修饰后的名称包含以下信息:函数的名称;如果该函数是一个成员函数,那么还有该函数所属于的类;如果该函数是一个命名空间的一部分,那么还有该函数所属的命名空间;函数参数的类型;调用约定;函数的返回类型。
例如下列函数:
int a(char){int i=3;return i;}; void __stdcall b::c(float){};
在Name Mangling后变成了:
?a@@YAHD@Z ?c@b@@AAGXM@Z
可以看到经过改编后的名字规则相对复杂,并不易于识别,因为它通常只是由编译器和链接器内部使用。幸运的是,微软提供了undname工具将符号转换为修饰前的形式。在VS开发者命令提示符中:
C:\>undname ?func1@a@@AAEXH@Z Microsoft (R) C++ Name Undecorator Copyright (C) Microsoft Corporation. All rights reserved. Undecoration of :- "?func1@a@@AAEXH@Z" is :- "private: void __thiscall a::func1(int)"
而由于C++标识符没有一个标准化的方案,不同编译器,甚至是同一编译器的不同版本,或不同平台上的同一编译器也会以完全不同(不兼容)的方式处理公共符号。例如下列函数:
void h(int) void h(int, char) void h(void) 在GCC Name Mangling后变成了: _Z1hi _Z1hic _Z1hv 后变成了: _Z1hi _Z1hic _Z1hv
可以看到GCC的规则与MSVC的规则完全不同。好在GNU Binutils中也提供了Name De-Mangling相关的工具,比如c++filt和nm,nm正是指Name Mangling。(各种架构的这些工具在SkyEye天目全数字实时仿真软件中有完整提供)
c++filt _ZN9NS_QZSOCK10CTcpClient11SendAndRecvEPciRiRjd NS_QZSOCK::CTcpClient::SendAndRecv(char*, int, int&, unsigned int&, double) nm -C bin/play_url.so | grep SendAndRecv 0000000000391090 T NS_QZSOCK::CTcpClient::SendAndRecv(char*, int, int&, unsigned int&, double) 0000000000390a10 T NS_QZSOCK::CUdpClient::SendAndRecv(char*, int, int&, unsigned int&, double)
另外,ld链接器也提供了参数来设置函数名字显示规则:--demangle将显示改编前的函数名,--no-demangle则强制显示改编后的符号名。
04.“undefined reference to”问题
因为C++改编修饰函数规则的不同,这也就导致了它们之间互相难以直接调用,会在链接时报出可恶的undefined reference to错误。所以,为了让其它人能够正常引用你写的C++库函数,通常的做法是将函数声明用extern "C"包括起来,这样在函数名字改编时就会按照较为通用的C方式改编生成符号。例如:
#ifdef __cplusplus extern "C" {#endif /* ... */#ifdef __cplusplus}#endif
或者,我们可以通过链接器参数来手动指定某个函数要改编成什么名字。例如MSVC就支持下列写法,它可以将等号后的符号映射到等号前的符号:
#pragma comment(linker, "/export:__Z18create_message_pubNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_S4_i=?create_message_pub@@YAPAXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@00H@Z")
不过,这将无法保证函数调用和参数等的一致,链接器也无法检测这些潜在的错误,当声明与调用方式不一致时就可能会导致程序运行崩溃或异常。如果您是在编写SkyEye的设备模块,那么该异常也会被捕获到,并将函数和模块记录在日志中。
05.SkyEye天目全数字实时仿真软件
由迪捷软件自主研发的SkyEye天目全数字实时仿真软件,是基于可视化建模的硬件行为级仿真平台,利用拖拽的方式快速搭建任意的虚拟硬件平台,保证虚拟嵌入式系统的可靠性和实时性,进行嵌入式软件的开发和调试。SkyEye目前支持主流的嵌入式硬件平台,可以运行主流的操作系统,此外还能适配国内自主研发的操作系统天脉。通过利用基于LLVM的动态二进制翻译技术,使虚拟处理器在典型的桌面计算机上运行速度可以达到2000MIPS以上。
SkyEye支持调用Windows库函数,结合项目实际运行环境,并提供了二次开发接口,使降低项目成本、缩短项目周期成为可能。它还内置了多种架构的BinUtils二进制套件实用工具,包括aarch64、ARM、MIPS、PowerPC、SPARC、DSP的GDB、objdump、ar、as、ld、nm、readelf、strip等,让你在处理以上问题时更加游刃有余。
例如,SkyEye在计算机上运行各种架构的嵌入式软件,且支持源码调试,自动解析地址对应的函数,将其还原为可读的符号信息,并可以对全局变量进行数据注入和输出监视。另外,它还支持高度模块化的设计,并提供了完善的扩展开发API和详尽的文档,可以根据项目需要与外部第三方库进行兼容适配。通过MinGW开发环境可以在Windows下使用GCC规则进行模块开发自定义模块,同时对Linux也有较好的支持,提高自主可控性。