前言
這個命令功能單一,但是非常強大,可以用來還原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語言編譯後的函數名對比
我們來寫一段相同的代碼,分別使用 gcc
和 g++
進行編譯,從代碼到可執行文件需要經歷“預處理、編譯、彙編、鏈接”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++支持
陽光、空氣、水,這些真的是好東西,當你真的快要失去它們才意識的到的話就有些晚了…