Skip to content

调试指南

本指南介绍 NoobKernel 的调试方法,包括 GDB 调试、日志系统和常见问题排查。

GDB 调试

启动调试会话

make debug

此命令会: 1. 启动 QEMU,暂停等待 GDB 2. 自动连接 GDB 3. 在 GDB 中等待命令

常用 GDB 命令

执行控制

continue          # 继续执行
step              # 单步执行(进入函数)
next              # 单步执行(不进入函数)
finish            # 执行到当前函数返回

断点

break main        # 在 main 函数设置断点
break kerneltrap  # 在 kerneltrap 设置断点
break *0x80200000 # 在地址设置断点
delete 1          # 删除断点 1
info breakpoints  # 查看断点

查看信息

backtrace         # 调用栈
info registers    # 所有寄存器
print var         # 打印变量
print/x var       # 十六进制打印
print *ptr        # 打印指针指向的值
x/10x 0x80200000  # 查看内存(10 个十六进制值)

RISC-V 特定

info registers sstatus  # 查看 sstatus 寄存器
info registers satp     # 查看 satp 寄存器
print $sp               # 打印栈指针
print $ra               # 打印返回地址

VSCode 调试

make vs-debug

然后在 VSCode 中按 F5 开始调试。

.vscode/launch.json 配置:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Kernel",
            "type": "gdb",
            "request": "launch",
            "target": "./build/QEMU/kernel",
            "cwd": "${workspaceRoot}",
            "gdbpath": "gdb-multiarch",
            "preLaunchTask": "Start QEMU"
        }
    ]
}

调试技巧

调试启动问题

break *0x80200000  # 内核入口
break main
continue

调试中断处理

break kerneltrap
break handle_timer
break handle_external

调试内存分配

break kmalloc
break buddy_alloc
break kmem_cache_alloc

调试调度

break sched_yield
break context_switch

日志系统

日志级别

#define LOG_LEVEL 3  // 当前级别

#define LOG_DEBUG 4  // 调试信息(绿色)
#define LOG_INFO  3  // 一般信息(蓝色)
#define LOG_WARN  2  // 警告(黄色)
#define LOG_ERROR 1  // 错误(红色)

日志函数

debugf("debug message: %d", value);  // 绿色
infof("info message: %s", str);      // 蓝色
warnf("warning: %d", warning);       // 黄色
errorf("error: %d", error);          // 红色
panic("fatal error: %s", msg);       // 红色 + 关机

修改日志级别

include/config.h 中修改:

#define LOG_LEVEL 4  // 显示所有日志
#define LOG_LEVEL 0  // 只显示 panic

调试特定模块

在模块源文件中定义局部日志宏:

#undef LOG_LEVEL
#define LOG_LEVEL 4  // 本模块显示所有日志

infof("mm: allocating %d pages", n);

QEMU 调试选项

详细日志

qemu-system-riscv64 \
    -d guest_errors,unimp,cpu_reset \
    -D qemu.log \
    ...

-d 选项: - guest_errors:客户机错误 - unimp:未实现指令 - cpu_reset:CPU 复位 - int:中断 - exec:执行跟踪(大量输出)

监视器

QEMU 运行时按 Ctrl+A C 进入监视器:

(qemu) info registers    # 查看寄存器
(qemu) info mtree        # 内存映射
(qemu) info qtree        # 设备树
(qemu) quit              # 退出

反汇编调试

生成反汇编文件:

make  # 会生成 kernel.asm

查看反汇编:

less build/QEMU/kernel.asm

查找函数地址:

grep "kerneltrap>" build/QEMU/kernel.asm

符号表

查看符号表:

riscv64-unknown-elf-nm build/QEMU/kernel | sort

查找符号地址:

riscv64-unknown-elf-nm build/QEMU/kernel | grep main

常见问题

内核崩溃

症状:QEMU 无响应或输出异常。

排查步骤

  1. 查看 kernel.asm 找到崩溃地址
  2. 使用 GDB 定位崩溃位置
  3. 检查 scausestval 寄存器
break kerneltrap
continue
# 触发崩溃后
info registers scause
info registers stval

页故障

症状:页面错误异常。

排查

# 在 kerneltrap 中
print/x ktf->scause
print/x ktf->stval  # 故障地址

常见原因: - 访问空指针 - 访问未映射地址 - 权限错误

死锁

症状:内核停止响应。

排查

  1. 启用 SPINLOCK_DEBUG
  2. 检查锁持有情况
break panic
continue
backtrace

内存泄漏

症状:内存逐渐耗尽。

排查

  1. kmallockfree 添加日志
  2. 统计分配和释放次数
static int alloc_count = 0;
static int free_count = 0;

void *kmalloc(size_t size) {
    void *p = ...;
    if (p) alloc_count++;
    debugf("kmalloc: %d allocated, %d freed", alloc_count, free_count);
    return p;
}

void kfree(void *ptr) {
    free_count++;
    debugf("kfree: %d allocated, %d freed", alloc_count, free_count);
}

调试宏

assert

#include <misc/log.h>

void foo(int *ptr) {
    assert(ptr != NULL);  // 如果 ptr 为 NULL,触发 panic
    ...
}

BUG_ON

BUG_ON(condition);  // 如果条件为真,触发 panic

断点触发

#define BREAKPOINT() asm volatile("ebreak")

void debug_here() {
    BREAKPOINT();  // GDB 会停在这里
}

内存检查

检查内存分配器状态

void kalloc_test(void) {
    infof("Free pages: %d", get_free_pages_num());
    infof("Buddy free pages: %d", buddy_free_pages);
}

检查缓冲区缓存

void bcache_stat(void) {
    int used = 0;
    for (int i = 0; i < BCACHE_SIZE; i++) {
        if (bcache[i].refcnt > 0) used++;
    }
    infof("bcache: %d/%d used", used, BCACHE_SIZE);
}

性能分析

简单计时

u64 start = r_time();
// 要测量的代码
u64 end = r_time();
infof("Time: %d cycles", end - start);

函数调用计数

static int call_count = 0;

void my_function() {
    call_count++;
    debugf("my_function called %d times", call_count);
}

下一步