Day03

今天来学习制作IPL、显示一个最基础的图形界面(对操作系统进行基础的启动设置),以及导入c语言。

何为IPL

IPL(Initial Program Loader,启动程序装载器),在操作系统启动的时候,只会装载固定地址的很小字节(例如初始的512B),初始的部分即为IPL。在这里我们需要写一些汇编代码,来读取接下来的内容,或是启动操作系统的一些功能。

一些问题与感想

汇编语言中标签的顺序也很重要

类似c语言函数的声明顺序,汇编语言也是有前后依赖的关系的。在抄代码的时候,就随意把汇编语言中的函数放了个位置,结果意外地不能用了。

并不是从0开始导入C语言

如果说要真的从零开始导入C语言的话,那恐怕我们得先用汇编写一个编译器,之后再一步步导入各种库函数。本书直接把一些现成的函数给我们了,毕竟这也不是OS的主要部分,虽然有点遗憾,但也能理解吧。

下面是真正的书中内容

制作真正的IPL

harib00a

在程序核心部分内,我们加入了读取磁盘的操作。
当然,我们需要查阅相关说明后,发现 AH = 0x02时是读盘内容。对于其它关键字的意思,会在代码注释里给出。

; 读取磁盘
    MOV AX, 0x0820
    MOV ES, AX
    MOV CH, 0 ; 柱面0
    MOV DH, 0 ; 磁头0
    MOV CL, 2 ; 扇区2

    MOV AH, 0x02 ; AH = 0x02 读盘
    MOV AL, 1 ; 1个扇区
    MOV BX, 0
    MOV DL, 0x00 ; A驱动器
    INT 0x13 ; 调用磁盘BIOS
    JC error

段寄存器表示地址

其实计组课上也有类似的知识点,用 ES << 4 + BX 来拼接成想要的地址。在汇编中的用法:

MOV AL, [ES:BX]

其实,平常写的地址也是默认的段寄存器。如

MOV CX, [1234] 其实是 MOV CX, [DS:1234]

当然要默认把DS设置为0

查看内存分布图

在我们想要把程序装载入内存的某些地址的时候,我们先查一下那个地方是否已经被用上了,不然冲突了就不好了。

makefile里的变量替换

说是变量,其实更像c语言里的宏定义,可以做到很方便的只需要在一个地方改动即可。

试错

harib00b

在读软盘的时候,经常容易出错。所以我们可以加入少量的尝试。
代码接在00a之后

MOV SI, 0 ; 记录失败次数的寄存器

retry:
MOV AH, 0x02 ; 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

读到18扇区

harib00c

我们可以设置一个循环,这样可以一直读18个扇区。主要更新的部分为next的标签。
前三行其实就是等价 ES += 20,只是这个寄存器不能直接加。。

next:
    MOV AX, ES ; 把内存地址后移0x200
    ADD AX, 0x0020
    MOV ES, AX ; 因为没有ADD ES, 0x020指令,所以这里稍微绕个弯(把ES数放到AX里,加了之后再放回来)
    ADD CL, 1 ; 往CL里加1
    CMP CL, 18 ; 比较CL与18
    JBE readloop ; 如果CL <= 18 跳转至readloop

读到10个柱面

harib00d

在next里继续添加一些东西,开始读下一个柱面。这样就能往内存里装载180KB了。
显然,CH在不断增加,读取下一个柱面。

next:
    MOV AX, ES ; 把内存地址后移0x200
    ADD AX, 0x0020
    MOV ES, AX ; 因为没有ADD ES, 0x020指令,所以这里稍微绕个弯(把ES数放到AX里,加了之后再放回来)
    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 ; 如果CH < CYLS, 跳转到readloop
    JB readloop

另外,开头有一句

CYLS EQU 10 ; CYLS = 10

着手开发操作系统

harib00e

最简单的操作系统

目前还是一个最简单的操作系统,只有停机操作。

fin:
    HLT
    JMP fin

保存为nas文件后,编译成sys文件,然后通过edimg保存到磁盘映像img里。

向一个空软盘保存文件

文件名会写在 0x002600以后的地方
文件的内容会写在 0x004200以后的地方

所以接下来我们只要执行0x004200地方的东西,就可以执行这个程序了。

从启动区执行操作系统

harib00f

刚才说的情况是空磁盘,我们知道启动区从0x8000开始,所以0x4200的内容在0xc200的地址上。所以只要指明地址是0xc200就能执行想要的程序了。
更新一下haribote.nas

ORG 0xc200 ; 指定程序开始地址

fin:
HLT
JMP fin

更新一下ipl.nas

next:
    MOV AX, ES ; 把内存地址后移0x200
    ADD AX, 0x0020
    MOV ES, AX ; 因为没有ADD ES, 0x020指令,所以这里稍微绕个弯(把ES数放到AX里,加了之后再放回来)
    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 ; 如果CH < CYLS, 跳转到readloop
    JB readloop

; 读取完毕后执行haribote.sys
    JMP 0xc200

确认操作系统的执行情况

harib00g

通过调用显卡,我们可以看到一个漆黑的画面了。
PS:其实我写的这个代码有一个bug,所以看起来很奇怪(怎么这么长)。。到了day04才发现。。
haribote.nas

ORG 0xc200 ; 指定程序开始地址
MOV AL 0x13 ; VGA显卡,320*200*8位彩色
MOV AH, 0x00
INT 0x10

fin:
HLT
JMP fin

其它修改的地方

把ipl.nas 修改了为 ipl10.nas ,表示只能读取前10个柱面。还添加了磁盘读取的结束地址。
当然也要改makefile(作者应该觉得我们已经会了吧)。

32位模式前期准备

harib00h

BIOS需要16位机器语言,所以我们在16位下写好各种准备工作后,再进入32位模式。
BIOS准备了各种键盘状态、画面状态
haribote.nas

; haribote-os
; TAB=4

; 有关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显卡,320*200*8位彩色
    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 ; KEYBOARD BIOS
    MOV [LEDS], AL

fin:
    HLT
    JMP fin

其中,VRAM即为我们说的video RAM,用于显示每一个像素的颜色。这个地址当然也是设定好的,不能随便用。

开始导入C语言

harib00i

终于开始导入C语言了。当然C语言不是凭空不需要汇编就能写好的,所以作者直接给我们添加了些意义不明代码。没办法,只好把asmhead.nas复制过来好了。
来看C语言部分,还是很简单,很熟悉的。

void HariMain(void)
{
fin:
    /* 写不了HLT */
    goto fin;
}

C语言翻译成机器代码的过程

感觉这个过程要比平常直接.c .o .exe 更多啊。
翻译过程如下:
.c(C)->.gas(gas汇编)->.nas(nas汇编)->.obj(目标文件,需link)->.bim(二进制映像文件)->.hrb(对应的操作系统)->.sys
为了实现各层的指令,makefile也有了很多改动。到这里我就感觉梳理makefile的依赖关系已经比较麻烦了)。

实现HLT

harib00j

在这部我们初步尝试了c语言之后,现在可以尝试汇编(_func)与c(func)。就写一个简单的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程序中,只需要声明一下就好了
bootpack.c

void io_hlt(void);

void HariMain(void)
{
fin:
    io_hlt(); /* 执行naskfunc.nas的_io_hlt */
    goto fin;
}

在makefile里添加对应的naskfunc.nas 终于完成了。