目录:
0. 前导准备
- 情况说明
- 调试
- 调试信息处理
- 最后
知道什么 coredump 文件 和 怎么调试 coredump 的朋友,可以直接把 前导准备 跳过。
coredump叫做核心转储,它是进程运行时在突然崩溃的那一刻的一个内存快照。操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里。
coredump文件主要用程序调试,定位崩溃程序在代码出错的位置;作为源代码修改BUG的依据。
有如下几个:
明白 coredump 做是什么后,我们再看看怎么在 Linux 配置生成 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 文件的大小。
// 查看当前设置的 coredump 文件生成大小
ulimit -c
// 结果默认情况是0。
// 这里 unlimited 也可以换成其它的数字
ulimit -c unlimited
// 注:这种直接使用 ulimit 做的修改,只对当前 shell 有效。
// 如果想修改系统默认 coredump 大小就是 umlimited ,需要做进一步设置。如图:

在使用 GDB 调试 程序 + coredump文件,会有这种跟代码相关的 堆栈信息 是全是 ?? 这样的情况。
就我目前所知道的出现这种现象有2种原因:
- 程序本身无任何调试信息。也就是我们发布的 release版本程序;调试信息会在发布时被全部剥离/删除。
- 程序运行时,因为BUG,把程序运行时的 调用栈帧 给破坏掉了。
注:情况2 这种指是程序运行时,因为函数嵌套调试而产生的调用栈帧,而这个栈帧被破坏掉。如图:

受限于篇幅和时间的原因,本文只针对 情况1 做讨论和处理。
// 说真的,情况2其实是最难分析和说明的,需要对程序在系统程序上的运行机制非常了解;
// 还需要一些运气来说尝试/猜测没被破坏的上一个 “栈帧” 位置;
// 特别是那种野指针写飞了的那种,一般我宁愿 printf 来调试 -_-!
// 不过好在这种情况基本很少很少很少出现Q:做了什么操作会导致程序在崩溃时会出现情况1?
A:程序是运行前,使用 strip 命令把程序的所有调试信息给 剥离/删除 掉,就单纯的只有可运行二进制程序。注:只是剥离/删除 调试信息,和符号信息,并不会影响到程序本身的任何代码逻辑!!!
如图:

步骤:
-g 的程序。并使用 cp 命令做一个备份strip 命令删除该程序的 调试信息和符号信息ls -lh 命令可以发现:删除了调试信息的程序 和 备份程序,在大小上是有区别的file 命令可以发现修2个文件:有 stripped 和 with debug_info, not stripped的区别我们还可以使用 nm 命令来再次确认2个文件在符号信息上的区别,如图:

综上,当我们 strip 命令对程序的 调试信息、符号信息 进行了剥离/删除,可以达到如下目的:
注:我们在编程时,不主动带
-g的选项,最终编译出来的程序,也是会带有 符号信息 。大家可以简单试一下
示例程序
#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;
}编译运行
// 编译
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 文件
如图:

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

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

看到这里,我们已经明白是怎么回事,也知道怎么处理这种情况。
但是在真正的程序发布出去,到出现问题后的现场环境,没办法把 带调试的信息程序 一并带上的。
一般处理的办法:
注:适用于程序自由度比较高,可以随意网络、U盘之类的媒介来管理coredump文件
通过网络,或者现场人工的方式,把产生的 coredump 文件回传到 公司。公司内部再使用带调试信息的程序,与 coredump 文件进行调试,发现问题。
注:适用于程序应用环境,“不进不出”的情况。如:军工、银行、保险之类保密程度非常高的环境。
把不带调试信息,和带调试信息的程序一起发布。但是带有调试信息的程序,进行加密;加密的密码只有公司内部人员知道。
当出了问题,可以现场解开加密文件,进行 gdb 调试。
(但是这种还需要单纯对 加密密码 进行管理,后天发布的版本多了很麻烦,一堆问题。)
调试完后,记得一定要删除解压后的程序!
调试完后,记得一定要删除解压后的程序!
调试完后,记得一定要删除解压后的程序!
注:适用于程序应用环境,“只进不出”的情况。
公式:程序 = 运行二进制信息 + 调试信息
也就是把一个程序分成 2个文件,发布时只发布 运行二进制文件;
当程序崩溃的时候,把 调试信息文件 带现场,让这2个文件 关联 上,成为是一个 逻辑上 带调试信息程序。
// test文件: 删除调试信息后的二进制文件
// test_debug文件: 原始带调试信息的二进制文件
// 使用 objcopy 命令把 调试信息 单独保存为一个文件
objcopy --only-keep-debug test_debug test_only_debug// test文件: 删除调试信息后的二进制文件
// test_debug文件: 原始带调试信息的二进制文件
// test_only_debug文件: 只有调试信息的二进制文件
// 使用 objcopy 命令把 调试信息文件 与 二进制运行文件 关联想来
objcopy --add-gnu-debuglink=test_only_debug test// 当产生了关联后,再对 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作为区分的依据。如图:
只要是从 同一个 程序剥离出来的信息,通过 file 命令,可以查看到他们的 BuildID 是一样的。
参考:
0. linux 下调试剥去调试信息的程序崩溃
以上。
我是疯子,感谢阅读