汇编文件的本质与 Linux 0.11 引导扇区的编译、链接、写入全流程

汇编文件的本质与 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)要变成可执行文件,通常需要两个主要步骤:

  1. 汇编(Assemble):汇编器将汇编语言翻译成机器码,生成目标文件(Object File,通常以.o为后缀)。目标文件中除了机器码,还包含符号表、重定位信息等,但还不能直接运行。
  2. 链接(Link):链接器将一个或多个目标文件以及所需的库文件合并,解析符号引用,进行地址分配和重定位,最终生成可执行文件

这个过程可以用一张 Mermaid 图表示:

汇编器 as

链接器 ld

链接器 ld

链接器 ld

汇编源文件 .s

目标文件 .o

可执行文件

其他目标文件 .o

库文件 .a

对于 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 选择了as86ld86,这是由 Bruce Evans 开发的 8086/80386 汇编器和链接器,它们最初是为 Minix 操作系统设计的,支持生成 16 位和 32 位代码,语法更接近 Intel 风格,并且能够输出纯二进制格式,非常适合引导扇区这种特殊需求。

2.2 as86 汇编器的特点

  • 语法类似于 Intel 语法,操作数顺序为目的在前,源在后
  • 立即数使用#前缀,如mov ax, #BOOTSEG
  • 寄存器直接写名字,如axbx
  • 支持 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.oas86,生成 Minix.o
链接器ld,生成 ELF 可执行文件ld86,生成 Minix 可执行文件(含 32 字节头)
最终产物可执行文件,由操作系统加载纯二进制镜像(去掉头部),由 BIOS 加载

三、bootsect.s 的特殊性:引导扇区的要求

3.1 引导扇区的硬件规则

当计算机上电或复位时,BIOS 会进行硬件自检(POST),然后按照启动顺序查找可引导设备(软盘、硬盘、光盘等)。对于软盘和硬盘,引导扇区是设备的第一个扇区(512 字节)。

BIOS 检查引导扇区的条件:

  1. 扇区大小为 512 字节;
  2. 最后两个字节必须是0x550xAA(即字0xAA55,小端序)。
  3. 如果满足,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-obootsect.o bootsect.s

参数含义:

  • -0:生成 8086 指令集(16 位);
  • -a:产生汇编列表文件(可选);
  • -o bootsect.o:指定输出目标文件。

汇编后,bootsect.o包含了机器码、符号表等信息,但其地址是相对的,且带有 Minix 目标文件格式的头部信息。

4.2.2 链接:ld86

接下来,链接器ld86将目标文件链接成可执行文件:

ld86-0-s-obootsect bootsect.o

参数含义:

  • -0:生成 8086 代码,与汇编器保持一致;
  • -s:剥离符号表,减小文件体积;
  • -o bootsect:输出可执行文件,名为bootsect

链接后生成的bootsect文件是Minix 可执行文件格式。它前面有一个 32 字节的头,包含入口点、段信息等。这个头对于引导扇区来说是多余的,必须去掉。

4.2.3 去除头部:dd

链接后得到的bootsect文件,前 32 字节是 Minix 可执行文件头。真正的机器码从偏移 32 开始。因此,我们需要跳过这 32 字节,提取纯二进制部分:

ddbs=1if=bootsectof=Imageskip=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-lImage

应该是 512 字节。如果不是,说明构建过程可能有问题。


五、将 bootsect 写入软盘第一扇区

5.1 写入物理软盘

如果有一张实际的软盘,并且系统有软驱,可以使用dd命令直接写入:

ddif=Imageof=/dev/fd0bs=512count=1
  • if=Image:输入文件是纯二进制引导扇区;
  • of=/dev/fd0:输出到第一个软盘驱动器;
  • bs=512:块大小为 512 字节;
  • count=1:只复制一个块,即第一扇区。

注意:执行此操作需要 root 权限,因为直接访问设备文件。

5.2 写入软盘镜像文件

现代开发通常使用软盘镜像文件(如boot.img)来模拟软盘。构建流程可能是:

  1. 创建一个空的 1.44MB 镜像文件;
  2. bootsect写入镜像的第一个扇区;
  3. setup和内核写入后续扇区。

在 Linux 0.11 的原始构建系统中,Makefile会生成一个完整的启动镜像,通常名为Imageboot.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文件的不同偏移处,形成一个完整的磁盘镜像。

对于软盘第一扇区的写入,最简单的情况就是:

ddif=bootsectof=Imagebs=512count=1conv=notrunc

conv=notrunc确保不会截断Image文件的其余部分。

5.3 启动过程回顾

当 BIOS 从软盘启动时:

  1. BIOS 读取软盘第一个扇区(512 字节)到内存0x7C00
  2. 检查最后两个字节是否为0xAA55
  3. 如果是,跳转到0x7C00执行;
  4. bootsect代码开始运行,它会将自己从0x7C00复制到0x9000,然后继续加载setup和内核,最终进入保护模式。

六、深入细节:为什么链接后要跳过 32 字节?

6.1 Minix 可执行文件头

ld86生成的可执行文件是 Minix 的 a.out 变体。它的头部结构如下(简化):

偏移大小含义
04 字节魔数(0x0301 或其他)
44 字节代码段大小
84 字节数据段大小
124 字节BSS 段大小
164 字节入口点
204 字节符号表大小
244 字节未使用/其他
284 字节未使用/其他

总共 32 字节。这 32 字节对于引导扇区来说没有任何用处,因为 BIOS 不会解析它,只会把它当作机器码的一部分执行(会出问题)。所以必须去掉。

dd skip=32就是跳过这 32 字节,从纯机器码开始提取。

6.2 为什么不用ld86 -o直接生成纯二进制?

ld86本身并不支持直接生成纯二进制文件,它总是输出带有 Minix 头的可执行文件。所以需要借助dd来剥离头部。这也是早期系统构建中常见的手工做法。


七、完整构建流程示例

让我们假设你已经下载了 Linux 0.11 源码,并在现代 Linux 系统上配置好了交叉编译环境(可能需要安装bin86包,其中包含as86ld86)。

7.1 安装工具

在 Debian/Ubuntu 上:

sudoapt-getinstallbin86

7.2 编译 bootsect.s

进入linux-0.11/boot目录:

cdlinux-0.11/boot

查看Makefile中与bootsect相关的部分,然后执行:

makebootsect

实际执行的命令可能是:

as86-0-a-obootsect.o bootsect.s ld86-0-s-obootsect bootsect.oddbs=1if=bootsectof=bootsect.binskip=32

或者直接输出为Image的一部分。

7.3 检查生成的文件

ls-lbootsect bootsect.bin

bootsect可能是 544 字节(512 + 32 头),bootsect.bin是 512 字节。

7.4 写入软盘镜像

创建软盘镜像:

ddif=/dev/zeroof=floppy.imgbs=512count=2880# 1.44MB

写入引导扇区:

ddif=bootsect.binof=floppy.imgbs=512count=1conv=notrunc

之后可以继续写入setupsystem


八、总结与思考

从汇编源码到软盘第一扇区,整个过程展现了早期操作系统开发的纯粹与硬核。

  • 汇编器将人类可读的助记符转换为机器码目标文件;
  • 链接器将目标文件组合并解析符号,生成可执行文件;
  • dd工具则负责剥离文件头、写入原始设备,完成从“程序”到“引导扇区”的转变。

Linux 0.11 的bootsect.s之所以采用这样的流程,是因为它必须生成512 字节的纯二进制引导扇区,而当时的工具链无法直接输出这种格式,只能通过手动剥离头部来实现。

这不仅是技术问题,更是一种历史缩影:在那个资源有限、工具简陋的时代,开发者们用最直接的方式与硬件对话,亲手搭建起操作系统的第一级台阶。

希望这篇文章能让你对汇编语言的本质、编译链接流程,以及 Linux 0.11 引导扇区的构建有一个透彻的理解。当你下次看到bootsect.s中的那些指令时,你会明白它们是如何一步步变成软盘上的 512 字节,并在开机时被 BIOS 唤醒的。