本系列文章将一步步实现一个简单的操作系统。实验环境是在Linux系统下通过Bochs虚拟机运行我们自己写的操作系统。

一、实验环境搭建

1. Ubuntu的安装,Windows用户可以选择在虚拟机中安装Ubuntu,具体安装教程可自行搜索。

2. Bochs虚拟机的安装

在学习编写操作系统的过程中,我们需要一个虚拟机来模拟出一个虚拟的计算机硬件环境,比如cpu、内存、硬盘等,并且能够运行并且调试我们写的代码。Bochs很好的提供了以上所有功能,在它面前我们就像上帝一样随时可以让时间停止,”钻“到计算机内部,查看这个虚拟电脑的一切信息,这正是开发操作系统所需要的。

Ubuntu下我们可以直接运行以下命令以源码方式安装Bochs(注意我们之所以选择以源码方式安装,是因为通过apt install安装的Bochs是没有调试功能的)

$ sudo apt update
$ sudo apt install build-essential libx11-dev xorg-dev libgtk2.0-dev
$ wget https://sourceforge.net/projects/bochs/files/bochs/2.7/bochs-2.7.tar.gz
$ tar zxvf bochs-2.7.tar.gz
$ cd bochs-2.7/
$ ./configure --enable-debugger --enable-disasm --enable-debugger-gui
$ make 
$ sudo make install

./configure后面的参数便是打开调试功能的开关

到此实验环境搭建完毕。

二、简单的引导扇区汇编代码

先简单讲一下计算机的启动流程,详细启动过程可参考我的另一篇博客 操作系统启动过程

按下开机键后,计算机首先会运行BIOS中的代码,BIOS在进行硬件检查和初始化后,会按照设置好的启动顺序(我们在使用U盘安装系统时,经常要进入BIOS设置这个启动顺序),依次寻找启动设备(比如硬盘、U盘等)。然后将第一个可用的启动设备的第一个扇区载入内存0x7c00处,并把执行权限交给它。

启动设备的第一个扇区我们称之为引导扇区(MBR),共512个字节,必须以数值0x55及0xaa结尾。包括三部分内容:引导加载程序(Boot Loader)(前446个字节,如GRUB等)、磁盘分区表(DPT,Disk Partition Table)、分区有效性标志(55AA)。其中的引导加载程序负责加载启动硬盘分区中的操作系统。

在BIOS向引导程序移交执行权之前,BIOS会对处理器进行初始化,这其中就包括处理器的代码段寄存器CS和指令指针寄存器IP。当BIOS跳转至引导程序时,CS和IP的值分别为0x0000和0x7c00。此时的处理器处于实模式下,物理地址必须经过CS寄存器和IP寄存器转换才能得到。转换公式为:物理地址=CS<<4+IP,也就是物理地址0x7c00处。

BIOS由Bochs虚拟机提供,我们接下来写的就是这个512字节的引导扇区(MBR)的汇编代码。目前它并不用加载操作系统,我们只让它在屏幕上打印出经典的“hello world”即可。

首先看一下汇编代码:

	org 0x07c00
 	mov ax,cs
	mov ds,ax
	mov es,ax
	mov ax,Message
	mov bp,ax
	mov cx, 13
	mov ax,0x1301
	mov bx,0x0002
	mov dh,0
    mov dl,0
	int 0x10
	jmp $
Message: 
	db "Hello, world!"
    times 510-($-$$) db 0
    dw  0xaa55

代码和数据是按汇编程序的编写顺序依次连续存放到内存的,即上面的程序在0x7c00处开始存放的是org 0x07c00的机器指令,在512字节最后放的是0xaa55数据。

BIOS程序在把引导程序加载到内存时,同时还创建了中断系统,在物理内存的前1KB空间初始化中断向量表,在物理内存最后256KB物理地址空间内保存中断处理程序。cpu运行完BIOS后,物理内存的布局如下:

本程序就是调用0x10号中断,在屏幕上打印字符串。在调用0x10号中断处理程序往显示器的屏幕上打印字符串时,所有的参数都是通过cpu中的寄存器传递的,各参数的含义如下:

  • 寄存器ah:0x13表示向屏幕打印字符串;
  • 寄存器al:指定光标和字符的属性
    0,表示字符的属性值保存在寄存器bl中,光标停留在字符串的首字符
    1,表示字符的属性值保存在寄存器bl中,光标停留在字符串的尾字符
    2,表示字符的属性值紧跟在字符之后,光标停留在字符串的首字符
    3,表示字符的属性值紧跟在字符之后,光标停留在字符串的尾字符
  • 寄存器bl:若寄存器al的值为0或者1时,保存字符的属性值。如图所示,字符属性由一个字节大小的数据表示。
  • 寄存器[es:bp]:保存字符串的首字符在数据段中的逻辑地址
  • 寄存器cx:保存字符串的长度
  • 寄存器dh,dl:字符串在屏幕上的起始坐标,其中寄存器dh为行号,寄存器dl为列号。显示器的屏幕只能显示25行字符,并且每行只能显示80个字符,因此dh的取指范围为0~24,dl的取指范围为0~79。

代码解析:

第1行,告诉编译器程序加载到内存的0x7c00处。

第2~4行,统一数据段寄存器DS和附加段寄存器ES的值和代码段寄存器CS一致,即不论数据段还是代码段,段起始地址都是0x7c00。

第5~6行,将字符串"Hello, world!"的首地址传递给寄存器bp。注意任何不被方括号[ ]括起来的标签或变量名都被认为是地址,访问标签或变量中的内容必须使用[ ]

第7行,将字符串的长度传递给寄存器cx

第8~11行,字符串属性设置

第12行,调用0x10号中断处理程序,在屏幕上显示字符串

第13行,使cpu进入死循环
因为cpu会不停的根据寄存器[cs:ip]中的逻辑地址转换后的物理地址,从物理内存读取机器指令,然后对其解析、执行。其中,当运行完一条机器指令后,cpu自动将该机器指令的下一条机器指令的偏移地址赋值给ip。当cpu运行完可执行文件中的最后一个机器指令后,若不采取任何措施,则cpu会将下面的数据看做机器指令,进行取指、解析、执行。因此,需要一条让cpu进入死循环的机器指令作为可执行文件的最后一条机器指令。

$表示当前行被汇编后的地址,$$表示程序被汇编后的开始地址,也就是0x7c00.

第15、17行,分别以字节和字的形式存放的数据

第16行,表示将0这个字节重复510-($-$$)遍,也就是在剩下的空间不停填充0,直到第510个字节为止。这样加上结束标志0xaa55占用的两个字节,恰好是512个字节。

在运行代码之前我们需要将其转换成计算机能读懂的机器指令形式,这就需要编译器。我们编译c代码使用GCC,编译汇编程序使用nasm编译器。

把上面的代码保存成boot.asm,然后使用nasm编译一下,生成二进制可执行文件boot.bin

$ nasm boot.asm -o boot.bin

三、虚拟硬盘的制作

下面我们将制作一个虚拟硬盘并将已经生成的可执行文件boot.bin放到虚拟硬盘的第一个磁盘块(引导扇区MBR)中。Bochs虚拟机将使用这块“硬盘”引导启动。

首先选择合适的地方创建一个工程目录

$ mkdir projectest
$ cd projectest

将可执行文件boot.bin拷贝到该工程目录中

然后在本层目录中创建一个大小为1MB的硬盘镜像文件b.img的命令如下:

$ dd if=/dev/zero of=b.img bs=512 count=2048

dd是文件拷贝命令,其中:

  • if=/dev/zero:表示拷贝的源文件的路径,"/dev/zero"是一个特殊的文件,可以提供n个0(n的值等于bs和count参数的积)
  • of=b.img:表示拷贝的目标文件路径。若不存在,则创建该文件
  • bs=512:表示块大小,单位为B
  • count=2048:表示拷贝的文件的块的数量。

由bs和count参数可知,硬盘镜像文件的大小为:2048*512B=1MB,硬盘镜像文件制作好后,将可执行文件boot.bin拷贝到硬盘镜像文件b.img(硬盘)的引导扇区的命令如下:

$ dd if=boot.bin of=b.img bs=512 seek=0 conv=notrunc

其中:

  • seek=0:表示把可执行文件boot.bin拷贝到硬盘镜像文件b.img的引导扇区(扇区号为0)。
  • conv=notrunc:表示不改变目标文件的大小,若没有该选项,则硬盘镜像文件b.img的大小会由1MB变为可执行文件boot.bin的大小512B。

这样一个写入了引导程序的“硬盘”就制作好了。

四、Bochs的使用

1. 启动Bochs

"硬盘”制作好后,要想启动bochs还需要一个配置文件——bochsrc.bxrc。为什么需要配置文件呢?因为你需要告诉bochs你希望的虚拟机是什么样的,比如,内存多大,使用哪个硬盘启动等等。在下载bochs的源码包中有一个.bochsrc,就是官方提供的配置文件示例,我们可以根据这个更改。

下面是本实验用到的bochs配置文件代码

romimage: file=/usr/local/share/bochs/BIOS-bochs-latest 
vgaromimage: file=/usr/local/share/bochs/VGABIOS-lgpl-latest 
ata0-master: type-disk, path="b.img"
megs: 16
cpu: count=1
boot: disk

其中:

  • romimage:指定bochs运行过程中使用的ROM-BIOS的路径。
  • vgaromimage:指定bochs运行过程中使用的VGA的ROM-BIOS的路径。
  • ata0-master:指定硬盘镜像文件b.img的路径。
  • megs:指定物理内存的大小,单位为MB。
  • cpu:指定cpu的个数,1个。
  • boot:指定启动方式,从硬盘启动。

将上面的代码保存为bochsrc.bxrc,也存到工程目录下。

现在一切准备就绪,启动bochs的命令如下:

$ bochs -q -f bochsrc.bxrc

其中:

  • -q: 跳过bochs启动后的配置界面。
  • -f : bochsrc.bxrc:指定配置文件的路径。
  • 如果不指定路径,那么Bochs将按照如下顺序在当前目录中寻找配置文件:
  1. .bochsrc

  2. bochsrc

  3. bochsrc.txt

  4. bochsrc.bxrc (windows only)

  5. /home/.bochsrc (Unix only)

  6. /etc/bochsrc(Unix only)

运行bochs后会在终端出现bochs调试命令行,等待我们输入调试命令,这里输入c继续执行
在这里插入图片描述
可以看到在虚拟机中我们的引导程序已成功运行,在屏幕上打印出了hello world。
在这里插入图片描述

2. Bochs调试

常用调试命令作用
b使用物理地址打断点
vb使用逻辑地址打断点
blist查看所有断点信息
n单步执行(遇到函数跳过)
s单步执行(遇到函数进入函数内部)
c继续执行
r查看所有通用寄存器的值
(eax、ebx、ecx、edx、esp、ebp、esi、edi、eip、eflags)
sreg查看所有段寄存器的值
u /5打印CPU接下来将执行的5条指令
xp查看物理内存中指定物理地址的内容
xp /2bx 物理地址:打印两个字节,以十六进制格式输出。
xp /13c 物理地址:打印13个字节,以ASCII码对应的字符显示
watch 变量名运行时,若某一行代码修改了变量,则中断,并打印修改前后的值。
q退出调试继续执行
Logo

鸿蒙生态一站式服务平台。

更多推荐