
排查Linux程序崩溃问题时最有价值的工具之一就是core dump。程序因为段错误等致命信号挂掉的瞬间内核会把整个内存镜像保存成一个文件也就是core文件。拿到它以后你可以用gdb准确还原程序挂掉那一刻的调用栈、寄存器值、内存里的数据比对着零散日志大海捞针高效太多了。很多Linux发行版默认不生成core文件所以线上服务一崩找半天发现目录里什么都没有简直是每个后端开发都经历过的噩梦。下面我就按自己平时踩坑的顺序把core dump的开启配置、底层机制、实际调试方法和常见问题完整讲一遍后端、运维、嵌入式、安全分析的同学都可以直接照着操作。1. core dump到底是什么它和普通日志有啥区别1.1 内核在程序崩溃时做了什么先回到最基础的问题上core dump到底是怎么产生的。Linux里进程在运行过程遇到非法内存访问、执行非法指令这类情况时内核会向进程发送致命信号最常见的就是SIGSEGV也就是段错误。当进程接收到这类会导致终止的信号时内核会检查当前进程的core资源限制如果限制允许就把整个进程的用户态地址空间内容写进磁盘里的一个文件这个文件就是core文件。这个动作是由内核独立完成的跟应用程序本身几乎没有关系所以哪怕你手上只有编译好的二进制没源码也一样能通过core文件分析崩溃原因。core文件本质上是ELF格式里面除了进程的内存映像还记录了寄存器上下文、内存映射表、触发崩溃的信号编号、当时的程序计数器PC等信息。这些信息组合在一起基本就是程序猝死那一刻的完整存证。很多人最初会把core dump和日志混为一谈觉得程序崩了有日志不就行了。但日志属于应用程序主动写出来的东西开发者只能记录“自己事先想到要记录的内容”而core dump是操作系统被动拍下的完整快照记录的是开发者没预料到或者根本没来得及写入日志的内容。我用办案来打个比方日志是现场证人的口头描述可能遗漏关键线索core文件则相当于覆盖了完整过程的监控录像细节全都在里面。1.2 默认关闭是拿安全和磁盘空间换稳定既然core dump这么好用为什么绝大多数Linux发行版默认不生成说到底是为了安全和磁盘稳定。安全问题很直接core文件包含进程完整的地址空间。程序运行过程中如果处理过密码、令牌、密钥、用户隐私数据这些内容原封不动地存在于内存中崩溃时就会跟着进程一起被写进core文件。一旦这个文件落到了不合适的人手里就相当于把内部数据直接交了出去。所以系统默认关掉这个能力在安全上是最保险的。磁盘问题同样现实一个常驻服务进程的内存占用往往是几GB甚至几十GB真到了崩溃那一刻把这些数据全部写盘会把磁盘瞬间打满。如果服务器没有容量告警写满磁盘会造成一系列连锁反应比如别的进程无法生成日志、数据库拒绝写入影响面比原来那个进程崩溃大得多。这也就是为什么内核默认把RLIMIT_CORE设为0也就是禁止生成。2. 开启core dump的完整配置流程2.1 动手前先查三个状态在动手配置之前一定要先摸清当前系统的三个状态不然很容易出现“配置了但没生效”的困惑。我每次排查都会先执行这三条命令ulimit -c cat /proc/sys/kernel/core_pattern cat /proc/sys/kernel/core_uses_pidulimit -c输出0表示当前shell会话禁止生成core输出unlimited或一个正数表示允许数字代表core文件的最大字节数默认单位是KB。这里有个关键认知这个限制只对当前shell以及从它启动的子进程生效换一个终端窗口配置就会恢复成系统默认值。/proc/sys/kernel/core_pattern是用来定义core文件生成路径和命名规则的全局参数。如果输出的是裸的core说明core会写到崩溃进程的工作目录文件名就叫core如果输出带|开头比如|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h说明内核把core内容交给外部程序处理了不直接落盘。/proc/sys/kernel/core_uses_pid是老一代内核的开关值为1时会在core文件名后面自动追加PID避免多个进程崩溃时互相覆盖。现在更推荐直接用core_pattern里的%p占位符来实现这个需求比依赖这个开关更直观但了解它仍然有用因为很多老系统还在用。2.2 临时开启验证路径如果只是想快速验证一下当前环境下core dump能不能用用ulimit命令改一下就行ulimit -c unlimited这里要强调一下unlimited的意思是允许生成任意大小的core文件而不是生成了一个叫unlimited大小的文件。也可以设置成具体数值比如ulimit -c 102400表示最多允许生成100MB左右的core文件超了就不写了。设置完成之后拿一个最简单的崩溃程序来验证。写个小小的C程序空指针解引用#include stdio.h int main() { int *p NULL; *p 42; return 0; }编译运行gcc -g -o segv segv.c ./segv如果输出Segmentation fault (core dumped)就说明系统已经把core文件写下来了用ls -l core*就能看到。如果在运行目录下没找到先别急多半是core_pattern里配置了systemd接管或者路径指向了别处用前面说的cat /proc/sys/kernel/core_pattern看一下再去对应目录找。ulimit这种方式只适合临时验证。因为每个终端新建session后都要重新设置生产环境不可能每次登录都手动敲一遍所以下面的持久化配置才是重点。2.3 持久化配置limits.conf怎么改想让core限制在用户每次登录后都生效标准做法是修改/etc/security/limits.conf。这个文件会被PAM模块在用户登录时读取格式简单明了user type item value比如对所有用户放开core限制追加这样两行* soft core unlimited * hard core unlimited*表示全部用户。如果想限定某个用户把*换成用户名就行。soft代表软限制进程运行中还允许自行调高到hard限制以内hard代表硬限制普通用户不能超过这个值。开发环境一般两个都设成unlimited生产环境我建议设一个具体上限比如core 2097152大约2GB防止某个超大进程异常崩溃时把磁盘打爆。改完后不需要重启机器重新登录一次让PAM重新加载配置即可。再执行ulimit -c应该能看到新设置。如果还是0优先检查三件事是否真的重新登录了、是否把配置写错了位置、系统登录途径是不是走了其他PAM模块。2.4 core文件命名规则与存放目录用limits.conf解决了“能不能生成”的问题接下来要解决“生成到哪、叫什么”的问题这就要改内核参数kernel.core_pattern。最粗暴的临时设置方法是echo /var/log/core/core.%e.%p.%t /proc/sys/kernel/core_pattern这样core会统一放到/var/log/core/这个目录文件名由程序名、PID、时间戳组成互不覆盖看着也直观。这里必须提醒一句如果目录不存在或者崩溃进程所属用户对目录没有写权限core文件依然不会出现。很多新手都会忽略这一点把core_pattern改成了一个不存在的路径然后死活找不到core。core_pattern支持的占位符我把最常用的列出来占位符含义%p崩溃进程的PID%u崩溃进程的用户ID%e可执行文件名%s触发崩溃的信号编号%t崩溃时刻的unix时间戳%h主机名实际项目中%e.%p.%t这个组合已经很合理。如果你想按崩溃时间定期清理文件名里带时间戳尤其方便。要想重启后依然生效就把配置写进/etc/sysctl.conf或者更推荐在/etc/sysctl.d/下新建一个专门的文件比如/etc/sysctl.d/90-core.confkernel.core_pattern /var/log/core/core.%e.%p.%t kernel.core_uses_pid 0写完执行sysctl -p让当前内核立刻加载新配置。这种做法好处是专人文件专人管理不会和系统默认配置混在一起。2.5 systemd接管core的特殊情况如果你用的是近几年的CentOS、Ubuntu、Debian发行版大概率会发现core_pattern默认被初始化成了管道方式内容类似|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h这里|开头的含义是内核不直接写磁盘文件而是把core数据通过管道传给systemd-coredump程序统一处理。这些core默认存放在/var/lib/systemd/coredump/下面并且是压缩过的普通ls确实看不出来。这种情况下分析core文件不用自己去目录翻直接用两个更方便的命令coredumpctl list coredumpctl info PIDcoredumpctl list列出所有记录到的core dump包括时间、程序名、PIDcoredumpctl info显示指定PID的详细信息。想在崩溃后立刻用gdb分析也可以执行coredumpctl debug PID它会自动找到对应的core文件和可执行文件直接拉起gdb并切换到崩溃现场。虽然systemd这套方案带索引、带压缩省心不少但我的生产环境实践中还是更习惯改成直接落盘方式后面会说具体原因。如果你确定要用systemd方案那么前面配置core_pattern的步骤就可以跳过只需要保证systemd-coredump服务是启用的。3. core文件生成后的调试实战3.1 用一个越界崩溃案例走完整流程配置讲完下面走一遍真实调试流程。我经常拿一个“循环边界越界”的程序来演示因为这类bug很有代表性平时不崩一旦数据量或输入条件变化就会触发崩溃。#include stdio.h #include string.h #include stdlib.h typedef struct { char name[32]; int id; } user_t; int find_user(user_t *users, int count, int target) { for (int i 0; i count; i) { if (users[i].id target) { printf(found: %s\n, users[i].name); return 0; } } return -1; } int main() { user_t *users malloc(2 * sizeof(user_t)); strcpy(users[0].name, alice); users[0].id 100; strcpy(users[1].name, bob); users[1].id 101; find_user(users, 2, 101); return 0; }问题出在for (int i 0; i count; i)数组只有2个元素循环却访问了users[2]也就是越界访问了未分配的内存。这种bug有时候能跑过去有时候一碰就段错误典型的定时炸弹。编译时记得加-g这样core分析阶段才能看到源码行号gcc -g -o app app.c ulimit -c unlimited ./app崩溃后我们去core_pattern指定的目录找文件ls -l /var/log/core/ core.app.12345.1699999999先执行file /var/log/core/core.app.12345.1699999999确认文件格式完整再进入下一步。3.2 用gdb还原崩溃现场拿到core文件后标准操作是用崩溃时那个可执行文件和core文件一起启动gdbgdb ./app /var/log/core/core.app.12345.1699999999进入gdb后最简单却最重要的命令就是bt这条命令打印当前线程的完整调用栈输出通常是#0 0x00005555555551a7 in find_user (users0x5555555592a0, count2, target101) at app.c:12 #1 0x0000555555555230 in main () at app.c:25看到崩溃发生在app.c:12也就是users[i].id target这一行再结合循环条件i count问题基本就锁定了。gdb中frame 0切到崩溃帧info locals查看局部变量list显示附近源码都能帮助你进一步确认。实际排查中我比较依赖的gdb命令整理成一张速查表命令作用bt打印调用栈frame N切换到第N个栈帧info locals查看当前函数的局部变量info registers查看CPU寄存器list显示当前行的源码上下文thread apply all bt打印所有线程的调用栈p 变量打印任意可访问的变量值当然gdb能不能看懂源码和函数名前提是编译时保留调试符号。如果线上二进制是strip过的至少要保留函数符号完全strip掉的就只能看到一堆地址分析成本会大很多。3.3 多线程崩溃不能只看出事线程多线程程序的崩溃分析要多留个心眼崩溃线程往往只是“受害者”真正出问题的是另一个线程。举个例子线程A正在使用一个堆上的对象线程B先把这块内存free掉又改了内容A再去访问就到非法地址触发SIGSEGV。表面看起来是A崩溃根因却在B对内存生命周期的管理上。所以进入gdb后的第一件事不要急着看单线程栈先运行thread apply all bt把所有线程的栈都打印出来。然后逐个确认崩溃线程其调用栈指向哪个函数它访问的数据结构由哪个线程创建、按理应该哪个线程释放其他线程此刻在做什么有没有可能和崩溃线程竞争同一块内存。gdb还可以配合info sharedlibrary查看加载的动态库disas反汇编崩溃点附近指令。面对堆损坏、悬垂指针这类问题单靠core文件有时不够还需要配合AddressSanitizer等在程序运行阶段提前暴露问题的工具但在已经崩了的那一刻core文件仍然是信息最全的现场。4. 常见问题与排查技巧4.1 core文件不生成按这个顺序查配置都做了程序也崩了core文件就是没出现这是最让人恼火的场景。我在实际运维里总结了一套排查顺序遇到这种情况就从表格最上面往下查检查项命令常见失败原因shell的core限制ulimit -c还是0说明limits.conf没生效或没重新登录内核core_patterncat /proc/sys/kernel/core_pattern路径指向了不存在的目录目标目录权限ls -ld /var/log/core崩溃进程的用户对目录没有写权限suid程序的转储开关cat /proc/sys/fs/suid_dumpablesetuid进程默认不转储磁盘空间df -h磁盘满或inode耗尽程序自身限制检查代码有没有setrlimit程序运行中改掉了RLIMIT_CORE最后一个容易忽略。程序完全可以在代码里调用setrlimit(RLIMIT_CORE, 0, 0)把系统允许core的限制改成0这会导致所有外层配置全部失效。遇到这种情况要么改代码要么看看有没有其他手段绕开比如用gdb直接attach到进程上手动触发生成core。还有个小技巧如果core_pattern还是系统默认的core那么文件会写到崩溃进程的工作目录很多服务启动时切换到了/或某个只读目录core文件没权限写自然就不会出现。把core_pattern统一改到/var/log/core可以最大程度避免这种问题。4.2 core文件生成却打不开core文件明明在但gdb启动时报not a core file: File format not recognized这种情况我也碰到过很多回。原因无非这么几类第一文件不完整。系统提示core dumped但转储过程中磁盘满了或者被中断core文件写到一半停了。解决办法是先看格式file core.app.12345如果显示ELF 64-bit ... core file说明文件完整如果显示data、ASCII text或者乱码就别用它浪费时间了得想办法重新生成。第二架构不匹配。比如容器里跑的是ARM架构程序core文件拿到x86_64的主机上分析gdb会直接拒绝。跨架构分析通常需要对应架构的gdb或者交叉环境一般不建议。第三core文件和可执行文件对不上。程序重新编译过、代码路径变了、甚至系统glibc版本变了都可能导致gdb无法匹配符号。这时启动gdb后会提示module信息结果里函数名和行号大量缺失只能看到地址。识别这个问题的办法还是file它会显示是哪个可执行文件产生的core和手头的二进制做比对就能发现差异。4.3 磁盘被core文件塞满的预防措施生产环境开启core dump之后最怕的就是次生灾害——磁盘被写满。一个Java服务或者大型C服务动辄几个GB崩溃时全部落盘要是没监控整个系统都可能被拖垮。我的建议是三层防护第一层限制core大小。在limits.conf里给core设置上限比如2GB超过就不再写。第二层单独划分目录并做空间隔离。把/var/log/core独立成一个分区从而避免把系统根分区填满。第三层定期清理。写一条find命令加入cron或者systemd timer删除过期corefind /var/log/core -name core.* -type f -mtime 7 -delete定期清理虽然简单但特别容易被忽略。我看到很多线上机器一旦开放core几天就堆上几十GB的core文件磁盘空间莫名告警查半天发现全是旧core惹的祸。另一个建议是设置core文件权限。core目录权限用0750或者1700都可以避免其他低权限用户读到进程的内存内容。如果你们的合规要求很严还可以结合加密盘存放core文件。4.4 容器和内核新参数带来的坑容器场景下的core dump要复杂得多。容器里跑的进程其崩溃转储最终多半受宿主机内核参数控制因为core_pattern是全局的。比如宿主机把core_pattern设成了|/usr/lib/systemd/systemd-coredump容器内进程的core会被宿主机的systemd接管容器内怎么看都找不到。遇到这种情况不要愣在容器里查去宿主机上用coredumpctl list看看或者查宿主机上的core目录。反过来如果宿主机的core_pattern是直接落盘路径但路径在宿主机上容器里无法访问最终core可能写到宿主机某个目录权限还是root。新内核还有一个参数值得关注kernel.core_pipe_limit它控制允许并发转储到管道处理的进程数。多个进程同时崩溃时超过上限的都会被丢弃表现为“有的core有有的core没有”。这个参数在不同发行版默认值还不一样排查时别忘了看一眼。容器镜像启动脚本里最好也显式设置一下core限制因为docker run可以传--ulimit core0把容器内core关掉有些基础镜像本身可能也限制了RLIMIT_CORE。正确的思路是在镜像的entrypoint脚本里先ulimit -c unlimited再把core_pattern改成启动进程可写的位置保证每个环境行为一致。我在实际项目中处理过不止一次因为容器环境导致core“神秘消失”的问题印象最深的一次是service容器里死活找不到core最后发现宿主机开了systemd-coredump数据全被压进了宿主机的coredump仓库。从那以后我对所有生产主机都会提前确认一遍core_pattern不默认依赖容器或宿主机策略。最后再说说我的个人习惯开发机和测试机core大小不限制方便发现问题时快速定位生产机限制大小独立目录存放定时清理配合监控告警。工具层面我更偏爱直接落盘加grep文件名的老派方式原因很简单直接落盘的文件可以上传到分析机或者长期归档而systemd-coredump虽然带索引但在特殊环境里的兼容性和可迁移性反而没有独立文件灵活。配置一次受益很久强烈建议每个团队都把core dump这套能力提前铺好别真等到线上事故来了才临时抱佛脚。