Linux GDB如何调试无堆栈信息(全是??)的情况1
4194
2022.04.28
2022.05.03
发布于 未知归属地

目录:

0. 前导准备

  1. 情况说明
  2. 调试
  3. 调试信息处理
  4. 最后

知道什么 coredump 文件 和 怎么调试 coredump 的朋友,可以直接把 前导准备 跳过。


0 前导准备

0.1 coredump知识

coredump叫做核心转储,它是进程运行时在突然崩溃的那一刻的一个内存快照。操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里。

coredump文件主要用程序调试,定位崩溃程序在代码出错的位置;作为源代码修改BUG的依据。

0.2 一般产生coredump常见原因

有如下几个:

  1. 内存访问越界。如: strcpy / strcat 等等函数在使用时没有考虑 '\0' 结束的问题
  2. 多线程之间使用了不安全的访问方式。造成脏数据,程序处理逻辑异常
  3. 非法指针。常见野指针操作(这也是为什么很多编程的技巧里面都提到,内存释放后,要把指针 “置空”)
  4. 堆栈溢出。如在使用递归时,没有退出条件,或退出条件太深,导致栈撑爆,造成栈破坏

明白 coredump 做是什么后,我们再看看怎么在 Linux 配置生成 coredump 文件。

0.3 coredump配置

Linux 系统全局的 coredump 格式配置文件在:

文件路径
/proc/sys/kernel/core_pattern

使用如下命令来配置:

更新配置
echo "/data/coredump/core.%e.%p.%t" > /proc/sys/kernel/core_pattern

这条命令的意思,是告诉系统,以后产生的 coredump 文件都放在 /data/coredump/ 目录下,并以 core.%e.%p.%t 格式产生。其实中:

%e 表示程序的文件名
%p 表示进程的ID
%t 显示创建的时间
(想使用更多的参数,可自行查询使用)

还有↓

(Linux系统默认配置大小配置是无法产生 coredump文件的)
接下来就要指定 coredump 文件的大小。

使用的shell命令
// 查看当前设置的 coredump 文件生成大小
ulimit -c

// 结果默认情况是0。

// 这里 unlimited 也可以换成其它的数字
ulimit -c unlimited

// 注:这种直接使用 ulimit 做的修改,只对当前 shell 有效。
// 如果想修改系统默认 coredump 大小就是 umlimited ,需要做进一步设置。

1 情况说明

如图:
image.png

在使用 GDB 调试 程序 + coredump文件,会有这种跟代码相关的 堆栈信息 是全是 ?? 这样的情况。

就我目前所知道的出现这种现象有2种原因:

  1. 程序本身无任何调试信息。也就是我们发布的 release版本程序;调试信息会在发布时被全部剥离/删除
  2. 程序运行时,因为BUG,把程序运行时的 调用栈帧 给破坏掉了。

注:情况2 这种指是程序运行时,因为函数嵌套调试而产生的调用栈帧,而这个栈帧被破坏掉。如图:
image.png

受限于篇幅和时间的原因,本文只针对 情况1 做讨论和处理。

// 说真的,情况2其实是最难分析和说明的,需要对程序在系统程序上的运行机制非常了解;
// 还需要一些运气来说尝试/猜测没被破坏的上一个 “栈帧” 位置;
// 特别是那种野指针写飞了的那种,一般我宁愿 printf 来调试 -_-!
// 不过好在这种情况基本很少很少很少出现

Q:做了什么操作会导致程序在崩溃时会出现情况1?
A:程序是运行前,使用 strip 命令把程序的所有调试信息给 剥离/删除 掉,就单纯的只有可运行二进制程序。

注:只是剥离/删除 调试信息,和符号信息,并不会影响到程序本身的任何代码逻辑!!!

如图:
image.png

步骤:

  1. 随便编译一个带 -g 的程序。并使用 cp 命令做一个备份
  2. 使用 strip 命令删除该程序的 调试信息和符号信息
  3. 使用 ls -lh 命令可以发现:删除了调试信息的程序 和 备份程序,在大小上是有区别的
  4. 进一步使用 file 命令可以发现修2个文件:有 strippedwith debug_info, not stripped的区别

我们还可以使用 nm 命令来再次确认2个文件在符号信息上的区别,如图:
image.png

综上,当我们 strip 命令对程序的 调试信息、符号信息 进行了剥离/删除,可以达到如下目的:

  • 减少程序自身大小:很多大一点的程序,可以做到 Mb->Kb 的变化。这个其实在 嵌入场景 还是很有用的。
  • 安全:增大程序逆向工程的难度;程序的所有 调试信息和符号信息(函数、变量名) 都被删除,只保存二进制运行信息。

注:我们在编程时,不主动带 -g 的选项,最终编译出来的程序,也是会带有 符号信息 。大家可以简单试一下

2 调试

示例程序

test.c
#include <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void bug(int id) {
    /* illegal pointer, si_code = 128 (send by kernel) */
    printf("[%d] This is bug\n", id);
    int *p = (int *)-1;
    printf("%d\n", *p);
}

void extra_func() {
    printf("");
}

int func_b(int id) {
    printf("[%d] This is func_b\n", id);
    sleep(rand() % 5);
    extra_func();
    bug(id);
    extra_func();
}

int func_a(int id) {
    printf("[%d] This is func_a\n", id);
    sleep(rand() % 5);
    extra_func();
    func_b(id);
    extra_func();
}

void *func(void *param) {
    int id = (int)param;
    sleep(1);
    extra_func();
    func_a(id);
    extra_func();
    return NULL;
}

int main(int argc, char *argv[]) {
    int i;
    for (i = 0; i < 10; ++i) {
        pthread_t pid;
        if (0 != pthread_create(&pid, NULL, func, (void *)i)) {
            fprintf(stderr, "create thread %d failed (%d).\n", errno);
            exit(1);
        }
    }

    sleep(100);

    return 0;
}

编译运行

shell
// 编译
gcc -g -O2 test.c -lm -lpthread -o test
// 备份
cp test test_debug
// 删除调试信息
strip test


// 运行输出
./test
[2] This is func_a
[0] This is func_a
[1] This is func_a
[4] This is func_a
[5] This is func_a
[6] This is func_a
[8] This is func_a
[7] This is func_a
[9] This is func_a
[3] This is func_a
[4] This is func_b
[6] This is func_b
[8] This is func_b
[1] This is func_b
[8] This is bug
[9] This is func_b
Segmentation fault (core dumped)

当程序崩溃的时候,会在我们上面设置的路径产生一个 coredump 文件

如图:
image.png

而如果这个时候直接使用 gdb 对 test + core.test.33123.1651211138 这个文件进行调试;就会发现,不会有任何的 调用栈 信息。也就是我们最开始看到的那张图:
image.png

但是,使用 gdb 对 test_debug + core.test.33123.1651211138 进行查看,就能很直观的发现问题,如图:
image.png


3 调试信息处理

看到这里,我们已经明白是怎么回事,也知道怎么处理这种情况。

但是在真正的程序发布出去,到出现问题后的现场环境,没办法把 带调试的信息程序 一并带上的。

一般处理的办法:

3.1 coredump 文件回传

注:适用于程序自由度比较高,可以随意网络、U盘之类的媒介来管理coredump文件

通过网络,或者现场人工的方式,把产生的 coredump 文件回传到 公司。公司内部再使用带调试信息的程序,与 coredump 文件进行调试,发现问题。

3.2 加密调试文件

注:适用于程序应用环境,“不进不出”的情况。如:军工、银行、保险之类保密程度非常高的环境。

把不带调试信息,和带调试信息的程序一起发布。但是带有调试信息的程序,进行加密;加密的密码只有公司内部人员知道。
当出了问题,可以现场解开加密文件,进行 gdb 调试。
(但是这种还需要单纯对 加密密码 进行管理,后天发布的版本多了很麻烦,一堆问题。)

调试完后,记得一定要删除解压后的程序!
调试完后,记得一定要删除解压后的程序!
调试完后,记得一定要删除解压后的程序!

3.2 调试信息关联

注:适用于程序应用环境,“只进不出”的情况。

公式:程序 = 运行二进制信息 + 调试信息

也就是把一个程序分成 2个文件,发布时只发布 运行二进制文件
当程序崩溃的时候,把 调试信息文件 带现场,让这2个文件 关联 上,成为是一个 逻辑上 带调试信息程序。

1 单独保存调试信息
// test文件:             删除调试信息后的二进制文件
// test_debug文件:       原始带调试信息的二进制文件

// 使用 objcopy 命令把 调试信息 单独保存为一个文件
objcopy --only-keep-debug test_debug test_only_debug
2 产生关联
// test文件:             删除调试信息后的二进制文件
// test_debug文件:       原始带调试信息的二进制文件
// test_only_debug文件:  只有调试信息的二进制文件

// 使用 objcopy 命令把 调试信息文件 与 二进制运行文件 关联想来
objcopy --add-gnu-debuglink=test_only_debug test
3 gdb调试
// 当产生了关联后,再对 test 进行 gdb 调试,就和带调试的程序的一样的效果了
gdb ./test /data/coredump/core.test.33123.1651211138
...
(gdb) where
#0  printf (__fmt=0x56345f949016 "%d\n") at /usr/include/x86_64-linux-gnu/bits/stdio2.h:112
#1  bug (id=7) at test.c:12
#2  func_b (id=id@entry=7) at test.c:23
#3  0x000056345f948403 in func_a (id=<optimized out>) at test.c:31
#4  func (param=<optimized out>) at test.c:39
#5  0x00007fd3ca97c947 in start_thread (arg=<optimized out>) at pthread_create.c:435
#6  0x00007fd3caa0ca44 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:100
...

// 注:调试信息文件默认是跟调试文件在同一个目录。2个文件最好放同一个目录,要不还需要单独设置

通过这样的方式,就能在现场对程序进行调试。
当调试完成后,直接 删除调试信息文件 即可。

记得最后删除调试信息文件!
记得最后删除调试信息文件!
记得最后删除调试信息文件!


最后

到这里关于怎么调试无堆栈信息的办法已经了解了。

但是还有一个隐藏的问题:

Q:程序是持续更新,迭代发布的;那么多二进制运行文件,和调试信息文件,怎么管理区分?
A:可以使用文件的 BuildID 作为区分的依据。如图:
image.png

只要是从 同一个 程序剥离出来的信息,通过 file 命令,可以查看到他们的 BuildID 是一样的。


参考:
0. linux 下调试剥去调试信息的程序崩溃


以上。

我是疯子,感谢阅读

评论 (2)