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

news2024/7/7 17:12:51

1. 背景

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

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

2. 能量补给站

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

2.1 addr2line

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

2.2 帧指针和栈指针

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

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

图一

如上图:
栈指针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);

如下图所示:

图二

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

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

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

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

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

        1. 如图二一致。

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

图三

对于情景一:

我们的分析思路类似,只不过若文件已经被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;
}

做如下编译流程:

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

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

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

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

计算过程:

        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

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 编译选项,导致无法查看变量内容

编译流程如下:

环境复现:

如图所示,当通过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进行反汇编。如图所示:

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

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

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

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

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

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

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

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

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

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

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

总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/751481.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端面试题-js(三)

31 介绍js有哪些内置对象 Object 是 JavaScript 中所有对象的⽗对象数据封装类对象&#xff1a; Object 、 Array 、 Boolean 、 Number 和 String其他对象&#xff1a; Function 、 Arguments 、 Math 、 Date 、 RegExp 、 Error 32 说⼏条写JavaScript的基本规范 不要在同…

【算法集训之线性表篇】Day 09

文章目录 题目基本设计思想代码实现效果 题目 定义三元组(a,b,c)(a,b,c均为整数)的距离D|a-b||b-c||c-a|。给定3个非空集合S1&#xff0c;S2&#xff0c;S3&#xff0c;按升序分别存储在三个数组中。请设计一个尽可能高效的算法&#xff0c;计算并输出所有有可能的三元组中的最…

TabView, DarkMode 的使用

1. TabView 选项卡视图的使用 1.1 实现 /// 选项卡视图 struct TabViewBootcamp: View {State var selectedTab: Int 0let icons: [String] ["heart.fill", "globe", "house.fill", "person.fill"]var body: some View {// tabView…

青岛大学_王卓老师【数据结构与算法】Week05_11_栈与递归_学习笔记

本文是个人学习笔记&#xff0c;素材来自青岛大学王卓老师的教学视频。 一方面用于学习记录与分享&#xff0c; 另一方面是想让更多的人看到这么好的《数据结构与算法》的学习视频。 如有侵权&#xff0c;请留言作删文处理。 课程视频链接&#xff1a; 数据结构与算法基础…

考虑设备动作损耗的配电网分布式电压无功优化(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【Nginx】proxy_set_header的变量与X-Forwarded-For伪造客户端IP漏洞

前言 上面突然说&#xff0c;需要检查Nginx反向代理的安全问题并给出了修改方法&#xff0c;小白的我一脸懵逼&#xff0c;明明都是中文&#xff0c;连在一起咋就看不明白了。于是乎&#xff0c;对着修改内容简单学习了一下&#xff0c;在此做个记录&#xff0c;如有问题请大佬…

【Ubuntu】Ubuntu14 安装 open-jdk-1.7(open-jdk-7) 最新 持续更新中

【Ubuntu】Ubuntu14 安装 open-jdk-1.7&#xff08;open-jdk-7&#xff09; 最新 持续更新中 一、概述二、特别说明三、开发环境四、安装 open-jdk-1.7五、配置环境1、配置环境变量路径2、环境变量3、完成安装 一、概述 一个好的文章能够帮助开发者完成更便捷、更快速的开发。…

Hive3.1.2伪分布式安装

Hive3.1.2安装 前言 Hive是何物&#xff0c;自己去百度&#xff0c;在此不多bb&#xff0c;直接开整… 学习一个组件&#xff0c;个人觉得最重要的是先学会安装再说&#xff0c;巧妇难为无米之炊撒… 下载 下载地址&#xff1a;https://downloads.apache.org/hive/hive-3.…

Python探索金融数据进行时间序列分析和预测

大家好&#xff0c;时间序列分析是一种基于历史数据和趋势分析进行预测的统计技术。它在金融和经济领域非常普遍&#xff0c;因为它可以准确预测趋势并做出明智的决策。本文将使用Python来探索经济和金融数据&#xff0c;执行统计分析&#xff0c;并创建时间序列预测。 我们将…

面试题更新之-CSS Hack是什么?ie6,7,8的hack分别是什么?

文章目录 导文CSS Hack的定义广泛应用的CSS Hack技巧ie6,7,8的hack分别是什么&#xff1f; 导文 面试题更新之-CSS Hack是什么?ie6,7,8的hack分别是什么&#xff1f; CSS Hack的定义 CSS Hack指的是在CSS中使用一些特定的代码或技巧&#xff0c;通过利用不同浏览器对CSS实现的…

大学生用一周时间给麦当劳做了个App(安卓版)

背景 有个大学生粉丝最近私信联系我&#xff0c;说基于我之前开源的多语言项目做了个仿麦当劳的项目&#xff0c;虽然只是个样子货&#xff0c;但是收获颇多&#xff0c;希望把自己写的代码开源出来供大家一起学习进度。这个小伙伴确实是非常积极上进&#xff0c;很多大学生&a…

Grid++Report 6.8.6 Crack

GridReport报表开发工具总体介绍 报表开发工具概述 GridReport是一款同时支持C/S软件(桌面程序)与B/S软件(WEB程序)开发的报表工具&#xff0c;功能全面易学易用。C/S开发适用于VB.NET、C#、VB、VC、Delphi、CBuilder、QT与易语言等。B/S开发适用于ASP.NET(C# & VB.NET),…

蒲公英打包环境搭建碰到问题

一&#xff1a;证书那边选择手动&#xff0c;不要自动&#xff0c;——》debug配置dev证书&#xff0c;release配置ad-hoc证书 二&#xff1a;证书有时候不生效&#xff0c;删除重新下载。~/Library/MobileDevice/Provisioning Profiles 三&#xff1a;更新测试手机时&#…

数字化孪生技术在工业上的应用场景和案例分享

编者按&#xff1a;数字化孪生是一个完整的生命周期概念&#xff0c;它从设计和制造开始&#xff0c;贯穿整个生产过程&#xff0c;直到产品退役。数字化孪生将现实世界和虚拟世界相结合&#xff0c;使企业能够更好地理解和优化物理系统的运行情况&#xff0c;从而提高生产效率…

Java多线程下的单例模式参考

Java多线程下的单例模式参考 单例有多种的写法&#xff0c;本例是懒汉式单例的一种写法。在高并发环境下需要注意的是&#xff1a; 1.单例在并发访问并调用其相应的getInstance方法的时候也会造成创建多个实例对象&#xff0c;加锁是必要的。 2.使用synchronized是比较好的解…

基于预测控制模型的自适应巡航控制仿真与机器人实现(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 自适应巡航控制技术为目前由于汽车保有量不断增长而带来的行车安全、驾驶舒适性及交通拥堵等问题提供了一条有效的解决途径&am…

JS、Vue鼠标拖拽

JS代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevic…

Python Web框架 Flask 安装、使用

Python Web框架 Flask 安装 安装 Flask 框架 首先需要安装 Flask 框架, 可以通过以下命令安装: [rootlocalhost web]# pip3 install Flask Collecting FlaskDownloading Flask-2.0.3-py3-none-any.whl (95 kB)|██████████████████████████████…

七大排序算法——归并排序,通俗易懂的思路讲解与图解(完整Java代码)

文章目录 一、排序的概念排序的概念排序的稳定性七大排序算法 二、归并排序核心思想代码实现 三、性能分析四、七大排序算法性能对比 一、排序的概念 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0…

基于Java的考研教室在线预约系统/基于springboot的考研教室在线预约系统

摘 要 网络的广泛应用给生活带来了十分的便利。所以把考研教室在线预约与现在网络相结合&#xff0c;利用java语言建设考研教室在线预约系统&#xff0c;实现考研教室在线预约的信息化。则对于进一步提高考研教室在线预约发展&#xff0c;丰富考研教室在线预约能起到不少的促进…