C++ Name Mangling 与 Demangling:C++ 符号在二进制文件中的表示

哈喽,各位好!

今天咱们来聊聊C++里一个有点“神秘”,但又无处不在的东西:Name Mangling(名字修饰)与 Demangling(名字反修饰)。这玩意儿就像C++编译器给函数、变量起的小名,目的是让它们在二进制文件里区分开来,避免重名冲突。听起来是不是有点像给幼儿园小朋友编号?

准备好了吗?咱们开始吧!

1. 为什么要Name Mangling?

想象一下,如果没有Name Mangling,会发生什么?

假设你有两个文件:

  • file1.cpp:

    int add(int a, int b) {
      return a + b;
    }
  • file2.cpp:

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

这两个文件里都有一个名为 add 的函数,但它们的参数类型不同。如果没有Name Mangling,编译器编译后会得到两个同名的函数,链接器在链接的时候就会懵逼:“我该用哪个add呢?” 这就导致了链接错误。

Name Mangling 就是为了解决这个问题。它通过在函数名后面加上一些信息,比如参数类型、类名、命名空间等,来生成一个独一无二的名字,让链接器可以区分不同的函数。

2. Name Mangling 的规则

C++标准没有规定Name Mangling的具体规则,因此不同的编译器(比如GCC、Clang、MSVC)有不同的实现方式。但它们的基本思路是相似的:把函数签名(包括函数名、参数类型、返回值类型、所属类、命名空间等)编码到函数名中。

咱们以GCC为例,来简单了解一下Name Mangling的规则:

  • 全局函数: 通常以 _Z 开头,后面跟着函数名的长度和函数名本身,然后是参数类型的编码。

    例如: int add(int a, int b) 会被 Mangling 成 _Z3addii (3表示函数名add的长度,ii表示两个int类型的参数)。

  • 成员函数: 会包含类名和作用域信息。

    例如: class MyClass { public: int method(int a); }; 会被 Mangling 成 _ZN7MyClass6methodEi (7表示类名MyClass的长度,6表示函数名method的长度, E表示结束, i表示 int类型的参数)。

  • 命名空间: 会包含命名空间的信息。

    例如: namespace MyNamespace { int func(); } 会被 Mangling 成 _ZN11MyNamespace4funcEv (11表示命名空间名MyNamespace的长度,4表示函数名func的长度, v表示 void,没有参数)。

当然,这只是一个非常简化的描述。实际的Name Mangling规则要复杂得多,涉及到各种类型、模板、const修饰符、异常规范等等。

3. Name Mangling 的例子

咱们来看一些具体的例子,感受一下Name Mangling的威力:

函数签名 GCC Mangled Name Clang Mangled Name MSVC Mangled Name 说明
int add(int a, int b); _Z3addii _Z3addii ?add@@YAHHH@Z 简单的全局函数,两个int参数
double add(double a, double b); _Z3adddd _Z3adddd ?add@@YANNN@Z 简单的全局函数,两个double参数
void print(); _Z5printv _Z5printv ?print@@YAXXZ 简单的全局函数,无参数
namespace MySpace { int foo(); } _ZN7MySpace3fooEv _ZN7MySpace3fooEv ?foo@MySpace@@YAHXZ 包含命名空间的函数
class MyClass { public: int bar(); }; _ZN7MyClass3barEv _ZN7MyClass3barEv ?bar@MyClass@@QAEHXZ 类成员函数
int add(int a, int b) noexcept; _Z3addiiNSt9nothrowtE _Z3addiiNSt9nothrowtE ?add@@YAHHH@Z (MSVC不encode noexcept) noexcept函数,注意GCC和Clang会编码noexcept关键字
template <typename T> T max(T a, T b); _Z3maxIiET_S0_S0_ _Z3maxIiET_S0_S0_ ??$max@H@@YAHHH@Z 函数模板,Ii表示 int 类型
int MyClass::operator+(int other) { return 0; } _ZN7MyClassplEi _ZN7MyClassplEi ??HMyClass@@QAEHH@Z 重载运算符,注意pl代表operator+
int add(int a, int b, int c = 0); _Z3addiii _Z3addiii ?add@@YAHHHH@Z 带有默认参数的函数。 注意:GCC和Clang会编码所有的参数类型,即使有默认参数。 MSVC也编码所有的参数类型。

注意: 以上只是一些简单的例子,实际的Name Mangling规则要复杂得多。不同的编译器和不同的C++标准可能会产生不同的Mangled Name。

咱们可以用一些工具来查看Name Mangling的结果,比如:

  • c++filt (Linux/macOS): 这是一个命令行工具,可以用来 Demangle Mangled Name。

    例如: c++filt _Z3addii 会输出 add(int, int)

  • undname (Windows): 这是MSVC提供的工具,可以用来 Demangle MSVC Mangled Name。

    例如: undname "?add@@YAHHH@Z" 会输出 int __cdecl add(int,int)

4. Demangling 的作用

Demangling 就是 Name Mangling 的逆过程,它可以把 Mangled Name 转换回原始的函数签名,让人更容易理解。

Demangling 在以下情况下非常有用:

  • 调试: 当你在调试器里看到 Mangled Name 时,可以用 Demangling 工具将其转换回原始的函数签名,方便你理解代码的逻辑。

  • 分析崩溃报告: 崩溃报告里通常会包含 Mangled Name,可以用 Demangling 工具将其转换回原始的函数签名,帮助你定位问题。

  • 逆向工程: 在逆向工程中,Demangling 可以帮助你理解二进制代码的功能。

5. 如何在代码中使用 Demangling

虽然我们可以使用 c++filtundname 这样的工具来 Demangle Mangled Name,但有时候我们也需要在代码中进行 Demangling。

C++标准库并没有提供 Demangling 的函数,但我们可以使用编译器提供的扩展来实现。

  • GCC/Clang: 可以使用 abi::__cxa_demangle 函数。

    #include <iostream>
    #include <cxxabi.h>
    #include <string>
    
    std::string demangle(const char* mangled_name) {
      int status = -4; // some arbitrary value to detect errors
      std::unique_ptr<char, void(*)(void*)> res {
          abi::__cxa_demangle(mangled_name, NULL, NULL, &status),
          std::free
      };
      return (status==0) ? res.get() : mangled_name;
    }
    
    int main() {
      std::cout << demangle("_Z3addii") << std::endl; // Output: add(int, int)
      return 0;
    }

    注意: abi::__cxa_demangle 是 GCC/Clang 的扩展,不是C++标准的一部分。

  • MSVC: 可以使用 UnDecorateSymbolName 函数。

    #include <iostream>
    #include <string>
    #include <windows.h>
    #include <memory>
    
    std::string demangle(const char* mangled_name) {
      std::unique_ptr<char[]> buffer(new char[1024]); // 假设buffer足够大
      DWORD result = UnDecorateSymbolName(
        mangled_name,
        buffer.get(),
        1024,
        UNDNAME_COMPLETE // Use full expansion
      );
    
      if (result != 0) {
        return std::string(buffer.get());
      } else {
        return mangled_name; // Demangling failed, return original name
      }
    }
    
    int main() {
      std::cout << demangle("?add@@YAHHH@Z") << std::endl;
      return 0;
    }

    注意: UnDecorateSymbolName 是 Windows API 的一部分,不是C++标准的一部分。

6. Name Mangling 的影响

Name Mangling 虽然解决了函数重名的问题,但也带来了一些影响:

  • 可读性降低: Mangled Name 很难阅读和理解,给调试和分析代码带来了一定的困难。

  • ABI兼容性问题: 不同的编译器使用不同的Name Mangling规则,导致不同编译器编译的代码在链接时可能会出现问题。这就是ABI(Application Binary Interface)兼容性问题。

  • 动态链接库版本问题: 如果动态链接库的函数签名发生变化(比如参数类型改变),会导致Name Mangling的结果发生变化,从而导致链接错误。

为了解决ABI兼容性问题,可以采取以下措施:

  • 使用C接口: C语言没有Name Mangling,因此可以使用C接口来避免ABI兼容性问题。

  • 使用稳定的ABI: 有些编译器提供了稳定的ABI选项,可以保证Name Mangling的结果在不同的编译器版本之间保持一致。

  • 使用版本控制: 对动态链接库进行版本控制,可以避免因函数签名变化导致的链接错误。

7. extern "C" 的作用

extern "C" 是一个C++关键字,用于告诉编译器按照C语言的规则来编译代码。这意味着:

  • 不进行Name Mangling: 使用 extern "C" 声明的函数不会进行Name Mangling,而是使用原始的函数名。

  • 使用C调用约定: 使用 extern "C" 声明的函数使用C语言的调用约定,而不是C++的调用约定。

extern "C" 通常用于以下情况:

  • 与C代码交互: 当C++代码需要调用C代码时,需要使用 extern "C" 来声明C函数,以避免Name Mangling和调用约定不匹配的问题。

  • 创建C接口: 当需要创建一个C接口供其他语言调用时,需要使用 extern "C" 来声明C函数,以避免Name Mangling。

// C++ header file (myheader.h)
#ifndef MYHEADER_H
#define MYHEADER_H

#ifdef __cplusplus
extern "C" {
#endif

int my_c_function(int x); // A C-style function

#ifdef __cplusplus
}
#endif

#endif // MYHEADER_H
// C++ implementation file (mycppfile.cpp)
#include "myheader.h"

int my_c_function(int x) {
  return x * 2;
}

8.总结

Name Mangling 是C++编译器为了解决函数重名问题而采用的一种技术,它通过在函数名后面加上一些信息来生成一个独一无二的名字。Demangling 是 Name Mangling 的逆过程,它可以把 Mangled Name 转换回原始的函数签名。 Name Mangling 带来了一些影响,比如可读性降低和ABI兼容性问题。可以使用 extern "C" 来避免Name Mangling。

表格总结:

特性 Name Mangling Demangling extern "C"
目的 解决函数重名问题,生成唯一符号 将Mangled Name还原为原始函数签名 避免Name Mangling,使用C调用约定
适用场景 C++编译,需要区分不同函数签名的情况 调试、分析崩溃报告、逆向工程 与C代码交互、创建C接口
影响 可读性降低、ABI兼容性问题 限制C++特性,降低类型安全性
编译器支持 各编译器实现不同规则 c++filt (GCC/Clang), undname (MSVC), 编译器扩展 C++关键字,所有编译器都支持
代码示例 (见前面的例子) (见前面的例子) (见前面的例子)
解决的问题 函数重载、命名空间、类成员函数等带来的重名问题 方便人类阅读和理解Mangled Name 与C代码的链接和调用问题,生成C风格的API
带来的问题 ABI兼容性,链接时可能出现问题 需要额外的工具或代码来实现 降低C++代码的类型安全性和灵活性,无法使用C++的一些高级特性

好啦,今天关于C++ Name Mangling 和 Demangling 的分享就到这里。希望大家有所收获!下次再见!

发表回复

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