资讯动态

程序崩溃后的“黑匣子”:core dump生成与gdb分析实战

发布时间:2026/10/16 12:57:52 来源:尧图企业网站定制
1. 到底什么是core dump——程序员视角的“命书”是什么搞了十几年后端我见过最玄学的调试现场就是线上进程突然猝死之后几个人围着屏幕说“这程序怕不是命不好”。真正能改判命运的往往不是运气而是那个经常被忽略、甚至被系统当作垃圾清理掉的core dump文件。core dump不是什么高深理论它是进程崩溃的那一刻操作系统内核替整个程序写下的“遗书”进程内存快照、CPU寄存器现场、函数调用栈、打开的文件映射关系一样不少。只要你当时开了转储程序死后就会留下一个包含完整现场信息的文件后端开发、C/C程序员、嵌入式开发者以及所有被段错误吓到过的人都应该学会读它。很多新手遇到程序崩溃第一反应是“重新跑一遍试试”。这恰恰是最浪费时间的方式很多崩溃是概率性的尤其是多线程的资源竞争、堆内存损坏、Use-After-Free这类问题重跑十次未必能复现一次。而core dump把崩溃瞬间的“案发现场”完整冻结了下来相当于飞行记录里的黑匣子。你不需要复现事故只需要读懂黑匣子。1.1 core文件里到底装了什么从文件格式看core dump本质上是一个ELF文件类型是ET_CORE。你可以用file命令验证一下$ file core core: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), from ./crash_demo注意“from”后面跟着产生这个core的可执行文件名。这是先决判断的第一步你说的“命”到底是程序的哪一“世”。用readelf可以拆开看细节$ readelf -h core ELF Header: Class: ELF64 Type: CORE (Core file) Machine: Advanced Micro Devices X86-64readelf -n core还能看到内核写入的note段里面记录了崩溃信号、进程PID、CPU寄存器状态。比如SIGSEGV对应的就是11号信号看到这个基本就知道是段错误。还有NT_FILE类型的note会把崩溃瞬间进程映射了哪些动态库、各个库在内存中的地址范围都列出来这对于局部变量被踩、返回地址被篡改这类问题非常有价值。1.2 为什么它被称为“程序员的命格档案”换个通俗的说法正常的程序运行是“阳间”逻辑进程死了之后留下的core则是“阴间”证据。你手里的不是程序本身而是它死后的一切痕迹。这些痕迹包括每个线程的调用栈栈回溯当前栈上的局部变量值全部通用寄存器的值堆内存上某个对象的内容加载的动态库及其地址范围这比崩溃日志里的那几行堆栈信息高到不知哪里去了。日志只能告诉你“这个位置崩了”core却可以告诉你“崩之前数据变成了什么样、在哪一步变坏的”。所谓“解读命格”其实就是把这份档案从头到尾梳理一遍最终找到压垮骆驼的那根稻草。1.3 跨平台视野Windows和macOS的“遗物”也值得看虽然今天重点讲Linux下的core dump但思路是通用的。Windows上有用户态转储文件.dmpdumpbin或者WinDbg可以分析macOS上崩溃后系统生成的.ips崩溃报告也记录了类似信息。不同系统的“命书”格式不同但核心信息殊途同归进程现场、线程栈、寄存器、崩溃指令地址。学会了Linux这一套换平台只是换工具名而已分析的思路完全一致。2. 怎么“起卦”先把core dump从系统里请出来很多人卡在第一步程序崩了但core文件根本没生成。要么是ulimit限制要么是内核转储路径被改成了管道导致core被交给systemd或者其他服务接管。这一节咱们把获取core的整套配置讲清楚这是所有分析工作的前置条件。2.1 打开资源限制ulimit -cLinux下最常见的原因就是core文件大小被限制成0。查询$ ulimit -c 00意味着禁止生成core。临时放开$ ulimit -c unlimited注意这个命令只对当前shell及其子进程生效而且是软限制只能往小调不能往大调除非你有权限改硬限制。调试的时候在同一个终端里先执行再启动目标程序这样core才能正常落地。如果是在脚本或者服务场景下就需要在启动程序之前先设置。服务的systemd unit文件里可以加LimitCOREinfinity不然服务进程继承了系统默认的限制一样不会产生core。这类细节容易让人误以为“程序没崩溃”实际上只是白崩了一点证据都没留下。2.2 内核转储路径core_pattern决定文件去哪内核将core写到什么位置、叫什么名字由/proc/sys/kernel/core_pattern控制。默认往往只是core所有进程的core都往当前工作目录写同名文件很容易被覆盖。强烈建议按“PID-可执行名-时间戳”组织sysctl -w kernel.core_pattern/cores/core.%p.%e.%t这样每个崩掉的进程都有独立文件后续排查时清楚知道是哪一个程序、什么时候死的。如果core_pattern被设置成了|/usr/lib/systemd/systemd-coredump或者|/usr/share/apport/apport这样的管道形式那么不会直接生成文件而是交给系统守护进程处理。这种模式下systemd一般会把core储存起来。2.3 systemd-coredump的使用现代发行版普遍集成systemd。当core_pattern指向systemd-coredump管道时查询崩溃记录$ coredumpctl list TIME PID UID GID SIG COREFILE EXE Thu 2025-01-16 15:23:01 CST 1234 1000 1000 SIGSEGV present /home/test/crash_demo查看某个崩溃的详细信息$ coredumpctl info 1234如果系统配置了存储可以用coredumpctl dump 1234 -o core导出一个gdb可用的core文件。这一步相当于从系统“请”出原始的命书保存下来慢慢看。有些人会觉得systemd-coredump多此一举但线上环境它其实更方便默认会保留较长时间支持按PID查询还自动附带元数据。缺点是当你需要在容器里或者极简系统上分析时可能没有这个组件那就老老实实改core_pattern。2.4 实操故意制造一次段错误并拿到core理论说再多不如直接动手跑一次。创建一个最简单的崩溃程序#include stdio.h int main(void) { int *p NULL; *p 42; return 0; }编译时务必带-g这是保留调试符号的关键gcc -g -O0 -o crash_demo crash_demo.c然后依次执行ulimit -c unlimited ./crash_demo如果环境走systemd用第三节的coredumpctl list找记录如果直接写文件cwd下就能看到core文件。肉眼可见Segmentation fault (core dumped)提示。这只证明“命案”发生了还谈不上解读。2.5 拿不到core时的排查顺序如果系统提示了崩溃却没有core文件按这个顺序排查ulimit -c确认软/硬限制是否为0。cat /proc/sys/kernel/core_pattern看路径是文件路径还是管道。管道下不会直接生成文件转存到systemd或apport了。看目标目录是否有写权限磁盘是否满了df -h。检查是否设置了setuid等特权属性内核默认不允许这类程序产生core受fs.suid_dumpable控制。注意即便上面全检查了还有一种情况是进程通过prctl(PR_SET_DUMPABLE, 0)主动拒绝转储。某些服务出于安全考虑会主动关闭dumpable这类进程再怎么调ulimit都不出core需要从代码层面去掉该调用才能调试。3. 核心环节用gdb解读命书core文件拿在手里接下来就是真正“解卦”了。之所以说gdb是解卦核心是因为它既能关联可执行文件的符号表又能读取core里的内存、寄存器、栈帧直接把“遗书”翻译成看得懂的代码行。3.1 开坛基本姿势gdb执行文件和core配对启动gdb时同时指定崩溃程序和core文件gdb ./crash_demo core也可以先进gdb再加载gdb (gdb) file ./crash_demo (gdb) core-file ./core关键在于可执行文件要和core“对得上”也就是说程序崩溃前运行的二进制就是当前这个。如果后来重新编译过符号表行号、函数地址会对不上gdb会给出警告这时分析结果可能失真。保存core文件的同时把对应的可执行文件版本一起保存是排查事故的第一习惯。载入成功后gdb通常直接停在崩溃线程的崩溃点。首先是bt(gdb) bt #0 0x0000555555555154 in main () at crash_demo.c:5这一行已经说明了一切崩溃发生在crash_demo.c第5行地址0x0000555555555154。如果有多个栈帧会从上到下依次列出最上面是当前崩溃点下面是调用者。这个顺序很多人容易搞反一定记住#0是说“我正在哪儿死”往下才是“我是被谁叫到这里来的”。查看完整栈帧信息用bt full会同时打印每个栈帧的局部变量(gdb) bt full #0 0x0000555555555154 in main () at crash_demo.c:5 p 0x0直接看到了p 0x0空指针解引用案情清楚。3.2 看寄存器现场崩溃指令到底做了什么栈只是“结果”寄存器才是“动作”。常见的组合拳(gdb) info registers rip rsp rbp rip 0x555555555154 0x555555555154 main20 rsp 0x7fffffffe3e0 0x7fffffffe3e0再看当前要执行的指令(gdb) x/i $rip 0x555555555154 main20: mov DWORD PTR [rax], 0x2a这条指令意思很明确往rax寄存器指向的内存地址写入42。结合变量信息rax其实是空指针于是段错误。光看崩溃日志你只知道“段错误”看了寄存器才知道具体的机器指令动作。栈顶数据也值得一瞧(gdb) x/8gx $rsp 0x7fffffffe3e0: 0x0000555555555166 0x00007ffff7dd8390 ...如果崩溃点涉及函数返回地址被破坏这里就能发现栈上的返回地址异常常见于栈缓冲区溢出。3.3 变量和内存还原数据扭曲的过程核心解读中的“命格扭转”往往体现在变量的值上。切换栈帧(gdb) frame 1 #1 0x0000555555555160 in caller_func () at demo.c:20这个栈帧里的局部变量是崩溃发生前一瞬间还存活的数据。用info locals全部打出来p打印某个变量ptype查看结构定义x查看内存内容。这些操作组合起来可以完成一次标准的事后尸检。比如你怀疑某个对象被写坏但core里对象的值正常那要把目光放到附近的内存区域。用x/16gx 地址连续打印16个8字节的字很多越界写的问题会在相邻变量上留下异常值比如0x41414141这种特征。3.4 多线程的魂兮归来apply all bt线上程序很少是单线程。core文件保存了所有线程的栈而不只是崩溃线程。查看全局(gdb) thread apply all bt这一步极其重要很多时候崩溃线程只是“受害现场”真正的凶手线程还活着或者在另一条栈上。比如A线程释放了一块内存B线程随后访问崩溃发生在B但根因在A的释放时机。只有把每个线程的栈都打出来才能找出交叉污染的来龙去脉。切换线程用thread 编号查看线程局部存储用thread apply all bt full把每个线程的局部变量一并导出。多线程core分析的价值就在于这张“全景图”。3.5 综合实战一个链表遍历崩掉的完整解读写一个稍复杂的例子。链表遍历函数typedef struct node { int value; struct node *next; } node_t; int walk_list(node_t *head) { int sum 0; while (head) { sum head-value; head head-next; } return sum; }某次线上传入了一个被破坏的链表头导致崩溃。如果只从日志看就是walk_list崩溃。但为什么head是一个非法地址用gdb实际操作(gdb) bt #0 0x00005555555552f0 in walk_list (head0x4141414141414141) at list.c:8 #1 0x0000555555555315 in main () at main.c:15出现0x41414141这种地址几乎可以立刻联想到内存被某个“垃圾填充值”覆盖了。在glibc的调试模式或某些内存分配器的fill模式里0x41很常见也可能来自写过野指针的某段代码。这时候看walk_list的参数head说明调用它之前这个指针就已经坏了。再看frame 1(gdb) frame 1 (gdb) p head $1 (node_t *) 0x4141414141414141说明传入时就坏了。是谁把head改成0x41414141的那就要看之前的写操作。实践里最有效的办法是看附近的内存分配上下文、对象生命周期以及是否有释放后再访问的嫌疑。配合x/16gx看地址周围基本能判断出覆盖是栈上越界还是堆上越界。3.6 符号缺失时怎么硬解如果core里只有裸地址没有函数名常见于发布版没有带符号。此时不要慌也有办法(gdb) info symbol 0x00007f1234567890gdb会尝试在已加载的符号表里反查。如果完全没有符号就只能手工算偏移info proc mappings查看各个库的内存基址。把崩溃地址减去所属库的基址得到库内偏移。用addr2line -f -e /path/to/lib.so 偏移量转换成函数和行号需要该库有符号。当然最可靠的还是在编译时保留符号。后面专门讲。4. 常见命格图谱不同信号对应的“死法”说真的多年排查下来core dump里出现最多的崩溃原因翻来覆去就那么几类。把这套“命格图谱”背下来拿到core先看信号心里就能猜个七八分。信号值死法俗称典型触发场景SIGSEGV11段错误之王空指针、野指针、越界访问、Use-After-FreeSIGABRT6自我了断assert失败、C未捕获异常、glibc堆损坏检测SIGFPE8算术禁忌整数除零、取模零SIGILL4非法指令损坏的函数指针、CPU指令集不匹配SIGBUS7总线事故mmap后文件被截短、未对齐访问SIGKILL9天降制裁OOM Killer、外部强杀4.1 SIGSEGV最经典的“命中注定”空指针解引用是最常见的。代码里*p时p NULL内核直接拍板段错误。在core里表现为崩溃地址为0x0。野指针则是变量未被初始化保存了栈上的垃圾值表现为崩溃地址是个奇怪的数字。还有一种隐蔽的场景Use-After-Free。内存释放后再访问如果这片内存已经被重新分配给了别的对象读出来的数据是脏的写进去就是破坏。这是core分析里最难的一类需要结合堆布局和对象生命周期判断。遇到这类问题ASanAddressSanitizer是比core dump更趁手的工具后面会说到。4.2 SIGABRT程序自己把自己送走了很多人拿到SIGABRT的core会困惑为什么栈顶是__GI_raise、__GI_abort其实这相当于程序主动引爆。典型原因是assert()宏条件不满足直接abort。C的std::terminate比如抛出的异常没有catch。glibc检测到堆元数据被破坏double free、越界写越过了chunk头调用abort并打印 “malloc(): invalid pointer”。这类core的分析重点不是栈顶的abort函数而是向上看是哪个业务逻辑触发的。bt一路往下找到最接近业务代码的栈帧才是根因。4.3 SIGFPE你以为的“浮点异常”其实不是浮点SIGFPE最经典的成因不是浮点数除法问题而是整数除以零。int a 10 / 0;直接崩。core里崩溃指令会出现div或idiv指令被除数是0。浮点除以零在默认设置下不一定产生SIGFPE所以不要把名字带“FPE”就以为是浮点的事。4.4 SIGILL与SIGBUS指令和环境的问题SIGILL常出现在函数指针被破坏后程序跳到垃圾地址执行那串数据恰好不是合法指令。也有可能是程序在A机器编译用了老CPU没有的新指令放到B机器直接非法。SIGBUS则通常是mmap的文件被截断映射区域访问越界或者某些架构上结构体成员没有对齐硬件直接报总线错误。4.5 SIGKILL没有遗书的死法这里要泼一盆冷水SIGKILL的进程是没法产生core dump的因为SIGKILL默认动作就是直接杀死进程不给清理和转储的机会。线上最常见的场景是OOM Killer。查这类问题不能靠core要看系统日志里有没有Out of memory: Killed process的记录。换句话说服务器内存不够的“天劫”死亡现场被系统处理掉了留下的只有系统日志。5. 排坑实录解卦时的常见坑与高级技巧说实话在这一行干久了你会发现真正难的往往不是读懂core而是core本身有问题、符号不全、打印出来的栈“骗人”。这里把踩过的坑做一个系统性的整理。5.1 core文件分析前先看build-id每次拿到core文件第一时间用readelf看$ readelf -n core | grep -E build-id|CORE同时看可执行文件的build-id$ readelf -n ./crash_demo | grep build-id两者一致才能保证当前的源码、二进制、core是同一时刻的产物。这一步能避免白白分析几个小时的尴尬。5.2 发布版二进制被strip的窘境线上发布时经常有人为了“减小体积”用strip去掉符号。这样core文件虽然还在但gdb只能给出一堆地址。我的经验是发布时一定要保留一份带符号的二进制与产物一起存到制品库。线上崩溃之后把带符号的二进制和线上core配对gdb照样能给出行号。如果实在没有还有一个办法用debuginfod目前主流发行版都支持从符号服务器自动下载匹配的debug符号。gdb里设置(gdb) set debuginfod enabled on前提是网络上能访问到符号服务器且build-id匹配。核心思路就是不要丢失“与生产完全一致的调试版本”。5.3 优化代码让行号像“命书里的错别字”-O2会将函数内联、变量优化到寄存器甚至完全删除。结果core里的栈回溯可能显示崩溃在某一行但那一行根本不可能产生该指令或者变量显示optimized out。这是最误导人的情况。缓解策略编译时增加-fno-omit-frame-pointer保证栈回溯完整。关键服务保留一个-O1 -g的debug版本虽然性能略降但崩溃可读性大增。实在只能分析优化版本时不要拘泥于行号转为直接看反汇编disassemble $pc-32,$pc32。我自己线上生产常用-O2 -g把符号带上同时配合-fno-omit-frame-pointer。这样体积稍微增大“命书”却清晰得多。5.4 明明有崩溃但core指向的线程看起来“干净无害”多线程程序经常出现这种情况崩溃线程的栈毫无问题逻辑上不该死但就是死了。这时候要立刻用thread apply all bt full把全部线程的栈导出来看。罪魁祸首往往是某个线程正在做破坏内存的操作比如越界写、释放共享对象而受害线程恰好这个时候访问到了被破坏的数据。还有一个典型状况是C异常在某个线程中未被捕获terminate调用触发了abort。gr时thread apply all bt会看到异常链配合catch throw做活体调试更直接但没有活体时只能从栈顶的函数推断。5.5 高级技巧从core里“还魂”出一个检查列表遇到复杂问题直接在gdb里写表达式来验证假设。比如链表长度循环有问题可以(gdb) p head (gdb) set $tmp head (gdb) while $tmp ! 0 set $tmp $tmp-next end不过为了效率我通常干脆在代码里加“体检函数”遍历链表时检查每个节点的magic number和next地址是否在某段合理区间。这种自定义检查逻辑放在业务代码里core文件一加载就可以直接从函数栈上看到数据被改坏的时机点。注意core文件分析里最忌讳的是“跳到结论”。看到一个无形指针就往空指针上套看到的越界就往数组上套。正确姿势是先通读全部线程栈再看崩溃指令的寄存器最后根据数据特征反推来源。证据链齐了结论才有说服力。5.6 用Sanitizer做预防性体检这里扯回来一句core dump是事后补救Sanitizer是事前预防。ASan/UBSan可以在开发阶段把越界、未初始化、溢出、整数溢出直接抓出来。跑测试的时候加上-fsanitizeaddress,undefined -fno-omit-frame-pointer崩溃时的错误信息直接指出是哪一行。这相当于在“命运”发生之前就改写了它比拿core翻案省太多时间。6. 写在最后让“命数”真正可控的日常习惯这一节算是夹带私货的个人经验。我实际调试core dump这么多年最深的体会是拿到一个好的core文件解决一半问题拿不到再牛的工具也白搭。所以项目里我永远保留这么几条铁律每台Linux服务器都确认core pattern配置正确路径写到独立目录不要堆在程序的工作目录里。发布包保留-g的带符号版本即使体积翻倍也值得。文件存储便宜排查事故的成本却很高。崩溃信息里带版本号、提交号、环境信息。很多公司把可执行文件版本写进日志这是最便宜的“命理锚点”。定期模拟崩溃验证core转储链路是通的。别等到线上事故才发现core根本没生成那种感觉很绝望。再分享一个小习惯每次分析完一个core把结论写成简短的“事故档案”记录信号、栈顶、根因、修复方式。时间长了就是一本真实的“命格奖惩表”哪些编码方式容易招鬼一目了然。回到标题说的“程序员易经”其实真正懂行的人都知道所谓命格无非是概率和习惯的结果。core dump就是一张反推因果的“命盘”读懂了它你就能从数据里看见事故是如何一步步走到崩溃那一刻的。这份技能比任何玄学都可靠。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑