内核代码调试指南
1. 集成 SDK 与开启调试
点击顶部蓝色的“继续”按钮,输出面板应显示以下内容:
[build] Hello CMake
[build]
[build] Child exited with status 0
[build] [100%] Built target build_and_debug
[build] Build finished with exit code 0
恭喜!你已成功使用 CMake 将基于 Yocto 构建的 SDK 集成到 Visual Studio Code 中,从而能够在目标设备上进行 GDB 远程调试。现在你已了解操作方法,也能在自己的项目中实现同样的功能。
2. 内核代码调试工具概述
2.1 kgdb
kgdb 可用于源代码级调试,类似于使用 gdbserver 进行远程调试。它是内核 GDB 存根,多年来一直是主流 Linux 的一部分。用户手册可在内核 DocBook 中找到,也能在 这里 查看在线版本。多数情况下,通过串行接口连接 kgdb,这种实现方式称为 kgdboc(kgdb over console)。它需要支持 I/O 轮询而非中断的平台 tty 驱动,因为 kgdb 与 GDB 通信时需禁用中断。少数平台支持通过 USB 使用 kgdb,也曾有通过以太网使用的版本,但都未进入主流 Linux。
2.2 kdb
kdb 是自托管的内核调试器,适用于轻量级任务,如查看指令是否执行以及获取回溯信息。它有简单的命令行界面,可在串行控制台使用。
2.3 内核 Oops 消息和崩溃信息
当内核进行无效内存访问或执行非法指令时,会在内核日志中写入 Oops 消息,其回溯信息有助于定位故障代码行。
3. 使用 kgdb 调试内核代码
3.1 内核调试的注意事项
内核是复杂的实时系统,调试不像应用程序那样简单。单步执行更改内存映射或切换上下文的代码可能产生奇怪结果。内核编写时假设至少使用 -O1 优化级别,可通过设置 KCFLAGS 覆盖内核编译标志。
3.2 内核调试的配置选项
| 配置选项 | 位置 |
|---|---|
| CONFIG_DEBUG_INFO | Kernel hacking | Compile-time checks and compiler options | Compile the kernel with debug info menu |
| CONFIG_FRAME_POINTER | Kernel hacking | Compile-time checks and compiler options | Compile the kernel with frame pointers menu(可能因架构而异) |
| CONFIG_KGDB | Kernel hacking | KGDB: kernel debugger menu |
| CONFIG_KGDB_SERIAL_CONSOLE | Kernel hacking | KGDB: kernel debugger | KGDB: use kgdb over the serial console menu |
3.3 内核镜像要求
除 zImage 或 uImage 压缩内核镜像外,内核镜像必须是 ELF 对象格式(即 vmlinux 文件),以便 GDB 将符号加载到内存。在 Yocto 中,可请求将其包含在目标镜像和 SDK 中,它作为名为 kernel - vmlinux 的包构建,可通过添加到 IMAGE_INSTALL 列表进行安装。文件会被放入 sysroot 启动目录,例如:
/opt/poky/3.1.5/sysroots/cortexa8hf - neon - poky - linux - gnueabi/boot/vmlinux - 5.4.72 - yocto - standard
在 Buildroot 中,可在 output/build/linux - <version string>/vmlinux 找到 vmlinux 文件。
3.4 示例调试会话
3.4.1 指定串口
可通过内核命令行或运行时通过 sysfs 告诉 kgdb 使用哪个串口。
- 内核命令行方式 :在命令行添加 kgdboc=<tty>,<baud rate> ,例如 kgdboc=ttyO0,115200 。
- 运行时 sysfs 方式 :启动设备后,将终端名称写入 /sys/module/kgdboc/parameters/kgdboc 文件,例如:
# echo ttyO0 > /sys/module/kgdboc/parameters/kgdboc
注意,这种方式无法设置波特率。若与控制台使用相同 tty,则波特率已设置;否则,使用 stty 或类似程序设置。
3.4.2 启动 GDB
在主机上启动 GDB,选择与运行内核匹配的 vmlinux 文件:
$ arm - poky - linux - gnueabi - gdb ~/linux/vmlinux
GDB 从 vmlinux 加载符号表并等待进一步输入。
3.4.3 连接到 kgdb
关闭连接到控制台的终端模拟器,然后在 GDB 中尝试连接到 kgdb:
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
此时可能会收到无用响应,因为 kgdb 未监听连接。需通过 SSH 在目标设备上启动另一个 shell,并向 /proc/sysrq - trigger 写入 g 来强制内核进入陷阱:
# echo g > /proc/sysrq - trigger
目标设备停止后,再次在 GDB 中连接:
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
3.4.4 设置断点和调试
GDB 控制后,可设置断点、检查变量和查看回溯信息等。例如,设置 sys_sync 断点:
(gdb) break sys_sync
(gdb) c
在目标设备上输入 sync 会触发断点。
3.4.5 禁用 kgdboc
调试结束后,若要禁用 kgdboc,将 kgdboc 终端设置为空:
# echo "" > /sys/module/kgdboc/parameters/kgdboc
3.5 调试早期代码
若要在系统启动早期进行调试,可在 kgdboc 选项后添加 kgdbwait 到内核命令行:
kgdboc=ttyO0,115200 kgdbwait
启动时,控制台会显示:
[ 1.103415] console [ttyO0] enabled
[ 1.108216] kgdb: Registered I/O driver kgdboc.
[ 1.113071] kgdb: Waiting for connection from remote gdb...
此时关闭控制台,按常规方式从 GDB 连接。
3.6 调试模块
调试内核模块时,代码在运行时会重新定位,需通过 sysfs 查找其所在地址。模块各部分的重定位地址存储在 /sys/module/<module name>/sections 中。例如,对于名为 mbx 的模块:
# cat /sys/module/mbx/sections/.text
0xbf000000
# cat /sys/module/mbx/sections/.data
0xbf0003e8
# cat /sys/module/mbx/sections/.bss
0xbf0005c0
在 GDB 中使用这些地址加载模块符号表:
(gdb) add - symbol - file /home/chris/mbx - driver/mbx.ko 0xbf000000 - s .data 0xbf0003e8 - s .bss 0xbf0005c0
之后可像在 vmlinux 中一样设置断点和检查变量。例如:
(gdb) break mbx_write
(gdb) c
强制设备驱动调用 mbx_write 会触发断点。
4. 使用 kdb 调试内核代码
4.1 配置内核以使用 kdb
要通过串行控制台调用 kdb,需先按前文所述启用 kgdb,然后启用以下额外选项:
- CONFIG_KGDB_KDB ,位于 KGDB: Kernel hacking | kernel debugger | KGDB_KDB: Include kdb frontend for kgdb 菜单。
4.2 进入 kdb 调试
当强制内核进入陷阱时,会在控制台看到 kdb shell,而不是进入 GDB 会话。例如:
# echo g > /proc/sysrq-trigger
此时控制台会显示:
[ 42.971126] SysRq : DEBUG
Entering kdb (current=0xdf36c080, pid 83) due to Keyboard Entry
kdb>
4.3 kdb 常用命令
| 命令分类 | 命令 | 功能 |
|---|---|---|
| 获取信息 | ps | 显示活动进程 |
| ps A | 显示所有进程 | |
| lsmod | 列出模块 | |
| dmesg | 显示内核日志缓冲区 | |
| 断点操作 | bp | 设置断点 |
| bl | 列出断点 | |
| bc | 清除断点 | |
| bt | 打印回溯信息 | |
| go | 继续执行 | |
| 检查内存和寄存器 | md | 显示内存 |
| rd | 显示寄存器 |
以下是一个设置断点的示例:
kdb> bp sys_sync
Instruction(i) BP #0 at 0xc01304ec (sys_sync)
is enabled addr at 00000000c01304ec, hardtype=0 installed=0
kdb> go
当在控制台输入 sync 时,会触发断点并再次进入 kdb。
5. 分析内核 Oops 消息
5.1 Oops 消息示例
以下是一个由向邮箱驱动写入数据生成的 Oops 消息:
Unable to handle kernel NULL pointer dereference at virtual
address 00000004
pgd = dd064000
[00000004] *pgd=9e58a831, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#1] PREEMPT ARM
Modules linked in: mbx(O)
CPU: 0 PID: 408 Comm: sh Tainted: G O 4.8.12-yocto-standard #1
Hardware name: Generic AM33XX (Flattened Device Tree)
task: dd2a6a00 task.stack: de596000
PC is at mbx_write+0x24/0xbc [mbx]
LR is at __vfs_write+0x28/0x48
pc : [<bf0000f0>] lr : [<c024ff40>] psr: 800e0013
sp : de597f18 ip : de597f38 fp : de597f34
r10: 00000000 r9 : de596000 r8 : 00000000
r7 : de597f80 r6 : 000fda00 r5 : 00000002 r4 : 00000000
r3 : de597f80 r2 : 00000002 r1 : 000fda00 r0 : de49ee40
Flags: Nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment none
Control: 10c5387d Table: 9d064019 DAC: 00000051
Process sh (pid: 408, stack limit = 0xde596210)
其中 PC is at mbx_write+0x24/0xbc [mbx] 表明最后执行的指令在名为 mbx 的内核模块的 mbx_write 函数中,且位于该函数起始位置偏移 0x24 字节处,函数长度为 0xbc 字节。
5.2 分析回溯信息
回溯信息如下:
Stack: (0xde597f18 to 0xde598000)
7f00: bf0000cc 00000002
7f20: 000fda00 de597f80 de597f4c de597f38 c024ff40 bf0000d8
de49ee40 00000002
7f40: de597f7c de597f50 c0250c40 c024ff24 c026eb04 c026ea70
de49ee40 de49ee40
7f60: 000fda00 00000002 c0107908 de596000 de597fa4 de597f80
c025187c c0250b80
7f80: 00000000 00000000 00000002 000fda00 b6eecd60 00000004
00000000 de597fa8
7fa0: c0107700 c0251838 00000002 000fda00 00000001 000fda00
00000002 00000000
7fc0: 00000002 000fda00 b6eecd60 00000004 00000002 00000002
000ce80c 00000000
7fe0: 00000000 bef77944 b6e1afbc b6e73d00 600e0010 00000001
d3bbdad3 d54367bf
[<bf0000f0>] (mbx_write [mbx]) from [<c024ff40>] (__vfs_
write+0x28/0x48)
[<c024ff40>] (__vfs_write) from [<c0250c40>] (vfs_
write+0xcc/0x158)
[<c0250c40>] (vfs_write) from [<c025187c>] (SyS_
write+0x50/0x88)
[<c025187c>] (SyS_write) from [<c0107700>] (ret_fast_
syscall+0x0/0x3c)
Code: e590407c e3520b01 23a02b01 e1a05002 (e5842004)
---[ end trace edcc51b432f0ce7d ]---
从回溯信息可知, mbx_write 函数是从虚拟文件系统函数 __vfs_write 调用的。
5.3 使用 GDB 定位故障代码行
使用 GDB 的 disassemble 命令和 /s 修饰符来显示源代码和汇编代码:
$ arm-poky-linux-gnueabi-gdb mbx.ko
(gdb) disassemble /s mbx_write
输出如下:
Dump of assembler code for function mbx_write:
99 {
0x000000f0 <+0>: mov r12, sp
0x000000f4 <+4>: push {r4, r5, r6, r7, r11, r12, lr, pc}
0x000000f8 <+8>: sub r11, r12, #4
0x000000fc <+12>: push {lr} ; (str lr, [sp, #-4]!)
0x00000100 <+16>: bl 0x100 <mbx_write+16>
100 struct mbx_data *m = (struct mbx_data *)file->private_data;
0x00000104 <+20>: ldr r4, [r0, #124] ; 0x7c
0x00000108 <+24>: cmp r2, #1024 ; 0x400
0x0000010c <+28>: movcs r2, #1024 ; 0x400
101 if (length > MBX_LEN)
102 length = MBX_LEN;
103 m->mbx_len = length;
0x00000110 <+32>: mov r5, r2
0x00000114 <+36>: str r2, [r4, #4]
Oops 消息表明错误发生在 mbx_write+0x24 处,从反汇编结果可知 mbx_write 函数起始地址为 0xf0,加上 0x24 得到 0x114,对应代码行 103。由此可知, m 变量可能是一个空指针,导致了 Oops 错误。通过修改驱动代码初始化该指针,问题可得到解决:
static int mbx_open(struct inode *inode, struct file *file)
{
if (MINOR(inode->i_rdev) >= NUM_MAILBOXES) {
printk("Invalid mbx minor number\n");
return -ENODEV;
}
file->private_data = &mailboxes[MINOR(inode->i_rdev)];
return 0;
}
6. 保存内核 Oops 消息
6.1 问题描述
若系统在启动过程中崩溃,且在控制台启用之前或挂起后发生 Oops,可能无法看到该消息。因此,需要一种方法来保存 Oops 消息。
6.2 保存 Oops 消息的方法
可通过 U - Boot 来显示内核日志缓冲区的内容。首先,在 System.map 中查找 __log_buf 符号的位置:
$ grep __log_buf System.map
c0f72428 b __log_buf
然后,将内核逻辑地址映射为 U - Boot 能理解的物理地址。假设 PAGE_OFFSET 为 0xc0000000,BeagleBone 的 RAM 起始地址为 0x80000000,则计算如下:
c0f72428 - 0xc0000000 + 0x80000000 = 80f72428
最后,使用 U - Boot 的 md 命令显示日志:
U-Boot# md 80f72428
80f72428: 00000000 00000000 00210034 c6000000 ........4.!.....
80f72438: 746f6f42 20676e69 756e694c 6e6f2078 Booting Linux on
80f72448: 79687020 61636973 5043206c 78302055 physical CPU 0x
80f72458: 00000030 00000000 00000000 00730084 0.............s.
80f72468: a6000000 756e694c 65762078 6f697372 ....Linux versio
80f72478: 2e34206e 30312e31 68632820 40736972 n 4.1.10 (chris@
80f72488: 6c697562 29726564 63672820 65762063 builder) (gcc ve
6.3 总结
通过上述方法,可在系统崩溃后获取内核 Oops 消息,从而帮助定位和解决内核问题。在调试内核代码时,kgdb、kdb 和分析 Oops 消息都是非常有用的工具,根据不同的场景选择合适的方法能提高调试效率。
以下是一个简单的调试流程 mermaid 流程图:
graph TD;
A[开始调试] --> B{选择调试工具};
B -->|kgdb| C[配置内核和镜像];
B -->|kdb| D[配置内核启用 kdb];
B -->|分析 Oops| E[获取 Oops 消息];
C --> F[指定串口和启动 GDB];
F --> G[连接到 kgdb 并设置断点];
D --> H[强制内核进入陷阱进入 kdb];
H --> I[使用 kdb 命令调试];
E --> J[分析回溯信息和使用 GDB 定位代码];
G --> K[完成调试,禁用 kgdboc];
I --> L[结束 kdb 调试];
J --> M[修复代码问题];
K --> N[调试结束];
L --> N;
M --> N;
以上就是内核代码调试的详细介绍,希望能帮助你更好地进行内核调试工作。
超级会员免费看
2525

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



