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命令,这里就有一个跨平台实现,

 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 }

 

 

C++中的名字重整技术,古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。