上一篇文章我们制作了一个启动层,它可以在屏幕上输出一条信息。但是,由于一个扇区只有512字节,而一个操作系统少说也有1MB左右,所以,我们需要从磁盘读取内核程序到内存,然后用汇编jmp指令跳转到这个内存物理地址。
笔者在这里卡了好几天,不是因为不会写,而是笔者写的太快了,没有一步一个脚印走,导致写出来的程序乱七八糟的。(笔者将扇区和柱面号搞错了,导致让CPU执行了一堆0000000)今天总算做出来了,笔者真的很高兴,直接要疯了。
首先,我们要从硬盘中读取内核程序的代码。BIOS给我们提供了方法。具体如下:
int 0x13 AH寄存器 = 0x02 读取磁盘中的内容
AL寄存器 = 要处理的扇区数
DL寄存器 = 磁盘驱动器号码
CH寄存器 = 柱面号
DH寄存器 = 使用的磁头号码
CL寄存器 = 起始扇区号
ES:BX寄存器 = 读取后存储到的内存地址(ES = 段地址 BX = 偏移地址)
返回值:
FLAG寄存器 = 0 没有错误
FLAG寄存器 = 1 有错误 错误号码存在AH寄存器中
对了,既然操作系统的名字已经变成了Cunix,那么在GitHub上的网址也就变成了https://github.com/pengruiyang-cpu/Cunix。笔者在boot.asm中将输出的部分注释掉了,因为一个操作系统不应该有这种东西的。还有一点:
笔者在写boot.asm中突然发现这个文件编译出的结果大于512字节了,只能将这些类似于
mov ah, 0x00
mov al, 0x01
之类的代码写成了
mov ax, 0x0001
程序干的事情没变,但是写法缩短了。这是为什么呢?
这是因为AX寄存器的全拼是Accumulator X Register(累加寄存器,X用来占位,毕竟写成A不太好看),而AL寄存器的全拼是Accumulator X Regiter Low(累加寄存器低位),本来就是AX寄存器的一部分,AH就是Accumulator X Register High(累加寄存器高位),也是AX的一部分,所以可以直接给AX寄存器赋值。
另外,BX、CX等大多数带有X的寄存器都可以这样写。
回到主题,我们对应上面可以写出下面的代码。
; int 0x13 AH寄存器 = 0x02 读取磁盘中的内容
; AL寄存器 = 要处理的扇区数
; DL寄存器 = 磁盘驱动器号码
; CH寄存器 = 柱面号
; DH寄存器 = 使用的磁头号码
; CL寄存器 = 起始扇区号
; ES:BX寄存器 = 读取后存储到的内存地址(ES = 段地址 BX = 偏移地址)
; 返回值:
; FLAG寄存器 = 0 没有错误
; FLAG寄存器 = 1 有错误 错误号码存在AH寄存器中
; 设置BIOS函数号码为0x02 读取磁盘中的内容
mov ah, 0x02
; 处理的扇区数量,以后随着内核程序的变大会更改
mov al, 0x01
; 设置磁盘驱动器的号码 一般为0号 也就是第一个
mov dl, 0x00
; 要处理的柱面号,就是磁道,以后会使用这个词语
; 柱面从0开始计数
mov ch, 0x00
; 磁头号码
mov dh, 0x00
; 起始扇区数
; 扇区从1开始计算,第一扇区是启动层
mov cl, 0x02
; 段地址,读取出的内容将会放到这里
; 0x7e00是笔者随便设置的,这一段内存是空的,正好在0x7c00位置的启动层后面
mov es, 0x7e00
; 偏移地址 与段地址共同组成物理地址0x7e0000
mov bx, 0x00
; 调用函数
int 0x13
; 判断Flag的值
; 如果不进位就表示读取成功,跳转到标号end
; Jump if not carry,如果不进位则跳转
jnc end
; 如果有问题就跳转到error
; Jump if carry,如果进位则跳转
jc error
end:
; 从启动层跳转到内核程序
jmp 0x7e00:0x00000
error:
; 出错就重复执行
jmp $
以上这段程序可以将磁盘映像文件中第一柱面第二扇区中的内容读取到内存物理地址0x0e0000处,然后跳转到该地址。我们在文章《不要欺负BIOS,好吗》中已经讲到了如何将将这段代码写入到磁盘映像文件中。
然后,我们编写kernel.asm作为内核程序。
org 0x7e00
; 初始化寄存器
; mov指令将右边的寄存器或数的值复制到右边的寄存器中
; 将CS(Code Segment)的值赋给AX寄存器
mov ax, cs
; 将AX寄存器(从CS寄存器取来)的值赋给DS(Data Segment)寄存器
mov ds, ax
; 将AX寄存器的值赋给ES(Extra Segment)寄存器
mov es, ax
; 将AX寄存器的值赋给SS(Stack Segment)寄存器
mov ss, ax
; 将这段代码的起始位置赋值给SP寄存器(Stack Point)
mov sp, 0x7e00
; 调整画面模式
; int 0x10 AH寄存器 = 0x00 切换显卡的模式
; AL = 显卡的模式
; 0x03 16色的字符模式,80 x 25
; 0x12 VGA图形模式,640 x 480 x 4位彩色模式,独特的4面存储模式
; 0x13 VGA图形模式,320 x 200 x 8位彩色模式,调色板模式
; 0x6a 扩展VGA图形模式,800 x 600 x 4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)
mov ax, 0x0013
int 0x10
这段代码只是切换了以下画面的显示模式。因为我们以后肯定是要做像Linux和MS-DOS之类的操作系统,所以要切换一下。切换之后光标是会没有的,要我们自己做。
下面是boot.asm的全部内容,注释已经写的很详细了,笔者将显示文字的部分注释掉了,一方面是一个操作系统不会这样的,另一方面是boot.asm的512个字节有点不太够。
; 设置程序的起始位置为0x7c00(为了让CPU执行)
org 0x7c00
; 初始化寄存器
; mov指令将右边的寄存器或数的值复制到右边的寄存器中
; 将CS(Code Segment)的值赋给AX寄存器
mov ax, cs
; 将AX寄存器(从CS寄存器取来)的值赋给DS(Data Segment)寄存器
mov ds, ax
; 将AX寄存器的值赋给ES(Extra Segment)寄存器
mov es, ax
; 将AX寄存器的值赋给SS(Stack Segment)寄存器
mov ss, ax
; 将这段代码的起始位置赋值给SP寄存器(Stack Point)
mov sp, 0x7c00
; 显示Loading Boot
; int 0x10 AH = 0x13 显示一行字符串
; AL寄存器 = 写入的模式
; AL = 0x00 字符串的属性由BL寄存器提供,CX寄存器提供字符串的长度以B为单位,显示后光标的位置不变
; AL = 0x01 同AL = 0x00 但光标在显示完成后会移动到字符串的末尾
; AL = 0x02 字符串的属性由每个字符后面的字节提供,CX寄存器提供的Word为单位,显示后光标的位置不变
; AL = 0x03 同AL = 0x02,但光标会移动到字符串的末尾
; CX寄存器 = 字符串的长度
; DH寄存器 = 光标的行数
; DL寄存器 = 光标的列数
; ES:BP = 字符串在内存中的物理地址(ES为段地址,BP为偏移地址)
; BH = 页码
; BL = 字符串的属性/颜色属性
; 0~2Bits = 字体的颜色(0=黑色,1=蓝色,2=绿色,3=青色,4=红色,5=紫色,6=棕色,7=白色)
; 3Bit = 字体的亮度(0=普通,1=高亮度)
; 4~6Bits = 背景颜色(0=黑色,1=蓝色,2=绿色,3=青色,4=红色,5=紫色,6=棕色,7=白色)
; 7Bit = 字体闪烁(0=不闪烁,1=闪烁)
; 相当于mov al, 0x01 mov ah, 0x13
; 写入的模式和函数号码
;mov ax, 0x1301
; 字符串的长度
;mov cx, 12
; 光标的行数和列数都为0
;mov dx, 0x0000
; 将AX寄存器PUSH进入栈中
;push ax
; 设置AX寄存器的值为DS寄存器的值
;mov ax, ds
; 将AX寄存器中DS寄存器的值赋给ES寄存器
;mov es, ax
; 取回AX寄存器的内容
;pop ax
; 设置显示的字符
;mov bp, loading_msg
; 页码和属性
;mov bx, 0x000f
; 调用BIOS中断
;int 0x10
;jmp $
; 读取磁盘中的内容并加载到内存0x7e00处,最后跳转到这个地址
; 内核程序kernel.bin存储在柱面1号,扇区1号,此时占一个扇区大小(以后随着操作系统的更新会越来越大)
; int 0x13 AH寄存器 = 0x02 读取磁盘中的内容
; AL寄存器 = 要处理的扇区数
; DL寄存器 = 磁盘驱动器号码
; CH寄存器 = 柱面号
; DH寄存器 = 使用的磁头号码
; CL寄存器 = 起始扇区号
; ES:BX寄存器 = 读取后存储到的内存地址(ES = 段地址 BX = 偏移地址)
; 返回值:
; FLAG寄存器 = 0 没有错误
; FLAG寄存器 = 1 有错误 错误号码存在AH寄存器中
; 设置函数号码以及扇区数量
mov ax, 0x0201
; 设置驱动器号码与磁头号码
mov dx, 0x0000
; 设置起始的扇区号和柱面号
mov cx, 0x0002
; 设置读取到的内存地址 由于寄存器放不下
; 同样,由于ES寄存器无法直接使用操作数赋值,需要使用AX寄存器
; 先将AX寄存器目前的值保存到栈中
push ax
; 将磁盘中的内容保存至0x7e00中
mov ax, 0x7e00
mov es, ax
; AX寄存器已经使用完成,恢复原来的状态
pop ax
; 偏移地址为0
mov bx, 0
; 调用BIOS中断
int 0x13
; 判断是否读取成功
; JNC指令判断FLAG寄存器是否不为1,再跳转到目标位置(Jump if not carry)
jnc end
; 如果有问题将会跳转到error
; JC指令判断FLAG寄存器是否为1,再跳转到目标位置(Jump if carry)
jc error
end:
; 跳转到0x7e00处执行
jmp 0x7e00:0x0000
error:
jmp $
; 要显示的信息
;loading_okay: db "Loading Okay"
;loading_msg: db "Loading Okay"
; 让BIOS知道这是一个启动层
;loading_msg: db "Loading Boot"
times 510 - ($ - $$) db 0
; 填充0xaa和0x55
dw 0xaa55
然后,启动一下虚拟机。
大家看起来很简单的样子,其实笔者也忙活了很久……毕竟笔者都做不好是没资格教大家做的。
接下来,我们会为使用C语言做一些准备,包括切换CPU到保护模式啊,获取硬件信息啊之类的。不过,一旦切换到C语言成功,做起来就简单多了。
你学会了吧?