- 文件:
boot.asm
- 完整项目代码在末尾
从上篇可知, 留给我们的代码部分只有420字节. 而boot代码需要完成的任务也很简单, 只需要从磁盘加载loader到内存中, 并跳转到loader执行
加载inc文件和jmp
在之前我们已经完成了两个文件的设计def.inc
和fat32.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_SEGMENT
和DEF_AREA_STACK_OFFSET
就是我们在def.inc
中为堆栈分配的空间地址
在这段代码中你可以看到
第一步没有也可以, 因为一般cs都是0
- 将cs段寄存器中的值赋值给了ax寄存器, 一般这个时候cs寄存器中的值都是0
- 然后将ax的值赋值给es, ds段寄存器, 也就是说现在cs, es, ds寄存器中的值都一样了
第二步是必须的, 保证堆栈在我们预期的位置
- 将之前分配的堆栈空间的段地址存入ax
- 将ax中的值赋值给ss, 也就是堆栈段寄存器(注意无法直接给任何段寄存器直接赋值, 必须通过通用寄存器间接赋值)
- 将之前分配的堆栈空间在段地址下的偏移存入sp, 也就是堆栈指针
最后一个步也是必须的, 保证fs段寄存器为0
- xor就是异或, 自身异或自身, 就会变成0. (当然你也可以
mov ax, 0
,但是这条指令编译出来占3个字节, 而通过xor ax, ax
只会占用2个字节, 省出来1个字节, 同时执行速度也会更快一些) - 然后就是将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
到这一步, 我们实际已经为ss
和sp
寄存器赋值了自己想要的数值, 那么怎么来验证呢?
那么就要引入我们用到的其中一个工具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中详细说明
整理文件结构
现在各种文件已经开始多起来了, 我们就初步设计下整个项目的目录结构吧
下面是笔者目前的目录结构, LICENSE
和README.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
, 执行到htl
后Ctrl-C
停止运行. 此时我们发现bochs的显示器窗口已经一片黑了, 只有左上角有个光标, 表示清除屏幕已经成功
bochs内存查看方法
我们知道内存地址分为物理地址和线性地址. 物理地址表示绝对的,实际存在的,物理内存芯片特定位置的地址. 线性地址是经过地址转换机制后,映射出来的地址
在bochs中通过下面两个命令查看, 注意/
后面的nuf
和addr
有各自的含义, 需要自行替换
- 物理地址通过
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
, 通过xor
将dl
置零, 表示最左侧那一列
然后就是调用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寄存器的数值.
然后利用push
后pop
到另一个寄存器的方式将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
标志位控制, 默认情况下每次比较操作后si
和di
都会增加1
比较字符串最简单的方法就是使用smpsb
来比较单个字符, 然后使用repe
来循环执行, 通过cx
来控制循环次数, 每次repe
后cx
都会减少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的大小. 然后我们按照小端序分别将0x14
和0x1a
这两个位置的各两个字节压栈, 这样就会组成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
: 寄存器内的值减2mul
: 将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
截至目前项目代码
除另有声明外,本博客文章均采用 知识共享 (Creative Commons) 署名 4.0 国际许可协议 进行许可。转载请注明原作者与文章出处。