第3天 导入32位模式并导入C语言
1.制作真正的IPL
本次修改的内容为:
; 程序主体
entry:
MOV AX, 0 ; 初始化寄存器
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
; 读取磁盘
MOV AX, 0x0820
MOV ES, AX
MOV CH, 0 ; 柱面0
MOV DH, 0 ; 磁头0
MOV CL, 2 ; 扇区2
MOV AH, 0x02 ; 读取软盘/硬盘
MOV AL, 1
MOV BX, 0
MOV DL, 0x00 ; A驱动器
INT 0x13 ; 调用BIOS
JC error
首先介绍“INT 0x13”指令,这个指令与硬盘操作有关,详细的信息可以到aprilsloan.ys168.com网址,在“文件”文件夹下的BIOS接口技术参考手册87.4.pdf有关于BIOS功能的介绍。如要对磁盘进行读盘、写盘操作,则需对如下寄存器赋予不同的值:
- AH = 0x02;(读盘)
- AH = 0x03;(写盘)
- AL = 处理对象的扇区数;(只能同时处理连续的扇区)
- CH = 柱面号;
- CL = 扇区号;
- DH = 磁头号;
- DL = 驱动器号;
- ES:BX = 缓冲地址;
通过上面对寄存器的说明,就可以知道代码的意思了。我们要读取0号柱面0号磁头的第2扇区,并把读取到内容放在内存中地址为0x8200~0x83ff的地方。
读盘写盘操作得到的返回值为:
①FLAGS.CF == 0:没有错误,AH=0
②FLAGS.CF == 1:有错误,错误号码存入AH内
FLAGS.CF是什么?它表示的是标志寄存器FLAGS的进位标志CF(carry flag),现在还不必了解CF的功能,所以就不讲了。
而下方的JC就与磁盘读取操作产生了联系,JC是“jump if carry”的缩写,意思是如果CF位是1的话,就进行跳转。联系磁盘读取操作的代码,如果磁盘读取错误的话,就跳转到error标签去。
那为什么要用0x8200~0x83ff,而不用0x8000~0x81ff呢?看书去,书上都有。启动区的内容拷贝了一份到0x8000~0x81ff地址中,你敢用这块地方,系统就敢罢工。
下图是软盘的示意图,关于软盘的知识就不在此赘述,多看看书总是能懂的。
error:
MOV SI, msg
putloop:
MOV AL, [SI]
ADD SI, 1 ; 给SI加1
CMP AL, 0
JE fin
MOV AH, 0x0e ; 显示一个文字
MOV BX, 15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
msg:
DB 0x0a, 0x0a ; 2个换行
DB "load error"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 填写0x00,直到0x07dfe
DB 0x55, 0xaa
接下来这一段是当读取磁盘出错时才会运行的,若是看过我第2天的博客的人应该能看懂这段代码吧,看不懂可以去复习一下。最后会在屏幕上显示"load error",相信你是看不到这个界面的。
最后运行出来不显示什么信息就是对的。
2.试错
软盘并没有那么可靠,有时会发生不能读数据的状况,需要重新读上几遍看是不是真的坏了,于是对代码做出新的调整。(软盘这玩意儿现在都看不到了,这段代码是否有必要添加吗?)
; 读取磁盘
MOV AX, 0x0820
MOV ES, AX
MOV CH, 0 ; 柱面0
MOV DH, 0 ; 磁头0
MOV CL, 2 ; 扇区2
MOV SI, 0 ; 记录失败次数的寄存器
retry:
MOV AH, 0x02 ; 读取软盘/硬盘
MOV AL, 1 ; 1个扇区
MOV BX, 0
MOV DL, 0x00 ; A驱动器
INT 0x13 ; 调用BIOS
JNC fin ; 没出错的话跳转到fin
ADD SI, 1 ; 将SI加1
CMP SI, 5 ; 比较SI与5
JAE error ; SI >= 5时,跳转到error
MOV AH, 0x00
MOV DL, 0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry
读取磁盘的操作并没有发生什么变化,用SI来记录读取失败的次数。retry标签下前五行代码与上一节的一样。
第六行出现了新的指令JNC,它是无进位跳转(Jump if Not Carry)的缩写,在没有进位的时候才发生跳转。以下是一些短转移指令,我将它们放在一起,方便各位读者整理。
JA/JNBE | 无符号整数大于时跳转 | JG/GNLE | 有符号整数大于时跳转 |
JAE/JNB | 无符号整数大于或等于时跳转 | JGE/JNL | 有符号整数大于或等于时跳转 |
JB/JNAE | 无符号整数小于时跳转 | JL/JNGE | 有符号整数小于时跳转 |
JBE/JNA | 无符号整数小于或等于时跳转 | JLE/JNG | 有符号整数小于或等于时跳转 |
JE/JZ | 等于时跳转 | JNE/JNZ | 不等于时跳转 |
JC | 有进位时跳转 | JNC | 无进位时跳转 |
JO | 溢出时跳转 | JNO | 无溢出时跳转 |
JP/JPE | 奇偶性为偶数时跳转 | JNP/JPO | 奇偶性为奇数时跳转 |
JS | 符号位为“1”时跳转 | JNS | 符号位为“0”时跳转 |
还记得上一节的内容吗?当读取磁盘成功时,CF=0,当读取失败时,CF=1。所以,当成功读取磁盘后,就跳转到fin标签。而读取失败后,将SI加1,与5进行对比,JAE指令的含义如上表所示,当SI大于5,即读取磁盘失败超过5次后,跳转到error标签,否则重置驱动器再次进行读取。
最后执行得到的界面如图:
其实我就是把第一节的图粘贴下来了,毕竟第一节都没出错,第二节就更不会出错了。
3.读到18扇区
再次对代码做出调整:
readloop:
MOV SI, 0 ; 记录失败次数的寄存器
retry:
MOV AH, 0x02 ; 读取软盘/硬盘
MOV AL, 1 ; 1个扇区
MOV BX, 0
MOV DL, 0x00 ; A驱动器
INT 0x13 ; 调用BIOS
JNC next ; 没出错的话跳转到next
ADD SI, 1 ; 将SI加1
CMP SI, 5 ; 比较SI与5
JAE error ; SI >= 5时,跳转到error
MOV AH, 0x00
MOV DL, 0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry
next:
MOV AX, ES
ADD AX, 0x20 ; 把内存地址后移0x200
MOV ES, AX ; 因为没有ADD ES, 0x20指令,所以以这种方式计算
ADD CL, 1 ; 往CL里加1,指定下一个扇区号
CMP CL, 18 ; 比较CL与18
JBE readloop ; 如果CL <= 18跳转至readloop
首先是在MOV SI, 0指令前面添加一个标签,取名readloop,这样就能通过条件转移指令跳转到这里进行循环了。
retry的内容并没有改变。在下方添加了next代码段,这里是实现读取到18扇区的关键。在读取完一个扇区之后,更改ES的值,其中0x200=512(1个扇区的大小),给CL加1,读取下一个扇区,JBE指令可以看看上面的表,将CL与18进行比较,如果CL小于或等于18就跳转到readloop读取下一个扇区。
最后得到的界面:
没错,这个图还是粘贴过来的,作者为什么不在读取成功的时候显示个信息呢?
4.读入10个柱面
这一节的代码还是比较简单,让我们看看有什么变化吧。
CYLS EQU 10
...
readloop:
MOV SI, 0 ; 记录失败次数的寄存器
retry:
...
next:
MOV AX, ES
ADD AX, 0x20 ; 把内存地址后移0x200
MOV ES, AX ; 因为没有ADD ES, 0x20指令,所以以这种方式计算
ADD CL, 1 ; 往CL里加1,指定下一个扇区号
CMP CL, 18 ; 比较CL与18
JBE readloop ; 如果CL <= 18跳转至readloop
MOV CL, 1
ADD DH, 1
CMP DH, 2
JB readloop ; 如果DH < 2,则跳转到readloop
MOV DH, 0
ADD CH, 1
CMP CH, CYLS
JB readloop ; 如果CH < CYLS,则跳转到readloop
唯有next标签下的代码段发生了变化,再读取完第18扇区之后,重置CL为1,将DH加1,更换磁头从第1扇区开始读取,再对DH做对比,如果DH小于2就跳转到readloop继续读扇区。若2个磁头都读完了,再更换柱面读取扇区,直至读完10个柱面后结束。
CYLS EQU 10的意思是CYLS = 10,若是用C语言写的话就是#define CYLS 10,它用来定义常数。
OK,代码的部分就讲完了,下面来运行看看吧。
万年不变的运行界面。。。
5.着手开发操作系统
你以为以前你写的是操作系统?不不不,之前的只是启动区,它被用来把操作系统装载到内存中,现在才是真正的开始。
新建一个文本文档并命名为haribote.nas,并把以下内容输入文档中。
fin:
HLT
JMP fin
这短小的代码,是不是让人很失望?但学习还是要慢慢来的。
另外,因为新添了文件,需要的Makefile中规定它的编译规则。
ipl.bin : ipl.nas Makefile
$(NASK) ipl.nas ipl.bin ipl.lst
haribote.sys : haribote.nas Makefile
$(NASK) haribote.nas haribote.sys haribote.lst
haribote.img : ipl.bin haribote.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl.bin len:512 from:0 to:0 \
copy from:haribote.sys to:@: \
imgout:haribote.img
这是Makefile中需要修改的部分。从上我们可以看出,首先要将haribote.nas通过nask转换为haribote.sys,再将ipl.bin和haribote.sys写入镜像中。这就完成了。
用二进制编辑器打开haribote.img,可以发现在0x2600地址附近保存着文件名。在0x4200地址附近,看到“F4 EB FD”。
以上内容可以总结为:一般向一个空软盘保存文件时,
(1)文件名会写在0x2600以后的地方;
(2)文件的内容会写在0x4200以后的地方。
要记住这两点内容哦,以下的某些地方会用到它们的。
好,再次运行代码。
6.从启动区执行操作系统
要怎么才能执行磁盘镜像上0x4200地址的程序呢?程序是从启动区开始的,把磁盘内容转载到内存0x8000地址上,所以磁盘0x4200出的内容就应该位于内存0x8000+0x4200=0xc200地址中。知道了这个,我们就往代码里添点东西,haribote.nas中的代码变为:
ORG 0xc200 ; 把程序转载到0xc200地址中
fin:
HLT
JMP fin
这代码一如既往地简洁。接下来还需要对ipl.nas进行处理:
CMP CH, CYLS
JB readloop ; 如果CH < CYLS,则跳转到readloop
JMP 0xc200 ; 读取完磁盘后执行haribote.sys
error: ; 打印错误信息
MOV SI, msg
添加JMP 0xc200这句代码。意思是当ipl.nas中读取磁盘操作结束后就跳转到0xc200地址。
最后运行代码。
这图片我都粘贴5次了,下一节中终于要出现新界面了。
7.确定操作系统的执行情况
这次就让我们切换一下画面模式吧。代码如下:
; haribote-os
; TAB = 4
ORG 0xc200 ; 将程序加载到这个内存地址
MOV AL, 0x13 ; VGA显卡,320x200x8位彩色
MOV AH, 0x00
INT 0x10
fin:
HLT
JMP fin
INT 0x10在我们显示字符的时候就出现过,这是有关显示服务的BIOS功能,下面介绍一下有关的寄存器配置:
- AH = 0x00; 设置显卡模式
- AL = 模式: (省略了一些不重要的模式)
0x03: 16色字符模式,80 * 25
0x12: VGA图形模式,640 * 480 * 4位彩色模式,独特的4面存储模式
0x13: VGA图形模式,320 * 200 * 8位彩色模式,调色板模式
0x6a: 扩展VGA图形模式,800 * 600 * 4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)
- 返回值:无
参照说明,我们采用的是8位彩色模式,可以使用2^8=256种颜色。
此次还修改了其他一些地方。首先将ipl.nas的文件名变成了ipl10.nas(不要忘记在Makefile中更改名字哦)。这是为了提醒大家这个程序只能读入10个柱面。另外,想要吧磁盘装载内容的结束地址告诉给haribote.nas,所以我们在JMP 0xc200之前,加入了一行命令,将CYLS的值写到内容地址0x0ff0中。
MOV [0x0ff0], CH ; 将读取的柱面数保存在这个地址中
JMP 0xc200 ; 读取完磁盘后执行haribote.sys
如果运行正常的话,画面会变为一片漆黑,来运行看看吧。
虽然画面终于有变动了,但黑漆漆一片还是不好看,要想制作好看点的界面还得等到下一章。
8. 32位模式前期准备
这节是为了C语言开发做准备,作者提供的C编译器只能生成32位模式的机器语言。这个32位指的是CPU的模式,不同模式使用的寄存器不一样,所以一个模式的机器语言在另一个模式下不能运行。另外,32位模式下可以使用的内存容量远远大于1MB,还具有CPU的自我保护功能。
BIOS是用16位机器语言写的,使用32位模式就不能使用BIOS了,所以全部要放在开头先做。
下面我们来修改代码吧,这次只修改haribote.nas。
; 有关BOOT_INFO
CYLS EQU 0x0ff0 ; 设定启动区
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色数目的信息,颜色的位数
SCRNX EQU 0x0ff4 ; 分辨率的X(screen x)
SCRNY EQU 0x0ff6 ; 分辨率的Y(screen y)
VRAM EQU 0x0ff8 ; 图像缓冲器的开始地址
ORG 0xc200 ; 将程序加载到这个内存地址
MOV AL, 0x13 ; VGA显卡,320x200x8位彩色
MOV AH, 0x00
INT 0x10
MOV BYTE [VMODE], 8 ; 记录画面模式
MOV WORD [SCRNX], 320
MOV WORD [SCRNY], 200
MOV DWORD [VRAM], 0x000a0000
; 用BIOS取得键盘上各种LED指示灯的状态
MOV AH, 0x02
INT 0x16
MOV [LEDS], AL
首先我们定义了几个变量的值,它们的用处已经写在注释中。在设置好显卡模式之后,开始向内存地址存入一些数据,唯一需要说明的是向VRAM地址存入0xa0000,可以从内存图中看出0xa0000图像视频缓冲器的开始地址,之后可以通过指针很方便的修改其中的数据。
最后是调用BIOS功能。INT 0x16代表的是调用键盘的功能。下面介绍一些寄存器参数:
- AH = 0x02; (Shift Status)
- 返回值:AL = 当前键盘状态情况
AL中各位表示的含义如下表所示:
Bit | 含义 |
7 | 插入上锁 |
6 | 大写锁定上锁 |
5 | 数字锁定上锁 |
4 | 滚动锁定上锁 |
3 | Alt键按下 |
2 | Ctrl键按下 |
1 | 左Shift键按下 |
0 | 右Shift键按下 |
目前AL中的数据还用不到,这就当做扩展知识介绍给大家了。
下面来运行程序看看吧。
一如既往的黑,你以为我会告诉你这是从上面粘贴下俩的吗?
9.开始导入C语言
终于要开始我最喜欢的C语言了。
首先把haribote.nas改名为asmhead.nas(别忘了也要在Makefile里面改哦),为了能够调用C语言写的程序,还要添加一点点代码,放心,你看不懂这段代码的,作者也认为你看不懂,他表示以后再讲,我也就偷懒了。
新建文件并命名为bootpack.c,以下是内容:
void HariMain(void)
{
fin:
/* 这里想写上HTL,但C语言中不能用HTL! */
goto fin;
}
还是一个很简单的死循环语句。这里的主函数名是HariMain,和C语言有些区别。C语言的基本语法不会多讲,只针对语言的逻辑结构和意义进行说明。
bootpack.c编程机器语言的过程比较复杂,而且书上有,就不赘述了。另外,Makefile也不进行讲解了,反正你们也有源码,复制粘贴多省事。
以下是运行的界面:
我没复制粘贴,你别乱说。
10.实现HLT
作者真的是对hlt这个指令情有独钟。让我们再新建一个文件,命名为naskfunc.nas,今后会在这里写汇编函数,以下是代码:
; naskfunc
; TAB = 4
[FORMAT "WCOFF"] ; 制作目标文件的模式
[BITS 32] ; 制作32位模式用的机器语言
; 制作目标文件的信息
[FILE "naskfunc.nas"] ; 源文件名信息
GLOBAL _io_hlt ; 程序中包含的函数名
; 以下是实际的函数
[SECTION .text] ; 目标文件中谢了这些之后再写程序
_io_hlt: ; void io_hlt(void);
HLT
RET
注意要在函数名之前加上“_”,否则就不能很好地与C语言函数链接。需要链接的函数名都要用GLOBAL指令声明。
我在谷歌上搜了WCOFF也没搜出是什么东西,知道它是制作目标文件的模式就好了。并设定为32位机器语言模式。
下面是bootpack.c里面的内容:
/* 告诉C编译器,有一个函数在别的文件里 */
void io_hlt(void);
void HariMain(void)
{
fin:
io_hlt(); /* 这里想写上HTL,但C语言中不能用HTL! */
goto fin;
}
这样就写好了,内容很简单,相信你一定能看懂。另外也不要修改Makefile,赶紧来运行看看吧。
第3章的内容就到此结束了,这章的内容比较多,请大家好好理解。这篇博客花了好几天才写完,在内容的说明中花费了最多的时间,我也不知道自己是否清晰的解释了代码的含义,若是不清晰,很高兴大家在下方评论指正。愿诸君能继续看第四章的内容。