Skip to content

KGDB远程调试详解

KGDB(Kernel GNU Debugger)是Linux内核的一个源码级调试工具,它允许开发者像调试用户空间程序一样调试内核代码。通过KGDB,你可以设置断点、单步执行、检查变量值、查看调用栈等,极大地提高了内核开发和调试的效率。

基本概念和工作原理

KGDB由两部分组成:

  1. KGDB内核补丁:在内核中运行,负责处理调试请求
  2. 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/kgdboc

gdbserver方式

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. 备份系统:调试内核可能导致系统崩溃,务必做好备份
  2. 使用虚拟机:在虚拟机中进行调试更加安全
  3. 准备恢复方案:准备好从调试状态恢复的方法
  4. 逐步调试:从小问题开始,逐步增加复杂度
  5. 记录调试过程:详细记录调试步骤和发现的问题

常见问题和解决方案

问题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内核,快速定位和解决问题,提高内核开发效率。