使用汇编语言编辑的是接近于机器语言的程序,但是汇编语言操作的基本都是寄存器、内存之类的底层的东西,阅读起来其实挺困难的。就如上一天的循环,不仔细阅读并模拟代码逻辑,还真的很难看出来是个循环呢。于是我们希望在今天开始引入C语言,相对来说比较友好一些。
但实际上,本篇绝大部分篇幅还是使用汇编语言,姑且先忍耐一下吧
磁盘的分区
由于本篇内容主要是在介绍如何使用汇编程序读取磁盘内容,因而在正式开讲前,先来介绍一下磁盘是如何划分区域的。
以前我们都是用的磁盘,是一张带有磁粉的碟。在读取的时候,需要有一个特殊的磁头来读取。反正挺高深的,至于有磁性为什么就可以存储数据,这个我也不懂。
一张软盘被人为划分了若干个区域。首先在一张软盘上以同心圆划分出了80个区域(编号从0开始),这个称为柱头;然后再以直径方向将一个柱面划分成了18等分,这个称为扇区,每个扇区可以存放512字节。如果是一张双面的软盘,则在软盘背面也有这样一个区域。
那么计算一下软盘的大小:
512*18(扇区)*80(柱面)2(双面)=14401024=1440 KB
磁盘中断0x13
这个中断是让CPU调用磁盘的BIOS。与这个中断有关的设置如下:AH=0x02; (读盘)
AH=0x03; (写盘)
AH=0x04; (校验)
AH=0x0C; (寻道)
AL=处理对象的扇区数;(只能同时处理连续的扇区)
CH=柱面号 & 0xff;
CL=扇区号
DH=磁头号;
DL=驱动器号;
ES : BS=缓冲地址;
返回值:
FLAGS.CF==0;没有错误
FLAGS.CF==1;有错误
缓冲区和地址计算
缓冲区是内存中一个区域,从磁盘中读取出来的数据会存放到这个区域等待进一步处理。
我们知道,一个十六位的寄存器,最大是一个16位的二进制数,也就是2的16次方,也就是64KB。如果只用一个寄存器来存放地址,那么最大的内存访问量也就是64KB,很多早期的电脑就是这样。
在这里ES和BX寄存器一起用来表示这个地址,其形式就是[ES:BX],计算规则为ES*16+BX,最大就能表示1M的内存地址了。
段寄存器
事实上,在制定内存中某个地址时(即用方括号表示),实际上都考虑了段寄存器DS,只不过DS可以忽略。比如下面两句诗等效的:
MOV CS,[1234]
MOV CS,[DS:1234]
为了避免错误的发生,需要将DS设置为0。
读取磁盘程序
程序如下:
; hello-os
; TAB=4
CYLS EQU 10 ; 一共读10个柱面
ORG 0x7c00 ; 指名程序的装载地址
; 以下一段是标准FAT12格式软盘专用的代码
JMP entry
DB 0x90
DB "HELLOIPL" ; 引导扇区的名称,随意写,8字节
DW 512 ; 每个扇区(sector)的大小,必须是512
DB 1 ; 簇(cluster)的大小,必须为1
DW 1 ; FAT起始位置,一般从第一个扇区开始
DB 2 ; FAT的个数,必须为2
DW 224 ; 根目录大小,一般设置成224
DW 2880 ; 磁盘的大小,必须为2880扇区
DB 0xf0 ; 磁盘的种类,必须为0xf0
DW 9 ; FAT的长度,必须是9扇区
DW 18 ; 1个磁道(track)有几个扇区,必是18
DW 2 ; 磁头数,必须为2
DD 0 ; 因为不使用分区,所以必须是0,4字?
DD 2880 ; 重写一次磁盘大小,4字节
DB 0,0,0x29 ; 意义不明,固定
DD 0xffffffff ; 卷标号
DB "HELLO-OS " ; 磁盘名称,11字节
DB "FAT12 " ; 磁盘格式名称,8字节
RESB 18 ; 空出18字节
; 程序主体
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,第二个扇区开始
readloop:
MOV SI,0 ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; 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 ; 内存地址后移0x200,即512个字节
ADD AX,0x0020
MOV ES,AX ; 因为没有ADD ES,0x020 ,所以曲线救国
ADD CL,1 ; CL加1,下一个扇区
CMP CL,18 ; CL与18比较
JBE readloop ; CL 小于或等于 18 时,如果未读完18个扇区,跳转到readloop
MOV CL,1 ; 从第一个扇区开始
ADD DH,1 ; 磁头+1
CMP DH,2 ;
JB readloop ; DH 小于 2 时,两个磁头未读完,跳转到readloop
MOV DH,0 ; 磁头0
ADD CH,1 ; 下一个柱面
CMP CH,CYLS
JB readloop ; CH 小于 CYLS 时,跳转到readloop
MOV SI,msg
JMP putloop
fin:
HLT ; 让CPU停止,等待指令
JMP fin ; 无限循环
error:
MOV SI,errormsg ; 将msg的地址存放到SI
putloop:
MOV AL,[SI] ; 将SI中的地址存放到AL
ADD SI,1 ; SI+1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
errormsg:
DB 0x0a, 0x0a ; 2个换行
DB "load error"
DB 0x0a ; 换行
DB 0
msg:
DB 0x0a, 0x0a ; 2个换行
DB "hello world"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 填写0x00,直到0x001fe
DB 0x55, 0xaa
这段程序使用了3个嵌套的循环,按照柱面、磁头、扇区的顺序遍历了10个柱面、2个磁头、总共360个扇区。每次读取一个扇区,即512个字节,将其存放在0x08200开始的内存中(虽然在程序中看到的是0x0820,但是由于ES寄存器中的地址要乘以16,因而实际是0x08200的地址处)。
需要注意的是,最早的512个字节是留给引导程序也就是这个程序本身的,所以在最开始是从0柱头第1磁头的第2个扇区开始读起。
总的而言,这段引导程序本身(共512字节)被装入了0x7c00一直到0x7dff内存中。然后利用了这段程序读取磁盘中剩余的359个扇区,存放到了0x08200开始的内存中,总共180KB。
此处出现的指令有:
JC和JNC
这两个指令是用来判断CF标识有没有值而进行跳转的。本例中,当读取没有错误时,该标识是0,反之则为1。因而可以根据该值采取不同的操作。
EQU
这是一个常量声明的语句。语法为:
常量名 EQU 值
此后,该常量可以被之后的程序所引用。
JA, JAE, JE, JBE, JB
这几个是条件跳转的语句。分别表示
- JA(Jump if Above ) 大于
- JAE(Jump if Above or Equal) 大于或等于
- JE(Jump if Equal) 等于
- JBE(Jump if Below or Equal) 小于或等于
- JB(Jump if Below ) 小于
32位模式并引入C语言
磁盘中的文件系统
在空磁盘中,如果写入一个文件,那么:
- 文件名写入地址0x002600以后的地方
- 文件内写入0x004200以后的地方
引导程序后续
根据这个特性,我们可以通过引导程序运行第一个文件内的程序。首先,在引导程序中,最开始的512个字节存放在了0x07c00的地方,之后读取的磁盘内容从0x08200开始,那么第一个文件的地址应该是0x08200-0x200(最早的512字节)+0x4200=0xc200.
因而只要将引导程序再跳转到0xc200处,然后在文件中指明ORG 0xc200,那么可以将我们需要在启动之后运行的程序写在一个文件中,并在引导程序执行之后执行。
如此一来,就将引导程序和系统程序分开了。
VGA模式
VGA模式需要用到显卡中断,编号位0x10
设置基本信息如下:
AH = 0x00
AL = 模式
0x03 : 80 * 25 * 16 ;16 色
0x12 : VGA 640 * 480 * 4 ;4位颜色
0x13 : VGA 320 * 200 * 8 ;8位颜色
0x6a : VGA 800 * 600 * 4 ;4位颜色
当然还有些其他的设置,都是直接写在规定的内存地址的。此次的程序如下:
; haribote-os
; TAB=4
; BOOT_INFO信息
CYLS EQU 0x0ff0 ; 设定启动区
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色数目的信息,位数
SCRNX EQU 0x0ff4 ; x分辨率
SCRNY EQU 0x0ff6 ; y分辨率
VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址
ORG 0xc200 ; 程序装载的位置
MOV AL,0x13 ; VGA图像、320x200x8bit颜色
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; 画面模式
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000
; 获取键盘各种LED指示灯状态
MOV AH,0x02
INT 0x16 ; 键盘 BIOS
MOV [LEDS],AL
fin:
HLT
JMP fin
这样就可以启动成一个一片乌漆抹黑的界面了。
引入C语言
接着就到了重点,引入C语言。此处,作者写了一堆汇编代码,但是说以后再解释,那就以后再说。
最终,我们的程序被拆成了3个汇编程序文件和一个C语言文件:
- 就是最早的512字节的引导程序
- 作者没做解释的部分,说是为了引入C语言写了这部分
- 汇编写的函数,可以供C语言直接调用。
这个文件的代码如下:
; 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来声明
函数部分就没啥好解释的了,HLT之后就返回(RET)
然后是C语言的调用
/* 告诉C编译器,有一个函数在别的文件里 */
void io_hlt(void);
/* 函数的声明,不用{},直接;意思是函数在别的文件里面,需要找一下 */
void HariMain(void)
{
fin:
io_hlt(); /* 执行naskfunc.nas中的_io_hlt函数 */
goto fin;
}
然后编译过程大致是这样的:
- 先把c文件和函数的汇编编译成目标文件
- 然后把1的目标文件连接起来
- 然后把为了引进C语言写的汇编编译一下
- 把2和3的文件组合起来生成一个sys文件
- 最后,用引导程序生成img文件,再将4的文件复制进去
这一章我断断续续花了3天才写完这些,需要仔细看源程序文件以及书中的说明,才能知道在干些什么。