使用c++filt命令還原C++編譯後的函數名

前言

這個命令功能單一,但是非常強大,可以用來還原C++編譯後的函數名,爲什麼C++的函數名需要單獨的命令來還原,因爲他們看起來都是這樣 _ZNK4Json5ValueixEPKc、這樣 _Z41__static_initialization_and_destruction_0ii 或者這樣的 _ZN6apsara5pangu15ScopedChunkInfoINS0_12RafChunkInfoEED1Ev,僅通過這一串字母很難知道原函數的名字是什麼,參數類型就更難分析了,實際上C++在編譯函數時有一套命名函數的規則,每種參數使用什麼字母表示都是有約定的,但是通過學習這些約定來還原函數太麻煩了,還好有人編寫了 c++filt 命令可以讓我們直接得到編譯前的函數名,真好……

C++編譯後的函數名

C++ 編譯後的函數名字非常古怪,相比而言 C 語言編譯後的函數看起來就正常許多了,extern "C"、函數重載、name mangling 這些知識點都與 C++ 這個奇怪的函數名有些關係,extern "C" 的作用簡而言之就是告訴編譯器和鏈接器被“我”修飾的變量和函數需要按照 C 語言方式進行編譯和鏈接,這樣做是由於 C++ 支持函數重載,而 C 語言不支持,結果導致函數被 C++ 編譯後在符號庫中的名字和被 C語言編譯後的名字是不一樣的,程序編譯和連接就會出現問題,此類問題一般出現在 C++ 代碼調用 C 語言寫的庫函數的時候。

name mangling 就是實現 C++ 函數重載的一種技術或者叫做方式,要求同名的 C++ 函數參數個數不同或參數類型不同,如果只有返回值類型不同,那麼兩個函數被認爲是相同的函數,無法成功通過編譯。接下來我們就來看幾個例子,看看 C++ 編譯後的函數名有什麼變化。

C++和C語言編譯後的函數名對比

我們來寫一段相同的代碼,分別使用 gccg++ 進行編譯,從代碼到可執行文件需要經歷“預處理、編譯、彙編、鏈接”4個步驟,接下來爲了看到編譯後函數名的不同,我們只進行前兩步,生成彙編代碼,再來比較不同。

gcc編譯simple.c文件

// simple.c

int myadd(int a, int b)
{
    return a + b;
}

int main()
{
    int a = 110;
    int b = 119;
    int c = myadd(a, b);

    return 0;
}

gcc simple.c -S 生成彙編代碼文件simple.s內容

    .file   "simple.c"
    .text
    .globl  myadd
    .type   myadd, @function
myadd:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   myadd, .-myadd
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $110, -12(%rbp)
    movl    $119, -8(%rbp)
    movl    -8(%rbp), %edx
    movl    -12(%rbp), %eax
    movl    %edx, %esi
    movl    %eax, %edi
    call    myadd
    movl    %eax, -4(%rbp)
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

g++編譯simple.cpp文件

// simple.cpp

int myadd(int a, int b)
{
    return a + b;
}

int main()
{
    int a = 110;
    int b = 119;
    int c = myadd(a, b);

    return 0;
}

g++ simple.cpp -S 生成彙編代碼文件simple.s內容

    .file   "simple.cpp"
    .text
    .globl  _Z5myaddii
    .type   _Z5myaddii, @function
_Z5myaddii:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -20(%rbp)
    movl    %esi, -24(%rbp)
    movl    $0, -4(%rbp)
    movl    -20(%rbp), %edx
    movl    -24(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   _Z5myaddii, .-_Z5myaddii
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $110, -12(%rbp)
    movl    $119, -8(%rbp)
    movl    $0, -4(%rbp)
    movl    -8(%rbp), %edx
    movl    -12(%rbp), %eax
    movl    %edx, %esi
    movl    %eax, %edi
    call    _Z5myaddii
    movl    %eax, -4(%rbp)
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

雖然只有幾行代碼,可是生成彙編文件之後變成了50多行,我們只需要關注 myadd() 這個函數編譯之後變成了什麼就可以了,彙編代碼雖然不好讀,但是查找一個函數名應該沒問題的,對照着上面的代碼我們發現,myadd() 這個函數通過 gcc 編譯之後的函數名還是 myadd,而通過 g++ 編譯之後的函數名變成了 _Z5myaddii,可以明顯感覺到最後的兩個字母 i 代表的是參數 int,使用 c++filt 命令還原如下:

$ c++filt _Z5myaddii
myadd(int, int)

C++函數重載編譯後的函數名對比

我們還是在剛纔的代碼的基礎上增加一個參數類型不同的 myadd 函數,修改後的代碼如下:

int myadd(int a, int b)
{
    return a + b;
}

float myadd(float a, float b)
{
    return a + b;
}

int main()
{
    int c = myadd(110, 119);
    float d = myadd(52.0f, 13.14f);

    return 0;
}

g++ simple.cpp -S 生成彙編代碼文件simple.s內容爲:

    .file   "simple.cpp"
    .text
    .globl  _Z5myaddii
    .type   _Z5myaddii, @function
_Z5myaddii:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   _Z5myaddii, .-_Z5myaddii
    .globl  _Z5myaddff
    .type   _Z5myaddff, @function
_Z5myaddff:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movss   %xmm0, -4(%rbp)
    movss   %xmm1, -8(%rbp)
    movss   -4(%rbp), %xmm0
    addss   -8(%rbp), %xmm0
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   _Z5myaddff, .-_Z5myaddff
    .globl  main
    .type   main, @function
main:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $119, %esi
    movl    $110, %edi
    call    _Z5myaddii
    movl    %eax, -8(%rbp)
    movss   .LC0(%rip), %xmm1
    movss   .LC1(%rip), %xmm0
    call    _Z5myaddff
    movd    %xmm0, %eax
    movl    %eax, -4(%rbp)
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .section    .rodata
    .align 4
.LC0:
    .long   1095908721
    .align 4
.LC1:
    .long   1112539136
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

這次一共3個函數,生成的彙編代碼更長,但是我們一眼就能看見彙編代碼中包含 _Z5myaddii_Z5myaddff 兩個函數,這就是函數重載的產物,兩個參數類型不同的同名函數編譯之後生成了不同的名字,_Z5myaddff 函數末尾的兩個 f 應該指的就是參數類型 float

使用c++filt定位問題示例

c++filt的作用就是還原函數名字,它可以幫我們查找動態鏈接庫中缺少的函數,還原崩潰堆棧中一大串的函數名字母等等,下面來看一個崩潰堆棧的例子,代碼內容儘量簡寫,只爲了說明問題,現實情況可能要複雜的多。

首先定義一個打印函數堆棧的函數,參考之前的總結《linux環境下C++代碼打印函數堆棧調用情況》,代碼如下:

#include <execinfo.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <iostream>

void show_stack(int nSignal)
{
    static const int MAX_STACK_FRAMES = 12;
    void *pStack[MAX_STACK_FRAMES];
    static char szStackInfo[1024 * MAX_STACK_FRAMES];

    char ** pStackList = NULL;
    int frames = backtrace(pStack, MAX_STACK_FRAMES);
    pStackList = backtrace_symbols(pStack, frames);
    if (NULL == pStackList)
        return;

    strcpy(szStackInfo, "stack traceback:\n");
    for (int i = 0; i < frames; ++i)
    {
        if (NULL == pStackList[i])
            break;

        strncat(szStackInfo, pStackList[i], 1024);
        strcat(szStackInfo, "\n");
    }

    std::cout << szStackInfo; // 輸出到控制檯,也可以打印到日誌文件中
}

再寫一段隱藏着崩潰問題的代碼:

#include <string>
class CTest
{
public:
    const std::string& get_string() {return s;}
    void set_string(const std::string& str) {s = str;}
private:
    std::string s;
};

void foo(float z)
{
    int *p = nullptr;
    *p = 110;
    std::cout << z;
}

void test(std::string str)
{
    CTest* pTest = new CTest();
    pTest->set_string("20200517");
    const std::string& s = pTest->get_string();
    delete pTest;

    std::cout << str << std::endl;
    if (s == "20200517") foo(13.14);
}

void func(int a, int b)
{
    std::string s = std::to_string(a) + std::to_string(b);
    test(s);
}

int main()
{
    signal(SIGSEGV, show_stack);
    func(250, 520);
    return 0;
}

編譯運行,果然崩潰了:

$ g++ simple.cpp --std=c++11
$ ./a.out
stack traceback:
./a.out() [0x401aff]
/lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7fd5f98b54b0]
/lib/x86_64-linux-gnu/libc.so.6(+0x16eff6) [0x7fd5f99eeff6]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE7compareEPKc+0x3a) [0x7fd5f9f9145a]
./a.out() [0x4022b6]
./a.out() [0x401d30]
./a.out() [0x401e27]
./a.out() [0x401ed8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fd5f98a0830]
./a.out() [0x4019f9]

這時崩潰的堆棧中發現了一個特別長的函數 _ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE7compareEPKc,使用 c++filt 命令來還原函數:

$ c++filt _ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE7compareEPKc
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::compare(char const*) const

從函數名來看是一個與字符串相關的 compare 函數,查看代碼發現是 s == "20200517" 這一句的問題,所以說能確切的知道函數名對我們查找問題來說還是挺有幫助的。

總結

  • c++filt 命令可以還原 C++ 爲實現函數重載採用 name mangling 搞出來的奇奇怪怪的函數名
  • 註冊信號回調函數方式:signal(SIGSEGV, show_stack);SIGSEGV代表無效的內存引用
  • 注意 C 語言和 C++ 在編譯後函數命名方式的不同,C 語言不支持嚴格意義的重載,C++支持

陽光、空氣、水,這些真的是好東西,當你真的快要失去它們才意識的到的話就有些晚了…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章