很多童鞋想从第一行汇编代码开始调试 Linux 内核,那么本文介绍如何使用 BiscuitOS 实现这个计划。在实践之前先了解一下 Linux 内核启动的几个核心阶段: 首先是上电启动 BIOS,BIOS 初始化基础的硬件之后,接着是 GRUB 选择要启动的内核,选择完毕之后加载内核,此时才是内核正在启动的时刻,其分作以下几步:
- Bootloader: 在 X86 架构下,Linux 内核启动前的一小段代码,低版本内核里,BIOS 将 bootloader 加载到 0x7C00 处,然后将执行权限交接给内核进行启动,在高版本内核里,这个地址修改为 0x10000. bootloader 的主要主要是在加载内核之前将系统初始化到一个已知的的环境.
- Compressed Kernel: 在高版本内核里,内核镜像不是直接加载到系统物理内存的,因为其体积太大,导致加载太慢,因此先将内核镜像压缩成一个体积小的压缩镜像,然后这个阶段用于将压缩镜像加载到物理内存,并将内核解压到指定物理内存.
- Boot ASM: 解压完之后,将 RIP 指向内核镜像的入口,在 x86 架构下一般是 startup_64, 此时内核正式启动,不过这个阶段是汇编代码.
- Boot C: 内核启动完毕之后,开始加载运行第一行 C 代码,在 x86 架构下一般是 x86_64_start_kernel
- Live kernel: 这个阶段内核已经启动完整,并持续运行中.
本文档用户介绍在 X86 高版本内核里,如何使用 GDB 从 Bootloader 第一行汇编开始单步调试内核. 通过单步调试有利于弄明白内核是如何启动,以及一些早期内存分配器的工作原理等。话不多说,接下来一步步介绍如何实践,参考如下命令(在部署之前请确保已经部署 BiscuitOS 开发环境,如果未部署请参考 《BiscuitOS 用户手册 - 1.2》):
# 切换到 BiscuitOS 项目目录
cd /BiscuitOS
# 选择开发环境,如果已经选择过可以跳过,这里与 linux 6.10 X86 为例
make linux-6.10-x86_64_defconfig
make docker
# 通过 Kbuild 选择需要部署的应用程序
make menuconfig
[*] Package --->
[*] BiscuitOS Debug Stub Set
[*] BiscuitOS Linux BOOT KERNEL
# 配置完毕保存,然后进行部署
make
# 切换到实践案例所在目录
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
# 准备依赖工具
make prepare
# 编译实践案例
make download
当指定以上命令之后,可以看到上图文件. 接下里对不同阶段进行调试.
Bootloader 是操作系统启动流程中的关键组成部分,其主要作用是在系统上电或重启后,初始化处理器和基本硬件环境,为内核加载和运行做好准备。在 Linux 启动过程中,bootloader 首先执行 arch/x86/boot/header.S 文件中的汇编代码,该部分负责设置处理器工作模式、建立最基础的内存空间、检测和初始化必要的外设(如内存、显卡、控制台等),并将内核映像从存储设备加载到内存的指定位置。除此之外,bootloader 还会传递启动参数和系统配置信息,为内核提供一个受控、可预测的运行环境。最终,bootloader 跳转到内核入口点,正式将控制权交给操作系统内核。可以说,bootloader 是连接硬件与操作系统内核的桥梁,确保内核能够顺利、可靠地启动和运行. 对于这部分的调试,需要准备两个终端,分别执行如下命令:
# 终端 1
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make bootloader
# 终端 2
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make debug
此时可以看到打印字符串 “Debug ON Bootloader Kernel” 说明调试启动成功,此时内核已经停在 “Booting from ROM..”, 此时显示了 Bootloader 启动的第一行汇编,此时对应内核 “arch/x86/boot/header.S” 里的 “start_of_setup”. 接着使用 “si” 命令运行下一行汇编, 可以看到其会将接下来三行汇编都打印,利于参考源码分析逻辑. 开发者可以使用 GDB 提供的各种命令辅助调试.
在 Linux 启动过程中,bootloader 完成基本硬件初始化后,会将压缩的内核镜像加载到内存中,随后进入解压阶段,这一阶段的主要任务在 arch/x86/boot/compressed/head_64.S 文件中的 startup_32 入口处展开。此阶段的核心任务包括建立最小的执行环境,切换处理器到合适的模式(如从实模式切换到保护模式或长模式),初始化基本的段寄存器和堆栈,为后续的解压操作做准备。同时,它还负责解析 boot 参数、设置页表、为内核分配合适的内存空间,并准备和调用解压缩器,将压缩的 Linux 内核镜像解包到目标物理地址。这个阶段的目标,是在复杂的硬件和内存限制下,安全可靠地将压缩内核解压到正确的内存位置,并为内核的最终启动做好准备。完成解压后,控制权才会转交给真正的内核入口,继续系统引导. 对于这部分的调试,需要准备两个终端,分别执行如下命令:
# 终端 1
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make compressed
# 终端 2
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make debug
此时可以看到打印字符串 “Debug ON Compressed Kernel” 说明调试内核压缩阶段成功,此时内核已经停住, 此时显示了 Compressed 内核的第一行汇编,此时对应内核 “arch/x86/boot/compressed/head_64.S” 里的 “startup_32”. 接着使用 “si” 命令运行下一行汇编, 可以看到其会将接下来三行汇编都打印,利于参考源码分析逻辑. 开发者可以使用 GDB 提供的各种命令辅助调试.
在 Linux 内核被解压到指定物理内存后,内核启动的前期阶段主要由汇编代码完成,这一阶段的核心任务是对处理器和系统环境进行最基本且关键的初始化。具体来说,汇编代码会完成处理器模式的设置(如切换到内核所需的长模式)、初始化段寄存器和内核栈,为 C 语言内核代码的执行建立安全的基础环境。同时,这一阶段还会设置早期的页表和内存映射,确保内核能够正确访问物理和虚拟内存空间。此外,汇编代码还会完成多核处理器的相关初始化,包括主核与辅助核的启动准备、BSS 段清零、异常向量表的设置等。通过这些步骤,内核为后续复杂的 C 语言初始化流程打下坚实基础,确保系统能够平稳地过渡到更高层次的内核功能初始化和各类驱动加载. 对于这部分的调试,需要准备两个终端,分别执行如下命令:
# 终端 1
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make boot
# 终端 2
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make debug
此时可以看到打印字符串 “Debug on BOOT-ASM Kernel” 说明调试内核启动 ASM 阶段成功,此时内核已经停住, 此时终端 1 上被划分成两个窗口,上面的窗口是对应的源码,此时对应内核 “arch/x86/kernel/head_64.S” 里的 “startup_64”. 接着在下面窗口使用 “si” 命令运行下一行汇编, 可以上面的窗口跳转到下一行汇编,利于参考源码分析逻辑. 开发者可以使用 GDB 提供的各种命令辅助调试.
在 Linux 内核早期启动时,完成汇编阶段后,内核便进入 C 语言阶段,这一阶段的主要任务是完成内核剩余的初始化工作,为系统的正常运行做好全面准备。具体来说,C 语言阶段会初始化各种内核子系统,包括内存管理、进程管理、中断和定时器、设备驱动框架等,同时根据硬件检测结果配置系统资源。此阶段还会解析和处理启动参数,初始化文件系统、构建内核页表,启动内核线程,并为用户空间提供必要的接口和支持。此外,C 语言阶段还会加载和初始化各类硬件驱动,确保系统能够识别和管理各类外部设备。完成这些初始化之后,内核会挂载根文件系统,最终启动第一个用户空间进程(如 init 或 systemd),从而将控制权交给用户空间,整个系统正式进入正常运行状态. 对于这部分的调试,需要准备两个终端,分别执行如下命令:
# 终端 1
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make cboot
# 终端 2
cd /BiscuitOS/output/linux-6.10-x86_64/package/BiscuitOS-DEBUG-BOOT-KERNEL-default
make debug
此时可以看到打印字符串 “Debug on BOOT-C Kernel” 说明调试内核启动 ASM 阶段完成,开始运行第一行 C 代码,此时内核已经停住, 此时终端 1 上被划分成两个窗口,上面的窗口是对应的源码,此时对应内核 “arch/x86/kernel/head64.c” 里的 “x86_64_start_kernel”. 接着在下面窗口使用 “n” 命令运行下一行 C 代码, 可以上面的窗口跳转到下一行 C 代码,也可以使用 “ni” 命令进入函数内部. 利于参考源码分析逻辑. 开发者可以使用 GDB 提供的各种命令辅助调试.