C++ Name Mangling 与 Demangling:理解 C++ 符号的底层表示

好的,各位观众老爷,今天咱们来聊聊 C++ 里一个挺有意思,但也经常让人头疼的玩意儿:Name Mangling 和 Demangling。简单来说,就是 C++ 编译器怎么给函数和变量起“暗号”,以及咱们怎么把这些“暗号”翻译回人话。

第一幕:啥是 Name Mangling?为啥要有这玩意?

首先,啥是 Name Mangling?中文里,有叫“名字修饰”的,也有叫“名字改编”的,意思都差不多。它指的是 C++ 编译器为了支持函数重载、命名空间、类等特性,把函数和变量的名字进行编码,变成一个更复杂、更独特的字符串。

为啥要这么干呢?你想想,C++ 允许函数重载,也就是可以有多个函数名字一样,但是参数列表不一样。比如:

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }

如果我们直接用 add 这个名字,编译器咋知道你调用的是哪个 add? 就算没有重载,C++还有命名空间, namespace A { void foo(); }namespace B { void foo(); } 两个 foo 函数,名字一样,编译器也需要区分。

所以,编译器必须给每个函数和变量生成一个独一无二的名字,才能在链接的时候不出岔子。这个过程就是 Name Mangling。

第二幕:Name Mangling 的规则:一团乱麻?

Name Mangling 的规则比较复杂,而且不同的编译器(比如 GCC、Clang、MSVC)的规则还不一样!这真是让人头大。

不过,我们可以了解一些基本的规则,起码能看懂个大概。

  • 基本原则: 编译器会把函数名、参数类型、命名空间、类名等信息编码到 Mangled Name 里。

  • 常见符号:

    • _Z (GCC/Clang): Mangled Name 的起始标志。
    • ? (MSVC): Mangled Name 的起始标志。
    • N (GCC/Clang): 命名空间的开始。
    • E (GCC/Clang): 命名空间的结束。
    • I (GCC/Clang): 模板实例化的开始
    • v: void
    • i: int
    • d: double
    • f: float
    • P: 指针
  • 举个栗子 (GCC/Clang):

    namespace MySpace {
        class MyClass {
        public:
            int myMethod(int x, double y);
        };
    }

    这个 myMethod 函数的 Mangled Name 可能是这样的:_ZN8MySpace7MyClass9myMethodEidi

    分解一下:

    • _Z: 起始标志
    • N: 命名空间开始
    • 8MySpace: 命名空间名长度为 8,名字是 MySpace
    • 7MyClass: 类名长度为 7,名字是 MyClass
    • 9myMethod: 函数名长度为 9,名字是 myMethod
    • E: 命名空间结束
    • i: 第一个参数是 int
    • d: 第二个参数是 double
  • 再来一个栗子 (MSVC):

    int __stdcall MyFunction(int a, float b);

    这个 MyFunction 函数的 Mangled Name 可能是这样的:?MyFunction@@YGHIH@Z

    分解一下:

    • ?: 起始标志
    • MyFunction: 函数名
    • @@: 分隔符
    • YG: 调用约定 (__stdcall)
    • H: int
    • I: float
    • H: int
    • @Z: 结束标志

表格总结 (GCC/Clang 常见符号):

符号 含义
_Z Mangled Name 起始标志
N 命名空间开始
E 命名空间结束
C 常量 (const)
V volatile
v void
i int
l long
x long long
f float
d double
b bool
P 指针
R 引用
F 函数类型
T 模板
I 模板实例化的开始

表格总结 (MSVC 常见符号):

符号 含义
? Mangled Name 起始标志
@@ 分隔符
Y 调用约定
H int
I float
N double
_ __stdcall 调用约定
A __cdecl 调用约定
E __thiscall 调用约定
M 静态成员
Q const
R 引用
@Z 结束标志

重要提示: 这些规则只是个大概,具体的 Mangling 规则非常复杂,而且会随着编译器版本变化。不要试图完全记住所有规则,理解基本原理就好。

第三幕:Demangling:把“暗号”翻译成人话

Name Mangling 后的名字,人类是看不懂的。所以,我们需要 Demangling,也就是把 Mangled Name 翻译回人话。

  • 工具: 很多编译器都提供了 Demangling 工具。

    • c++filt (GCC/Clang): Unix-like 系统下常用的 Demangling 工具。
    • undname (MSVC): Windows 下的 Demangling 工具。
  • 使用方法:

    # GCC/Clang
    c++filt _ZN8MySpace7MyClass9myMethodEidi
    
    # MSVC (在 Visual Studio 开发人员命令提示符中)
    undname "?MyFunction@@YGHIH@Z"
  • 在线 Demangler: 网上也有很多在线 Demangler 工具,方便快捷。

  • 代码 Demangling: 也可以在代码里进行 Demangling,但这通常需要使用特定的库或者编译器扩展,可移植性较差,不太推荐。

第四幕:实战演练:查看 Mangled Name

怎么知道一个函数或者变量的 Mangled Name 呢?

  • 编译器的输出: 编译器的错误信息和警告信息里,有时会包含 Mangled Name。
  • 反汇编工具: 使用反汇编工具(比如 objdumpIDA Pro)可以查看编译后的目标文件里的 Mangled Name。
  • 链接器: 链接器在链接过程中也会用到 Mangled Name,可以在链接器的输出里找到。
  • nm 工具: nm 工具可以列出一个目标文件或者库文件中的符号表,其中就包含 Mangled Name。

举个栗子:

  1. 创建 C++ 文件 (test.cpp):

    #include <iostream>
    
    namespace MySpace {
        class MyClass {
        public:
            int myMethod(int x, double y) {
                std::cout << "Hello from myMethod!" << std::endl;
                return 0;
            }
        };
    }
    
    int main() {
        MySpace::MyClass obj;
        obj.myMethod(10, 3.14);
        return 0;
    }
  2. 编译:

    g++ -c test.cpp -o test.o
  3. 使用 nm 查看 Mangled Name:

    nm test.o

    输出结果可能包含类似这样的行:

    0000000000000000 T _ZN8MySpace7MyClass9myMethodEidi

    这就是 myMethod 函数的 Mangled Name。

  4. 使用 c++filt 进行 Demangling:

    c++filt _ZN8MySpace7MyClass9myMethodEidi

    输出结果:

    MySpace::MyClass::myMethod(int, double)

    是不是清晰多了?

第五幕:Name Mangling 的应用场景

  • 动态链接库 (DLL/SO): 动态链接库需要使用 Mangled Name 来解决符号冲突和函数重载的问题。
  • 库的兼容性: 由于不同编译器的 Mangling 规则不一样,所以用不同编译器编译的库,可能无法直接链接在一起。
  • 调试: 在调试过程中,如果看到 Mangled Name,可以使用 Demangling 工具来理解代码的含义。
  • 逆向工程: 在逆向工程中,理解 Name Mangling 规则可以帮助分析代码的结构。

第六幕:Name Mangling 的坑:兼容性问题

Name Mangling 最大的问题就是兼容性。由于不同编译器 Mangling 规则不一样,所以:

  • 不同编译器编译的库,不能混用。 比如,用 GCC 编译的库,不能直接链接到用 MSVC 编译的程序里。
  • 即使是同一个编译器,不同版本之间,Mangling 规则也可能发生变化。 这会导致库的二进制兼容性问题。

为了解决这个问题,可以考虑以下方案:

  • 使用 C 接口: C 语言没有 Name Mangling,所以可以使用 C 接口作为桥梁,连接不同编译器编译的代码。
  • 使用标准 ABI: 一些平台定义了标准的 ABI (Application Binary Interface),规定了 Name Mangling 的规则,可以提高兼容性。
  • 避免使用复杂的 C++ 特性: 尽量避免使用复杂的 C++ 特性(比如模板、重载等),可以减少 Name Mangling 带来的问题。

第七幕:总结与建议

Name Mangling 是 C++ 编译器为了支持函数重载、命名空间等特性而采用的一种技术。虽然 Mangling 规则比较复杂,而且容易引起兼容性问题,但是它是 C++ 语言的重要组成部分。

  • 理解 Name Mangling 的基本原理,可以帮助我们更好地理解 C++ 的底层机制。
  • 学会使用 Demangling 工具,可以方便地查看和理解 Mangled Name。
  • 在实际开发中,要尽量避免 Name Mangling 带来的兼容性问题。

给各位观众老爷的建议:

  • 不要试图完全记住所有 Mangling 规则。
  • 善用 Demangling 工具。
  • 关注编译器的文档,了解最新的 Mangling 规则。
  • 在需要兼容性的场景下,尽量使用 C 接口。

好了,今天的 Name Mangling 和 Demangling 就聊到这里。希望各位观众老爷有所收获! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注