news 2026/9/27 7:19:45

Linux 内核引导过程完全解析:从按下电源键到内核解压入内存(linux-insides-zh Booting 章导读)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 内核引导过程完全解析:从按下电源键到内核解压入内存(linux-insides-zh Booting 章导读)

【免费下载链接】linux-insides-zh

Linux 内核揭秘

项目地址:https://gitcode.com/hust-open-atom-club/linux-insides-zh
点击查看免费下载

导读

本篇文章以hust-open-atom-club/linux-insides-zh仓库中Booting/章节为主线,为你完整梳理 Linux 内核从你按下电源键那一刻起,到内核镜像最终加载到内存为止的全部启动阶段。你将掌握:CPU 复位向量与实模式寻址的来龙去脉、GRUB 等引导程序如何把控制权交给内核、内核早期setup code的堆栈/BSS/堆初始化、视频模式设置、从实模式经保护模式到 64 位长模式的切换,以及内核解压与 KASLR 地址随机化的实现细节。读完本章,你不仅能看懂arch/x86/boot/下的早期启动代码,还能把每个启动阶段与具体寄存器、数据结构和内核源码位置一一对应起来。

阅读方法与前置知识

本章假设你熟悉基础计算机架构,对C语言和x86_64汇编语法(特别是 AT&T 语法)略有了解。你不需要成为内核专家,但能读懂短代码片段、识别硬件术语会极大提升阅读收益——因为引导过程的绝大多数细节都以寄存器操作、内存地址和 BIOS 中断的形式呈现。

建议的阅读方法是:第一次按顺序通读(从第一部分依次读到第六部分),之后在需要将某个符号或寄存器映射到启动序列中的具体位置时,再把对应章节作为参考点逐步回顾。若想在阅读时对照真实代码,最好在本地拥有一份 Linux 内核源码,可使用 git 克隆获取:

git clone git@github.com:torvalds/linux.git

章节内各篇文章提到的关键源码(如arch/x86/boot/header.S、arch/x86/boot/main.c、arch/x86/boot/video.c、arch/x86/boot/pm.c、arch/x86/boot/compressed/head_64.S等)均可在克隆后的源码树中直接定位研读。

术语表:读懂引导章节的必备词汇

在阅读本章乃至后续其他章节时,你会频繁遇到以下专业术语,建议先通读一遍建立印象:

  • CS、DS、SS、CR0、CR3、CR4、EFER——代表 x86 的段寄存器和控制寄存器。CS(代码段)、DS(数据段)、SS(堆栈段)在实模式与保护模式下扮演完全不同的角色;CR0的PE位控制保护模式的开启,CR3保存页表基址,CR4承载各类特性开关,EFER(Extended Feature Enable Register)则与 64 位长模式的使能直接相关。
  • 0x...——表示十六进制值,内核启动代码中几乎所有地址与寄存器值都以此形式出现。
  • entry_*和startup_*——早期引导符号的常见前缀,例如startup_32、startup_64分别对应 32 位与 64 位入口点。
  • setup code——Linux 内核的早期部分,负责执行将内核代码本身加载到内存之前的准备工作。
  • decompressor——setup code中负责把压缩后的内核映像解压到内存的那部分代码。

你将学到什么

本章明确给出了三个核心学习目标,后续六个部分全部围绕它们展开:

  1. 处理器如何从固件(BIOS/UEFI)和引导加载程序(如 GRUB 2)一步步到达内核入口点;
  2. x86_64 处理器的不同运行模式(实模式、保护模式、长模式)及其切换机制;
  3. 内核本身启动之前的早期setup code如何被加载到内存并开始工作。

阅读顺序与六大部分一览

本章共六个部分,按启动的时间线严格递进:

部分主题对应文件
第一部分从引导程序到内核Booting/linux-bootstrap-1.md
第二部分在内核设置代码的第一步Booting/linux-bootstrap-2.md
第三部分视频模式初始化和保护模式切换Booting/linux-bootstrap-3.md
第四部分切换 64 位模式Booting/linux-bootstrap-4.md
第五部分内核解压Booting/linux-bootstrap-5.md
第六部分内核地址随机化Booting/linux-bootstrap-6.md

下面逐部分展开核心内容。

第一部分:从引导程序到内核

按下电源键之后发生了什么

尽管这是一系列关于 Linux 内核的文章,第一部分并不从内核代码开始,而是从硬件视角出发。按下电源开关后,主板向电源发送信号,电源回送"电源备妥信号"(Power Good Signal),主板随即尝试启动 CPU。CPU 复位所有寄存器并设置预定义值。自 80386 起,CPU 复位后寄存器被设置为:

IP 0xfff0 CS selector 0xf000 CS base 0xffff0000

处理器由此开始在实模式下工作。要理解实模式,必须先掌握 8086 时代就确立的段式内存管理:8086 只有 20 位寻址总线(可访问 1MB 地址空间)和 16 位寄存器(最大寻址0xffff,即 64KB)。为突破寄存器宽度限制,实模式将地址拆为"段基址 + 偏移"两部分,物理地址计算公式为:

PhysicalAddress = Segment * 16 + Offset

例如CS:IP = 0x2000:0x0010时,物理地址为(0x2000 << 4) + 0x0010 = 0x20010。而 16 位能表达的最大组合0xffff:0xffff计算得到0x10ffef,超出 1MB 达 65519 字节——这正是A20 地址线存在的原因:在 A20 被禁用时,0x10ffef会回卷为0x00ffef。

复位向量与 BIOS 启动

结合复位后的CS基址与IP值,逻辑地址为0xffff0000:0xfff0,最终物理地址是0xfffffff0——位于 4GB - 16 字节处,即著名的复位向量(Reset Vector)。这是 CPU 复位后期望执行的第一条指令所在地址,它包含一条jump指令,通常跳向 BIOS 入口点。文章以 coreboot 源码为例展示了.reset段中的跳转指令(opcode0xe9):

.section ".reset", "ax", %progbits .code16 .globl _start _start: .byte 0xe9 .int _start16bit - ( . + 2 )

在实模式下 CPU 只能访问 1MB 地址空间,那 CPU 如何访问到0xfffffff0这样的高位地址?答案来自 coreboot 的内存映射文档:0xFFFE_0000 - 0xFFFF_FFFF这段 128KB 的 ROM 被映射进地址空间,因此复位向量处执行的第一条指令来自ROM 而非 RAM。

BIOS 寻找可引导设备与 MBR

BIOS 完成初始化和硬件检查后,会按配置顺序尝试从不同设备寻找引导程序。对硬盘而言,BIOS 寻找引导扇区:MBR 分区下引导扇区位于第一个扇区(512 字节)的头 446 字节,且扇区末尾必须是魔术字节0x55与0xaa,BIOS 据此判定设备可引导。第一部分给出了一个完整的极简引导扇区示例:

[BITS 16] [ORG 0x7c00] boot: mov al, '!' mov ah, 0x0e mov bh, 0x00 mov bl, 0x07 int 0x10 jmp $ times 510-($-$$) db 0 db 0x55 db 0xaa

用 NASM 汇编并用 QEMU 直接运行:

nasm -f bin boot.nasm && qemu-system-x86_64 boot

该二进制以0x7c00为起始(BIOS 约定把引导扇区加载到0x7c00),通过int 0x10打印!字符,剩余 510 字节用 0 填充,最后以0xaa、0x55收尾。运行后 QEMU 屏幕显示如下:

还可以用objdump查看其二进制转储:

nasm -f bin boot.nasm objdump -D -b binary -mi386 -Maddr16,data16,intel boot

第一部分还给出了实模式下 1MB 地址空间的经典分配表,值得牢记:0x00000000-0x000003FF是实模式中断向量表,0x00007C00-0x00007DFF是引导加载程序区,0x000A0000-0x000BFFFF是视频内存(VRAM),0x000F0000-0x000FFFFF是系统 BIOS 等。

GRUB 2 与内核启动协议

现实世界中可选的引导程序很多(GRUB 2、syslinux 等),Linux 内核通过Boot Protocol规定引导程序应如何实现。章节以 GRUB 2 为例:BIOS 把控制权交给引导扇区中的boot.img(仅占一个扇区,只做必要初始化),随后跳转到 core image(如diskboot.img),后者把整个 core image(含 GRUB 2 内核代码与文件系统驱动)载入内存,最终调用grub_main:初始化控制台、计算模块基地址、设置 root 设备、读取 grub 配置文件、加载模块,最后进入 normal 模式由grub_normal_execute显示操作系统菜单;用户选择系统后,grub_menu_execute_entry调用boot命令引导所选操作系统。

按 boot protocol 要求,引导程序必须填充kernel setup header(位于 setup code 偏移0x01f1处),其定义始于arch/x86/boot/header.S:

.globl hdr hdr: setup_sects: .byte 0 root_flags: .word ROOT_RDONLY syssize: .long 0 ram_size: .word 0 vid_mode: .word SVGA_MODE root_dev: .word 0 boot_flag: .word 0xAA55

引导程序必须填充 boot protocol 中标记为write的字段(如type_of_loader),这些信息可能来自命令行或由计算得到。内核被引导入内存后,内存使用布局如下:

| Protected-mode kernel | 100000 +------------------------+ | I/O memory hole | 0A0000 +------------------------+ | Reserved for BIOS | Leave as much as possible unused ~ ~ | Command line | (Can also be below the X+10000 mark) X+10000 +------------------------+ | Stack/heap | For use by the kernel real-mode code. X+08000 +------------------------+ | Kernel setup | The kernel real-mode code. | Kernel boot sector | The kernel legacy boot sector. X +------------------------+ | Boot loader | <- Boot sector entry point 0x7C00 001000 +------------------------+ | Reserved for MBR/BIOS | 000800 +------------------------+ | Typically used by MBR | 000600 +------------------------+ | BIOS use only | 000000 +------------------------+

其中X是内核 boot sector 被加载进内存的位置。示例环境里X = 0x10000,可通过内存 dump 验证:

内核设置代码的入口_start

内核设置代码的起点是arch/x86/boot/header.S中的_start函数。_start之前的代码其实是内核自带的"引导程序"——早期内核确实可以如此启动,但现代内核中这部分代码已不再启动内核,只输出错误信息。直接用qemu-system-x86_64 vmlinuz-...启动时即可看到该错误:

qemu-system-x86_64 vmlinuz-3.18-generic

为了兼容 UEFI 硬件,header.S开头定义了 DOS MZ 魔术数字与 PE 头。真正的内核代码从_start开始——其他引导程序(GRUB 2 等)知道_start位于 MZ 头之后偏移0x200字节处,因此会跳过前面位于.bstext段的代码直接跳转至此:

.globl _start _start: .byte 0xeb .byte start_of_setup-1f 1: // // rest of the header //

_start首条指令是短跳转(opcode0xeb),跳到start_of_setup标号(位于.entrytext段,其中第一条指令才是内核执行开始后的第一条指令)。GRUB 2 跳转到_start时设置:

state.gs = state.fs = state.es = state.ds = state.ss = segment; state.cs = segment + 0x20;

当内核被加载到0x10000时,fs = es = ds = ss = 0x1000、cs = 0x1020,第一条指令地址为0x10200(正好是0x10000起偏移0x200处)。从start_of_setup开始的代码需要完成四件事:将所有段寄存器值统一、设置堆栈、设置 BSS(静态变量区)、跳转到main.c。

段寄存器设置

先让ds与es指向相同地址并用cld清方向标志,然后用一个经典技巧统一cs:

pushw %ds pushw $6f lretw

lretw会把标号6的地址装入ip、把ds的值装入cs,从而让cs与其他段寄存器一致。

设置堆栈

接下来检查ss寄存器,处理三种情况(ss == 0x10000、ss != 0x10000且CAN_USE_HEAP置位、ss != 0x10000且CAN_USE_HEAP未置位)。核心代码将sp4 字节对齐,若为 0 则设为0xfffc(堆栈向下增长,64KB 段的最后一个 4 字节地址),然后把0x10000写入ss:

2: andw $~3, %dx jnz 3f movw $0xfffc, %dx 3: movw %ax, %ss movzwl %dx, %esp sti

loadflags字段的Bit 7即CAN_USE_HEAP标志(置位表示heap_end_ptr值有效)。loadflags的全部标志位包括:

#define LOADED_HIGH (1<<0) #define QUIET_FLAG (1<<5) #define KEEP_SEGMENTS (1<<6) #define CAN_USE_HEAP (1<<7)

若CAN_USE_HEAP置位,则把heap_end_ptr加上最小堆栈大小STACK_SIZE(512 字节)得到堆栈顶;未置位则只加STACK_SIZE。三种情形下堆栈布局分别如 stack1.png、stack2.png、minimal_stack.png 所示。

BSS 段设置

执行 C 代码前还要校验 magic 签名并清零 BSS 段。签名校验失败则跳入setup_bad:

cmpl $0x5a5aaa55, setup_sig jne setup_bad

随后将 BSS 段全部清零:

movw $__bss_start, %di movw $_end+3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl

即把__bss_start放入di、_end + 3(4 字节对齐)放入cx、eax清零,计算差值得到 BSS 大小,除以 4 后以rep; stosl按 4 字节写零,最终效果见 bss.png。至此即可call main进入第一个 C 语言函数(定义于arch/x86/boot/main.c)。

第二部分:内核设置代码的第一步

第二部分从main()函数开始,主线是:把启动参数拷贝到 zeropage、初始化控制台、初始化堆、检查 CPU、侦测内存、初始化键盘、查询系统参数。

将启动参数拷贝到 boot_params

main()首先调用copy_boot_params(void),把内核设置信息拷贝到boot_params结构(定义于arch/x86/include/uapi/asm/bootparam.h)的struct setup_header hdr字段。该函数完成两件事:把header.S中定义的hdr结构拷贝到boot_params.hdr;若内核通过旧命令行协议运行,则更新命令行指针。注意这里的memcpy不是 C 库函数,而是定义在arch/x86/boot/copy.S中的汇编实现,采用fastcall调用规则(参数经ax、dx、cx寄存器传递):

GLOBAL(memcpy) pushw %si pushw %di movw %ax, %di ;move &boot_param.hdr to di movw %dx, %si ;move &hdr to si pushw %cx ;push cx ( sizeof(hdr) ) shrw $2, %cx rep; movsl ;copy based on 4 bytes popw %cx andw $3, %cx ;cx = cx % 4 rep; movsb ;copy based on one byte popw %di popw %si retl ENDPROC(memcpy)

调用memcpy(&boot_params.hdr, &hdr, sizeof hdr)时,ax指向boot_params.hdr、dx指向hdr、cx为结构大小。实现先以 4 字节为单位复制,剩余不足 4 字节的部分再按字节复制。GLOBAL宏(定义于arch/x86/include/asm/linkage.h)给代码段分配全局可见的名字标签,ENDPROC宏(定义于include/linux/linkage.h)标记汇编函数结束,便于静态分析工具识别。

控制台初始化

console_init(定义于arch/x86/boot/early_serial_console.c)解析命令行是否包含earlyprintk选项并初始化串口,可能的取值包括:

  • serial,0x3f8,115200
  • serial,ttyS0,115200
  • ttyS0,115200

若命令行含debug选项,会出现输出early console in setup code。puts(定义于arch/x86/boot/tty.c)逐字节调用putchar:

void __attribute__((section(".inittext"))) putchar(int ch) { if (ch == '\n') putchar('\r'); bios_putchar(ch); if (early_serial_base != 0) serial_putchar(ch); }

putchar会把\n扩展为\r\n,先经bios_putchar用int 0x10(AH = 0x0e)输出到显示器,若串口已初始化再输出到串口。bios_putchar使用initregs(内部先memset清零biosregs结构)填充寄存器。memset同样是copy.S中的汇编实现,用imull $0x01010101把单字节值扩展为 4 字节重复模式,再配合rep; stosl高效写内存。.inittext段定义在setup.ld链接脚本中。

堆初始化

init_heap先检查loadflags的CAN_USE_HEAP标志,随后计算堆栈结束地址与堆结束地址:

char *stack_end; if (boot_params.hdr.loadflags & CAN_USE_HEAP) { asm("leal %P1(%%esp),%0" : "=r" (stack_end) : "i" (-STACK_SIZE)); } //heap_end = heap_end_ptr + 512 heap_end = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);

即stack_end = esp - STACK_SIZE、heap_end = heap_end_ptr + 0x200。若heap_end大于stack_end,则用stack_end接管——因为在大多数系统中全局堆与堆栈相邻但增长方向相反。堆初始化完成后即可使用GET_HEAP等操作。

检查 CPU 类型

validate_cpu(arch/x86/boot/cpu.c)调用check_cpu(arch/x86/boot/cpucheck.c)获取 CPU 级别并与预设最低级别比较:

check_cpu(&cpu_level, &req_level, &err_flags); if (cpu_level < req_level) { printf("This kernel requires an %s CPU, ", cpu_name(req_level)); printf("but only detected an %s CPU.\n", cpu_name(cpu_level)); return -1; }

check_cpu还会检查 CPU 标志(若为 64 位 CPU 则设置 long mode)、检查制造商并据此设置不同的 CPU 选项(例如 AMD CPU 若不支持SSE+SSE2则禁止之)。

内存分布侦测

detect_memory(arch/x86/boot/memory.c)通过0xe820(获取全部内存分配)、0xe801和0x88(获取临近内存大小)三类 BIOS 接口侦测内存分布。以detect_memory_e820为例,它填充biosregs结构:

initregs(&ireg); ireg.ax = 0xe820; ireg.cx = sizeof buf; ireg.edx = SMAP; // 0x534d4150 ireg.di = (size_t)&buf;

其中ax固定为0xe820,cx是缓冲区大小,edx必须是魔术数字SMAP(0x534d4150),es:di是缓冲区地址,ebx初始为 0。随后循环调用int 0x15,把返回的ebx写回寄存器继续下一次调用,直到返回的 eflags 含X86_EFLAGS_CF为止。收集到的信息写入e820entry数组,每个元素含内存段起始地址、大小与类型(reserved、usable 等)。这些内容可在dmesg输出中看到:

[ 0.000000] e820: BIOS-provided physical RAM map: [ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable [ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved [ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved [ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000003ffdffff] usable

键盘初始化与系统参数查询

keyboard_init()先通过int 0x16(AH = 0x02)获取键盘状态并存入boot_params.kbd_status,再以AX = 0x0305设置按键重复频率。系统参数查询则包括query_mca(arch/x86/boot/mca.c):设置AH = 0xc0调用int 0x15,若 CF 被置位则无 MCA(Micro Channel Architecture);否则ES:BX指向系统信息表,函数把表内容拷贝到boot_params.sys_desc_table。query_ist、query_apm_bios等查询将在后续章节继续介绍。

保护模式与段描述符

在深入后续部分前,必须理解保护模式的内存管理。实模式 20 位地址线在保护模式中被替换为 32 位,可访问多达 4GB 地址空间,并引入内存分页能力。保护模式下,每个内存段的大小和起始位置由"段描述符"描述,所有段描述符存放于全局描述符表(GDT),GDT 地址保存在 48 位寄存器GDTR中(16 位表大小 + 32 位表基址),通过lgdt指令加载。段描述符为 64 位结构,核心字段包括:

  1. Limit(20 位)——分存于 0-15 与 16-19 位,配合G位决定段长度:G=0时按字节增长(最大 1MB),G=1时按 4KB 增长(最大 4GB);段长度 = 粒度 × (LIMIT + 1)。
  2. Base(32 位)——分存于 0-15、32-39、56-63 位,定义段基址。
  3. Type/Attribute(40-47 位)——S位(第 44 位)区分系统段与代码/数据段;第 43 位区分数据段(E扩展方向、W可写、A已访问)与代码段(C一致、R可读、A已访问)。数据段全部可读,代码段永远不可写;C=1时低优先级代码可访问该代码段。
  4. DPL(2 位)——段优先级(0-3)。
  5. P 标志(bit 47)——段是否存在于内存,P=0时访问会报错。
  6. L 标志(bit 53)——仅对代码段有意义,L=1表示该段运行于 64 位模式。
  7. D/B 标志(bit 54)——对可执行代码段称D(默认操作数/地址长度 32 位或 16 位),对堆栈段称B(32 位esp或 16 位sp栈指针),对下扩数据段则决定堆栈段上界限(0xFFFFFFFF或0xFFFF)。

保护模式下段寄存器保存的是一种称为段选择子的 16 位结构:Index(GDT 中段描述符索引)、TI(选 GDT 还是 LDT)、RPL(请求者优先级)。CPU 寻址步骤为:装入段选择子 → 按选择子从 GDT 取段描述符放入段寄存器隐藏部分 → 非下扩段下物理地址 = 描述符基址 + 偏移。线性地址的形成过程见 linear_address.png。从实模式进入保护模式的标准操作是:禁止中断 →lgdt装载 GDT → 置CR0.PE = 1→ 跳转执行保护模式代码。

第三部分:视频模式初始化和保护模式切换

内核数据类型与堆操作 API

第三部分从set_video函数(arch/x86/boot/video.c)开始,它会读取boot_params.hdr.vid_mode。在此之前,先认识内核常用数据类型(建议阅读内核代码时牢记):

Typecharshortintlongu8u16u32u64
Size12481248

内核在boot.h中提供了一套堆操作 API:

  • RESET_HEAP()——((void *)( HEAP = _end )),把堆头重置到_end标号(内核初始化时堆与栈共享内存空间)。
  • GET_HEAP(type, n)——((type *)__get_heap(sizeof(type),__alignof__(type),(n))),按类型大小与对齐要求分配内存。
  • __get_heap(s, a, n)——先按对齐要求调整HEAP,把当前HEAP存入tmp,再让HEAP += s*n预留空间并返回tmp。
  • heap_free(n)——判断heap_end - HEAP >= n是否成立。

设置显示模式

set_video首先调用RESET_HEAP(),然后调用store_mode_params把显示模式参数写入boot_params.screen_info(结构定义于include/uapi/linux/screen_info.h)。其内部流程为:store_cursor_position通过int 0x10(AH = 0x3)查询光标行列并存入screen_info.orig_x/orig_y;store_video_mode保存当前显示模式到orig_video_mode;根据显示模式设置video_segment(单色文本内存段0xB000或彩色文本内存段0xB800);再通过set_fs(0)配合rdfs16(0x485)读取字体大小存入orig_video_points,并从0x44a、0x484读取行列信息存入orig_video_cols/orig_video_lines。

随后save_screen把当前屏幕信息(屏幕大小、光标位置、字符数据)保存到saved_screen结构({ int x, y; int curx, cury; u16 *data; }),并检查堆空间是否足够:

if (!heap_free(saved.x*saved.y*sizeof(u16)+512)) return;

probe_cards(0)(arch/x86/boot/video-mode.c)遍历所有显卡驱动:

for (card = video_cards; card < video_cards_end; card++) { if (card->unsafe == unsafe) { if (card->probe) card->nmodes = card->probe(); else card->nmodes = 0; } }

video_cards符号来自arch/x86/boot/setup.ld中定义的.videocards段,其中存放所有以__videocard宏(即struct card_info __attribute__((used,section(".videocards"))))定义的显卡结构,例如:

static __videocard video_vga = { .card_name = "VGA", .probe = vga_probe, .set_mode = vga_set_mode, };

card_info结构含card_name、set_mode、probe、modes、nmodes、unsafe、xmode_first、xmode_n字段。set_video随后进入主循环:若vid_mode为ASK_VGA则弹出菜单让用户选择,再调用set_mode设置模式,失败则提示Undefined video mode number并再次询问:

for (;;) { if (mode == ASK_VGA) mode = mode_menu(); if (!set_mode(mode)) break; printf("Undefined video mode number: %x\n", mode); mode = ASK_VGA; }

set_mode→raw_set_mode遍历card_info调用对应set_mode(如vga_set_mode,内部按VIDEO_80x25、VIDEO_8POINT、VIDEO_80x43等模式调用vga_set_***函数,每个函数最终都是调用int 0x10设置显示模式)。设置成功后写回boot_params.hdr.vid_mode,vesa_store_edid把 EDID 数据写入内存,do_restore还原此前保存的屏幕信息。

关于vid_mode的传入方式,boot protocol 提供了vga=命令行选项:

vga=<mode> <mode> here is either an integer (in C notation, either decimal, octal, or hexadecimal) or one of the strings "normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask" (meaning 0xFFFD). This value should be entered into the vid_mode field, as it is used by the kernel before the command line is parsed.

即可以把vga=ask(或0xFFFD)写入 grub 或引导程序配置文件,内核启动时便会显示交互式菜单供用户挑选显示模式:

进入保护模式前的最后准备

go_to_protected_mode(arch/x86/boot/pm.c)完成最后的准备工作。首先realmode_switch_hook:若存在realmode_swtchhook 则用lcallw远调用之(用于 DOS 等宿主环境),否则直接禁止中断与 NMI:

static void realmode_switch_hook(void) { if (boot_params.hdr.realmode_swtch) { asm volatile("lcallw *%0" : : "m" (boot_params.hdr.realmode_swtch) : "eax", "ebx", "ecx", "edx"); } else { asm volatile("cli"); outb(0x80, 0x70); /* Disable NMI */ io_delay(); } }

io_delay通过对 I/O 端口0x80写入任意字节获得约 1ms 延时。

随后enable_a20(arch/x86/boot/a20.c)激活 A20 地址线:先用a20_test检测是否已激活——把FS设为0x0000、GS设为0xffff,在A20_TEST_ADDR写入并读取GS:A20_TEST_ADDR+0x10判断是否回卷;未激活则尝试多种方法(包括 BIOSint 0x15等)。若最终失败会调用die(arch/x86/boot/header.S中hlt+ 死循环)结束运行。

接着reset_coprocessor通过向 I/O 端口0xf0、0xf1写 0 复位数字协处理器;mask_all_interrupts屏蔽从中断控制器(PIC)的所有中断和主 PIC 上除 IRQ2(级联中断)以外的全部中断:

outb(0xff, 0xa1); /* Mask all interrupts on the secondary PIC */ outb(0xfb, 0x21); /* Mask all but cascade on the primary PIC */

设置 IDT 与 GDT,进入保护模式

setup_idt用lidtl装载一个空 IDT(长度 0),使所有中断调用暂时为空:

static void setup_idt(void) { static const struct gdt_ptr null_idt = {0, 0}; asm volatile("lidtl %0" : : "m" (null_idt)); }

gdt_ptr是__attribute__((packed))的 48 位结构(16 位长度 + 32 位指针)。setup_gdt则装载启动用 GDT:

static const u64 boot_gdt[] __attribute__((aligned(16))) = { [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff), [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff), [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103), };

这里定义了代码段、数据段和 TSS 段的描述符;由于尚未设置任何中断调用,TSS 实际不会被使用,其存在只是为了满足 Intel 处理器进入保护模式的要求。数组以 16 字节对齐(__attribute__((aligned(16))))。准备工作全部完成后,内核通过jmpl *%eax跳转到 32 位入口点(arch/x86/boot/pmjump.S),正式进入保护模式。

第四部分:切换到 64 位模式

第四部分(Booting/linux-bootstrap-4.md)承接保护模式,展示了在保护模式中的最初几步:确认 CPU 是否支持长模式(Long Mode)、SSE 与分页,初始化页表,最后完成向 64 位长模式的切换。本部分包含大量汇编代码,阅读前最好备一本汇编参考书。

入口点为arch/x86/boot/pmjump.S的jmpl *%eax,eax寄存器保存 32 位入口点地址。按照 x86 Linux 内核引导协议:"When using bzImage, the protected-mode kernel was relocated to 0x100000"(使用 bzImage 时,保护模式内核被重定位至0x100000)。示例中寄存器状态验证了这一点:

eax 0x100000 1048576 ecx 0x0 0 edx 0x0 0 ebx 0x0 0 esp 0x1ff5c 0x1ff5c ebp 0x0 0x0 esi 0x14470 83056 edi 0x0 0 eip 0x100000 0x100000 eflags 0x46 [ PF ZF ]

本部分随后验证 CPU 能力(长模式、SSE、分页支持)、初始化页表,并最终跳转到 64 位入口点startup_64。切换完成后,代码段寄存器加载__KERNEL_CS并执行lret完成远跳转:

pushl $__KERNEL_CS leal startup_64(%ebp), %eax ... pushl %eax ... lret

第五部分:内核解压

第五部分(Booting/linux-bootstrap-5.md)涵盖跳进内核代码的最后步骤:解压前准备、重定位与直接内核解压。我们在 64 位入口点startup_64(源文件arch/x86/boot/compressed/head_64.S)开始。进入startup_64后先建立数据段:

.code64 .org 0x200 ENTRY(startup_64) xorl %eax, %eax movl %eax, %ds movl %eax, %es movl %eax, %ss movl %eax, %fs movl %eax, %gs

随后进行解压前的准备工作(如初始化解压器环境、处理重定位)并调用extract_kernel完成对压缩内核映像的直接解压,把真正的内核代码展开到指定物理地址。

第六部分:内核地址随机化(KASLR)

第六部分(Booting/linux-bootstrap-6.md)讲述引导过程的高级部分——内核加载地址随机化。Linux 内核的入口点是init/main.c的start_kernel函数,它在LOAD_PHYSICAL_ADDR地址开始执行,该地址依赖CONFIG_PHYSICAL_START配置选项,默认0x1000000:

config PHYSICAL_START hex "Physical address where the kernel is loaded" if (EXPERT || CRASH_DUMP) default "0x1000000" ---help--- This gives the physical address where the kernel is loaded.

该选项可在内核配置时修改,但也可让加载地址取随机值——为此需启用CONFIG_RANDOMIZE_BASE。启用后,内核镜像解压和加载的物理地址会被随机化,这是出于**地址空间布局随机化(ASLR)**的安全考量。

随机化的最早步骤之一是初始化恒等映射页表(identity mapped,虚拟地址与物理地址相同):若引导加载程序使用 16 位或 32 位引导协议,此时已存在页表;但若解压器选择页表覆盖范围之外的内存区域,就需要建立新页表。随机化从choose_random_location函数开始:

void choose_random_location(unsigned long input, unsigned long input_size, unsigned long *output, unsigned long output_size, unsigned long *virt_addr)

该函数负责在解压前挑选随机的物理(与虚拟)加载地址,为start_kernel的最终运行位置奠定安全基础。

内核版本说明

按照本仓库章节 README 的标注,本章对应Linux kernel v6.19。需要注意的是,章节内各篇译文源自较早的上游版本(文中多处引用的源码路径与行号对应v3.18时代的内核源码树),阅读时如遇到与现行内核源码的细微差异,应以最新内核源码树为准,并欢迎向仓库提交修正。整个书籍的目录结构可查看 SUMMARY.md,书中其余章节(如初始化、中断、系统调用、内存管理等)同样配有各自的技术主题与源码讲解。

小结与下一步

至此,一条完整的引导链已经清晰:电源复位 → 实模式复位向量 → BIOS 自检与 MBR 引导扇区 → GRUB 2 按 boot protocol 填充 setup header → 内核_start/start_of_setup统一段寄存器、设置堆栈与 BSS →main()拷贝启动参数、初始化控制台与堆、校验 CPU、侦测内存、初始化键盘 → 视频模式设置(vga=选项)→ 禁止中断与 NMI、激活 A20、屏蔽 PIC、设置空 IDT 与 boot GDT → 进入保护模式 → 校验长模式能力、初始化页表 → 切换 64 位长模式 → 解压准备与重定位 →extract_kernel解压 → KASLR 随机化加载地址 →start_kernel。每一个环节都对应着 Booting/ 章节中可逐行研读的源码与讲解,是理解后续初始化、中断与内存管理章节的坚实起点。

【免费下载链接】linux-insides-zh

Linux 内核揭秘

项目地址:https://gitcode.com/hust-open-atom-club/linux-insides-zh
点击查看免费下载

相关推荐

上一篇:【亲测免费】 QModBus 安装和配置指南
下一篇:终极Noto Emoji定制指南:3步打造专属个性化emoji字体

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 7:17:29

高级软件架构师学习笔记——质量属性分析真题

本文重点在前面的课程中&#xff0c;我们学习了质量属性和质量效用树,下面我们来做几个真题&#xff0c;如果你可以把下面的每个内容列举的质量属性都能够识别出来&#xff0c;那么案例的第一题你就稳了。这里要和大家说一个非常牛掰的技巧&#xff0c;就是你做下面的题&#x…

作者头像 李华
网站建设 2026/9/27 7:09:39

计算机毕业设计选题推荐:基于大数据的电子游戏特卖数据分析与可视化、毕业设计选题、选题推荐、高质量项目、毕设指导、项目定制、源码、讲解文档

&#x1f496;&#x1f496;作者&#xff1a;计算机毕业设计小途 &#x1f499;&#x1f499;个人简介&#xff1a;曾长期从事计算机专业培训教学&#xff0c;本人也热爱上课教学&#xff0c;语言擅长Java、微信小程序、Python、Golang、安卓Android等&#xff0c;开发项目包括…

作者头像 李华
网站建设 2026/9/27 7:09:01

AI论文工具怎么选?11款工具按任务对照,选对效率翻倍!

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华