除去上篇文章总结的通过maps文件和反汇编定位崩溃问题原因的办法外,在嵌入式项目中,还有一种常用的定位办法-coredump,在嵌入式Linux项目中启用coredump功能,相当于为程序安装了一个“黑匣子”,当程序意外崩溃时,它能保存崩溃瞬间的内存状态、寄存器值等关键信息,极大方便了事后调试。下面大概介绍下coredump以及如何在嵌入式项目中配置和使用coredump。
1.coredump概述
coredump(核心转储)是操作系统在程序异常终止(如段错误、非法指令等)时生成的内存快照文件,记录进程崩溃时的内存、寄存器状态、堆栈信息等。主要用于调试和分析程序崩溃原因。
2.coredump 生成条件
- 程序触发严重错误(如 SIGSEGV、SIGABRT 等信号)。
- 系统配置允许生成 coredump(通过
ulimit -c
设置大小,默认可能为 0 即禁用)。 - 文件系统有足够空间,且进程对目标目录有写入权限。
3.配置coredump
(1)首先make menuconfig,查看内核是否支持并已开启coredump功能,如果没有,全局查找到coredump关键字,开启coredump功能,然后重新编译内核并烧写到设备上;
(2)内核支持后,需要在系统层面进行配置,以控制coredump的生成和保存。
-
解除资源限制:使用
ulimit -c unlimited
命令,允许生成任意大小的coredump文件。这对于资源紧张的嵌入式设备很重要,你也可以根据存储空间指定文件大小上限,例如ulimit -c 1024
(单位为KB)。注意:
ulimit
命令的作用范围仅限于当前shell会话及其派生的子进程。对于通过其他方式(如后台服务、开机自启动)启动的程序,需要采用其他方法设置。 -
设置coredump路径和格式:通过修改
/proc/sys/kernel/core_pattern
文件来定制coredump文件的保存路径和文件名,
# 示例:将coredump保存到/tmp目录,文件名包含程序名和PID
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
常用的文件名格式符包括:
%e
:可执行文件名
%p
:进程PID
%t
:时间戳
%s
:导致coredump的信号编号
-
启用PID扩展名(可选):执行
echo 1 > /proc/sys/kernel/core_uses_pid
,可以让coredump文件名自动加上进程PID,便于区分。 -
设置SUID程序dump(如需要):如果你的程序设置了SUID权限,可能需要执行
echo 2 > /proc/sys/fs/suid_dumpable
才能为其生成coredump。
4.对于后台守护进程或开机自启动程序,由于它们不继承当前shell的 ulimit
设置,需要在程序代码中主动启用coredump功能。
下面是一个C语言的示例代码,可以在程序的main函数初始化部分调用:
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static int enable_coredump(void) {
struct rlimit limit;
// 将core文件的大小限制设置为无限制
limit.rlim_cur = RLIM_INFINITY;
limit.rlim_max = RLIM_INFINITY;
if (setrlimit(RLIMIT_CORE, &limit) != 0) {
fprintf(stderr, "Error: setrlimit failed: %s\n", strerror(errno));
return -1;
}
// 可选:设置core_pattern,确保路径可写
system("echo \"/media/mmcblk0p1/core.%e.%p.%t\" > /proc/sys/kernel/core_pattern");
printf("Coredump enabled.\n");
return 0;
}
// 在main函数早期调用enable_coredump()
int main() {
enable_coredump();
// ... 程序主要逻辑
}
这里需要说明的一点,在查找应用崩溃问题的时候,为了方便调试,第四点最好都是要的,因为ulimit设置都只是单次开机生效(适合./demo_app_test调试),机器重启后,会恢复默认设置,再次调试需要重新设置ulimit。
5.编译和调试准备
为了后续能进行有效的调试,在编译程序时需要加上调试符号信息。
例如,使用GCC编译时,务必添加 -g
选项,例如 gcc -g -o my_program my_program.c;
这样生成的coredump文件才能显示详细的符号和代码信息;
如果是使用makefile编译,需要在包含目标文件的makefile中添加CFLAGS+=-g -o0;-O0
(禁用优化),避免编译器优化改变代码结构,导致调试信息与源代码行号错位。
6.分析coredump文件
当程序崩溃生成coredump文件后,可以使用GDB进行分析。
-
使用GDB分析:将coredump文件复制到你的开发环境(如果嵌入式设备与开发机架构不同,需要使用交叉编译工具链中的GDB,例如
arm-linux-gnueabihf-gdb
)。# 格式:gdb <带调试信息的程序> <coredump文件> arm-buildroot-linux-uclibcgnueabihf-gdb ./my_program /tmp/core.my_program.1234
-
常用GDB命令:
-
bt
或where
:查看崩溃时的函数调用堆栈(backtrace),这是定位问题最关键的指令。 -
info registers
:查看寄存器状态。 -
print variable
:打印变量的值。 -
list
:查看崩溃点附近的源代码。
-
7.实用技巧与注意事项
-
存储空间管理:嵌入式设备存储空间有限。可以通过
core_pattern
将coredump文件保存到特定分区(如SD卡或扩展存储),或使用管道符号|
将其直接发送到网络或压缩脚本,以避免占满根文件系统。 -
交叉调试:确保使用与目标板架构匹配的交叉调试器(如
arm-linux-gnueabihf-gdb
)来分析coredump文件。 -
符号文件:如果GDB无法直接解析动态库符号,可以使用
set solib-search-path
或set sysroot
命令指定动态库的路径。
8.常见问题和解决方向
问题现象 |
主要可能原因 |
核心解决思路 |
---|---|---|
提示 |
可执行文件在编译时未包含调试信息(缺少 |
使用 |
堆栈信息显示为问号 |
1. 缺少对应的调试符号库(如系统库或动态库)。 2. 调试符号的版本与生成coredump时的库版本不匹配。 |
1. 安装匹配的调试符号包(如使用 |
动态库中的函数和变量无法查看 |
动态库本身在编译时未使用 |
使用 |
可执行文件与coredump文件不匹配 |
用于调试的可执行文件或动态库,与当初生成coredump文件时的程序版本不同。 |
确保用于调试的可执行文件和动态库与生成coredump时的版本完全一致。 |
堆栈本身可能已损坏 |
内存越界等问题破坏了调用堆栈,导致GDB无法正常解析。 |
尝试手动检查堆栈内存,或分析其他线程的堆栈来寻找线索。 |
9.如何验证Makefile编译生成的可执行文件确实包含了完整的调试符号?
要验证 Makefile 编译生成的可执行文件是否包含了完整的调试符号,你可以通过以下几种方法进行系统性检查。下面这个表格汇总了核心的验证手段和它们能告诉你什么。
验证方法 |
关键命令 |
成功标志(表明包含调试符号) |
---|---|---|
|
|
输出中包含 |
|
|
能输出大量 DWARF 格式的调试信息,而非提示 |
|
|
GDB 欢迎信息显示 |
检查编译日志 |
查看 |
每个编译命令(如 |
个人感觉file命令是最快可以辨别可执行文件是否含有调试信息的,它同时也可以用于辨别动态库是否已加入调试信息。