C++ 符号解析与 `demangling`:理解编译器如何处理 C++ 符号

哈喽,各位好!今天我们要聊聊 C++ 符号解析和 demangling,这玩意儿听起来有点像魔法,但实际上是编译器背后默默付出的辛勤劳动。如果你曾经在编译错误信息里看到一堆乱码,或者在调试器里发现函数名变得奇奇怪怪,那么这篇文章就是为你准备的。

第一幕:符号,符号,到处都是符号!

在 C++ 的世界里,几乎所有东西都有一个符号(Symbol)。变量、函数、类、命名空间…… 它们就像一个个贴着标签的盒子,方便编译器和链接器找到它们。

想象一下,你写了一个简单的 C++ 程序:

// my_math.h
namespace my_math {
  int add(int a, int b);
}

// my_math.cpp
#include "my_math.h"

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

// main.cpp
#include <iostream>
#include "my_math.h"

int main() {
  int result = my_math::add(5, 3);
  std::cout << "Result: " << result << std::endl;
  return 0;
}

当你编译这段代码时,编译器会为 my_math::add 函数生成一个符号。这个符号不是简单的 "add",而是一个经过“修饰”(mangled)的版本,包含了足够的信息来唯一标识这个函数。

第二幕:为什么需要 Mangling?

你可能会问,为什么编译器要把函数名搞得这么复杂?直接用 "add" 不行吗?

原因在于 C++ 支持函数重载(Function Overloading)。你可以定义多个同名但参数不同的函数:

int add(int a, int b);         // add(int, int)
double add(double a, double b);  // add(double, double)
int add(int a, int b, int c);   // add(int, int, int)

如果编译器都用 "add" 作为符号,那链接器就懵逼了: "我要链接哪个 add 啊?"

所以,Mangling 的目的就是创造唯一的符号,即使函数名相同,但参数类型或所在的命名空间不同,它们的符号也不同。

第三幕:Mangling 的规则(剧透:很复杂!)

不同的编译器有不同的 Mangling 规则,但它们通常都遵循一定的模式。我们以 g++ (GNU Compiler Collection) 为例,看看 Mangling 后的符号长什么样:

原始函数签名 Mangled 后的符号
int add(int a, int b) _Z3addi(int, int)
double add(double a, double b) _Z3adddd(double, double)
int my_math::add(int a, int b) _ZN7my_math3addEii

看起来像外星文?别怕,我们来解剖一下:

  • _Z: 这是 g++ Mangling 的前缀,表示这是一个 Mangled 的符号。其他编译器可能有不同的前缀。
  • 3add: 函数名的长度和函数名本身。例如, 3 表示函数名 add 有三个字符。
  • i: 参数类型 int 的编码。
  • d: 参数类型 double 的编码。
  • N7my_math3addEii: 比较复杂,N 表示命名空间开始,7my_math 表示命名空间 my_math 长度为 7,3add是函数名, E 表示命名空间结束,后面的 ii 是参数类型。

更复杂的例子:

class MyClass {
public:
  int multiply(int a, int b) { return a * b; }
};

Mangled 后的符号可能是: _ZN7MyClass8multiplyEii (具体取决于编译器版本和编译选项)。 8multiply 表示 multiply 函数名长度为 8。

表格:常见的 C++ 类型编码

类型 编码
int i
double d
float f
char c
long l
unsigned int j
void v
std::string Ss
指针 int* Pi
引用 int& Ri

第四幕:Demangling – 把外星文翻译成人话

看到这些 Mangled 后的符号,我们人类肯定受不了。幸好,我们有 Demangling 工具!Demangling 就是把 Mangled 后的符号还原成我们能理解的形式。

大多数 C++ 编译器都提供了 Demangling 工具。例如,g++ 提供了一个名为 c++filt 的工具。

你可以这样使用 c++filt

c++filt _ZN7my_math3addEii

输出:

my_math::add(int, int)

哇,瞬间清爽了!

在 Linux 系统中,addr2line 工具也可以结合 c++filt 来使用,从地址信息还原到函数名称。 假设你有一个程序的 core dump 文件,并且知道某个地址,你可以这样做:

addr2line -C -f -e your_program address

其中 -C 选项告诉 addr2line 使用 c++filt 进行 Demangling,-f 选项显示函数名,-e your_program 指定可执行文件。

第五幕: Demangling 在实际开发中的应用场景

  1. 调试器 (Debugger):

    几乎所有的调试器(例如 GDB, LLDB)都内置了 Demangling 功能。当你单步调试代码时,调试器会自动把 Mangled 后的函数名还原成可读的形式,方便你理解代码的执行流程。
    如果你使用的是 GDB,你可以使用 set print demangle on 命令来启用 Demangling。

    (gdb) set print demangle on
    (gdb) break my_math::add
    Breakpoint 1 at 0x400644: file my_math.cpp, line 5.
    (gdb) run
  2. 日志 (Logging):

    在日志中记录函数名是很常见的做法。如果你想让日志更易读,可以使用 Demangling 工具来还原函数名。

    #include <iostream>
    #include <cxxabi.h> // For demangling
    
    std::string demangle(const char* name) {
        int status = -1;
        std::unique_ptr<char, void(*)(void*)> res {
            abi::__cxa_demangle(name, NULL, NULL, &status),
            std::free
        };
        return (status==0) ? res.get() : name ;
    }
    
    int main() {
        std::cout << demangle("_ZN7my_math3addEii") << std::endl; // 输出 my_math::add(int, int)
        return 0;
    }

    注意: 上面的代码依赖于 cxxabi.h 头文件,这是 GNU C++ 标准库的一部分。其他编译器可能有不同的 Demangling API。 代码使用了 std::unique_ptr 来自动管理 __cxa_demangle 分配的内存,防止内存泄漏。

  3. 错误信息 (Error Messages):

    编译器产生的错误信息通常包含 Mangled 后的符号。虽然编译器也会尝试 Demangling 一部分信息,但有时候不够完整。 你可以使用 c++filt 手动 Demangling 错误信息中的符号,以便更好地理解错误的原因。

    例如,你可能会看到这样的错误信息:

    undefined reference to `_ZN7MyClass5doSomethingEi'

    使用 c++filt _ZN7MyClass5doSomethingEi 可以得到:

    MyClass::doSomething(int)

    这样就能更清楚地知道哪个函数未定义了。

  4. 动态库 (Dynamic Libraries):

    在开发动态库时,导出符号 (Exported Symbols) 会影响库的兼容性。了解 Mangling 规则可以帮助你控制导出的符号,避免不必要的 ABI (Application Binary Interface) 兼容性问题。

    例如,你可以使用 extern "C" 来阻止 C++ 编译器 Mangling 函数名,从而创建一个 C 风格的 API,方便其他语言调用。

    extern "C" {
      int my_c_function(int a, int b) {
        return a + b;
      }
    }

    这样导出的符号就是 my_c_function,而不是 Mangled 后的版本。

第六幕:Mangling 的兼容性问题

不同的编译器使用不同的 Mangling 规则。这意味着用一个编译器编译的代码,不能直接链接到用另一个编译器编译的代码(除非它们恰好使用了相同的 Mangling 规则)。

这被称为 ABI 不兼容 (ABI Incompatibility)。ABI 不兼容会导致链接错误,或者更糟糕,运行时崩溃。

为了避免 ABI 不兼容,通常需要:

  • 使用相同的编译器和编译器版本编译所有代码。
  • 使用 C 风格的 API (extern "C") 来创建跨编译器兼容的接口。
  • 使用稳定的 ABI,例如 COM (Component Object Model) 或 CORBA (Common Object Request Broker Architecture)。

第七幕:实战演练 – 查看 Mangled 后的符号

我们可以使用 objdumpnm 工具来查看目标文件或库中的符号。

  1. 使用 objdump:

    g++ -c my_math.cpp -o my_math.o  // 编译成目标文件
    objdump -t my_math.o | grep add

    输出可能包含:

    0000000000000000 g     F .text  0000000000000019 _ZN7my_math3addEii

    -t 选项表示显示符号表。 grep add 用于过滤包含 "add" 的符号。

  2. 使用 nm:

    nm my_math.o | grep add

    输出可能包含:

                     U _GLOBAL_OFFSET_TABLE_@GOTPCREL
    0000000000000000 T _ZN7my_math3addEii

    nm 工具也可以显示符号表。

第八幕:高级话题 – Name Decoration (Windows)

在 Windows 上,Microsoft Visual C++ 编译器使用一种称为 Name Decoration 的 Mangling 机制。它的规则与 g++ 的 Mangling 规则不同。

例如,int add(int a, int b) 在 Visual C++ 中可能会被 Mangled 成 ?add@@YAHHH@Z

  • ? : Name Decoration 的前缀。
  • add : 函数名。
  • @@YA : 调用约定 (Calling Convention)。
  • H : int 类型的编码。
  • @Z : Name Decoration 的结束符。

Visual Studio 提供了 DUMPBIN 工具来查看和 Demangling Windows 上的符号。

DUMPBIN /SYMBOLS your_program.exe

第九幕:总结与展望

C++ 符号解析和 Demangling 是编译器背后默默工作的机制,它们对于理解编译、链接和调试过程至关重要。

  • Mangling:将函数名进行编码,生成唯一的符号,以支持函数重载和命名空间等特性。
  • Demangling:将 Mangled 后的符号还原成可读的形式,方便我们理解代码和错误信息。
  • 兼容性:不同的编译器使用不同的 Mangling 规则,可能导致 ABI 不兼容。

虽然 Mangling 规则很复杂,但我们不需要完全记住它们。掌握 Demangling 工具的使用,以及了解 Mangling 的基本原理,就足以应对大多数开发场景。

随着 C++ 标准的不断发展,Mangling 规则也在不断变化。了解这些变化可以帮助我们更好地理解 C++ 的底层机制,编写更健壮、更可维护的代码。

希望这次讲座能帮助你揭开 C++ 符号解析和 Demangling 的神秘面纱! 下次再遇到 Mangled 后的符号,不要害怕,用 Demangling 工具把它变成人话!

好啦,今天就到这里,感谢大家的聆听!

发表回复

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