汇编文件的本质与 Linux 0.11 引导扇区的编译、链接、写入全流程
汇编语言,可以说是人类与 CPU 之间最直接的对话方式。它不像 C 语言那样高度抽象,也不像机器语言那样难以阅读。汇编语言本质上就是一种用助记符(Mnemonics)代表机器指令的语言。
而一个汇编源文件(如
.s文件),从我们写下的那些看似晦涩的指令,到最终真正被 CPU 执行,中间要经历汇编、链接、加载等几个关键步骤。对于普通应用程序,这个流程可能已经被集成开发环境(IDE)隐藏得很好了;但对于 Linux 0.11 这样的早期操作系统内核,尤其是它的引导扇区
bootsect.s,这个过程却充满了硬核的手工操作和精巧的底层设计。今天,我们就来一场彻底的探索:从汇编文件的本质讲起,一路追踪到 Linux 0.11 的
bootsect.s是如何被编译、链接,并最终写入软盘第一扇区的。我会用大量图解和真实代码,让你身临其境地感受那个 1991 年的手工构建时代。
一、汇编语言的本质与编译链接流程
1.1 汇编语言:人读得懂的机器指令
CPU 只认识二进制机器码,比如 1011 1000 0000 0001 可能表示“把立即数 1 放入 AX 寄存器”。这种二进制对人类来说非常不友好,难以记忆和阅读。
于是,人们发明了汇编语言。它用一些容易记忆的助记符来代表机器指令,例如 mov ax, 1 就对应那条二进制机器码。
汇编语言与机器指令通常是一一对应的(除了某些宏汇编扩展)。一个汇编源文件中的每一条指令,都会被汇编器翻译成对应的机器码。
因此,汇编语言被称为“低级语言”,它直接操作寄存器、内存地址、I/O 端口,几乎和硬件层面对话。
1.2 从汇编源码到可执行文件的流程
一个典型的汇编源文件(.s 或 .asm)要变成可执行文件,通常需要两个主要步骤:
- 汇编(Assemble):汇编器将汇编语言翻译成机器码,生成目标文件(Object File,通常以
.o为后缀)。目标文件中除了机器码,还包含符号表、重定位信息等,但还不能直接运行。 - 链接(Link):链接器将一个或多个目标文件以及所需的库文件合并,解析符号引用,进行地址分配和重定位,最终生成可执行文件。
这个过程可以用一张 Mermaid 图表示:
对于 C 语言程序,还会有编译(Compile)步骤,将 C 代码先翻译成汇编代码,再经过汇编和链接。但汇编语言直接跳过编译阶段,从汇编开始。
1.3 目标文件与可执行文件的区别
- 目标文件:包含机器码,但地址是相对的,符号引用尚未解决,不能直接执行。
- 可执行文件:所有符号都已解析,地址已经确定,操作系统加载后可以直接运行。
在 Linux 0.11 的 bootsect.s 场景中,情况又有不同:它最终需要的不是标准的可执行文件,而是一段纯二进制机器码,被 BIOS 加载到固定物理地址 0x7C00 并执行。因此,它的编译链接流程与普通程序有显著差异。
二、Linux 0.11 的构建工具链:as86 与 ld86
2.1 为什么不用 GNU as 和 ld?
现代 Linux 开发中,我们通常使用 GNU 工具链:as(GNU Assembler)和 ld(GNU Linker)。它们支持 32 位和 64 位保护模式代码,生成 ELF 格式的目标文件和可执行文件。
但 Linux 0.11 诞生于 1991 年,当时 GNU 工具链虽然已经存在,但对其支持并不完善,特别是对于 16 位实模式代码的支持。而 Linux 0.11 的引导扇区 bootsect.s 和设置程序 setup.s 必须运行在 16 位实模式下,因此需要能够生成 16 位代码的汇编器和链接器。
Linus Torvalds 选择了 as86 和 ld86,这是由 Bruce Evans 开发的 8086/80386 汇编器和链接器,它们最初是为 Minix 操作系统设计的,支持生成 16 位和 32 位代码,语法更接近 Intel 风格,并且能够输出纯二进制格式,非常适合引导扇区这种特殊需求。
2.2 as86 汇编器的特点
- 语法类似于 Intel 语法,操作数顺序为目的在前,源在后;
- 立即数使用
#前缀,如mov ax, #BOOTSEG; - 寄存器直接写名字,如
ax、bx; - 支持 16 位和 32 位代码生成(通过
.386等伪指令); - 生成的目标文件格式为 Minix 的目标文件格式(a.out 变体)。
2.3 ld86 链接器的特点
- 能够将 as86 生成的目标文件链接成 Minix 可执行文件;
- 支持
-0选项,生成适用于 8086 的 16 位代码; - 支持
-s选项,剥离符号表; - 生成的可执行文件包含一个32 字节的头部,这是 Minix 可执行文件格式的一部分。
2.4 工具链对应关系
| 步骤 | 普通程序(GNU 工具链) | Linux 0.11 引导扇区 |
|---|---|---|
| 汇编器 | as,生成 ELF .o |
as86,生成 Minix .o |
| 链接器 | ld,生成 ELF 可执行文件 |
ld86,生成 Minix 可执行文件(含 32 字节头) |
| 最终产物 | 可执行文件,由操作系统加载 | 纯二进制镜像(去掉头部),由 BIOS 加载 |
三、bootsect.s 的特殊性:引导扇区的要求
3.1 引导扇区的硬件规则
当计算机上电或复位时,BIOS 会进行硬件自检(POST),然后按照启动顺序查找可引导设备(软盘、硬盘、光盘等)。对于软盘和硬盘,引导扇区是设备的第一个扇区(512 字节)。
BIOS 检查引导扇区的条件:
- 扇区大小为 512 字节;
- 最后两个字节必须是
0x55和0xAA(即字0xAA55,小端序)。 - 如果满足,BIOS 就会把这 512 字节加载到物理内存
0x7C00处,然后跳转到0x7C00执行。
因此,bootsect.s 编译链接后必须生成一个恰好 512 字节的纯二进制文件,并且最后两个字节是 0xAA55。
3.2 为什么必须是纯二进制?
引导扇区是由 BIOS 直接加载执行的,此时还没有操作系统,也没有文件系统。BIOS 只做一件事:把扇区内容原封不动地复制到内存,然后跳转。它不理解 ELF、PE 或任何可执行文件格式。
所以,bootsect.s 的最终产物必须是裸机代码——没有任何文件头、元数据,纯粹是 CPU 要执行的机器指令和数据。
这与普通程序完全不同。普通程序由操作系统加载,操作系统会解析可执行文件头,设置内存映射、栈、入口点等。而引导扇区没有这些,它只有 512 字节,一切都要自己搞定。
四、Linux 0.11 中 bootsect.s 的编译与链接
4.1 源码文件与相关定义
Linux 0.11 的 boot/bootsect.s 文件使用 as86 语法编写。我们先看它的开头和结尾部分,了解整体结构。
开头部分:
BOOTSEG = 0x07c0
INITSEG = 0x9000
SETUPSEG = 0x9020
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
entry start
start:
mov ax, #BOOTSEG
mov ds, ax
mov ax, #INITSEG
mov es, ax
mov cx, #256
sub si, si
sub di, di
cld
rep movw
jmpi go, INITSEG
结尾部分(包含 root_dev 和 boot_flag):
.org 506
root_dev:
.word ROOT_DEV
boot_flag:
.word 0xAA55
这里 ROOT_DEV 是一个宏,通常在 Makefile 中定义,例如 -DROOT_DEV=0x301。
4.2 编译与链接的具体命令
在 Linux 0.11 的 boot/Makefile 中,与 bootsect 相关的构建规则大致如下:
bootsect: bootsect.o
ld86 -0 -s -o bootsect bootsect.o
dd bs=1 if=bootsect of=Image skip=32
我们逐步解析这两条命令。
4.2.1 汇编:as86
首先,bootsect.s 需要被汇编成目标文件 bootsect.o。这一步通常由 Makefile 中的隐式规则完成,但大致命令是:
as86 -0 -a -o bootsect.o bootsect.s
参数含义:
-0:生成 8086 指令集(16 位);-a:产生汇编列表文件(可选);-o bootsect.o:指定输出目标文件。
汇编后,bootsect.o 包含了机器码、符号表等信息,但其地址是相对的,且带有 Minix 目标文件格式的头部信息。
4.2.2 链接:ld86
接下来,链接器 ld86 将目标文件链接成可执行文件:
ld86 -0 -s -o bootsect bootsect.o
参数含义:
-0:生成 8086 代码,与汇编器保持一致;-s:剥离符号表,减小文件体积;-o bootsect:输出可执行文件,名为bootsect。
链接后生成的 bootsect 文件是 Minix 可执行文件格式。它前面有一个 32 字节的头,包含入口点、段信息等。这个头对于引导扇区来说是多余的,必须去掉。
4.2.3 去除头部:dd
链接后得到的 bootsect 文件,前 32 字节是 Minix 可执行文件头。真正的机器码从偏移 32 开始。因此,我们需要跳过这 32 字节,提取纯二进制部分:
dd bs=1 if=bootsect of=Image skip=32
bs=1:每次复制 1 字节(为了精确跳过 32 字节);if=bootsect:输入文件是bootsect;of=Image:输出文件是Image(纯二进制);skip=32:跳过前 32 字节。
经过这一步,得到的 Image 文件就是纯二进制引导扇区代码。
4.3 验证大小与引导标志
引导扇区必须恰好 512 字节,且最后两个字节为 0xAA55。
bootsect.s 使用了 .org 506 伪指令,将 root_dev 定位到偏移 506 处,然后 root_dev 占 2 字节(506-507),boot_flag 从偏移 508 开始,占 2 字节(508-509)。这样最后两个字节就是 510-511,正好是 boot_flag 的内容 0xAA55。
但需要注意,如果代码部分超过 506 字节,.org 506 会导致汇编器报错。因此,Linux 0.11 的 bootsect.s 经过精简,确保代码和数据总计不超过 506 字节。
链接并去除头部后,我们可以检查 Image 文件大小:
ls -l Image
应该是 512 字节。如果不是,说明构建过程可能有问题。
五、将 bootsect 写入软盘第一扇区
5.1 写入物理软盘
如果有一张实际的软盘,并且系统有软驱,可以使用 dd 命令直接写入:
dd if=Image of=/dev/fd0 bs=512 count=1
if=Image:输入文件是纯二进制引导扇区;of=/dev/fd0:输出到第一个软盘驱动器;bs=512:块大小为 512 字节;count=1:只复制一个块,即第一扇区。
注意:执行此操作需要 root 权限,因为直接访问设备文件。
5.2 写入软盘镜像文件
现代开发通常使用软盘镜像文件(如 boot.img)来模拟软盘。构建流程可能是:
- 创建一个空的 1.44MB 镜像文件;
- 将
bootsect写入镜像的第一个扇区; - 将
setup和内核写入后续扇区。
在 Linux 0.11 的原始构建系统中,Makefile 会生成一个完整的启动镜像,通常名为 Image 或 boot.img。这个镜像包含引导扇区、设置程序、内核等,可以直接用模拟器或写入设备启动。
例如,在 boot/Makefile 中可能会有这样的规则:
Image: bootsect setup system
dd bs=512 if=bootsect of=Image count=1
dd bs=512 if=setup of=Image skip=1 seek=1 count=4
dd bs=512 if=system of=Image skip=1 seek=5 count=...
这些 dd 命令将各个部分依次写入 Image 文件的不同偏移处,形成一个完整的磁盘镜像。
对于软盘第一扇区的写入,最简单的情况就是:
dd if=bootsect of=Image bs=512 count=1 conv=notrunc
conv=notrunc 确保不会截断 Image 文件的其余部分。
5.3 启动过程回顾
当 BIOS 从软盘启动时:
- BIOS 读取软盘第一个扇区(512 字节)到内存
0x7C00; - 检查最后两个字节是否为
0xAA55; - 如果是,跳转到
0x7C00执行; bootsect代码开始运行,它会将自己从0x7C00复制到0x9000,然后继续加载setup和内核,最终进入保护模式。
六、深入细节:为什么链接后要跳过 32 字节?
6.1 Minix 可执行文件头
ld86 生成的可执行文件是 Minix 的 a.out 变体。它的头部结构如下(简化):
| 偏移 | 大小 | 含义 |
|---|---|---|
| 0 | 4 字节 | 魔数(0x0301 或其他) |
| 4 | 4 字节 | 代码段大小 |
| 8 | 4 字节 | 数据段大小 |
| 12 | 4 字节 | BSS 段大小 |
| 16 | 4 字节 | 入口点 |
| 20 | 4 字节 | 符号表大小 |
| 24 | 4 字节 | 未使用/其他 |
| 28 | 4 字节 | 未使用/其他 |
总共 32 字节。这 32 字节对于引导扇区来说没有任何用处,因为 BIOS 不会解析它,只会把它当作机器码的一部分执行(会出问题)。所以必须去掉。
dd skip=32 就是跳过这 32 字节,从纯机器码开始提取。
6.2 为什么不用 ld86 -o 直接生成纯二进制?
ld86 本身并不支持直接生成纯二进制文件,它总是输出带有 Minix 头的可执行文件。所以需要借助 dd 来剥离头部。这也是早期系统构建中常见的手工做法。
七、完整构建流程示例
让我们假设你已经下载了 Linux 0.11 源码,并在现代 Linux 系统上配置好了交叉编译环境(可能需要安装 bin86 包,其中包含 as86 和 ld86)。
7.1 安装工具
在 Debian/Ubuntu 上:
sudo apt-get install bin86
7.2 编译 bootsect.s
进入 linux-0.11/boot 目录:
cd linux-0.11/boot
查看 Makefile 中与 bootsect 相关的部分,然后执行:
make bootsect
实际执行的命令可能是:
as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
dd bs=1 if=bootsect of=bootsect.bin skip=32
或者直接输出为 Image 的一部分。
7.3 检查生成的文件
ls -l bootsect bootsect.bin
bootsect 可能是 544 字节(512 + 32 头),bootsect.bin 是 512 字节。
7.4 写入软盘镜像
创建软盘镜像:
dd if=/dev/zero of=floppy.img bs=512 count=2880 # 1.44MB
写入引导扇区:
dd if=bootsect.bin of=floppy.img bs=512 count=1 conv=notrunc
之后可以继续写入 setup 和 system。
八、总结与思考
从汇编源码到软盘第一扇区,整个过程展现了早期操作系统开发的纯粹与硬核。
- 汇编器将人类可读的助记符转换为机器码目标文件;
- 链接器将目标文件组合并解析符号,生成可执行文件;
dd工具则负责剥离文件头、写入原始设备,完成从“程序”到“引导扇区”的转变。
Linux 0.11 的 bootsect.s 之所以采用这样的流程,是因为它必须生成 512 字节的纯二进制引导扇区,而当时的工具链无法直接输出这种格式,只能通过手动剥离头部来实现。
这不仅是技术问题,更是一种历史缩影:在那个资源有限、工具简陋的时代,开发者们用最直接的方式与硬件对话,亲手搭建起操作系统的第一级台阶。
希望这篇文章能让你对汇编语言的本质、编译链接流程,以及 Linux 0.11 引导扇区的构建有一个透彻的理解。当你下次看到 bootsect.s 中的那些指令时,你会明白它们是如何一步步变成软盘上的 512 字节,并在开机时被 BIOS 唤醒的。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)