使用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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    .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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    .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 命令还原如下:

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

C++函数重载编译后的函数名对比

我们还是在刚才的代码的基础上增加一个参数类型不同的 myadd 函数,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    .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++代码打印函数堆栈调用情况》,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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; // 输出到控制台,也可以打印到日志文件中
}

再写一段隐藏着崩溃问题的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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;
}

编译运行,果然崩溃了:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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 命令来还原函数:

1
2
$ 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++支持

阳光、空气、水,这些真的是好东西,当你真的快要失去它们才意识的到的话就有些晚了…

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客