1. 环境准备与工具安装
调试Linux内核听起来像是只有内核黑客才能玩转的高级技能,但其实只要用对工具和方法,小白也能轻松上手。我自己在折腾内核调试时,发现Qemu和GDB这个组合真是绝配——Qemu提供了完整的虚拟硬件环境,而GDB则是调试利器,两者结合就能实现源码级的内核调试。
先说说需要准备的东西。我的实验环境是x86架构的Ubuntu 20.04,但你用其他Linux发行版也没问题,只是包管理器的命令可能需要调整。核心工具就三个:Qemu模拟器、Linux内核源码和BusyBox工具集。Qemu建议用7.0.0版本,Linux内核用5.15.30,这两个版本搭配起来比较稳定,我实测过很多次都没出过问题。
安装依赖包是第一步,这些包是编译Qemu和内核所必需的。打开终端,执行以下命令:
sudo apt update
sudo apt install libudev-dev libusb-1.0-0-dev libusbredirparser-dev
sudo apt install flex bison libssl-dev libelf-dev bc
sudo apt install gcc-aarch64-linux-gnu
这里特别注意要安装aarch64的交叉编译工具链,因为我们要在x86机器上编译ARM64架构的内核。很多人会忘记这一步,导致后面编译出错。
下载Qemu源码并编译安装:
wget https://download.qemu.org/qemu-7.0.0.tar.xz
tar xvJf qemu-7.0.0.tar.xz
cd qemu-7.0.0
mkdir build install
cd build
../configure --target-list=aarch64-softmmu --prefix=/path/to/your/install
make -j$(nproc)
make install
把/path/to/your/install替换成你想要的安装路径。编译完成后,记得把bin目录加入PATH环境变量,这样就能直接使用新编译的Qemu了。
2. 内核编译与调试配置
接下来要准备Linux内核。我选择5.15.30这个长期支持版本,因为它足够稳定,而且社区支持很好。下载和解压命令很简单:
wget https://www.kernel.org/pub/linux/kernel/v5.x/linux-5.15.30.tar.xz
tar xvf linux-5.15.30.tar.xz
cd linux-5.15.30
现在来到最关键的一步——配置内核编译选项。很多人在这一步栽跟头,其实只要注意几个关键设置就行。首先设置架构和交叉编译工具链:
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
然后生成默认配置:
make defconfig
接下来运行menuconfig进行详细设置:
make menuconfig
这里必须确保以下几个选项正确设置,否则无法调试:
在Kernel hacking → Kernel debugging中打开内核调试支持 在Compile-time checks and compiler options中开启Compile the kernel with debug info和Provide GDB script for kernel debugging 在Kernel Features中关闭Randomize the address of the kernel image(地址随机化)
关闭地址随机化特别重要,如果不禁用这个功能,GDB就无法正确设置断点。我记得第一次调试时忘了关这个选项,结果断点完全不起作用,折腾了好久才找到原因。
配置完成后就可以编译了:
make -j$(nproc)
编译过程可能需要十几分钟到半小时,取决于你的机器性能。编译成功后,在arch/arm64/boot目录下会生成Image.gz文件,这就是我们要用的内核镜像。
3. 制作根文件系统
光有内核还不够,我们还需要一个根文件系统。我选择BusyBox,因为它轻量又实用,包含了最基本的Linux工具。下载和编译BusyBox:
wget https://www.busybox.net/downloads/busybox-1.35.0.tar.bz2
tar xvf busybox-1.35.0.tar.bz2
cd busybox-1.35.0
运行配置菜单:
make menuconfig
这里需要设置几个关键选项:在Settings中打开Build BusyBox as a static binary,这样编译出来的BusyBox就不依赖动态库了;在Cross compiler prefix中填入aarch64-linux-gnu-;如果需要调试BusyBox本身,可以开启DEBUG选项,但一般来说用不到。
编译和安装:
make -j$(nproc)
现在来制作根文件系统镜像:
dd if=/dev/zero of=rootfs.img bs=1M count=100
mkfs.ext4 rootfs.img
mkdir mnt
sudo mount -o loop rootfs.img mnt
创建一个10MB的镜像文件,格式化为ext4文件系统,然后挂载到本地目录。接下来把BusyBox安装到镜像中:
sudo make install CONFIG_PREFIX=./mnt
还需要创建一些必要的目录和配置文件:
sudo mkdir -p mnt/{proc,dev,sys,etc}
sudo cp -r examples/bootfloppy/etc/* mnt/etc/
编辑fstab文件,添加必要的挂载信息:
sudo sh -c 'echo "proc /proc proc defaults 0 0" >> mnt/etc/fstab'
sudo sh -c 'echo "sysfs /sys sysfs defaults 0 0" >> mnt/etc/fstab'
最后卸载镜像:
sudo umount mnt
这样我们就得到了一个完整的根文件系统镜像,里面包含了最基本的Linux环境和工具。
4. Qemu启动配置与调试技巧
一切准备就绪,现在可以启动Qemu了。我习惯写一个启动脚本,这样每次使用都很方便。创建一个名为qemu_start.sh的文件,内容如下:
#!/bin/bash
qemu-system-aarch64 \
-M virt \
-cpu cortex-a72 \
-smp 4 \
-m 2G \
-kernel ./linux-5.15.30/arch/arm64/boot/Image.gz \
-drive file=./rootfs.img,format=raw \
-append "console=ttyAMA0 root=/dev/vda rw" \
-nographic \
-gdb tcp::1234 \
-S
这个脚本做了几件重要的事情:指定了ARM64架构的virt机器,设置了4核CPU和2GB内存,指定了内核镜像和根文件系统,最后两个参数-gdb tcp::1234和-S是最关键的——它们告诉Qemu在1234端口监听GDB连接,并在启动时暂停等待调试器连接。
给脚本执行权限并运行:
chmod +x qemu_start.sh
./qemu_start.sh
这时候Qemu会启动并停在那里,等待GDB连接。现在我们可以开始调试了。
调试有两种方式:命令行GDB或者VSCode。先说说命令行方式,虽然看起来不如图形界面友好,但其实很强大:
gdb-multiarch ./linux-5.15.30/vmlinux
在GDB中连接Qemu:
(gdb) target remote :1234
连接成功后,你就可以设置断点、单步执行、查看变量了。比如在start_kernel函数设断点:
(gdb) break start_kernel
(gdb) continue
这样内核就会在启动时停在这个断点处。我经常用这种方法跟踪内核启动过程,查看各个子系统是如何初始化的。
5. VSCode图形化调试配置
虽然命令行GDB很强大,但我更喜欢用VSCode进行图形化调试,这样更直观也更方便。配置起来其实很简单,只需要创建一个launch.json文件。
首先在Linux源码目录下创建.vscode文件夹,然后创建launch.json文件:
{
"version": "0.2.0",
"configurations": [
{
"name": "Linux Kernel Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/vmlinux",
"miDebuggerServerAddress": "localhost:1234",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
这个配置告诉VSCode使用gdb-multiarch连接本地的1234端口进行调试。注意program字段要指向vmlinux文件,这是带有调试信息的内核镜像。
配置好后,先在终端启动Qemu:
./qemu_start.sh
然后在VSCode中打开内核源码目录,按F5启动调试。VSCode会自动连接Qemu,并在入口处暂停。这时候你可以在源码中设置断点,查看变量,单步调试,就像调试普通程序一样。
我特别喜欢VSCode的调试功能,特别是变量监视和调用栈查看,这些在分析复杂内核问题时特别有用。比如你可以监视某个重要数据结构的變化,或者查看函数调用关系来理解代码执行流程。
6. 实战调试案例与技巧
掌握了基本调试方法后,来看看一些实际使用中的技巧和案例。我在调试内核时积累了一些经验,分享给大家。
首先是怎么设置有效的断点。内核代码量巨大,如果盲目设置断点,可能会陷入无关代码的泥潭。我通常先用以下命令列出某个模块的所有函数:
nm vmlinux | grep function_name
或者用GDB的info functions命令:
(gdb) info functions pattern
找到感兴趣的函数后,再设置断点。比如想调试进程调度,可以在schedule函数设断点:
(gdb) break schedule
另一个实用技巧是使用条件断点。比如只想在特定进程调用某个函数时中断:
(gdb) break do_fork if pid == 1234
这样只有当进程ID为1234的进程调用do_fork时才会中断。
查看内核日志也很重要。在Qemu中,内核输出会直接显示在控制台,但你也可以使用GDB来查看内核数据结构。比如查看当前运行队列:
(gdb) p runqueues
或者查看当前进程的信息:
(gdb) p current->pid
(gdb) p current->comm
有时候内核会崩溃,这时候回溯调用栈就很重要。GDB的backtrace命令可以显示函数调用关系:
(gdb) backtrace
在VSCode中,调用栈会自动显示在调试侧边栏,点击就可以跳转到对应代码位置。
我还经常使用watchpoint来监视变量变化。比如想跟踪某个全局变量的修改:
(gdb) watch global_variable
这样当这个变量被修改时,GDB就会中断,你可以看到是哪里修改了它。
调试模块加载也是常见需求。首先要在模块代码中设置断点,然后让GDB在模块加载时自动添加符号表:
(gdb) set solib-search-path /path/to/module
(gdb) break module_function
这些技巧需要一些练习才能熟练掌握,但一旦掌握,就能大大提高内核调试的效率。
7. 常见问题与解决方案
在Qemu中调试Linux内核时,难免会遇到各种问题。我整理了一些常见问题和解决方法,希望能帮你少走弯路。
最常见的问题是GDB连接失败。如果出现"Connection refused"或"Connection timeout",首先检查Qemu是否正确启动了GDB服务器:
netstat -tlnp | grep 1234
应该看到Qemu进程在监听1234端口。如果没有,检查启动脚本中是否包含了-gdb tcp::1234 -S参数。
另一个常见问题是断点不生效。这通常是因为地址随机化没有关闭,或者调试信息没有包含在内核中。确保在menuconfig中正确设置了调试选项,并且重新编译了内核。
有时候GDB会显示"找不到符号"的错误。这通常是因为GDB没有加载正确的符号表。确保在GDB中正确指定了vmlinux文件的路径:
(gdb) file /path/to/linux-5.15.30/vmlinux
在VSCode中,检查launch.json中的program字段是否正确。
性能问题也需要注意。在Qemu中调试内核可能会比较慢,特别是单步执行时。如果觉得太慢,可以尝试减少模拟的CPU核心数或内存大小。在我的经验中,1-2个CPU核心和1GB内存对于大多数调试任务已经足够了。
还有一个棘手的问题是硬件模拟不完整。Qemu的virt机器是纯软件模拟的,某些硬件特性可能不支持。如果你在调试驱动程序时遇到问题,可能需要检查Qemu是否支持相关硬件特性。
最后,记得定期保存你的工作。虽然Qemu环境是虚拟的,但内核修改和调试配置却是实实在在的。我建议使用版本控制系统来管理内核源码的修改,这样即使出了问题也能快速恢复。
调试内核确实需要耐心和技巧,但一旦掌握了方法,就能深入理解Linux内核的工作原理。我在这个过程中学到了很多,希望这些经验对你有帮助。
2617



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



