资讯详情

Android内核空指针崩溃实战:从日志到源码的定位方法

📅 2026/10/13 23:51:19 | 华诺云谱 👁 阅读
Android内核空指针崩溃实战:从日志到源码的定位方法
做Android内核调试这些年被凌晨的崩溃日志叫醒是常有的事。前两天又处理了一起典型的空指针问题从拿到日志到定位根因花了不到一个小时整个过程没有特别高深的技术就是把日志读细、把符号环境对齐、把反汇编看完。空指针在内核里出现频率极高尤其是驱动代码集中的区域每次版本迭代几乎都能撞见几例。这篇文章把这个case从原理到定位的完整路径整理出来日志怎么读、PC指针怎么解、源码怎么对上、修复怎么写全是实际操作验证过的方法希望能帮你把排查时间压缩下来。1. 内核空指针到底是怎么发生的1.1 一次典型的崩溃现场还原先说这次的具体场景。某款量产机型在跑压力测试时出现内核panic抓到的log是典型的空指针访问[ 123.456789] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000 [ 123.456812] Mem abort info: [ 123.456823] ESR 0x96000005 [ 123.456831] EC 0x25: DABT (current EL), IL 32 bits [ 123.456842] SET 0, FnV 0 [ 123.456851] EA 0, S1PTW 0 [ 123.456859] Data abort info: [ 123.456868] ISV 0, ISS 0x00000005 [ 123.456878] CM 0, WnR 0 [ 123.456887] user pgtable: 4k pages, 48-bit VAs, pgdp00000000abcd1234 [ 123.456899] [0000000000000000] pgd0000000000000000, pte0000000000000000 [ 123.456913] CPU: 2 PID: 857 Comm: kworker/u16:4 Tainted: G W 4.19.75 [ 123.456925] Hardware name: SomeBoard (DT) [ 123.456933] pstate: 80400005 (Nzcv daif PAN -UAO) [ 123.456943] pc : foo_irq_handler0x34/0x80 [my_driver] [ 123.456952] lr : __handle_irq_event_percpu0x60/0x178 [ 123.456963] Call trace: [ 123.456971] foo_irq_handler0x34/0x80 [ 123.456979] __handle_irq_event_percpu0x60/0x178 [ 123.456988] handle_irq_event_percpu0x2c/0x78 [ 123.456996] handle_irq_event0x54/0xa0 [ 123.457004] handle_irq_event0x54/0xa0 ...一眼扫过去崩溃点在foo_irq_handler0x34这是一个自定义驱动的中断处理函数。为什么中断处理里会解引用空指针这是很多空指针问题的常见面貌中断触发时某些资源还没准备好或者已经被释放代码却直接拿过来用。1.2 空指针背后的硬件机制空指针在ARM64上真正触发时是CPU尝试访问虚拟地址0x0MMU在页表里找不到对应的映射于是产生一个data abort异常。内核接收到这个异常后判断当前是内核态访问非法地址直接走oops/panic流程打印出上面那一段现场信息。这里有一个关键点地址0本身并不是什么删除或标记的状态它就是一块没有映射的虚拟地址。代码访问它硬件层面和访问一个随机的非法地址没有本质区别只是程序员的直觉上NULL通常是这个指针还没有指向任何有效对象的意思。用户态程序遇到段错误进程崩了就完了内核态遇到空指针整个系统陪你一起崩。Android设备尤其敏感因为系统几乎不允许内核空指针继续存活——通常在打印完调用栈后直接panic并重启避免脏数据继续写入存储。2. 崩溃日志的正确读法2.1 先抓五行关键信息拿到日志不要急着翻调用栈先看前几行。Unable to handle kernel NULL pointer dereference说明异常类型at virtual address 0000000000000000说明访问的目标地址就是0pc和lr是两个最重要的寄存器分别代表当前执行位置和函数返回地址。接着看CPU: 2 PID: 857 Comm: kworker/u16:4。这三样信息经常被忽略但它们的价值非常高CPU号告诉你崩溃发生在哪个核上PID和Comm告诉你当前是谁在执行。如果Comm是kworker或者irq/xxx间接说明问题是异步触发的——不是某个进程主动调用而是内核工作队列或中断上下文里出的岔子。本次就是这样中断上下文里崩了进程上下文根本来不及反应。看到Tainted: G W也要留意W表示有警告产生过G表示模块是GPL的。如果看到Tainted: P或者OE说明加载了专有模块或者有oops发生过这些信息在评估问题时能帮你判断是不是外部模块搞的鬼。2.2 pc与lr的解读逻辑pc那一行的格式是函数名偏移/函数总大小比如foo_irq_handler0x34/0x80意思是崩溃点位于foo_irq_handler这个函数内部从函数起始地址算起偏移0x34字节函数总大小0x80字节。0x34在0x80范围内说明确实在函数体内。这个偏移信息是整个定位过程中最关键的入口。拿到0x34后配合vmlinux符号表可以直接换算到具体的源码行号也就是后面要讲的addr2line操作。lr寄存器同样重要。它是CPU在跳进当前函数之前保存的返回地址。如果pc所在的函数加了inline或者被优化得面目全非lr能帮你看清是从哪一层调用过来的。再配合完整的Call trace整个调用链就出来了__handle_irq_event_percpu调用了foo_irq_handler所以这个驱动的中断处理是注册在常规中断框架里的不是乱七八糟的私有路径。2.3 地址直接显示为十六进制时怎么办有些日志里pc不显示符号名而是裸地址比如pc : 0xffffff8009123456。这种情况通常出现在内核符号表不完整、kallsyms被裁剪或者模块没有加载符号的时候。处理办法有两条路一是查kallsyms。内核编译开了CONFIG_KALLSYMS的话/proc/kallsyms里能看到符号和地址的对应关系但在Android设备上通常被权限限制只能拿到全零地址。量产机上这条路多数不通。二是在主机端用vmlinux解析。先把vmlinux导入gdb或者objdump再根据日志里的PC地址减去KASLR偏移就能定位到具体函数。关于KASLR偏移怎么算后面专门讲这里先记住有这回事。提示拿到日志后先确认工具链版本和内核源码版本能对上vmlinux必须是这次崩溃对应的那个编译产物。版本对不上后面的解析全部白费。3. 从裸地址到源码行号完整定位实操3.1 准备符号解析环境解析内核崩溃地址最可靠的方式是用编译内核时生成的vmlinux。普通Android kernel发布包只带Image或Image.gz不带vmlinux所以需要自己从源码编译或者找构建服务器上保留的产物。vmlinux包含完整的符号表和调试信息这是后续所有操作的根基。工具链上如果内核是用GCC编的用aarch64-linux-android-addr2line如果用Clang编的用llvm-addr2line。Android平台近几年的内核基本上都是用Clang编的所以我习惯直接用llvm那套。下面以我这次的case为例# 假设vmlinux已经解压到工作目录 aarch64-linux-android-addr2line -e vmlinux -f -C 0xffffff8009123470但实际问题没有这么简单日志里给的是foo_irq_handler0x34这种形式直接拿这个offset去addr2line是解不出来的因为addr2line要的是虚拟地址。所以需要先把偏移换算成绝对地址找到foo_irq_handler在vmlinux里的起始地址加上0x34。3.2 用nm找到函数起始地址# 用nm在vmlinux里找foo_irq_handler的地址 aarch64-linux-android-nm vmlinux | grep foo_irq_handler输出大概是这样ffffff8009123440 t foo_irq_handlert表示该符号在text段内地址是ffffff8009123440。加上日志里的偏移0x34ffffff8009123440 0x34 ffffff8009123474把这个地址喂给addr2lineaarch64-linux-android-addr2line -e vmlinux -f -C ffffff8009123474正常情况下会输出foo_irq_handler /path/to/kernel/drivers/xxx/foo_irq.c:217一行源码文件一行行号直接锁定了问题点。3.3 跳过手工计算的捷径faddr2line每次都要先nm找地址再手工加偏移做多了就觉得麻烦。内核源码树里自带一个脚本scripts/faddr2line专门干这个活。它的用法简洁得多./scripts/faddr2line vmlinux foo_irq_handler0x34这个脚本会自动解析符号地址、计算偏移然后调用addr2line输出文件行号。实测下来它对ARM64的vmlinux支持得不错唯一要注意的是它对CONFIG_DEBUG_INFO有依赖如果编译内核时没开-g脚本会直接报错提示缺少调试符号。之前有人问过我用gdb行不行。当然行gdb加载vmlinux后执行(gdb) list *foo_irq_handler0x34也能直接列出对应的源码行。但在批量分析多个崩溃点时faddr2line的脚本输出更规整适合写进自动化分析流程里。3.4 反汇编确认根因拿到行号不等于完事还得确认为什么这一行会访问空指针。我用addr2line定位到了foo_irq.c:217打开源码一看static irqreturn_t foo_irq_handler(int irq, void *dev_id) { struct foo_dev *fdev dev_id; // 217行附近 struct foo_regs *regs fdev-regs; ... }问题一目了然注册中断时dev_id传进来的是NULL中断一触发fdev-regs就在解引用空指针。root cause不在中断处理函数本身而在中断注册的地方。所以我还需要反汇编来验证执行流确实走到了这里aarch64-linux-android-objdump -d vmlinux --start-address0xffffff8009123440 --stop-address0xffffff80091234c0输出中能看到崩溃那条指令是ldr x0, [x19, #0x10]其中x19里装的就是那个NULL的dev_id。反汇编的价值在于它能把编译器优化后的真实执行序列摆出来避免被源码的假象误导。有时候源码看起来访问的是A变量实际由于编译器重排崩在B变量的加载上。3.5 修复与回归验证确认根因是中断注册时传参为NULL后修复分两层。第一层是调用侧检查注册中断前确认设备对象已创建成功失败就不注册。第二层是处理侧防御static irqreturn_t foo_irq_handler(int irq, void *dev_id) { struct foo_dev *fdev dev_id; if (!fdev) return IRQ_NONE; ... }防御性判空会被人批评掩盖问题但在中断上下文这种不能轻易睡眠、不能上复杂锁的环境里判空后返回IRQ_NONE是合理的兜底策略。真正的修复重点应该放在注册路径上让NULL不可能出现。修复后重新编译内核跑同一套压力测试确认问题复现不出来。同时抓一份正常情况下的日志对比中断注册流程确保不是碰巧掩盖。注意修空指针最重要的不是那一行判空而是搞清楚空值是从哪条路径传进来的。只堵住崩溃点源头不改换个姿势还会崩。4. 高频空指针场景与排查思路4.1 驱动probe阶段的“早产”解引用驱动probe阶段是空指针高发区。常见写法是这样static int foo_probe(struct platform_device *pdev) { struct foo_data *data; struct device_node *np pdev-dev.of_node; data devm_kzalloc(pdev-dev, sizeof(*data), GFP_KERNEL); >
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑