Akvicor
Akvicor
发布于 2025-03-30 / 1 阅读
0
0

AOS开发 03 - Boot

文件: boot.asm

完整项目代码在末尾

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

加载inc文件和jmp

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

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

%include "def.inc"

[bits 16]
org 0x7c00

  jmp short _start_boot
  nop

%include "fat32.inc"


_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)

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

寄存器介绍

通用寄存器

寄存器

名称

主要用途

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段寄存器的目的

; init register
  mov ax, cs
  mov es, ax
  mov ds, ax
  mov ax, DEF_AREA_STACK_SEGMENT
  mov ss, ax
  mov sp, DEF_AREA_STACK_OFFSET
  xor ax, ax
  mov fs, ax

配置bochs

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

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

安装完成后, 我们还需要下载这几个文件 https://fs.ksyaki.com/share/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 只是项目的开源许可和描述文件, 无需在意

$ tree .  
.
├── LICENSE
├── README.md
├── asset
│   ├── BIOS-bochs-latest
│   ├── VGABIOS-lgpl-latest
│   └── bochsrc
├── bootloader
│   ├── boot.asm
│   ├── def.inc
│   └── fat32.inc
└── tools
    └── gen_fat32_inc
        ├── gen_fat32.sh
        └── 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) 是传递过来的参数

BUILD = .
BIN = boot.bin

CD = cd

NASM = /usr/bin/nasm
MAKE = /usr/bin/make

default :

build : $(BUILD)
	$(MAKE) $(BIN)

$(BIN) : %.bin : %.asm
	$(NASM) $< -o $(BUILD)/$@ $(FLAGS)

./Makefile

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

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

变量部分

首先映入眼帘就是一串嵌套 _PATH :=(abspath(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中获取一些信息用于生成版本号

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

FULL_VERSION = AOS $(VERSION) ($(BUILD_TIME)) x86_64

磁盘位置

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

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

build编译

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

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

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

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

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

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

之后将boot写入磁盘

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

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

  • qemu: 先启动一个新终端, 打开gdb工具, 然后运行qemu从指定的磁盘启动

  • bochs: 先删除锁文件, bochs运行时会生成一个 .lock 文件锁定磁盘, 但如果非正常退出bochs会导致这个文件无法自动删除, 进而导致无法再次启动bochs.

qemu :
	$(TERM) $(TERM_FLAGS)
	$(SUDO) $(QEMU) -hda $(DISK) $(QEMU_FLAGS)

bochs :
	-$(SUDO) $(RM) $(DISK).lock
	$(SUDO) $(BOCHS) $(BOCHS_FLAGS)

最后 clean 部分就是清理垃圾

完整Makefile

_PATH := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))

CD = cd
CP = cp
LS = ls
CPDIR = cp -r
RM = /usr/bin/rm
RMDIR = /usr/bin/rm -rf
NASM = /usr/bin/nasm
MAKE = /usr/bin/make
MKDIR = /usr/bin/mkdir
SUDO = /usr/bin/sudo
SLEEP = /usr/bin/sleep
DD = /usr/bin/dd
MOUNT = /usr/bin/mount
UMOUNT = /usr/bin/umount
SYNC = /usr/bin/sync
MKFAT32 = /usr/sbin/mkfs.vfat
NOTIFY = /usr/bin/notify-send
GIT = /usr/bin/git
SED = /usr/bin/sed
DATE = /usr/bin/date

ZERO = /dev/zero

# 编译信息
BRANCH=$(shell $(GIT) rev-parse --abbrev-ref HEAD)
VERSION=$(shell $(GIT) describe --tags --always | $(SED) 's/^v//')
COMMIT=$(shell $(GIT) rev-parse --verify HEAD)
BUILD_TIME=$(shell $(DATE) +"%Y-%m-%d %H:%M:%S %z")

FULL_VERSION = AOS $(VERSION) ($(BUILD_TIME)) x86_64


TERM = /usr/bin/terminator
TERM_FLAGS = -x "gdb build/system -ex=\"target remote :1234\""

BOCHS = /usr/bin/bochs
BOCHS_FLAGS = -debugger -f asset/bochsrc
QEMU = /usr/bin/qemu-system-x86_64
QEMU_FLAGS = -m 128M -smp sockets=1,cores=1,threads=1 -S -s -monitor stdio
#QEMU_FLAGS = -m 1024M -smp sockets=1,cores=4,threads=2 -S -s
#QEMU = /usr/bin/kvm
#QEMU_FLAGS = -m 1024M -cpu host -smp sockets=1,cores=4,threads=2 -S -s

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


default :
	$(MAKE) build
	$(MAKE) bochs
#	$(MAKE) qemu



.PHONY : build
build : $(DISK)
# clean old file
	-$(RMDIR) build
	$(MKDIR) build
# build bootloader
	$(MAKE) -C bootloader build BUILD="$(_PATH)build"
# clean disk
	$(SUDO) $(DD) if=$(ZERO) of=$(DISK) bs=1M count=64 conv=notrunc status=progress
	$(SUDO) $(MKFAT32) $(DISK)
# write boot.bin
	$(SUDO) $(DD) if=$(_PATH)build/boot.bin of=$(DISK) bs=512 count=1 conv=notrunc


qemu :
	$(TERM) $(TERM_FLAGS)
	$(SUDO) $(QEMU) -hda $(DISK) $(QEMU_FLAGS)


bochs :
	-$(SUDO) $(RM) $(DISK).lock
	$(SUDO) $(BOCHS) $(BOCHS_FLAGS)


.PHONY : clean
clean :
	-$(RMDIR) build
	-$(SUDO) $(RM) $(DISK).lock

此时我们的目录结构

$ tree .  
.
├── LICENSE
├── Makefile
├── README.md
├── asset
│   ├── BIOS-bochs-latest
│   ├── VGABIOS-lgpl-latest
│   └── bochsrc
├── bootloader
│   ├── Makefile
│   ├── boot.asm
│   ├── def.inc
│   └── fat32.inc
├── build
│   └── boot.bin
└── tools
    └── gen_fat32_inc
        ├── gen_fat32.sh
        └── gen_fat32_inc.c

此时boot.asm完整代码

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

%include "def.inc"

[bits 16]
org 0x7c00

  jmp short _start_boot
  nop

%include "fat32.inc"

_start_boot:

; init register
  mov ax, cs
  mov es, ax
  mov ds, ax
  mov ax, DEF_AREA_STACK_SEGMENT
  mov ss, ax
  mov sp, DEF_AREA_STACK_OFFSET
  xor ax, ax
  mov fs, ax

_hlt:
  hlt
  jmp short _hlt

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

使用bochs调试

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

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

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

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

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

<bochs:1> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
	Data segment, base=0xffff0000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x0000000000000000, limit=0xffff
idtr: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 , 也就是我们在之前为堆栈分配的段地址

00017404921i[BIOS  ] Booting from 0000:7c00
00017404985i[CPU0  ] WARNING: HLT instruction with IF=0!
^C03057664000i[      ] Ctrl-C detected in signal handler.
Next at t=3057664006
(0) [0x000000007c6d] 0000:7c6d (unk. ctxt): jmp .-3  (0x00007c6c)     ; ebfd
<bochs:3> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ss:0x5000, dh=0x00009305, dl=0x0000ffff, valid=1
	Data segment, base=0x00050000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00000000000f9af7, limit=0x30
idtr:base=0x0000000000000000, limit=0x3ff

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

<bochs:3> reg
CPU0:
rax: 00000000_00000000
rbx: 00000000_00000000
rcx: 00000000_00090000
rdx: 00000000_00000080
rsp: 00000000_00000000
rbp: 00000000_00000000
rsi: 00000000_000e0000
rdi: 00000000_0000ffac
r8 : 00000000_00000000
r9 : 00000000_00000000
r10: 00000000_00000000
r11: 00000000_00000000
r12: 00000000_00000000
r13: 00000000_00000000
r14: 00000000_00000000
r15: 00000000_00000000
rip: 00000000_00007c6d
eflags: 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时用的上

; clean up VAR's memory
  push es
  mov ax, DEF_AREA_VAR_SEGMENT
  mov es, ax
  mov di, DEF_AREA_VAR_OFFSET
  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
  xor ax, ax
  cld
  rep stosb
  pop es

; initialize variable
  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH

清空屏幕

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

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

  • ah=0x00: 表示设置视频模式

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

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

; clean screen
  mov ax, 0x0003
  int 10h

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

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

%include "def.inc"

[bits 16]
org 0x7c00

  jmp short _start_boot
  nop

%include "fat32.inc"

_start_boot:

; init register
  mov ax, cs
  mov es, ax
  mov ds, ax
  mov ax, DEF_AREA_STACK_SEGMENT
  mov ss, ax
  mov sp, DEF_AREA_STACK_OFFSET
  xor ax, ax
  mov fs, ax

; clean up VAR's memory
  push es
  mov ax, DEF_AREA_VAR_SEGMENT
  mov es, ax
  mov di, DEF_AREA_VAR_OFFSET
  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
  xor ax, ax
  cld
  rep stosb
  pop es

; initialize variable
  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH

; clean screen
  mov ax, 0x0003
  int 10h


_hlt:
  hlt
  jmp short _hlt

  times 510-($-$$) db 0
  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

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

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

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

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

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

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

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

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

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

<bochs:5> xp /1bx 0x7EF0
[bochs]:
0x0000000000007ef0 <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 计算得出(当前内存地址-字符串开头的内存地址), 就是字符串的长度

MSG_start_boot:			db	"start boot"
MSG_start_boot_len	equ	$ - MSG_start_boot

MSG_read_sector_e:		db	"read sector error"
MSG_read_sector_e_len	equ	$ - MSG_read_sector_e

MSG_loader_not_found:			db	"loader not found"
MSG_loader_not_found_len	equ	$ - MSG_loader_not_found

MSG_loader_not_loaded:		db	"loader not loaded"
MSG_loader_not_loaded_len	equ	$ - MSG_loader_not_loadedMSG_start_boot:			db	"start boot"
MSG_start_boot_len	equ	$ - MSG_start_boot

MSG_read_sector_e:		db	"read sector error"
MSG_read_sector_e_len	equ	$ - MSG_read_sector_e

MSG_loader_not_found:			db	"loader not found"
MSG_loader_not_found_len	equ	$ - MSG_loader_not_found

MSG_loader_not_loaded:		db	"loader not loaded"
MSG_loader_not_loaded_len	equ	$ - MSG_loader_not_loaded

打印函数封装

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

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

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

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

;;;;;;; print string
;; AL=01h: Assign all characters the attribute in BL; update cursor
;; BH=00h: Display page number
;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
;; DL=00h: Display Column number
;;;; !Required
;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
;;;; !Input
;; ES:BP: Points to string to be printed
;; CX: Number of characters in string
;;;; !Modified
;; [fs:DEF_VAR_DISPLAY_LINE]
;;;;;;;
func_print:
  mov ax, 0x1301
  mov bx, 0x000f
  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
  xor dl, dl
  int 10h
  inc byte [fs:DEF_VAR_DISPLAY_LINE]
  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 代表的值

; msg: start boot
  mov bp, MSG_start_boot
  mov cx, MSG_start_boot_len
  call func_print

完整boot代码

%include "def.inc"

[bits 16]
org 0x7c00

  jmp short _start_boot
  nop

%include "fat32.inc"

_start_boot:

; init register
  mov ax, cs
  mov es, ax
  mov ds, ax
  mov ax, DEF_AREA_STACK_SEGMENT
  mov ss, ax
  mov sp, DEF_AREA_STACK_OFFSET
  xor ax, ax
  mov fs, ax

; clean up VAR's memory
  push es
  mov ax, DEF_AREA_VAR_SEGMENT
  mov es, ax
  mov di, DEF_AREA_VAR_OFFSET
  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
  xor ax, ax
  cld
  rep stosb
  pop es

; initialize variable
  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH

; clean screen
  mov ax, 0x0003
  int 10h

; msg: start boot
  mov bp, MSG_start_boot
  mov cx, MSG_start_boot_len
  call func_print


_hlt:
  hlt
  jmp short _hlt

;;;;;;; print string
;; AL=01h: Assign all characters the attribute in BL; update cursor
;; BH=00h: Display page number
;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
;; DL=00h: Display Column number
;;;; !Required
;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
;;;; !Input
;; ES:BP: Points to string to be printed
;; CX: Number of characters in string
;;;; !Modified
;; [fs:DEF_VAR_DISPLAY_LINE]
;;;;;;;
func_print:
  mov ax, 0x1301
  mov bx, 0x000f
  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
  xor dl, dl
  int 10h
  inc byte [fs:DEF_VAR_DISPLAY_LINE]
  ret


MSG_start_boot:     db   "start boot"
MSG_start_boot_len  equ  $ - MSG_start_boot

MSG_read_sector_e:     db   "read sector error"
MSG_read_sector_e_len  equ  $ - MSG_read_sector_e

MSG_loader_not_found:     db   "loader not found"
MSG_loader_not_found_len  equ  $ - MSG_loader_not_found

MSG_loader_not_loaded:     db   "loader not loaded"
MSG_loader_not_loaded_len  equ  $ - MSG_loader_not_loaded

  times 510-($-$$) db 0
  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 " )

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

;;;;;;; read sector from boot disk
;;;; !Required
;; [fs:DEF_VAR_DAP]
;; [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
;;;;;;;
func_read_sector:
  push ds
  push fs
  pop ds
  mov esi, DEF_VAR_DAP
  mov ah, 42h
  mov dl, byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
  int 13h
  pop ds
  jc .e_func_read_sector

  ret

.e_func_read_sector:
  mov bp, MSG_read_sector_e
  mov cx, MSG_read_sector_e_len
  call func_print
  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方法, 跳转到读取磁盘的函数了.

; read 4 KiB of FAT32 Root Entry
  mov word [fs:DEF_VAR_DAP_TOTAL], 0x08 ; 0x08 * 0x200 = 0x1000 bytes
  mov dword [fs:DEF_VAR_DAP_LBA_LOW], FAT32_ROOT_REGION
  mov word [fs:DEF_VAR_DAP_SEGMENT], DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT
  mov word [fs:DEF_VAR_DAP_OFFSET], DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
  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

%include "def.inc"

[bits 16]
org 0x10000

  jmp short _start_loader
  nop
_magic_start: dd 0x20000229


_start_loader:

[section .magic]
[bits 16]

_magic_end: dd 0x19991205

bootloader/Makefile

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

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

BUILD = .
BIN = boot.bin loader.bin

CD = cd

NASM = /usr/bin/nasm
MAKE = /usr/bin/make

default :

build : $(BUILD)
	$(MAKE) $(BIN)

$(BIN) : %.bin : %.asm
	$(NASM) $< -o $(BUILD)/$@ $(FLAGS)

Makefile

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

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

.PHONY : build
build : $(DISK)
# clean old file
	-$(RMDIR) build
	$(MKDIR) build
# build bootloader
	$(MAKE) -C bootloader build BUILD="$(_PATH)build"
# clean disk
	$(SUDO) $(DD) if=$(ZERO) of=$(DISK) bs=1M count=64 conv=notrunc status=progress
	$(SUDO) $(MKFAT32) $(DISK)
# write boot.bin
	$(SUDO) $(DD) if=$(_PATH)build/boot.bin of=$(DISK) bs=512 count=1 conv=notrunc
# mount disk
	-$(MKDIR) mount
	$(SUDO) $(MOUNT) $(DISK) mount
# copy file
	$(SUDO) $(CP) build/loader.bin mount
# umount disk
	$(SUDO) $(UMOUNT) $(DISK)
	$(NOTIFY) "AOS build finished"

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

.PHONY : clean
clean :
	-$(SUDO) $(UMOUNT) mount
	-$(RMDIR) build
	-$(RMDIR) mount
	-$(SUDO) $(RM) $(DISK).lock

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

再次确认boot.asm中的代码

%include "def.inc"

[bits 16]
org 0x7c00

  jmp short _start_boot
  nop

%include "fat32.inc"

_start_boot:

; init register
  mov ax, cs
  mov es, ax
  mov ds, ax
  mov ax, DEF_AREA_STACK_SEGMENT
  mov ss, ax
  mov sp, DEF_AREA_STACK_OFFSET
  xor ax, ax
  mov fs, ax

; clean up VAR's memory
  push es
  mov ax, DEF_AREA_VAR_SEGMENT
  mov es, ax
  mov di, DEF_AREA_VAR_OFFSET
  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
  xor ax, ax
  cld
  rep stosb
  pop es

; initialize variable
  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH

; clean screen
  mov ax, 0x0003
  int 10h

; msg: start boot
  mov bp, MSG_start_boot
  mov cx, MSG_start_boot_len
  call func_print

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


_hlt:
  hlt
  jmp short _hlt


;;;;;;; read sector from boot disk
;;;; !Required
;; [fs:DEF_VAR_DAP]
;; [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
;;;;;;;
func_read_sector:
  push ds
  push fs
  pop ds
  mov esi, DEF_VAR_DAP
  mov ah, 42h
  mov dl, byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
  int 13h
  pop ds
  jc .e_func_read_sector
  ret

.e_func_read_sector:
  mov bp, MSG_read_sector_e
  mov cx, MSG_read_sector_e_len
  call func_print
  jmp $


;;;;;;; print string
;; AL=01h: Assign all characters the attribute in BL; update cursor
;; BH=00h: Display page number
;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
;; DL=00h: Display Column number
;;;; !Required
;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
;;;; !Input
;; ES:BP: Points to string to be printed
;; CX: Number of characters in string
;;;; !Modified
;; [fs:DEF_VAR_DISPLAY_LINE]
;;;;;;;
func_print:
  mov ax, 0x1301
  mov bx, 0x000f
  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
  xor dl, dl
  int 10h
  inc byte [fs:DEF_VAR_DISPLAY_LINE]
  ret


MSG_start_boot:     db   "start boot"
MSG_start_boot_len  equ  $ - MSG_start_boot

MSG_read_sector_e:     db   "read sector error"
MSG_read_sector_e_len  equ  $ - MSG_read_sector_e

MSG_loader_not_found:     db   "loader not found"
MSG_loader_not_found_len  equ  $ - MSG_loader_not_found

MSG_loader_not_loaded:     db   "loader not loaded"
MSG_loader_not_loaded_len  equ  $ - MSG_loader_not_loaded

FILE_loader:  db "LOADER  BIN"

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

bochs测试

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

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

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

---------- 0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F -------------------
00ee4000  41 6c 00 6f 00 61 00 64  00 65 00 0f 00 ab 72 00  |Al.o.a.d.e....r.|
00ee4010  2e 00 62 00 69 00 6e 00  00 00 00 00 ff ff ff ff  |..b.i.n.........|
00ee4020  4c 4f 41 44 45 52 20 20  42 49 4e 20 00 7b dd 8b  |LOADER  BIN .{..|
00ee4030  82 5a 82 5a 00 00 dd 8b  82 5a 03 00 0c 00 00 00  |.Z.Z.....Z......|
00ee4040  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. 可以看到数据已经被正确加载入内存中了

<bochs:6> xp /64bx 0x8000 
[bochs]:
0x0000000000008000 <bogus+       0>:	0x41	0x6c	0x00	0x6f	0x00	0x61	0x00	0x64
0x0000000000008008 <bogus+       8>:	0x00	0x65	0x00	0x0f	0x00	0xab	0x72	0x00
0x0000000000008010 <bogus+      16>:	0x2e	0x00	0x62	0x00	0x69	0x00	0x6e	0x00
0x0000000000008018 <bogus+      24>:	0x00	0x00	0x00	0x00	0xff	0xff	0xff	0xff
0x0000000000008020 <bogus+      32>:	0x4c	0x4f	0x41	0x44	0x45	0x52	0x20	0x20
0x0000000000008028 <bogus+      40>:	0x42	0x49	0x4e	0x20	0x00	0x7b	0xdd	0x8b
0x0000000000008030 <bogus+      48>:	0x82	0x5a	0x82	0x5a	0x00	0x00	0xdd	0x8b
0x0000000000008038 <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个条目

; search loader.bin
  push es ; 保存原始es寄存器数据
  push word DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT ; 将Root Directory的内存地址压栈, 便于赋值给es寄存器
  pop es ; 将Root Directory的内存地址出栈, 放入es寄存器
  mov edi, DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
  mov esi, FILE_loader
  mov ecx, 11
  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 寄存器恢复

.loop_search_loader:

  pushad ; protect ecx, edi, esi
  repe cmpsb
  popad
  je .loader_found

  add edi, 32
  dec edx
  jnz .loop_search_loader

  pop es
  mov bp, MSG_loader_not_found
  mov cx, MSG_loader_not_found_len
  call func_print
  jmp $

.loader_found:

  mov eax, dword [es:edi+0x1c]
  mov dword [fs:DEF_VAR_KERNEL_SIZE], eax
  push word [es:edi+0x14]
  push word [es:edi+0x1a]
  pop eax
  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 就可以读取完成了

; read FAT32_SECTORS_PER_CLUSTER KiB of loader.bin
  sub eax, 2
  mov edx, FAT32_SECTORS_PER_CLUSTER
  mul edx
  add eax, FAT32_ROOT_REGION
  mov dword [fs:DEF_VAR_DAP_LBA_LOW], eax
  mov word [fs:DEF_VAR_DAP_TOTAL], FAT32_SECTORS_PER_CLUSTER ; 0x08 * 0x200 = 0x1000 bytes
  mov word [fs:DEF_VAR_DAP_SEGMENT], DEF_AREA_LOADER_SEGMENT
  mov word [fs:DEF_VAR_DAP_OFFSET], DEF_AREA_LOADER_OFFSET
  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

; integrity checking
  mov ax, DEF_AREA_LOADER_SEGMENT
  mov ds, ax
  cmp dword [ds:DEF_AREA_LOADER_OFFSET+0x03], 0x20000229
  jne .integrity_check_fail
  mov eax, dword [fs:DEF_VAR_KERNEL_SIZE]
  add eax, DEF_AREA_LOADER_OFFSET - 4
  cmp dword [ds:eax], 0x19991205
  jne .integrity_check_fail

; enter loader
  jmp DEF_AREA_LOADER_SEGMENT:DEF_AREA_LOADER_OFFSET
  
.integrity_check_fail:
  mov bp, MSG_loader_not_loaded
  mov cx, MSG_loader_not_loaded_len
  call func_print
  jmp $

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

完整的boot.asm代码

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

%include "def.inc"

[bits 16]
org 0x7c00

  jmp short _start_boot
  nop

%include "fat32.inc"

_start_boot:

; init register
  mov ax, cs
  mov es, ax
  mov ds, ax
  mov ax, DEF_AREA_STACK_SEGMENT
  mov ss, ax
  mov sp, DEF_AREA_STACK_OFFSET
  xor ax, ax
  mov fs, ax

; clean up VAR's memory
  push es
  mov ax, DEF_AREA_VAR_SEGMENT
  mov es, ax
  mov di, DEF_AREA_VAR_OFFSET
  mov cx, DEF_AREA_VAR_END - DEF_AREA_VAR_START
  xor ax, ax
  cld
  rep stosb
  pop es

; initialize variable
  mov byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER], dl
  mov byte [fs:DEF_VAR_DISPLAY_LINE], al
  mov word [fs:DEF_VAR_VBE_MODE], DEF_VBE_MODE
  mov byte [fs:DEF_VAR_DAP_SIZE], DEF_VAR_DAP_LENGTH

; clean screen
  mov ax, 0x0003
  int 10h

; msg: start boot
  mov bp, MSG_start_boot
  mov cx, MSG_start_boot_len
  call func_print

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

; search loader.bin
  push es
  push word DEF_AREA_FAT32_ROOT_ENTRY_SEGMENT
  pop es
  mov edi, DEF_AREA_FAT32_ROOT_ENTRY_OFFSET
  mov esi, FILE_loader
  mov ecx, 11
  mov edx, 0x1000 >> 5 ; 32 bit per entries

.loop_search_loader:

  pushad ; protect ecx, edi, esi
  repe cmpsb
  popad
  je .loader_found

  add edi, 32
  dec edx
  jnz .loop_search_loader

  pop es
  mov bp, MSG_loader_not_found
  mov cx, MSG_loader_not_found_len
  call func_print
  jmp $

.loader_found:

  mov eax, dword [es:edi+0x1c]
  mov dword [fs:DEF_VAR_KERNEL_SIZE], eax
  push word [es:edi+0x14]
  push word [es:edi+0x1a]
  pop eax
  pop es

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

; integrity checking
  mov ax, DEF_AREA_LOADER_SEGMENT
  mov ds, ax
  cmp dword [ds:DEF_AREA_LOADER_OFFSET+0x03], 0x20000229
  jne .integrity_check_fail
  mov eax, dword [fs:DEF_VAR_KERNEL_SIZE]
  add eax, DEF_AREA_LOADER_OFFSET - 4
  cmp dword [ds:eax], 0x19991205
  jne .integrity_check_fail

; enter loader
  jmp DEF_AREA_LOADER_SEGMENT:DEF_AREA_LOADER_OFFSET

.integrity_check_fail:
  mov bp, MSG_loader_not_loaded
  mov cx, MSG_loader_not_loaded_len
  call func_print
  jmp $

;;;;;;; read sector from boot disk
;;;; !Required
;; [fs:DEF_VAR_DAP]
;; [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
;;;;;;;
func_read_sector:
  push ds
  push fs
  pop ds
  mov esi, DEF_VAR_DAP
  mov ah, 42h
  mov dl, byte [fs:DEF_VAR_BOOT_DRIVE_NUMBER]
  int 13h
  pop ds
  jc .e_func_read_sector
  ret

.e_func_read_sector:
  mov bp, MSG_read_sector_e
  mov cx, MSG_read_sector_e_len
  call func_print
  jmp $


;;;;;;; print string
;; AL=01h: Assign all characters the attribute in BL; update cursor
;; BH=00h: Display page number
;; BL=0Fh: Attribute: font: light white; background: black; disable blinking
;; DL=00h: Display Column number
;;;; !Required
;; DH=[fs:DEF_VAR_DISPLAY_LINE]: Row
;;;; !Input
;; ES:BP: Points to string to be printed
;; CX: Number of characters in string
;;;; !Modified
;; [fs:DEF_VAR_DISPLAY_LINE]
;;;;;;;
func_print:
  mov ax, 0x1301
  mov bx, 0x000f
  mov dh, byte [fs:DEF_VAR_DISPLAY_LINE]
  xor dl, dl
  int 10h
  inc byte [fs:DEF_VAR_DISPLAY_LINE]
  ret


MSG_start_boot:     db   "start boot"
MSG_start_boot_len  equ  $ - MSG_start_boot

MSG_read_sector_e:     db   "read sector error"
MSG_read_sector_e_len  equ  $ - MSG_read_sector_e

MSG_loader_not_found:     db   "loader not found"
MSG_loader_not_found_len  equ  $ - MSG_loader_not_found

MSG_loader_not_loaded:     db   "loader not loaded"
MSG_loader_not_loaded_len  equ  $ - MSG_loader_not_loaded

FILE_loader:  db "LOADER  BIN"

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

loader添加hlt

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

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

%include "def.inc"

[bits 16]
org 0x10000

  jmp short _start_loader
  nop
_magic_start: dd 0x20000229


_start_loader:

_hlt:
  hlt
  jmp short _hlt

[section .magic]
[bits 16]

_magic_end: dd 0x19991205

通过bochs验证

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

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

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

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

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

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

截至目前项目结构

$ tree .                           
.
├── LICENSE
├── Makefile
├── README.md
├── asset
│   ├── BIOS-bochs-latest
│   ├── VGABIOS-lgpl-latest
│   └── bochsrc
├── bootloader
│   ├── Makefile
│   ├── boot.asm
│   ├── def.inc
│   ├── fat32.inc
│   └── loader.asm
└── tools
    └── gen_fat32_inc
        ├── gen_fat32.sh
        └── gen_fat32_inc.c

5 directories, 13 files

截至目前项目代码下载 03.tgz


评论