当Linux程序“跑路”:利用Backtrace快速精准定位“案发现场”(转)

在 Linux 开发的奇妙旅程中,我遭遇过无数次程序异常退出的 “惊险时刻”。那是一种让人抓狂的体验,满心期待程序能顺利运行,结果它却毫无征兆地 “罢工”。有时候,程序在运行一段时间后突然消失,没有任何提示,就像人间蒸发;有时候,伴随着一堆晦涩难懂的错误信息,直接把人搞懵。

我记得有一次开发一个文件处理程序,本以为逻辑已经很完善,测试时却频繁异常退出。没有明确的错误提示,只知道程序莫名其妙就终止了。那一刻,那种无助和迷茫,相信很多开发者都感同身受。为了解决这个问题,我查阅了大量资料,尝试了各种方法,终于在 backtrace 这个强大工具的帮助下,找到了问题的根源。

我深知还有很多开发者在程序异常退出的困境中苦苦挣扎,所以今天就迫不及待地想把 backtrace 定位分析的方法分享给大家,希望能帮助大家在遇到类似问题时,快速找到解决之道 ,少走一些弯路。

一、Linux 程序异常退出的常见原因

在日常的代码调试工作中,开发者们时常会面临各种各样的挑战,其中段错误或是各类异常致使进程意外退出的情况尤为棘手,这种情况在处理大型工程代码时更为凸显。大型工程代码往往规模庞大、结构复杂,涉及众多的模块和函数调用,各个部分之间相互关联又相互影响。当异常发生时,整个代码体系就像一团错综复杂的乱麻,让人一时难以理清头绪。

倘若缺乏一个高效且实用的调试工具,定位这类问题的过程将会变得异常艰难,不仅耗费大量的时间和精力,还可能会让开发者陷入迷茫和困惑之中。可能需要在海量的代码中逐行排查,反复地运行程序,观察各种可能出现问题的细节,这无疑是一项艰巨的任务。在深入探索强大的调试工具 backtrace 之前,我们有必要先来全面且细致地梳理一下那些容易导致 Linux 程序异常退出的常见原因:

  • 内存溢出:当程序在运行过程中申请的内存超出了系统所能提供的可用内存时,就会发生内存溢出。这就好比你只有一个小口袋,却想要装下比口袋大得多的东西,肯定是装不下的。程序申请内存时,如果系统没有足够的内存分配给它,就可能导致程序异常退出。比如,在一个循环中不断地申请内存,却没有释放,最终就会把系统内存耗尽,导致内存溢出。
  • 空指针引用:空指针就像是一个没有指向任何有效内存地址的 “迷路指针”。当程序试图访问一个空指针时,就如同在一个没有目的地的地图上寻找位置,必然会导致程序异常。例如,定义了一个指针变量,但没有给它分配内存空间,就直接去使用它,这就会引发空指针引用错误。
  • 文件读写错误:文件读写操作是程序中常见的操作,但如果文件不存在、没有读写权限,或者文件在读写过程中被其他程序占用等,都可能导致文件读写错误,进而使程序异常退出。比如,你尝试读取一个不存在的文件,程序就会因为找不到文件而报错退出。
  • 系统资源不足:除了内存,系统的其他资源,如文件描述符、进程数、网络连接数等也是有限的。当程序大量占用这些资源,而系统无法再提供更多资源时,程序就会因为资源不足而异常退出。想象一下,一个停车场的停车位是有限的,如果所有车位都被占满了,再有车辆想要进来停车就会被拒绝,程序也是如此。
  • 代码逻辑错误:代码逻辑错误是一种比较隐蔽的问题,它可能不会像前面几种情况那样直接报错,但会导致程序运行结果不符合预期,甚至出现异常退出。比如,在条件判断语句中,条件写错了,导致程序进入了错误的分支,执行了错误的操作。
  • 系统异常:硬件故障、系统崩溃等系统层面的异常也会导致程序异常退出。这就好比电脑突然断电,正在运行的程序自然就无法继续运行了。

二、backtrace 是什么?

2.1定义与作用

backtrace,中文可译为 “回溯”,是一个用于生成函数调用栈的工具,在程序崩溃或者出现异常时,可以通过 backtrace 来获取函数调用栈信息。这些信息就像是程序运行的 “脚印”,记录了程序执行的路径,能帮助我们了解到程序的执行流程,定位问题发生的位置。

在程序运行过程中,函数之间会相互调用,形成一个调用链。当程序出现异常退出时,我们往往不知道问题出在哪里。backtrace 就像是一位 “侦探”,通过分析当前堆栈,回溯上层函数在栈中的帧地址,直至顶层函数,从而为我们提供一份详细的函数调用清单,让我们清楚地看到程序在崩溃前执行了哪些函数,以及这些函数是如何调用的。这样,我们就能根据这份清单,逐步排查,找到问题的根源。

2.2工作原理

要理解 backtrace 的工作原理,我们得先了解一下函数调用栈。函数调用栈是一个记录程序中函数调用关系的数据结构,它在程序运行时动态生成和维护。当程序执行函数调用时,它将当前函数的返回地址和一些其他信息(如函数参数、局部变量等)压入堆栈中,并跳转到被调用的函数执行。当被调用函数执行完毕后,它将返回地址弹出堆栈,并跳回到调用函数继续执行。

backtrace 的工作原理就是利用函数调用栈中的这些信息来追踪程序执行的路径和调用关系。当程序出现错误或崩溃时,backtrace 通过分析函数调用栈信息,从当前函数开始,逐步回溯到调用它的函数,再到调用该函数的函数,以此类推,直到找到顶层函数。在这个过程中,backtrace 会获取每个函数的返回地址、函数名、参数等信息,并将这些信息整理成一份调用栈报告。

例如,假设我们有一个程序,包含函数 A、函数 B 和函数 C,函数 A 调用函数 B,函数 B 调用函数 C。当程序在函数 C 中出现异常退出时,backtrace 就会从函数 C 开始,找到函数 C 的返回地址,这个返回地址指向函数 B 中调用函数 C 的位置。然后,backtrace 根据这个返回地址,找到函数 B 的堆栈信息,获取函数 B 的相关信息,并继续回溯到函数 A。最终,backtrace 会生成一份包含函数 A、函数 B 和函数 C 的调用栈报告,告诉我们程序在崩溃前是如何调用这些函数的。

三、backtrace 相关函数及使用

3.1获取程序堆栈的 API 介绍

在 Linux 系统中,有几个关键的函数可以帮助我们获取和处理程序的堆栈信息,下面就来详细介绍一下。

int backtrace(void *buffer, int size)

这个函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针数组。buffer指向的数组中的每一项都是void*类型,存放的是来自相应堆栈帧的返回地址。参数size用来指定buffer中可以保存多少个void*元素。函数的返回值是实际获取的指针个数,最大不超过size大小。

例如,我们定义一个大小为 100 的buffer数组来存储堆栈信息:

#define BT_BUF_SIZE 100
void *buffer[BT_BUF_SIZE];
int nptrs = backtrace(buffer, BT_BUF_SIZE);

这里,nptrs就是实际获取到的指针个数。如果堆栈大小大于size指定的值,那么只能获取到最近调用该函数的部分堆栈帧地址;为了获得完整的堆栈,请确保buffer和size设置得足够大 。

char *backtrace_symbols(void const *buffer, int size)

该函数将backtrace函数获取的信息转化为一个字符串数组。参数buffer是backtrace获取的堆栈指针,size是backtrace返回值,即实际获取的指针个数。函数返回值是一个指向字符串数组的指针,它包含char*元素个数为size。每个字符串包含了一个相对于buffer中对应元素的可打印信息,包括函数名(如果可以确定的话)、十六进制的函数偏移量和十六进制的实际返回地址。

比如,我们在获取到backtrace信息后,使用backtrace_symbols将其转换为字符串数组并打印:

char **strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
    perror("backtrace_symbols");
    exit(EXIT_FAILURE);
}
for (int j = 0; j < nptrs; j++) {
    printf("%s\n", strings[j]);
}
free(strings);

需要注意的是,backtrace_symbols生成的字符串占用的内存是malloc出来的,使用完后必须使用free函数释放这个指针,否则会造成内存泄漏 。

void backtrace_symbols_fd(void const *buffer, int size, int fd)

此函数与backtrace_symbols函数功能相同,只是它不会malloc内存,而是将结果写入文件描述符为fd的文件中,每个函数对应一行。例如,我们可以将堆栈信息写入标准输出(STDOUT_FILENO):

backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO);

该函数可重入,适用于有可能调用该函数会失败的情况,比如在内存紧张的情况下,backtrace_symbols可能因为无法分配内存而失败,而backtrace_symbols_fd则不会有这个问题,因为它不需要分配额外的内存 。

3.2使用 backtrace 的注意事项

在使用backtrace进行程序调试时,有一些细节需要我们特别注意,否则可能无法得到准确的堆栈信息。

(1)编译选项

在使用gcc编译链接程序时,不加非零优化等级(-On参数)和栈指针优化参数-fomit-frame-pointer。这是因为backtrace的实现依赖于栈指针(fp寄存器),任何非零的优化等级或加入了栈指针优化参数后,都可能导致栈指针被优化掉,从而不能正确得到程序栈信息。例如,下面的编译命令就会导致backtrace无法正常工作:

gcc -O2 -fomit-frame-pointer -o myprogram myprogram.c

同时,为了让backtrace_symbols能够正确解析出函数名,需要加-rdynamic参数。-rdynamic可用来通知链接器将所有符号添加到动态符号表中,这样在获取堆栈信息时才能显示函数名。正确的编译命令应该是:

gcc -rdynamic -o myprogram myprogram.c

(2)特殊情况

1.静态函数名不输出:如果函数被声明为static,即静态函数,那么在打印堆栈时不会显示该函数名。这是因为static函数的作用域仅限于当前文件,它的符号不会被导出,所以无法在堆栈信息中显示。例如:

static void my_static_function() {
    // 函数体
}

在这种情况下,my_static_function在堆栈信息中可能只会显示为一个地址,而没有函数名。

2. 内联函数无堆栈框架:内联函数在编译过程中会被展开在调用的位置,它没有自己独立的堆栈框架。这就导致backtrace无法获取到内联函数的堆栈信息。例如:

inline void my_inline_function() {
    // 函数体
}

当my_inline_function被调用时,backtrace无法追踪到这个函数,因为它在运行时并没有真正的函数调用过程。

3. 尾调用优化影响栈信息获取:尾调用优化(Tail-call Optimization)会复用当前函数栈,而不再生成新的函数栈。这将导致栈信息不能正确被获取,因为堆栈的结构被改变了。例如:

void function_a() {
    // 一些操作
    function_b(); // 尾调用
}
void function_b() {
    // 函数体
}

如果编译器对function_a中的function_b调用进行了尾调用优化,那么在function_b执行时,堆栈信息可能会丢失function_a的相关信息,从而影响我们对程序执行流程的分析 。

四、捕获系统异常信号获取程序调用栈

4.1系统异常信号与程序退出的关系

在 Linux 系统中,程序异常退出往往是由于内核发出了某个异常信号,这些信号就像是系统给程序发送的 “警报”,告诉程序出现了一些异常情况,导致进程终止。

比如,当程序进行内存非法访问时,内核会发出 SIGSEGV 信号,就像你试图进入一个没有权限进入的房间,系统会马上发出警报。而当程序中出现错误的运算符,比如除以零这种情况时,内核将发出 SIGFPE 信号,这就好比你在做数学运算时,使用了错误的计算方法,系统会及时提醒你。不同的异常情况会对应不同的信号,这些信号就是我们追踪程序异常的重要线索 。

4.2利用 signal 机制捕获异常信号

为了在程序出现异常时获取调用栈信息,我们可以利用 Linux 的 signal 机制来捕获这些异常信号。signal 机制就像是一个信号接收器,它可以接收系统发送的各种信号,并对这些信号做出相应的处理。

在 C 语言中,我们可以使用signal函数来注册信号处理函数,当指定的信号到达时,就会跳转到我们指定的处理函数执行。下面是一个简单的示例代码,展示了如何捕获 SIGSEGV 信号并在处理函数中调用backtrace获取调用栈信息:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <execinfo.h>

// 信号处理函数
void signal_handle(int signal) {
    void *buffer[100];
    char **strings;
    int nptrs;

    printf("\n==========> catch signal %d <==========\n", signal);
    printf("Dump stack start...\n");

    // 获取调用栈信息
    nptrs = backtrace(buffer, 100);
    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL) {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }

    // 打印调用栈信息
    for (int i = 0; i < nptrs; i++) {
        printf("%s\n", strings[i]);
    }

    printf("Dump stack end...\n");
    free(strings);
    exit(EXIT_FAILURE);
}

int main() {
    // 注册SIGSEGV信号的处理函数
    signal(SIGSEGV, signal_handle);

    // 模拟内存非法访问,触发SIGSEGV信号
    int *ptr = NULL;
    *ptr = 10;

    return 0;
}

在这个示例中,我们首先定义了一个signal_handle函数,它作为信号处理函数,在接收到信号时,会获取并打印调用栈信息。然后在main函数中,我们使用signal函数将SIGSEGV信号与signal_handle函数关联起来。当程序执行到*ptr = 10;这一行时,由于ptr是一个空指针,会触发SIGSEGV信号,此时系统就会调用我们注册的signal_handle函数,从而获取到程序崩溃时的调用栈信息 。通过这些信息,我们就可以一步步追溯程序的执行路径,找到问题所在。

五、backtrace 分析定位问题实例

5.1测试代码展示

为了更直观地展示 backtrace 在定位程序异常问题中的作用,我们来看一个具体的测试代码示例。这个示例包含了两种常见的异常操作:空指针引用和除零操作,同时还展示了如何使用信号处理和 backtrace 调用。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <execinfo.h>

// 信号处理函数
void signal_handle(int signal) {
    void *buffer[100];
    char **strings;
    int nptrs;

    printf("\n==========> catch signal %d <==========\n", signal);
    printf("Dump stack start...\n");

    // 获取调用栈信息
    nptrs = backtrace(buffer, 100);
    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL) {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }

    // 打印调用栈信息
    for (int i = 0; i < nptrs; i++) {
        printf("%s\n", strings[i]);
    }

    printf("Dump stack end...\n");
    free(strings);
    exit(EXIT_FAILURE);
}

// 模拟空指针引用的函数
void null_pointer_dereference() {
    int *ptr = NULL;
    *ptr = 10; // 空指针引用,会触发SIGSEGV信号
}

// 模拟除零操作的函数
void division_by_zero() {
    int a = 10;
    int b = 0;
    int result = a / b; // 除零操作,会触发SIGFPE信号
}

int main() {
    // 注册SIGSEGV信号的处理函数
    signal(SIGSEGV, signal_handle);
    // 注册SIGFPE信号的处理函数
    signal(SIGFPE, signal_handle);

    // 测试空指针引用
    null_pointer_dereference();

    // 测试除零操作
    // division_by_zero();

    return 0;
}

在这段代码中,signal_handle函数是信号处理函数,当接收到SIGSEGV(内存非法访问信号)或SIGFPE(错误的算术运算信号,如除零)时,它会被调用。在signal_handle函数中,我们使用backtrace函数获取调用栈信息,并使用backtrace_symbols函数将这些信息转换为字符串形式以便打印。

null_pointer_dereference函数模拟了空指针引用的情况,将一个空指针赋值为 10,这会导致程序触发SIGSEGV信号。division_by_zero函数模拟了除零操作,将 10 除以 0,这会触发SIGFPE信号 。在main函数中,我们注册了这两个信号的处理函数,并分别调用了null_pointer_dereference和division_by_zero函数来测试异常情况。

5.2错误信息分析与定位

当我们运行上述测试程序时,由于null_pointer_dereference函数中的空指针引用,程序会触发SIGSEGV信号,进而调用signal_handle函数打印出调用栈信息。假设我们的编译命令是gcc -g -rdynamic -o test test.c,运行结果可能如下:

==========> catch signal 11 <==========
Dump stack start...
./test(signal_handle+0x55) [0x400975]
/lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7f0ff12af4b0]
./test(null_pointer_dereference+0x10) [0x400a40]
./test(main+0x2f) [0x400a9f]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f0ff129a830]
./test(_start+0x29) [0x400929]
Dump stack end...

从输出的调用栈信息中,我们可以看到,signal_handle函数是被调用的第一个函数,因为它是信号处理函数。然后,我们可以看到null_pointer_dereference函数,这表明问题出在这个函数中。

接下来,我们借助addr2line工具来进一步定位到具体的代码行。addr2line是一个用于将程序中的地址转换为源代码文件中的行号的工具。我们可以使用以下命令:

addr2line -e test 0x400a40 -f

这里,-e选项指定可执行文件的名称,后面跟着null_pointer_dereference函数的地址(从 backtrace 输出中获取),-f选项表示同时输出函数名。运行上述命令后,输出结果可能如下:

null_pointer_dereference
/path/to/test.c:19

这就明确告诉我们,问题出在test.c文件的第 19 行,也就是*ptr = 10;这一行,因为这里进行了空指针引用。通过这样的方式,我们就利用 backtrace 和 addr2line 成功定位到了程序异常的具体位置 。如果我们测试division_by_zero函数,也可以按照同样的方法,根据SIGFPE信号触发时的 backtrace 输出和 addr2line 工具来定位到除零操作的代码行。

5.3动态库里的错误信息分析定位

【1】先编译动态库,编译动态库时“-g -rdynamic”记得带上。

qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic backtrace.cpp -fPIC -shared -o libbacktrace.so
qiuhui@ubuntu:~/work/share/backtrace$ ls -l libbacktrace.so 
-rwxrwxr-x 1 qiuhui qiuhui 11424 Jan 12 22:07 libbacktrace.so
qiuhui@ubuntu:~/work/share/backtrace$ 

【2】编译执行程序,“-Wl,-rpath=.”参数表示程序运行时优先去rpath指定路径寻找库文件,可加多个包含路径(-Wl,-rpath,/home/:/lib/:.),程序运行时的寻找顺序为添加的顺序。

qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic main.cpp -I ./ -L ./ -lbacktrace -Wl,-rpath=.

【3】运行程序

qiuhui@ubuntu:~/work/share/backtrace$ ./a.out SIGFPE
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./libbacktrace.so(_Z13signal_handlei+0x59) [0x7fe1b2cfe9a9]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7fe1b29694b0]
[02]./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7fe1b2cfeada]
[03] ./a.out(main+0xc1) [0x400a47]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fe1b2954830]
[05] ./a.out(_start+0x29) [0x4008b9]
Dump stack end…
qiuhui@ubuntu:~/work/share/backtrace$

由以上输出信息可得到实际结束地址是[0x7fe1b2cfeada],用addr2line命令来分析可以看到并不能定位到哪一行。

qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0x7fe1b2cfeada
??:0

因为动态库是动态链接,每次链接的内存都是不一样,所以刚刚得到的结束地址[0x7fe1b2cfeada]实际是不准确的,它是一个内存地址,并不是库文件里面的偏移地址,如果将刚刚的地址减去libbacktrace.so库加载的开始地址,其实就可以定位正确了。在main函数中加入输出进程的maps文件的信息,如下:

char l_s8aBuff[128] = {0x00};
snprintf(l_s8aBuff, sizeof(l_s8aBuff), "cat /proc/%d/maps", getpid());
system(l_s8aBuff);

再看下输出信息,输出信息太多,只把相关的贴上来:

7f9eddc84000-7f9eddc85000 r-xp 00000000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9eddc85000-7f9edde84000 —p 00001000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9edde84000-7f9edde85000 r–p 00000000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9edde85000-7f9edde86000 rw-p 00001000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./libbacktrace.so(_Z13signal_handlei+0x59) [0x7f9eddc849a9]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7f9edd8ef4b0]
[02] ./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7f9eddc84ada]
[03] ./a.out(main+0xd4) [0x400a9a]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f9edd8da830]
[05] ./a.out(_start+0x29) [0x4008f9]
Dump stack end…

咱把0x7f9eddc84ada减去0x7f9eddc84000得到0xada再分析就可以了

qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0xada
/home/qiuhui/work/share/backtrace/backtrace.cpp:47

【4】通过反汇编代码段分析函数入口地址来定位行号,由上面信息可知实际返回地址是在函数([02] ./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7f9eddc84ada]) 偏移量0x16处。执行反汇编指令将其信息保存到log文件

qiuhui@ubuntu:~/work/share/backtrace$ objdump -d libbacktrace.so > log

在保存的log文件里搜索函数名_Z18Test_SIGFPE_signali就可以得到函数的入口地址(0xac4 ),信息如下:

0000000000000ac4 <_Z18Test_SIGFPE_signali>:
ac4: 55 push %rbp
ac5: 48 89 e5 mov %rsp,%rbp
ac8: 48 83 ec 20 sub $0x20,%rsp
acc: 89 7d ec mov %edi,-0x14(%rbp)
acf: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
ad6: 8b 45 ec mov -0x14(%rbp),%eax
ad9: 99 cltd
ada: f7 7d fc idivl -0x4(%rbp)
add: 89 45 ec mov %eax,-0x14(%rbp)
ae0: 8b 45 ec mov -0x14(%rbp),%eax
ae3: 89 c6 mov %eax,%esi
ae5: 48 8d 3d 8e 00 00 00 lea 0x8e(%rip),%rdi # b7a <_fini+0x7e>
aec: b8 00 00 00 00 mov $0x0,%eax
af1: e8 0a fd ff ff callq 800 printf@plt
af6: 8b 45 ec mov -0x14(%rbp),%eax
af9: c9 leaveq
afa: c3 retq
Disassembly of section .fini:

将(函数入口地址)0xac4 +(偏移量)0x16 = 0xada,再分析此地址就可以和前面得出的行号一致了。

qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0xada
/home/qiuhui/work/share/backtrace/backtrace.cpp:47

【5】用g++编译生成map文件搜索函数名Test_SIGFPE_signal也可以获取函数入口地址,计算方式和上面提到的反汇编代码段一样,具体操作如下:

qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic backtrace.cpp -fPIC -shared -o libbacktrace.so -Wl,-Map,map.log

打开map.log得到如下信息:

.text 0x0000000000000950 0x1ab /tmp/ccjH9RPy.o
0x0000000000000950 signal_handle(int)
0x0000000000000a74 Test_SIGSEGV_signal(char*)
0x0000000000000ac4 Test_SIGFPE_signal(int)

六、全文总结

回顾整个利用 backtrace 定位分析 Linux 程序异常退出的过程,它就像是一场有条不紊的解谜之旅。

当程序毫无征兆地异常退出时,我们首先要明确这可能是由多种原因导致的,如内存溢出、空指针引用、文件读写错误、系统资源不足、代码逻辑错误或系统异常等。这就需要我们像侦探一样,不放过任何一个可能的线索 。

接着,我们要借助 backtrace 这个强大的工具。在使用 backtrace 之前,务必注意编译选项,要添加-g和-rdynamic参数,避免使用可能影响栈指针的优化选项,如-On和-fomit-frame-pointer。这样才能确保在程序出现异常时,backtrace 能够准确地获取到程序的调用栈信息 。

然后,利用signal机制捕获异常信号,如SIGSEGV(内存非法访问信号)、SIGFPE(错误的算术运算信号)等。在信号处理函数中,调用backtrace函数获取调用栈信息,再使用backtrace_symbols或backtrace_symbols_fd函数将这些信息转换为我们易于理解的字符串形式,打印出来供我们分析 。

最后,根据打印出的调用栈信息,我们可以初步判断问题所在的函数。再结合addr2line工具,将函数地址转换为具体的代码行,从而精准定位到问题的根源 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值