C++雾中风景15:聊聊让人抓狂的Name Mangling

Name Mangling,直接翻译过来为名字改写 。它是深刻理解 C++ 编译连接模型的必由之路。
笔者近期进行数据库开发工做时,涉及到MySQL客户端的编译连接的问题,经过从新厘清了以前理解只知其一;不知其二的Name Manging,解决了让人抓狂的编译连接问题。
接下来,和你们聊聊C++的Name Manglingios

1.什么是Name Mangling

1.1 Name Mangling的做用

在进行编程的过程之中,咱们经常碰见变量或函数重名的状况。好比:函数的重载,或经过不一样程序块与命名空间变量与函数的重名。c++

而在出现变量或函数名相同的状况下,编译器进行代码编译时须要保证变量与函数的签名的全局惟一性。若是没法进行上述保证,在连接阶段就会产生连接的二义性,会致使编译器不知道应该如何取用正确的变量与函数符号的内存地址。数据库

为了解决上述问题,编译器实现了一种叫作Name Mangling的方式:它经过一个固定的命名规则来从新组织源代码之中咱们定义的变量名和函数名,来确保了可以将被连接的目标文件中的符号签名的惟一性。(因为在C++的标准之中,并未强制规定Name Mangling的实现机制,因此不一样的编译器在不一样的平台上实现是彻底不一样的。笔者的后续关于Name Mangling的讲解将基于Linux上的GCC展开。)编程

1.2 举个栗子

上述内容讲明白了Name Mangling的意义,咱们来经过实际的代码来瞅瞅它是如何生效的。api

首先看看以下代码:app

#include <iostream>
#include <string>
#include <vector>

namespace Happen {
   struct MyClass {
       std::vector<std::string> _str_vec;
   };
}

int main() {
   Happen::MyClass myClass;
   return 0;
}

接下来,咱们使用g++获取它的汇编代码编辑器

g++ -S main.cpp

使用编辑器打开生成的main.s文件,咱们就能够看到下面这些被Name Mangling以后的命名了。函数

call    _ZN6Happen7MyClassC1Ev
        movl    $0, %ebx
        leaq    -48(%rbp), %rax
        movq    %rax, %rdi
        call    _ZN6Happen7MyClassD1Ev

这里能够看到,代码调用了_ZN6Happen7MyClassC1Ev_ZN6Happen7MyClassD1Ev这两个函数。这其实就是代码之中调用了咱们定义的MyClass的构造函数与析构函数。而这里使人望而生畏的命名就是Name Mangling的功劳啦~~工具

2. Name DeMangling

既然有了Name Mangling了,天然就要有Name DeMangling。上面的_ZN6Happen7MyClassC1Ev还能大概齐猜想出意思,可是你肯定你能看懂下面的这一长串Name Mangling以后的结果:学习

MN6Happen7MyClassESt6vectorINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESaIS7_EE
2.1 经过API进行Name DeMangling

在C++之中,咱们经常使用typeid,来获取类型的type_info信息,而Name Mangling就包含在type_info之中,咱们来看以下代码:

#include <iostream>
#include <string>
#include <vector>

namespace Happen {
    struct MyClass {
        std::vector<std::string> _str_vec;
    };
}

int main() {
    std::cout << typeid(&Happen::MyClass::_str_vec).name() << "\n";
    return 0;
}

它的输出正是上面那串让人「抓狂」的命名,咱们如今尝试经过GNU的API来脱掉它的马甲,真正的看看它究竟是啥。
这里使用了abi::__cxa_demangle来获取DeMangling时真正的结果。

#include <iostream>
#include <string>
#include <vector>
#include <cxxabi.h>

namespace Happen {
    struct MyClass {
        std::vector<std::string> _str_vec;
    };
}

int main() {
    char* real_name = abi::__cxa_demangle(typeid(&Happen::MyClass::_str_vec).name(), \
    nullptr, nullptr, nullptr);
    std::cout << real_name << "\n";
    return 0;
}

这是经过Name DeMangling实际输出的结果。(囧rz,好像可读性也并无太好,C++的类型系统实在是太复杂了,不过起码能让咱们看清楚真正的名字是啥了。)

std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > Happen::MyClass::*
2.2 使用nm或c++filt进行Name Demangling

经过代码进行名字辨析确实会带来诸多不便,因此Linux提供了两个好用的工具:
nm与c++filt,它们能够做用在二进制文件,函数连接库等之上(nm其实就是name mangling的缩写)

经过nm的-C参数就能够直接输出name demangling以后的结果了。

nm -C bin/.so/.a

或者也能够经过c++filt来实现一样的功能

nm bin/.so/.a | c++filt

3.C语言的Name Mangling

C++可以支持调用C语言的函数,一样也支持实现函数库被C语言调用,这个过程之中就涉及到两种语言交互的Name Mangling了。(这个问题会经常致使编译时出现使人抓狂的undefined reference to 『xxx』, 不少时候会让人丈二和尚摸不着头脑

3.1 二者的区别

因为C语言不支持函数重载,命名空间,类等逻辑,因此C语言的Name Mangling比C++简单不少。咱们来看看经过gcc和g++的编译结果有和不一样吧,首先咱们定义一个简单的函数sum

int sum(int a, int b) {
    return a + b;
}
  • g++的编译结果
    _Z3sumii
  • gcc的编译结果
    sum

这里能够明显看到两者的不一样,因为C++支持函数重载。因此须要在Name Mangling时添加参数的信息,也就是后面的两个ii,指代两个int类型。

3.2 extern "C"

因此经过C++定义的函数须要被C语言调用时,须要经过keyword:extern C来显式的让编译器明白须要使用C语言的Name Mangling规则,以便编译器连接时可以正确的识别函数签名来定位到所需的函数。

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

将上述函数改写为上面的方式以后,经过g++编译的结果也变为了咱们所期待的sum了。

4.小结

C++的编译连接问题经常让人抓狂,不少时候若是没有深刻了解这个过程之中的逻辑,很容易陷入困境。本篇聊了聊笔者在遇到编译问题时学习Name Mangling来最终解决问题的学习小结。

但愿你们可以有所收获,笔者水平有限。成文之处不免有理解谬误之处,欢迎你们多多讨论,指教。