C++中的名字重整技术
C++ 一直为人诟病之一的原因是他的二进制模块兼容性不好,即ABI(Application Binary Interface)问题。对于同一源代码,不同编译器,甚至同一编译器不同版本都不兼容,其编译出来的ABI不能相互使用。比如其中一个ABI问题是为了支持函数重载,C++使用了Name Mangling(翻译为命名重整、名字改编、名字修饰等)技术,而Name Mangling在不同编译器间基本是完全不兼容的。
Name Mangling是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过一定算法,重新修饰为一个全局唯一的名称。
C++除了支持函数重载,也即是允许多个函数拥有一样的名字,同时也支持命名空间,也即同时允许多个同样的函数定义在在不同的名称空间。这使得Name mangling尤其复杂。
下面对此进行测试验证。
说明:本文只简单介绍在Windows平台Visual Studio 2010编译器和Linux平台下GCC4.4.7编译器下关于Name Mangling的异同。并不涉及过多原理内容,如想详细了解Name Mangling的理论请参考以下两篇文章:
wikipedia Name_mangling
Visual C++名字修饰
先看部分代码(完整程序附在文章结尾处):
1 int /*__cdecl*/ func(int);//windows平台下在函数名前可加__cdecl、__stdcall、__fastcall,默认__cdecl 2 3 float func(float); 4 5 int func(const std::vector<std::string>& vec); 6 7 namespace NP1 8 { 9 int func(int); 10 11 class C 12 { 13 public: 14 int func(int); 15 }; 16 }; 17 18 namespace NP2 19 { 20 int func(int); 21 22 class C 23 { 24 public: 25 int func(int); 26 }; 27 };
对于上面这段代码,只有同名的func函数,但是有重载的、全局的、不同名字空间的、不同类的。那么通过C++编译器进行名字重整后的结果是什么呢,请继续,
VS2010编译以上代码没问题,但是在链接时显示如下经典的error LNK2001: unresolved external symbol错误(你问我如何显示的?不要实现函数,但在代码中又调用该函数即可了,下面提供有所有源代码):
1 error LNK2019: unresolved external symbol "int __cdecl func(class std::vector<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,class std::allocator<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > > const &)" (?func@@YAHABV?$vector@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@V?$allocator@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@@std@@@Z) referenced in function _main 2 error LNK2019: unresolved external symbol "public: int __thiscall NP2::C::func(int)" (?func@C@NP2@@QAEHH@Z) referenced in function _main 3 error LNK2019: unresolved external symbol "int __cdecl NP2::func(int)" (?func@NP2@@YAHH@Z) referenced in function _main 4 error LNK2019: unresolved external symbol "public: int __thiscall NP1::C::func(int)" (?func@C@NP1@@QAEHH@Z) referenced in function _main 5 error LNK2019: unresolved external symbol "int __cdecl NP1::func(int)" (?func@NP1@@YAHH@Z) referenced in function _main 6 error LNK2019: unresolved external symbol "int __cdecl func(int)" (?func@@YAHH@Z) referenced in function _main 7 error LNK2019: unresolved external symbol "float __cdecl func(float)" (?func@@YAMM@Z) referenced in function _main
在上面的链接错误中,我们可以看到这同一组func函数分别被编译器重新改名了,且每个名字是唯一的。
注意这里在函数名前增加了__cdecl标识,这也是Visual Studio平台下C++开发时的默认函数调用方式,另外还有两种常用的是__stdcall、__fastcall,拿函数int func(int)来说,分别按__cdecl、__stdcall、__fastcall调用,其重整后的名字也不相同,分别如下:
1 ?func@@YAHH@Z //__cdecl 2 ?func@@YGHH@Z //__stdcall 3 ?func@@YIHH@Z //__fastcall
简单解释一点,重整后的名字都以?开始,?后紧跟函数名,之后是两个@@,第一个@表示函数名字结束,第二个@之后的第一个字母Y表示是全局函数,第二个字母A、G、I分别表示__cdecl、__stdcall、__fastcall,第三个字母H表示返回类型是整形,之后是多个参数类型表示(该函数只有一个整形参数,以H表示),参数结束以@表示,最后用Z表示名字结束。
详细的重整规则可以参考文章:Visual C++名字修饰
对上面的代码在GCC下编译,链接报错如下:
1 static_dynamic_polymorphic.cpp:(.text+0x4e): undefined reference to `func(float)' 2 static_dynamic_polymorphic.cpp:(.text+0x58): undefined reference to `func(int)' 3 static_dynamic_polymorphic.cpp:(.text+0x62): undefined reference to `NP1::func(int)' 4 static_dynamic_polymorphic.cpp:(.text+0x73): undefined reference to `NP1::C::func(int)' 5 static_dynamic_polymorphic.cpp:(.text+0x7d): undefined reference to `NP2::func(int)' 6 static_dynamic_polymorphic.cpp:(.text+0x8e): undefined reference to `NP2::C::func(int)' 7 static_dynamic_polymorphic.cpp:(.text+0x9a): undefined reference to `func(std::vector<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > > const&)'
从这里暂时看不出来名字重整后的结果,接下来会有的,不过话说GCC的报错方式似乎总是比Visual Studio强,呵呵。
实现所有函数定义,然后在GCC下重新编译并查看名字重整后的结果,
1 [lizheng@lzv6 c++]$ g++ c++_name_mangling.cpp 2 [lizheng@lzv6 c++]$ nm a.out | grep func 3 0000000000400983 t _GLOBAL__I__Z4funci 4 000000000040081a T _Z4funcRKSt6vectorISsSaISsEE 5 0000000000400802 T _Z4funcf 6 00000000004007f4 T _Z4funci 7 0000000000400838 T _ZN3NP11C4funcEi 8 0000000000400829 T _ZN3NP14funcEi 9 0000000000400858 T _ZN3NP21C4funcEi 10 000000000040084a T _ZN3NP24funcEi 11 [lizheng@lzv6 c++]$ nm a.out | grep func | c++filt 12 0000000000400983 t global constructors keyed to _Z4funci 13 000000000040081a T func(std::vector<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > > const&) 14 0000000000400802 T func(float) 15 00000000004007f4 T func(int) 16 0000000000400838 T NP1::C::func(int) 17 0000000000400829 T NP1::func(int) 18 0000000000400858 T NP2::C::func(int) 19 000000000040084a T NP2::func(int)
从上面可以清晰的看出函数名字重整后的命名,也可以直接根据重整后的名字反解析原函数原型说明,这个很有用处哦,不用不知道!
对于GCC下名字重整规则,这里不详述了,只做简单介绍,比如_ZN3NP21C4funcEi,默认以_Z(G++规定)开头,N表示有命名空间,数字表示后面跟着的几个字符是一个整体,比如3NP2,表示后面有3个字符,即NP2,1C、4func也是这个意思。
同样的,对于该函数原型(NP2::C::func(int))在Windows平台下重整后为?func@C@NP2@@QAEHH@Z,其名字空间、类名、函数名是通过@区分的,而不像GCC下的用数字标识。
从上面也可以看出,同样的C++代码,在不同编译器下编译后的名字重整并不一致,这必然C++的ABI不统一,也就导致C++的二进制代码不能直接跨平台使用。
对于如何从重整后的名字解析出函数原型声明,也是有办法的,比如上面linux下的c++filt命令(Windows平台下也有类似命令:undname.exe),这里就有一个跨平台的代码实现,
1 void UnDecorateName() 2 { 3 const size_t max_size = 1024; 4 char szDecorateName[max_size] = {0}; 5 char szUnDecorateName[max_size] = {0}; 6 printf("Please Input Mangled Name: "); 7 scanf("%s", szDecorateName); 8 9 #ifdef WINDOWS_IMPL 10 if (::UnDecorateSymbolName(szDecorateName, szUnDecorateName, sizeof(szUnDecorateName), UNDNAME_COMPLETE) == 0) 11 { 12 printf("UnDecorateSymbolName Failed. GetLastError() = %d", GetLastError()); 13 } 14 else 15 { 16 printf("Name after Mangled : %s \nName before Mangled : %s\n", szDecorateName, szUnDecorateName); 17 } 18 system("pause"); 19 #else 20 int status; 21 size_t n = max_size; 22 abi::__cxa_demangle(szDecorateName,szUnDecorateName,&n,&status); 23 printf("Name after Mangled : %s \nName before Mangled : %s\n", szDecorateName, szUnDecorateName); 24 #endif 25 }
总结:
名字重整在C++中必须的,不然也就没法支持重载的概念了,但重整规则没有统一标准,各编译器厂商各自为政,相互之间很难兼容,尤其是在Windows、Linux平台间。因此我认为解决跨平台的最好办法就是分别在相应平台相应编译器下重新编译。但有时候这又很难满足,比如你使用了别人提供的C++二进制模块,没有对方源码,也不清楚对方的编译环境,呵呵,这就超过本文范围了,再次在探讨吧。
附上本次测试代码,请参考,
1 #include <iostream> 2 #include <stdio.h> 3 #include <vector> 4 #include <string> 5 6 #if defined(_WIN32) || defined(WIN32) /**Windows*/ 7 #define WINDOWS_IMPL 8 #include <Windows.h> 9 #include <DbgHelp.h> /*用于实现将重整后的名字解析为原始名字*/ 10 #pragma comment(lib,"DbgHelp.lib") 11 #else 12 #define LINUX_IMPL 13 #include<cxxabi.h> /*用于实现将重整后的名字解析为原始名字*/ 14 #endif 15 16 17 int /*__cdecl*/ func(int);//windows平台下在函数名前可加__cdecl、__stdcall、__fastcall,默认__cdecl 18 19 float func(float); 20 21 int func(const std::vector<std::string>& vec); 22 23 namespace NP1 24 { 25 int func(int); 26 27 class C 28 { 29 public: 30 int func(int); 31 }; 32 }; 33 34 namespace NP2 35 { 36 int func(int); 37 38 class C 39 { 40 public: 41 int func(int); 42 }; 43 }; 44 45 //#define IMPLEMENT_ALL /**打开该宏,则定义以上函数实现*/ 46 #ifdef IMPLEMENT_ALL 47 48 int func(int) { return 1; } 49 50 float func(float) { return (float)1.11; } 51 52 int func(const std::vector<std::string>& vec) { return 0; } 53 54 namespace NP1 55 { 56 int func(int) { return 2; } 57 58 int C::func(int) { return 3; } 59 }; 60 61 namespace NP2 62 { 63 int func(int) { return 4; } 64 65 int C::func(int) { return 5; } 66 }; 67 68 #endif 69 70 /****************************************************** 71 根据重整后的名字解析出原函数原型名字(Windows/Linux) 72 *******************************************************/ 73 void UnDecorateName() 74 { 75 const size_t max_size = 1024; 76 char szDecorateName[max_size] = {0}; 77 char szUnDecorateName[max_size] = {0}; 78 printf("Please Input Mangled Name: "); 79 scanf("%s", szDecorateName); 80 81 #ifdef WINDOWS_IMPL 82 if (::UnDecorateSymbolName(szDecorateName, szUnDecorateName, sizeof(szUnDecorateName), UNDNAME_COMPLETE) == 0) 83 { 84 printf("UnDecorateSymbolName Failed. GetLastError() = %d", GetLastError()); 85 } 86 else 87 { 88 printf("Name after Mangled : %s \nName before Mangled : %s\n", szDecorateName, szUnDecorateName); 89 } 90 system("pause"); 91 #else 92 int status; 93 size_t n = max_size; 94 abi::__cxa_demangle(szDecorateName,szUnDecorateName,&n,&status); 95 printf("Name after Mangled : %s \nName before Mangled : %s\n", szDecorateName, szUnDecorateName); 96 #endif 97 } 98 99 100 int main(void) 101 { 102 int i = 1; 103 float f = 1.0; 104 std::vector<std::string> vec; 105 NP1::C *pac = new NP1::C; 106 NP2::C *pbc = new NP2::C; 107 108 #if 0 109 func(f); 110 func(i); 111 112 NP1::func(i); 113 pac->func(i); 114 115 NP2::func(i); 116 pbc->func(i); 117 118 func(vec); 119 #endif 120 121 UnDecorateName(); 122 return 0; 123 }