ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

安卓内核崩溃日志导出指南:pstore、last_kmsg与ramdump实战

安卓内核崩溃日志导出指南:pstore、last_kmsg与ramdump实战

干安卓底层调试这一行,最让人血压飙升的时刻,不是崩溃本身,而是设备“啪”一下重启,然后你翻遍所有日志文件,发现自己什么都抓不到。内核崩溃日志(kernel panic log)就是这么个玩意——它就像飞机失事后的黑匣子,明明在里面记着最关键的事故原因,可你要是不知道怎么把它“取出来”,重启一次就全没了。

这篇文章我想聊聊“安卓导出内核崩溃日志”这件事。先说结论:在不同场景下,导出崩溃日志的路径完全不一样。设备还没死透有没死透的抓法,设备已经重启死了有死重启的捞法,开发板、工程机、量产机又各有各的门道。这篇主要面向做了几年系统或应用开发、开始遇到“无规律重启” “死机后无日志”这类问题的同学,也顺带照顾一下刷机爱好者——你会明白那些第三方内核日志工具到底在帮你读什么文件。

1. 崩溃日志去哪儿了:先搞懂死机那一刻系统的处理链路

在动手敲adb命令之前,我非常建议你先花十分钟搞清楚一件事:内核崩溃的时候,Linux内核到底把日志写到了哪里,以及重启后这些日志凭什么能留下来。这个底层逻辑一旦通了,后面所有命令都能自己推导出来,而不是死记硬背。

1.1 内核panic和普通APP崩溃,根本是两套处理机制

很多人有个误解,觉得“记录崩溃日志”是系统默认就有的能力,就像APP崩溃会有tombstone一样。但事实上,用户态APP崩溃和内核态崩溃走的完全是两条路。

APP崩溃时,系统里的debuggerd守护进程会被唤醒,帮你把进程的调用栈、内存映射、寄存器的信息整理好,写到/data/tombstone/目录下,或者通过logcat输出一条大段的崩溃信息。这个过程是“有人在场记录的”,有进程、有文件系统、有文件描述符,一切都好商量。

但内核panic就不一样了。内核是系统里最底层的管理者,当它自己出了问题——比如空指针解引用、内存越界、锁死——它是没法借助任何普通进程去落盘的,因为这时候进程调度器可能都已经罢工了。内核能做的,只是在屏幕上打印一堆字符,然后要么死等(panic不重启),要么立刻重启。这些打印出来的字符,如果没人提前布置“后手”,就会随着内存断电瞬间清零,变成永远的谜。

所以,导出内核崩溃日志的核心思路,其实只有一个:在内核陷入混乱、无法正常写入文件系统之前,把日志放进一块“能在重启后存活下来的地方”。

1.2 日志暂存的位置:ring buffer、预留分区与重启后幸存差异

内核日志在正常运行时会持续写进一个环形缓冲区(ring buffer),上限通常是256KB或按内核配置来。平时你用dmesg读到的就是这块内存里的内容。这个缓冲区运行速度极快,但它有两个致命弱点:重启后清零,还有容量满了以后会被新日志覆盖。

真正能为“崩溃后导出”服务的,是下面这几类位置:

存储位置是否依赖文件系统重启后是否存活典型读取方式
内核dmesg ring buffer否,纯内存否(清零)崩溃前dmesg抓取
pstore/ramoops保留内存区否,特定保留RAM是(复位后不清理)重启后/sys/fs/pstore
last_kmsg引导区残留依赖启动流程实现部分设备支持重启后/proc/last_kmsg
ramdump完整内存镜像否,通过底层接口导出是(在调试机/工程机)离线解析镜像文件

你可以把pstore理解成一块“专门留给灾难现场的内存”:系统正常时,这块内存空着不用;一旦系统崩溃重启,引导阶段会检测到这块内存里有上次留下的内容,于是把它挂载成一个文件系统,让你能读到上一次崩溃的日志。这比拯救一个完整的文件系统要可靠得多,因为这块内存是为极端情况设计的,不需要文件系统参与。

1.3 为什么有的日志重启就没了:从Android启动流程说起

Android设备重启时,引导程序会重新初始化内存控制器、DDR、各种外设。按常理,内存一上电复位,里面的数据就应该全部乱掉。所以普通内存里的日志,重启后必然没戏。这里唯一的例外,就是那些“故意不被重置”的保留区域——它们的物理地址被提前标注出来,引导流程里即使看到这块区域有数据,也不会去抹掉。

很多设备量产之后,厂商并不会打开pstore相关的内核配置,因为这会占用一部分调试内存,并且还要做一些权限上的协调。于是你在市面上买到的非root设备,崩溃之后重启,基本上就是“干净利落地失忆”。这也是为什么“导出内核崩溃日志”这件事,在纯量产机上往往要先过root这一关。

2. 设备还没挂时怎么抓:三类应急捞日志的手段

如果设备现在正处于“频繁卡死但还没完全死透”的状态,或者你正把设备放在测试台上做压测,那你有的是时间主动出击。下面是三种最常用的应急抓取方案,按“硬核程度”从高到低排。

2.1 串口抓日志:开发阶段才用得上的硬管道

如果你的设备是开发板、工程机,而且主板上留有调试串口焊点,那串口日志就是最原始也最可靠的方案。内核的早期启动日志、panic瞬间的打印,甚至比pstore还要全,因为它能看到从bootloader到内核挂掉的完整过程。

实操上你需要一根USB转串口线(常见的是3.3V TTL电平),连接板上标记为TX、RX、GND的三个测试点。通常TX连线材的RX,RX连线材的TX,GND对GND。波特率常见的有115200和921600,具体以平台资料为准。

连上以后,在电脑端用minicom或screen打开串口:

# 安装minicom(以Debian/Ubuntu系为例) sudo apt install minicom # 配置端口和波特率,然后打开 sudo minicom -s

配置里选好/dev/ttyUSB0,波特率选115200,8N1,无流控。打开之后你就会看到一个不断滚动的系统打印流。如果你用screen,命令更简单:

sudo screen /dev/ttyUSB0 115200

在这个终端里,系统每打一行字你都能实时看到。panic出现的那一刻,最后的那些打印就是第一手证据,比事后去翻文件还直接。

但要提醒一句:串口抓日志只适合开发阶段的板子,量产机上基本没有引出调试串口,你也很难飞线。所以这个方法对多数人来说属于“认识一下即可”,真正天天用的还是下边的方案。

2.2 sysrq触发:让内核在崩溃边缘交底细

Linux内核里藏着一个“魔术快捷键”——SysRq,Android上虽然不方便按键盘,但可以通过往/proc/sysrq-trigger里写字符来触发对应的内核响应。

这个机制最妙的地方在于:即使系统已经明显卡顿,甚至界面死了,只要内核的进程调度部分还活着,写sysrq-trigger通常仍然有效。因为它走的内核处理路径非常短,不需要用户态进程配合。

平时你可以用几个关键的字符来获取快照:

# 查看当前系统状态、CPU负载等 adb shell "echo l > /proc/sysrq-trigger" # 显示所有CPU上的任务栈(最常用,能看到大家在卡在什么内核函数上) adb shell "echo t > /proc/sysrq-trigger" # 显示当前正在使用内存的进程和内存信息 adb shell "echo m > /proc/sysrq-trigger"

这些输出默认会打进内核日志,所以紧接着你就可以用dmesg把它们捞出来。如果你在做压测,可以在脚本里每隔10秒触发一次t,然后把dmesg持续存到电脑上。等设备真的崩溃了,至少你能知道“死之前最后一刻各个进程在内核里做什么”。

需要注意,Android为了安全,默认把sysrq功能收敛得很紧。你需要在adb shell里先确认并打开开关:

# 查看当前sysrq权限掩码 cat /proc/sys/kernel/sysrq # 如果需要,设为1表示允许所有功能 echo 1 > /proc/sys/kernel/sysrq

这个改动重启后就失效,所以适合临时调试。如果你需要长期开启,就得改内核启动参数或init配置了。

2.3 adb持续抓取dmesg与内核logcat:应用面的临时方案

如果设备还活着,最直接的抓取方式就是adb连着持续捞日志。你可以开两个终端,一个捞dmesg,一个捞logcat,双管齐下。

# 捞内核日志,-w 表示持续等待新输出 adb shell dmesg -w > kernel_log_$(date +%Y%m%d_%H%M%S).txt # 很多设备上内核日志也会输出到 logcat 的 kernel buffer adb logcat -b kernel -v threadtime > kernel_logcat.txt # 常规应用日志也别放过 adb logcat -v threadtime > app_logcat.txt

这里dmesg -w有没有权限读完内核环形缓冲区,取决于系统是否允许所有用户访问。通常root设备没问题,非root的某些设备会限制读取。

不过这类方案有一个很现实的坑:dmesg环形缓冲区本身容量有限,如果系统不崩溃但一直有大量内核log在刷,旧的崩溃信息可能早就被冲掉了。所以这种方式更适合“设备处于亚健康状态,你预期它可能随时崩”的场景,不适合“已经崩完重启”的场景。

3. 设备重启后怎么捞:pstore、last_kmsg与ramdump三条路径

设备已经重启,前面的招数全部失效,这时候能救你的就是“复位后仍存留”的日志通道。按现代程度和可操作性,我建议你按 pstore → last_kmsg → ramdump 的顺序去尝试。

3.1 pstore:现代安卓最常规的“黑匣子”

从Android 8.0时代开始,主线内核基本都用pstore框架来承接崩溃日志。具体实现上常见的是ramoops后端,也就是我们前面说的:在内核启动参数里预留一小段内存,专门存放崩溃现场。

检查你的设备支不支持pstore,开机后直接看:

adb shell ls /sys/fs/pstore/ # 常见的文件有: # console-ramoops-0 (串口console最后一段输出) # dmesg-ramoops-0 (崩溃时的内核环回缓冲,最重要) # pmsg-ramoops-0 (来自用户态的持久化消息)

如果设备支持,内容通常已经自动挂载好了。你可以直接把这些文件拉回电脑:

mkdir -p pstore_export adb shell "cat /sys/fs/pstore/dmesg-ramoops-0" > pstore_export/dmesg_ramoops.txt adb shell "cat /sys/fs/pstore/console-ramoops-0" > pstore_export/console_ramoops.txt

有时候设备会把pstore挂载在/sys/fs/pstore,但读取时因为SELinux策略限制出现Permission denied。这种情况要么在root下临时关闭SELinux(setenforce 0),要么在支持的工程固件里直接读。

你拿到的dmesg-ramoops-0文件,如果里面全是对不上的内存地址,别急着嫌它烂——它恰恰是崩溃现场最完整的记录。后面第4节会讲怎么从里面提炼关键信息。

3.2 last_kmsg:老内核的备忘录,新设备为什么没了

如果你用的是一台比较老的设备,崩溃重启后很可能存在/proc/last_kmsg这个文件。翻译过来就是“上一次的内核日志”。

在老内核里,有些平台的启动流程会在系统重启后、重新初始化内存前,把上一次残留的内核日志拷到某个内存位置,并在启动后把它暴露成/proc/last_kmsg。所以重启后你读这个文件,相当于读“上一次死前”的内核日志。

adb shell cat /proc/last_kmsg > last_kmsg.txt

但新设备上这个文件基本上已经不存在了,原因就是pstore框架取代了这种bootloader配合的方案:pstore能存更多内容、不依赖特定bootloader实现、也更统一。如果你在设备上找不到/proc/last_kmsg,不用折腾,直接回到pstore那一步。

值得留意的是,有些第三方ROM还保留着last_kmsg的兼容节点,或者把pstore里的内容又映射了一份到这里。所以两条路都试一下,没有坏处。

3.3 ramdump:真·整内存兜底,留给最棘手的场景

在系统工程师圈子里,“拉ramdump”是一个重量级操作。它不是在系统里执行一个命令就完事,而是让设备在崩溃时进入一个特殊的下载模式,把整片DDR内存镜像通过USB导出到电脑。pstore只能给你内核日志,ramdump能给你崩溃那一刻“整台设备的内存快照”,包括所有进程的用户态栈、内核栈、寄存器、堆数据。

理解ramdump之前,你得知道它为什么存在。有些死机问题,光看内核日志根本找不到凶手,因为“病灶”发生在某个进程的用户态内存里,或者一块已经被释放的堆内存中。只有拿到完整的内存镜像,才能做深度的离线分析。

普及型的操作流程大概是:

  1. 设备先开启“ramdump模式”引导选项(通常在工程机/调试固件里才有)。
  2. 设备崩溃后,不再正常重启,而是停留在等待下载的端口模式。
  3. 电脑上用厂商提供的解析工具(通常是基于某种私有协议的工具集)连接设备,导出*.bin或*.mTP格式的镜像文件。
  4. 拿镜像文件配合符号表,在离线环境里解析出崩溃调用栈。

需要说明一点:ramdump对普通开发者来说,门槛很高,因为能开这个功能的设备很少;对刷机玩家来说,更是基本见不到。它更像是整机厂商内部和深度系统开发才会用到的“最后兜底方案”。如果你在开发应用,遇到无规律崩溃,先优先pstore;pstore实在拿不到,再考虑判断自己有没有条件走ramdump这条路。

4. 日志到手后如何定位:识别panic现场的关键字段与还原调用栈

导出日志只是第一步,真正让人头大的是拿到一份满是内存地址的文本,怎么从里面看出是谁闯的祸。这一节是干货中的干货,我按“先找现场、再还原栈、再对号入座”的顺序讲。

4.1 从“PC is at”到“Call trace”:崩溃现场的固定句式

你看一份内核崩溃日志,不需要全文通读,先找这几个“标志性段落”:

第一条线:搜索包含panic、Kernel panic - not syncing、Unable to handle kernel、Internal error的行。这些是崩溃事件的“标题”。

第二条线:搜索PC is at、LR is at、还有Call trace附近的段落。PC寄存器指向的是发生异常时CPU正在执行的指令地址,LR是函数返回地址。这两行直接告诉你“崩溃执行到了哪一行代码”。

举个例子,日志里常见的这一段:

Unable to handle kernel NULL pointer dereference at virtual address 0000000000000008 Mem abort info: ... PC is at foo_worker+0x24/0x98 [kernel] LR is at bar_dequeue+0x1c/0x44 [kernel] ... Call trace: foo_worker+0x24/0x98 worker_thread+0x1e8/0x450 kthread+0x118/0x120

Unable to handle kernel NULL pointer dereference只是告诉你类型是空指针解引用;virtual address 0000000000000008说明代码尝试访问地址0x8附近的偏移量,通常是某个结构体指针为空,然后访问了它的某个偏移字段。PC is at foo_worker+0x24/0x98则明确指认了:崩溃发生在foo_worker函数内部、距离函数起点偏移0x24字节处。

这时候,下面的Call trace就是“崩溃时的函数调用链”。它从最底层往上排列,最下面通常是线程入口(worker_thread、kthread),最上面是崩溃点。这条链的价值在于:你可以看到崩溃点是谁一路调用过来的,从而判断触发路径。

4.2 调用栈还原:符号表、System.map与在线反查

看到Call trace里带着函数名,是因为内核启用了一个叫kallsyms的选项,能把运行时地址翻译成函数名。可并不是所有设备都会开放完整的符号表,有些函数会显示成foo_worker+0x24/0x98这样——函数名还在,但你要是想精确到“是哪一行C代码”,还差一步。

做到代码级定位,你需要两样东西并行:崩溃日志里记录的PC地址,以及和你设备当前固件对应的符号文件。对整机调试来说,这个符号文件通常是带调试信息的vmlinux或单独的符号表文件。有了它,再用addr2line这一类的工具就能把地址翻译成源文件+行号:

# 假设备选为C库路径,架构为aarch64 aarch64-linux-gnu-addr2line -f -C -e vmlinux 0xffffff8000123456

你需要把日志里PC/LR地址换算成“相对符号基址”的偏移,这个过程不同平台工具链不太一样,但思路一致:在符号表里找到对应函数,再把偏移和反汇编对应上。

如果没有官方符号文件,也别直接放弃。你还可以结合设备上导出的/proc/kallsyms(需要root)来获取运行时符号地址,把日志里的PC地址与之对齐:

adb shell "cat /proc/kallsyms" > kallsyms.txt # 然后在本机搜索崩溃PC地址附近最近的符号 grep -B1 -A1 "ffffff8000123455" kallsyms.txt

这个方法虽然粗糙,但很多情况下能帮你锁定“到底是哪个驱动函数崩的”。从实战角度看,锁定到函数级别已经能解决80%的问题,剩下20%才需要追到源码行号。

4.3 对号入座:常见崩溃类型的日志长相

不同类型的内核崩溃,日志一定有各自的特征。我整理一个对照表,你拿到日志后可以先按表对号入座:

崩溃类型关键日志特征常见原因
空指针解引用Unable to handle kernel NULL pointer dereference驱动未判空就访问成员
野指针/非法地址Unable to handle kernel paging request at virtual address内存已被释放但仍在使用
内核栈溢出Kernel stack overflow/BUG: stack guard page was hit递归调用过深、栈分配过小
死锁BUG: scheduling while atomic/possible circular locking中断上下文里睡眠、锁顺序相反
内存越界伴随KASAN或UBSAN的异常行数组越界、越界拷贝
看门狗超时watchdog: BUG: soft lockup - CPU#N stuck某CPU长时间关闭中断或死循环

举个例子,你看到BUG: scheduling while atomic,那不用犹豫,大概率是某个驱动在自旋锁保护区域内调用了可能睡眠的函数(比如kmalloc带GFP_KERNEL标志,或直接调用了msleep)。这类问题在内核驱动的并发场景里非常常见。

再看watchdog: BUG: soft lockup,这表示某个CPU在一段时间内没“喘气”,通常是关中断后在等待某个永远不会来的硬件响应,或者在内核态死循环了。日志里通常会列出卡住的CPU号和当前任务名。

把崩溃类型弄清楚了,再回看PC和Call trace,定位就会快很多。不要一上来就纠结每一行都读懂——内核日志里90%的噪声都可以忽略,抓住这几个关键段落就够了。

5. 多次导出实测中的几个坑与准备工作

最后这部分,我想分享一些在一次次“重启后发现没抓到”的经历里踩出来的坑。这些细节如果你不提前知道,很可能会在关键时刻白忙一场。

5.1 热重启与冷重启:日志幸存条件完全不同

这里的“热重启”指系统软重启(不切断DDR供电的reset),冷重启则是整机断电再开机。部分设备只在热重启后才保留pstore内容,因为你一旦拔电池或者长按电源键让硬件彻底断电,那块保留内存的供电也跟着断了,数据就没了。

所以做崩溃复现实验的时候,最好别急着“断电大法”。设备死机后,先等几秒,让它自己走完重启流程,再检查pstore。在某些工程机上,如果你在panic后强制长按电源键强制关机,日志就蒸发了。

额外补一条:有些平台在系统重启后,pstore内容还会被标记为“已消费”,第二次重启前如果你已经读过了,就可能被清除。所以一旦发现设备重启,优先做的事就是马上读取pstore,别等。

5.2 root权限与SELinux:拿到pstore前先过安全限制

pstore的读取权限,在严格模式下往往只给root或特定系统进程。你用adb shell的普通shell用户去cat,大概率碰到Permission denied。如果你的设备已经root,直接:

adb root adb shell "cat /sys/fs/pstore/dmesg-ramoops-0"

如果没有root,但设备解锁了bootloader,你可以刷一个临时的userdebug boot镜像,通常也能获得足够的权限。更麻烦的情况是SELinux enforcing状态把/sys/fs/pstore的访问策略卡死了,这时:

adb shell su -c "setenforce 0"

再试读取,读完后记得把SELinux恢复成enforcing,避免影响其他测试。

这里的经验是:不要把“所有设备都能轻易导出内核日志”当作默认前提。做调试前,先花半小时确认当前设备有哪些调试权限,否则真出事时才发现处处受限,就太晚了。

5.3 我更推荐的日常准备:一个预防性的崩溃日志导出方案

与其每次崩溃后慌慌张张地找日志,我更建议你在开始长时间测试前,花十分钟把环境布置好。下面是我自己常用的准备清单:

  1. 确认设备的pstore节点存在,并且当前用户能读取。
  2. 打开sysrq,方便在设备卡死时手动触发任务栈快照。
  3. 备好adb脚本,把pstore、dmesg、logcat三路日志同时落盘。
  4. 如果设备支持热重启自动保留pstore,就不要轻易强制断电。
  5. 准备一台能连串口的板子时,提前用串口工具验证过链路,别等到崩溃时才发现波特率不对。

一个实用的启动脚本大概是这样的:

#!/bin/bash # save_kernel_logs.sh DEVICE_SERIAL=$1 OUT_DIR="crash_logs_$(date +%Y%m%d_%H%M%S)" mkdir -p "$OUT_DIR" adb -s "$DEVICE_SERIAL" shell cat /sys/fs/pstore/dmesg-ramoops-0 > "$OUT_DIR/pstore_dmesg.txt" adb -s "$DEVICE_SERIAL" shell cat /sys/fs/pstore/console-ramoops-0 > "$OUT_DIR/pstore_console.txt" adb -s "$DEVICE_SERIAL" logcat -d -v threadtime > "$OUT_DIR/logcat.txt" adb -s "$DEVICE_SERIAL" shell dmesg > "$OUT_DIR/dmesg.txt" echo "Logs saved to $OUT_DIR"

你可以在脚本里再加一层判断:如果pstore文件不存在,就直接跳过并打印提示,避免中断整套流程。

最后再留一个我自己的体会:导出日志这件事,看着简单,但真正稳定可靠的方案,从来都是“提前准备出来的”,而不是“事后灵机一动想出来的”。我见过太多人在设备崩溃重启后才发现自己连root权限都没有,或者压根不知道这块芯片支持哪种日志导出方式。提前把那五个准备步骤走一遍,你的调试效率能直接翻倍,而且能少掉不少头发。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进