Linux 内核启动过程全解析:从按下电源键到解压内核的第一条指令(linux-insides Booting 章节导读与实践指南)
发布时间:2026/9/30 2:13:59 作者:尧图编辑部 阅读量:1,286
)
文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载本篇文章是 linux-insides 仓库 Booting 章节的完整导读与实战指南它沿着电源上电 → 固件 → 引导加载程序 → 内核 setup 代码 → 保护模式 → 64 位长模式 → 内核解压 → KASLR 随机化这条主线把整个启动流程拆解为六个可独立阅读的部分并结合仓库内六篇正文的源码级细节进行深化。读完本文你将掌握 x86_64 处理器的各运行模式、真实模式下的内存分段寻址、Linux 内核引导协议Boot Protocol的核心字段以及如何使用 QEMU GDB 实际观察内核启动的每一步——从第一条汇编指令一直追踪到解压后的内核入口点。如何阅读本章节How to read这一章假定你已经具备基本的计算机体系结构知识并对C语言和 x86_64 汇编语法有初步了解。你不需要成为内核专家但能够阅读短小的代码片段、认识硬件术语会非常有帮助。每一部分聚焦一个引导阶段第一次阅读时请按顺序通读之后可以把各个步骤当作参考资料随时回来对照某个具体符号或寄存器设置在整个序列中的位置。要跟上细节强烈建议在本地准备好 Linux 内核源码。可以通过git clone获取git clone gitgithub.com:torvalds/linux.git或者通过 GitHub CLI 获取gh repo clone torvalds/linux拿到源码后文中所引用的arch/x86/boot/header.S、arch/x86/boot/main.c、arch/x86/boot/compressed/kaslr.c等文件都可以直接对照阅读。术语与符号约定Notation used阅读本文及后续章节时你会遇到以下特殊记号CS、DS、SS、CR0、CR3、CR4、EFER——指 x86 段寄存器与控制寄存器0x...——表示十六进制数值entry_*与startup_*——早期引导符号的常见前缀setup code——Linux 内核的早期部分负责执行把内核代码本身加载进内存之前的准备工作decompressor——setup code中负责把压缩的内核镜像解压进内存的部分。你将学到什么What you will learn处理器如何从固件和引导加载程序一路到达内核入口点x86_64 处理器的不同运行模式真实模式、保护模式、长模式及其内存模型在内核本身被加载进内存并开始工作之前早期 setup 代码究竟做了什么。阅读顺序Reading order本章由以下六个部分构成建议按此顺序阅读。下面逐一给出各部分的主题摘要并结合对应正文提炼关键细节。1. 从引导加载程序到内核linux-bootstrap-1.md第一部分覆盖从按下电源键到内核第一条指令的完整路径是整个章节的基石。上电与复位向量Reset Vector按下电源键后主板向电源发出信号电源在收到 power good 信号后向各组件供电并触发 CPU 启动。x86_64 处理器在复位后以**真实模式real mode**开始执行——这是为兼容最早期的 8086 处理器而保留的模式。复位后寄存器被设置为特殊值ip 0xFFF0、cs选择子 0xF000、而cs隐藏的段基址 0xFFFF0000。两者相加得到0xFFFFFFF0即复位向量——CPU 复位后执行的第一条指令所在地址通常是一条跳转到 BIOS 或 UEFI 入口的jmp指令。coreboot 的entry16.S与bootblock.ld正是把.reset段链接到0xfffffff0位置来印证这一点。真实模式与内存分段8086 是 16 位微处理器却拥有 20 位地址总线可寻址 1 MB2^20字节物理内存。为了用 16 位寄存器访问 20 位地址8086 采用内存分段物理地址 (段选择子 4) 偏移。例如cs:ip 0x2000:0x0010对应物理地址0x20010。真实模式的内存布局从0x00000000的中断向量表到0x00007C00的引导加载程序再到0x000A0000的视频内存与0x000F0000的系统 BIOS均有固定含义。引导扇区与 MBRBIOS 完成 POST上电自检后按引导顺序寻找操作系统。从硬盘引导时BIOS 会在第一个扇区512 字节中寻找引导扇区前 446 字节为 MBR 代码最后两字节必须是魔数0x55与0xAA验证通过后 BIOS 将其复制到0x7C00并跳转执行。正文给出了一个可运行的 NASM 引导扇区实验[BITS 16] boot: mov al, ! ;; 要打印的字符 mov ah, 0x0e ;; TTY 文本输出 mov bh, 0x00 ;; 打印位置 mov bl, 0x07 ;; 颜色 int 0x10 ;; 中断调用 jmp $ times 510-($-$$) db 0 db 0x55 db 0xaa构建并在 QEMU 中运行nasm -f bin boot.S qemu-system-x86_64 boot -nographic如果一切正确屏幕上会出现 SeaBIOS 横幅与打印出的!字符。按Ctrla x可退出虚拟机。引导加载程序阶段与内核 setup 头GRUB 2 的第一阶段代码boot.S只做一件事——把 core image 加载进内存随后grub_main初始化控制台、设置根设备、解析配置文件并加载模块。Linux 内核有一个 引导协议规定引导加载程序必须把内核加载进内存、填充 setup header 中的若干字段并转交控制权。内核镜像以传统引导扇区开头紧跟真实模式 setup 代码内核 setup header 位于镜像偏移0x01F1处可用arch/x86/boot/setup.ld中的ASSERT(hdr 0x1f1, ...)验证。header 的第一部分字段包括.globl hdr hdr: .byte setup_sects - 1 root_flags: .word ROOT_RDONLY syssize: .long ZO__edata / 16 ram_size: .word 0 /* Obsolete */ vid_mode: .word SVGA_MODE root_dev: .word 0 /* Default to major/minor 0/0 */ boot_flag: .word 0xAA55调试实验用 QEMU GDB 观察加载地址直接用 QEMU 加载内核时镜像起始地址为0x10000hexdump能看到MZ头用 GRUB 2 加载时则为0x90000。正文给出了一套完整的实操流程先用qemu-img/parted/losetup/grub2-install制作带 GRUB 的磁盘镜像配置grub.cfg再启动 QEMU 并挂上 GDBqemu-system-x86_64 -drive formatraw,filehdd.img -m 256M -s -S -no-reboot -no-shutdown -vga virtio$ gdb (gdb) target remote localhost:1234 (gdb) break *0x90200 (gdb) c Continuing.在 GRUB 提示符执行set pager1、linux /boot/bzImage、boot即可看到relocator: min_addr 0x0, max_addr 0xffffffff, target 0x90000的调试输出确认内核被加载到0x90000i r则显示cs 0x9020。对0x100000打断点并dump binary memory后能在0x90000处再次看到MZ头在0x90200处看到eb 6a 48 64 72 53 ...setup header 的HdrS签名。setup 代码的真正起点引导加载程序把控制权交给内核后执行从 arch/x86/boot/header.S 的_start符号开始。第一条指令是0xEB短跳转跳到start_of_setup标签——位于0x90200即加载地址 512 字节。随后 setup 代码依次完成四件实事统一段寄存器movw %ds, %ax; movw %ax, %es; cld确保ds、es指向同一地址并清除方向标志为后续清.bss做准备建立栈比较ss与ds把栈指针 4 字节对齐异常时回退到0xFFFC栈从0x9000:0x9000向下增长见 early-stack.svg校验魔数并清.bss用cmpl $0x5a5aaa55, setup_sig验证签名随后用rep stosl从__bss_start填充到_end 3。通过readelf -a arch/x86/boot/setup.elf | grep bss可确认.bss区域超出 setup 镜像本身如__bss_start 0x3f00、__bss_end 0x5280布局见 early-bss.svg跳入 C 代码calll main进入arch/x86/boot/main.c。2. 内核 setup 代码的第一步linux-bootstrap-2.md第二部分从main()出发讲解为进入保护模式做的全部准备核心内容包括保护模式与 GDT真实模式只能寻址 1 MB保护模式80286 起引入改用 32 位地址总线可访问 4 GB。其内存管理分为分段与分页两个相对独立的机制。保护模式的分段由Segment Descriptor定义描述符存放在Global Descriptor TableGDT中其地址由 48 位的gdtr寄存器保存通过lgdt gdt指令加载。每个描述符为 64 位其BASE、LIMIT、G粒度、DPL特权级、P存在位、L64 位代码、D/B等字段共同决定段属性结构见 segment-descriptor.svg。数据段Type字段Expand-Down / Writable / Accessed与代码段Type字段Conforming / Readable / Accessed各有 8 种组合。CPU 通过 16 位的段选择子IndexTIRPL见 segment-selector.svg在 GDT 或 LDT 中查找描述符加载进段寄存器的隐藏部分再以描述符基址 偏移得到物理地址。main()内的初始化序列init_default_io_opsarch/x86/boot/io.h为pio_ops.f_inb、f_outb、f_outw注册回调最终由内联汇编in/out指令实现端口读写copy_boot_params调用 setup 代码自带的汇编版memcpyarch/x86/boot/copy.S配合-mregparm3用ax/dx/cx传参rep movsl逐 4 字节复制把 bootloader 填充的 setup header 复制进 C 结构boot_params.hdrconsole_initarch/x86/boot/early_serial_console.c解析内核命令行中的earlyprintk选项支持serial,0x3f8,115200、serial,ttyS0,115200、ttyS0,115200等格式初始化串口后即可输出第一条消息init_heap检查loadflags中的CAN_USE_HEAP位结合heap_end_ptr字段协议要求设置为栈/堆末尾偏移减0x200GRUB 取0x9000 - 0x200计算堆的末尾并确保堆不与栈重叠。堆起始于_end位于.bss之上、栈之下见 early-heap.svgvalidate_cpuarch/x86/boot/cpu.c通过cpuid检查 CPU 等级与特性长模式、SSESSE2 等低于CONFIG_X86_MINIMUM_CPU_FAMILY要求则中止引导detect_memoryarch/x86/boot/memory.c依次尝试0xE820、0xE801、0x88三种 BIOS 服务获取物理内存图。0xE820通过int 0x15循环读取内存区域填入struct boot_e820_entry { __u64 addr; __u64 size; __u32 type; }最终在dmesg中呈现为BIOS-e820: [mem ...] usable/reserved列表keyboard_init通过int 0x16获取键盘状态如 Caps Lock并设置重复率供视频模式选择时使用系统信息收集query_ist()查询 Intel SpeedStep、query_apm_bios()查询 APMACPI 之前的电源管理、query_edd()查询增强磁盘驱动器信息均存入boot_params供后续使用。3. 视频模式初始化与保护模式转换linux-bootstrap-3.md第三部分讲解切换到保护模式前的最后准备工作——视频模式。set_videoarch/x86/boot/video.c从boot_params.hdr.vid_mode读取模式号依次执行RESET_HEAP()、store_mode_params()、save_screen()、probe_cards(0)探测显卡若模式号为ASK_VGA则弹出模式选择菜单循环调用set_mode直到成功最后保存 EDID 信息并恢复屏幕。vid_mode字段在引导协议中的偏移为01FA其值可通过内核命令行vgamode设置normal0xFFFF、ext0xFFFE、ask0xFFFD或整数模式号。用 QEMU 试验sudo qemu-system-x86_64 -kernel ./linux/arch/x86/boot/bzImage \ -nographic \ -append consolettyS0 nokaslr vgaask \ -initrd /boot/initramfs-...img内核会提示Press ENTER to see video modes available随后列出完整的模式表例如 VGA 文本模式F00 80x25、F01 80x50以及 VESA 图形模式340 320x200x32、343 800x600x32、344 1024x768x32、38C 2560x1600x32等供选择或输入scan扫描更多模式。4. 过渡到 64 位模式linux-bootstrap-4.md进入保护模式后处理器可寻址 4 GB 且内存访问具备特权级但这只是兼容模式。第四部分详细讲解向 x86_64 原生模式——长模式long mode——的切换其关键在于建立**分页paging**机制。正文从保护模式入口跳转开始arch/x86/boot/pmjump.S中的jmpl *%eax跳转到 32 位入口点。按引导协议bzImage 的 protected-mode 内核被重定位到0x100000。仍可用 QEMU GDB 验证sudo qemu-system-x86_64 -kernel ./linux/arch/x86/boot/bzImage \ -nographic \ -append consolettyS0 nokaslr -s -S \ -initrd /boot/initramfs-...img(gdb) target remote :1234 (gdb) hbreak *0x100000 (gdb) c断点命中后i r可确认eax 0x100000。随后 setup 代码启用 A20 地址线、重新加载 GDT并按需为长模式配置页表最终通过lret把执行权交给 64 位入口点。5. 内核解压linux-bootstrap-5.md此时内存中的还只是 setup 代码第五部分处理最后一个里程碑——把压缩内核解压出来。64 位入口点位于arch/x86/boot/compressed/head_64.S的startup_64符号它是我们看到的第一段 64 位代码。解压前需完成cld; cli——清方向标志并关中断防止解压过程被中断干扰段寄存器统一清零长模式下段寄存器不再参与寻址计算内核重定位地址——比较编译时预期加载位置与实际加载位置的差值重新加载 GDT加载中断描述符表IDT。随后解压器建立**恒等映射identity mapping**页表覆盖内核镜像_head到_end、boot_params与命令行必要时由kernel_add_identity_map动态分配页表项最后write_cr3(top_level_pgt)切换页表。解压本身由extract_kernelarch/x86/boot/compressed/misc.c完成它会重新初始化视频与控制台因为无法假设 bootloader 走的是 16/32/64 位哪条协议路径设置堆指针后执行解压最终movq %r15, %rdi movq %rbp, %rsi call extract_kernel /* returns kernel entry point in %rax */ movq %r15, %rsi jmp *%raxjmp *%rax把控制权交给解压后的内核入口至此早期 setup 阶段结束Linux 内核正式开始运行。6. 内核加载地址随机化KASLRlinux-bootstrap-6.md第六部分回到 setup 代码深入讲解KASLR内核地址空间布局随机化。正常情况下内核加载在CONFIG_PHYSICAL_START指定的固定地址默认0x1000000为了抵御基于固定地址的内核结构猜测攻击需要开启CONFIG_RANDOMIZE_BASE。启用后每次启动内核镜像都被放置到不同的物理地址。解压前的关键调用位于extract_kernel中choose_random_location((unsigned long)input_data, input_len, (unsigned long *)output, needed_size, virt_addr);choose_random_locationarch/x86/boot/compressed/kaslr.c先检查命令行中的nokaslr——存在则直接返回这也是前面调试命令带nokaslr的原因。否则它会从熵源获取随机值结合CONFIG_PHYSICAL_ALIGN对齐要求在合法物理内存范围内挑选一个CONFIG_PHYSICAL_ALIGN大小的槽位作为随机物理地址在 x86_64 下进一步调用find_random_virt_addr(LOAD_PHYSICAL_ADDR, output_size)按slots 1 (KERNEL_IMAGE_SIZE - minimum - image_size) / CONFIG_PHYSICAL_ALIGN计算可用槽位数再以kaslr_get_random_long(Virtual) % slots选择随机虚拟地址static unsigned long find_random_virt_addr(unsigned long minimum, unsigned long image_size) { unsigned long slots, random_addr; slots 1 (KERNEL_IMAGE_SIZE - minimum - image_size) / CONFIG_PHYSICAL_ALIGN; random_addr kaslr_get_random_long(Virtual) % slots; return random_addr * CONFIG_PHYSICAL_ALIGN minimum; }最终物理与虚拟地址同时被随机化解压器据此确定内核解压的目标位置。内核版本说明Kernel version本章内容对应Linux kernel v7.2.0。需要留意的是正文作者在撰写时提到系列最初基于内核3.18并持续更新到现代内核当前聚焦v6.16因此个别符号地址、配置选项默认值会随内核版本变化——例如正文强调的__bss_start、__bss_end地址取决于内核版本。当你对照源码阅读时应以手头内核树的实际文件如arch/x86/boot/header.S、arch/x86/boot/main.c为准。赞分享文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐Linux 内核引导过程完全解析从按下电源键到内核解压入内存linux-insides-zh Booting 章导读Linux 内核引导过程完全解析从按下电源键到内核解压入内存linux insides zh Booting 章导读 导读 本篇文章以 hust openLinux 内核解压全流程从 startup_64 到 extract_kernellinux-insides-zh 引导过程第五部分Linux 内核解压全流程从 startup_64 到 extract_kernel linux insides zh 引导过程第五部分 本文是《Linu按下电源键那一刻发生了什么Linux 内核揭秘linux-insides-zh引导章节 6 大阶段完整解读按下电源键那一刻发生了什么Linux 内核揭秘linux insides zh引导章节 6 大阶段完整解读 按下电源键的那一刻Linux 内核引导过程就上一篇如何一键生成黑苹果EFI配置智能自动化工具终极指南下一篇PaddleFormers 中文词向量模型 w2v_sikuquanshu_target_word-bigram_dim300 完整使用指南安装、API 与 Serving 部署创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考