「DAY 01」- 从计算机结构到汇编程序入门

第一步、编写汇编程序(day-01.nas)

; hello-os
; TAB=4

; 以下这段是标准 FAT12 格式软盘专用的代码

		DB		0xeb, 0x4e, 0x90
		DB		"HELLOIPL"		; 启动区名称可以是任意字符串
		DW		512				; 每个扇区的大小,必须是 512 字节
		DB		1				; 簇(Cluster)的大小,必须为 1 个扇区
		DW		1				; FAT 的起始位置(一般从第一个扇区开始)
		DB		2				; FAT 的个数(必须为 2)
		DW		224				; 根目录的大小(一般设成 244 项)
		DW		2880			; 该磁盘的大小(必须是 2880 扇区)
		DB		0xf0			; 磁盘的种类(必须是 0xf0)
		DW		9				; FAT的长度(必须是 9 扇区)
		DW		18				; 1 个磁道(Track)有几个扇区(必须是 18)
		DW		2				; 磁头数(必须是 2)
		DD		0				; 不使用分区(必须是 0)
		DD		2880			; 重写一次磁盘大小
		DB		0,0,0x29		; 意义不明,固定
		DD		0xffffffff		; 可能是卷标号码
		DB		"HELLO-OS   "	; 磁盘的名称(11 字节)
		DB		"FAT12   "		; 磁盘格式名称(8 字节)
		RESB	18				; 先空出 18 字节

; 程序主体

		DB		0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
		DB		0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
		DB		0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
		DB		0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
		DB		0xee, 0xf4, 0xeb, 0xfd

; 信息显示部分

		DB		0x0a, 0x0a		; 2 个换行
		DB		"hello, world"
		DB		0x0a			; 1 个换行
		DB		0

		RESB	0x1fe - ($ - $$)			; 使用 0x00 填充,直到 0x01fe 结束

		DB		0x55, 0xaa

; 以下是启动区以外的部分的输出

		DB		0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
		RESB	4600
		DB		0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
		RESB	1469432

第二步、编译并运行程序

nasm day-01.nas

qemu-system-i386 day-01

一、先动手操作

第一步、选择二进制编辑器

我们使用 Emacs,但是不太行。我们最后使用 hexedit 编辑器。

第二步、编写系统镜像文件

以下是二进制文件的简写,其中 *** 代表该部分全为 00 字节):

00000000   EB 4E 90 48  45 4C 4C 4F  49 50 4C 00  02 01 01 00  .N.HELLOIPL.....
00000010   02 E0 00 40  0B F0 09 00  12 00 02 00  00 00 00 00  ...@............
00000020   40 0B 00 00  00 00 29 FF  FF FF FF 48  45 4C 4C 4F  @.....)....HELLO
00000030   2D 4F 53 20  20 20 46 41  54 31 32 20  20 20 00 00  -OS   FAT12   ..
***
00000050   B8 00 00 8E  D0 BC 00 7C  8E D8 8E C0  BE 74 7C 8A  .......|.....t|.
00000060   04 83 C6 01  3C 00 74 09  B4 0E BB 0F  00 CD 10 EB  ....<.t.........
00000070   EE F4 EB FD  0A 0A 68 65  6C 6C 6F 2C  20 77 6F 72  ......hello, wor
00000080   6C 64 0A 00  00 00 00 00  00 00 00 00  00 00 00 00  ld..............
***
000001F0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 55 AA  ..............U.
00000200   F0 FF FF 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
***
00001400   F0 FF FF 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
***
00168000

检查错误:(1)使用 xxd 以十六禁止打印,(2)然后使用 diff 与作者提供的文件进行对比。

第三步、镜像文件写入软盘

1)保存镜像文件:将镜像文件保存退出,上面的镜像文件是可引导的,正好是 1474560 字节。

2)写入到软盘中:软盘倒是有,怎么写入、如何运行才是问题。我们可以使用 qemu-system-i386 模拟器代替。

二、究竟做了些什么

我们写的这个二进制文件什么意思呢?在这之前需要先了解一下电脑结构:

CPU,Central Processing Unit,中央处理单元,是处理中心,负责与其他电路元件进行电信号交换,并且只能理解开(ON)与关(OFF)两种电信号。我们的日常操作,对于 CPU 来说,只是在处理电信号。虽然 CPU 具有计算指令,可以进行加减乘除元算,也可以计算小数、负数、次方,以及更高级的函数等等。虽然功能强大,但是 CPU 并不理解数的概念。CPU 只是个电路板,执行电信号给它的指令,输出相应的信号。我们可以用 0 与 1 来表示任何信息,而 CPU 将这些 0 与 1 视为电信号的开与关,并进行处理。我们很多问题可以转为十进制数学计算,而十进制计算可以转为二进制计算,这正巧与计算机对应起来。

我们编写的二进制文件,交由 CPU 执行,它将二进制文件的 1 与 0 视为电信号开与关进行处理。

三、初次体验汇编程序

编写二进制文件繁琐且进度慢,现在开始使用汇编语言。

第一步、使用汇编语言编写

(编写)这里与原文结构不同,因此 RESB 指令保留的字节数也不同:

DB   0xEB, 0x4E, 0x90, 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x49, 0x50, 0x4C, 0x00, 0x02, 0x01, 0x01, 0x00
DB   0x02, 0xE0, 0x00, 0x40, 0x0B, 0xF0, 0x09, 0x00, 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00
DB   0x40, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x29, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, 0x45, 0x4C, 0x4C, 0x4F
DB   0x2D, 0x4F, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41, 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00
RESB 16
DB   0xB8, 0x00, 0x00, 0x8E, 0xD0, 0xBC, 0x00, 0x7C, 0x8E, 0xD8, 0x8E, 0xC0, 0xBE, 0x74, 0x7C, 0x8A
DB   0x04, 0x83, 0xC6, 0x01, 0x3C, 0x00, 0x74, 0x09, 0xB4, 0x0E, 0xBB, 0x0F, 0x00, 0xCD, 0x10, 0xEB
DB   0xEE, 0xF4, 0xEB, 0xFD, 0x0A, 0x0A, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x77, 0x6F, 0x72
DB   0x6C, 0x64, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 352
DB   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xAA
DB   0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4592
DB   0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1469424

第二步、汇编代码编译镜像

# nasm -o boot.img boot.asm

汇编指令含义说明

DB – DEFINE BYTE,定义字节,前缀 0x 表示十六禁止(否则被汇编器视为十进制数);

RESB – RESERVE BYTE,保留字节,将指定长度的字节使用 0x00 表示;

四、加工润色

使用汇编重新编写(更具可读性)

作者使用的汇编器 nask 是自己开发的,以下代码使用其他汇编器可能无法通过:

; hello-os
; TAB=4

; 以下是标准 FAT12 格式软盘专用的代码
DB	0xeb, 0x4e, 0x90
DB	"HELLOIPL" ; 启动区的名称可以是任意字符串,必须为八字节
DW	512 ; 每个扇区的大小,必须是 512 字节
DB	1 ; 簇的大小,必须为 1 个扇区
DW	1 ; FAT 的起始位置,一般从第一个扇区开始
DB	2 ; FAT 的个数,必须为 2
DW	224 ; 根目录的大小,一般设置成 224 项
DW	2880 ; 该磁盘的大小,必须是 2880 扇区
DB	0xf0 ; 磁盘种类,必须是 0xf0
DW	9 ; FAT 的长度,必须是 9 扇区
DW	19 ; 一个磁道有几个扇区,必须是 18
DW	2 ; 磁头数,必须是 2
DD	0 ; 不使用分区,必须是 0
DD	2880 ; 重写一次磁盘大小
DB	0, 0, 0x29 ; 意义不明,固定写法
DD	0xffffffff ; (可能是)卷标号码
DB	"HELLO-OS   " ; 磁盘名称,11 字节
DB	"FAT12   " ; 磁盘格式名称,8字节
RESB	18 ; 先空出 18 字节

; 程序主体
DB   0xB8, 0x00, 0x00, 0x8E, 0xD0, 0xBC, 0x00, 0x7C, 0x8E, 0xD8, 0x8E, 0xC0, 0xBE, 0x74, 0x7C, 0x8A
DB   0x04, 0x83, 0xC6, 0x01, 0x3C, 0x00, 0x74, 0x09, 0xB4, 0x0E, 0xBB, 0x0F, 0x00, 0xCD, 0x10, 0xEB
DB   0xEE, 0xF4, 0xEB, 0xFD

; 信息显示部分
DB	0x0a, 0x0a ; 2 个换行
DB	"hello, world"
DB	0x0a ; 换行
DB	0

RESB	0x1fe-$ ; 填写 0x00,直到 0x001fe
DB	0x55, 0xaa

; 以下是启动区以外部分的输出
DB   0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4592
DB   0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1469424

汇编指令含义说明

; – 注释

DB – DEFINE BYTE,定义字节,前缀 0x 表示十六禁止(否则被汇编器视为十进制数);还可以直接定义字符串,汇编器将自动转化为十六进制编码

DW – DEFINE WORD,定义字(指十六位)

DD – DEFINE DOUBLE WORD,定义双字(指三十二位)

RESB – RESERVE BYTE,保留字节,将指定长度的字节使用 0x00 表示;0x1fe-$ 表示从当前位置延续到 0x1fe 位置,用于动态定义 0x00 字节,防止由于前面动态变化而需要调节 RESB 指令参数的场景。

专用术语解释说明

TAB=4 – 指定缩进,方便源码阅读

FAT12 – 文件系统格式,参考 Wikipedia/File Allocation Table 百科。用 Windows 或 MS-DOS 格式化出来的软盘就是这种格式。我们的操作系统就使用这种格式,兼容性好,剩余磁盘空间用于保存文件。

Boot Sector – 启动区,软盘的首个扇区被称为启动区。什么是扇区?当计算机读写软盘时以 512 字节为单位进行读取,而不是单个字节读取,因此 512 字节被成为一个扇区。软盘 1474560 字节,共计 2880 扇区。什么是启动扇区?我们在磁盘中保存程序,为了执行程序,需要确定程序起始部分在磁盘中的位置。这就是启动扇区的作用,我们约定将程序的开始写入软盘的首个扇区,并以 0x55AA 结尾。软盘的首个扇区最先被读取,当该扇区以 0x55AA 结尾时,则视为该扇区启动程序的开始,并开始执行这个程序。

IPL – Initial Program Loader,启动程序加载器。由于启动区只有 512 字节,但是操作系统通常大于启动区。因此操作系统并没有保存在启动区,而是通过启动区的程序加载操作系统。在启动区中的程序被称为启动程序加载器,就是今天的 Bootloader 程序。它的名字只能是八字节。

Boot – 启动,Bootstrap,至于启动为什么使用 bootstrap 可以参考 Wikipedia/Bootstrapping/Etymology 解释。简单的说,引导程序加载器器是操作系统的组成部分,这有点像“让操作系统启动自己”的意思(我们也没有深入研究这个词源)。

参考文献

How do I compare binary files in Linux?
Any x86-64 Linux assembler?