《30天自制操作系统》第3天

第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滚动锁定上锁
3Alt键按下
2Ctrl键按下
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章的内容就到此结束了,这章的内容比较多,请大家好好理解。这篇博客花了好几天才写完,在内容的说明中花费了最多的时间,我也不知道自己是否清晰的解释了代码的含义,若是不清晰,很高兴大家在下方评论指正。愿诸君能继续看第四章的内容。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值