Linux 调试进阶(多场景覆盖)

这篇具有很好参考价值的文章主要介绍了Linux 调试进阶(多场景覆盖)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

1. 背景

        最近在小米项目中分析较多crash问题,结合之前在《程序员的自我修养——链接,状态与库》书中学到的一点皮毛。感触颇深,总结了一些经验,相信对大家分析相关问题有一定的帮助。因为本文的知识点偏于底层,大部分思路都是基于《程序员的自我修养》,对于没有相关知识储备的朋友,可能会有一定难度。不过还是建议过一遍,图一乐呵,万一哪天遇到,能够为您提供解决思路,我也会觉得很欣慰😁。

本文核心目的是结合常见案例场景进行分析,提供解决思路。因此不再介绍基础,比如gdb 调试方式,coredump 的生成,backtrace 的等。

2. 能量补给站

        案例分析前,需要介绍两个知识点,后续的很多思路,都是依靠这两点。

2.1 addr2line

        addr2line命令来自英文词组“address to line”的缩写,其功能是将函数地址解析成文件名或行号的工具。给出一个可执行文件中的地址或一个可重定位对象中的偏移部分的地址,使用调试信息来找出与之相关的文件名和行号。【这个地址指的是可执行文件或动态库的代码段地址

2.2 帧指针和栈指针

        我们知道每一个线程都有自己独立的线程栈,而函数调用操作都是基于栈实现的。那么栈的内容包括哪些呢?比如:函数的返回地址,函数入参,局部变量,寄存器值等等。【而这些信息,对于我们分析问题有极大的帮助】。

        困难在于如何分析出有用信息,那就需要先了解栈帧结构,可参考下图:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

图一

如上图:
栈指针esp: 一直指向线程栈的栈顶。感觉作用不大。
帧指针ebp:函数执行过程中对大部分数据的访问都基于帧指针ebp进行。并且通过ebp 指针的上下文,我们可以清楚的知道函数调用关系。【ebp 保存调用者的 帧指针, ebp+4 保存调用者的返回地址】这句话,会在案例中演示分析。

3. 案例分析

3.1 基于backtrace 的日志分析

3.1.1 设计与实施

        在一些程序中,会采用linux 提供的backtrace 和 backtrace_symbols 接口,将对应的堆栈信息打印。大致思路:

        1. 在程序初始化时,对一些异常信号的注册,比如SIGSEGV, SIGABRT,SIGPIPE等

        2. 当线程触发信号时,会在信号处理函数中,通过backtrace 和 backtrace_symbols 将当前线程的堆栈信息进行打印。

        3. 退出当前进程,exit(-1);

如下图所示:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

图二

之后通过addr2line 命令即可定位到指定的源码文件以及代码行数。

3.1.2 符号被strip或没有加-g 编译选项

        上述的分析方法比较依赖理想的环境,如果存在以下情况,则会不太适用。

可执行文件/动态库被strip  编译过程没有增加-g 选项

        我们知道linux 设备中磁盘资源有限,宿主机往往会对集成方 SDK的大小做要求。因此,大部分场景下,在量产发行版本中,会对可执行文件以及动态库进行strip, 达到减少SDK大小的目的。若进行了strip,那么backstrace 得到的信息有两种情况:

        1. 如图二一致。

        2. 没有符号+偏移地址【可能因为glibc版本不同】,仅有程序的虚拟地址。即如下:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

图三

对于情景一:

我们的分析思路类似,只不过若文件已经被strip 那么你是无法通过nm 查看符号地址+偏移 得到代码段地址。也无法通过addr2line 从文件中得到代码行数。解决方式:

        1. 确定文件的版本号,比如通过 git commit Id。

        2. 重现编译程序或动态库。增加 -g 编译选项 且不 strip

        3. 再通过nm查看符号+偏移,得到代码段地址。

        4. addr2line 得到代码行数

上述的核心思想:同样的代码,通过增加编译选项-g,是不会改变代码段内容的。【-O优化选项会改变

因此,大部分SDK 提供方,编译过程中,会生成两份SDK,一份是不加-g编译选项且strip 过的,一份是增加-g 选项,不strip的。

对于情景二:

从日志中我们仅能得到虚拟地址的调用栈,而虚拟地址在可执行程序每一次运行时,可能是不一样的。【可执行程序的代码段对应的虚拟地址应该不会变化。但是动态库的代码段会随着加载地址的不同,发生变化

因此,我们要做的就是,在这一次的crash中,通过虚拟地址计算出对应的代码段偏移,可以从当前进程的maps中计算。我写了一个简单的示例。main.c, func1.c , func2.c , func3.c。

main.c
#include<stdlib.h>
#include<stdio.h>
extern void func1(int i);
int main()
{
        printf("enter %s\n",__func__);
        func1(1);
        return 0;
}

 

func1.c
#include<stdlib.h>
#include<stdio.h>
extern void func2(int i);
void func1(int i)
{
        printf("enter func1 : i = %d\n",i);
        func2(2);
        return;
}

func2.c
#include<stdlib.h>
#include<stdio.h>
extern void func3(int i);
void func2(int i)
{
        printf("enter func2 : i = %d\n",i);
        func3(3);
        return;
}

func3.c
#include<stdlib.h>
#include<stdio.h>
void func3(int i)
{
        printf("enter func3:i = %d\n",i);
        char * p = NULL;
        p = 0x1234;
        *p = "123";
        return;
}

做如下编译流程:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

即模拟调用栈main->func1->func2->func3, 且func3 产生crash。分析流程如下:

        1. 通过gdb 调试,当出现crash 出现后,我们关注调用栈的虚拟地址。(忽略其它提示信息)

        2. 查看程序的进程号,查看maps

        3. 通过虚拟地址计算出代码段偏移地址

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

计算过程:

        1. 从调用栈中,我们知道虚拟地址的调用顺序分别为:0x0000555555554790 > 0x00007ffff7bd1685 > 0x00007ffff79cf685 > 0x00007ffff77cd648

        2. 从maps 中可以看到,这些虚拟地址分别映射在main,libfunc1.so,libfunc2.so ,libfunc3.so的代码段中。因此通过计算,既可得到对应的代码段的偏移。如下:

0x0000555555554790 - 0x555555554000 = 0x790
0x00007ffff7bd1685 - 0x7ffff7bd1000 = 0x685
0x00007ffff79cf685 - 0x7ffff79cf000 = 0x685
0x00007ffff77cd648 - 0x7ffff77cd000 = 0x648

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

3.1.3 总结

因此,为了兼容更多的场景,backtrace一般会和maps 一起使用。所以信号处理函数的实现如下:

static void backtrace_signal_handler(int32_t signal)
{
    char buf[10 * 1024] = {0};
    uint64_t buflen = sizeof(buf) / sizeof(buf[0]);
    int32_t len = 0;
    void* array[128] = {0};
   
    /**
获取调用堆栈*/
    int32_t size = backtrace(array, 128);
    /** 将堆栈信息进行符号转换,可能会因为某些场景,导致转换失败*/
    char** strings = (char**)backtrace_symbols(array, size);
    if (NULL != strings) {
        for (int32_t i = 0; i < size; i++) {
            if (NULL != strings[i]) {
                len += sprintf(buf + len, "%s\n", strings[i]);
            }
        }
        log_w("backtrace:%s", buf);
        free(strings);
    }
   
    /** 将maps打印*/
    char file[64] = {0};
    (void)snprintf(file, sizeof(file) / sizeof(file[0]), "/proc/%d/maps", getpid());
    FILE* fp = fopen(file, "r");
    if (fp != NULL) {
        uint64_t rlen = fread(buf, 1, buflen - 1u, fp);
        if (rlen > 0u) {
            buf[rlen] = '\0';
            log_w("%s\n", buf);
        }
        (void)fclose(fp);
    }
}

3.2 基于Coredump的日志分析

Coredump叫做核心转储,它是进程运行时在突然崩溃的那一刻的一个内存快照。操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里。【理论上基于coredump ,你可以得到进程中任何一个变量的值,以及预测程序的运行状态

正常情况下,通过Coredump 可以定位到大部分问题。但是有些情况下,也会遇到头疼的问题。常见的如下:

        1. 变量被优化或没有-g 编译选项,导致无法查看变量内容

        2. 栈被破坏,无法查看调用栈

3.2.1 变量被优化或没有-g 编译选项,导致无法查看变量内容

编译流程如下:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

环境复现:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

如图所示,当通过gdb 查看到 crash 点在 func3 内部中时,我们若想知道传入参数i的值,通过 gdb 是无法进行打印的。(忽略程序中的printf打印),因为libfunc3.so 编译时,没有增加-g 选项。

若这个异常很难复现,我们该如何进一步分析呢?

分析:

        1. 通过汇编语言进行解析,分析入参的传递过程

        2. 确定入参的地址

        3. 进行打印

通过objdump -d libfunc3.so > symbol3 ,objdump -d libfunc2.so > symbol2对libfunc3.so 和libfunc2.so进行反汇编。如图所示:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

由此可知,func2调用func3时,先将入参保存至 %edi 寄存器中,而func3 内部将 edi 寄存器的值保存至 (%rbp-0x14)的地址。因此,我们仅需要查看该内存地址的值即可。

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

由此可知当前的值传入的值为3。

3.2.2 栈被破坏,无法查看调用栈

        这种情况比较特殊,因为堆栈被破坏了。导致无法直接查看有用的信息。如图:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

        这种情况一般是因为你的线程栈被越界访问,导致函数的返回地址被破坏。堆栈信息打印不出来。可参考这篇博客,较为详细的描述了堆栈被破坏的原理。https://blog.csdn.net/minghe_uestc/article/details/7977997

        遇到该类问题,我一般会将栈顶指针$sp尽可能多的栈空间内容打印。从内容中得到关键内容。【为什么打印$sp指针,而不是$bp指针?,因为栈的顺序是从高地址向低地址增加,而$sp指针作为栈顶指针,肯定是线程栈内的最低地址,而程序访问,赋值的操作是向上增加,因此$sp指针大部分情况不会被破坏,$bp指针,可能会被破坏

        1. 从栈空间中,过滤去特定的数据,从而缩小排查范围。如上面的博客。

        2. 从栈空间中,过滤出可能的$bp 指针。从而缩小排查范围。如下图:

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

如图所示:
红色框内是栈帧的bp

黄色框内很明显是错误的old bp 以及错误的函数返回地址。通过打印,可以知道是被字符串覆盖了

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

绿色框内,则是函数的返回地址。之后通过maps进行计算,则可以定位到接口。

总结

        crash 问题是我们linux 开发无法避免的一个难题。很多时候,异常出现后,因为无法获取更多的分析信息。我们需要花费大量的人力去测试复现。

        因此在第一现场,利用仅有的资源分析出更多的有用信息,就显得十分重要。希望本篇能够给相关同事,提供分析思路。

 若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途

Linux 调试进阶(多场景覆盖),linux,coredump,gdb

Linux 调试进阶(多场景覆盖),linux,coredump,gdb文章来源地址https://www.toymoban.com/news/detail-568218.html

到了这里,关于Linux 调试进阶(多场景覆盖)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux--调试器:gdb

    gcc与g++默认动态链接形成的可执行程序(比如a.out)是 release 版本,不可调试!!! 如何搞成debug可调试版本? 查看可执行程序存储了哪些内存空间:   调试语法:

    2024年02月13日
    浏览(50)
  • Linux——gdb调试器

    目录    前言: 二.gdb定义及指令: 如何查看该exe文件是否为Debug版本?两种方法: 三.gdb调试:         调试指令1:l指令(小写L) run指令:运行程序,相当于VS中的直接运行不调试——可简化输入r  break指令:设置断点——指令可简化输入为b ​编辑  info b指令:查看整个代码

    2024年02月07日
    浏览(36)
  • Linux - 还不懂 gdb 调试器?(调试软件)

    当前,我们可以使用 make/makefile 来程序化执行代码文件;可以使用 gcc/g++ 等编译器来编译代码;可以使用 vim 编辑器来编写代码;其实在 Linux 当中还有一个工具,可以实现调试工作,这个工具就是 -- gdb。 在了解调试器之前,你应该对代码的发布版本做一些了解: 我们在 VS

    2024年02月07日
    浏览(52)
  • 【Linux】Linux下调试器gdb的使用

    👑作者主页:@安 度 因 🏠学习社区:StackFrame 📖专栏链接:Linux 如果无聊的话,就来逛逛 我的博客栈

    2024年02月19日
    浏览(40)
  • Linux调试器之gdb

    我们前面介绍了几个基本的环境开发工具。例如通过yum我们可以安装和卸载软件、通过vim我们可以写代码、通过gcc和g++我们可以编译代码成可执行程序。但是如何在Linux下调试代码呢?我们并未介绍,本期我们将来介绍最后一个工具 --- 调试器gdb。 程序的发布方式 gdb基本的调

    2024年04月16日
    浏览(66)
  • Linux系统gdb调试常用命令

    GDB(GNU调试器)是一款常用的调试工具,用于调试C、C++等编程语言的程序。以下是一些常用的GDB命令: 1. 启动程序:    - `gdb executable`:启动GDB调试器,并加载可执行文件。 2. 设置断点:    - `break line`:在指定行设置断点。    - `break function`:在指定函数的入口处设置断

    2024年02月10日
    浏览(38)
  • 【linux】“gdb“调试方法与技巧

    🎉博客主页:小智_x0___0x_ 🎉欢迎关注:👍点赞🙌收藏✍️留言 🎉系列专栏:Linux入门到精通 🎉代码仓库:小智的代码仓库 gdb是一款开源的、功能强大的调试器,它可以用于调试多种编程语言,如C、C++、汇编语言等。gdb可以帮助开发者在程序运行时进行调试,并找到程序

    2024年02月16日
    浏览(34)
  • 【Linux】gdb调试器的使用

    文章目录 一、gdb简介 二、调试前的准备 1、生成调试文件 2、启动 gdb  三、gdb 使用方法  1、查看源代码 2、设置 / 查看断点(多种方式设置断点) 方法一 方法二 方法三 3、run  4、删除断点、断点无效 5、逐过程调试(以函数为单位) 6、逐语句调试 7、查看调用链 8、查看变

    2024年02月02日
    浏览(51)
  • 在linux中的GDB调试一些用法

    -g: 选项的作用是在可执行文件中加入源代码的信息 -Wall:尽量不影响程序行为的情况下选项打开所有warning program:为程序名 help:使用帮助 gdb:启动程序 quit:退出调试 set args num1 num2 …:设置程序中需要的参数(就是cin需要接收的值) show args:展示参数 list或者l :从默认位

    2024年02月05日
    浏览(47)
  • Linux 使用gdb调试C程序

    一、gdb的一些基础命令 l :显示代码 l n :跳转到当前代码页的第n行的代码 l filename.c :n :跳转到filename.c文件的第n行代码 b 行号 :加断点 info break :查看断点信息 delete 断点编号 :删除断点 r ,运行程序 n ,单步执行 c ,继续执行,遇到断点停止执行 p ,打印 s ,进入函数

    2024年02月13日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包