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

60 篇文章 6 订阅
2 篇文章 0 订阅

前言

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

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

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 30
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AlbertS

常来“玩”啊~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值