真正一小步,编写属于你自己的X86 Boot loader
前言
笔者一直对操作系统非常感兴趣,一直希望能够编写一款属于自己的操作系统。不过各种事情让我一直抽不开身。现在大四了,终于有时间好好搞一搞自己喜欢的东西。
经过深思熟虑,我决定还是从Boot Loader做起。国内各种网站和博客关于这方面内容的东西不是特别多,我在编写Boot Loader的时候也是摸着石头过河。国内大部分博客所写的,如”编写一个最简单的操作系统“,”编写一个基本的Boot Loader“之类的内容绝大多数都只是完成了一个Boot sector,在裸机上啪啪打出一串字符就完了。
对于一个Boot Loader, 它的基本功能肯定是要从硬盘上读取出操作系统内核(或是用户程序),再将其搬运到内存中,最后跳转到操作系统内核。
本文中的Boot Loader是一个最简单的Boot Loader,能够从硬盘中读取到用户程序并将其放到内存中指定的位置。
参考资料
- 《x86汇编语言:从实模式到保护模式》:非常好的一本书,详细地介绍了X86汇编语言。在编写一个Boot loader之前所需了解的知识,通过阅读这本书,大家基本都能够学习到。
- OSDev : 完整翔实地介绍操作系统各种相关技术的网站,包括Boot loader。
环境和工具
- Linux : 最好是Linux, 在Windows下也可以完成,不过在Linux下使用各种工具会非常方便。
- Bochs :一个非常优秀的X86计算机模拟器。它有着非常强大的调试功能。
- NASM : 汇编语言。
- dd :Linux下自带的文件复制工具
工具安装
我是在Ubuntu 下进行的开发:
- Bochs:
sudo apt-get install bochs
sudo apt-get install bochs-x
- NASM:
sudo apt-get install nasm
编写Boot Loader
在完成Boot Loader之前要大家要了解基本的X86汇编语言,还要了解计算机启动的基本过程等知识。在《x86汇编语言:从实模式到保护模式》中都有对相关知识非常详细的介绍,网上也有电子书可以下载。
这里直接贴写好的代码:
mbr.asm:
;mbr程序,加载用户程序
;日期:2020年1月5日
LOADER_LBA_START equ 1 ; 用户程序在硬盘的扇区
SECTION mbr align=16 vstart=0x7c00
;读取硬盘中的loader
xor ax, ax ;ax寄存器清零
mov ss, ax ;堆栈段
mov sp, ax ;栈顶指针
mov ax, LOADER_LBA_START ;低16位LBA地址
push ax
xor ax, ax; 清零
push ax ; 高16位地址
push ax; 偏移地址为0
mov ax, [boot_loader_memory_start_base_address]
push ax
call readDataFromHDD
; 为了能够验证是否读取了硬盘中的数据
mov ds, ax
xor bx, bx
add bx, 4
mov cx, [bx]
jmp $
readDataFromHDD:
; 输入参数1 LBA地址低16位
; 输入参数2 LBA地址高16位
; 输入参数3 目的起始位置的偏移地址
; 输入参数4 目的起始位置的段基地址
push ax
push bx
push cx
push dx
push ds
mov dx, 0x1f2 ;硬盘控制命令字
mov al, 1 ; 读取的扇区数目
out dx, al
; 取出参数
; 从栈中取出LBA地址低16位
mov bp, sp
add bp, 18
mov ax, [bp]
inc dx ; 端口 0x1f3
out dx, al
inc dx ; 端口自增 0x1f4
mov al, ah ; 输出只能用al
out dx, al
; 从栈中取出LBA地址高16位
sub bp, 2
mov ax, [bp]
; 输入高16位地址
inc dx ; 0x1f5
out dx, al
inc dx ; 0x1f6
mov al, 0xe0 ; LBA28模式,主盘
or al, ah
out dx, al
inc dx ;0x1f7
mov al, 0x20 ; 读扇区控制命令字
out dx, al
;=========================================
; 等待硬盘准备阶段
.wait:
in al, dx ; 读取硬盘是否忙碌状态
and al, 0x88 ; 只关心这几位
cmp al, 0x08 ; 比较忙碌标志位是否忙碌
jnz .wait ; 忙碌则跳转
;=========================================
; 读取数据阶段
mov cx, 256 ; 256个字
mov dx, 0x1f0 ; 硬盘的输出端口
; 从栈中获取目标位置的偏移地址
sub bp, 2
mov bx, [bp] ; 偏移地址保存到基址寄存器中
; 从栈中读取目标位置的段基地址
sub bp, 2
mov ax, [bp]
mov ds, ax
.readw:
; 读取字
in ax, dx
mov [bx], ax
add bx, 2
loop .readw
pop ds
pop dx
pop cx
pop bx
pop ax
ret 5
;boot loader 加载到内存的位置
boot_loader_memory_start_base_address dw 0x1000 ; 将用户程序加载到内存的段基地址
times 510-($-$$) db 0
db 0x55, 0xaa
这里我需要对我的程序进行一些说明:
该程序在开机上电后会被BIOS搬运到内存的0x0000:0x7c00处,并开始执行。整个程序只定义了一个段,叫mbr。
程序读取硬盘的方式是LBA 28,具体的运行机制大家可以查阅其他资料了解。
第24-28行代码只是为了验证我个人的用户程序是否被load而被编写。大家可以根据自己编写的用户程序编写其他代码。
readDataFromHDD只会读取一个扇区的数据,大家对其进行适当修改也可以编写出能够读取多个扇区的代码。
另外还要说明的是,程序第4行申明的 LOADER_LBA_START 变量代表的是大家自己编写的用户程序所在的硬盘扇区号(扇区号从0号开始,这里我们将Boot Loader放到0号扇区,用户程序放到1号扇区)。
boot_loader_memory_start_base_address, 是我定义的用户程序搬运到内存位置的段基地址。大家可以自己定义,注意不要将用户程序放到在已经被BIOS、外设等占用的内存区域。
编写用户程序
因为我将主要精力放在Boot loader的编写上了,所以我写了一个最简单的用户程序——仅仅包含了一串数字,用于验证我们写的Boot loader功能是否正确:
data.asm
jmp start
data db 1, 2, 3, 4, 5, 6
start:
jmp $
运行
汇编
nasm mbr.asm -o mbr.bin
nasm data.asm -o data.bin
创建硬盘镜像
绝大多数博客介绍的都是如何创建软盘镜像。但是我们这里并不能创建软盘映像,我们写的Boot Loader应该被放在硬盘主引导扇区(0号扇区),用户程序也应该被放在硬盘当中。因此,我们需要创建一个硬盘映像并且将程序写入该硬盘映像当中。
首先在终端使用bochs自带的 bximage, 输入:
bximage
会进入到程序当中:
接下来bximage会询问生成磁盘镜像类型、大小等信息,我们一路按回车就好。
最后,软件提示磁盘镜像创建成功:
请记住最后一段话,编写配置文件的时候会用到它。
编写Bochs配置文件bochsrc
我们要在配置文件里面告诉bochs,我们的计算机需要从硬盘启动,我们硬盘映像的位置等等。
我编写了一个bochsrc供大家参考:
bochsrc
romimage: file=/usr/share/bochs/BIOS-bochs-latest
megs: 32
vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest
ata0-master: type=disk, path="disk.img", cylinders=20, heads=16, spt=63
boot: disk
log: bochsout.txt
mouse: enabled=0
第4行 “ata0-master” 一段便是设置你的硬盘映像。这句话直接填写bximage生成硬盘映像后,反馈给你的那段话。第5行“boot:” 即启动方式,我们填写 “disk” 代表从硬盘启动。
将Boot loader和用户程序写入磁盘映像
dd if=mbr.bin of=disk.img bs=512 count=1 conv=notrunc
dd if=data.bin of=disk.img bs=512 seek=1 count=1 conv=notrunc
“if” 是我们汇编生成的二进制文件,“of”是之前生成的磁盘映像文件。“bs=512”代表块的大小是512字节,“count”为写入扇区的数量。 “conv=notrunc” 属性一定要加上,这样dd就只会覆盖已经创建的硬盘映像中对应的扇区,而不会重新再创建。
“seek”参数代表的是dd将汇编生成的二进制文件写入到“of”时,dd从0号扇区起需要跳过的扇区数目。因为我们要将用户程序写入到硬盘的1号扇区(mbr.asm 中LOADER_LBA_START的值为1),所以在这里“seek=1”。
Bochs运行和调试
在终端中输入
bochs -f bochsrc
运行bochs模拟器,输入
b 0x7c00
c
以在Boot loader程序的第一条指令(0x7c00)处打断点,并且执行到该断点处。接下来进行调试,类似于GDB,“s” 是单步执行,“n” 是逐行执行,“c"是继续执行。
执行到mbr.asm的第28行,即程序最后一条指令, “mov cx, [4]”。在这里该指令的作用是将用户程序所在内存,偏移地址为0x4的数据赋值给cx寄存器。
在Bochs的CLI中输入“r”,查看寄存器的情况。
可以看到,cx寄存器(16位)的值为0x0403(低8位是3, 高8位是4),正好就是我们用户程序那一串数字中的“3”和“4”。说明我们自己写的Boot loader程序成功地读取了硬盘后,将用户程序搬运到了内存的指定位置。
当然,大家还可以编写更加复杂的用户程序。只需要将用户程序拷贝到内存后,在Boot loader 中执行跳转指令,跳转过去即可执行大家自己所编写的用户程序。
来源:CSDN
作者:aabbfan
链接:https://blog.csdn.net/aabbfan/article/details/103862010