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

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

加载inc文件和jmp

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

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

nasm ▽
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)

nasm ▽
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段寄存器的目的
nasm ▽
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只是项目的开源许可和描述文件, 无需在意

text ▽
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)是传递过来的参数
makefile ▽
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中获取一些信息用于生成版本号

makefile ▽
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

磁盘位置

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

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

build编译

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

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

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

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

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

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

之后将boot写入磁盘

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

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

  • qemu: 先启动一个新终端, 打开gdb工具, 然后运行qemu从指定的磁盘启动
  • bochs: 先删除锁文件, bochs运行时会生成一个.lock文件锁定磁盘, 但如果非正常退出bochs会导致这个文件无法自动删除, 进而导致无法再次启动bochs.
makefile ▽
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

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

此时我们的目录结构

text ▽
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

nasm ▽
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的调试, 应该会停在类似下面这个输出, 然后会等待你输入命令

text ▽
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

text ▽
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, 也就是我们在之前为堆栈分配的段地址

text ▽
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, 这就说明我们的堆栈相关的两个寄存器已经正确配置

text ▽
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时用的上
nasm ▽
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文本模式

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

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

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

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

nasm ▽
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

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

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

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

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

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

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

fallback ▽
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, 符合之前的配置

fallback ▽
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计算得出(当前内存地址-字符串开头的内存地址), 就是字符串的长度

nasm ▽
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可以返回到当前位置继续向下执行

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

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

nasm ▽
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代表的值
nasm ▽
1; msg: start boot 2 mov bp, MSG_start_boot 3 mov cx, MSG_start_boot_len 4 call func_print

完整boot代码

nasm ▽
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 ")

nasm ▽
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 $进入死循环, 防止程序乱跑

nasm ▽
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方法, 跳转到读取磁盘的函数了.

nasm ▽
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

nasm ▽
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就已经彻底编写完成, 不需要再修改了. 完整内容如下:

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的功能, 修改后的代码如下:

makefile ▽
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:部分需要做一些额外的清理, 例如删除挂载目录

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

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

再次确认boot.asm中的代码

nasm ▽
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 查看

text ▽
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. 可以看到数据已经被正确加载入内存中了

text ▽
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个条目
nasm ▽
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寄存器恢复

nasm ▽
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就可以读取完成了

nasm ▽
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

nasm ▽
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代码, 方便出错时对比

nasm ▽
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的完整代码应该类似下面

nasm ▽
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分配的段

gdscript3 ▽
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

截至目前项目结构

text ▽
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 国际许可协议 进行许可转载请注明原作者与文章出处