Name Mangling,直接翻譯過來為名字改寫 。它是深入理解 C++ 編譯鏈接模型的必由之路。
筆者近期進行數據庫開發工作時,涉及到MySQL客戶端的編譯鏈接的問題,通過重新厘清了之前理解一知半解的Name Manging,解決了讓人抓狂的編譯鏈接問題。
接下來,和大家聊聊C++的Name Mangling。
1.什么是Name Mangling
1.1 Name Mangling的作用
在進行編程的過程之中,我們常常遇見變量或函數重名的情況。比如:函數的重載,或通過不同程序塊與命名空間變量與函數的重名。
而在出現變量或函數名相同的情況下,編譯器進行代碼編譯時需要保證變量與函數的簽名的全局唯一性。如果無法進行上述保證,在鏈接階段就會產生鏈接的二義性,會導致編譯器不知道應該如何取用正確的變量與函數符號的內存地址。
為了解決上述問題,編譯器實現了一種叫做Name Mangling的方式:它通過一個固定的命名規則來重新組織源代碼之中我們定義的變量名和函數名,來確保了能夠將被鏈接的目標文件中的符號簽名的唯一性。(由於在C++的標准之中,並未強制規定Name Mangling的實現機制,所以不同的編譯器在不同的平台上實現是完全不同的。筆者的后續關於Name Mangling的講解將基於Linux上的GCC展開。)
1.2 舉個栗子
上述內容講明白了Name Mangling的意義,我們來通過實際的代碼來瞅瞅它是如何生效的。
首先看看如下代碼:
#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來最終解決問題的學習小結。
希望大家能夠有所收獲,筆者水平有限。成文之處難免有理解謬誤之處,歡迎大家多多討論,指教。