一、实验目的
本实验基于 QEMU + GDB 远程调试,对 Linux 3.18.6 内核的启动过程进行源码级跟踪,重点观察从start_kernel()到第一个用户态进程 init(PID 1)启动的完整链路。通过本次实验,可以掌握以下核心能力:
- 掌握 QEMU 加载内核镜像(
bzImage)与根文件系统(initrd)启动 Linux 的方法。 - 掌握使用 QEMU 的
-s -S选项配合 GDB 进行内核源码级远程调试的方法。 - 使用 GDB 跟踪 Linux 3.18.6 内核从
start_kernel到第一个用户态进程(init,PID 1)启动的完整过程。 - 理解内核启动两大阶段的分界:0 号进程(
init_task)的自举与进程 1 的诞生。
二、实验环境与命令说明
2.1 实验环境概览
| 项目 | 内容 |
|---|---|
| 实验环境 | 实验楼 LinuxKernel 虚拟机(Ubuntu,QEMU + GDB) |
| 内核版本 | linux-3.18.6 |
| 根文件系统 | rootfs.img(内含 MenuOS) |
| 调试方式 | QEMU 内置 gdbserver(-s -S)+ GDB 远程调试 |
| 跟踪主线 | start_kernel()→rest_init()→kernel_init()→run_init_process() |
| 断点地址 | 0xc1a2f7c5(init/main.c:501,start_kernel函数体第一条语句) |
2.2 启动内核
进入实验目录并启动内核:
cd ~/LinuxKernel/ ls qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img-kernel:直接加载可引导的 x86 内核镜像bzImage(QEMU 自带的 SeaBIOS 会按 Linux Boot Protocol 跳转,不经过 GRUB);-initrd:把rootfs.img作为初始内存根文件系统传给内核。
2.3 以调试模式启动内核
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S| 选项 | 含义 |
|---|---|
-S | freeze CPU at startup,QEMU 启动后冻结 CPU,等待调试器发c才继续执行 |
-s | -gdb tcp::1234的简写,在 TCP1234端口开启 gdbserver |
-gdb tcp:xxxx | 若不使用 1234 端口,可用该选项自定义端口 |
2.4 建立 GDB 调试会话
在另开的一个 shell 窗口中:
gdb (gdb) file linux-3.18.6/vmlinux # 在 target remote 之前加载符号表 (gdb) target remote:1234 # 连接 QEMU 的 gdbserver (gdb) break start_kernel # 断点可在 target remote 之前或之后设置 (gdb) c # 让 QEMU 上的 Linux 继续运行要点:
vmlinux是编译内核时产生的、带调试信息的 ELF 文件(未压缩),bzImage是被压缩的引导镜像。GDB 必须加载vmlinux,才能把地址翻译成函数名与源码行。
三、实验原理
3.1 x86 内核启动流程总览
BIOS/SeaBIOS └─ 引导设备 → arch/x86/boot/header.S → boot/compressed/head_64.S(解压内核) └─ arch/x86/kernel/head_64.S:startup_64 → 建立初始页表、进入长模式 └─ arch/x86/kernel/head64.c:x86_64_start_kernel() └─ arch/x86/kernel/head.c:x86_64_start_reservations() └─ init/main.c:start_kernel() ← 本实验跟踪起点 └─ rest_init() ├─ kernel_thread(kernel_init) → PID 1 ├─ kernel_thread(kthreadd) → PID 2 └─ cpu_startup_entry() → PID 0 空闲进程start_kernel()是内核与体系结构无关部分的 C 语言入口(每个体系结构最终都会调用它),因此是跟踪内核启动最理想的断点位置。
3.2 start_kernel 之前做了什么
在断点命中之前,QEMU 的窗口中已经打印了下面这些信息:
SeaBIOS (version 1.7.4-20140917_150519-kapok) iPXE (http://ipxe.org) 00:03.0 C900 PCI2.10 PnP PMM+07FC0FD0+07F20... Booting from ROM.. early console in decompress_kernel Decompressing Linux... Parsing ELF... No relocation needed... done Booting the kernel.这说明:
- SeaBIOS完成硬件自检后,按 Linux Boot Protocol 把
bzImage加载到内存并跳转; decompress_kernel在保护模式下把压缩的内核解压到最终位置(early console in decompress_kernel是解压程序的早期控制台输出);Booting the kernel.之后进入startup_64,建立长模式与临时页表(内核映射到0xffffffff80000000起始的高地址区),随后才调用start_kernel()。
3.3 init 进程的前世今生
内核启动过程中的"第一个进程"并不是直接由调度器创建出来的,它有一条清晰的演化链:
init_task (0 号进程 / swapper,静态定义的 task_struct) │ start_kernel() 内部一直以"当前进程 = init_task"的身份运行 └─ rest_init() ├─ kernel_thread(kernel_init, NULL, CLONE_FS) → 创建 PID 1 │ └─ kernel_init() → kernel_init_freeable() → run_init_process("/init") │ └─ do_execve() ⇒ 用户态 init(MenuOS)真正开始运行 └─ cpu_startup_entry() → cpu_idle() → 0 号进程退化为空闲进程- 0 号进程:
init_task是编译期静态初始化的task_struct(定义在init/init_task.c),没有通过fork()产生,它是所有进程的祖先; - 1 号进程 init:由
rest_init()通过kernel_thread()创建,最终通过execve装入用户态程序,成为所有用户态进程的祖先; - 2 号进程 kthreadd:内核线程的父进程,后续所有内核线程由它派生。
四、实验步骤与结果分析
4.1 启动内核并进入 MenuOS
进入~/LinuxKernel/目录,可以看到内核源码目录linux-3.18.6、根文件系统rootfs.img以及 MenuOS 源码目录menu。命令执行后,弹出 QEMU 窗口,内核开始输出启动日志,可见cdrom: Uniform CD-ROM driver Revision: 3.20、console [netcon0] enabled、netconsole: network logging started、ALSA device list: No soundcards found.等子系统初始化日志,以及Freeing unused kernel memory与Write protecting the kernel text / read-only data。屏幕中央出现了 MenuOS 的 ASCII 艺术字,并给出MenuOS>>提示符,说明内核启动完成、用户态 init 程序已经跑起来了。
4.2 MenuOS 命令测试
MenuOS 是《软件工程 C 编码实践篇》的课程项目,内核启动完成后作为 init 进程(PID 1)运行,支持三个命令:help、version、quit。
| 命令 | 功能 |
|---|---|
help | 显示 MenuOS 支持的所有命令及说明 |
version | 显示 MenuOS 的版本信息 |
quit | 退出 MenuOS(在实验环境下通常表现为系统停机/退出) |
MenuOS 的命令框架通常由menu.c中的命令表驱动,核心结构类似:
typedef struct cmd { const char *name; const char *help; int (*func)(int argc, char *argv[]); } cmd_t; static cmd_t cmd_table[] = { { "help", "show all commands", cmd_help }, { "version", "show version info", cmd_version }, { "quit", "exit MenuOS", cmd_quit }, /* 可在此处继续添加自定义命令 */ };本实验的核心任务是GDB 跟踪内核启动,因此 MenuOS 部分只验证其能够被内核正常启动(MenuOS>>即为证据),不再展开其命令实现。
4.3 以调试模式重启内核
关闭上一次 QEMU 窗口后,再次以调试模式启动内核(加了-s -S)。此时 QEMU 不显示 SeaBIOS 的任何输出——因为-S把 CPU 冻结在了启动的第一条指令处,等待 GDB 的c命令。QEMU 窗口标题为QEMU [Stopped],屏幕全黑。这正是-S(freeze CPU at startup)的效果。
4.4 启动 GDB、加载符号表
新开一个 shell 窗口,执行gdb进入调试器。在 GDB 中先加载符号表:
(gdb) file linux-3.18.6/vmlinux Reading symbols from linux-3.18.6/vmlinux...done.file linux-3.18.6/vmlinux读入符号表,GDB 回显Reading symbols from linux-3.18.6/vmlinux...done.,此时 GDB 已经能把内核地址映射到函数名与源码行。
4.5 建立远程调试连接
(gdb) target remote:1234 Remote debugging using :1234 0x0000fff0 in ?? ()target remote:1234与 QEMU 的 gdbserver 建立连接,GDB 回显Remote debugging using :1234。
注意
0x0000fff0 in ?? ():CPU 目前停在0x0000fff0,这是 x86 实模式复位后执行的第一条指令(BIOS 跳转入口),此时内核尚未解压、符号表也还没有与运行地址对应,所以 GDB 显示?? ()。这是-S冻结 CPU 时的正常现象,不是错误。
4.6 在 start_kernel 处下断点并继续运行
(gdb) break start_kernel Breakpoint 1 at 0xc1a2f7c5: file init/main.c, line 501.断点被成功设置,GDB 直接给出了源码位置init/main.c:501——这正是start_kernel函数体开始的那一行{。同时给出了运行时地址0xc1a2f7c5。右侧 QEMU 窗口仍然是[Stopped]全黑状态,等待c命令。
输入c后 QEMU 重新开始运行:窗口先显示 BIOS 与内核解压信息,随后在执行到start_kernel的第一条指令时再次停住:
SeaBIOS (version 1.7.4-20140917_150519-kapok) iPXE (http://ipxe.org) 00:03.0 C900 PCI2.10 PnP PMM+07FC0FD0+07F20... Booting from ROM.. early console in decompress_kernel Decompressing Linux... Parsing ELF... No relocation needed... done Booting the kernel.(gdb) c Continuing. Breakpoint 1, start_kernel () at init/main.c:501 501 { (gdb) _至此成功把内核冻结在了start_kernel的入口,后续所有单步/断点操作都以这里为起点。这也说明-s -S+ GDB 的组合确实可以在内核启动的最早阶段切入并精确控制执行。
4.7 查看源码,验证断点位置
使用list命令查看init/main.c中start_kernel附近的源码。由于 GDB 窗口的左侧边缘被截去了一列字符,行号496~505被显示成了96、97、98、99、00、01、02、03、04、05。据此还原list的真实输出如下:
496 pgtable_init(); 497 vmalloc_init(); 498