Linux内核调试技巧

编写代码并不总是内核开发中最难的部分。调试是真正的瓶颈,即使对于有经验的内核开发人员也是如此。也就是说,大多数内核调试工具都是内核本身的一部分。有时,内核通过称为Oops的消息来帮助查找错误的起源。调试可以归结为分析消息。

Oops 和 panic分析

Oops是当发生错误或未处理的异常时由Linux内核打印的消息。它尽力描述异常,并在错误或异常发生之前转储调用堆栈。如果内核配置了CONFIG_FRAME_POINTER,当出现 Oops 信息时,会打印栈回溯信息,从而找出函数的调用关系。

以下面的内核模块为例:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static void __attribute__ ((__noinline__)) create_oops(void) {
    *(int *)0 = 0;
}

static int __init my_oops_init(void) {
    printk("oops from the module\n");
    create_oops();
    return 0;
}

static void __exit my_oops_exit(void) {
    printk("Goodbye world\n");
}

module_init(my_oops_init);
module_exit(my_oops_exit);
MODULE_LICENSE("GPL");

在前面的模块代码中,我们尝试对空指针进行解引用,以使内核产生panic。此外,我们使用 __noinline__ 属性是为了使create_oops()不被内联,允许它在反汇编期间和在调用堆栈中作为一个单独的函数出现。这个模块已经在ARM和x86平台上进行了构建和测试。Oops消息和内容将因机器而异:

# insmod /oops.ko
[29934.977983] Unable to handle kernel NULL pointer dereference
at virtual address 00000000
[29935.010853] pgd = cc59c000
[29935.013809] [00000000] *pgd=00000000
[29935.017425] Internal error: Oops - BUG: 805 [#1] PREEMPT ARM
[...]
[29935.193185] systime: 1602070584s
[29935.196435] CPU: 0 PID: 20021 Comm: insmod Tainted: P
O 4.4.106-ts-armv7l #1
[29935.204629] Hardware name: Columbus Platform
[29935.208916] task: cc731a40 ti: cc66c000 task.ti: cc66c000
[29935.214354] PC is at create_oops+0x18/0x20 [oops]
[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops]
[29935.224068] pc : [<bf2a8018>] lr : [<bf045018>] psr:
60000013
[29935.224068] sp : cc66dda8 ip : cc66ddb8 fp : cc66ddb4
[29935.235572] r10: cc68c9a4 r9 : c08058d0 r8 : c08058d0
[29935.240813] r7 : 00000000 r6 : c0802048 r5 : bf045000 r4
: cd4eca40
[29935.247359] r3 : 00000000 r2 : a6af642b r1 : c05f3a6a r0
: 00000014
[29935.253906] Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA
ARM Segment none
[29935.261059] Control: 10c5387d Table: 4c59c059 DAC:
00000051
[29935.266822] Process insmod (pid: 20021, stack limit =
0xcc66c208)
[29935.272932] Stack: (0xcc66dda8 to 0xcc66e000)
[29935.277311] dda0: cc66ddc4 cc66ddb8
bf045018 bf2a800c cc66de44 cc66ddc8
[29935.285518] ddc0: c01018b4 bf04500c cc66de0c cc66ddd8
c01efdbc a6af642b cff76eec cff6d28c
[29935.293725] dde0: cf001e40 cc24b600 c01e80b8 c01ee628
cf001e40 c01ee638 cc66de44 cc66de08
[...]
[29935.425018] dfe0: befdcc10 befdcc00 004fda50 b6eda3e0
a0000010 00000003 00000000 00000000
[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000
(e5833000)
[29935.462814] ---[ end trace ebc2c98aeef9342e ]---
[29935.552962] Kernel panic - not syncing: Fatal exception

让我们仔细看看前面的转储,以理解一些重要的信息:

[29934.977983] Unable to handle kernel NULL pointer dereference
at virtual address 00000000

第一行描述了错误及其本质,在这种情况下,代码试图解引用一个NULL指针:

[29935.214354] PC is at create_oops+0x18/0x20 [oops]

PC代表程序计数器,它表示内存中当前执行的指令地址。在这里,我们看到我们位于create_oops函数中,该函数位于oops模块中(方括号中列出)。十六进制数字(0x18)表明指令指针在函数里的位置是第24字节,函数总大小是32字节(0x20):

[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops]

LR是链接寄存器,它包含程序计数器在到达“从子例程返回”指令时应该设置的地址。换句话说,LR保存了调用当前执行函数(PC所在的函数)的函数的地址,在本例中,也就是LR保存了my_oops_init函数的地址。首先,这意味着my_oops_init是调用执行代码的函数。这也意味着如果PC中的函数已经返回,下一行要执行的将是my_oops_init+0x18,这意味着CPU将在my_oops_init的起始地址的0x18偏移量处执行:

[29935.224068] pc : [<bf2a8018>] lr : [<bf045018>] psr:
60000013

在上面的代码行中,pc和lr是PCLR的实际十六进制内容,没有显示符号名称。这些地址可以与addr2line程序一起使用,addr2line程序是我们可以用来查找故障线路的另一个工具。如果内核是在禁用CONFIG_KALLSYMS选项的情况下构建的,这就是我们在打印输出中看到的结果。然后我们可以推断create_oops和my_oops_init的地址分别是0xbf2a8000和0xbf045000:

[29935.224068] sp : cc66dda8 ip : cc66ddb8 fp : cc66ddb4

sp代表堆栈指针,保存堆栈中的当前位置,而fp代表帧指针,指向堆栈中当前活动的帧。当函数返回时,堆栈指针恢复到帧指针,即函数被调用之前的堆栈指针的值。

[29935.235572] r10: cc68c9a4 r9 : c08058d0 r8 : c08058d0
[29935.240813] r7 : 00000000 r6 : c0802048 r5 : bf045000 r4
: cd4eca40
[29935.247359] r3 : 00000000 r2 : a6af642b r1 : c05f3a6a r0
: 00000014

上面是一些CPU寄存器的转储:

[29935.266822] Process insmod (pid: 20021, stack limit =
0xcc66c208)

上面一行显示了发生panic的进程,在本例中是insmod,其PID为20021。

也有一些oops出现了回溯,有点像下面,这是输入echo c > /proc/sysrq-trigger生成的oops的摘录:

 backtrace可以跟踪生成oops之前的函数调用历史:

[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000
(e5833000)

Code是发生错误时正在运行的机器代码部分的十六进制转储。

Trace dump on oops

当内核崩溃时,可以使用kdump/kexec和crash实用程序来检查系统在崩溃时的状态。但是,这种技术不能让您看到在导致崩溃的事件之前发生了什么,这可能是理解或修复错误的一个很好的线索。

Ftrace附带了一个试图解决这个问题的特性。为了启用它,您可以echo 1 > /proc/sys/kernel/ftrace_dump_on_oops,或者在内核引导参数中启用ftrace_dump_on_oops。在启用此功能的同时配置Ftrace将指示Ftrace在oops或panic时以ASCII格式将整个跟踪缓冲区转储到控制台。将控制台输出到串行行可以使调试崩溃更容易。这样,你就可以设置好一切,然后等待崩溃。一旦发生,您将在控制台上看到跟踪缓冲区。然后,您将能够追溯导致崩溃的事件。跟踪事件可以追溯到多远的时间取决于跟踪缓冲区的大小,因为这是存储事件历史数据的地方。

也就是说,转储到控制台可能需要很长时间,并且通常在将所有内容都放置到位之前收缩跟踪缓冲区,因为默认的Ftrace环缓冲区每个CPU超过1兆字节。您可以使用/sys/kernel/debug/tracing/ buffer_size_kb来减少跟踪缓冲区的大小,方法是在该文件中写入您想要的循环缓冲区的千字节数。注意,该值是每个CPU,而不是循环缓冲区的总大小。

修改跟踪缓冲区大小的示例如下:

echo 3 > /sys/kernel/debug/tracing/buffer_size_kb

上面的命令将Ftrace环缓冲区缩小到每个CPU 3K字节(1 kb可能足够了;这取决于你需要回到crash发生前的哪个位置)。

使用objdump来识别内核模块中的错误代码行

我们可以使用objdump来分解object文件并识别生成oops的行。我们使用反汇编的代码来处理符号名称和偏移量,以便指向准确的错误行。

下面的代码行将分解内核模块到oops.as文件中:

arm-XXXX-objdump -fS oops.ko > oops.as

生成的输出文件内容类似如下:

[...]
architecture: arm, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
Disassembly of section .text.unlikely:
00000000 <create_oops>: 0: e1a0c00d mov ip, sp 4: e92dd800 push {fp, ip, lr, pc} 8: e24cb004 sub fp, ip, #4 c: e52de004 push {lr} ; (str lr, [sp, #-4]!) 10: ebfffffe bl 0 <__gnu_mcount_nc> 14: e3a03000 mov r3, #0 18: e5833000 str r3, [r3] 1c: e89da800 ldm sp, {fp, sp, pc}
Disassembly of section .init.text:
00000000 <init_module>: 0: e1a0c00d mov ip, sp 4: e92dd800 push {fp, ip, lr, pc} 8: e24cb004 sub fp, ip, #4 c: e59f000c ldr r0, [pc, #12] ; 20 <init_module+0x20> 10: ebfffffe bl 0 <printk> 14: ebfffffe bl 0 <init_module> 18: e3a00000 mov r0, #0 1c: e89da800 ldm sp, {fp, sp, pc} 20: 00000000 .word 0x00000000
Disassembly of section .exit.text:
00000000 <cleanup_module>: 0: e1a0c00d mov ip, sp 4: e92dd800 push {fp, ip, lr, pc} 8: e24cb004 sub fp, ip, #4 c: e59f0004 ldr r0, [pc, #4] ; 18 <cleanup_module+0x18> 10: ebfffffe bl 0 <printk> 14: e89da800 ldm sp, {fp, sp, pc} 18: 00000016 .word 0x00000016

重要提示:在编译模块时启用调试选项将使调试信息在.ko对象中可用。在这种情况下,objdump -S将插入源代码和程序集,以便更好地查看。

从oops中,我们已经看到PC位于create_oops+0x18,这是create_oops地址的0x18偏移量。这将我们引向“18: e5833000 str r3, [r3]”行。为了理解我们感兴趣的这行,我们来描述它前面的这行,“mov r3, #0”。这行执行之后,我们有r3 = 0。回到”str r3, [r3]“这一行,对于熟悉ARM汇编语言的人来说,这意味着将r3写入r3指向的原始地址(C语言中[r3]的等价物是*r3)。记住,这对应于我们代码中的*(int *)0 = 0。

找出函数的调用关系
posted @ 2023-02-06 19:06  闹闹爸爸  阅读(173)  评论(0编辑  收藏  举报