Linux内核调试技巧与最佳实践
1. 使用Ftrace接口跟踪特定进程
Ftrace可用于跟踪内核中的事件和函数。默认情况下,使用Ftrace能跟踪启用跟踪的内核跟踪点和函数,而不考虑这些函数是为哪个进程运行的。若要仅跟踪为特定进程执行的内核函数,可将伪变量
set_ftrace_pid
设置为该进程的进程ID(PID),PID可通过
pgrep
等工具获取。
若进程尚未运行,可使用包装脚本和
exec
命令以已知PID执行命令,示例脚本如下:
#!/bin/sh
echo $$ > /debug/tracing/set_ftrace_pid
# [可在此处设置其他过滤条件]
echo function_graph > /debug/tracing/current_tracer
exec $*
在上述示例中,
$$
是当前执行进程(即脚本本身)的PID。将其设置到
set_ftrace_pid
变量中,然后启用
function_graph
跟踪器,之后脚本会执行指定的命令(由脚本的第一个参数指定)。
假设脚本名为
trace_process.sh
,使用示例如下:
sudo ./trace_command ls
2. Linux内核调试技巧
在Linux内核开发中,编写代码并非最困难的部分,调试才是真正的瓶颈,即使是经验丰富的内核开发者也不例外。大多数内核调试工具是内核本身的一部分。有时,内核会通过名为“Oops”的消息来帮助定位故障起源,调试工作也就转化为对这些消息的分析。
3. Oops和崩溃分析
Oops是Linux内核在发生错误或未处理的异常时打印的消息。内核会尽力描述异常情况,并在错误或异常发生前转储调用栈。
以下是一个内核模块示例:
#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");
在上述模块代码中,尝试解引用空指针以使内核崩溃。此外,使用
__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
描述了故障及其性质,表明代码尝试解引用空指针。
-
PC信息
:
[29935.214354] PC is at create_oops+0x18/0x20 [oops]
,PC代表程序计数器,指示当前执行指令在内存中的地址。这里表明当前处于
create_oops
函数中,该函数位于
oops
模块中(方括号内列出)。十六进制数字表示指令指针位于该函数起始位置偏移24(十六进制为
0x18
)字节处,该函数长度为32(十六进制为
0x20
)字节。
-
LR信息
:
[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops]
,LR是链接寄存器,包含当程序计数器遇到“从子程序返回”指令时应设置的地址。也就是说,LR保存了调用当前执行函数(即PC所在函数)的函数地址。这意味着
my_oops_init
是调用当前执行代码的函数,并且如果PC所在函数返回,下一条要执行的指令将是
my_oops_init+0x18
,即CPU将从
my_oops_init
的起始地址偏移
0x18
处跳转执行。
-
寄存器信息
:
-
sp
(栈指针):
[29935.224068] sp : cc66dda8
,保存栈的当前位置。
-
fp
(帧指针):
[29935.224068] fp : cc66ddb4
,指向栈中当前活动的帧。当函数返回时,栈指针会恢复为帧指针的值,即函数调用前栈指针的值。
- 其他CPU寄存器的值也会被转储,如
r10
、
r9
等。
-
进程信息
:
[29935.266822] Process insmod (pid: 20021, stack limit = 0xcc66c208)
显示了引发崩溃的进程是
insmod
,其PID为20021。
-
代码信息
:
[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000 (e5833000)
是Oops发生时正在运行的机器代码段的十六进制转储。
4. Oops时的跟踪转储
当内核崩溃时,可以使用
kdump/kexec
和
crash
工具来检查崩溃时刻系统的状态。然而,这种技术无法让我们看到导致崩溃的事件发生之前的情况,而这些信息对于理解和修复bug可能非常有用。
Ftrace提供了一个功能来解决这个问题。要启用该功能,可以将
1
写入
/proc/sys/kernel/ftrace_dump_on_oops
,或者在内核启动参数中启用
ftrace_dump_on_oops
。配置Ftrace并启用此功能后,Ftrace会在Oops或崩溃时将整个跟踪缓冲区以ASCII格式转储到控制台。将控制台输出连接到串口可以使调试崩溃问题变得更加容易。这样,只需设置好一切,等待崩溃发生即可。崩溃发生后,就能在控制台看到跟踪缓冲区的内容,从而追溯导致崩溃的事件。能够追溯事件的时间范围取决于跟踪缓冲区的大小,因为它存储了事件历史数据。
由于将跟踪缓冲区转储到控制台可能需要很长时间,通常在设置之前会缩小跟踪缓冲区的大小。默认的Ftrace环形缓冲区每个CPU超过1兆字节。可以通过向
/sys/kernel/debug/tracing/buffer_size_kb
文件写入所需的千字节数来减小跟踪缓冲区的大小。注意,该值是每个CPU的大小,而不是环形缓冲区的总大小。
修改跟踪缓冲区大小的示例如下:
# echo 3 > /sys/kernel/debug/tracing/buffer_size_kb
上述命令会将Ftrace环形缓冲区每个CPU的大小缩小到3千字节(1千字节可能就足够了,具体取决于需要追溯到崩溃前的时间范围)。
5. 使用objdump识别内核模块中的故障代码行
可以使用
objdump
对目标文件进行反汇编,从而识别引发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。对于熟悉ARM汇编语言的人来说,
str r3, [r3]
表示将
r3
的值写入
r3
所指向的原始地址(在C语言中相当于
*r3
),这对应于代码中的
*(int *)0 = 0
。
综上所述,本文介绍了多种内核调试技巧,包括如何使用Ftrace跟踪代码以识别异常行为,如耗时函数和中断延迟;还涵盖了核心或设备驱动相关代码的API打印方法;最后学习了如何分析和调试内核Oops。
下面用mermaid流程图展示使用Ftrace跟踪特定进程的流程:
graph TD;
A[开始] --> B[获取当前进程PID];
B --> C[将PID写入/debug/tracing/set_ftrace_pid];
C --> D[启用function_graph跟踪器];
D --> E[执行指定命令];
E --> F[结束];
再用表格总结一些重要的调试工具和操作:
| 工具/操作 | 描述 | 示例命令 |
| — | — | — |
| Ftrace跟踪特定进程 | 跟踪为特定进程执行的内核函数 |
echo $$ > /debug/tracing/set_ftrace_pid; echo function_graph > /debug/tracing/current_tracer; exec $*
|
| objdump反汇编 | 识别内核模块中的故障代码行 |
arm-XXXX-objdump -fS oops.ko > oops.as
|
| 缩小Ftrace跟踪缓冲区 | 减少转储时间 |
echo 3 > /sys/kernel/debug/tracing/buffer_size_kb
|
Linux内核调试技巧与最佳实践(续)
6. 调试技巧总结与应用场景
在前面的内容中,我们介绍了多种内核调试技巧,下面对这些技巧进行总结,并说明它们的应用场景。
| 调试技巧 | 应用场景 | 操作步骤 |
|---|---|---|
| 使用Ftrace跟踪特定进程 | 当需要跟踪特定进程执行的内核函数,定位时间消耗大的函数或中断延迟问题时 |
1. 获取进程PID;2. 将PID写入
/debug/tracing/set_ftrace_pid
;3. 启用
function_graph
跟踪器;4. 执行指定命令
|
| 分析Oops消息 | 内核出现错误或未处理异常时,定位故障起源 | 1. 查看Oops消息描述故障性质;2. 分析PC、LR等寄存器信息确定故障函数和调用关系;3. 查看进程信息和代码十六进制转储辅助分析 |
| Oops时的跟踪转储 | 内核崩溃时,追溯导致崩溃的事件 |
1. 启用
ftrace_dump_on_oops
功能;2. 可缩小跟踪缓冲区大小;3. 等待崩溃发生,查看控制台输出的跟踪缓冲区内容
|
| 使用objdump识别故障代码行 | 已知Oops信息,需要定位内核模块中具体的故障代码行 |
1. 使用
objdump
对内核模块进行反汇编;2. 根据Oops中PC的偏移量定位到具体代码行
|
7. 调试实践案例分析
为了更好地理解和应用这些调试技巧,下面通过一个实际案例进行分析。
假设我们在运行一个内核模块时,系统出现了Oops错误,控制台输出了如下Oops信息:
[12345.678901] Unable to handle kernel NULL pointer dereference
at virtual address 00000000
[12345.711768] pgd = abcd1234
[12345.714724] [00000000] *pgd=00000000
[12345.718340] Internal error: Oops - BUG: 901 [#1] PREEMPT x86_64
[...]
[12345.894001] CPU: 0 PID: 12345 Comm: insmod Tainted: P
O 5.10.0-10-amd64 #1
[12345.902186] Hardware name: SomePlatform
[12345.906473] task: ef012345 ti: ef123456 task.ti: ef123456
[12345.912270] PC is at faulty_function+0x20/0x30 [faulty_module]
[12345.916998] LR is at module_init_function+0x18/0x100 [faulty_module]
[12345.921984] pc : [<deadbeef>] lr : [<cafebabe>] psr:
70000013
[12345.921984] sp : ef123456 ip : ef123457 fp : ef123458
[12345.933490] r10: ef345678 r9 : ef456789 r8 : ef456789
[12345.938731] r7 : 00000000 r6 : ef567890 r5 : cafebabe r4
: ef678901
[12345.945277] r3 : 00000000 r2 : 12345678 r1 : ef789012 r0
: 00000020
[12345.951824] Flags: nZCv IRQs on FIQs on Mode SVC_64 ISA
x86_64 Segment none
[12345.958977] Control: 12345678 Table: 56789012 DAC:
00000067
[12345.964740] Process insmod (pid: 12345, stack limit =
0xef123678)
[12345.970850] Stack: (0xef123456 to 0xef123800)
[12345.975229] 4560: ef123470 ef123457
cafebabe deadbeef ef123500 ef123474
[12345.983436] 4760: 12345678 cafebabe ef1234d8 ef123480
12345679 1234567a 1234567b 1234567c
[12345.991643] 4960: ef1234e0 ef1234f0 1234567d 1234567e
ef1234e0 1234567f ef123500 ef1234d8
[...]
[12346.122934] 7e00: 12345680 12345681 12345682 12345683
12345684 12345685 12345686 12345687
[12346.131173] Code: 12345678 23456789 34567890 45678901
(56789012)
[12346.160729] ---[ end trace 123456789abcdef0 ]---
[12346.250878] Kernel panic - not syncing: Fatal exception
下面按照调试步骤进行分析:
1.
分析Oops消息
:
- 第一行表明系统尝试解引用空指针,这是故障的根本原因。
- PC信息显示当前处于
faulty_function
函数,偏移量为
0x20
,函数长度为
0x30
。
- LR信息显示调用
faulty_function
的函数是
module_init_function
。
- 进程信息显示是
insmod
进程触发了Oops,PID为12345。
2.
使用objdump反汇编内核模块
:
执行命令:
sh
x86_64-objdump -fS faulty_module.ko > faulty_module.as
打开
faulty_module.as
文件,根据PC的偏移量
0x20
定位到具体代码行。
3.
追溯事件
:
如果之前启用了
ftrace_dump_on_oops
功能,在控制台可以查看跟踪缓冲区的内容,追溯导致崩溃的事件。例如,查看在调用
faulty_function
之前执行了哪些函数,是否有异常的操作。
8. 调试注意事项
在进行内核调试时,还需要注意以下几点:
-
编译选项
:在编译内核模块时,启用调试选项(如
-g
)可以包含更多的调试信息,方便使用
objdump
等工具进行分析。
-
跟踪缓冲区大小
:根据实际情况合理调整Ftrace跟踪缓冲区的大小,避免过大导致转储时间过长,过小则可能无法追溯到足够多的事件。
-
系统稳定性
:调试过程中可能会对系统的稳定性产生影响,特别是在使用
kdump/kexec
等工具时,需要确保系统有足够的资源和备份。
9. 总结与展望
通过对内核调试技巧的学习和实践,我们可以更高效地定位和解决内核中的问题。无论是使用Ftrace跟踪代码、分析Oops消息,还是使用
objdump
反汇编,都为我们提供了强大的调试手段。
在未来的内核开发和调试中,随着技术的不断发展,可能会出现更多先进的调试工具和方法。我们需要不断学习和掌握这些新的技术,以应对日益复杂的内核系统。同时,良好的编程习惯和代码审查也是减少内核bug的重要手段。
下面用mermaid流程图展示内核调试的一般流程:
graph TD;
A[内核出现问题] --> B[查看Oops消息];
B --> C{是否能定位故障?};
C -- 是 --> D[使用objdump反汇编定位具体代码行];
C -- 否 --> E[启用Ftrace跟踪特定进程或Oops时的跟踪转储];
D --> F[修复代码];
E --> G[分析跟踪缓冲区内容,定位故障];
G --> D;
F --> H[测试修复效果];
H --> I{问题解决?};
I -- 是 --> J[结束];
I -- 否 --> B;
希望这些调试技巧和案例分析能帮助你更好地进行内核调试工作,提高内核开发的效率和质量。
超级会员免费看
1万+

被折叠的 条评论
为什么被折叠?



