内容纲要

基础环境

操作系统: Ubuntu 22.04.5 LTS
实验根目录: /data/linux-kernel-lab
使用工具: VS code、gcc、wget、git
使用源码:

  1. busybox-1.38.0.tar.bz2
  2. linux v6.18-rc7
  3. VS code多平台安装包下载
  4. VS code多平台安装包下载2

搭建阅读环境

本地阅读源码

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来理解内核的复杂的宏定义、条件编译和包含路径。

file

在线阅读源码

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

file

搭建调试环境

代码不能光“看”,还要“跑”起来看!所以,我们需要搭建一套调试环境。

首先,我们需要一个极简的根文件系统,我们这里采用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

执行效果如图所示:

file

编译 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

脚本执行效果如下图所示:

file

打包根文件系统镜像

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

根文件系统打包效果如下图所示:

file

根文件系统压缩包:

file

编译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

内核源码编译截图如下:

调试开关配置合并:

file

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

file

启动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

内核运行效果如下:

file

内核信息:

file

关机:

file

启动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

刚启动调试模式,等待调试中:

file

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

file

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

file

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

file

执行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

远程调试连接启动:

file

开始逐行调试:

file

查看变量信息:

file

qemu退出后,gdb窗口自动detach,然后输入exitquit退出调试。

手动杀死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."

脚本使用流程说明

完整流程如下图所示。

  1. 若更新根文件系统相关文件,则重新依次执行02-build-busybox.sh03-package-initramfs.sh脚本;
  2. 若修改内核代码,则重新执行build-kernel.sh脚本;

然后重新启动start-qemu.shstart-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;

愉快的开始阅读源码吧

写到这里,基于BusyBoxQEMULinux Kernel代码调试环境已然完成搭建,接下来就是愉快的阅读内核源码,灵活运用脚本工具去玩啦~

References

  1. busybox-1.38.0.tar.bz2
  2. linux v6.18-rc7
  3. VS code多平台安装包下载
  4. VS code多平台安装包下载2
  5. linux v6.18-rc7在线阅读源码
最后修改日期: 2026-09-11

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。