
在内核开发中,编写代码并不是最困难的部分。真正的挑战其实是调试问题,即使是经验丰富的开发者也会为此感到头疼。实际上,很多调试工具本身就是内核功能的一部分。当程序出错时,内核会通过名为 "Oops" 的错误信息帮助定位问题原因,而调试的关键就是仔细分析这些错误信息。
什么是内核 oops?
当 Linux 内核遇到严重错误(称为 "kernel panic")时,会立即显示 Oops 信息。这些信息包含当前 CPU 寄存器的数值、程序运行时的堆栈内容,以及发生错误时的函数调用路径,帮助开发者快速定位问题。
大部分错误源于错误地使用空指针(NULL 指针)或无效地址。这类问题通常会触发 Oops 提示。处理器处理内存时,会把程序使用的虚拟地址通过"页表"映射为物理地址。当程序访问非法地址时,系统无法完成地址转换,处理器就会发出"页面无效"警告。如果此时程序在最高权限(内核模式)运行且无法恢复,系统就会触发 Oops。
以下是触发 Oops 的常见场景:
64 位系统调用出错时会直接显示 Oops
CPU 进入异常工作模式且对应的异常处理程序直接触发 Oops
程序主动调用内核中的 BUG() 调试函数时触发 Oops
内核程序访问非法内存地址(如未初始化的指针)时触发 Oops
简单来说,Oops 就是内核遇到无法处理的致命错误时的"自检报告",通过显示关键运行状态帮助开发者排查问题。
一、OOPS 日志分析
OOPS 是 Linux 内核在发生错误或未处理的异常时打印的消息。 它尽最大努力描述异常,并在错误或异常发生之前转储调用堆栈。以下内核模块为例:

在前面的模块代码中,我们试图取消引用空指针,以使内核恐慌。 此外,我们使用 noinline 属性以使 create_oops() 不是内联的,从而允许它在反汇编期间和调用堆栈中显示为一个单独的函数。

以下是针对内核 Oops(崩溃)信息的简化解释:
核心错误信息
[29934.977983] Unable to handle kernel NULL pointer dereference at virtual address 00000000- 问题:代码尝试访问一个无效的
NULL指针(即地址00000000),导致内核崩溃。
崩溃发生的位置
[29935.214354] PC is at create_oops+0x18/0x20 [oops]PC(程序计数器):当前执行的指令位于
create_oops函数中。函数细节:
create_oops函数总长0x20(32 字节),崩溃发生在距离函数起始地址0x18(24 字节)的位置。
崩溃的调用来源
[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops]LR(链接寄存器):记录了调用
create_oops的函数是my_oops_init。位置:
my_oops_init函数的第0x18字节(24 字节)处调用了create_oops。
寄存器的十六进制值
[29935.224068] pc : [<bf2a8018] lr : [<bf045018] psr: 60000013pc:崩溃时的程序计数器地址(
bf2a8018)。lr:调用返回地址(
bf045018),即崩溃后若能恢复,会跳转到此处继续执行。
堆栈信息
[29935.224068] sp : cc66dda8 ip : cc66ddb8 fp : cc66ddb4sp(堆栈指针):指向当前堆栈的顶部位置(
cc66dda8)。fp(帧指针):记录函数调用前的堆栈位置(
cc66ddb4),帮助函数返回时恢复堆栈状态。
其他寄存器的值
[29935.247359] r3 : 00000000 r2 : a6af642b r1 : c05f3a6a r0 : 00000014- 这些是 CPU 寄存器的当前值(如
r0到r3),记录了崩溃时的数据状态,用于调试。
发生崩溃的进程
[29935.266822] Process insmod (pid: 20021, stack limit = 0xcc66c208)- 进程信息:崩溃发生在进程
insmod(进程 ID 为20021),该进程用于加载内核模块。
函数调用历史(回溯)
[回溯信息...]作用:显示崩溃前函数调用的链条,例如:
用途:帮助定位问题发生在哪一层调用中。
机器码转储
[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000 (e5833000)内容:崩溃时正在执行的机器指令的十六进制代码。
用途:开发者可通过反汇编工具(如
addr2line)将这些地址映射到具体的源代码行。
总结
错误根源:代码在
create_oops函数中尝试访问NULL指针。调用链:问题由
my_oops_init函数触发,最终导致崩溃。调试线索:通过寄存器值、堆栈信息和机器码,可以定位代码中的具体错误位置。
如果内核未启用符号信息(如 CONFIG_KALLSYMS),则地址需通过工具转换为函数名和行号。
二、OOPS 上的跟踪转储
当内核崩溃时,虽然可以用 kdump/kexec 和 crash 工具查看系统状态,但这些方法无法显示崩溃发生前的事件。而了解崩溃前的事件对排查问题至关重要。
Ftrace 工具能解决这个问题。要开启它的记录功能,只需做以下两件事之一:
在终端执行命令:
echo 1 > /proc/sys/kernel/ftrace_dump_on_oops;在开机启动参数里添加
ftrace_dump_on_oops。
开启后,当系统崩溃时,Ftrace 会自动把记录的事件日志以文本形式打印到屏幕(控制台)。如果把控制台输出连接到串口线(比如通过网线或 USB 转串口设备),日志会更方便保存和分析。
设置完成后,只需等待崩溃发生。一旦崩溃出现,控制台就会显示 Ftrace 记录的日志,帮助追溯崩溃前的事件。能回溯多久取决于 Ftrace 的存储空间大小,默认每个 CPU 的存储区超过 1MB,但可能不够用。
如何调整存储空间大小?
执行命令:
echo [数值] > /sys/kernel/debug/tracing/buffer_size_kb,其中[数值]是每个 CPU 的存储空间(单位为 KB)。例如:
echo 3 > /sys/kernel/debug/tracing/buffer_size_kb,会让每个 CPU 的存储空间变为 3KB。如果只需要简单记录,1KB 可能就足够了。
注意:
存储空间是每个 CPU 单独分配的,总大小是所有 CPU 的数值之和。
如果崩溃时存储区还没完全输出,数据可能被覆盖。因此要根据需求调整合适的大小。
三、使用 objdump 识别内核模块中的错误代码行
我们可以使用 objdump 来反汇编目标文件,并识别生成 OOP 的行。 我们使用反汇编的代码来处理符号名称和偏移量,以便指向准确的故障线。
以下行将反汇编 oops.as 文件中的内核模块:
arm-XXXX-objdump -fS oops.ko oops.as生成的输出文件将包含类似以下内容的内容:

编译模块时启用调试选项将使调试信息在 .ko 对象中可用。 在这种情况下,objdump -S 将插入源代码和程序集以获得更好的视图。
从 OOPS 中,我们已经看到 PC 位于 create_oops+0x18,它位于 create_oops 地址的 0x18 偏移量。 这就把我们带到了 18: e5833000 str r3, [r3] 线。 为了理解我们感兴趣的行,让我们描述一下它之前的行 mov r3, #0。 在这行之后,我们有 r3 = 0。 回到我们感兴趣的领域,对于熟悉 ARM 汇编语言的人来说,这意味着将 r3 写到 r3 所指向的原始地址([r3] 的 C 等价物是 *r3)。 请记住,这对应于我们代码中的 *(int *)0 = 0。