2025-03-30  2025-04-07    15999 字  32 分钟
AOS
  • 文件: boot.asm
  • 完整项目代码在末尾

从上篇可知, 留给我们的代码部分只有420字节. 而boot代码需要完成的任务也很简单, 只需要从磁盘加载loader到内存中, 并跳转到loader执行

加载inc文件和jmp

在之前我们已经完成了两个文件的设计def.incfat32.inc, 分别是内存分布的定义和FAT32分区的信息

我们先编写boot开头的部分, 也就是BIOS完成自身的任务后, 跳转到的位置0x7c00

 1%include "def.inc"
 2
 3[bits 16]
 4org 0x7c00
 5
 6  jmp short _start_boot
 7  nop
 8
 9%include "fat32.inc"
10
11
12_start_boot:

我们首先引入了def.inc, 这个文件里面都是define的常量

然后我们通过 bits 16 告诉编译器生成16位环境的机器码, 这是因为CPU在这时还处在16位模式下

通过org 0x7c00告诉编译器这段代码预期的加载地址, 因为BIOS会将引导扇区(512字节)加载到内存地址0x7c00处

然后就是两行经典的代码, 跳转到实际代码的位置执行, 后跟一个nop空指令. 实际jmp short也可以用jmp, 没有什么区别.

通过%include "fat32.inc"将FAT32相关的内容, 注意这个文件里并非都是define的常量, 反而大部分内容都会被编译成二进制数据生成在boot文件中, 具体哪些输入可以去看 AOS开发 02

最后通过_start_boot:定义了一个标签, 用于标识这个位置处于代码或数据的那个位置, 在其他地方通过引用_start_boot就可以使用这个标签所代表的地址位置. 就像上面的jmp指令, 表示跳转到_start_boot位置

启动扇区结束

还记得启动扇区最后的55 aa吗, 因为是小端序, 所以最后两个字节就是0xaa55, 汇编指令dw表示写入两个字符

我们还记得启动扇区是512字节, 可是如果只有前面那些和最后的0xaa55, 远远不够512字节, 那么我们会想到用0填充中间, 使得编译出的文件大小为512字节.

510就是512去掉最后两个字节的0xaa55, $$$是常用的位置引用符号

  • $表示当前指令的地址(例如jmp $就是死循环, 重复回到指令的开头位置继续执行).
  • $$表示当前段的起始地址, 在这个代码中我们并未手动指定段, 那么就属于编译器默认创建的段.

那么$-$$就得到了从开始到这条指令一共有多少个字节, 再510-($-$$)就得到了需要填充多少个字节

times指令表示执行多少次, 后面跟着的就是执行次数, 然后就是执行的指令db 0(表示写入一个字节, 数值是0)

1  times 510-($-$$) db 0
2  dw 0xAA55

寄存器介绍

我们要知道在x86 16位下的寄存器有下面这几种

通用寄存器

寄存器 名称 主要用途
AX 累加器(Accumulator) 算术运算,输入/输出操作,函数返回值
BX 基址寄存器(Base) 内存间接寻址的基址指针
CX 计数寄存器(Count) 循环计数,字符串操作中的计数器
DX 数据寄存器(Data) 算术运算,I/O操作的端口地址

每个16位通用寄存器可以分解为两个8位寄存器:

  • AX: AH (高8位) 和 AL (低8位)
  • BX: BH (高8位) 和 BL (低8位)
  • CX: CH (高8位) 和 CL (低8位)
  • DX: DH (高8位) 和 DL (低8位)

索引寄存器和指针寄存器

寄存器 名称 主要用途
SI 源索引(Source Index) 字符串操作的源地址指针
DI 目标索引(Destination Index) 字符串操作的目标地址指针
BP 基址指针(Base Pointer) 指向堆栈中的数据,通常指向函数参数
SP 堆栈指针(Stack Pointer) 指向栈顶位置

段寄存器

16位模式使用基于段的内存寻址模式,以下寄存器用于存储段地址

寄存器 名称 主要用途
CS 代码段(Code Segment) 当前执行代码所在的内存段
DS 数据段(Data Segment) 默认的数据访问段
ES 附加段(Extra Segment) 额外的数据段,通常用于字符串操作
SS 堆栈段(Stack Segment) 堆栈所在的内存段
FS F段(F Segment) 附加数据段
GS G段(G Segment) 附加数据段

注意:在80286之后添加了FS和GS两个额外段寄存器,在纯16位编程中较少使用, 但我用

指令指针寄存器

寄存器 名称 主要用途
IP 指令指针(Instruction Pointer) 指向下一条要执行的指令地址(段内偏移

标志寄存器

16位的标志寄存器(FLAGS)包含多个单独的位,反映处理器的状态:

位索引 位名称 缩写 功能描述 可编程
0 进位标志 CF (Carry Flag) • 无符号算术运算产生进位时置1
• 最高位有进出时置1
• 影响加、减、比较、位移操作
1 保留位 1 始终为1(在8086/8088中)
2 奇偶标志 PF (Parity Flag) • 结果中1的位数为偶数时置1
• 用于错误检测协议
3 保留位 0 始终为0
4 辅助进位标志 AF (Auxiliary Carry) • 低4位(半字节)算术操作有进位时置1
• 主要用于BCD(二进制编码十进制)运算
5 保留位 0 始终为0
6 零标志 ZF (Zero Flag) • 操作结果为0时置1
• 常用于条件跳转指令
7 符号标志 SF (Sign Flag) • 操作结果为负数(最高位为1)时置1
• 表示结果的符号
8 陷阱标志 TF (Trap Flag) • 启用单步模式(调试)
• 每条指令执行后产生中断
9 中断启用标志 IF (Interrupt Enable) • 允许(1)或禁止(0)可屏蔽硬件中断
• 由CLI和STI指令控制
10 方向标志 DF (Direction Flag) • 控制字符串操作的方向
• 0: 地址递增(CLD指令)
• 1: 地址递减(STD指令)
11 溢出标志 OF (Overflow Flag) • 有符号数运算结果超出范围时置1
• 表示算术结果是否溢出
12-13 I/O特权级 IOPL (I/O Privilege Level) • 两位字段,控制I/O指令特权
• 保护模式下使用
14 嵌套任务 NT (Nested Task) • 控制中断返回行为
• 指示当前任务是否被嵌套
15 保留位 0 始终为0(在16位FLAGS中)

扩展标志(EFLAGS中的附加位)

位索引 位名称 缩写 功能描述 可编程
16 恢复标志 RF (Resume Flag) • 控制调试异常响应
• 允许恢复调试异常
17 虚拟8086模式 VM (Virtual-8086 Mode) • 启用虚拟8086模式
• 允许在保护模式下执行实模式代码
18 对齐检查 AC (Alignment Check) • 非对齐内存访问时产生异常
• 与CR0.AM配合使用
19 虚拟中断标志 VIF (Virtual Interrupt Flag) • 虚拟IF标志
• 用于虚拟8086模式
20 虚拟中断等待 VIP (Virtual Interrupt Pending) • 表示有虚拟中断等待处理
21 能够使用CPUID ID (CPUID Available) • 表示处理器支持CPUID指令
• 可通过程序修改表示支持可编程标志位
22-31 保留位 - 保留位,通常为0

初始化寄存器

_start_boot:后面的就是我们正式的代码部分了, 首先第一件事就是初始化一下寄存器, 例如段寄存器和堆栈寄存器

DEF_AREA_STACK_SEGMENTDEF_AREA_STACK_OFFSET就是我们在def.inc中为堆栈分配的空间地址

在这段代码中你可以看到

第一步没有也可以, 因为一般cs都是0

  1. 将cs段寄存器中的值赋值给了ax寄存器, 一般这个时候cs寄存器中的值都是0
  2. 然后将ax的值赋值给es, ds段寄存器, 也就是说现在cs, es, ds寄存器中的值都一样了

第二步是必须的, 保证堆栈在我们预期的位置

  1. 将之前分配的堆栈空间的段地址存入ax
  2. 将ax中的值赋值给ss, 也就是堆栈段寄存器(注意无法直接给任何段寄存器直接赋值, 必须通过通用寄存器间接赋值)
  3. 将之前分配的堆栈空间在段地址下的偏移存入sp, 也就是堆栈指针

最后一个步也是必须的, 保证fs段寄存器为0

  1. xor就是异或, 自身异或自身, 就会变成0. (当然你也可以mov ax, 0,但是这条指令编译出来占3个字节, 而通过xor ax, ax只会占用2个字节, 省出来1个字节, 同时执行速度也会更快一些)
  2. 然后就是将ax赋值给fs, 就达到了清零fs段寄存器的目的
1; init register
2  mov ax, cs
3  mov es, ax
4  mov ds, ax
5  mov ax, DEF_AREA_STACK_SEGMENT
6  mov ss, ax
7  mov sp, DEF_AREA_STACK_OFFSET
8  xor ax, ax
9  mov fs, ax

配置bochs

到这一步, 我们实际已经为sssp寄存器赋值了自己想要的数值, 那么怎么来验证呢?

那么就要引入我们用到的其中一个工具bochs了, 也就是第一篇配置环境时安装的bochs-x

安装完成后, 我们还需要下载这几个文件https://dav.akvicor.com/asset/bochs/

  • 配置文件: bochsrc
  • BIOS文件: BIOS-bochs-latest (对应配置文件中的romimage: file="./asset/BIOS-bochs-latest")
  • VGA文件: VGABIOS-lgpl-latest (对应配置文件中的vgaromimage: file="./asset/VGABIOS-lgpl-latest")

在配置文件中ata0-master: type=disk, path="/dev/sda", mode=flat表示让bochs从磁盘/dev/sda启动, 也可以换成disk.img, 这样就是从镜像启动

除了上述这三个文件路路径需要根据自己的路径修改, 其他部分不需要修改

具体如何调用, 我们在后面的Makefile中详细说明

配置qemu

相对于bochs来说, qemu就没有那么多的配置了, 只需要安装qemu-system

具体如何调用, 我们在后面的Makefile中详细说明

整理文件结构

现在各种文件已经开始多起来了, 我们就初步设计下整个项目的目录结构吧

下面是笔者目前的目录结构, LICENSEREADME.md只是项目的开源许可和描述文件, 无需在意

 1$ tree .  
 2.
 3├── LICENSE
 4├── README.md
 5├── asset
 6│   ├── BIOS-bochs-latest
 7│   ├── VGABIOS-lgpl-latest
 8│   └── bochsrc
 9├── bootloader
10│   ├── boot.asm
11│   ├── def.inc
12│   └── fat32.inc
13└── tools
14    └── gen_fat32_inc
15        ├── gen_fat32.sh
16        └── gen_fat32_inc.c

编写Makefile

为了方便编译和调试, 我们就需要编写Makefile文件来帮助我们

注意Makefile必须使用Tab, 也就是\t来缩进, 不能使用空格代替

./bootloader/Makefile

首先是bootloader下的的Makefile, 这是专门用来编译boot和loader的

BIN表示生成的文件是什么, 因为目前只有boot.asm, 所以BIN = boot.bin

可以看到最后面部分, $(BIN) : %.bin : %.asm, 表示每个BIN文件都是.bin结尾, 要生成.bin结尾的文件需要依赖同名的.asm结尾的文件, 也就是说要生成boot.bin文件, 就需要用到boot.asm文件

  • $<表示依赖文件, 对于boot.bin来说, $<就是boot.asm
  • $@表示目标文件, 对于boot.bin来说, $@就是boot.bin
  • $(FLAGS)是传递过来的参数
 1BUILD = .
 2BIN = boot.bin
 3
 4CD = cd
 5
 6NASM = /usr/bin/nasm
 7MAKE = /usr/bin/make
 8
 9default :
10
11build : $(BUILD)
12	$(MAKE) $(BIN)
13
14$(BIN) : %.bin : %.asm
15	$(NASM) $< -o $(BUILD)/$@ $(FLAGS)

./Makefile

这是根目录下的Makefile文件, 负责调用bootloader下的Makefile, 以及未来kernel下的Makefile

和上面的Makefile相比, 根目录下的Makefile就要大的多, 因为还涉及到了bochs和qemu两个模拟器的调用

变量部分

首先映入眼帘就是一串嵌套_PATH := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))

这是用来计算当前Makefile所在的目录的绝对路径的MAKEFILE_LIST是当前解析的所有Makefile的文件列表, 通过lastword取最后一个, 通过abspath转换为绝对目录, 通过dir提取路径中目录部分

后面紧跟着的就是一堆命令的define, 方便后面调用. 同时_FLAGS后缀的配置了某些命令执行时用到的一些参数

  • TERM_FLAGS: 通过TERM启动一个新终端, 然后调用gdb命令并连接到qemu的调试的端口
  • BOCHS_FLAGS: 这是bochs的启动参数, 启用了debug功能, 同时指定了配置文件路径
  • QEMU_FLAGS: 这是qemu的启动参数. -m 128M指定了内存为128M. -smp sockets=1,cores=1,threads=1配置了CPU为1个物理插槽,每个cpu有1个物理核心,每个物理核心有1个线程. -S启动时冻结cpu执行. -s启用了GDB调试,端口默认1234. -monitor stdio将qemu监视器重定向到标准输入输出.

版本号

因为使用git管理代码, 所以可以从git中获取一些信息用于生成版本号

1BRANCH=$(shell $(GIT) rev-parse --abbrev-ref HEAD)
2VERSION=$(shell $(GIT) describe --tags --always | $(SED) 's/^v//')
3COMMIT=$(shell $(GIT) rev-parse --verify HEAD)
4BUILD_TIME=$(shell $(DATE) +"%Y-%m-%d %H:%M:%S %z")
5
6FULL_VERSION = AOS $(VERSION) ($(BUILD_TIME)) x86_64

磁盘位置

然后就是指定了物理磁盘的路径

1PHYSICAL_DISK = /dev/sda
2DISK := $(PHYSICAL_DISK)

build编译

这个部分需要将代码编译出来并写入磁盘

首先就是创建一个临时文件夹, 用于保存编译出的文件 $(MKDIR) build, 在在创建之前我先尝试删除了旧build文件夹-$(RMDIR) build, 通过在命令前面加-来忽略文件夹不存在时执行出错

然后就是编译loader的部分, 将编译的临时文件夹传递给了bootloader下的Makefile. -C bootloader build 表示进入bootloader目录执行build编译,

1$(MAKE) -C bootloader build BUILD="$(_PATH)build"

先清除磁盘开头的那部分数据, 然后格式化为FAT32, 这样方便hexdump来查看数据(因为hexdump会忽略值为0的字节,只显示非0数据)

1$(SUDO) $(DD) if=$(ZERO) of=$(DISK) bs=1M count=64 conv=notrunc status=progress
2$(SUDO) $(MKFAT32) $(DISK)

之后将boot写入磁盘

1$(SUDO) $(DD) if=$(_PATH)build/boot.bin of=$(DISK) bs=512 count=1 conv=notrunc

然后就是两个虚拟机工具的快捷调用

  • qemu: 先启动一个新终端, 打开gdb工具, 然后运行qemu从指定的磁盘启动
  • bochs: 先删除锁文件, bochs运行时会生成一个.lock文件锁定磁盘, 但如果非正常退出bochs会导致这个文件无法自动删除, 进而导致无法再次启动bochs.
1qemu :
2	$(TERM) $(TERM_FLAGS)
3	$(SUDO) $(QEMU) -hda $(DISK) $(QEMU_FLAGS)
4
5bochs :
6	-$(SUDO) $(RM) $(DISK).lock
7	$(SUDO) $(BOCHS) $(BOCHS_FLAGS)

最后clean部分就是清理垃圾

完整Makefile

 1_PATH := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
 2
 3CD = cd
 4CP = cp
 5LS = ls
 6CPDIR = cp -r
 7RM = /usr/bin/rm
 8RMDIR = /usr/bin/rm -rf
 9NASM = /usr/bin/nasm
10MAKE = /usr/bin/make
11MKDIR = /usr/bin/mkdir
12SUDO = /usr/bin/sudo
13SLEEP = /usr/bin/sleep
14DD = /usr/bin/dd
15MOUNT = /usr/bin/mount
16UMOUNT = /usr/bin/umount
17SYNC = /usr/bin/sync
18MKFAT32 = /usr/sbin/mkfs.vfat
19NOTIFY = /usr/bin/notify-send
20GIT = /usr/bin/git
21SED = /usr/bin/sed
22DATE = /usr/bin/date
23
24ZERO = /dev/zero
25
26# 编译信息
27BRANCH=$(shell $(GIT) rev-parse --abbrev-ref HEAD)
28VERSION=$(shell $(GIT) describe --tags --always | $(SED) 's/^v//')
29COMMIT=$(shell $(GIT) rev-parse --verify HEAD)
30BUILD_TIME=$(shell $(DATE) +"%Y-%m-%d %H:%M:%S %z")
31
32FULL_VERSION = AOS $(VERSION) ($(BUILD_TIME)) x86_64
33
34
35TERM = /usr/bin/terminator
36TERM_FLAGS = -x "gdb build/system -ex=\"target remote :1234\""
37
38BOCHS = /usr/bin/bochs
39BOCHS_FLAGS = -debugger -f asset/bochsrc
40QEMU = /usr/bin/qemu-system-x86_64
41QEMU_FLAGS = -m 128M -smp sockets=1,cores=1,threads=1 -S -s -monitor stdio
42#QEMU_FLAGS = -m 1024M -smp sockets=1,cores=4,threads=2 -S -s
43#QEMU = /usr/bin/kvm
44#QEMU_FLAGS = -m 1024M -cpu host -smp sockets=1,cores=4,threads=2 -S -s
45
46PHYSICAL_DISK = /dev/sda
47DISK := $(PHYSICAL_DISK)
48
49
50default :
51	$(MAKE) build
52	$(MAKE) bochs
53#	$(MAKE) qemu
54
55
56
57.PHONY : build
58build : $(DISK)
59# clean old file
60	-$(RMDIR) build
61	$(MKDIR) build
62# build bootloader
63	$(MAKE) -C bootloader build BUILD="$(_PATH)build"
64# clean disk
65	$(SUDO) $(DD) if=$(ZERO) of=$(DISK) bs=1M count=64 conv=notrunc status=progress
66	$(SUDO) $(MKFAT32) $(DISK)
67# write boot.bin
68	$(SUDO) $(DD) if=$(_PATH)build/boot.bin of=$(DISK) bs=512 count=1 conv=notrunc
69
70
71qemu :
72	$(TERM) $(TERM_FLAGS)
73	$(SUDO) $(QEMU) -hda $(DISK) $(QEMU_FLAGS)
74
75
76bochs :
77	-$(SUDO) $(RM) $(DISK).lock
78	$(SUDO) $(BOCHS) $(BOCHS_FLAGS)
79
80
81.PHONY : clean
82clean :
83	-$(RMDIR) build
84	-$(SUDO) $(RM) $(DISK).lock

此时我们的目录结构

 1$ tree .  
 2.
 3├── LICENSE
 4├── Makefile
 5├── README.md
 6├── asset
 7│   ├── BIOS-bochs-latest
 8│   ├── VGABIOS-lgpl-latest
 9│   └── bochsrc
10├── bootloader
11│   ├── Makefile
12│   ├── boot.asm
13│   ├── def.inc
14│   └── fat32.inc
15├── build
16│   └── boot.bin
17└── tools
18    └── gen_fat32_inc
19        ├── gen_fat32.sh
20        └── gen_fat32_inc.c

此时boot.asm完整代码

可以看到除了之前说明的部分, 我添加了_hlt部分, 这是为了让cpu停在这里, 这是一个死循环, 一直执行hlt指令, 这个指令会让cpu处于停止状态, 但中断会让hlt结束, 因此使用死循环不断hlt

 1%include "def.inc"
 2
 3[bits 16]
 4org 0x7c00
 5
 6  jmp short _start_boot
 7  nop
 8
 9%include "fat32.inc"
10
11_start_boot:
12
13; init register
14  mov ax, cs
15  mov es, ax
16  mov ds, ax
17  mov ax, DEF_AREA_STACK_SEGMENT
18  mov ss, ax
19  mov sp, DEF_AREA_STACK_OFFSET
20  xor ax, ax
21  mov fs, ax
22
23_hlt:
24  hlt
25  jmp short _hlt
26
27  times 510-($-$$) db 0
28  dw 0xAA55

使用bochs调试

到这里, 我们就可以在项目的根目录直接执行make命令, make就会调用default的指令, 也就是先make build编译代码, 然后make bochs进入bochs虚拟机调试

执行make之后, 会自动编译并进入bochs的调试, 应该会停在类似下面这个输出, 然后会等待你输入命令

1Switching to CPU0
2Next at t=0
3(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
4<bochs:1> 

这个时候, 我们输入sreg, 按回车, 会看到类似下面的输出. 这就是段寄存器内存储的数据

可以看到cs寄存器内的数据是0xf000, ss内的数据为0x0000

 1<bochs:1> sreg
 2es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
 3	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
 4cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
 5	Data segment, base=0xffff0000, limit=0x0000ffff, Read/Write, Accessed
 6ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
 7	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
 8ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
 9	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
10fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
11	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
12gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
13	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
14ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
15tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
16gdtr:base=0x0000000000000000, limit=0xffff
17idtr:base=0x0000000000000000, limit=0xffff

这个时候我们输入c按下回车, 让程序执行一会, 然后按下Ctrl-C停止执行, 再次输出sreg按下回车. 我们会看到类似下面的输出

第一行Booting from 0000:7c00表示BIOS已经跳转到0000:7c00处执行, 也就是我们编写的这部分代码的位置

第二行WARNING: HLT instruction with IF=0!表示CPU已经执行到了我们的hlt指令, 这时候就可以Ctrl-C

sreg指令的输出中, 我们看到cs段寄存器已经变成了0x0000, 这是因为BIOS将执行权限交给boot时, 会将cs置零.

同时我们也发现ss堆栈段寄存器已经变成了0x5000, 也就是我们在之前为堆栈分配的段地址

 100017404921i[BIOS  ] Booting from 0000:7c00
 200017404985i[CPU0  ] WARNING: HLT instruction with IF=0!
 3^C03057664000i[      ] Ctrl-C detected in signal handler.
 4Next at t=3057664006
 5(0) [0x000000007c6d] 0000:7c6d (unk. ctxt): jmp .-3  (0x00007c6c)     ; ebfd
 6<bochs:3> sreg
 7es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
 8	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
 9cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
10	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
11ss:0x5000, dh=0x00009305, dl=0x0000ffff, valid=1
12	Data segment, base=0x00050000, limit=0x0000ffff, Read/Write, Accessed
13ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
14	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
15fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
16	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
17gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
18	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
19ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
20tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
21gdtr:base=0x00000000000f9af7, limit=0x30
22idtr:base=0x0000000000000000, limit=0x3ff

我们在bochs输入reg看看其他寄存器, 发现堆栈指针寄存器rsp, 在16位下就是sp, 保存的值就是_后面的8个0, 数值也是0, 这就说明我们的堆栈相关的两个寄存器已经正确配置

 1<bochs:3> reg
 2CPU0:
 3rax: 00000000_00000000
 4rbx: 00000000_00000000
 5rcx: 00000000_00090000
 6rdx: 00000000_00000080
 7rsp: 00000000_00000000
 8rbp: 00000000_00000000
 9rsi: 00000000_000e0000
10rdi: 00000000_0000ffac
11r8 : 00000000_00000000
12r9 : 00000000_00000000
13r10: 00000000_00000000
14r11: 00000000_00000000
15r12: 00000000_00000000
16r13: 00000000_00000000
17r14: 00000000_00000000
18r15: 00000000_00000000
19rip: 00000000_00007c6d
20eflags: 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf

至此, 通过bochs我们发现我们在boot.asm中编写的指令, cpu已经正确执行了. 因此可以在bochs中输入exit退出bochs

初始化部分变量

我们前清零了变量区域的内存数据, 使得变量默认是0.

首先是保存es寄存器的值, 然后我们将变量区域的段地址赋值给es, 段内指针赋值给di, 区域大小赋值给cx, 然后清零ax寄存器(因为指令会使用al填充数据)

通过cld确保DF标志位是0, 使得每次stosb操作后di是自增而不是自减

rep会根据cx来判断循环多少次

最后有些变量我们现在就可以保存到内存了

  • DEF_VAR_BOOT_DRIVE_NUMBER: dl中保存了启动磁盘的号码, 在后面读取loader时用的上
  • DEF_VAR_DISPLAY_LINE: 是当前显示的行号, 在boot中显示字符串时需要
  • DEF_VBE_MODE是选择的VBE模式号, 这个在loader时保存入内存也可以, 在boot中暂时用不上
  • DEF_VAR_DAP_LENGTH是DAP数据的大小, 是DAP的重要组成部分, 表示我们提供的DAP的大小是多少, 在后面读取loader时用的上
 1; clean up VAR's memory
 2  push es
 3  mov ax, DEF_AREA_VAR_SEGMENT
 4  mov es, ax
 5  mov di, DEF_AREA_VAR_OFFSET
 6  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
 7  xor ax, ax
 8  cld
 9  rep stosb
10  pop es
11
12; initialize variable
13  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
14  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
15  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
16  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH

清空屏幕

还记得bochs启动后, 弹出的画面中有很多文字信息吗, 这个里面显示了一些bochs提供的硬件的信息, 我们想要在屏幕上显示自己的信息最好先将这些信息清除掉

只需要调用int 10h, 这是一个显示服务的BIOS中断调用

  • ah=0x00: 表示设置视频模式
  • al=0x03: 表示设置为80×25文本模式

切换模式的同时会清除屏幕内容

1; clean screen
2  mov ax, 0x0003
3  int 10h

通过bochs验证内存中的变量是否配置正确

此时我们的代码看起来应该像下面一样

 1%include "def.inc"
 2
 3[bits 16]
 4org 0x7c00
 5
 6  jmp short _start_boot
 7  nop
 8
 9%include "fat32.inc"
10
11_start_boot:
12
13; init register
14  mov ax, cs
15  mov es, ax
16  mov ds, ax
17  mov ax, DEF_AREA_STACK_SEGMENT
18  mov ss, ax
19  mov sp, DEF_AREA_STACK_OFFSET
20  xor ax, ax
21  mov fs, ax
22
23; clean up VAR's memory
24  push es
25  mov ax, DEF_AREA_VAR_SEGMENT
26  mov es, ax
27  mov di, DEF_AREA_VAR_OFFSET
28  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
29  xor ax, ax
30  cld
31  rep stosb
32  pop es
33
34; initialize variable
35  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
36  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
37  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
38  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH
39
40; clean screen
41  mov ax, 0x0003
42  int 10h
43
44
45_hlt:
46  hlt
47  jmp short _hlt
48
49  times 510-($-$$) db 0
50  dw 0xAA55

我们在项目根目录执行make, 然后在bochs输入c, 执行到htlCtrl-C停止运行. 此时我们发现bochs的显示器窗口已经一片黑了, 只有左上角有个光标, 表示清除屏幕已经成功

bochs内存查看方法

我们知道内存地址分为物理地址线性地址. 物理地址表示绝对的,实际存在的,物理内存芯片特定位置的地址. 线性地址是经过地址转换机制后,映射出来的地址

在bochs中通过下面两个命令查看, 注意/后面的nufaddr有各自的含义, 需要自行替换

  • 物理地址通过xp /nuf addr命令查看
  • 线性地址通过x /nuf addr命令查看

参数说明, addr就是十六进制的地址, nuf分别代表了查看的单位数量, 单位大小, 显示格式

  • n: 要查看的单位数量, 例如n个字节, n个字
  • u: 单位的大小: b(字节), h(半字,2个字节), w(字,4个字节), g(双字,8个字节), q(四字,16个字节)
  • f: 显示格式: x(十六进制), d(十进制), u(无符号十进制), o(八进制), t(二进制), c(字符)
  • addr: 要查看的地址, 十六进制

回到我们的bochs, 根据我们之前配置的三个变量的地址, 查看下他们的数据都对不对

DEF_VAR_BOOT_DRIVE_NUMBER 中保存的是启动磁盘的编号, 我们给他指定的位置是0x7E00开始的1个字节大小

那么我们就可以使用xp /1bx 0x7E00来查看, 发现是0x80

1<bochs:2> xp /1bx 0x7E00
2[bochs]:
30x0000000000007e00 <bogus+       0>:	0x80

DEF_VAR_DISPLAY_LINE 中保存的是显示字符串的行号, 我们给他指定的位置是0x7E10开始的1个字节大小

那么我们就可以使用xp /1bx 0x7E10来查看, 发现是0x00

1<bochs:3> xp /1bx 0x7E10
2[bochs]:
30x0000000000007e10 <bogus+       0>:	0x00

DEF_VAR_VBE_MODE 中保存的是我们选择的VBE模式号, 我们之前配置的是0x0000

那么我们就可以使用xp /2bx 0x7E01来查看, 发现是0x00 0x00, 符合之前的配置, 当然内存默认好像就是0

1<bochs:4> xp /2bx 0x7E01
2[bochs]:
30x0000000000007e01 <bogus+       0>:	0x00 0x00

DEF_VAR_DAP_SIZE 中保存的是DAP数据的大小, 我们通过DEF_VAR_DAP_LENGTH指定的是0x10, 也就是16个字节

那么我们就可以使用xp /1bx 0x7EF0来查看, 发现是0x10, 符合之前的配置

1<bochs:5> xp /1bx 0x7EF0
2[bochs]:
30x0000000000007ef0 <bogus+       0>:	0x10

也就是说我们成功的将制定数值写入了内存, 并且清空了屏幕

显示字符串

这个世界上最伟大的调试方法当然是printf调试, 我们需要一个直观的方式, 来显示当前程序执行到什么地方, 显然输出一串字符串就是很好的方式. 而BIOS恰好提供了一个方法, 可以让我们很方便的输出字符串

BIOS中断调用 INT10h AH=13h

我们详细介绍下调用这个BIOS功能号需要那些准备

AL=写入模式

  • 00h:字符串的属性由BL寄存器提供,而CX寄存器提供字符串长度(以B为单位),显示后光标位置不变。
  • 01h:同00h,但光标会移动至字符串尾端位置
  • 02h:字符串属性由每个字符后面紧跟的字节提供,故CX寄存器提供的字符串长度改成以Word为单位,显示后光标位置不变
  • 03h:同02h,但光标会移动至字符串尾端位置

CX=字符串的长度

DH=游标的坐标行号

DL=游标的坐标列号

ES:BP => 要显示字符串的内存地址

BH=页码

BL=字符属性/颜色属性

  • bit 0~2:字体颜色(0:黑,1:蓝,2:绿,3:青,4:红,5:紫,6:棕,7:白)
  • bit 3:字体亮度(0:正常,1:字体高亮度)
  • bit 4~6:背景颜色(0:黑,1:蓝,2:绿,3:青,4:红,5:紫,6:棕,7:白)
  • bit 7:字体闪烁(0:不闪烁,1:字体闪烁)

字符串变量

在编写打印函数之前, 我们先定义几个字符串, 分为四组

以第一组为例, MSG_start_boot用法前面介绍过, 就是代指当前的内存地址, 后面通过db指令将字符串写入. MSG_start_boot_len这行是一个类似define的定义, 代表的数值通过$ - MSG_start_boot计算得出(当前内存地址-字符串开头的内存地址), 就是字符串的长度

 1MSG_start_boot:			db	"start boot"
 2MSG_start_boot_len	equ	$ - MSG_start_boot
 3
 4MSG_read_sector_e:		db	"read sector error"
 5MSG_read_sector_e_len	equ	$ - MSG_read_sector_e
 6
 7MSG_loader_not_found:			db	"loader not found"
 8MSG_loader_not_found_len	equ	$ - MSG_loader_not_found
 9
10MSG_loader_not_loaded:		db	"loader not loaded"
11MSG_loader_not_loaded_len	equ	$ - MSG_loader_not_loaded

打印函数封装

你没看错, 就是函数, 但此函数是通过call配合ret来实现.

那么为什么我们要用call调用呢? 因为call会将返回地址压栈, 使用ret可以返回到当前位置继续向下执行

虽然说是封装, 但实际也没干啥

先看一遍这个函数, 然后我们再详细介绍, 注意;开始的部分是注释, 简单介绍了下函数, 可以忽略

 1;;;;;;; print string
 2;; AL=01h: Assign all characters the attribute in BL; update cursor
 3;; BH=00h: Display page number
 4;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
 5;; DL=00h: Display Column number
 6;;;; !Required
 7;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
 8;;;; !Input
 9;; ES:BP: Points to string to be printed
10;; CX: Number of characters in string
11;;;; !Modified
12;; [fs:DEF_VAR_DISPLAY_LINE]
13;;;;;;;
14func_print:
15  mov ax, 0x1301
16  mov bx, 0x000f
17  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
18  xor dl, dl
19  int 10h
20  inc byte [fs:DEF_VAR_DISPLAY_LINE]
21  ret

根据上面每个寄存器代表的参数, 我们可以看到这个函数固定使用的写入模式是01h, 显示后光标移动到字符串尾端

页码是0, 字体颜色是白色, 字体亮度为高亮, 背景为黑色, 字体不闪烁

同时将DEF_VAR_DISPLAY_LINE保存的行号读取到dh, 通过xordl置零, 表示最左侧那一列

然后就是调用10h中断号来显示字符串

显示完成后, 我们将DEF_VAR_DISPLAY_LINE保存的数值增加1, 表示下一次的字符串显示在下一行.

调用func_print打印字符串

现在打印函数有了, 字符串也有了, 就可以显示字符串了

  • es段寄存器在最开始初始化寄存器时已经清零, 就不用管了
  • bp寄存器就指向字符串开头的地址, 也就是MSG_start_boot代指的地址
  • cx寄存器保存字符串长度, 也就是MSG_start_boot_len代表的值
1; msg: start boot
2  mov bp, MSG_start_boot
3  mov cx, MSG_start_boot_len
4  call func_print

完整boot代码

 1%include "def.inc"
 2
 3[bits 16]
 4org 0x7c00
 5
 6  jmp short _start_boot
 7  nop
 8
 9%include "fat32.inc"
10
11_start_boot:
12
13; init register
14  mov ax, cs
15  mov es, ax
16  mov ds, ax
17  mov ax, DEF_AREA_STACK_SEGMENT
18  mov ss, ax
19  mov sp, DEF_AREA_STACK_OFFSET
20  xor ax, ax
21  mov fs, ax
22
23; clean up VAR's memory
24  push es
25  mov ax, DEF_AREA_VAR_SEGMENT
26  mov es, ax
27  mov di, DEF_AREA_VAR_OFFSET
28  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
29  xor ax, ax
30  cld
31  rep stosb
32  pop es
33
34; initialize variable
35  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
36  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
37  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
38  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH
39
40; clean screen
41  mov ax, 0x0003
42  int 10h
43
44; msg: start boot
45  mov bp, MSG_start_boot
46  mov cx, MSG_start_boot_len
47  call func_print
48
49
50_hlt:
51  hlt
52  jmp short _hlt
53
54;;;;;;; print string
55;; AL=01h: Assign all characters the attribute in BL; update cursor
56;; BH=00h: Display page number
57;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
58;; DL=00h: Display Column number
59;;;; !Required
60;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
61;;;; !Input
62;; ES:BP: Points to string to be printed
63;; CX: Number of characters in string
64;;;; !Modified
65;; [fs:DEF_VAR_DISPLAY_LINE]
66;;;;;;;
67func_print:
68  mov ax, 0x1301
69  mov bx, 0x000f
70  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
71  xor dl, dl
72  int 10h
73  inc byte [fs:DEF_VAR_DISPLAY_LINE]
74  ret
75
76
77MSG_start_boot:     db   "start boot"
78MSG_start_boot_len  equ  $ - MSG_start_boot
79
80MSG_read_sector_e:     db   "read sector error"
81MSG_read_sector_e_len  equ  $ - MSG_read_sector_e
82
83MSG_loader_not_found:     db   "loader not found"
84MSG_loader_not_found_len  equ  $ - MSG_loader_not_found
85
86MSG_loader_not_loaded:     db   "loader not loaded"
87MSG_loader_not_loaded_len  equ  $ - MSG_loader_not_loaded
88
89  times 510-($-$$) db 0
90  dw 0xAA55

通过bochs验证代码

make编译运行, 输入c启动后, 我们发现成功的在屏幕第一行显示了start boot, 并且光标在字符串末尾, 说明我们已经成功打印出了想要的字符串

loader的文件名

我们需要提前将loader的文件名写入代码, 这样后面搜索loader时才能对比文件名是否一致

我们编译出来的loader的文件名是 loader.bin, 但是在FAT32中显示为两个Root Directory Entry, 我们代码中使用的是短文件名(SFN), SFN中文件名部分为11位字符, 且全部使用大写储存, 其中前8位是文件名, 后3位是拓展名.

所以我们放在代码里的文件名就像下面这样, 文件名LOADER与拓展名BIN之间间隔了2个空格. (如果文件名为l.in, 那么对应SFN应为"L IN ")

1FILE_loader:	db "LOADER  BIN"

扇区读取函数 INT 13h AH=42h

在这里, 我们首先使用了push通过压栈方式保存了ds寄存器的数值.

然后利用pushpop到另一个寄存器的方式将fs寄存器的值拷贝到ds寄存器. 因为我们需要ds寄存器指向当前段, 这样esi才能正确指向DAP的结构的地址, DAP的数据需要我们在调用前准备好.

还记得刚进入boot时在DEF_VAR_BOOT_DRIVE_NUMBER地址保存的dl的数据吗, 这个时候就用上了. 我们通过mov将保存的磁盘号复制到dl寄存器.

最后就是调用INT 13h读取磁盘, 完成后恢复ds寄存器的值

INT 13h执行完成后会在ah保存返回值, 清除CF标志位, 执行出错时, 会设置CF标志位

通过jc来判断, 如果CF标志位被置位, 跳转到e_func_read_sector打印错误信息到屏幕. 在打印信息完成后通过jmp $进入死循环, 防止程序乱跑

 1;;;;;;; read sector from boot disk
 2;;;; !Required
 3;; [fs:DEF_VAR_DAP]
 4;; [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
 5;;;;;;;
 6func_read_sector:
 7  push ds
 8  push fs
 9  pop ds
10  mov esi, DEF_VAR_DAP
11  mov ah, 42h
12  mov dl, byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
13  int 13h
14  pop ds
15  jc .e_func_read_sector
16
17  ret
18
19.e_func_read_sector:
20  mov bp, MSG_read_sector_e
21  mov cx, MSG_read_sector_e_len
22  call func_print
23  jmp $

读取FAT32的 Root Directory

这里是FAT32存储文件列表的地方, 我们想要查找loader, 首先就要读取这块的数据

在这里我们只读取了4KiB字节的数据, 正常如果每次格式化磁盘后把loader复制进磁盘, loader肯定是前几个Root Directory Entry之一.

  • DEF_VAR_DAP_TOTAL存储的是要读取的扇区数量, 我们使用的是INT 13h AH=42h来读取磁盘, 因此这里的扇区是512字节大小
  • DEF_VAR_DAP_LBA_LOW存储的是fat32.inc文件里的FAT32_ROOT_REGION, 表示Root Directory的起始扇区号, 因此我们要读的扇区号比较小, 因此低4位就足够了
  • DEF_VAR_DAP_SEGMENT存储的是我们分配的Root Directory的内存区域的段
  • DEF_VAR_DAP_OFFSET存储的是我们分配的Root Directory的内存区域的段内偏移

最后就是使用call方法, 跳转到读取磁盘的函数了.

1; read 4 KiB of FAT32 Root Entry
2  mov word [fs:DEF_VAR_DAP_TOTAL], 0x08 ; 0x08 * 0x200 = 0x1000 bytes
3  mov dword [fs:DEF_VAR_DAP_LBA_LOW], FAT32_ROOT_REGION
4  mov word [fs:DEF_VAR_DAP_SEGMENT], DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT
5  mov word [fs:DEF_VAR_DAP_OFFSET], DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
6  call func_read_sector

测试下Root Directory是否读取正确

如果分区没有文件, 那么Root Directory就是空的, 我们无法判断. 因此就需要我们生成一个loader.bin文件, 而最简单的生成方式, 就是dd命令.

loader.asm

当然我们最好还是创建一个loader.asm的汇编文件, 让他编译出我们想要的内容.

这里我们仿照boot.asm在同目录简单生成一个loader.asm文件. org 0x10000是因为我们要将loader加载到内存0x10000的位置, [section .magic]则是为后面代码段命名为.magic

这里我刻意写入了两个4字节的魔数 就是普通数字 , 方便我们快速验证loader是否完整加载进入内存, 毕竟我们以后的loader会跨越好几个扇区, 甚至跨越好几个簇, 因此开头和末尾的两个魔数是很有必要的. 当然有能力你也可以编写一个函数来校验loader的MD5

 1%include "def.inc"
 2
 3[bits 16]
 4org 0x10000
 5
 6  jmp short _start_loader
 7  nop
 8_magic_start: dd 0x20000229
 9
10
11_start_loader:
12
13[section .magic]
14[bits 16]
15
16_magic_end: dd 0x19991205

bootloader/Makefile

既然加入了loader.asm文件, 那么我们就需要在bootloader/Makefile添加loader, 让loader也能自动编译出来

此时我们bootloader/Makefile就已经彻底编写完成, 不需要再修改了. 完整内容如下:

 1BUILD = .
 2BIN = boot.bin loader.bin
 3
 4CD = cd
 5
 6NASM = /usr/bin/nasm
 7MAKE = /usr/bin/make
 8
 9default :
10
11build : $(BUILD)
12	$(MAKE) $(BIN)
13
14$(BIN) : %.bin : %.asm
15	$(NASM) $< -o $(BUILD)/$@ $(FLAGS)

Makefile

在项目根目录的这个Makefile也需要修改, 它需要将编译出来的loader拷贝到磁盘的FAT32分区中, 也就是挂载分区->拷贝->卸载分区

那么首先build:部分需要追加拷贝loader的功能, 修改后的代码如下:

 1.PHONY : build
 2build : $(DISK)
 3# clean old file
 4	-$(RMDIR) build
 5	$(MKDIR) build
 6# build bootloader
 7	$(MAKE) -C bootloader build BUILD="$(_PATH)build"
 8# clean disk
 9	$(SUDO) $(DD) if=$(ZERO) of=$(DISK) bs=1M count=64 conv=notrunc status=progress
10	$(SUDO) $(MKFAT32) $(DISK)
11# write boot.bin
12	$(SUDO) $(DD) if=$(_PATH)build/boot.bin of=$(DISK) bs=512 count=1 conv=notrunc
13# mount disk
14	-$(MKDIR) mount
15	$(SUDO) $(MOUNT) $(DISK) mount
16# copy file
17	$(SUDO) $(CP) build/loader.bin mount
18# umount disk
19	$(SUDO) $(UMOUNT) $(DISK)
20	$(NOTIFY) "AOS build finished"

其次clean:部分需要做一些额外的清理, 例如删除挂载目录

1.PHONY : clean
2clean :
3	-$(SUDO) $(UMOUNT) mount
4	-$(RMDIR) build
5	-$(RMDIR) mount
6	-$(SUDO) $(RM) $(DISK).lock

至此, 我们的Makefile已经支持自动编译loader并拷贝到磁盘

再次确认boot.asm中的代码

  1%include "def.inc"
  2
  3[bits 16]
  4org 0x7c00
  5
  6  jmp short _start_boot
  7  nop
  8
  9%include "fat32.inc"
 10
 11_start_boot:
 12
 13; init register
 14  mov ax, cs
 15  mov es, ax
 16  mov ds, ax
 17  mov ax, DEF_AREA_STACK_SEGMENT
 18  mov ss, ax
 19  mov sp, DEF_AREA_STACK_OFFSET
 20  xor ax, ax
 21  mov fs, ax
 22
 23; clean up VAR's memory
 24  push es
 25  mov ax, DEF_AREA_VAR_SEGMENT
 26  mov es, ax
 27  mov di, DEF_AREA_VAR_OFFSET
 28  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
 29  xor ax, ax
 30  cld
 31  rep stosb
 32  pop es
 33
 34; initialize variable
 35  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
 36  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
 37  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
 38  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH
 39
 40; clean screen
 41  mov ax, 0x0003
 42  int 10h
 43
 44; msg: start boot
 45  mov bp, MSG_start_boot
 46  mov cx, MSG_start_boot_len
 47  call func_print
 48
 49; read 4 KiB of FAT32 Root Entry
 50mov word [fs:DEF_VAR_DAP_TOTAL], 0x08 ; 0x08 * 0x200 = 0x1000 bytes
 51mov dword [fs:DEF_VAR_DAP_LBA_LOW], FAT32_ROOT_REGION
 52mov word [fs:DEF_VAR_DAP_SEGMENT], DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT
 53mov word [fs:DEF_VAR_DAP_OFFSET], DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
 54call func_read_sector
 55
 56
 57_hlt:
 58  hlt
 59  jmp short _hlt
 60
 61
 62;;;;;;; read sector from boot disk
 63;;;; !Required
 64;; [fs:DEF_VAR_DAP]
 65;; [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
 66;;;;;;;
 67func_read_sector:
 68  push ds
 69  push fs
 70  pop ds
 71  mov esi, DEF_VAR_DAP
 72  mov ah, 42h
 73  mov dl, byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
 74  int 13h
 75  pop ds
 76  jc .e_func_read_sector
 77  ret
 78
 79.e_func_read_sector:
 80  mov bp, MSG_read_sector_e
 81  mov cx, MSG_read_sector_e_len
 82  call func_print
 83  jmp $
 84
 85
 86;;;;;;; print string
 87;; AL=01h: Assign all characters the attribute in BL; update cursor
 88;; BH=00h: Display page number
 89;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
 90;; DL=00h: Display Column number
 91;;;; !Required
 92;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
 93;;;; !Input
 94;; ES:BP: Points to string to be printed
 95;; CX: Number of characters in string
 96;;;; !Modified
 97;; [fs:DEF_VAR_DISPLAY_LINE]
 98;;;;;;;
 99func_print:
100  mov ax, 0x1301
101  mov bx, 0x000f
102  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
103  xor dl, dl
104  int 10h
105  inc byte [fs:DEF_VAR_DISPLAY_LINE]
106  ret
107
108
109MSG_start_boot:     db   "start boot"
110MSG_start_boot_len  equ  $ - MSG_start_boot
111
112MSG_read_sector_e:     db   "read sector error"
113MSG_read_sector_e_len  equ  $ - MSG_read_sector_e
114
115MSG_loader_not_found:     db   "loader not found"
116MSG_loader_not_found_len  equ  $ - MSG_loader_not_found
117
118MSG_loader_not_loaded:     db   "loader not loaded"
119MSG_loader_not_loaded_len  equ  $ - MSG_loader_not_loaded
120
121FILE_loader:  db "LOADER  BIN"
122
123  times 510-($-$$) db 0
124  dw 0xAA55

bochs测试

执行make编译进入bochs, 先不要着急查看内存, 我们先看一下物理磁盘中是否存在loader.

我们看一下自己的fat32.inc文件, 发现FAT32_ROOT_REGION的数值为0x00007720, 在十进制下为30496, 乘以扇区大小512可得15613952, 这就是Root Directory在物理磁盘的偏移

我们通过 hexdump -s 15613952 -n 512 /dev/sda 查看

1---------- 0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F -------------------
200ee4000  41 6c 00 6f 00 61 00 64  00 65 00 0f 00 ab 72 00  |Al.o.a.d.e....r.|
300ee4010  2e 00 62 00 69 00 6e 00  00 00 00 00 ff ff ff ff  |..b.i.n.........|
400ee4020  4c 4f 41 44 45 52 20 20  42 49 4e 20 00 7b dd 8b  |LOADER  BIN .{..|
500ee4030  82 5a 82 5a 00 00 dd 8b  82 5a 03 00 0c 00 00 00  |.Z.Z.....Z......|
600ee4040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

我们发现loader已经在文件列表里了, 这时候就可以去看看程序有没有将这块数据读入内存.

我们看一下自己的def.inc文件, 发现我们为Root Directory分配的内存地址是0x8000

记得先输入c让程序运行到hlt指令, 然后停止运行再输入xp /64bx 0x8000. 可以看到数据已经被正确加载入内存中了

 1<bochs:6> xp /64bx 0x8000 
 2[bochs]:
 30x0000000000008000 <bogus+       0>:	0x41	0x6c	0x00	0x6f	0x00	0x61	0x00	0x64
 40x0000000000008008 <bogus+       8>:	0x00	0x65	0x00	0x0f	0x00	0xab	0x72	0x00
 50x0000000000008010 <bogus+      16>:	0x2e	0x00	0x62	0x00	0x69	0x00	0x6e	0x00
 60x0000000000008018 <bogus+      24>:	0x00	0x00	0x00	0x00	0xff	0xff	0xff	0xff
 70x0000000000008020 <bogus+      32>:	0x4c	0x4f	0x41	0x44	0x45	0x52	0x20	0x20
 80x0000000000008028 <bogus+      40>:	0x42	0x49	0x4e	0x20	0x00	0x7b	0xdd	0x8b
 90x0000000000008030 <bogus+      48>:	0x82	0x5a	0x82	0x5a	0x00	0x00	0xdd	0x8b
100x0000000000008038 <bogus+      56>:	0x82	0x5a	0x03	0x00	0x0c	0x00	0x00	0x00

搜索loader文件

我们已经读取了Root Directory的一部分, 我们需要从这里找到loader.bin文件的信息, 从而确定loader的大小和保存在那些簇中

搜索前的准备

既然是搜索文件, 那么就需要借助循环来遍历Root Directory, 然后比较文件名是否是loader的文件名

  • es: 保存目标字符串地址的段, 就是Root Directory的段地址
  • di: 保存目标字符串地址的指针, 就是Root Directory的指针地址
  • ds: 保存字符串地址的段, 就是FILE_loader所在的段
  • si: 保存字符串地址的指针, 就是FILE_loader的地址
  • cx: 保存计数器, 我们一个字节一个字节比较, 因此这里就是字符串长度
  • dx: 保存Root Directory共有多少个条目, 因为我们只读取了4KiB大小的Root Directory, 每个条目32字节, 因此就是0x1000 >> 5共128个条目
1; search loader.bin
2  push es ; 保存原始es寄存器数据
3  push word DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT ; 将Root Directory的内存地址压栈, 便于赋值给es寄存器
4  pop es ; 将Root Directory的内存地址出栈, 放入es寄存器
5  mov edi, DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
6  mov esi, FILE_loader
7  mov ecx, 11
8  mov edx, 0x1000 >> 5 ; 32 bit per entries

可以看到这里我们压栈es后并未恢复, 不需要担心, 我们在搜索完成之后会恢复

搜索

字节比较使用smpsb, 方向通过DF标志位控制, 默认情况下每次比较操作后sidi都会增加1

比较字符串最简单的方法就是使用smpsb来比较单个字符, 然后使用repe来循环执行, 通过cx来控制循环次数, 每次repecx都会减少1

因为repe cmpsb会修改众多通用寄存器的值, 因此需要在执行前压栈所有通用寄存器, 执行结束后恢复所有寄存器

如果repe cmpsb比较结束后, 零标志位ZF=1, 那就表示文件名匹配, 跳转到loader_found, 也就是找到loader文件后的处理代码. 如果文件名不匹配, 那就将di也就是指向Root Directory条目的指针加32字节, 也就是单个条目大小, 让di指向下个条目. 同时让dx减1, 判断是否等于0, 也就是判断还有没有条目, 如果有就继续搜索, 如果没有就在屏幕打印字符串, 表示没有找到loader

找到loader后, 我们先将loader的大小保存在DEF_VAR_KERNEL_SIZE, 这个地址原本是保存内核大小的, 但在当前阶段可以暂时储存loader的大小. 然后我们按照小端序分别将0x140x1a这两个位置的各两个字节压栈, 这样就会组成4个字节的簇号, 然后我们一次性将这四个字节出栈到eax寄存器, 这样我们就获得了loader所在的第一个簇

最后我们再将 搜索前的准备 时压栈的es寄存器恢复

 1.loop_search_loader:
 2
 3  pushad ; protect ecx, edi, esi
 4  repe cmpsb
 5  popad
 6  je .loader_found
 7
 8  add edi, 32
 9  dec edx
10  jnz .loop_search_loader
11
12  pop es
13  mov bp, MSG_loader_not_found
14  mov cx, MSG_loader_not_found_len
15  call func_print
16  jmp $
17
18.loader_found:
19
20  mov eax, dword [es:edi+0x1c]
21  mov dword [fs:DEF_VAR_KERNEL_SIZE], eax
22  push word [es:edi+0x14]
23  push word [es:edi+0x1a]
24  pop eax
25  pop es

读取loader

既然我们已经找到了loader所在的簇, 那么接下来就是读取loader的时间了

为了缩小boot的代码量, 我们不得不删减了很多东西

  • 读取Root Directory时只读取了4KiB的大小, 使得我们只能在前128个条目中搜索loader的信息
  • 读取loader时, 我们也不得不去掉查找FAT表的逻辑, 使得我们只能从第一个簇中读取loader数据, 进而限制了loader最大只能是一个簇的大小

我们来使用之前读取Root Directory时编写的func_read_sector函数读取这个簇

根据我们在先前FAT32部分总结出的簇起始位置的计算方式, 以及函数所需的扇区起始位置, 对我们获取到的簇号进行处理.

  • sub: 寄存器内的值减2
  • mul: 将eax内的数值乘以指定的寄存器内的数值, 也就是edx中的每个簇的扇区数量. 结果的高32位在edx, 低32位在eax, 我们这里eax足够储存下结果
  • add: 寄存器内的值加FAT32_ROOT_REGION, 也就是数据区的起始扇区

最后将我们的计算结果, 也就是eax的值放入DEF_VAR_DAP_LBA_LOW位置

然后就是配置

  • DEF_VAR_DAP_TOTAL: 读取的扇区总数, 也就是每个簇的扇区数量
  • DEF_VAR_DAP_SEGMENT: 读取到的数据保存位置的段
  • DEF_VAR_DAP_OFFSET: 读取到的数据保存位置的段内指针

准备好后调用func_read_sector就可以读取完成了

 1; read FAT32_SECTORS_PER_CLUSTER KiB of loader.bin
 2  sub eax, 2
 3  mov edx, FAT32_SECTORS_PER_CLUSTER
 4  mul edx
 5  add eax, FAT32_ROOT_REGION
 6  mov dword [fs:DEF_VAR_DAP_LBA_LOW], eax
 7  mov word [fs:DEF_VAR_DAP_TOTAL], FAT32_SECTORS_PER_CLUSTER ; 0x08 * 0x200 = 0x1000 bytes
 8  mov word [fs:DEF_VAR_DAP_SEGMENT], DEF_AREA_LOADER_SEGMENT
 9  mov word [fs:DEF_VAR_DAP_OFFSET], DEF_AREA_LOADER_OFFSET
10  call func_read_sector

验证并进入loader

还记得我们编写的简易loader中的那两个魔数吗, 一个在loader开头, 一个在loader末尾.

我们可以通过比较这两个魔数, 来判断loader是否完整的加载入内存(当然你也可以不验证, 直接进入loader)

比较方式也很简单, 因为两个数字都是四字节, 直接使用cmp指令, 通过dword指定判断4个字节

这里我们只判断了loader开头的部分

使用je指令, 等价于jz, 也就是标志位ZF = 1时跳转, 如果相等就显示loader加载失败

最后通过jmp DEF_AREA_LOADER_SEGMENT:DEF_AREA_LOADER_OFFSET跳转进入loader

 1; integrity checking
 2  mov ax, DEF_AREA_LOADER_SEGMENT
 3  mov ds, ax
 4  cmp dword [ds:DEF_AREA_LOADER_OFFSET+0x03], 0x20000229
 5  jne .integrity_check_fail
 6  mov eax, dword [fs:DEF_VAR_KERNEL_SIZE]
 7  add eax, DEF_AREA_LOADER_OFFSET - 4
 8  cmp dword [ds:eax], 0x19991205
 9  jne .integrity_check_fail
10
11; enter loader
12  jmp DEF_AREA_LOADER_SEGMENT:DEF_AREA_LOADER_OFFSET
13  
14.integrity_check_fail:
15  mov bp, MSG_loader_not_loaded
16  mov cx, MSG_loader_not_loaded_len
17  call func_print
18  jmp $

到这里boot的512字节已经几乎用完, 如果你在我的代码基础上增加了一些东西, 可能在编译时会报错显示类似error: TIMES value is negative信息, 那么说明你的代码编译出来后会超过512字节, 需要缩减代码. 比如你可以删掉_hlt:部分.

完整的boot.asm代码

这里贴出完整的boot.asm代码, 方便出错时对比

  1%include "def.inc"
  2
  3[bits 16]
  4org 0x7c00
  5
  6  jmp short _start_boot
  7  nop
  8
  9%include "fat32.inc"
 10
 11_start_boot:
 12
 13; init register
 14  mov ax, cs
 15  mov es, ax
 16  mov ds, ax
 17  mov ax, DEF_AREA_STACK_SEGMENT
 18  mov ss, ax
 19  mov sp, DEF_AREA_STACK_OFFSET
 20  xor ax, ax
 21  mov fs, ax
 22
 23; clean up VAR's memory
 24  push es
 25  mov ax, DEF_AREA_VAR_SEGMENT
 26  mov es, ax
 27  mov di, DEF_AREA_VAR_OFFSET
 28  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
 29  xor ax, ax
 30  cld
 31  rep stosb
 32  pop es
 33
 34; initialize variable
 35  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
 36  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
 37  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
 38  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH
 39
 40; clean screen
 41  mov ax, 0x0003
 42  int 10h
 43
 44; msg: start boot
 45  mov bp, MSG_start_boot
 46  mov cx, MSG_start_boot_len
 47  call func_print
 48
 49; read 4 KiB of FAT32 Root Entry
 50  mov word [fs:DEF_VAR_DAP_TOTAL], 0x08 ; 0x08 * 0x200 = 0x1000 bytes
 51  mov dword [fs:DEF_VAR_DAP_LBA_LOW], FAT32_ROOT_REGION
 52  mov word [fs:DEF_VAR_DAP_SEGMENT], DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT
 53  mov word [fs:DEF_VAR_DAP_OFFSET], DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
 54  call func_read_sector
 55
 56; search loader.bin
 57  push es
 58  push word DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT
 59  pop es
 60  mov edi, DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
 61  mov esi, FILE_loader
 62  mov ecx, 11
 63  mov edx, 0x1000 >> 5 ; 32 bit per entries
 64
 65.loop_search_loader:
 66
 67  pushad ; protect ecx, edi, esi
 68  repe cmpsb
 69  popad
 70  je .loader_found
 71
 72  add edi, 32
 73  dec edx
 74  jnz .loop_search_loader
 75
 76  pop es
 77  mov bp, MSG_loader_not_found
 78  mov cx, MSG_loader_not_found_len
 79  call func_print
 80  jmp $
 81
 82.loader_found:
 83
 84  mov eax, dword [es:edi+0x1c]
 85  mov dword [fs:DEF_VAR_KERNEL_SIZE], eax
 86  push word [es:edi+0x14]
 87  push word [es:edi+0x1a]
 88  pop eax
 89  pop es
 90
 91; read FAT32_SECTORS_PER_CLUSTER KiB of loader.bin
 92  sub eax, 2
 93  mov edx, FAT32_SECTORS_PER_CLUSTER
 94  mul edx
 95  add eax, FAT32_ROOT_REGION
 96  mov dword [fs:DEF_VAR_DAP_LBA_LOW], eax
 97  mov word [fs:DEF_VAR_DAP_TOTAL], FAT32_SECTORS_PER_CLUSTER ; 0x08 * 0x200 = 0x1000 bytes
 98  mov word [fs:DEF_VAR_DAP_SEGMENT], DEF_AREA_LOADER_SEGMENT
 99  mov word [fs:DEF_VAR_DAP_OFFSET], DEF_AREA_LOADER_OFFSET
100  call func_read_sector
101
102; integrity checking
103  mov ax, DEF_AREA_LOADER_SEGMENT
104  mov ds, ax
105  cmp dword [ds:DEF_AREA_LOADER_OFFSET+0x03], 0x20000229
106  jne .integrity_check_fail
107  mov eax, dword [fs:DEF_VAR_KERNEL_SIZE]
108  add eax, DEF_AREA_LOADER_OFFSET - 4
109  cmp dword [ds:eax], 0x19991205
110  jne .integrity_check_fail
111
112; enter loader
113  jmp DEF_AREA_LOADER_SEGMENT:DEF_AREA_LOADER_OFFSET
114
115.integrity_check_fail:
116  mov bp, MSG_loader_not_loaded
117  mov cx, MSG_loader_not_loaded_len
118  call func_print
119  jmp $
120
121;;;;;;; read sector from boot disk
122;;;; !Required
123;; [fs:DEF_VAR_DAP]
124;; [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
125;;;;;;;
126func_read_sector:
127  push ds
128  push fs
129  pop ds
130  mov esi, DEF_VAR_DAP
131  mov ah, 42h
132  mov dl, byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
133  int 13h
134  pop ds
135  jc .e_func_read_sector
136  ret
137
138.e_func_read_sector:
139  mov bp, MSG_read_sector_e
140  mov cx, MSG_read_sector_e_len
141  call func_print
142  jmp $
143
144
145;;;;;;; print string
146;; AL=01h: Assign all characters the attribute in BL; update cursor
147;; BH=00h: Display page number
148;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
149;; DL=00h: Display Column number
150;;;; !Required
151;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
152;;;; !Input
153;; ES:BP: Points to string to be printed
154;; CX: Number of characters in string
155;;;; !Modified
156;; [fs:DEF_VAR_DISPLAY_LINE]
157;;;;;;;
158func_print:
159  mov ax, 0x1301
160  mov bx, 0x000f
161  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
162  xor dl, dl
163  int 10h
164  inc byte [fs:DEF_VAR_DISPLAY_LINE]
165  ret
166
167
168MSG_start_boot:     db   "start boot"
169MSG_start_boot_len  equ  $ - MSG_start_boot
170
171MSG_read_sector_e:     db   "read sector error"
172MSG_read_sector_e_len  equ  $ - MSG_read_sector_e
173
174MSG_loader_not_found:     db   "loader not found"
175MSG_loader_not_found_len  equ  $ - MSG_loader_not_found
176
177MSG_loader_not_loaded:     db   "loader not loaded"
178MSG_loader_not_loaded_len  equ  $ - MSG_loader_not_loaded
179
180FILE_loader:  db "LOADER  BIN"
181
182  times 510-($-$$) db 0
183  dw 0xAA55

loader添加hlt

在验证之前, 我们首先在loader中加入_hlt, 让程序在进入loader中停止

添加后loader的完整代码应该类似下面

 1%include "def.inc"
 2
 3[bits 16]
 4org 0x10000
 5
 6  jmp short _start_loader
 7  nop
 8_magic_start: dd 0x20000229
 9
10
11_start_loader:
12
13_hlt:
14  hlt
15  jmp short _hlt
16
17[section .magic]
18[bits 16]
19
20_magic_end: dd 0x19991205

通过bochs验证

准备都做好了, 我们可以直接通过make编译并进入bochs

输入c让bochs执行到HLT指令, 然后通过Ctrl-C停止运行

如果一切正常的话, 虚拟机的屏幕上应该只会显示一行 start boot

通过bochs的输出我们可以看出来, 虚拟机停在了1000:0008位置, 段地址是1000, 也就是我们为loader分配的段

100017404921i[BIOS  ] Booting from 0000:7c00
200017483235i[CPU0  ] WARNING: HLT instruction with IF=0!
3^C03360084000i[      ] Ctrl-C detected in signal handler.
4Next at t=3360084006
5(0) [0x000000010008] 1000:0008 (unk. ctxt): jmp .-3  (0x00010007)     ; ebfd
6<bochs:2> 

至此, 我们成功的完成了boot.asm的编写, 并进入了loader

截至目前项目结构

 1$ tree .                           
 2.
 3├── LICENSE
 4├── Makefile
 5├── README.md
 6├── asset
 7│   ├── BIOS-bochs-latest
 8│   ├── VGABIOS-lgpl-latest
 9│   └── bochsrc
10├── bootloader
11│   ├── Makefile
12│   ├── boot.asm
13│   ├── def.inc
14│   ├── fat32.inc
15│   └── loader.asm
16└── tools
17    └── gen_fat32_inc
18        ├── gen_fat32.sh
19        └── gen_fat32_inc.c
20
215 directories, 13 files

截至目前项目代码

03.tgz

除另有声明外本博客文章均采用 知识共享 (Creative Commons) 署名 4.0 国际许可协议 进行许可转载请注明原作者与文章出处