第1章 上电、固件与 x86_64 启动契约

发布时间:2026/7/13 1:41:31
第1章 上电、固件与 x86_64 启动契约 第1章 上电、固件与 x86_64 启动契约内核版本Linux 7.1.3架构x86_64核心源码路径Documentation/arch/x86/boot.rstarch/x86/include/uapi/asm/bootparam.harch/x86/include/asm/e820/types.harch/x86/platform/efi/efi.c1.1 内核不是从main()开始的那么从哪里开始如果我们从用户态程序的经验出发很容易下意识问一句Linux 内核的main()在哪里这个问题本身就带着用户态假设。用户态进程有运行时、有 ELF loader、有栈、有动态链接器、有 C 库内核刚获得 CPU 时这些东西都不存在。更准确地说在 x86_64 上内核启动不是从一个 C 语言main()开始而是从一份硬件和固件共同塑造的启动现场开始。这个现场至少包含三层交接第一层是 CPU 上电后的硬件入口。x86 CPU reset 后并不会知道 Linux 是什么它只会从规定的复位入口取指。传统语境里常说 reset vector 是0xFFFFFFF0也就是 4GB 物理地址空间末尾向前 16 字节的位置。那里通常映射到固件 ROM。CPU 执行的第一批指令属于 BIOS 或 UEFI 固件而不是 Linux。第二层是固件完成平台初始化。固件负责让机器从“电气上可运行”变成“可以寻找启动介质”初始化最基本的 CPU/chipset 状态发现内存枚举必要设备提供 BIOS interrupt 或 UEFI Boot Services最后选择某个 boot target。这个阶段仍然不是 Linux 内核代码在运行。第三层是 bootloader 接管。以 GRUB 为例它从磁盘、网络或其他介质读取 Linux bzImage、initrd 和启动命令行然后按照 Linux/x86 boot protocol 准备一块struct boot_params把内存布局、命令行、initrd 位置、EFI 信息、E820 内存图等内容写进去最后跳转到内核入口。内核真正开始执行时手上最重要的不是 argc/argv而是 bootloader 留下的 boot protocol 现场。所以本章回答开篇问题的工程版答案是Linux/x86_64 内核不是从main()开始。它从 CPU reset vector 进入固件固件加载 bootloaderbootloader 按 Linux/x86 boot protocol 构造boot_params、命令行和内存图然后跳入内核早期入口。后续x86_64_start_kernel()和start_kernel()看到的世界都是这份启动契约的结果。init/main.c:start_kernel()确实很像内核的“主函数”但它不是机器执行的第一段内核代码。它前面还有 setup code、compressed kernel、x86_64 汇编入口、早期页表、解压、KASLR、boot_params拷贝与校验。第 1 章暂时不进入这些细节我们先把更前面的契约讲清楚固件和 bootloader 到底给内核留下了什么。1.2 CPU reset vector第一条指令属于固件不属于内核x86 机器上电后CPU 处在一个非常受限的初始环境里。传统 PC 兼容启动模型中处理器从 reset vector 取第一条指令地址通常表述为0xFFFFFFF0。这个地址离 4GB 边界只有 16 字节固件会在那里放一个跳转或等价的入口让 CPU 转到固件真正的初始化代码。这里有几个容易混淆的点。第一reset vector 不是 Linux 内核入口。它是硬件规定的固件入口。Linux 不会被 CPU reset 直接执行。即使在 UEFI 机器上CPU 也是先跑固件再由固件的 boot manager 或 bootloader 链接到 Linux。第二reset 后的 CPU 环境不是我们熟悉的 64 位内核环境。早期 CPU 状态更接近兼容历史包袱的最小执行环境。进入 long mode、建立页表、设置 GDT/IDT、打开或关闭中断、准备栈这些都不是天然存在的。后续 Linux 的arch/x86/boot/和arch/x86/boot/compressed/目录中会有大量代码处理这些事情。第三固件不仅是“找内核文件”的程序。固件还给后续软件提供平台事实内存有多大哪些物理地址是 RAM哪些是 MMIOACPI 表在哪里EFI system table 在哪里Secure Boot 状态是什么图形输出信息是什么。这些事实后面会被setup_arch()、memblock、E820、ACPI、EFI、early console 等子系统反复使用。从工程角度看reset vector 的意义不是那个地址本身多神秘而是它把启动责任划分清楚了CPU 负责从固定入口取指。BIOS/UEFI 负责把平台带到可启动状态。bootloader 负责把 Linux 镜像、initrd、命令行和启动参数组织成 Linux 能理解的格式。Linux 负责验证这些参数然后建立自己的内存、调度、中断、文件系统和用户态世界。本书从 Linux 内核源码出发所以不会深入固件实现。但你读内核启动代码时必须记住早期内核读到的大量信息都不是内核自己发现的第一手事实而是 bootloader 和固件按协议交给它的输入。1.3 BIOS 与 UEFI两条固件路径一个共同目标传统 BIOS 和 UEFI 的实现模型差异很大但对 Linux 内核来说它们最终都要服务同一个目标把控制权交给内核并提供足够的启动参数。BIOS 路径里历史包袱更重。Linux/x86 boot protocol 文档开头就说明x86 平台的启动约定很复杂原因包括历史兼容、PC 内存模型复杂以及早期希望内核镜像本身可被直接启动。传统布局里低端内存被切得很碎0x00000附近有 BIOS/MBR 使用区0x7C00是传统 boot sector 入口0xA0000到0x100000是 BIOS、显存和 I/O hole 相关区域bzImage 的 protected-mode kernel 通常被放到0x100000以上。UEFI 路径里固件提供的是 Boot Services、Runtime Services、EFI memory map、EFI system table 和 PE/COFF 加载模型。Linux 支持 EFI stub 时内核镜像可以作为 PE/COFF 程序由 UEFI 直接加载。Documentation/arch/x86/boot.rst也提到老的 EFI Handover Protocol 已经过时推荐使用普通 PE/COFF 入口。即便如此Linux 仍然需要把 EFI 世界里的信息转成自己启动主线能消费的结构例如struct efi_info、EFI memory map以及必要时转换进 E820 表。这两条路径的共同点是内核需要一份“启动账本”。BIOS 可以通过 E820 调用给出内存图UEFI 可以通过 EFI memory map 给出更丰富的描述。bootloader 或 EFI stub 再把这些信息放到boot_params、setup_data或 EFI 相关字段里。进入内核后x86 代码不会直接相信“机器上所有地址都能用”而是围绕这份账本建立 memblock 和后续页分配器。在 Linux 7.1.3 的源码里本章最关键的几处连接点是Documentation/arch/x86/boot.rst定义 bootloader 和内核之间的 x86 boot protocol。arch/x86/include/uapi/asm/bootparam.h定义struct boot_params也就是协议核心数据结构。arch/x86/include/asm/e820/types.h定义内核内部使用的 E820 类型和 E820 表。arch/x86/platform/efi/efi.c负责把 EFI memory map 等信息导入 EFI/E820/memblock 相关路径。1.4 GRUB 与 Linux 的交接不是“跳一下”这么简单以 GRUB 启动 Linux 为例用户在配置里看到的可能只是几行linux /boot/vmlinuz root/dev/sda2 ro quiet initrd /boot/initrd.img boot但从内核视角看GRUB 至少要做这些工作读取 Linux 内核镜像识别 real-mode header 中的 boot protocol 版本和字段。决定 setup code、protected-mode kernel、命令行、initrd 放到哪些物理地址。构造或填充struct boot_params也就是传统说法里的 zero page。填写hdr.type_of_loader让内核知道是谁加载了它。boot protocol 文档给 GRUB 分配的 loader ID 是0x7。填写hdr.cmd_line_ptr和必要时的ext_cmd_line_ptr指向内核命令行字符串。填写hdr.ramdisk_image、hdr.ramdisk_size以及超过 4GB 时对应的扩展字段。填写e820_entries和e820_table[]或通过 EFI 信息、setup_data等机制提供更完整的内存图。在 x86_64 boot protocol 要求的 CPU 状态下跳入 64 位内核入口并把struct boot_params的地址放到%rsi。Documentation/arch/x86/boot.rst对 64-bit boot protocol 的要求很明确bootloader 首先要设置struct boot_params并把 setup header 从内核镜像偏移0x01f1开始复制进去启动 64 位内核时CPU 应处于 64 位模式且 paging enabled%rsi必须保存struct boot_params的基地址。这个%rsi后面非常关键。进入解压内核和解压后的内核入口时注释里反复能看到 “boot_params pointer” 的传递。Linux 7.1.3 的arch/x86/kernel/head64.c中早期 C 代码会把 bootloader 留在内存里的 boot params 拷贝到内核自己的全局boot_paramsstaticunsignedlongget_cmd_line_ptr(void){unsignedlongcmd_line_ptrboot_params.hdr.cmd_line_ptr;cmd_line_ptr|(u64)boot_params.ext_cmd_line_ptr32;returncmd_line_ptr;}staticvoid__initcopy_bootdata(char*real_mode_data){memcpy(boot_params,real_mode_data,sizeof(boot_params));sanitize_boot_params(boot_params);/* 随后根据 cmd_line_ptr 拷贝 boot_command_line */}这段代码说明两件事。第一bootloader 给内核的参数不是“只读参考”。内核会把它拷进自己的全局变量并做清理。sanitize_boot_params()用来处理一些 bootloader 没有正确清零或错误复制字段的情况。第二命令行不是魔法字符串。它只是 boot params 里一个物理地址指针指向的 NUL 结尾字符串。x86_64 支持ext_cmd_line_ptr所以地址可以由低 32 位和高 32 位拼起来。后续parse_early_param()、start_kernel()、各子系统的early_param()和__setup()都会从这条命令行分流出自己的配置。1.5 boot protocol 的核心struct boot_paramsstruct boot_params是本章最重要的数据结构。它定义在arch/x86/include/uapi/asm/bootparam.h属于 UAPI 头文件因为这不是内核内部随便改的私有结构而是 bootloader、kexec、EFI stub 等外部启动主体需要遵守的 ABI。源码里称它为 “zeropage”。这个名字来自历史早期启动参数放在一页大小的固定区域里很多偏移必须兼容老协议。Linux 7.1.3 中struct boot_params仍然是 4096 字节arch/x86/boot/main.c里还有构建期检查确认这一点。对读内核启动源码的人来说这个结构的字段大致可以分成六类显示与控制台信息screen_info、edid_info。固件和平台信息apm_bios_info、ist_info、acpi_rsdp_addr、efi_info。加载器填入的信息hdr.type_of_loader、hdr.loadflags、hdr.cmd_line_ptr、hdr.ramdisk_image。内存图信息e820_entries、e820_table[]。扩展指针hdr.setup_data、ext_cmd_line_ptr、ext_ramdisk_image、ext_ramdisk_size。兼容字段和填充字段各种_pad、obsolete 字段、sentinel。下面是围绕本章主题整理过的节选版字段顺序保持源码布局注释改成工程解释structboot_params{structscreen_infoscreen_info;/* 早期屏幕/显卡状态printk/console 会用 */structapm_bios_infoapm_bios_info;/* 旧 APM BIOS 信息主要服务兼容路径 */__u64 tboot_addr;/* Intel TXT/tboot 相关入口信息 */structist_infoist_info;/* 旧式 Intel SpeedStep 信息 */__u64 acpi_rsdp_addr;/* ACPI RSDP 物理地址ACPI 子系统入口线索 */__u32 ext_ramdisk_image;/* initrd 起始地址高 32 位 */__u32 ext_ramdisk_size;/* initrd 大小高 32 位 */__u32 ext_cmd_line_ptr;/* 命令行地址高 32 位 */__u32 cc_blob_address;/* confidential computing blob 地址 */structedid_infoedid_info;/* 显示设备 EDID 信息 */structefi_infoefi_info;/* EFI system table 与 EFI memory map 信息 */__u8 e820_entries;/* e820_table 中有效项数量 */__u8 secure_boot;/* Secure Boot 状态安全子系统会关心 */__u8 sentinel;/* 检测 bootloader 是否错误复制了未初始化区域 */structsetup_headerhdr;/* boot protocol 的 setup header 核心字段 */structboot_e820_entrye820_table[E820_MAX_ENTRIES_ZEROPAGE];/* legacy zeropage 中最多 128 项 E820 内存图 */structedd_infoeddbuf[EDDMAXNR];/* BIOS EDD 磁盘信息服务老式磁盘发现 */}__attribute__((packed));这里最值得注意的是__attribute__((packed))。这不是普通内核对象而是 ABI 布局。字段偏移写进了协议和汇编路径bootloader 也按这些偏移填数据。随便插一个字段、改变对齐、调整类型都会破坏启动契约。因此这类结构读起来会有很多_pad和 obsolete 字段看着不优雅但它们是兼容性成本。struct setup_header嵌在boot_params.hdr中它对应 boot protocol 文档里的 real-mode kernel header偏移从0x1f1开始。节选如下structsetup_header{__u8 setup_sects;/* setup code 有多少个 512 字节扇区 */__u16 root_flags;/* 历史 root 只读标志现代更多用命令行 ro/rw */__u32 syssize;/* protected-mode code 大小 */__u16 vid_mode;/* 早期视频模式请求 */__u16 root_dev;/* 历史 root 设备号现代更多用 root */__u16 boot_flag;/* 0xAA55老协议魔数 */__u32 header;/* HdrS 魔数表示新式 boot protocol */__u16 version;/* boot protocol 版本例如 0x020f */__u8 type_of_loader;/* bootloader IDGRUB 是 0x7 */__u8 loadflags;/* LOADED_HIGH、QUIET、CAN_USE_HEAP 等标志 */__u32 code32_start;/* protected-mode kernel 入口/加载地址 */__u32 ramdisk_image;/* initrd 低 32 位起始地址 */__u32 ramdisk_size;/* initrd 低 32 位大小 */__u32 cmd_line_ptr;/* 命令行低 32 位地址 */__u32 initrd_addr_max;/* initrd 允许占用的最高地址 */__u32 kernel_alignment;/* relocatable kernel 所需/偏好的对齐 */__u8 relocatable_kernel;/* protected-mode kernel 是否可重定位 */__u16 xloadflags;/* 64 位、4G 以上加载、EFI handover 等能力 */__u32 cmdline_size;/* 命令行最大长度 */__u64 setup_data;/* 扩展启动参数链表的物理地址 */__u64 pref_address;/* relocatable kernel 偏好的加载地址 */__u32 init_size;/* 初始化阶段需要映射的线性内存范围 */__u32 handover_offset;/* 旧 EFI handover 入口偏移 */__u32 kernel_info_offset;/* kernel_info 偏移协议 2.15 引入 */}__attribute__((packed));setup_header是 bootloader 读内核镜像时最先关心的结构。文档要求 bootloader 从内核镜像偏移0x01f1处读取 header。header字段如果是HdrS说明是 2.00 的新式协议version决定哪些字段可用loadflags告诉 bootloader 内核是否按 bzImage high loadxloadflags则告诉 bootloader 这是 64 位内核、能不能加载到 4GB 以上、是否支持 EFI 相关入口。工程上可以把boot_params理解成内核启动时的argv envp platform descriptor但它比用户态参数严肃得多。用户态参数错了进程退出boot params 错了内核可能在打开分页、解压、早期内存管理或挂载根文件系统前就死掉而且很多时候连正式 console 都还没有。1.6 命令行一根字符串牵动半个内核Linux 内核命令行在 boot protocol 中就是一个 NUL 结尾字符串。hdr.cmd_line_ptr保存低 32 位地址ext_cmd_line_ptr保存高 32 位。boot protocol 文档要求 bootloader 即使不支持命令行也应该把它指向空字符串或类似auto的字符串如果置零内核会认为 bootloader 不支持 2.02 协议。从用户角度看命令行是root/dev/sda2 ro quiet consolettyS0 earlyprintk...这样的文本。从内核角度看它是启动早期唯一方便扩展的配置通道。为什么很多内核功能都喜欢命令行参数因为启动早期还没有文件系统没有/etc没有 sysfs没有模块参数文件也没有用户态服务。此时最稳的输入就是 bootloader 交过来的那串字符。命令行在后续章节会不断出现root会影响根文件系统选择最终进入init/do_mounts.c。init会影响 PID 1 执行哪个用户态程序。console、earlycon会影响 printk 和控制台路径。mem、efi,add_efi_memmap会影响内存发现和 EFI 行为。nokaslr会影响压缩内核的随机化选择。ro、rw会影响根文件系统初始挂载标志。Linux 7.1.3 的arch/x86/kernel/head64.c中早期代码会把命令行从 bootloader 提供的位置复制到boot_command_line。这一步很早因为后续start_kernel()要解析 early params。也就是说命令行并不是等到用户态才有意义它从内核刚进入 C 代码不久就开始参与控制流。1.7 E820 内存图哪些地址能用不能靠猜操作系统最怕的一类启动 bug 是“把不能用的物理内存当 RAM 用”。机器的物理地址空间不是一整块 DRAM。里面有 BIOS/UEFI 保留区、ACPI 表、NVS、MMIO、PCI BAR、持久内存、坏内存、设备专用区域、soft reserved 区域。内核必须知道哪些范围能交给页分配器哪些必须保留。在传统 BIOS 世界里这个信息通过 E820 内存图传递。Linux 的arch/x86/include/asm/e820/types.h定义了内核认识的 E820 类型enume820_type{E820_TYPE_RAM1,/* 普通可用 RAM */E820_TYPE_RESERVED2,/* 固件或设备保留不能当普通页分配 */E820_TYPE_ACPI3,/* ACPI reclaim memory */E820_TYPE_NVS4,/* ACPI NVS休眠/固件状态相关 */E820_TYPE_UNUSABLE5,/* 不可用内存 */E820_TYPE_PMEM7,/* persistent memory */E820_TYPE_PRAM12,/* legacy persistent RAM 表示 */E820_TYPE_SOFT_RESERVED0xefffffff,/* EFI soft reserved 转换结果 */};structe820_entry{u64 addr;/* 起始物理地址 */u64 size;/* 区间长度 */enume820_typetype;/* 这段物理地址的用途 */}__attribute__((packed));structe820_table{u32 nr_entries;/* 有效条目数 */structe820_entryentries[E820_MAX_ENTRIES];};boot_params里的 legacy E820 表最多只有E820_MAX_ENTRIES_ZEROPAGE项也就是 128 项。这是 zero page 空间限制导致的 ABI 现实。大机器、复杂 NUMA、EFI memory map 或持久内存场景可能远远不止 128 项。因此 Linux 内部的E820_MAX_ENTRIES会更大并且可以通过 EFI memory map 或setup_data扩展。这个限制非常重要。很多人第一次读boot_params.e820_table会以为它就是完整内存事实。实际不是。它是启动协议里最传统、最基础的一份内存图现代机器上EFI memory map、setup_data扩展、平台 quirk、内核命令行修正都会参与最终内存图的形成。后续setup_arch()会把这些输入整理进e820_table和 memblock再由mm_core_init()、伙伴系统和 SLUB 接管。你可以把 E820 看成内核内存管理的第一张地图。地图不一定最终但没有它内核连“哪里可以放自己、哪里可以分配页、哪里必须避开 ACPI/固件/设备”都不知道。1.8 EFI memory mapUEFI 世界如何进入 Linux 内存模型UEFI 提供的内存图比传统 E820 更丰富。EFI memory descriptor 有自己的类型和属性例如 loader code/data、boot services code/data、conventional memory、ACPI reclaim、ACPI NVS、unusable memory、persistent memory、runtime services、memory mapped I/O 等。Linux 不能直接把这些原样交给伙伴系统它需要把 EFI 的描述转成内核自己的内存模型。arch/x86/platform/efi/efi.c中的do_add_efi_memmap()展示了这个转换思路。核心逻辑可以压缩成下面这样for_each_efi_memory_desc(md){startmd-phys_addr;sizemd-num_pagesEFI_PAGE_SHIFT;switch(md-type){caseEFI_LOADER_CODE:caseEFI_LOADER_DATA:caseEFI_BOOT_SERVICES_CODE:caseEFI_BOOT_SERVICES_DATA:caseEFI_CONVENTIONAL_MEMORY:if(md-attributeEFI_MEMORY_SP)e820_typeE820_TYPE_SOFT_RESERVED;elseif(md-attributeEFI_MEMORY_WB)e820_typeE820_TYPE_RAM;elsee820_typeE820_TYPE_RESERVED;break;caseEFI_ACPI_RECLAIM_MEMORY:e820_typeE820_TYPE_ACPI;break;caseEFI_ACPI_MEMORY_NVS:e820_typeE820_TYPE_NVS;break;caseEFI_UNUSABLE_MEMORY:e820_typeE820_TYPE_UNUSABLE;break;caseEFI_PERSISTENT_MEMORY:e820_typeE820_TYPE_PMEM;break;default:e820_typeE820_TYPE_RESERVED;}e820__range_add(start,size,e820_type);}e820__update_table(e820_table);这段代码的工程含义很直接EFI 说这段地址是什么Linux 要把它翻译成自己能处理的 E820 类型。可写回的 conventional memory 才可能成为普通 RAMACPI reclaim 和 NVS 要有特殊语义persistent memory 不能简单当 DRAMsoft reserved 需要保留给平台或特殊用途未知或 runtime/MMIO 类区域默认保守处理为 reserved。efi_memblock_x86_reserve_range()还会从boot_params.efi_info取出 EFI memory map 的物理地址、大小、descriptor size 和版本structefi_info*eboot_params.efi_info;pmape-efi_memmap|((u64)e-efi_memmap_hi32);data.phys_mappmap;data.sizee-efi_memmap_size;data.desc_sizee-efi_memdesc_size;data.desc_versione-efi_memdesc_version;这再次说明boot_params不是启动早期用一下就丢的东西。EFI 子系统会从中读取 EFI 表地址和 memory map 信息ACPI 会从acpi_rsdp_addr或 EFI config table 找 RSDP内存管理会围绕这些信息保留固件区域避免把它们当作普通页使用。1.9 boot params 在内核后续路径里的第一批消费者第 1 章不进入start_kernel()但可以提前看一眼这些字段后面会被谁消费。这样读者会明白本章不是启动前传而是全书的数据入口。arch/x86/kernel/setup.c:parse_boot_params()会读取多个 boot params 字段ROOT_DEVold_decode_dev(boot_params.hdr.root_dev);sysfb_primary_display.screenboot_params.screen_info;saved_video_modeboot_params.hdr.vid_mode;bootloader_typeboot_params.hdr.type_of_loader;if(boot_params.efi_info.efi_loader_signatureEFI64_LOADER_SIGNATURE)set_bit(EFI_BOOT,efi.flags);if(!boot_params.hdr.root_flags)root_mountflags~MS_RDONLY;这段代码看起来朴素但覆盖了多个子系统入口ROOT_DEV和root_mountflags会一路影响根文件系统挂载。screen_info会影响 sysfb、early console、后续图形/控制台。type_of_loader会记录 bootloader 类型供诊断和 sysfs 暴露。efi_loader_signature决定内核是否认为自己在 EFI 环境下启动。root_flags虽然是老字段但仍然参与只读/读写挂载默认值。另外arch/x86/kernel/ksysfs.c还会把 boot params 暴露到 sysfs例如/sys/kernel/boot_params相关节点。这意味着启动契约不只是早期内部状态用户态诊断工具也可以回看一部分启动参数。1.10 本章引出的四个子系统本章正式引出四条线后续章节会反复回到它们。x86 boot protocol它是 bootloader 与 Linux/x86 内核之间的 ABI。它规定 real-mode header 的位置、字段含义、bootloader 应该写什么、内核支持什么、64 位入口状态是什么、%rsi如何传递boot_params。没有这个协议GRUB、kexec、EFI stub、QEMU 直接加载内核都无法形成统一交接。boot paramsstruct boot_params是协议的载体。它把屏幕、固件、命令行、initrd、E820、EFI、ACPI、loader ID、扩展 setup_data 等信息放在一页大小的 ABI 布局里。它是 x86 早期内核最重要的输入对象。命令行命令行是启动早期最灵活的配置通道。它从 bootloader 传入经cmd_line_ptr定位被早期 x86 代码复制然后在start_kernel()中参与 early param 和普通内核参数解析。后续内存、控制台、根文件系统、安全、调试都会读它。E820/EFI 内存图内存图决定哪些物理地址能用、哪些必须避开。E820 是传统 BIOS 语境下的内存描述EFI memory map 是 UEFI 语境下更丰富的描述Linux 会把这些输入合并、修正、转换再喂给 memblock 和页分配器。1.11 时序定位卡片上电 - CPU 从 reset vector 取指传统位置为 0xFFFFFFF0 - BIOS/UEFI 固件初始化 CPU、芯片组、内存和必要设备 - 固件选择启动项加载或启动 bootloader - [本章] bootloader/EFI stub 按 Linux/x86 boot protocol 准备启动契约 - 读取 bzImage setup header - 构造 struct boot_params - 填写命令行、initrd、loader ID - 提供 E820 或 EFI memory map - 准备 ACPI/EFI 相关地址 - GRUB 加载内核和 initrd 到内存 - 跳入 x86_64 内核早期入口%rsi 指向 boot_params - arch/x86/boot/compressed/ 建立解压环境 - 解压后的 startup_64 / x86_64_start_kernel() - start_kernel() - setup_arch() - mm_core_init() / sched_init() / trap_init() / init_IRQ() - rest_init() - kernel_init_freeable() - do_initcalls() - 挂载根文件系统 - execve(/sbin/init)进入用户态1.12 小结启动契约会贯穿全书本章没有急着进入start_kernel()因为在 x86_64 上start_kernel()能做什么取决于它之前拿到了什么。boot_params告诉内核自己被谁加载、命令行在哪里、initrd 在哪里、物理内存如何分布、EFI 表在哪里、ACPI RSDP 在哪里、早期显示状态是什么。E820/EFI 内存图决定 memblock 的基础边界命令行决定大量 early policyloader ID 和协议版本决定哪些字段可信setup_data则给后来的协议扩展留下空间。后续章节会反复引用这些结构解压内核会用boot_params避开 initrd、命令行和自身占用区域。KASLR 会读取命令行和内存图选择随机化位置。x86_64_start_kernel()会拷贝并清理 boot params。setup_arch()会解析 E820、EFI、ACPI、NUMA 和 CPU 拓扑。内存管理会把 E820/EFI 结果转成 memblock再建立struct page。根文件系统和 PID 1 路径会消费命令行里的root、init、ro/rw。因此从本章开始请把struct boot_params当作全书第一份核心数据结构。它不是启动流程里的背景资料而是从上电到用户态这条长链路的第一张工程交接单。