关于 C++ Name Mangling 的故事,还要从 C 语言说起。
C 语言的场景
在 C 语言中,一个函数的函数名唯一标识这个函数。换另外两个表述:
- 不同函数的界定与区分只看函数名,与返回值、入参类型和个数等因素皆无关;
- 出现多个相同函数名的函数定义意味着重定义错误。
1void foo() { }
2void foo(int a) { } // error: redefinition of ‘foo’
3int foo() { } // error: conflicting types for ‘foo’
C 语言的这种对函数的处理比较原始,但也有好处。这里涉及到一个背景知识:C/C++ 源代码经过编译器编译后,编译器会用汇编语言中的符号(symbol)来标定各个函数的首地址。用 gcc -S foo.c
可查看:
1foo:
2 pushq %rbp
3 movq %rsp, %rbp
4 nop
5 popq %rbp
6 ret
7foo1:
8 pushq %rbp
9 movq %rsp, %rbp
10 nop
11 popq %rbp
12 ret
13foo2:
14 pushq %rbp
15 movq %rsp, %rbp
16 movl %edi, -4(%rbp)
17 nop
18 popq %rbp
19 ret
同时,这个符号还会在链接和动态加载的过程中发挥作用。用 nm foo.o
可查看:
10000000000000000 T foo
20000000000000007 T foo1
3000000000000000e T foo2
可见,函数名作为函数的唯一标识,直接就被拿来用作函数的符号。方便,省事儿,反编译分析的时候也清晰明了。
C++ 的场景
然而,到了 C++ 的时代,事情发生了些许变化。
-
支持函数重载(Overloading),允许定义同名但入参不同的函数:
1void foo() { } 2void foo(int a) { } // OK 3void foo(double a, char b) { } // OK 4 5// int foo() { } /* error: ambiguating new declaration of ‘int foo()’ */
-
还要考虑命名空间(Namespace)的影响(还有类成员,不过和命名空间类似):
1void foo() { } 2 3namespace A { 4 void foo() { } 5} 6 7int main() 8{ 9 foo(); 10 A::foo(); 11}
此时,单一依靠函数名已经不足以区分多个函数了,编译器生成的汇编代码也不能仅使用函数名来作为函数的符号了。 因此就引入了 Name Mangling 机制,实质上就是将各个入参的类型和函数所在的命名空间,连同函数名一起编码成一个特定的字符串,来作为该函数的符号。用 g++ -c foo.cpp && nm foo.o
看一下:
10000000000000007 T _Z3fooi
20000000000000000 T _Z3foov
30000000000000011 T _ZN1A3fooEv
可见,这些函数符号与函数名相关(都带有“foo”字段),但带有前后缀,可想而知是由命名空间和函数入参信息编码而成的。若是想将这些函数符号恢复成可读的形式,可以使用 c++filt
命令:
1$ c++filt _ZN1A3fooEv
2A::foo()
C/C++ 混用的场景
在一些场景下,由 C++ 写的 .o 或库文件需要被由 C 写的代码链接或调用。由于 C 语言的编译机制中没有 Name Mangling 的部分,因此 C 语言的链接器无法理解经过 Name Mangling 的 C++ 函数符号,从而产生链接错误。实际操作一下:
1// foo1.cpp
2int cpp_foo(const char *str, int len) {}
将其编译成动态库 libfoo1.so:
1g++ -c ./foo1.cpp
2g++ -shared -fPIC -o libfoo1.so ./foo1.o
然后写一个使用到该库的 C 程序:
1#include <string.h>
2
3int cpp_foo(const char *str, int len);
4
5int main()
6{
7 const char *str = "Hello World!";
8 cpp_foo(str, strlen(str));
9}
编译:
1gcc ./main.c -L. -lfoo1
正如所料,编译器会报一个链接错误,找不到 cpp_foo
的定义:
1/tmp/ccDcM1wb.o: In function `main':
2main.c:(.text+0x2b): undefined reference to `cpp_foo'
3collect2: error: ld returned 1 exit status
解决这个问题的方法,是在 C++ 中明确要求编译器不使用 Name Mangling 并恢复到传统 C 语言的符号风格,其方法是使用 extern "C"
这一特殊字段:
1extern "C" void func() {}
2extern "C" {
3 int g_a;
4 int foo() {}
5}
回到上面的例子,对 C++ 库的源代码作出修改:
1// foo2.cpp
2extern "C" int cpp_foo(const char *str, int len) {}
编译成 libfoo2.so,然后在 C 语言处使用:
1gcc ./main.c -L. -lfoo2
此时编译就能顺利通过了~