引言:内核是从 main() 开始的吗?
不是。#1 里讲的 main()(arch/x86/boot/main.c:136)已经是"实模式 setup 代码"的 C 入口了。在它之前,还有一整段**"bootloader 怎么找到内核、怎么跳进来、双方怎么约定"**的故事。
这段故事的核心是一份公开 ABI ------boot protocol(引导协议)。它规定了:
- 内核镜像(bzImage)长什么样、分几段;
- bootloader(GRUB、SYSLINUX、UEFI 固件等)从哪个地址跳进来;
- 跳进来时 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 代码。这是"从他人手里接管机器"的标准姿势。
七、记住三点
- bzImage 两段 + 双身份 :setup 段(MZ 头 + setup_header + 实模式代码)+ compressed 段(解压器 + 压缩内核);开头
MZ魔数(header.S:46)让 BIOS 当实模式代码、UEFI 当 PE 应用,同一镜像两用。 - boot protocol =
setup_header这份 ABI :嵌在镜像偏移 0x1f1(bootparam.h:225),HdrS魔数 /version/code32_start/kernel_alignment等字段,是 bootloader 和内核几十年不变的契约;C 侧(bootparam.h:67)和汇编侧(header.S.header)逐字节对齐。 - 入口链 =
_start→start_of_setup→main():bootloader 跳_start(偏移 512,手工 2 字节短跳转,header.S:300-305);start_of_setup重设段/栈、验签名0x5a5aaa55、清 bss,最后calll main(header.S:642)接 #1。