使用汇编语言编写一个简单的X86 Boot loader

耗尽温柔 提交于 2020-01-11 01:57:52

前言

笔者一直对操作系统非常感兴趣,一直希望能够编写一款属于自己的操作系统。不过各种事情让我一直抽不开身。现在大四了,终于有时间好好搞一搞自己喜欢的东西。

经过深思熟虑,我决定还是从Boot Loader做起。国内各种网站和博客关于这方面内容的东西不是特别多,我在编写Boot Loader的时候也是摸着石头过河。国内大部分博客所写的,如”编写一个最简单的操作系统“,”编写一个基本的Boot Loader“之类的内容绝大多数都只是完成了一个Boot sector,在裸机上啪啪打出一串字符就完了。
对于一个Boot Loader, 它的基本功能肯定是要从硬盘上读取出操作系统内核(或是用户程序),再将其搬运到内存中,最后跳转到操作系统内核。

本文中的Boot Loader是一个最简单的Boot Loader,能够从硬盘中读取到用户程序并将其放到内存中指定的位置。

参考资料

  1. 《x86汇编语言:从实模式到保护模式》:非常好的一本书,详细地介绍了X86汇编语言。在编写一个Boot loader之前所需了解的知识,通过阅读这本书,大家基本都能够学习到。
  2. OSDev : 完整翔实地介绍操作系统各种相关技术的网站,包括Boot loader。

环境和工具

  1. Linux : 最好是Linux, 在Windows下也可以完成,不过在Linux下使用各种工具会非常方便。
  2. Bochs :一个非常优秀的X86计算机模拟器。它有着非常强大的调试功能。
  3. NASM : 汇编语言。
  4. dd :Linux下自带的文件复制工具

工具安装

我是在Ubuntu 下进行的开发:

  1. Bochs:
sudo apt-get install bochs
sudo apt-get install bochs-x
  1. 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 中执行跳转指令,跳转过去即可执行大家自己所编写的用户程序。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!