Appearance
KGDB远程调试详解
KGDB(Kernel GNU Debugger)是Linux内核的一个源码级调试工具,它允许开发者像调试用户空间程序一样调试内核代码。通过KGDB,你可以设置断点、单步执行、检查变量值、查看调用栈等,极大地提高了内核开发和调试的效率。
基本概念和工作原理
KGDB由两部分组成:
- KGDB内核补丁:在内核中运行,负责处理调试请求
- GDB补丁:扩展了标准GDB,使其能够与KGDB通信
KGDB通过串口或网络连接与外部调试器通信,在调试过程中,被调试的机器(目标机)会暂停执行,等待调试器(主机)发送命令。
内核配置和编译
要在内核中启用KGDB支持,需要在内核配置中启用以下选项:
bash
# 内核配置选项
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_TESTS=y # 可选,用于测试
CONFIG_KGDB_LOW_LEVEL_TRAP=y # 可选,用于捕获早期异常
CONFIG_KGDB_KDB=y # 可选,启用kdb前端通过menuconfig配置:
bash
make menuconfig
# 进入 Kernel hacking
# 选中 KGDB: kernel debugger串口调试配置
硬件连接
KGDB最常用的调试方式是通过串口连接。你需要一根串口线连接目标机和主机:
目标机串口 <----> 串口线 <----> 主机串口/USB转串口适配器内核启动参数
在目标机上,需要添加KGDB相关的启动参数:
bash
# 通过GRUB添加内核参数
kgdboc=ttyS0,115200 kgdbwait
# 参数说明:
# kgdboc: 指定KGDB使用的串口设备和波特率
# ttyS0: 第一个串口设备
# 115200: 波特率
# kgdbwait: 内核启动时等待调试器连接GDB连接
在主机端,使用交叉编译的GDB连接到目标机:
bash
# 启动GDB
<arch>-linux-gnu-gdb vmlinux
# 连接到目标机
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
# 或者如果使用telnet
(gdb) target remote localhost:6443网络调试配置
KGDB也支持通过网络进行调试,这种方式更加便捷:
目标机配置
bash
# 内核启动参数
kgdboc=kms,kdb kgdbwait
# 或者在运行时启用
echo ttyS0 > /sys/module/kgdboc/parameters/kgdbocgdbserver方式
bash
# 在目标机上启动kgdb
echo g > /proc/sysrq-trigger
# 在主机上连接
gdb vmlinux
(gdb) target remote udp:192.168.1.100:6443基本调试命令
断点设置
bash
# 设置断点
(gdb) break function_name
(gdb) break file.c:line_number
(gdb) break *address
# 查看断点
(gdb) info breakpoints
# 删除断点
(gdb) delete breakpoint_number
(gdb) clear function_name执行控制
bash
# 继续执行
(gdb) continue
(gdb) c
# 单步执行
(gdb) step
(gdb) s
# 下一行
(gdb) next
(gdb) n
# 执行完当前函数
(gdb) finish数据查看
bash
# 查看变量
(gdb) print variable_name
(gdb) p variable_name
# 查看内存
(gdb) x/10x address
(gdb) x/10i address
# 查看寄存器
(gdb) info registers
# 查看调用栈
(gdb) backtrace
(gdb) bt高级调试技巧
条件断点
bash
# 设置条件断点
(gdb) break function_name if condition
# 示例:当变量等于特定值时停止
(gdb) break driver_init if device_count == 5观察点
bash
# 设置观察点
(gdb) watch variable_name
(gdb) rwatch variable_name # 读取时触发
(gdb) awatch variable_name # 访问时触发宏定义展开
bash
# 展开宏定义
(gdb) macro expand MACRO_NAME
# 查看所有宏定义
(gdb) macro list实际调试案例
案例1:调试模块加载问题
c
// simple_module.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int faulty_variable = 0;
static int __init simple_init(void)
{
printk(KERN_INFO "Simple module loading\n");
// 故意制造错误
if (faulty_variable == 0) {
printk(KERN_ERR "Error condition detected\n");
return -EINVAL;
}
printk(KERN_INFO "Simple module loaded successfully\n");
return 0;
}
static void __exit simple_exit(void)
{
printk(KERN_INFO "Simple module unloaded\n");
}
module_init(simple_init);
module_exit(simple_exit);
MODULE_LICENSE("GPL");调试步骤:
bash
# 1. 编译模块
make
# 2. 在simple_init函数设置断点
(gdb) break simple_init
# 3. 加载模块时触发断点
# 在目标机上执行
insmod simple_module.ko
# 4. 在GDB中检查变量
(gdb) print faulty_variable
# 5. 修改变量值继续执行
(gdb) set faulty_variable = 1
(gdb) continue案例2:调试内核死锁
c
// deadlock_demo.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/mutex.h>
static DEFINE_MUTEX(lock_a);
static DEFINE_MUTEX(lock_b);
static void function_a(void)
{
mutex_lock(&lock_a);
// 模拟一些工作
msleep(100);
// 尝试获取另一个锁(可能导致死锁)
mutex_lock(&lock_b);
printk(KERN_INFO "Function A acquired both locks\n");
mutex_unlock(&lock_b);
mutex_unlock(&lock_a);
}
static void function_b(void)
{
mutex_lock(&lock_b);
// 模拟一些工作
msleep(100);
// 尝试获取另一个锁(可能导致死锁)
mutex_lock(&lock_a);
printk(KERN_INFO "Function B acquired both locks\n");
mutex_unlock(&lock_a);
mutex_unlock(&lock_b);
}
static int __init deadlock_init(void)
{
// 创建两个内核线程分别调用function_a和function_b
// 这将导致死锁
printk(KERN_INFO "Deadlock demo module loaded\n");
return 0;
}调试步骤:
bash
# 1. 在mutex_lock处设置断点
(gdb) break mutex_lock
# 2. 加载模块触发死锁
# 3. 使用Ctrl+C中断GDB,查看调用栈
(gdb) bt
# 4. 分析死锁原因
(gdb) info threads
(gdb) thread apply all bt案例3:内存泄漏检测
c
// memory_leak.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
static void *leaked_memory = NULL;
static int __init leak_init(void)
{
// 分配内存但不释放
leaked_memory = kmalloc(1024, GFP_KERNEL);
if (!leaked_memory) {
return -ENOMEM;
}
strcpy(leaked_memory, "This memory will be leaked");
printk(KERN_INFO "Memory allocated but not freed\n");
return 0;
}
static void __exit leak_exit(void)
{
// 忘记释放内存
// kfree(leaked_memory);
printk(KERN_INFO "Module unloaded\n");
}
module_init(leak_init);
module_exit(leak_exit);
MODULE_LICENSE("GPL");调试步骤:
bash
# 1. 在kmalloc处设置断点
(gdb) break kmalloc
# 2. 检查分配的内存地址
(gdb) print $retval
# 3. 在模块卸载时检查是否释放内存
(gdb) break leak_exit
# 4. 验证是否存在内存泄漏KDB前端使用
KDB是KGDB的一个简单前端,提供了命令行界面:
bash
# 启用KDB
echo kdb > /proc/sysrq-trigger
# 常用KDB命令
kdb> help # 显示帮助
kdb> bt # 显示调用栈
kdb> rd address # 读取内存
kdb> rm address # 修改内存
kdb> go # 继续执行
kdb> kill # 终止进程调试最佳实践
- 备份系统:调试内核可能导致系统崩溃,务必做好备份
- 使用虚拟机:在虚拟机中进行调试更加安全
- 准备恢复方案:准备好从调试状态恢复的方法
- 逐步调试:从小问题开始,逐步增加复杂度
- 记录调试过程:详细记录调试步骤和发现的问题
常见问题和解决方案
问题1:无法连接到KGDB
bash
# 检查串口设备
ls -l /dev/ttyUSB*
# 检查权限
sudo chmod 666 /dev/ttyUSB0
# 检查波特率设置
stty -F /dev/ttyUSB0 115200问题2:GDB找不到符号
bash
# 确保使用正确的vmlinux文件
file vmlinux
# 检查符号表
nm vmlinux | grep symbol_name
# 重新编译内核
make vmlinux问题3:调试过程中系统崩溃
bash
# 启用内核崩溃转储
CONFIG_KEXEC=y
CONFIG_CRASH_DUMP=y
# 配置kdump
# 在/etc/default/kdump-tools中设置通过掌握KGDB的使用方法,你可以在源码级别调试Linux内核,快速定位和解决问题,提高内核开发效率。