C++ 中的 Name Mangling

关于 C++ Name Mangling 的故事,还要从 C 语言说起。

C 语言的场景

在 C 语言中,一个函数的函数名唯一标识这个函数。换另外两个表述:

  1. 不同函数的界定与区分只看函数名,与返回值、入参类型和个数等因素皆无关;
  2. 出现多个相同函数名的函数定义意味着重定义错误。
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

此时编译就能顺利通过了~


用 GDB 调试动态链接库
Using Pbuilder to (Cross-)Build Debian Packages