☰
Linux 内核启动过程跟踪与分析:基于 QEMU + GDB 从 start_kernel 到 init 进程启动
2026/10/15 19:27:34 网站建设 项目流程

一、实验目的

本实验基于 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
选项含义
-Sfreeze 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.

这说明:

  1. SeaBIOS完成硬件自检后,按 Linux Boot Protocol 把bzImage加载到内存并跳转;
  2. decompress_kernel在保护模式下把压缩的内核解压到最终位置(early console in decompress_kernel是解压程序的早期控制台输出);
  3. 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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询