使用c++filt命令还原C++编译后的函数名

文章目录

前言C++编译后的函数名C++和C语言编译后的函数名对比gcc编译simple.c文件g++编译simple.cpp文件

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

使用c++filt定位问题示例总结

前言

这个命令功能单一,但是非常强大,可以用来还原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

#include

#include

#include

#include

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

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, std::allocator >::compare(char const*) const

从函数名来看是一个与字符串相关的 compare 函数,查看代码发现是 s == "20200517" 这一句的问题,所以说能确切的知道函数名对我们查找问题来说还是挺有帮助的。

总结

c++filt 命令可以还原 C++ 为实现函数重载采用 name mangling 搞出来的奇奇怪怪的函数名注册信号回调函数方式:signal(SIGSEGV, show_stack);,SIGSEGV代表无效的内存引用注意 C 语言和 C++ 在编译后函数命名方式的不同,C 语言不支持严格意义的重载,C++支持

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