基础环境
操作系统: Ubuntu 22.04.5 LTS
实验根目录: /data/linux-kernel-lab
使用工具: VS code、gcc、wget、git
使用源码:
搭建阅读环境
本地阅读源码
VS code + clangd插件
生成compile_commands.json
#==
#-- 进入内核源码根目录
cd linux-kernel
#-- 执行命令生成 compile_commands.json
python3 scripts/clang-tools/gen_compile_commands.py
#-- 确认文件在源码根目录生成
ls -alh compile_commands.json
#== End
clangd插件通过compile_commands.json来理解内核的复杂的宏定义、条件编译和包含路径。

在线阅读源码
在线阅读Linux Kernel源码可以使用linux v6.18-rc7在线阅读源码,支持所有内核版本的完美跳转。

搭建调试环境
代码不能光“看”,还要“跑”起来看!所以,我们需要搭建一套调试环境。
首先,我们需要一个极简的根文件系统,我们这里采用BusyBox构建根文件系统。其次,我们需要下载Linux Kernel代码在本地编译,编译采用gcc编译器,这个在Linux上自带。最后,我们还需要一个虚拟机环境来运行内核,这里采用QEMU做为运行环境,过程中采用gcc自带的gdb进行断点调试代码。
整体操作下来比较繁琐,这里提炼成了几个shell脚本文件,边用边调整。实验中涉及到的脚本文件如无特别声明,则均保存在实验根目录下。
BusyBox 源码包下载
博主这边通过脚本自动下载经常出错,所以只能提前手动下载好。如果按照下面流程使用脚本自动下载出错,建议手动下载好再执行脚本。
公共环境变量
公共环境变量配置文件
filename: env.src
# 软件依赖清单,编译过程中遇到缺少的随时补充更新
PACKAGES_LIST=(bc bison build-essential cpio flex gzip libncurses-dev wget)
# 设置基目录
BASE_DIR=$(dirname $(readlink -f $0))
# 设置 Linux Kernel 源码目录
KERNEL_SRC_DIR=$BASE_DIR/linux-kernel
# 设置 rootfs 构建根目录
BASE_ROOTFS_DIR=$BASE_DIR/rootfs_build
# 根目录
ROOTFS_DIR=$BASE_ROOTFS_DIR/rootfs
# BusyBox 源码包 URL
SRC_URL=https://busybox.net/downloads/busybox-1.38.0.tar.bz2
# BusyBox 源码包文件名
SRC_FILE=$(basename $SRC_URL)
# BusyBox 目录名
SRC_NAME=${SRC_FILE%%\.tar*}
# BusyBox 源码目录
BASE_ROOTFS_SRC=$BASE_ROOTFS_DIR/$SRC_NAME
# BusyBox 安装目录
BUSYBOX=$BASE_ROOTFS_SRC/_install
# initramfs 镜像
INITRAMFS=$KERNEL_SRC_DIR/rootfs.cpio.gz
# qemu command
COMMAND_QEMU=qemu-system-x86_64
#== End
前置依赖包检查
这里写了一个脚本来帮助检查依赖包安装情况,
filename: 01-check-packages.sh
#!/bin/bash
# 加载公共配置
source env.src
function check_package() {
dpkg --status $1 2>1 >/dev/null
if [ $? == 0 ]; then
echo -e "\033[032m==INFO=> $1 installed.\033[0m"
else
echo -e "\033[031m==ERROR=> $1 uninstall.\033[0m"
sudo apt -y install $1
fi
}
for package_name in ${PACKAGES_LIST[@]}; do
check_package $package_name
done
执行效果如图所示:

编译 BuxyBox
filename: 02-build-busybox.sh
#!/bin/bash
# 加载公共配置
source env.src
# 创建目录
[[ ! -e ${BASE_ROOTFS_DIR} ]] && mkdir -p ${BASE_ROOTFS_DIR}
[[ -e $BASE_ROOTFS_SRC ]] && \rm -rf $BASE_ROOTFS_SRC
[[ -e $ROOTFS_DIR ]] && \rm -rf $ROOTFS_DIR
mkdir $ROOTFS_DIR
# 下载源码文件,网速慢的可以提前下载好,下载完成后再次执行本脚本即可。
if [ ! -e $SRC_FILE ]; then
wget -c -o $SRC_FILE $SRC_URL
fi
if [ $? != 0 ]; then
echo "Download $SRC_URL failed."
exit
fi
tar xf $SRC_FILE -C $BASE_ROOTFS_DIR
if [ -e $BASE_ROOTFS_SRC ]; then
#-- 1.0. 进入源码目录,准备静态编译 BusyBox
cd $BASE_ROOTFS_SRC
# 1.1. 生成默认配置
make defconfig
# 1.2. 开启静态编译选项
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
# 1.3. 使用所有 CPU 核心编译
make -j$(nproc)
# 1.4. 将编译好的 busybox 及其符号链接安装到当前目录的 _install 中
make install
#-- 2.0. 进入 rootfs 目录,准备构建根文件系统的目录结构
cd $ROOTFS_DIR
# 2.1. 复制 BusyBox编译出的基础命令
cp -r $BUSYBOX/* .
# 2.2. 创建 Linux 标准的目录结构
mkdir -p proc sys dev etc/init.d tmp root home mnt
# 2.3. 设置目录权限
chmod 1777 tmp
#-- 3.0 创建核心设备节点
# 创建 /dev/console (主设备号5,次设备号1)
sudo mknod dev/console c 5 1
sudo chmod 622 dev/console
# 创建 /dev/null (主设备号1,次设备号3)
sudo mknod dev/null c 1 3
sudo chmod 666 dev/null
# 创建 /dev/tty (主设备号5,次设备号0)
sudo mknod dev/tty c 5 0
sudo chmod 666 dev/tty
# 创建 /dev/ttyS0 (主设备号4,次设备号64)
sudo mknod dev/ttyS0 c 4 64
sudo chmod 666 dev/ttyS0
# 添加 root 分组信息
cat <<EOF >etc/group
root:x:0:
EOF
# 添加 root 用户信息
cat <<EOF >etc/passwd
root:x:0:0:root:/root:/bin/sh
EOF
chmod 644 etc/group etc/passwd
#-- 启动必需文件,缺少它内核无法正常启动
cat <<EOF >init
#!/bin/bash
# 1. 打印启动成功信息(带点仪式感)
echo "============================================="
echo " Welcome to Minimal Linux (BusyBox + v6.18)!"
echo "============================================="
# 2. 启动一个交互式的 ash shell
# -i 表示交互式,让系统停在这里等待你的输入,而不是直接退出
exec /bin/sh -i
EOF
#-- 启动必需文件,缺少它内核无法正常启动
cat <<EOF >etc/init.d/rcS
#!/bin/sh
# /etc/init.d/rcS
# 1. 挂载内核提供的虚拟文件系统(proc、sys、dev)
# 这是用户态程序与内核交互的桥梁,极其重要!
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
# 2. 设置主机名
hostname rtfsc8.rtfsc8.top
echo "============ System Initialized. ============"
EOF
# 赋予执行权限
chmod +x init
chmod +x etc/init.d/rcS
fi
脚本执行效果如下图所示:

打包根文件系统镜像
filename: 03-package-initramfs.sh
#!/bin/bash
# 加载公共配置
source env.src
if [ -e $ROOTFS_DIR ]; then
# 进入根文件系统目录
cd $ROOTFS_DIR
# 开始打包
find . -print0 | \
cpio --null -ov --format=newc | \
gzip -9 > ${INITRAMFS}
fi
根文件系统打包效果如下图所示:

根文件系统压缩包:

编译Linux内核
filename: build-kernel.sh
#!/bin/bash
# 加载公共配置
source env.src
if [ -e $KERNEL_SRC_DIR ]; then
# 进入源码目录
cd $KERNEL_SRC_DIR
cat <<EOF >debug.config
# 开启调试信息
CONFIG_DEBUG_INFO_NONE=n
CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y
CONFIG_DEBUG_KERNEL=y
CONFIG_FRAME_POINTER=y
# 可选但推荐:改善 GDB 调试体验
CONFIG_GDB_SCRIPTS=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
EOF
# 开始编译
make clean && make defconfig && \
./scripts/kconfig/merge_config.sh .config debug.config && \
make olddefconfig && \
make -j$(nproc) && \
echo "================ Show Debug Info ================" && \
readelf -S vmlinux | grep debug_info
fi
内核源码编译截图如下:
调试开关配置合并:

编译完成,检查内核存在调试信息:

启动QEMU
filename: start-kernel.sh
#!/bin/bash
# 加载公共配置
source env.src
if [ -e $KERNEL_SRC_DIR ]; then
# 进入源码目录
cd $KERNEL_SRC_DIR
# 开始启动QEMU
exec $COMMAND_QEMU \
-kernel arch/x86/boot/bzImage \
-initrd rootfs.cpio.gz \
-append "console=ttyS0 root=/dev/ram rdinit=/init nokaslr" \
-nographic -m 512M
# -nographic: 禁用图形界面,将虚拟机的串口输出重定向到
# 当前终端(配合 console=ttyS0)
# root=/dev/ram: 告诉内核根文件系统在内存里
# rdinit=/init: 明确告诉内核启动后执行根目录下的 /init 脚本
# nokaslr: 关键调试参数!关闭内核地址空间布局随机化,
# 否则每次启动内核函数的内存地址都会变,GDB
# 断点会全部失效
fi
内核运行效果如下:

内核信息:

关机:

启动QEMU远程调试
filename: start-qemu.sh
#!/bin/bash
# 加载公共配置
source env.src
if [ -e $KERNEL_SRC_DIR ]; then
# 进入源码目录
cd $KERNEL_SRC_DIR
# 开始启动QEMU
exec $COMMAND_QEMU \
-kernel arch/x86/boot/bzImage \
-initrd rootfs.cpio.gz \
-append "console=ttyS0 root=/dev/ram rdinit=/init nokaslr" \
-nographic -m 512M \
-s -S
# -nographic: 禁用图形界面,将虚拟机的串口输出重定向到
# 当前终端(配合 console=ttyS0)
# root=/dev/ram: 告诉内核根文件系统在内存里
# rdinit=/init: 明确告诉内核启动后执行根目录下的 /init 脚本
# nokaslr: 关键调试参数!关闭内核地址空间布局随机化,
# 否则每次启动内核函数的内存地址都会变,GDB
# 断点会全部失效
# -s: 相当于 -gdb tcp::1234,在 1234 端口开启 gdb 服务
# -S: 启动时冻结 CPU,直到 GDB 发出 continue 命令。此时终端
# 会卡住,没有任何输出。
fi
刚启动调试模式,等待调试中:

逐行执行,当console_init()执行完成后,控制台开始出现文字信息:

执行完rest_init(),系统启动完成:

按下Enter键进入命令行,执行命令如下:

执行poweroff命令后,qemu退出执行,gdb调试自动断开远程连接。
启动调试
filename: start-gdb.sh
#!/bin/bash
# 加载公共配置
source env.src
if [ -e $KERNEL_SRC_DIR ]; then
# 进入源码目录
cd $KERNEL_SRC_DIR
cat <<EOF >gdb.cmd
file vmlinux
target remote localhost:1234
break start_kernel
continue
info sources
info breakpoints
EOF
# 开始调试
# gdb vmlinux
gdb -x gdb.cmd
fi
远程调试连接启动:

开始逐行调试:

查看变量信息:

qemu退出后,gdb窗口自动detach,然后输入exit或quit退出调试。
手动杀死qemu进程
若是出现qemu虚拟机无法操作,可使用以下脚本手动杀死进程。
filename: stop-qemu.sh
#!/bin/bash
# 加载公共配置
source env.src
echo "================ Begin searching $COMMAND_QEMU process..."
\ps -ef | grep $COMMAND_QEMU
echo "================ Begin killing $COMMAND_QEMU process..."
\ps -ef | grep $COMMAND_QEMU | \
grep -v grep | \
awk '{print $2}' | \
xargs kill -9 2>/dev/null
echo "================ Kill done."
脚本使用流程说明
完整流程如下图所示。
- 若更新根文件系统相关文件,则重新依次执行02-build-busybox.sh、03-package-initramfs.sh脚本;
- 若修改内核代码,则重新执行build-kernel.sh脚本;
然后重新启动start-qemu.sh和start-gdb.sh调试。
flowchart TB;
check[01-check-packages.sh 前置依赖包检查];
subgraph buildBusybox [BusyBox根文件系统构建]
building[02-build-busybox.sh 编译BusyBox];
package[03-package-initramfs.sh 打包根文件系统];
end
subgraph kernel [编译Linux内核代码]
buildKernel[build-kernel.sh 编译内核];
end
subgraph commands [执行命令验证内核]
cmd_id[id 命令查看当前用户];
cmd_groups[groups 命令查看组名];
cmd_whoami[whoami 命令查看当前用户];
end
subgraph debugCommands [gdb调试命令]
gdb_file[file vmlinux 加载携带调试信息的内核];
gdb_target[target remote localhost:1234 连接远程调试];
gdb_break[break start_kernel 在start_kernel函数设置断点];
gdb_continue[continue 继续向下执行];
gdb_infobp[info breakpoints 查看断点列表];
gdb_list[list 查看代码];
gdb_next[next 执行下一行];
gdb_source[source gdb.cmd 加载gdb.cmd预先写好的批量命令方便调试];
gdb_detach[detach 断开虚拟机连接];
end
startQemu[start-qemu.sh 启动内核远程调试];
startGdb[start-gdb.sh 连接远程调试];
startKernel[start-kernel.sh 虚拟运行内核];
stopQemu[stop-qemu.sh 异常情况手动停止QEMU];
check-->buildBusybox-->kernel;
kernel-- 1号终端验证内核是否运行正常 -->startKernel-->commands;
kernel-- 2号终端打开调试模式 -->startQemu-- 3号终端gdb远程调试 -->startGdb-->debugCommands;
startQemu-- 2号终端卡死无法操作则在4号终端执行 -->stopQemu;
愉快的开始阅读源码吧
写到这里,基于BusyBox和QEMU的Linux Kernel代码调试环境已然完成搭建,接下来就是愉快的阅读内核源码,灵活运用脚本工具去玩啦~
留言