当前位置: 首页 > news >正文

使用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语言编译后的函数名对比

我们来写一段相同的代码,分别使用 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++支持

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

相关文章:

  • 配置Beyond Compare 4作为git mergetool来解决git merge命令导致的文件冲突
  • git在回退版本时HEAD~和HEAD^的作用和区别
  • 对称加密、非对称加密、公钥、私钥究竟是个啥?
  • 认证、HTTPS、证书的基本含义
  • 码龄10年工作6年的搬砖小哥,最常访问的学习网站都在这里了
  • C++中的std::lower_bound()和std::upper_bound()函数
  • 根证书的应用和信任基础
  • Shell脚本中获取命令运行结果、特殊变量使用、条件判断等常用操作
  • gdb调试解决找不到源代码的问题
  • GDB调试指北大全
  • 小白眼中的docker究竟是个什么东西
  • GDB调试指北-启动GDB并查看说明信息
  • Redis源码-BFS方式浏览main函数
  • GDB调试指北-启动调试或者附加到进程
  • Python中时间戳、时间字符串、时间结构对象之间的相互转化
  • ES6指北【2】—— 箭头函数
  • [deviceone开发]-do_Webview的基本示例
  • [js高手之路]搞清楚面向对象,必须要理解对象在创建过程中的内存表示
  • 03Go 类型总结
  • CentOS从零开始部署Nodejs项目
  • echarts的各种常用效果展示
  • IDEA常用插件整理
  • javascript 哈希表
  • JS基础篇--通过JS生成由字母与数字组合的随机字符串
  • k8s如何管理Pod
  • MySQL主从复制读写分离及奇怪的问题
  • Odoo domain写法及运用
  • Python利用正则抓取网页内容保存到本地
  • React 快速上手 - 07 前端路由 react-router
  • Selenium实战教程系列(二)---元素定位
  • 前端每日实战:61# 视频演示如何用纯 CSS 创作一只咖啡壶
  • 前端性能优化--懒加载和预加载
  • 进程与线程(三)——进程/线程间通信
  • ​ssh-keyscan命令--Linux命令应用大词典729个命令解读
  • ​用户画像从0到100的构建思路
  • #调用传感器数据_Flink使用函数之监控传感器温度上升提醒
  • (8)STL算法之替换
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (pojstep1.3.1)1017(构造法模拟)
  • (八十八)VFL语言初步 - 实现布局
  • (力扣)1314.矩阵区域和
  • (十一)c52学习之旅-动态数码管
  • (一)RocketMQ初步认识
  • (正则)提取页面里的img标签
  • (转)Android学习笔记 --- android任务栈和启动模式
  • (转)iOS字体
  • (转)visual stdio 书签功能介绍
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .NET CORE 第一节 创建基本的 asp.net core
  • .NET Standard、.NET Framework 、.NET Core三者的关系与区别?
  • .netcore 如何获取系统中所有session_ASP.NET Core如何解决分布式Session一致性问题
  • .NET和.COM和.CN域名区别
  • .NET性能优化(文摘)
  • .NET中 MVC 工厂模式浅析
  • .net中生成excel后调整宽度