41、Linux内核调试技巧与最佳实践

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;

希望这些调试技巧和案例分析能帮助你更好地进行内核调试工作,提高内核开发的效率和质量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值