Linux 6.6内核 CPU 深度解析(零):x86 内核引导协议 — 从 bootloader 到 main()

引言:内核是从 main() 开始的吗?

不是。#1 里讲的 main()(arch/x86/boot/main.c:136)已经是"实模式 setup 代码"的 C 入口了。在它之前,还有一整段**"bootloader 怎么找到内核、怎么跳进来、双方怎么约定"**的故事。

这段故事的核心是一份公开 ABI ------boot protocol(引导协议)。它规定了:

  1. 内核镜像(bzImage)长什么样、分几段;
  2. bootloader(GRUB、SYSLINUX、UEFI 固件等)从哪个地址跳进来;
  3. 跳进来时 CPU 处于什么状态、哪些寄存器该装什么。

bootloader 和内核互不信任、由不同团队维护,全靠这份协议对齐。理解了它,才能回答"内核为什么从这个地址开始跑"。


全景图

#mermaid-svg-PFto5qf3zbdnsKTa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PFto5qf3zbdnsKTa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PFto5qf3zbdnsKTa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PFto5qf3zbdnsKTa .error-icon{fill:#552222;}#mermaid-svg-PFto5qf3zbdnsKTa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PFto5qf3zbdnsKTa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PFto5qf3zbdnsKTa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PFto5qf3zbdnsKTa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PFto5qf3zbdnsKTa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PFto5qf3zbdnsKTa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PFto5qf3zbdnsKTa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PFto5qf3zbdnsKTa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PFto5qf3zbdnsKTa .marker.cross{stroke:#333333;}#mermaid-svg-PFto5qf3zbdnsKTa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PFto5qf3zbdnsKTa p{margin:0;}#mermaid-svg-PFto5qf3zbdnsKTa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PFto5qf3zbdnsKTa .cluster-label text{fill:#333;}#mermaid-svg-PFto5qf3zbdnsKTa .cluster-label span{color:#333;}#mermaid-svg-PFto5qf3zbdnsKTa .cluster-label span p{background-color:transparent;}#mermaid-svg-PFto5qf3zbdnsKTa .label text,#mermaid-svg-PFto5qf3zbdnsKTa span{fill:#333;color:#333;}#mermaid-svg-PFto5qf3zbdnsKTa .node rect,#mermaid-svg-PFto5qf3zbdnsKTa .node circle,#mermaid-svg-PFto5qf3zbdnsKTa .node ellipse,#mermaid-svg-PFto5qf3zbdnsKTa .node polygon,#mermaid-svg-PFto5qf3zbdnsKTa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PFto5qf3zbdnsKTa .rough-node .label text,#mermaid-svg-PFto5qf3zbdnsKTa .node .label text,#mermaid-svg-PFto5qf3zbdnsKTa .image-shape .label,#mermaid-svg-PFto5qf3zbdnsKTa .icon-shape .label{text-anchor:middle;}#mermaid-svg-PFto5qf3zbdnsKTa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PFto5qf3zbdnsKTa .rough-node .label,#mermaid-svg-PFto5qf3zbdnsKTa .node .label,#mermaid-svg-PFto5qf3zbdnsKTa .image-shape .label,#mermaid-svg-PFto5qf3zbdnsKTa .icon-shape .label{text-align:center;}#mermaid-svg-PFto5qf3zbdnsKTa .node.clickable{cursor:pointer;}#mermaid-svg-PFto5qf3zbdnsKTa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PFto5qf3zbdnsKTa .arrowheadPath{fill:#333333;}#mermaid-svg-PFto5qf3zbdnsKTa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PFto5qf3zbdnsKTa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PFto5qf3zbdnsKTa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PFto5qf3zbdnsKTa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PFto5qf3zbdnsKTa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PFto5qf3zbdnsKTa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PFto5qf3zbdnsKTa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PFto5qf3zbdnsKTa .cluster text{fill:#333;}#mermaid-svg-PFto5qf3zbdnsKTa .cluster span{color:#333;}#mermaid-svg-PFto5qf3zbdnsKTa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PFto5qf3zbdnsKTa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PFto5qf3zbdnsKTa rect.text{fill:none;stroke-width:0;}#mermaid-svg-PFto5qf3zbdnsKTa .icon-shape,#mermaid-svg-PFto5qf3zbdnsKTa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PFto5qf3zbdnsKTa .icon-shape p,#mermaid-svg-PFto5qf3zbdnsKTa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PFto5qf3zbdnsKTa .icon-shape .label rect,#mermaid-svg-PFto5qf3zbdnsKTa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PFto5qf3zbdnsKTa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PFto5qf3zbdnsKTa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PFto5qf3zbdnsKTa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内核镜像 bzImage
setup 段

实模式 boot stub
compressed 段

解压器 + 压缩内核
MZ/PE 头

(UEFI 识别用)
setup_header

(boot protocol ABI)
实模式代码

.bstext + .entrytext
decompressor

head_64.S
vmlinux.bin

(压缩后内核)
bootloader / 固件交接
BIOS: 实模式跳 _start
UEFI: EFI stub 或 handover
start_of_setup → main()

(实模式)
efi_pe_entry / efi_stub_entry

(64 位)
进 compressed 段解压

一句话:bzImage 由 setup 段 + compressed 段拼成,bootloader 先读 setup_header 了解内核要求,然后走两条路之一------BIOS 跳 _start(偏移 512)经 start_of_setup 到 main(),或 UEFI 走 EFI stub/handover 直接进 64 位------最终都汇到 compressed 段解压。


一、bzImage 的结构:两段拼成的镜像

编译内核的最后一步,把几样东西拼成一个可启动文件 bzImage (vmlinuz 是它的历史别名):

复制代码
┌────────────────────────────────────────────┐
│ setup 段(实模式 boot stub,~16~32KB)      │
│   ├ MZ/PE 头      ------ UEFI 识别用            │
│   ├ setup_header  ------ boot protocol ABI      │
│   └ 实模式代码     ------ .bstext + .entrytext  │
├────────────────────────────────────────────┤
│ compressed 段                              │
│   ├ decompressor  ------ head_64.S 自解压       │
│   └ vmlinux.bin   ------ 压缩后的真正内核        │
└────────────────────────────────────────────┘

生成链:vmlinux(ELF,带符号)→ objcopy 去掉 ELF 头 → vmlinux.bin → 压缩(gzip/lzma/xz/zstd)→ 和 setup 段拼成 bzImage。

关键设计:MZ + PE 双身份 。setup 段的开头(arch/x86/boot/header.S:44-47):

asm 复制代码
#ifdef CONFIG_EFI_STUB
	# "MZ", MS-DOS header
	.word	MZ_MAGIC
#endif

MZ 是 DOS/PE 可执行文件的魔数。于是同一个 bzImage 有两种身份:

  • BIOS 看它:一段 16 位实模式代码,直接跳转执行。
  • UEFI 看它:一个 PE/COFF 可执行文件,由 EFI stub 加载。

这一个魔数,让内核不用出两个镜像就能同时支持传统 BIOS 和 UEFI 启动。


二、引导扇区 .bstext:一段"兜底报错"代码

setup 段最开头是一小段 16 位引导扇区代码(.bstext 段,header.S:40):

asm 复制代码
	.section ".bstext", "ax"

	.global bootsect_start
bootsect_start:                  # :43 ------ 引导扇区入口
#ifdef CONFIG_EFI_STUB
	.word	MZ_MAGIC         # :46 ------ UEFI 用 MZ 头
#endif
	ljmp	$BOOTSEG, $start2 # :50 ------ 归一化段地址

start2:
	movw	%cs, %ax          # 段寄存器全指到 BOOTSEG
	movw	%ax, %ds
	movw	%ax, %es
	movw	%ax, %ss
	xorw	%sp, %sp          # 清栈
	sti                        # 开中断
	cld

	movw	$bugger_off_msg, %si
msg_loop:                      # :63 ------ 用 BIOS int 0x10 打印字符
	lodsb
	andb	%al, %al
	jz	bs_die
	movb	$0xe, %ah
	movw	$7, %bx
	int	$0x10
	jmp	msg_loop

bs_die:                        # :72 ------ 等按键后 int 0x19 重启
	xorw	%ax, %ax
	int	$0x16
	int	$0x19
	ljmp	$0xf000,$0xfff0   # :80 ------ BIOS reset

这段代码干的事很"摆烂":如果你把内核文件直接当引导扇区烧进磁盘(没有 bootloader),它就打印一行 Use a boot loader("请用引导加载器"),等你按个键,然后重启。

Why 这段存在 :Linux 内核故意不能 自己当引导扇区启动------它需要 bootloader 做一大堆准备(加载到正确地址、填 boot_params、传命令行)。所以引导扇区代码只负责"报错 + 提醒用户装 bootloader",真正的入口在下一个扇区的 _start。

顺带:bugger_off_msg 这个变量名直译就是"滚蛋消息",内核源码里的幽默。


三、boot protocol:setup_header 这份 ABI

3.1 它嵌在内核镜像的固定偏移处

bootloader 和内核要"对上暗号",靠的是 struct setup_header(arch/x86/include/uapi/asm/bootparam.h:67)------这就是 boot protocol 的数据结构 ,嵌在内核镜像的 .header 段,位于 偏移 0x1f1 处(bootparam.h:225 注释明确标了 /* 0x1f1 */):

c 复制代码
struct setup_header {
	__u8	setup_sects;        // 0x1f1 ------ setup 占几个 512B 扇区
	__u16	root_flags;         // 0x1f2
	__u32	syssize;            // 0x1f4 ------ 保护模式内核大小
	__u16	ram_size;           // ...
	__u16	vid_mode;
	__u16	root_dev;
	__u16	boot_flag;          // 0x1fe ------ 0xAA55 实模式启动签名
	__u16	jump;               // 0x200 ------ 跳到实模式入口的跳转
	__u32	header;             // 0x202 ------ "HdrS" 魔数
	__u16	version;            // 0x206 ------ 协议版本
	__u32	realmode_swtch;     // 0x208
	__u16	start_sys_seg;
	__u16	kernel_version;
	__u8	type_of_loader;     // 0x210 ------ loader ID
	__u8	loadflags;          // 0x211 ------ LOADED_HIGH 等标志
	__u16	setup_move_size;
	__u32	code32_start;       // 0x214 ------ 32 位入口地址
	__u32	ramdisk_image;
	__u32	ramdisk_size;
	__u32	bootsect_kludge;
	__u16	heap_end_ptr;
	__u8	ext_loader_ver;
	__u8	ext_loader_type;
	__u32	cmd_line_ptr;       // 0x228 ------ 内核命令行地址
	__u32	initrd_addr_max;
	__u32	kernel_alignment;   // 0x230 ------ 内核对齐要求
	__u8	relocatable_kernel; // 内核是否可重定位
	__u8	min_alignment;
	__u16	xloadflags;
	__u32	cmdline_size;
	__u32	hardware_subarch;   // 0x23c ------ 虚拟化环境(0=PC, 2=Xen...)
	__u64	hardware_subarch_data;
	__u32	payload_offset;
	__u32	payload_length;
	__u64	setup_data;         // 0x250 ------ setup_data 链表头
	__u64	pref_address;
	__u32	init_size;          // init 段大小
	__u32	handover_offset;    // UEFI handover 入口偏移
	__u32	kernel_info_offset;
} __attribute__((packed));

3.2 几个最关键的字段

字段 作用 为什么关键
header("HdrS") 魔数,证明"这是个 Linux 内核" bootloader 靠它认内核
version(0x020f) 协议版本号 loader 和内核据此对齐行为
setup_sects setup 段占多少扇区 loader 知道要加载多少
code32_start 32 位入口地址(默认 0x100000) loader 最终跳到哪
kernel_alignment / relocatable_kernel 内核对齐要求、能否重定位 决定加载到哪个物理地址
cmd_line_ptr 命令行参数地址 传内核启动参数
handover_offset UEFI handover 入口偏移 UEFI 启动时用

Why 是固定偏移而不是别的 :bootloader 不能"读懂"内核的任何结构体,它只知道"在偏移 0x1f1 处有一块固定布局的数据"。所以 setup_header 必须 __attribute__((packed))、字段顺序和大小写死在协议里------这是内核和 GRUB 之间几十年不变的契约。

这些字段在汇编侧(header.S .header 段)有一模一样的手工定义 ,HdrS(header.S:310)、版本 0x020f(:311)、code32_start 默认 0x100000(:338-340)。C 侧和汇编侧必须逐字节对齐,build.c 负责编译后填充那些"编译期才知道"的字段(如 setup_sects)。


四、_start:bootloader 的跳入点

4.1 为什么在偏移 512

bootloader 读完 setup_header、按内核要求把镜像加载到内存后,跳到 _start (header.S:300)。它不在镜像开头,而在偏移 512(第 2 个扇区) ------因为前 512 字节是引导扇区(.bstext + MZ 头),正常启动根本不执行那段。

传统内存布局(boot protocol 约定,LOADED_HIGH 时):

内容 地址
setup 段 0x90000
_start(setup + 0x200) 0x90200
压缩内核 0x100000(1MB 以上)

所以 bootloader 实际是跳到 0x90200 执行。

4.2 一条 2 字节短跳转的玄机

_start 处的第一条指令不是普通跳转,而是手工拼的 2 字节短跳转(header.S:300-305):

asm 复制代码
	.globl	_start
_start:
	# Explicitly enter this as bytes, or the assembler
	# tries to generate a 3-byte jump here, which causes
	# everything else to push off to the wrong offset.
	.byte	0xeb		# short (2-byte) jump
	.byte	start_of_setup-1f
1:

0xeb 是"短跳转"操作码,后面跟 1 字节偏移,跳到 start_of_setup。为什么不用汇编器自动生成 jmp :注释说得很直白------汇编器会生成 3 字节的跳转,而这里的每个字节偏移都是 boot protocol 的契约:_start 位于偏移 0x200,它本身就是 setup_header 的 jump 字段(bootparam.h:75),紧接着才是 header(HdrS)、version 等字段。跳转一旦变成 3 字节,就把后面所有字段的偏移都顶错位了。所以必须手工锁死成 2 字节。


五、两条交接:BIOS 实模式 vs UEFI

同一个 bzImage,BIOS 和 UEFI 走不同的路。而 UEFI 下因为 MZ/PE 头,内核被识别成 PE 应用后,"谁来引导"又分两条(efi_mixed.S:26-47 注释画了这张图):

BIOS + GRUB UEFI 直接引导(EFI stub) UEFI + GRUB(handover)
谁加载 GRUB(实模式) UEFI 固件直接加载内核 GRUB(EFI 版)加载内核
进入模式 实模式(16 位) 已是 64 位(PE 入口) 已是 64 位
入口 _start(0x200) efi_pe_entry(PE/COFF 入口点) efi_stub_entry(handover 入口)
关键字段 code32_start 等 无(PE 头直接定位入口) handover_offset(setup_header:105)

BIOS 路径 :bootloader(GRUB 等)在实模式,加载 setup 到 0x90000、内核到 0x100000,填好 boot_params,跳到 _start(0x90200),走 start_of_setup。

UEFI 直接引导(EFI stub) :UEFI 固件把 bzImage 当 PE/COFF 应用直接加载 ,从 PE 入口点调 efi_pe_entry(efi_mixed.S:67)。这是"无 bootloader"的 UEFI 启动------内核自带一个 EFI 引导桩(CONFIG_EFI_STUB),UEFI 固件像启动一个 .efi 文件那样启动内核。

UEFI + GRUB(handover protocol) :GRUB(EFI 版)加载内核后,通过 handover protocol 跳到 efi_stub_entry,并额外传一个 boot_params 指针。efi_mixed.S:54-55 正是靠"有没有这个指针"来区分两条路------有指针(handover)就跳 efi_stub_entry,没有(固件直载)才走 efi_pe_entry。

Why 殊途同归 :无论哪条,最终都汇到"setup 代码准备好环境 → 切到保护/长模式 → 解压内核"这条主线。boot protocol 的价值就在于:把"固件/引导器差异"挡在入口之前,之后的内核代码不用关心自己是被 BIOS、UEFI 固件还是 GRUB 拉起来的。


六、start_of_setup:实模式 setup 的前奏

_start 短跳转后,进入 .entrytext 段的 start_of_setup(header.S:589),这是 setup 代码在汇编层的真正起点,干这几件事:

asm 复制代码
start_of_setup:
	# ① 强制 %es = %ds,清方向位
	movw	%ds, %ax
	movw	%ax, %es
	cld

	# ② 校验 %ss,不对就重算栈指针(兼容老 LILO 的怪调用约定)
	movw	%ss, %dx
	cmpw	%ax, %dx
	movw	%sp, %dx
	je	2f
	movw	$_end, %dx            # 重建栈
	...
	sti                          # :620 ------ 现在有能用的栈了

	# ③ 归一化 %cs
	pushw	%ds
	pushw	$6f
	lretw

	# ④ 检查 setup 签名
	cmpl	$0x5a5aaa55, setup_sig # :630 ------ 签名不对就去 setup_bad
	jne	setup_bad

	# ⑤ 清零 bss
	movw	$__bss_start, %di
	movw	$_end+3, %cx
	xorl	%eax, %eax
	subw	%di, %cx
	shrw	$2, %cx
	rep; stosl                    # :639 ------ 循环清零

	# ⑥ 跳进 C 代码
	calll	main                   # :642 ------ 从这里接 #1 的 main()

最后一步 calll main(header.S:642)------跳到 main()(main.c:136),从此进入 C 世界,无缝衔接 #1 讲的 main() → startup_32 → 解压 → start_kernel。

而如果签名 0x5a5aaa55 对不上,走 setup_bad(:645)打印 "setup corrupt" 后进 die(:652)------hlt 死循环。这个签名是"setup 段被完整加载、没被破坏"的校验。

Why 这一章的意义 :start_of_setup 是内核代码第一次"自己接管 CPU"。在此之前,CPU 的状态(段寄存器、栈、中断)全是 bootloader 摆的,不可信;所以它上来就重设段、重算栈、验签名、清 bss------把环境"洗"成内核自己预期的样子,才有资格进 C 代码。这是"从他人手里接管机器"的标准姿势。


七、记住三点

  1. bzImage 两段 + 双身份 :setup 段(MZ 头 + setup_header + 实模式代码)+ compressed 段(解压器 + 压缩内核);开头 MZ 魔数(header.S:46)让 BIOS 当实模式代码、UEFI 当 PE 应用,同一镜像两用。
  2. boot protocol = setup_header 这份 ABI :嵌在镜像偏移 0x1f1(bootparam.h:225),HdrS 魔数 / version / code32_start / kernel_alignment 等字段,是 bootloader 和内核几十年不变的契约;C 侧(bootparam.h:67)和汇编侧(header.S .header)逐字节对齐。
  3. 入口链 = _start → start_of_setup → main() :bootloader 跳 _start(偏移 512,手工 2 字节短跳转,header.S:300-305);start_of_setup 重设段/栈、验签名 0x5a5aaa55、清 bss,最后 calll main(header.S:642)接 #1。
相关推荐
Ruiery1 小时前
Linux 6.6内核 CPU 深度解析(十六):x86 保护机制 — 从 ring 到页表的权限防线
linux·运维·服务器
qetfw1 小时前
Linux 文件清理:find、mtime 与安全传参验证
linux·shell·1024程序员节
贵州诚鑫致达科技有限公司1 小时前
PHP两个9.2分RCE漏洞,速查版本
服务器·php·1024程序员节
不老刘2 小时前
异构双 Agent 协同:构建无人值守的长任务自动化研发流水线
运维·人工智能·自动化·ai编程·1024程序员节
可乐_不渴2 小时前
【Linux】 项目自动化构建工具makefile&& 版本管理Git&&第一个程序
linux·git·自动化·1024程序员节
织码weavecodes2 小时前
医疗、零售、物业培训怎么落地?织码在线教育系统的三类行业案例
运维·服务器·零售
智能运维指南2 小时前
企业运维监控平台哪家好?2026年五大全栈可观测方案对比与选型推荐
大数据·运维·人工智能
乔旭伟2 小时前
部署形态选型笔记:多租户 SaaS 与单机本地部署的运维模型对比
运维·服务器·1024程序员节
西安同步电子3 小时前
时统卡驱动踩坑:Windows 和 Linux 下时间同步的底层差异
linux·stm32·单片机