Linux 内核启动过程跟踪与分析:基于 QEMU + GDB 从 start_kernel 到 init 进程启动 一、实验目的本实验基于 QEMU GDB 远程调试对 Linux 3.18.6 内核的启动过程进行源码级跟踪重点观察从start_kernel()到第一个用户态进程 initPID 1启动的完整链路。通过本次实验可以掌握以下核心能力掌握 QEMU 加载内核镜像bzImage与根文件系统initrd启动 Linux 的方法。掌握使用 QEMU 的-s -S选项配合 GDB 进行内核源码级远程调试的方法。使用 GDB 跟踪 Linux 3.18.6 内核从start_kernel到第一个用户态进程initPID 1启动的完整过程。理解内核启动两大阶段的分界0 号进程init_task的自举与进程 1 的诞生。二、实验环境与命令说明2.1 实验环境概览项目内容实验环境实验楼 LinuxKernel 虚拟机UbuntuQEMU GDB内核版本linux-3.18.6根文件系统rootfs.img内含 MenuOS调试方式QEMU 内置 gdbserver-s -S GDB 远程调试跟踪主线start_kernel()→rest_init()→kernel_init()→run_init_process()断点地址0xc1a2f7c5init/main.c:501start_kernel函数体第一条语句2.2 启动内核进入实验目录并启动内核cd ~/LinuxKernel/ ls qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img-kernel直接加载可引导的 x86 内核镜像bzImageQEMU 自带的 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 startupQEMU 启动后冻结 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.Sstartup_64 → 建立初始页表、进入长模式 └─ arch/x86/kernel/head64.cx86_64_start_kernel() └─ arch/x86/kernel/head.cx86_64_start_reservations() └─ init/main.cstart_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 PMM07FC0FD007F20... 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() ⇒ 用户态 initMenuOS真正开始运行 └─ 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]屏幕全黑。这正是-Sfreeze 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 PMM07FC0FD007F20... 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 窗口的左侧边缘被截去了一列字符行号496505被显示成了96、97、98、99、00、01、02、03、04、05。据此还原list的真实输出如下496 pgtable_init(); 497 vmalloc_init(); 498