当前位置: 首页 > news >正文

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

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

内核版本:Linux 7.1.3
架构:x86_64
核心源码路径:

  • Documentation/arch/x86/boot.rst
  • arch/x86/include/uapi/asm/bootparam.h
  • arch/x86/include/asm/e820/types.h
  • arch/x86/platform/efi/efi.c

1.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 进入固件;固件加载 bootloader;bootloader 按 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,哪些是 MMIO,ACPI 表在哪里,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 入口,0xA00000x100000是 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_paramssetup_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 至少要做这些工作:

  1. 读取 Linux 内核镜像,识别 real-mode header 中的 boot protocol 版本和字段。
  2. 决定 setup code、protected-mode kernel、命令行、initrd 放到哪些物理地址。
  3. 构造或填充struct boot_params,也就是传统说法里的 zero page。
  4. 填写hdr.type_of_loader,让内核知道是谁加载了它。boot protocol 文档给 GRUB 分配的 loader ID 是0x7
  5. 填写hdr.cmd_line_ptr和必要时的ext_cmd_line_ptr,指向内核命令行字符串。
  6. 填写hdr.ramdisk_imagehdr.ramdisk_size,以及超过 4GB 时对应的扩展字段。
  7. 填写e820_entriese820_table[],或通过 EFI 信息、setup_data等机制提供更完整的内存图。
  8. 在 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_params

staticunsignedlongget_cmd_line_ptr(void){unsignedlongcmd_line_ptr=boot_params.hdr.cmd_line_ptr;cmd_line_ptr|=(u64)boot_params.ext_cmd_line_ptr<<32;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_params

struct 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_infoedid_info
  • 固件和平台信息:apm_bios_infoist_infoacpi_rsdp_addrefi_info
  • 加载器填入的信息:hdr.type_of_loaderhdr.loadflagshdr.cmd_line_ptrhdr.ramdisk_image
  • 内存图信息:e820_entriese820_table[]
  • 扩展指针:hdr.setup_dataext_cmd_line_ptrext_ramdisk_imageext_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 ID,GRUB 是 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 load;xloadflags则告诉 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 console=ttyS0 earlyprintk=...这样的文本。从内核角度看,它是启动早期唯一方便扩展的配置通道。为什么很多内核功能都喜欢命令行参数?因为启动早期还没有文件系统,没有/etc,没有 sysfs,没有模块参数文件,也没有用户态服务。此时最稳的输入就是 bootloader 交过来的那串字符。

命令行在后续章节会不断出现:

  • root=会影响根文件系统选择,最终进入init/do_mounts.c
  • init=会影响 PID 1 执行哪个用户态程序。
  • console=earlycon=会影响 printk 和控制台路径。
  • mem=efi=,add_efi_memmap会影响内存发现和 EFI 行为。
  • nokaslr会影响压缩内核的随机化选择。
  • rorw会影响根文件系统初始挂载标志。

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_RAM=1,/* 普通可用 RAM */E820_TYPE_RESERVED=2,/* 固件或设备保留,不能当普通页分配 */E820_TYPE_ACPI=3,/* ACPI reclaim memory */E820_TYPE_NVS=4,/* ACPI NVS,休眠/固件状态相关 */E820_TYPE_UNUSABLE=5,/* 不可用内存 */E820_TYPE_PMEM=7,/* persistent memory */E820_TYPE_PRAM=12,/* legacy persistent RAM 表示 */E820_TYPE_SOFT_RESERVED=0xefffffff,/* 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 map:UEFI 世界如何进入 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){start=md->phys_addr;size=md->num_pages<<EFI_PAGE_SHIFT;switch(md->type){caseEFI_LOADER_CODE:caseEFI_LOADER_DATA:caseEFI_BOOT_SERVICES_CODE:caseEFI_BOOT_SERVICES_DATA:caseEFI_CONVENTIONAL_MEMORY:if(md->attribute&EFI_MEMORY_SP)e820_type=E820_TYPE_SOFT_RESERVED;elseif(md->attribute&EFI_MEMORY_WB)e820_type=E820_TYPE_RAM;elsee820_type=E820_TYPE_RESERVED;break;caseEFI_ACPI_RECLAIM_MEMORY:e820_type=E820_TYPE_ACPI;break;caseEFI_ACPI_MEMORY_NVS:e820_type=E820_TYPE_NVS;break;caseEFI_UNUSABLE_MEMORY:e820_type=E820_TYPE_UNUSABLE;break;caseEFI_PERSISTENT_MEMORY:e820_type=E820_TYPE_PMEM;break;default:e820_type=E820_TYPE_RESERVED;}e820__range_add(start,size,e820_type);}e820__update_table(e820_table);

这段代码的工程含义很直接:EFI 说这段地址是什么,Linux 要把它翻译成自己能处理的 E820 类型。可写回的 conventional memory 才可能成为普通 RAM;ACPI reclaim 和 NVS 要有特殊语义;persistent memory 不能简单当 DRAM;soft reserved 需要保留给平台或特殊用途;未知或 runtime/MMIO 类区域默认保守处理为 reserved。

efi_memblock_x86_reserve_range()还会从boot_params.efi_info取出 EFI memory map 的物理地址、大小、descriptor size 和版本:

structefi_info*e=&boot_params.efi_info;pmap=e->efi_memmap|((u64)e->efi_memmap_hi<<32);data.phys_map=pmap;data.size=e->efi_memmap_size;data.desc_size=e->efi_memdesc_size;data.desc_version=e->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_DEV=old_decode_dev(boot_params.hdr.root_dev);sysfb_primary_display.screen=boot_params.screen_info;saved_video_mode=boot_params.hdr.vid_mode;bootloader_type=boot_params.hdr.type_of_loader;if(boot_params.efi_info.efi_loader_signature==EFI64_LOADER_SIGNATURE)set_bit(EFI_BOOT,&efi.flags);if(!boot_params.hdr.root_flags)root_mountflags&=~MS_RDONLY;

这段代码看起来朴素,但覆盖了多个子系统入口:

  • ROOT_DEVroot_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 params

struct 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 policy;loader 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当作全书第一份核心数据结构。它不是启动流程里的背景资料,而是从上电到用户态这条长链路的第一张工程交接单。

http://www.jsqmd.com/news/1177606/

相关文章:

  • 2026适合资深记者深度人物采访场景 面试记录整理哪个好
  • Agent是什么?
  • 抖音内容管理革命:如何用Python工具高效批量下载无水印视频
  • 2026年最新教程:视频怎么变成音频 亲测好用的免费方法 - 图片处理研究员
  • TensorFlow工业级交付:从SavedModel到Serving的工程实践
  • Ansys Fluent电化学模块电解制氢仿真:从基础到实战
  • 免费开源Windows桌面分区神器:NoFences终极使用指南,让你的桌面整洁如新
  • OpenCV 4.8.0 在 Visual Studio 2022 配置:3步解决“无法打开源文件”与 LNK2019 链接错误
  • VS2022 Qt 6.4 环境配置:MSVC 2022 与 MinGW 双编译器路径解析与 3 步验证
  • TMC7300与PIC18F86K90驱动有刷直流电机方案解析
  • 如何让Windows 11完美运行经典DirectX游戏:DDrawCompat终极解决方案
  • Windows安卓应用安装器:无需模拟器的跨平台解决方案终极指南
  • 数据结构期末复习:8大实验50题核心考点解析与易错点总结
  • 5G NR 帧结构配置实战:2.6GHz/3.5GHz/4.9GHz 三频段 5 种时隙方案详解
  • C++ MySQL数据库访问层设计:连接池、防注入与ORM封装实践
  • C++哈希表实战:从零构建高性能电话号码查找系统
  • C++字符串字面量全解析:从编码到C++23新特性
  • macOS 13 Ventura 安装 Git 2.40:5个常见错误与自动化脚本修复
  • 2026HVV(网络安全护网)行动详解
  • 国货洗衣液深度测评|主流十大高口碑洗衣液牌子全维度横向汇总实测
  • 终极指南:如何用HiveWE打造高性能魔兽争霸III地图编辑器
  • 影刀RPA 团队协作开发:多人维护同一个流程的最佳实践
  • 聊一聊功放接反对立体声的影响
  • 躺玩游戏: 手柄 + 平板 + 支架 + sunshine 局域网 投屏 (串流)
  • 系统架构师软考:3类图论应用题(最大流/最小生成树/最短路径)解题策略对比
  • 降 AIGC 的工具那么多,到底怎么选才不踩坑?
  • Kodi观影革命:当字幕下载变得像呼吸一样自然
  • Qt5 GUI编程实战:从自定义控件到多线程优化的核心技巧
  • AI学习者社区通讯录:真实工作流驱动的技术成长指南
  • ROS 2 Galactic 在 Ubuntu 22.04 环境下的 3 种安装方式与避坑实测