C++霧中風景15:聊聊讓人抓狂的Name Mangling

HappenLee發表於2020-09-27

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來最終解決問題的學習小結。

希望大家能夠有所收穫,筆者水平有限。成文之處難免有理解謬誤之處,歡迎大家多多討論,指教。

相關文章