《30天自制操作系统》从入门到放弃

前言

以下均是废话,大部分来自摘抄,只是记录本人放弃的过程!!!切记是摘抄!!!
相关资料在:https://download.csdn.net/download/wxkhturfun/22533044
内容为原书(pdf版,上面的字可以复制粘贴的那种)、原书源码、本博客的markdown文件。

01day

1.0写在前面的话

关于01day~03day,可以完全参考下述链接(下述内容部分也是来源此链接):

https://www.cnblogs.com/yucloud/category/1472969.html

1.1手打img

在下面这条链接中下载填制编辑软件

https://www.jb51.net/softs/601421.html#downintro2

(其实我不并推荐这种编辑器,使用vscode即可:https://www.cnblogs.com/baby123/p/14283440.html
(我推荐github上的一个高start软件:ImHex:https://github.com/WerWolv/ImHex)

之后输入以下内容:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
除上述三图外,其余填充均为零,一直填到168000处:
在这里插入图片描述
之后可以将helloo.img文件添加到VMware新建的操作系统的软盘上,此时注意去掉硬盘选项中的自动启动选项:
在这里插入图片描述在这里插入图片描述

此时开启该虚拟机即可正常显示

1.2从.nas到.img

双击 !cons_nt.bat 后输入下述内容:

..\z_tools\nask.exe helloos.nas helloos.img

其中helloos.nas的内容如下:

DB 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f
DB 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01, 0x01, 0x00
DB 0x02, 0xe0, 0x00, 0x40, 0x0b, 0xf0, 0x09, 0x00
DB 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00
DB 0x40, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x29, 0xff
DB 0xff, 0xff, 0xff, 0x48, 0x45, 0x4c, 0x4c, 0x4f
DB 0x2d, 0x4f, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41
DB 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00
RESB 16
DB 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
DB 0xee, 0xf4, 0xeb, 0xfd, 0x0a, 0x0a, 0x68, 0x65
DB 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72
DB 0x6c, 0x64, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 368
DB 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4600
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1469432

DB指令是“define byte”的缩写, 也就是往文件里直接写入1个字节的指令。

RESB指令是“reserve byte”的略写预约字节。如果想要从当前位置向后空出10个字节来,并且填0,如果后面18万行全是0x00的话 使用本命令可以省去填写18万行0x00的时间。

DD:4个字节

1.3对上一节的汇编的解释

; hello-os
; TAB=4
; 以下这段是标准FAT12格式软盘专用的代码
DB 0xeb, 0x4e, 0x90;eb 4e是jmp entry ,之所以是4e而不是50是因为jmp short是相对转移,4e是转移的位移量,你可以把书翻到第24页数一下从0x90开始到第25页的resb 18一共站的字节数,正好是4e个;0x90的意思是nop
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
DD 2880 ; 重写一次磁盘大小
DB 0,0,0x29 ; 意义不明, 固定
DD 0xffffffff ;(可能是) 卷标号码
DB "HELLO-OS " ; 磁盘的名称(11字节)
DB "FAT12 " ; 磁盘格式名称(8字节)
RESB 18 ; 先空出18字节
; 程序主体
DB 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
DB 0xee, 0xf4, 0xeb, 0xfd
; 信息显示部分
DB 0x0a, 0x0a ; 2个换行
DB "hello, world"
DB 0x0a ; 换行
DB 0
RESB 0x1fe-$ ; 填写0x00,直到 0x001fe
DB 0x55, 0xaa
; 以下是启动区以外部分的输出
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4600
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1469432

**RESB 0x1fe- , 这个美元符号是一个变量,可以告诉我们这一行现在的字节数(如果严格来说,有时候它还会有别的意思)。在这个程序里,我们已经在前面输出了 132 字节,所以这里的 , 这个美元符号是一个变量, 可以告诉我们这一行现在的字节数(如果严格来说, 有时候它还会有别的意思) 。 在这个程序里, 我们已经在前面输出了132字节, 所以这里的 ,这个美元符号是一个变量,可以告诉我们这一行现在的字节数(如果严格来说,有时候它还会有别的意思)。在这个程序里,我们已经在前面输出了132字节,所以这里的就是132。 因此nask先用0x1fe减去132, 得出378这一结果, 然后连续输出378个字节的0x00。那这里我们为什么不直接写378, 而非要用 呢?这是因为如果将显示信息从“ h e l l o , w o r l d ”变成“ t h i s i s a p e n . ”的话,中间要输出 0 x 00 的字节数也会随之变化。换句话说,我们必须保证软盘的第 510 字节(即第 0 x 1 f e 字节)开始的地方是 55 A A 。如果在程序里使用美元符号( 呢? 这是因为如果将显示信息从“hello,world”变成“this is a pen.”的话, 中间要输出0x00的字节数也会随之变化。 换句话说, 我们必须保证软盘的第510字节(即第0x1fe字节) 开始的地方是55 AA。 如果在程序里使用美元符号( 呢?这是因为如果将显示信息从hello,world变成thisisapen.”的话,中间要输出0x00的字节数也会随之变化。换句话说,我们必须保证软盘的第510字节(即第0x1fe字节)开始的地方是55AA。如果在程序里使用美元符号() 的话, 汇编语言会自动计算需要输出多少个00, 我们也就可以很轻松地改写输出信息了。 **

(boot sector) 软盘第一个的扇区称为启动区。 那么什么是扇区呢? 计算机读写软盘的时候, 并不是一个字节一个字节地读写的, 而是以512字节为一个单位进行读写。 因此,软盘的512字节就称为一个扇区。 一张软盘的空间共有1440KB, 也就是1474560字节, 除以512得2880, 这也就是说一张软盘共有2880个扇区。 那为什么第一个扇区称为启动区呢? 那是因为计算机首先从最初一个扇区开始读软盘, 然后去检查这个扇区最后2个字节的内容。

如果这最后2个字节不是0x55 AA, 计算机会认为这张盘上没有所需的启动程序, 就会报一个不能启动的错误。 (也许有人会问为什么一定是0x55AA呢? 那是当初的设计者随便定的, 笔者也没法解释) 。 如果计算机确认了第一个扇区的最后两个字节正好是0x55 AA, 那它就认为这个扇区的开头是启动程序, 并开始执行这个程序。

IPL ……………
initial program loader的缩写。 启动程序加载器。 启动区只有区区512字节, 实际的操作系统不像hello-os这么小, 根本装不进去。 所以几乎所有的操作系统, 都是把加载操作系统本身的程序放在启动区里的。 有鉴于此, 有时也将启动区称为IPL。 但hello-os没有加载程序的功能, 所以HELLOIPL这个名字不太顺理成章。 如果有人正义感特别强, 觉得“这是撒谎造假, 万万不能容忍! ”, 那也可以改成其他的名字。 但是必须起一个8字节的名字, 如果名字长度不到8字节的话, 需要在最后补上空格。

1.4关于FAT12

详见链接:https://www.cnblogs.com/yucloud/p/10943215.html

0x00到0x10(0x0F是15,0x10就是16,偏移量16就是0x10)

偏移量启动分区[boot sector]helloos里的值大小helloos值的说明0x0BS_jmpBoot(跳转指令)4E3bits跳转到指定地址4E0x3BS_OEMName(OEM、OS版本号)HELLOIPL8bits操作系统名字

零扇区BPB**[BIOS Parameter Block]**从下面内容可以看出它的作用,即储存了文件系统的参数信息

偏移量零扇区BPBhelloos里的值大小helloos值的说明
0xBBPB_BytesPersec(每扇字节数)5122bits可填512,1024,2048,4096,由于书里用的软盘,所以是512
0xCBPB_SecPerClus(每簇扇区数)11bit2^n, (n>=0)
0xDBPB_RsvdSecCnt(保留扇区数)12bitsFAT12/16硬性规定为1,FAT32为32
0x10BPB_NumFATs(FAT副本数)21bitFAT 建议写2

从0x11到20

偏移量helloos里的值大小helloos值的说明
0x11BPB_RootEntCnt(根目录项数)2242bitsFAT12/16为32的偶数倍,FAT32必须为0
0x13BPB_TotSec16(总扇区数)28802bitsFAT12/16填总扇区数
0x15BPB_Media(媒体描述符)0xf01bit可移动存储介质经常使用0xF0
0x16BPB_FATSz16(每FAT扇区数)92bitsFAT12/16一个FAT表所占的扇区数
0x18BPB_SecPerTrk(每磁道扇区数)182bits每磁道扇区数,这个数用于BIOS中断0x13
0x1ABPB_NumHeads(磁头数)22bits磁头数,也用于0x13中断
0x1CBPB_HiddSec(隐藏扇区数)04bitsFAT分区之前所隐藏的扇区数,调用0x13中断可得到,对于没有分区的储存介质,此域必须为0,具体使用什么由操作系统决定
0x20BPB_TotSec32(总扇区数)28804bitsFAT12/16中,若是总扇区>=10000(64KB),那么此域就是总扇区

02day

2.1寄存器

如果需要详细具体每个寄存器,请看下述链接:

https://www.cnblogs.com/FrankChen831X/p/10482718.html

这些寄存器全都是16位寄存器 (X:extend;因为早期的是8位,扩展到现在的16位)比如在这8个寄存器中, 不管使用哪一个, 差不多都能进行同样的计算, 但如果都用AX来进行各种运算的话, 程序就可以写得很简洁。“ADD CX,0x1234”编译成81 C1 34 12, 是一个4字节的命令。而 “ADD AX,0x1234”编译成05 34 12, 是一个3字节的命令。从上面例子可以看出, 这里所说的“程序可以写得简洁”是指“用机器语言写程序”的情况, 从汇编语言的源代码上是看不到这些区别的。CX是为方便计数而设计的, BX则适合作为计算内存地址的基点。

AX——accumulator, 累加寄存器
CX——counter, 计数寄存器
DX——data, 数据寄存器
BX——base, 基址寄存器
SP——stack pointer, 栈指针寄存器
BP——base pointer, 基址指针寄存器
SI——source index, 源变址寄存器
DI——destination index, 目的变址寄存器    

下面再给出8个8位寄存器:

AL——累加寄存器低位( accumulator low)
CL——计数寄存器低位( counter low)
DL——数据寄存器低位( data low)
BL——基址寄存器低位( base low)
AH——累加寄存器高位( accumulator high)
CH——计数寄存器高位( counter high)
DH——数据寄存器高位( data high)
BH——基址寄存器高位( base high)  

AX寄存器共有16位, 其中0位到7位的低8位称为AL, 而8位到15位的高8位称为AH。
那BP、 SP、 SI、 DI怎么没分为“L”和“H”呢? 能这么想, 就说明大家已经做到举一反三了, 但可惜的是这几个寄存器不能分为“L”和“H”。 如果无论如何都要分别取高位或低位数据的话, 就必须先用“MOV, AX, SI”将SI的值赋到AX中去, 然后再用AL、 AH来取值。 这貌似是英特尔(Intel) 的设计人员的思维模式。

EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI 这些就是32位寄存器。 这次的程序虽然没有用到它们, 但如果想用也是完全可以使用的。 在16位寄存器的名字前面加上一个E就是32位寄存器的名字了。 这个字母E其实还是来源于 “Extend”(扩展) 这个词。虽说EAX是个32位寄存器, 但其实跟前面一样, 它有一部分是与AX共用的, 32位中的低16位就是AX, 而高16位既没有名字, 也没有寄存器编号。 也就是说, 虽然我们可以把EAX作为2个16位寄存器来用, 但只有低16位用起来方便; 如果我们要用高16位的话, 就需要使用移位命令, 把高16位移到低16位后才能用。

下面再来几个段寄存器

ES——附加段寄存器(extra segment)
CS——代码段寄存器(code segment)
SS——栈段寄存器(stack segment)
DS——数据段寄存器(data segment)
FS——没有名称(segment part 2)
GS——没有名称(segment part 3)

2.2程序

helloos.nas:

; hello-os
; TAB=4

		ORG		0x7c00			; 指明程序装载地址

; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code

		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扇区1440*1024/512)
		DB		0xf0			; 磁盘类型(必须为0xf0)
		DW		9				; FAT的长度(必??9扇区)
		DW		18				; 一个磁道(track)有几个扇区(必须为18)
		DW		2				; 磁头数(必??2)
		DD		0				; 不使用分区,必须是0
		DD		2880			; 重写一次磁盘大小
		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 ES,AX;关于ES寄存器,请见3.1、3.2节内容以及3.3节中的图
            MOV SI,msg;由于在这里msg的地址是0x7c74, 所以这个指令就是把0x7c74代入到SI寄存器中去。
        putloop:
            MOV AL,[SI]
            ADD SI,1 ; 给SI加1
            CMP AL,0
            JE fin
            ;H、L是寄存器高低位,高位0x0e为当前光标处显示字符功能,低位为字符内容(这里通过把数组每个元素赋值给加法寄存器低位)
            MOV AH,0x0e ; 显示一个文字
            MOV BX,15 ; 指定字符颜色
            INT 0x10 ; 调用显卡BIOS
            JMP putloop
        fin:
            HLT ; 让CPU停止, 等待指令
            ;然而笔者讨厌让CPU毫无意义地空转。 如果没有HLT指令, CPU就会不停地全力去
            ;执行JMP指令, 这会使CPU的负荷达到100%, 非常费电。 这多浪费呀。 我们仅仅加
            ;上一个HLT指令, 就能让CPU基本处于睡眠状态, 可以省很多电
            JMP fin ; 无限循环
        ;按照ASCII码表,这个msg明显就是"\n\nhello, world\n\0"嘛,'\0'是字符串结束符 (NUL),'\0a'是换行符'\n'(LF)
        msg:
            DB 0x0a, 0x0a ; 换行2次
            DB "hello, world"
            DB 0x0a ; 换行
            DB 0

2.3 指令解释

MOV BYTE [678],123

这个指令是要用内存的“678”号地址来保存“123”这个数值

MOV WORD [678],123

在这种情况下, 内存地址中的678号和旁边的679号都会做出反应, 一共是16位。 这时, 123被解释成一个16位的数值, 也就是0000000001111011, 低位的01111011保存在678号, 高位的00000000保存在旁边的679号。
在这里插入图片描述

虽然我们可以用寄存器来指定内存地址, 但 只有BX、 BP、 SI、 DI这几个。 剩下的AX、 CX、 DX、 SP不能用来指定内存地址

所以想把DX内存里的内容赋值给AL的时候, 就会这样写:

MOV BX, DXMOV AL, BYTE [BX]  

根据以上说明我们知道可以用下面这个指令将SI地址的1字节内容读入到AL。

MOV AL, BYTE [SI]

可是MOV指令有一个规则1, 那就是源数据和目的数据必须位数相同。 也就是说,能向AL里代入的就只有BYTE, 这样一来就可以省略BYTE, 即可以写成:

MOV AL, [SI]  

下面这两条指令, 就相当于:**if (AL == 0) { goto fin; } **

CMP AL, 0JE fin
;显示一个字符AH=0x0e;AL=character code;BH=0;BL=color code;;返回值: 无;注: beep、 退格(back space) 、 CR、 LF都会被当做控制字符处理。所以, 如果大家按照这里所写的步骤, 往寄存器里代入各种值, 再调用INT 0x10,就能顺利地在屏幕上显示一个字符出来3

2.4内存分布

图片来源:https://download.csdn.net/download/tonyshengtan/6620387

在这里插入图片描述

在这里插入图片描述

可以看到0x07c00属于自由存储区,所以一开始就有:

ORG		0x7c00			; 指明程序装载地址

**0x00007c00-0x00007dff : 启动区内容的装载地址 **

在INT10下时有:

功能0AH
功能描述:在当前光标处按原有属性显示字符
入口参数:AH=0AH
AL=字符
BH=显示页码
BL=颜色(图形模式,仅适用于PCjr)
CX=重复输出字符的次数
出口参数:无

2.5 IPL

考虑到以后的开发, 我们不要一下子就用nask来做整个磁盘映像, 而是先只用它来制作512字节的启动区, 剩下的部分我们用磁盘映像管理工具来做, 这样以后用起来就方便了。

首先需要了解一下Makefile:http://www.ruanyifeng.com/blog/2015/02/make.html

make -r中的r代表不使用缺省规则

03day

在这里插入图片描述

上图是05day的图,与03day的构成类似:注意05day中程序的构成,注意baribote.sys文件由两个部分构成,而bootpack.hrb文件由三个文件构成(图中部分过程已经省略,只表示整体组成结构)

在ipl10.nas里有"JMP 0xc200",而asmhead.nas里写道:“ORG 0xc200”,也就是说ipl10.nas启动加载完毕后,跳转到0xc200执行asmhead.nas下的相关内容,具体见3.4节

3.1 磁盘读写

JC : Jump if carry如果进位标志为1,则跳转

MOV AX,0x0820
MOV ES,AXMOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区266
MOV AH,0x02 ; AH=0x02 : 读盘MOV AL,1 ; 1个扇区
MOV BX,0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOSJC error

磁盘读、 写, 扇区校验(verify) , 以及寻道(seek)
AH=0x02;(读盘)
AH=0x03;(写盘)
AH=0x04;(校验)
AH=0x0c;(寻道)
AL=处理对象的扇区数;(只能同时处理连续的扇区)
CH=柱面号 &0xff;
CL=扇区号(0-5位) |(柱面号&0x300) * * 2;
DH=磁头号;
DL=驱动器号;
ES:BX=缓冲地址; (校验及寻道时不使用)

返回值:
FLACS.CF0: 没有错误, AH0
FLAGS.CF==1: 有错误, 错误号码存入AH内(与重置(reset) 功能一样)
我们这次用的是AH=0x02, 哦, 原来是“读盘”的意思。

FLAGS.CF就是我们刚才讲到的进位标志。也就是说, 调用这个函数之后, 如果没有错, 进位标志就是0; 如果有错, 进位标志就是1。 这样我们就能明白刚才为什么要用JC指令了

在这里插入图片描述

CYLS	EQU		10;10个柱面CMP		CL,18	;18个扇区;共18*10=180个扇区

综上所述, 1张软盘有80个柱面, 2个磁头, 18个扇区, 且一个扇区有512字节。 所以, 一张软盘的容量是:80×2×18×512 = 1 474 560 Byte = 1 440KB

含有IPL的启动区, 位于C0-H0-S1(柱面0, 磁头0, 扇区1的缩写) , 下一个扇区是C0-H0-S2。 这次我们想要装载的就是这个扇区。 在有多个软盘驱动器的时候, 用磁盘驱动器号来指定从哪个驱动器的软盘上读取数据。 现在的电脑, 基本都只有1个软盘驱动器, 而以前一般都是2个。 既然现在只有
一个, 那不用多想, 指定0号就行了。

这就是“DH < 2 跳转到readloop”的原因:因为有两个磁头,磁头0和磁头1,而DH代表磁头号

3.2段寄存器

增加了一个叫EBX的寄存器, 这样就能处理4G内存了。 这是CPU能处理的最大内存量, 没有任何问题。 但EBX的导入是很久以后的事情, 在设计BIOS的时代, CPU甚至还没有32位寄存器, 所以当时只好设计了一个起辅助作用的段寄存器(segment register) 。 在指定内存地址的时候, 可以使用这个段寄存器,方法如下:

MOV AL,[ES:BX]

它代表ES×16+BX的内存地址,可以指定1M的内存(比不上4G)当然可以省略,一般如果省略的话就会
把“DS:”作为默认的段寄存器。以前我们用的“MOV CX,[1234]”, 其实是“MOV CX,[DS:1234]”的意思。 “MOV AL,[SI]”, 也就是“MOV AL,[DS:SI]”的意思。 在汇编语言中, 如果每回都这样写就太麻烦了, 所以可以省略默认的段寄存器DS。因为有这样的规则, 所以DS 必须预先指定为0, 否则地址的值就要加上这个数的16倍, 就会读写到其他的地方, 引起混乱。

这次, 我们指定了ES=0x0820, BX=0, 所以软盘的数据将被装载到内存中0x8200到0x83ff的地方。 可能有人会想, 怎么也不弄个整点的数, 比如0x8000什么的, 那多好。 但0x8000~0x81ff这512字节是留给启动区的, 要将启动区的内容读到那里, 所以就这样吧。
那为什么使用0x8000以后的内存呢? 这倒也没什么特别的理由, 只是因为从内存分布图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yjsxMix8-1631451901354)(./osmd/02day_2_内存分布图.bmp)]上看, 这一块领域没人使用, 于是笔者就决定将我们的“纸娃娃操作系统”装载到这一区域。 0x7c00~0x7dff用于启动区, 0x7e00以后直到0x9fbff为止的区域都没有特别的用途, 操作系统 可以随便使用

3.3磁盘error

中断向量表:

https://www.cnblogs.com/jadeshu/p/10663505.html

中断大全:必看

https://www.cnblogs.com/coderCaoyu/p/3638713.html

在这里插入图片描述

AH=
00H 未出错
01H 非法功能调用命令区。
02H 地址标记损坏, 扇区标识(ID) 无效或未找到。
03H 企图对有写保护的软盘执行写操作。
04H 所寻找的扇区没找到。
05H 复位操作失败。
06H 无介质。
07H 初始化错误, 数据未存在 DMA 的 64K 缓冲区内。
08H DMA 故障
09H DMA 边界错误, 数据未存在 DMA 的 64K 缓冲区内。
0AH 检测出错误码率的扇区标志。
0BH 所寻找的磁道没找到。
0CH 介质类型没发现。
0DH 扇区号有问题。
0EH 发现控制数据地址标记。
0FH 超出 DMA 边界
10H 读磁盘时奇偶校验错, 且纠错码(EDC) 不能纠正。
11H 读磁盘时奇偶校验错, 但纠错码(EDC) 已纠正错误。
20H 控制器错。
40H 查找操作无效。
80H 超时错误, 驱动器不响应。
AAH 驱动器未准备好。
BBH 不明错误。
CCH 被选驱动器出现写故障。
E0H 错误寄存器是零
FFH 非法操作

其他请详细“中断向量表”、“中断大全”两个链接

3.3程序

ipl10.nas

; haribote-ipl; TAB=4CYLS	EQU		10				; 声明CYLS=10		
ORG		0x7c00			; 指明程序装载地址; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code;……………………………………………………此处省略FAT12部分…………………………………………………………; 程序主体
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			; 扇区2readloop:		
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		retrynext:		MOV		AX,ES			; 把内存地址后移0x200(512/16十六进制转换)		
ADD		AX,0x0020		
MOV		ES,AX			; 
ADD ES,0x020因为没有ADD 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		
JB		readloop		; CH < CYLS 跳转到readloop; 读取完毕,跳转到haribote.sys执行!		
MOV		[0x0ff0],CH		; IPL把读到什么程度记下来		
JMP		0xc200error:		
MOV		SI,msgputloop:		
MOV		AL,[SI]		
ADD		SI,1			; 给SI加1		
CMP		AL,0		
JE		fin		
MOV		AH,0x0e			; 显示一个文字		
MOV		BX,15			; 指定字符颜色		
INT		0x10			; 调用显卡BIOS		
JMP		putloopfin:		
HLT						; 让CPU停止,等待指令		
JMP		fin				; 无限循环msg:		
DB		0x0a, 0x0a		; 换行两次		
DB		"load error"		
DB		0x0a			; 换行		
DB		0		
RESB	0x7dfe-$		; 填写0x00直到0x001fe		
DB		0x55, 0xaa

JBE : jump if below or equal

3.4 harbite.sys 为何为0x004200

下面, 我们先来编写一个非常短小的程序, 就只让它HLT。最简单的操作系统?

fin:HLTJMP fin

将以上内容保存为haribote.nas, 用nask编译, 输出成hanbote.sys。 到这里没什么难的。接下来, 将这个文件保存到磁盘映像haribote.img里

用二进制查看,0x004200那里, 可以看到“F4 EB FD”,这就是haribote.sys的内容。 因为我们用二进制编辑器看haribote.sys,它恰好也就是这三个字节。 好久没用的二进制编辑器这次又大显身手了。以上内容可以总结为:

一般向一个空软盘保存文件时,

  1. 文件名会写在0x002600以后的地方;
  2. 文件的内容会写在0x004200以后的地方。

那么, 要怎样才能执行磁盘映像上位于0x004200号地址的程序呢? 现在的程序是从启动区开始, 把磁盘上的内容装载到内存0x8000号地址, 所以磁盘0x4200处的内容就应该位于内存0x8000+0x4200=0xc200号地址。这样的话, 我们就往haribote.nas里加上ORG 0xc200, 然后在ipl.nas处理的最后加上JMP 0xc200这个指令。另外, 想要把磁盘装载内容的结束地址告诉给haribote.sys, 所以我们在“JMP 0xc200”之前, 加入了一行命令, 将CYLS(CYLS,cylinders 意为:磁盘的柱面)的值写到内存地址0x0ff0中。 这样启动区程序就算完成了

haribote.nas 因为内容的改变,其名字也要变:asmhead.nas:

asmhead.nas:

此处略去
  • 这次VRAM的值是0xa0000。 这个值又是从哪儿得来的呢? 还是来看看我们每次都参考的(AT) BIOS支持网页。 在INT 0x10的说明的最后写着, 这种画面模式
    下“VRAM是0xa0000~0xaffff的64KB”。

  • 键盘服务(Keyboard Service——INT 16H)

    (3)、功能02H和12H
    功能描述:读取键盘标志
    入口参数:AH=02H——普通键盘的移位标志

3.5图像显示

https://www.cnblogs.com/coderCaoyu/p/3638713.html在链接中可以看到中断10下的显示的相关设置

设定AH=0x00后, 调用显卡BIOS的函数, 这样就可以切换显示模式了。 我们还可以在支持网页(AT) BIOS里看看。设置显卡模式(video mode)
AH=0x00;
AL=模式: (省略了一些不重要的画面模式)
0x03: 16色字符模式, 80 × 25
0x12: VGA 图形模式, 640 × 480 × 4位彩色模式, 独特的4面存储模式
0x13: VGA 图形模式, 320 × 200 × 8位彩色模式, 调色板模式
0x6a: 扩展VGA 图形模式, 800 × 600 × 4位彩色模式, 独特的4面存储模式
(有的显卡不支持这个模式)
返回值: 无
参照以上说明, 我们暂且选择0x13画面模式, 因为8位彩色模式可以使用256种颜色, 这一点看来不错如果画面模式切换正常, 画面应该会变为一片漆黑。 也就是说, 因为可以看到画面的变化, 所以能判断程序是否运行正常。 由于变成了图形模式, 因此光标会消失

[VRAM]里保存的是0xa0000,在电脑的世界里, VRAM指的是显卡内存(videoRAM) , 也就是用来显示画面的内存。 这一块内存当然可以像一般的内存一样存储数据, 但VRAM的功能不仅限于此, 它的各个地址都对应着画面上的像素, 可以利用这一机制在画面上绘制出五彩缤纷的图案。

3.6切换到C语言

3.6节 目的:由于C语言中没有类似汇编中的HLT命令,所以为了实现待机的效果,进行C语言调用汇编,由此引入下述内容:

C语言源码 ——编译——> gas(GNU ASM汇编)——gas2nask.exe——> nas文件 ——NASK汇编器——> obj 文件 ————>编译用C写的源码(结果为GCC使用的GAS格式的汇编源码,即GNU ASM),然后转化成nas文件,再和nask汇编链接起来,最后再粗暴地合并几个相关的二进制文件。步骤如下:

  • 首先, 使用cc1.exe从bootpack.c生成bootpack.gas。

  • 第二步, 使用gas2nask.exe从bootpack.gas生成bootpack.nas。

  • 第三步, 使用nask.exe从bootpack.nas生成bootpack.obj。

  • 第四步, 使用obi2bim.exe从bootpack.obj生成bootpack.bim。

  • 最后, 使用bim2hrb.exe从bootpack.bim生成bootpack.hrb。

    naskfun.nas是将.c文件与汇编合并的一个中间文件

    naskfun.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

也就是说, 是用汇编语言写了一个函数。 函数名叫io_hlt。 虽然只叫hlt也行, 但在CPU的指令之中, HLT指令也属于I/O指令, 所以就起了这么一个名字。 顺便说一句, MOV属于转送指令, ADD属于演算指令。

用汇编写的函数, 之后还要与bootpack.obj链接, 所以也需要编译成目标文件。 因此将输出格式设定为WCOFF模式。 另外, 还要设定成32位机器语言模式。在nask目标文件的模式下, 必须设定文件名信息, 然后再写明下面程序的函数名。注意要在函数名的前面加上“_”, 否则就不能很好地与C语言函数链接。 需要链接的函数名, 都要用GLOBAL1指令声明。

https://www.cnblogs.com/zpc-uestc/p/10793759.html中提到在C语言中:

以单下划线(_)表明是标准库的变量

双下划线(__) 开头表明是编译器的变量

bootpack.c是被合并的C语言

bootpack.c:

/* 告诉C编译器,有一个函数在别的文件里 */
void io_hlt(void);/* 是函数声明却不用{},而用;,这表示的意思是:	函数在别的文件中,你自己找一下 */
void HariMain(void){fin:	
io_hlt(); /* 执行naskfunc.nas中的_io_hlt函数 */	
goto fin;}

3.7汇编与C

原文不解释汇编与C的调用关系,本人表示无奈。

C调用汇编,很简单:https://blog.csdn.net/listener51/article/details/78760718

bootpack.c:

/* 告诉C编译器,有一个函数在别的文件里 */
void io_hlt(void);/* 是函数声明却不用{},而用;,这表示的意思是:	函数在别的文件中,你自己找一下 */
void FUCKMain(void){fin:	
io_hlt(); /* 执行naskfunc.nas中的_io_hlt函数 */	
goto fin;}

使用make run会生成bootpack.nas

bootpack.nas:

[FORMAT "WCOFF"][INSTRSET "i486p"]
[OPTIMIZE 1][OPTION 1][BITS 32]	
EXTERN	_io_hlt[FILE "bootpack.c"][SECTION .text]	
GLOBAL	_FUCKMain_FUCKMain:	
PUSH	EBP
	MOV	EBP,ESPL2:	
CALL	_io_hlt	JMP	L2

至于汇编跳转到C:在asmhead.nas中有bootpack的加载地址,最后也是把该地址放入到ESI中,算是从汇编调到bootpack.c的入口地址了吧。关于汇编与C的链接详见:https://zhuanlan.zhihu.com/p/109983586

另外本人在查找HariMain的入口地址时看到:https://tieba.baidu.com/p/4434412756。基于此,修改bootback.c里的HariMain为FUCK,接着修改/z_tools/haribote/haribote.rul文件中的_HariStarup为 _FUCK,程序可以正常运行!

但还有一个问题,为啥从FUCK到_FUCK可以,从HariMain到 _HariStarup也可以,为啥多了个Starup还能正常运行呢?在haribote.rul文件里有如下内容(其中的日语我用有道翻译了一下):

format:	/* このセクションでリンクの方針を記述 */                        
//:这一节描述了链接的方针。	
code(align:1, logic:0x24,      file:0x24);	
data(align:4, logic:stack_end, file:code_end);file:
	/* このセクションでコマンドラインに書ききれなかった               
	//:我在这一段没能写完命令行		
.objファイル、.libファイルを記載 */                       
//:.记载obj文件、.lib文件	
/* なお、このセクションはフルパスで書いてもよい。 */             
//:另外,这个部分也可以写全通
/* 例:  c:/osask/gg00libc.lib;  */	
../z_tools/haribote/harilibc.lib;	
../z_tools/haribote/golibc.lib;label:	
/* 必ずリンクしなければいけないラベルを指定 */                  
//:指定必须链接的标签	
/* エントリポイントを指定すればいいと思ってください */          
 //:只要指定入口就可以了。	
_HariStartup;	
/* 上記3セクションの順序は入れ替えてはいけません! */             
//:上述3个部分的顺序不能调换!

以16进制方式打开golibc.lib,我没有发现什么,但是打开harilibc.lib可以看到如下:

在这里插入图片描述

图中可以看到_HariStartup字样,好家伙。

在这里插入图片描述

然后在最后一行,还可以看到_HariMain 和HariStartup字样,好吧,我确实看不懂.lib文件,但是这个.lib文件是写死的,不是生成的,原作已经设定好的,就这样吧,没啥意思。

04day

4.1 32位

我们想取得用参数指定的数字0x1234或0x56的内容, 就用MOV指令读入寄存器。因为CPU已经是32位模式, 所以我们积极使用32位寄存器。 16位寄存器也不是不能用, 但如果用了的话, 不只机器语言的字节数会增加, 而且执行速度也会变慢, 没什么好处。

在指定内存地址的地方, 如果使用16位寄存器指定[CX]或[SP]之类的就会出错, 但使用32位寄存器, 连[ECX]、 [ESP]等都OK, 基本上没有不能使用的寄存器。 真方便。 另外, 在指定地址时, 不光可以指定寄存器, 还可以使用往寄存器加一个常数, 或者减一个常数的方式。 另外说一下, 在16位模式下, 也能使用这种方式指定, 但那时候没有什么地方用得上, 所以没有使用。

如果与C语言联合使用的话, 有的寄存器能自由使用, 有的寄存器不能自由使用,能自由使用的只有EAX、 ECX、 EDX这3个。 至于其他寄存器, 只能使用其值, 而不能改变其值。 因为这些寄存器在C语言编译后生成的机器语言中, 用于记忆非常重要的值。 因此这次我们只用EAX和ECX。

这次还给naskfunc.nas增加了一行, 那就是INSTRSET指令。 它是用来告诉nask“这个程序是给486用的哦”, nask见了这一行之后就知道“哦, 那见了EAX这个词, 就解释成寄存器名”。 如果什么都不指定, 它就会认为那是为8086这种非常古老的、 而且只有16位寄存器的CPU而写的程序, 见了EAX这个词, 会误解成标签(Label) , 或是常数。 8086那时候写的程序中, 曾偶尔使用EAX来做标签, 当时也没想到这个单词后来会成为寄存器名而不能再随便使

如果写成“char* p,q;”, 我们看上去会觉得p和q都表示地址的变量, 但C编译器却不那样认为, q会被看作是一般的1字节的变量。 也就是被解释成“char p,q”。 为了避免这样的误解, 一般的程序员不写成“charp;”, 所以笔者也按照这个习惯编写程序。 另外, 如果想要声明两个地址变量, 就写成“char *q,*p;”。

4.2解释

今天的专栏写得好长呀, 我们来整理总结一下吧。 首先, 本书中出现的“char*p;”不必看作指针, 这是最重要的决窍。 p不是指针, 而是地址变量。 不要使用“p是指针”这种模棱两可的说法, “p是地址变量”这种说法比较好。

4.3VRAM

Video Random Access Memory 想要画东西的话, 只要往VRAM里写点什么就可以了

_write_mem8: ;
 void write_mem8(int addr, int data);
 MOV ECX,[ESP+4] ; [ESP + 4]中存放的是地址, 将其读入ECX
 MOV AL,[ESP+8] ; [ESP + 8]中存放的是数据, 将其读入ALMOV [ECX],ALRET
  • EDX:数据寄存器(Data Register),它的低16位即是DX,而DX又可分为高8位DH和低8位DL。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址;且总是被用来放整数除法产生的余数。

上述汇编很重要,因为后面C语言需要调用,但是写到后面作者用指针替换了上述代码,但是我感觉其中的原理有必要知道:https://blog.csdn.net/roshy/article/details/82499332 https://blog.csdn.net/weixin_42390670/article/details/96443575?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0.control&spm=1001.2101.3001.4242

ESP+4就指向第一个参数,+8就指向第二个参数,从而达到对参数进行操作的目的。

void HariMain(void){int i; /*变量声明。 变量i是32位整数*/
char *p; /*变量p, 用于BYTE型地址*/
for (i = 0xa0000; i <= 0xaffff; i++) {p = i; /*代入地址*/*p = i & 0x0f;/*这可以替代write_mem8(i, i & 0x0f);*/} 
for (;;) {io_hlt();}}

上面是用指针代替write_mem8

4.4汇编报错?

MOV [ 0x1234], 0x56

上句 会出错。 这是因为指定内存时, 不知道到底是BYTE, 还是WORD, 还是DWORD。 只有在另一方也是寄存器的时候才能省略, 其他情况都不能省略。其实C编译器也面临着同样的问题。 这次, 我们费劲写了一条C语句, 它的编译结
果相当于下面的汇编语句所生成的机器语言,

MOV [i], (i & 0x0f)

但却不知道[i]到底是BYTE, 还是WORD, 还是DWORD。 刚才就是出现了这种错误。那怎么才能告诉计算机这是BYTE呢?

char *p; /*, 变量p是用于内存地址的专用变量*/

声明一个上面这样变量p, p里放入与i相同的值, 然后执行以下语句。

p = i & 0x0f;

这样, C编译器就会认为“p 是地址专用变量, 而且是用于存放字符(char) 的, 所以就是BYTE.”。 顺便解释一下类似语句:

char *p; /*用于BYTE类地址*/
short *p; /*用于WORD类地址*/
int *p; /*用于DWORD类地址*/  

4.5本次的C程序

此处略去
  • 调色板的访问步骤。
  • 首先在一连串的访问中屏蔽中断(比如CLI) 。
  • 将想要设定的调色板号码写入0x03c8, 紧接着, 按R, G, B的顺序写入0x03c9。 如果还想继续设定下一个调色板, 则省略调色板号码, 再按照RGB的顺序写入0x03c9就行了。0x03c8、 0x03c9 是设备号,是固定的
  • 如果想要读出当前调色板的状态, 首先要将调色板的号码写入0x03c7, 再从0x03c9读取3次。 读出的顺序就是R, G, B。 如果要继续读出下一个调色板, 同样也是省略调色板号码的设定, 按RGB的顺序读出。
  • 如果最初执行了CLI, 那么最后要执行STI。

程序的头部罗列了很多的外部函数名, 这些函数必须在naskfunc.nas中写。 这有点麻烦, 但也没办法。 先跳过这一部分, 我们来看看主函数HariMain。 函数里只是增加了一行调用调色板置置的函数, 变更并不是太大。

set_palette中想要做的事情是在设定调色板之前首先执行CLI, 但处理结束以后一定要恢复中断标志, 因此需要记住最开始的中断标志是什么。 所以我们制作了一个函数io_load_eflags, 读取最初的eflags值。 处理结束以后, 可以先看看eflags的内容,再决定是否执行STI, 但仔细想一想, 也没必要搞得那么复杂, 干脆将eflags的值代入EFLAGS, 中断标志位就恢复为原来的值了。 函数o_store_eflags就是完成这个处理的。

3.5图像显示中,提到了调色板模式,在这个模式下,由程序员自定义调色板,然后调用。

4.6 IN命令&OUT命令

既然CPU与设备相连, 那么就有向这些设备发送电信号, 或者从这些设备取得信息的指令。 向设备发送电信号的是OUT指令; 从设备取得电气信号的是IN指令。 正如为了区别不同的内存要使用内存地址一样, 在OUT指令和IN指令中, 为了区别不同的设备, 也要使用设备号码。 设备号码在英文中称为port(端口) 。 port原意为“港口”, 这里形象地将CPU与各个设备交换电信号的行为比作了船舶的出港和进港。

IN AL, 21H    ;表示从21H端口读一个字节数据到AL;
OUT 21H,AL    ;表示将AL持有的数据写入21H端口

4.7汇编指令

下面是一段话,不是一段指令

;如果有 MOV EAX,EFLAGS;之类的指令就简单了, 但CPU没有这种指令。 能够用来读写EFLAGS的, 只有PUSHFD和POPFD指令。  ;USHFD是“push flags double-word”的缩写  ;POPFD是“pop flags double-word”的缩写;PUSHFD POP EAX是指首先将EFLAGS压入栈, 再将弹出的值代入EAX。 所以说它代替了;MOV EAX,EFLAGS; 另一方面, PUSH EAX POPFD正与此相反, 它相当于MOV EFLAGS,EAX
_io_load_eflags:	;
 int io_load_eflags(void);		
 PUSHFD		; 
 PUSH EFLAGS 		
 POP		EAX		RET

根据C语言的规约, 执行RET语句时, EAX中的值就被看作是函数的返回值,

4.8 画 画

有的显卡模式0xa0000到0xaffff这64kb内存地址。

在当前画面模式中, 画面上有320×200(=64 000) 个像素。 假设左上点的坐标是(0,0) , 右下点的坐标是(319,319) , 那么像素坐标(x,y) 对应的VRAM地址应按下式计算。
0xa0000 + x + y * 320

void HariMain(void){    char *p; /* p变量的地址 */    
init_palette(); /* 设置调色板 */   
 p = (char *) 0xa0000; /* 将地址赋值进去 */ 
boxfill8(p, 320, COL8_FF0000, 20, 20, 120, 120); 
boxfill8(p, 320, COL8_00FF00, 70, 50, 170, 150);  
boxfill8(p, 320, COL8_0000FF, 120, 80, 220, 180);  
for (;;) {    	io_hlt();	}}

上述代码是绘制三个矩形

在这里插入图片描述

05day

5.1 主程序节选

bootpack.c:

void HariMain(void){
char *vram;int xsize, ysize;
short *binfo_scrnx, *binfo_scrny;
int *binfo_vram;init_palette();
binfo_scrnx = (short *) 0x0ff4;
binfo_scrny = (short *) 0x0ff6;
binfo_vram = (int *) 0x0ff8;
xsize = *binfo_scrnx;ysize = *binfo_scrny;
vram = (char *) *binfo_vram;

这里出现的0x0ff4之类的地址到底是从哪里来的呢? 其实这些地址仅仅是为了与asmhead. nas保持一致才出现的。

而在asmhead.nas中我只找到如下内容:

; BOOT_INFO相关CYLS	EQU		0x0ff0			; 引导扇区设置
LEDS	EQU		0x0ff1V
MODE	EQU		0x0ff2			; 关于颜色的信息
SCRNX	EQU		0x0ff4			; 分辨率
XSCRNY	EQU		0x0ff6			; 分辨率
YVRAM	EQU		0x0ff8			; 图像缓冲区的起始地址

回到bootpack.c:

struct BOOTINFO {
char cyls, leds, vmode, reserve;
short scrnx, scrny;
char *vram;};
void HariMain(void){
char *vram;int xsize, ysize;
struct BOOTINFO *binfo;init_palette();binfo = (
struct BOOTINFO *) 0x0ff0;
xsize = (*binfo).scrnx;
ysize = (*binfo).scrny;vram = (*binfo).vram;

binfo是boot info的作者自定义写法,scrn:screen

(*binfo).scrnx也可以写成binfo→scrnx

为了表示其中的scrnx, 使用了(binfo) .scrnx这种写法。 如果不加括号直接写成binfo.scrnx, 虽然更容易懂, 但编译器会误解成*(binfo.scrnx) , 出现错误。 所以, 括号虽然不太好看, 但不能省略

binfo = (struct BOOTINFO *)0x0ff0;//本来想写“binfo =0x0ff0;”的, 但由于总出警告, 很讨厌, 所以我们就进行了类型转换。

以前我们显示字符主要靠调用BIOS函数, 但这次是32位模式, 不能再依赖BIOS了

5.2字体

我们这次就将hankaku.txt这个文本文件加入到我们的源程序大家庭中来。 这个文件的内容如下:
hankaku.txt的内容
char 0x41









..
.
.
.
.
.
.



■■■■■
当然, 这既不是C语言, 也不是汇编语言, 所以需要专用的编译器。 新做一个编译器很麻烦, 所以我们还是使用在制作OSASK时曾经用过的工具(makefont.exe) 。说是编译器, 其实有点言过其实了, 只不过是将上面这样的文本文件(256个字符的字体文件) 读进来, 然后输出成16×256=4096字节的文件而已。编译后生成hankaku.bin文件, 但仅有这个文件还不能与bootpack.obj连接, 因为它不是目标(obj) 文件。 所以, 还要加上连接所必需的接口信息, 将它变成目标文件。 这项工作由bin2obj.exe来完成。 它的功能是将所给的文件自动转换成目标程序, 就像将源程序转换成汇编那样。 也就是说, 好像将下面这两行程序编译成了汇编

_hankanku:	DB 各种数据(共4096字节)

当然, 如果大家不喜欢现在这种字体的话, 可以随便修改hankaku.txt。

如果在C语言中使用这种字体数据, 只需要写上以下语句就可以了。

extern char hankaku[4096];

像这种在源程序以外准备的数据, 都需要加上extern属性。 这样, C编译器就能够知道它是外部数据, 并在编译时做出相应调整。 当在一个源文件中需要访问同一工程下的另一个文件中的全局变量时,我们用到extern。extern用在变量或函数的声明前,用来说明这个变量或函数是在别处定义的,要在这里引用。另外如上extern在HariMain函数内部声明,则该变量不能在函数以外调用。我们会想如果要调用另一个文件中的变量或函数,干嘛不用include,这是因为extern能加快编译。但是你不能说我用了一个extern就可以吧,其实具体还是要在Makefile里进行设定的:

hankaku.bin : hankaku.txt Makefile	$(MAKEFONT) hankaku.txt hankaku.binhankaku.obj : hankaku.bin Makefile	$(BIN2OBJ) hankaku.bin hankaku.obj _hankakubootpack.bim : bootpack.obj naskfunc.obj hankaku.obj Makefile	$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \		bootpack.obj naskfunc.obj hankaku.obj

把.txt搞成.bin,再把.bin搞成.obj和_hankaku,最后与其他.obj一起整合成.img文件

OSASK的字体数据, 依照一般的ASCII字符编码, 含有256个字符。 A的字符编码是0x41, 所以A的字体数据, 放在自“hankaku + 0x41 * 16”开始的16字节里。 C语言中A的字符编码可以用’A’来表示, 正好可以用它来代替0x41, 所以也可以写成“hankaku + ‘A’ * 16”。

5.3字体打印到屏幕上

void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s){
extern char hankaku[4096];
for (; *s != 0x00; s++) {
putfont8(vram, xsize, x, y, c, hankaku + *s * 16);x += 8;}
return;}

C语言中, 字符串都是以0x00结尾的, 所以可以这么写。 函数名带着asc, 是为了提醒笔者字符编码使用了ASCII。这里还要再说明一点, 所谓字符串是指按顺序排列在内存里, 末尾加上0x00而组成的字符编码。 所以s是指字符串前头的地址, 而使用*s就可以读取字符编码。

5.4 debug

在开始的时候, 我们曾提到过, 自制操作系统中不能随便使用printf函数, 但sprintf可以使用。 因为sprintf不是按指定格式输出, 只是将输出内容作为字符串写在内存中。

这个sprintf函数, 是本次使用的名为GO的C编译器附带的函数。 它在制作者的精心设计之下能够不使用操作系统的任何功能。 或许有人会认为, 什么呀, 那样的话,怎么不做个printf函数呢? 这是因为输出字符串的方法, 各种操作系统都不一样,不管如何精心设计, 都不可避免地要使用操作系统的功能。 而sprintf不同, 它只对内存进行操作, 所以可以应用于所有操作系统。

sprintf函数的使用方法是: sprintf(地址, 格式, 值, 值, 值, ……) 。 这里的地址指定所生成字符串的存放地址。 格式基本上只是单纯的字符串, 如果有%d这类记号, 就置换成后面的值的内容。 除了%d, 还有%s, %x等符号, 它们用于指定数值以什么方式变换为字符串。 %d将数值作为十进制数转化为字符串, %x将数值作为十六进制数转化为字符串。
关于格式的详细说明

%d单纯的十进制数
%5d5位十进制数。 如果是123, 则在前面加上两个空格, 变成" 123", 强制达到5位
%05d5位十进制数。 如果是123, 则在前面加上0, 变成"00123", 强制达到5位
%x单纯的十六进制数。 字母部分使用小写abcdef
127%X单纯的十六进制数。 字母部分使用大写ABCDEF
%5x5位十六进制数。 如果是456(十进制) , 则在前面加上两个空格, 变成" 1c8", 强制达到5位。 还有%5X的形式
%05x5位十六进制数。 如果是456(十进制) , 则在前面加上两个0, 变成"001c8", 强制达到5位。 还有%05X的形式

5.5 鼠标

bc : back color背景色

实在不敢恭维,下面这个图就是鼠标。

void init_mouse_cursor8(
char *mouse, char bc)/* 准备鼠标光标(16x16) */{	
static char cursor[16][16] = {		"**************..",		"*OOOOOOOOOOO*...",		"*OOOOOOOOOO*....",		"*OOOOOOOOO*.....",		"*OOOOOOOO*......",		"*OOOOOOO*.......",		"*OOOOOOO*.......",		"*OOOOOOOO*......",		"*OOOO**OOO*.....",		"*OOO*..*OOO*....",		"*OO*....*OOO*...",		"*O*......*OOO*..",		"**........*OOO*.",		"*..........*OOO*",		"............*OO*",		".............***"	};	
int x, y;	
for (y = 0; y < 16; y++) {		
	for (x = 0; x < 16; x++) {			
	if (cursor[y][x] == '*') {				mouse[y * 16 + x] = COL8_000000;			}	
				if (cursor[y][x] == 'O') {				mouse[y * 16 + x] = COL8_FFFFFF;	}					
						 if (cursor[y][x] == '.') {				mouse[y * 16 + x] = bc;			}		}	}	
	return;
	}

5.6闲谈

英文是paging。 “分段”的基本思想是将4GB的内存分割; 而分页的思想是有多少个任务就要分多少页, 还要对内存进行排序。

需要注意的一点是, 我们用16位的时候曾经讲解过的段寄存器。 这里的分段, 使用的就是这个段寄存器。 但是16位的时候, 如果计算地址, 只要将地址乘以16就可以了。 但现在已经是32位了, 不能再这么用了。 如果写成“MOV AL,[DS:EBX]”,CPU会往EBX里加上某个值来计算地址, 这个值不是DS的16倍, 而是DS所表示的段的起始地址。 即使省略段寄存器(segment register) 的地址, 也会自动认为是指定了DS。 这个规则不管是16位模式还是32位模式, 都是一样的。

按这种分段方法, 为了表示一个段, 需要有以下信息。

  • 段的大小是多少

  • 段的起始地址在哪里

  • 段的管理属性(禁止写入, 禁止执行, 系统专用等)

CPU用8个字节(=64位) 的数据来表示这些信息。 但是, 用于指定段的寄存器只有16位。 或许有人会猜想在32位模式下, 段寄存器会扩展到64位, 但事实上段寄存器仍然是16位。

调色板中, 色号可以使用0~255的数。 段号可以用0~8191的数。 因为段寄存器是16位, 所以本来应该能够处理0~65535范围的数, 但由于CPU设计上的原因, 段寄存器的低3位不能使用。 因此能够使用的段号只有13位, 能够处理的就只有位于0~8191的区域了。

段号怎么设定呢? 这是对于CPU的设定, 不需要像调色板那样使用io_out(由于不是外部设备, 当然没必要) 。 但因为能够使用0~8191的范围, 即可以定义8192个段, 所以设定这么多段就需要8192×8=65 536字节(64KB) 。 大家可能会想, CPU没那么大存储能力, 不可能存储那么多数据, 是不是要写入到内存中去呀。 不错,正是这样。 这64KB(实际上也可以比这少) 的数据就称为GDT。

GDT是“global(segment) descriptor table”的缩写, 意思是全局段号记录表。 将这些数据整齐地排列在内存的某个地方, 然后将内存的起始地址和有效设定个数放在CPU内被称作GDTR5的特殊寄存器中, 设定就完成了。global (segment) descriptor table register的缩写。

另外, IDT是“interrupt descriptor table”的缩写, 直译过来就是“中断记录表”。 当CPU遇到外部状况变化, 或者是内部偶然发生某些错误时, 会临时切换过去处理这种突发事件。 这就是中断功能。

(此处略去为何要使用中断的缘由…)讲了这么长, 其实总结来说就是: 要使用鼠标, 就必须要使用中断。 所以, 我们必须设定IDT。 IDT记录了0~255的中断号码与调用函数的对应关系, 比如说发生了123号中断, 就调用○×函数, 其设定方法与GDT很相似(或许是因为使用同样的方法能简化CPU的电路) 。

如果段的设定还没顺利完成就设定IDT的话, 会比较麻烦, 所以必须先进行GDT的设定。

5.7 程序节选

.c文件:

此处略去

SEGMENT_DESCRIPTOR中存放GDT的8字节的内容, 它无非是以CPU的资料为基础, 写成了结构体的形式。 同样, GATE_DESCRIPTOR中存放IDT的8字节的内容, 也是以CPU的资料为基础的。
变量gdt被赋值0x00270000, 就是说要将0x270000~0x27ffff设为GDT。 至于为什么用这个地址, 其实那只是笔者随便作出的决定, 并没有特殊的意义。 从内存分布图可以看出这一块地方并没有被使用。变量idt也是一样, IDT被设为了0x26f800~0x26ffff。 顺便说一下, 0x280000~0x2fffff已经有了bootpack.h。 “哎? 什么时候? 我可没听说过这事哦! ”大家可能会有这样的疑问, 其实是后面要讲到的“asmhead.nas”帮我们做了这样的处理。
■■■■■
现在继续往下说明。

for (i = 0; i < 8192; i++) {set_segmdesc(gdt + i, 0, 0, 0);}

请注意一下以上几行代码。 gdt是0x270000, i从0开始, 每次加1, 直到8 191。 这样一来, 好像gdt+i最大也只能是0x271fff。 但事实上并不是那样。 C语言中进行指针的加法运算时, 内部还隐含着乘法运算。 变量gdt已经声明为指针, 指向SEGMENT_DESCRIPTOR这样一个8字节的结构体, 所以往gdt里加1, 结果却是地址增加了8。因此这个for 语句就完成了对所有8192个段的设定, 将它们的上限(limit, 指段的字节数-1) 、 基址(base) 、 访问权限都设为0。再往下还有这样的语句:

set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);

以上语句是对段号为1和2的两个段进行的设定。 段号为1的段, 上限值为0xffffffff即大小正好是4GB) , 地址是0, 它表示的是CPU所能管理的全部内存本身。 段的属性设为0x4092, 它的含义我们留待明天再说。 下面来看看段号为2的段, 它的大小是512KB, 地址是0x280000。 这正好是为bootpack.hrb而准备的。 用这个段, 就可以执行bootpack.hrb。 因为bootpack.hrb是以ORG 0为前提翻译成的机器语言。
■■■■■
下一个语句是:

load_gdtr(0xffff, 0x00270000);

这是因为依照常规, C语言里不能给GDTR赋值, 所以要借助汇编语言的力量, 仅此而已。再往下都是关于IDT的记述, 因为跟前面一样, 所以应该没什么问题。

程序节选2:

void putblock8_8(char *vram, int vxsize, int pxsize,	int pysize, int px0, int py0, char *buf, int bxsize)
{	int x, y;	for (y = 0; y < pysize; y++) {		
for (x = 0; x < pxsize; x++) {			
vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x];		
}	}	

下图是绘制A点对应关系:

在这里插入图片描述

06day

6.1程序分割

在这里插入图片描述

在这里插入图片描述

由于分割后,文件看的有点不顺,所以建议将05day的代码与06day的代码对比一下

6.2整理Makefile

分割虽然成功了, 但现在Makefile又有点长了, 足足有113行。 虽说出现这种情况是
情有可原, 但是, 像这样:

bootpack.gas : bootpack.c Makefile$(CC1) -o bootpack.gas bootpack.cgraphic.gas : graphic.c Makefile$(CC1) -o graphic.gas graphic.cdsctbl.gas : dsctbl.c Makefile$(CC1) -o dsctbl.gas dsctbl.c或者像这样:bootpack.nas : bootpack.gas Makefile$(GAS2NASK) bootpack.gas bootpack.nasgraphic.nas : graphic.gas Makefile$(GAS2NASK) graphic.gas graphic.nasdsctbl.nas : dsctbl.gas Makefile$(GAS2NASK) dsctbl.gas dsctbl.nas

它们做的都是同样的事。 为什么要写这么多同样的东西呢? 每次增加新的源文件,都要像这样增加这么多雷同的编译规则, 看着都烦。
其实有一个技巧可以将它们归纳起来, 这就是利用一般规则。 我们可以把上面6个独立的文件生成规则, 归纳成以下两个一般规则。

%.gas : %.c Makefile$(CC1) -o $*.gas $*.c%.nas : %.gas Makefile$(GAS2NASK) $*.gas $*.nas

make.exe会首先寻找普通的生成规则, 如果没找到, 就尝试用一般规则。 所以, 即使一般规则和普通生成规则有冲突, 也不会有问题。 这时候, 普通生成规则的优先级更高。 比如虽然某个文件的扩展名也是.c, 但是想用单独的规则来编译它, 这也没问题。

6.3 引入头文件

bootpack.h的内容

/* asmhead.nas */
struct BOOTINFO { /* 0x0ff0-0x0fff */
char cyls; /* 启动区读硬盘读到何处为止 */
char leds; /* 启动时键盘LED的状态 */
char vmode; /* 显卡模式为多少位彩色 */
char reserve;
short scrnx, scrny; /* 画面分辨率 */
char *vram;};
#define ADR_BOOTINFO 0x00000ff0/* naskfunc.nas */
void io_hlt(void);void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags);
void load_gdtr(int limit, int addr);
void load_idtr(int limit, int addr);/* graphic.c */
void init_palette(void);void set_palette(int start, int end, unsigned char *rgb);
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);
void init_screen8(char *vram, int x, int y);(以下略)

引入头文件,避免要把上述的各个函数声明写入每一个.c文件里

6.4 GDT

好了, 现在来详细讲一下昨天遗留下来的问题。 首先来说明一下naskfunc.nas的
_load_gdtr。

load_gdtr: ; void load_gdtr(int limit, int addr);MOV AX,[ESP+4] ; limitMOV [ESP+6],AXLGDT [ESP+6]RET

这个函数用来将指定的段上限(limit) 和地址值赋值给名为GDTR的48位寄存器。这是一个很特别的48位寄存器, 并不能用我们常用的MOV指令来赋值。 给它赋值的时候, 唯一的方法就是指定一个内存地址, 从指定的地址读取6个字节(也就是48位) , 然后赋值给GDTR寄存器。 完成这一任务的指令, 就是LGDT。该寄存器的低16位1(即内存的最初2个字节) 是段上限, 它等于“GDT的有效字节数 - 1”。 今后我们还会偶尔用到上限这个词, 意思都是表示量的大小, 一般为“字
节数 - 1”。 剩下的高32位(即剩余的4个字节) , 代表GDT的开始地址。

在最初执行这个函数的时候, DWORD[ESP + 4]里存放的是段上限,DWORD[ESP+8]里存放的是地址。 具体到实际的数值, 就是0x0000ffff和
0x00270000。 把它们按字节写出来的话, 就成了[FF FF 00 00 00 27 00](要注意低位放在内存地址小的字节里2) 。 为了执行LGDT, 笔者希望把它们排列成[FF FF 00 00 00 27 00]的样子, 所以就先用“MOV AX,[ESP + 4]”读取最初的0xffff, 然后再写到[ESP + 6]里。 这样, 结果就成了[FF FF FF FF 00 27 00 00], 如果从[ESP + 6]开始读6字节的话, 正好是我们想要的结果。

6.3本次的dsctbl.c节选

struct SEGMENT_DESCRIPTOR {
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;};
void set_segmdesc(
struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
	{if (limit > 0xfffff) {
		ar |= 0x8000; /* G_bit = 1 */
		limit /= 0x1000;}
		sd->limit_low = limit & 0xffff;
		sd->base_low = base & 0xffff;
		sd->base_mid = (base >> 16) & 0xff;
		sd->access_right = ar & 0xff;
		sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
		sd->base_high = (base >> 24) & 0xff;
return;}

说到底, 这个函数是按照CPU的规格要求, 将段的信息归结成8个字节写入内存
的。 这8个字节里到底填入了什么内容呢? 昨天已经讲到, 有以下3点:

  • 段的大小
  • 段的起始地址
  • 段的管理属性(禁止写入, 禁止执行, 系统专用等)

    首先看一下段的地址。 地址当然是用32位来表示。 这个地址在CPU世界的语言里,被称为段的基址。 所以这里使用了base这样一个变量名。 在这个结构体里base又分为low(2字节) , mid(1字节) , high(1字节) 3段, 合起来刚好是32位。 所以,这里只要按顺序分别填入相应的数值就行了。 虽然有点难懂, 但原理很简单。 程序中使用了移位运算符和AND运算符往各个字节里填入相应的数值。为什么要分为3段呢? 主要是为了与80286时代的程序兼容。 有了这样的规格,80286用的操作系统, 也可以不用修改就在386以后的CPU上运行了。
    ■■■■■
    下面再说一下段上限。 它表示一个段有多少个字节。 可是这里有一个问题, 段上限最大是4GB, 也就是一个32位的数值, 如果直接放进去, 这个数值本身就要占用4个字节, 再加上基址(base) , 一共就要8个字节, 这就把整个结构体占满了。 这样一来, 就没有地方保存段的管理属性信息了, 这可不行。因此段上限只能使用20位。 这样一来, 段上限最大也只能指定到1MB为止。 明明有4GB, 却只能用其中的1MB, 有种又回到了16位时代的错觉, 太可悲了。 在这里英特尔的叔叔们又想了一个办法, 他们在段的属性里设了一个标志位, 叫做Gbit。 这个标志位是1的时候, limit的单位不解释成字节(byte) , 而解释成页(page) 。 页是什么呢? 在电脑的CPU里, 1页是指4KB。这样一来, 4KB × 1M = 4GB, 所以可以指定4GB的段。 总算能放心了。 顺便说一
    句, G bit的“G”, 是“granularity”的缩写, 是指单位的大小。这20位的段上限分别写到limit_low和limit_high里。 看起来它们好像是总共有3字节, 即24位, 但实际上我们接着要把段属性写入limit_high的高4位里, 所以最后段上限还是只有20, 好复杂呀。
    ■■■■■
    最后再来讲一下12位的段属性。 段属性又称为“段的访问权属性”, 在程序中用变量名access_right或ar来表示。 因为12位段属性中的高4位放在limit_high的高4位里, 所以程序里有意把ar当作如下的16位构成来处理:
    xxxx0000xxxxxxxx(其中x是0或1)
    ar的高4位被称为“扩展访问权”。 为什么这么说呢? 因为这高4位的访问属性在80286的时代还不存在, 到386以后才可以使用。 这4位是由“GD00”构成的, 其中G是指刚才所说的G bit, D是指段的模式, 1是指32位模式, 0是指16位模式。 这里出现的16位模式主要只用于运行80286的程序, 不能用于调用BIOS。 所以, 除了运行80286程序以外, 通常都使用D=1的模式。
    ■■■■■
    ar的低8位从80286时代就已经有了, 如果要详细说明的话, 够我们说一天的了, 所
    以这里只是简单地介绍一下。
    00000000(0x00) : 未使用的记录表(descriptor table) 。
    10010010(0x92) : 系统专用, 可读写的段。 不可执行。
    10011010(0x9a) : 系统专用, 可执行的段。 可读不可写。
    11110010(0xf2) : 应用程序用, 可读写的段。 不可执行。
    11111010(0xfa) : 应用程序用, 可执行的段。 可读不可写。
    “系统专用”, “应用程序用”什么的, 听着让人摸不着头脑。 都是些什么东西呀? 在32位模式下, CPU有系统模式(也称为“ring0”3) 和应用模式(也称为“ring3”) 之分。 操作系统等“管理用”的程序, 和应用程序等“被管理”的程序, 运行时的模式是不同的。除此之外, 还有ring1和ring2, 这些中间阶段, 由device driver(设备驱动器)等使用。 ring原意是轮子或环, 有时用它来表示阶段, 故得此名。比如, 如果在应用模式下试图执行LGDT等指令的话, CPU则对该指令不予执行,并马上告诉操作系统说 “那个应用程序居然想要执行LGDT, 有问题! ”。 另外, 当应用程序想要使用系统专用的段时, CPU也会中断执行, 并马上向操作系统报告“那个应用程序想要盗取系统信息。 也有可能不仅要盗取信息, 还要写点东西来破坏系统呢。 ” “想要盗取系统信息这一点我明白, 但要阻止LGDT的执行这一点, 我还是不懂。 ”可能有人会有这种疑问。 当然要阻止啦。 因为如果允许应用程序执行LGDT,那应用程序就会根据自己的需要, 偷偷准备GDT, 然后重新设定LGDT来让它执行自己准备的GDT。 这可就麻烦了。 有了这个漏洞, 操作系统再怎么防守还是会防不胜防。CPU到底是处于系统模式还是应用模式, 取决于执行中的应用程序是位于访问权为0x9a的段, 还是位于访问权为0xfa的段。

6.4 背景知识

在这里插入图片描述

鼠标移动要使用PIC中断(programmable interrupt controller )。

随着PIC的初始化, 会产生一次IRQ7中断, 如果不对该中断处理程序执行STI(设置中断标志位, 见第4章) , 操作系统的启动会失败。

另外, 从PIC通过第2号IRQ与主PIC相连。 主板上的配线就是这样, 无法用软件来改变。

void init_pic(void)/* PIC的初始化 */{
	io_out8(PIC0_IMR, 0xff ); /* 禁止所有中断 */
	io_out8(PIC1_IMR, 0xff ); /* 禁止所有中断 */
	io_out8(PIC0_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */
	io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7由INT20-27接收 */
	io_out8(PIC0_ICW3, 1 << 2); /* PIC1由IRQ2连接 */
	io_out8(PIC0_ICW4, 0x01 ); /* 无缓冲区模式 */
	io_out8(PIC1_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */
	io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由INT28-2f接收 */
	io_out8(PIC1_ICW3, 2 ); /* PIC1由IRQ2连接 */
	io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区模式 */
	io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外全部禁止 */
	io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所有中断 */return;
	}

以上是PIC的初始化程序。 从CPU的角度来看, PIC是外部设备, CPU使用OUT指令进行操作。 程序中的PIC0和PIC1, 分别指主PIC和从PIC。 PIC内部有很多寄存器,用端口号码对彼此进行区别, 以决定是写入哪一个寄存器。具体的端口号码写在bootpack.h里 , 请参考这个程序。 但是, 端口号相同的东西有很多, 可能会让人觉得混乱。 不过笔者并没有搞错, 写的是正确的。 因为PIC有些很细微的规则, 比如写入ICW1之后, 紧跟着一定要写入ICW2等, 所以即使端口号
相同, 也能够很好地区别开来 。

现在简单介绍一下PIC的寄存器。 首先, 它们都是8位寄存器。 IMR是“interrupt maskregister”的缩写, 意思是“中断屏蔽寄存器”。 8位分别对应8路IRQ信号。 如果某一位的值是1, 则该位所对应的IRQ信号被屏蔽, PIC就忽视该路信号。 这主要是因为,正在对中断设定进行更改时, 如果再接受别的中断会引起混乱, 为了防止这种情况的发生, 就必须屏蔽中断。 还有, 如果某个IRQ没有连接任何设备的话, 静电干扰等也可能会引起反应, 导致操作系统混乱, 所以也要屏蔽掉这类干扰。

ICW是“initial control word”的缩写, 意为“初始化控制数据”。 因为这里写着word,所以我们会想, “是不是16位”? 不过, 只有在电脑的CPU里, word这个词才是16位的意思, 在别的设备上, 有时指8位, 有时也会指32位。 PIC不是仅为电脑的CPU而设计的控制芯片, 其他种类的CPU也能使用, 所以这里word的意思也并不是我们觉得理所当然的16位。

ICW有4个, 分别编号为1~4, 共有4个字节的数据。 ICW1和ICW4与PIC主板配线方式、 中断信号的电气特性等有关, 所以就不详细说明了。 电脑上设定的是上述程序所示的固定值, 不会设定其他的值。 如果故意改成别的什么值的话, 早期的电脑说不定会烧断保险丝, 或者器件冒 烟2; 最近的电脑, 对这种设定起反应的电路本身被省略了, 所以不会有任何反应。

中断发生以后, 如果CPU可以受理这个中断, CPU就会命令PIC发送2个字节的数据。 这2个字节是怎么传送的呢? CPU与PIC用IN或OUT进行数据传送时, 有数据信号线连在一起。 PIC就是利用这个信号线发送这2个字节数据的。 送过来的数据是“0xcd 0x??”这两个字节。 由于电路设计的原因, 这两个字节的数据在CPU看来, 与从内存读进来的程序是完全一样的, 所以CPU就把送过来的“0xcd 0x??”作为机器语言执行。这恰恰就是把数据当作程序来执行的情况。 这里的0xcd就是调用BIOS时使用的那个INT指令。 我们在程序里写的“INT 0x10”, 最后就被编译成了“0xcd0x10”。 所以, CPU上了PIC的当, 按照PIC所希望的中断号执行了INT指令

这次是以INT 0x200x2f接收中断信号IRQ015而设定的。 这里大家可能又会有疑问了。 “直接用INT 0x00~0x0f就不行吗? 这样与IRQ的号码不就一样了吗? 为什么非要加上0x20? ”不要着急, 先等笔者说完再问嘛。 是这样的, INT 0x00~0x1f不能用于IRQ, 仅此而已。之所以不能用, 是因为应用程序想要对操作系统干坏事的时候, CPU内部会自动产生INT 0x00~0x1f, 如果IRQ与这些号码重复了, CPU就分不清它到底是IRQ, 还是CPU的系统保护通知。这样, 我们就理解了这个程序, 把它保存为int.c。 今后要进行中断处理的还有很多, 所以我们就给它另起了一个名字。 从bootpack.c的HariMain调用init_pic。

6.5鼠标移动

鼠标是IRQ12, 键盘是IRQ1, 所以我们编写了用于INT 0x2c和INT 0x21的中断处理程序(handler) , 即中断发生时所要调用的程序

中断处理完成之后, 不能执行“return;”(=RET指令) , 而是必须执行IRETD指令, 真不好办。 而且, 这个指令还不能用C语言写2。 所以, 还得借助汇编语言的力量修改naskfunc.nas

_asm_inthandler21:		
PUSH	ES		
PUSH	DS	
	PUSHAD	
		MOV		EAX,ESP		
		PUSH	EAX		
		MOV		AX,SS		
		MOV		DS,AX		
		MOV		ES,AX		
		CALL	_inthandler21		
		POP		EAX		
		POPAD		
		POP		DS		
		POP		ES		
		IRETD

PUSH EAX这个指令, 相当于:

ADD ESP,-4
MOV [SS:ESP],EAX

也就是说, ESP的值减去4, 以所得结果作为地址值, 将寄存器中的值保存到该地
址所对应内存里。 反过来, POP EAX指令相当于:

MOV EAX,[SS:ESP]
ADD ESP, 4

还有一个不怎么常见的指令PUSHAD, 它相当于:

PUSH EAXPUSH ECXPUSH EDXPUSH EBXPUSH ESPPUSH EBPPUSH ESIPUSH EDI  

反过来, POPAD指令相当于按以上相反的顺序, 把它们全都POP出来。

6.6中断调用

这里因为IRET指令的需要进行了如下调用:在naskfunc.nas的汇编中调用了C语言函数

naskfunc.nas:

_asm_inthandler21:		
PUSH	ES		
PUSH	DS	
PUSHAD		MOV	
EAX,ESP		
PUSH	EAX	
MOV		AX,SS		
MOV		DS,AX	
MOV		ES,AX		
CALL	_inthandler21	
POP		EAX	
POPAD		
POP		DS	
POP		ES		IRETD

可以看到 CALL _inthandler21

int.c:

void inthandler21(int *esp)/* 来自PS/2键盘的中断 */{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);	
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");	
for (;;) {		
	io_hlt();	
	}}

如上即为调用。

asm_inthandler21调用inthandler21之后,又怎么样了呢,它被init_gdtidt()给调用了,用于初始化idt中的相关中断设置

07day

7.1 键盘中断

nt.c节选

#define PORT_KEYDAT 0x0060void
 inthandler21(int *esp){
 struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
 unsigned char data, s[4];
 io_out8(PIC0_OCW2, 0x61); /* 通知PIC"IRQ-01已经受理完毕" */
 data = io_in8(PORT_KEYDAT);
 sprintf(s, "%02X", data);
 boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
 putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
 return;}

首先请把目光转移到“io_out8(PIC0_OCW2, 0x61);”这句话上。 这句话用来通知PIC“已经知道发生了IRQ1中断”。 如果是IRQ3, 则写成0x63。也就是说,将“0x60+IRQ号码”输出给OCW2就可以。 执行这句话之后, PIC继续时刻监视IRQ1中断是否发生。 反过来, 如果忘记了执行这句话, PIC就不再监视IRQ1中断, 不管下次由键盘输入什么信息, 系统都感知不到了。

https://zhidao.baidu.com/question/361572607801228932.html此链接中提到,对OCW寄存器,置每X位为1,意味着屏蔽第X位。

另一方面, 字符显示是要花大块时间来进行的处理。 仅仅画一个字符, 就要执行8×16=128次if语句, 来判定是否要往VRAM里描画该像素。 如果判定为描画该像素, 还要执行内存写入指令。 而且为确定具体往内存的哪个地方写, 还要做很多地址计算。 这些事情, 在我们看来, 或许只是一瞬间的事, 但在计算机看来, 可不是这样。谁也不知道其他中断会在哪个瞬间到来。 事实上, 很可能在键盘输入的同时, 就有数据正在从网上下载, 而PIC正在等待键盘中断处理的结束。
■■■■■
那该如何是好呢? 结论很简单, 就是先将按键的编码接收下来, 保存到变量里, 然后由HariMain偶尔去查看这个变量。 如果发现有了数据, 就把它显示出来。 我们就这样试试吧。
int.c节选

struct KEYBUF {unsigned char data, flag;};#define PORT_KEYDAT 0x0060struct KEYBUF keybuf;void inthandler21(int *esp){unsigned char data;io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01已经受理完毕 */data = io_in8(PORT_KEYDAT);if (keybuf.flag == 0) {keybuf.data = data;keybuf.flag = 1;}return;}

我们先完成了上面的程序。 考虑到键盘输入时需要缓冲区, 我们定义了一个构造体, 命名为keybuf。 其中的flag变量用于表示这个缓冲区是否为空。 如果flag是0,表示缓冲区为空; 如果flag是1, 就表示缓冲区中存有数据。 那么, 如果缓冲区中存有数据, 而这时又来了一个中断, 那该怎么办呢? 这没办法, 我们暂时不做任何处理, 权且把这个数据扔掉

由于已经执行io_cli屏蔽了中断, 如果就这样去执行HLT指令的话, 即使有什么键被按下, 程序也不会有任何反应。 所以STI和HLT两个指令都要执行, 而执行这两个指令的函数就是io_stihlt。

可能有人会认为, 不做这个函数, 而是用“io_sti();io_hlt();”不也行吗? 但是,实际上这样写有点问题。 如果io_sti()之后产生了中断, keybuf里就会存入数据, 这时候让CPU进入HLT状态, keybuf里存入的数据就不会被觉察到。 根据CPU的规范, 机器语言的STI指令之后, 如果紧跟着HLT指令, 那么就暂不受理这两条指令之间的中断, 而要等到HLT指令之后才受理, 所以使用io_stihlt函数就能克服这一问题。

当按下右Ctrl键时, 会产生两个字节的键码值“E0 1D”, 而松开这个键之后, 会产生两个字节的键码值“E0 9D”。 在一次产生两个字节键码值的情况下, 因为键盘内部电路一次只能发送一个字节, 所以一次按键就会产生两次中断, 第一次中断时发送E0, 第二次中断时发送1D。

7.2FIFIO

类似一个循环链表,该结构定义如下:

struct FIFO8 {	unsigned char *buf;	int p, q, size, free, flags;};

缓冲区的总字节数保存在变量size里。 变量free用于保存缓冲区里没有数据的字节数。 缓冲区的地址当然也必须保存下来, 我们把它保存在变量buf里。 p代表下一个数据写入地址(next_w) , q代表下一个数据读出地址(next_r) 。

/* FIFO */
#include "bootpack.h"
#define FLAGS_OVERRUN		0x0001void 
fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)/* 初始化FIFO缓冲区 */{	
fifo->size = size;
fifo->buf = buf;	
fifo->free = size; /* 缓冲区大小 */
fifo->flags = 0;	
fifo->p = 0; /* 下一个数据写入位置 */	
fifo->q = 0; /* 下一个数据读出位置 */	
return;
}
int fifo8_put(struct FIFO8 *fifo, unsigned char data)/* 向FIFO传送数据并保存 */{	if (fifo->free == 0) {		/* 没有空间了,溢出 */		
fifo->flags |= FLAGS_OVERRUN;	
return -1;	
}	
fifo->buf[fifo->p] = data;	
fifo->p++;
if (fifo->p == fifo->size) {	
fifo->p = 0;//这里正是循环链表的体现	}
fifo->free--;
return 0;}
int fifo8_get(struct FIFO8 *fifo)/* 从FIFO取得一个数据 */{	
int data;
if (fifo->free == fifo->size) {		/* 如果缓冲区为空则返回-1 */	
return -1;
}
data = fifo->buf[fifo->q];
fifo->q++;
if (fifo->q == fifo->size) {		
fifo->q = 0;	}	fifo->free++;	
return data;}
int fifo8_status(struct FIFO8 *fifo)/* 报告一下积攒是数据量 */{	
return fifo->size - fifo->free;}

fifo8_get是从FIFO缓冲区取出1字节的函数

7.3键盘

#define PORT_KEYDAT 0x0060
#define PORT_KEYSTA 0x0064
#define PORT_KEYCMD 0x0064
#define KEYSTA_SEND_NOTREADY 0x02
#define KEYCMD_WRITE_MODE 0x60
#define KBC_MODE 0x47
void wait_KBC_sendready(void){/* 等待键盘控制电路准备完毕 */
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) 
{break;}} 
  return;}
   void init_keyboard(void){
   /* 初始化键盘控制电路 */
   wait_KBC_sendready();
   io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
   wait_KBC_sendready();
   io_out8(PORT_KEYDAT, KBC_MODE);
   return;}

函数wait_KBC_sendready。 它的作用是, 让键盘控制电路(keyboardcontroller, KBC) 做好准备动作, 等待控制指令的到来。 为什么要做这个工作呢?是因为虽然CPU的电路很快, 但键盘控制电路却没有那么快。 如果CPU不顾设备接收数据的能力, 只是一个劲儿地发指令的话, 有些指令会得不到执行, 从而导致错误的结果。 如果键盘控制电路可以接受CPU指令了, CPU从设备号码0x0064处所读取的数据的倒数第二位(从低位开始数的第二位) 应该是0

(if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0))。 在确认到这一位是0之前, 程序一直通过for语句循环查询。

init_keyboard。 它所要完成的工作很简单, 也就是一边确认可否往键盘控制电路传送信息, 一边发送模式设定指令, 指令中包含着要设定为何种模式。 模式设定的指令是0x60, 利用鼠标模式的模式号码是0x47, 当然这些数值必须通过调查才能知道。

7.3鼠标

#define KEYCMD_SENDTO_MOUSE 0xd4
#define MOUSECMD_ENABLE 0xf4
void enable_mouse(void){/* 激活鼠标 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
return; /* 顺利的话,键盘控制其会返送回ACK(0xfa)*/}

这个函数与init_keyboard函数非常相似。 不同点仅在于写入的数据不同。 如果往键盘控制电路发送指令0xd4, 下一个数据就会自动发送给鼠标。 我们根据这一特性来发送激活鼠标的指令。另一方面, 一直等着机会露脸的鼠标先生, 收到激活指令以后, 马上就给CPU发送答复信息: “OK, 从现在开始就要不停地发送鼠标信息了, 拜托了。 ”这个答复信息就是0xfa。因为这个数据马上就跟着来了, 即使我们保持鼠标完全不动, 也一定会产生一个鼠标中断。

08day

8.1鼠标三字节

.c文件此处略去

首先要把最初读到的0xfa舍弃掉。 之后, 每次从鼠标那里送过来的数据都应该是3个字节一组的, 所以每当数据累积到3个字节, 就把它显示在屏幕上。变量mouse_phase用来记住接收鼠标数据的工作进展到了什么阶段(phase) 。 接收到的数据放在mouse_dbuf[0~2]内

屏幕上会出现类似于“08 12 34”之类的3字节数字。 如果移动鼠标, 这个“08”部分(也就是mouse_dbuf[0]) 的“0”那一位, 会在0~3的范围内变化。 另外, 如果只是移动鼠标, 08部分的“8”那一位, 不会有任何变化, 只有当点击鼠标的时候它才会变化。 不仅左击有反应, 右击和点击中间滚轮时都会有反应。 不管怎样点击鼠标,这个值会在8~F之间变化。上述“12”部分(mouse_dbuf[1]) 与鼠标的左右移动有关系, “34”部分(mouse_dbuf[2]) 则与鼠标的上下移动有关系。趁着这个机会, 请大家仔细观察一下数字与鼠标动作的关系。 我们要利用这些知识去解读这3个字节的数据

但鼠标连线偶尔也会有接触不良、 即将断线的可能, 这时就会产生不该有的数据丢失, 这样一来数据会错开一个字节。 数据一旦错位, 就不能顺利解读, 那问题可就大了。 而如果添加上对第一字节的检查, 就算出了问题, 鼠标也只是动作上略有失误, 很快就能纠正过来, 所以笔者加上了这项检查

x和y, 基本上是直接使用buf[1]和buf[2] , 但是需要使用第一字节中对鼠标移动有反应的几位(参考第一节的叙述) 信息, 将x和y的第8位及第8位以后全部都设成1, 或全部都保留为0。 这样就能正确地解读x和y。 在解读处理的最后, 对y的符号进行了取反的操作。 这是因为, 鼠标与屏幕的y方向正好相反, 为了配合画面方向, 就对y符号进行了取反操作

8.2开始解释起来了(建议此处读原书)

asmhead.nas:

; PIC关闭一切中断; 根据AT兼容机的规格, 如果要初始化PIC,;
 必须在CLI之前进行, 否则有时会挂起。
 ; 随后进行PIC的初始化。MOV AL,0xffOUT 0x21,ALNOP
  ; 如果连续执行OUT指令, 有些机种会无法正常运行OUT 0xa1,ALCLI 
  ; 禁止CPU级别的中断

这段程序等同于以下内容的C程序

io_out(PIC0_IMR, 0xff); /* 禁止主PIC的全部中断 */
io_out(PIC1_IMR, 0xff); /* 禁止从PIC的全部中断 */
io_cli(); /* 禁止CPU级别的中断*/

如果当CPU进行模式转换时进来了中断信号, 那可就麻烦了。 而且, 后来还要进行PIC的初始化, 初始化时也不允许有中断发生。 所以, 我们要把中断全部屏蔽掉。顺便说一下, NOP指令什么都不做, 它只是让CPU休息一个时钟长的时间。

; 为了让CPU能够访问1MB以上的内存空间, 设定A20GATECALL waitkbdoutMOV AL,0xd1OUT 0x64,ALCALL waitkbdoutMOV AL,0xdf ; enable A20OUT 0x60,ALCALL waitkbdout

这里的waitkbdout, 等同于wait_KBC_sendready(以后还会详细说明) 。 这段程序在C语言里的写法大致如下:

#define KEYCMD_WRITE_OUTPORT 0xd1
#define KBC_OUTPORT_A20G_ENABLE 0xdf/* A20GATE的设定 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_OUTPORT);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_OUTPORT_A20G_ENABLE);
wait_KBC_sendready(); /* 这句话是为了等待完成执行指令 */

程序的基本结构与init_keyboard完全相同, 功能仅仅是往键盘控制电路发送指令。这里发送的指令, 是指令键盘控制电路的附属端口输出0xdf。 这个附属端口, 连接着主板上的很多地方, 通过这个端口发送不同的指令, 就可以实现各种各样的控制功能。

这次输出0xdf所要完成的功能, 是让A20GATE信号线变成ON的状态。 这条信号线的作用是什么呢? 它能使内存的1MB以上的部分变成可使用状态。 最初出现电脑的时候, CPU只有16位模式, 所以内存最大也只有1MB。 后来CPU变聪明了, 可以使用很大的内存了。 但为了兼容旧版的操作系统, 在执行激活指令之前, 电路被限制为只能使用1MB内存。 和鼠标的情况很类似哟。 A20GATE信号线正是用来使这个电路停止从而让所有内存都可以使用的东西。

最后还有一点, “wait_KBC_sendready();”是多余的。 在此之后, 虽然不会往键盘送命令, 但仍然要等到下一个命令能够送来为止。 这是为了等待A20GATE的处理切实完成。

; bootpack的转送
MOV ESI,bootpack
; 转送源
MOV EDI,BOTPAK
; 转送目的地
MOV ECX,512*1024/4
CALL memcpy
; 磁盘数据最终转送到它本来的位置去
; 首先从启动扇区开始
MOV ESI,0x7c00
; 转送源
MOV EDI,DSKCAC
; 转送目的地
MOV ECX,512/4
CALL memcp
y; 所有剩下的
MOV ESI,DSKCAC0+512
; 转送源
MOV EDI,DSKCAC+512
; 转送目的地
MOV ECX,0191
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 
; 从柱面数变换为字节数/4
SUB ECX,512/4 ; 减去 IPL
CALL memcpy

函数memcpy是复制内存的函数, 语法如下:memcpy(转送源地址, 转送目的地址, 转送数据的大小);转送数据大小是以双字为单位的, 所以数据大小用字节数除以4来指定。 在上面3个
memcpy语句中, 我们先来看看中间一句。
memcpy(0x7c00, DSKCAC, 512/4);
DSKCAC是0x00100000, 所以上面这句话的意思就是从0x7c00复制512字节到0x00100000。 这正好是将启动扇区复制到1MB以后的内存去的意思。 下一个
memcpy语句:
memcpy(DSKCAC0+512, DSKCAC+512, cyls * 512182/4-512/4);
它的意思就是将始于0x00008200的磁盘内容, 复制到0x00100200那里。上文中“转送数据大小”的计算有点复杂, 因为它是以柱面数来计算的, 所以需要减去启动区的那一部分长度。 这样始于0x00100000的内存部分, 就与磁盘的内容相吻合了。 顺便说一下, IMUL2是乘法运算, SUB3是减法运算。 它们与ADD(加法)运算同属一类。

现在我们还没说明的函数就只有有程序开始处的memcpy了。 bootpack是asmhead.nas的最后一个标签。 haribote.sys是通过asmhead.bin和bootpack.hrb连接起来而生成的(可以通过Makefile确认) , 所以asmhead结束的地方, 紧接着串连着bootpack.hrb最前面的部分。

memcpy(bootpack, BOTPAK, 512*1024/4); → 从bootpack的地址开始的512KB内容复制到0x00280000号地址去。这就是将bootpack.hrb复制到0x00280000号地址的处理。 为什么是512KB呢? 这是我们酌情考虑而决定的。 内存多一些不会产生什么问题, 所以这个长度要比bootpack.hrb的长度大出很多。

8.3内存分布

0x00000000 - 0x000fffff : 虽然在启动中会多次使用, 但之后就变空。 (1MB)
0x00100000 - 0x00267fff : 用于保存软盘的内容。 (1440KB)
0x00268000 - 0x0026f7ff : 空(30KB)
0x0026f800 - 0x0026ffff : IDT (2KB)
0x00270000 - 0x0027ffff : GDT (64KB)
0x00280000 - 0x002fffff : bootpack.hrb(512KB)
0x00300000 - 0x003fffff : 栈及其他(1MB)
0x00400000 - : 空

09day

9.1两次分割文件

把键盘控制和鼠标控制给分割出来了,多了keyboard.c和mouse.c

在最初启动时, BIOS肯定要检查内存容量, 所以只要我们问一问BIOS, 就能知道内存容量有多大。 但问题是, 如果那样做的话, 一方面asmhead.nas会变长, 另一方面, BIOS版本不同, BIOS函数的调用方法也不相同, 麻烦事太多了。 所以, 笔者想与其如此, 不如自己去检查内存。

386的CPU没有缓存, 486的缓存只有8-16KB, 但两者的性能就差了6倍以上1。 286进化到386时, 性能可没提高这么多。 386进化到486时, 除了缓存之外还有别的改善, 不能光靠缓存来解释这么大的性能差异, 但这个性能差异, 居然比16位改良到32位所带来的性能差异还要大, 笔者认为这主要应该归功于缓存。

#define EFLAGS_AC_BIT 0x00040000#define CR0_CACHE_DISABLE 0x60000000unsigned 
int memtest(unsigned int start, unsigned int end)
{
char flg486 = 0;
unsigned int eflg, cr0, i;/* 确认CPU是386还是486以上的 */
eflg = io_load_eflags();
eflg |= EFLAGS_AC_BIT; /* AC-bit = 1 */
io_store_eflags(eflg);
eflg = io_load_eflags();
if ((eflg & EFLAGS_AC_BIT) != 0) { /* 如果是386, 即使设定AC=1, AC的值还会自动回到0 */
flg486 = 1;}
eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */
io_store_eflags(eflg);
if (flg486 != 0) {
cr0 = load_cr0();
cr0 |= CR0_CACHE_DISABLE; /* 禁止缓存 */
store_cr0(cr0);}
 i= memtest_sub(start, end);
 if (flg486 != 0) {
 cr0 = load_cr0();
 cr0 &= ~CR0_CACHE_DISABLE; /* 允许缓存 */
 store_cr0(cr0);} 
 return i;}

最初对EFLAGS进行的处理, 是检查CPU是486以上还是386。 如果是486以上,EFLAGS寄存器的第18位应该是所谓的AC标志位; 如果CPU是386, 那么就没有这个标志位, 第18位一直是0。 这里, 我们有意识地把1写入到这一位, 然后再读出EFLAGS的值, 继而检查AC标志位是否仍为1。 最后, 将AC标志位重置为0。其中memtest_sub函数, 是内存检查处理的实现部分

但是由于反转两次,等于没反转,所以编译器将它给优化掉了!所以最后作者用汇编写了memtest_sub函数。(笑死)

为了禁止缓存, 需要对CR0寄存器的某一标志位进行操作。 对哪里操作, 怎么操作, 大家一看程序就能明白。 这时, 需要用到函数load_cr0和store_cr0, 与之前的情况一样, 这两个函数不能用C语言写, 只能用汇编语言来写, 存在naskfunc.nas里。
本次的naskfunc.nas节选

_load_cr0: ; 
int load_cr0(void);
MOV EAX,CR0RET_store_cr0: ; 
void store_cr0(int cr0);
MOV EAX,[ESP+4]
MOV CR0,EAXRET

10day

10.1内存续

为了便于内存管理,避免出现很多不连续的小段未使用空间,进行了,以4KB为单位的划分

unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size){
unsigned int a;
size = (size + 0xfff) & 0xfffff000;
a = memman_alloc(man, size);
return a;
} 
int memman_free_4k(struct MEMMAN *man, unsigned int addr, unsigned int size)
{
int i;
size = (size + 0xfff) & 0xfffff000;
i = memman_free(man, addr, size);
return i;
}

向上舍入方法:

那么如果对400元进行向上舍入呢? 先加上99元, 得到499元, 再进行向下舍入, 结果是400元。 看, 400元向上舍入的结果还是400元。
这种方法多方便呀, 可比if语句什么的好用多了。 不过其中的原理是什么呢? 其实加上99元就是判断进位, 如果最后两位不是00, 就要向前进一位, 只有当最后两位是00时, 才不需要进位。 接下来再向下舍入, 这样就正好把因为加法运算而改变的后两位设置成00了。 看, 向上舍入就成功了

由此可见, 如果以1000字节或4000字节单位进行内存管理的话, 每次分配内存时, 都不得不进行繁琐的除法计算。 但如果以1024字节或4096字节为单位进行
内存管理的话(两者都是在二进制下易于取整的数字。 附带说明:0x1000 =4096) , 在向上舍入的计算中就可以使用“与运算”, 这样也能够提高操作系统的运行速度, 因此笔者认为这个设计很高明。

10.2图层叠加

#define MAX_SHEETS 256struct SHTCTL {
unsigned char *vram;int xsize, ysize, top;
struct SHEET *sheets[MAX_SHEETS];
struct SHEET sheets0[MAX_SHEETS];};

我们创建了SHTCTL结构体, 其名称来源于sheet control的略语, 意思是“图层管理”。 MAX_SHEETS是能够管理的最大图层数, 这个值设为256应该够用了。
变量vram、 xsize、 ysize代表VRAM的地址和画面的大小, 但如果每次都从BOOTINFO查询的话就太麻烦了, 所以在这里预先对它们进行赋值操作。 top代表
最上面图层的高度。 sheets0这个结构体用于存放我们准备的256个图层的信息。 而sheets是记忆地址变量的领域, 所以相应地也要先准备256份。 这是干什么用呢? 由于sheets0中的图层顺序混乱, 所以我们把它们按照高度进行升序排列, 然后将其地址写入sheets中, 这样就方便多了。

程序中出现的&ctl—>sheets0[i]是“ctl—>sheets0[i]的地址”的意思。 也就是说, 指的是&(ctl—>sheets0[i]) , 而不是(&ctl) —> sheets0[i]。

void sheet_updown(struct SHTCTL *ctl, struct SHEET *sht, int height){	以下均略去	}		
sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize);
sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize); /* 按新图层信息重新绘制画面 */	}	return;}

在sheet_fresh函数中:对于已设定了高度的所有图层而言, 要从下往上, 将透明以外的所有像素都复制到VRAM中。 由于是从下开始复制, 所以最后最上面的内容就留在了画面上。

这里我们仅仅改写了sheet_refresh, 变更点共有4个。 只有每次要往buf_back中写入信息时, 才进行sheet_refresh。

sheet_updown(ctl, sht, -1); /* 如果处于显示状态, 则先设定为隐藏 */  

10.3移动

int vx0, int vy0, int vx1, int vy1,分别是鼠标移动前后的左上角坐标

void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1){	略去}

11day

太过简单,没什么需要记录的

12day

12.1 定时器

要在电脑中管理定时器,只需对PIT进行设定就可以了。PIT是“ ProgrammableInterval Timer”的缩写,翻译过来就是“可编程的间隔型定时器”。我们可以通过设定PIT,让定时器每隔多少秒就产生一次中断。因为在电脑中PIT连接着IRQ(interruptrequest,参考第6章)的0号,所以只要设定了PIT就可以设定IRQ0的中断间隔。……在旧机种上PIT是作为一个独立的芯片安装在主板上的,而现在已经和PIC(programmable interrupt controller,参考第6章)一样被集成到别的芯片里了。

  • IRQ0的中断周期变更:
  • AL=0x34:OUT(0x43,AL);
  • AL=中断周期的低8位; OUT(0x40,AL);
  • AL=中断周期的高8位; OUT(0x40,AL);

​ 到这里告一段落。如果指定中断周期为0,会被看作是指定为65536。实际的中断产生的频率是单位时间时钟周期数(即主频)/设定的数值。比如设定值如果是1000,那么中断产生的频率就是1.19318KHz。设定值是10000的话,中断产生频率就是119.318Hz。再比如设定值是11932的话,中断产生的频率大约就是100Hz了,即每10ms发生一次中断。

13day

13.1性能比较

对于这样的结果,笔者曾茫然不知所措,差一点要放弃性能比较。但后来笔者忽然想起,只要某些条件稍微有些变化,电脑初始化所花费的时间就会有很大变化。这就是为什么我们在起动后3秒钟之内不进行测试的原因。这样做之后,误差急剧减小,终于可以比较结果了,真是太好了。

14day

14.1按键分配表

00:没有被分配使用 20:D 40:F6 60:保留
01:ESC 21:F 41:F7 61:保留?
02:主键盘的1 22:G 42:F8 62:保留?
03:主键盘的2 23:H 43:F9 63:保留?
04:主键盘的3 24:J 44:F10 64:保留?
05:主键盘的4 25:K 45:NumLock 65:保留?
06:主键盘的5 26:L 46:ScrollLock 66:保留?
07:主键盘的6 27:; 47:数字键的7 67:保留?
08:主键盘的7 28::(在英语键盘是’ ) 48:数字键的8 68:保留?
09:主键盘的8 29:全角•半角(在英语键盘是` ) 49:数字键的9 69:保留?
0A:主键盘的9 2A:左Shift 4A:数字键的- 6A:保留?
0B:主键盘的0 2B:](在英语键盘是backslash(反斜线)) 4B:数字键的4 6B:保留?
0C:主键盘的- 2C:Z 4C:数字键的5 6C:保留?
0D:主键盘的^(在英语键盘是=) 2D:X 4D:数字键的6 6D:保留?
0E:退格键 2E:C 4E:数字键的+ 6E:保留?
0F:TAB键 2F:V 4F:数字键的1 6F:保留?
10:Q 30:B 50:数字键的2 70:平假名(日文键盘)
11:W 31:N 51:数字键的3 71:保留?
12:E 32:M 52:数字键的0 72:保留?
13:R 33:, 53:数字键的. 73:_
14:T 34:. 54:SysReq 74:保留?
15:Y 35:/ 55:保留? 75:保留?
16:U 36:右Shift 56:保留? 76:保留?
17:I 37:数字键的* 57:F11 77:保留?
18:O 38:左Alt 58:F12 78:保留?
19:P 39:Space 59:保留? 79:变换(日文键盘)
1A:@(在英语键盘是[ ) 3A:CapsLock 5A:保留? 7A:保留?
1B:[(在英语键盘是] ) 3B:F1 5B:保留? 7B:无变换(日文键盘)
1C:主键盘的Enter 3C:F2 5C:保留? 7C:保留?
1D:左Ctrl 3D:F3 5D:保留? 7D:
1E:A 3E:F4 5E:保留? 7E:保留?
1F:S 3F:F5 5F:保留? 7F:保留?

15day

15.1多任务

在一般的操作系统中,这个切换的动作每0.01~0.03秒就会进行一次。当然,切换的速度越快,让人觉得程序是在同时运行的效果也就越好。不过,CPU进行程序切换(我们称为“任务切换”)这个动作本身就需要消耗一定的时间,这个时间大约为0.0001秒左右,不同的CPU及操作系统所需的时间也有所不同。如果CPU每0.0002秒切换一次任务的话,该CPU处理能力的50%都要被任务切换本身所消耗掉。这意味着,如果同时运行2个程序,每个程序的速度就只有单独运行时的1/4,这样你会觉得开心吗?如果变成这种结果,那还不如干脆别搞多任务呢。

当你向CPU发出任务切换的指令时,CPU会先把寄存器中的值全部写入内存中,这样做是为了当以后切换回这个程序的时候,可以从中断的地方继续运行。接下来,为了运行下一个程序,CPU会把所有寄存器中的值从内存中读取出来(当然,这个读取的地址和刚刚写入的地址一定是不同的,不然就相当于什么都没变嘛),这样就完成了一次切换。我们前面所说的任务切换所需要的时间,正是对内存进行写入和读取操作所消耗的时间。

说点题外话,JMP指令实际上是一个向EIP寄存器赋值的指令。JMP 0x1234这种写法,CPU会解释为MOV EIP,0x1234,并向EIP赋值。也就是说,这条指令
其实是篡改了CPU记忆中下一条该执行的指令的地址,蒙了CPU一把。这样一来,CPU在读取下一条指令时,就会去读取0x1234这个地址中的指令。你看,这不就相当于是做了一个跳转吗?对了,如果你在汇编语言里用MOV EIP,0x1234这种写法是会出错的,还是不要尝试的好。在汇编语言中,应该使用JMP 0x1234来代替MOV EIP,0x1234。

JMP指令分为两种,只改写EIP的称为near模式,同时改写EIP和CS的称为far模式,在此之前我们使用的JMP指令基本上都是near模式的。不记得CS是什么了?CS就是代码段(code segment)寄存器

16day

16.1速度

和harib10i时的成绩14281323相比,这次的13438300还有6%左右的差距,这应该是每0.01秒刷新一次显示导致的,因此我们把这个去掉之后再来测试一下速度。做法和之前有所不同,采用的是将task_b_main中的两处timer_settime(timer_put, 1);改成timer_settime(timer_put. 100);的方法。测试结果为13782000,差距缩小到3%左右了,可以说和没有多任务的状态非常接近了。其实一开始笔者也是按照之前的方法,把timer_settime(timer_put, 1);删掉来测试速度的,但测出来的结果居然令人震惊地高达36343300,这相当于14281323的2.5倍。速度比没有多任务的时候还快,这个结果是十分异常的,这是由于JMP指令的跳转目标地址发生变化所致。因此,为了不让跳转目标地址发生变化,我们只好保留那条语句。本来想把timer_put的中断间隔设置为1000或者10000左右的,不过这样一来指令的长度会发生变化,所以只好设置成100了。从这个问题来看,用C语言测试速度还是有局限性的。如果要精确比较速度的话,只能在仔细考虑地址问题的前提下,用汇编语言来编写程序来实现了。

17day

看不下去了,写的什么垃圾,艹

  • 57
    点赞
  • 137
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
### 回答1: 用Linux内核作为操作系统的基础,可以实现一个新的操作系统。在这个过程中,需要深入了解Linux内核的各个方面,包括内存管理、进程管理、文件系统、设备驱动程序等。 需要使用C语言编写内核代码,并且需要了解一些汇编语言来实现一些底层的功能。同时,还需要了解操作系统的整体架构和设计原则,以确保操作系统的可靠性、安全性和效率。 这是一个非常复杂的任务,需要经验丰富的程序员和计算机科学家的共同努力才能实现。即使使用了Linux内核,也需要进行大量的开发和测试工作,才能构建出一个完整、稳定、可靠的操作系统。 ### 回答2: 要用 Linux 内核写一个操作系统,首先需要了解操作系统的基本概念和功能,并熟悉 Linux 内核的架构和编程接口。接下来,可以按照以下步骤进行: 1. 确定操作系统的需求和目标:思考要开发的操作系统的用途、目标用户群体和基本功能等方面的需求。 2. 下载和安装适当的开发工具:根据所选操作系统开发的需求,从 Linux 官方网站或其他可靠资源下载合适版本的 Linux 内核,并安装在开发机上。 3. 初步配置和编译内核:根据实际需求,进行初步的内核配置,确定需要的功能模块。然后,使用内核配置工具进行配置保存,接着开始编译内核源码。 4. 针对具体应用进行内核定制:根据所需操作系统的功能需求,可以对内核进行二次开发和定制,添加或删除相应模块,扩充或精简某些功能。 5. 编写系统级驱动程序和服务:根据需求,编写特定的设备驱动和系统服务程序。可以利用 Linux 内核提供的接口函数,编写驱动程序,控制硬件设备的操作。 6. 进行内核的构建和连接:按照所做的修改和定制,重新编译内核源码,生成适合的内核映像文件。 7. 进行系统的测试和调试:将生成的内核映像文件烧录到目标设备上,进行系统的验证和测试。调试是一个不可或缺的环节,可以通过调试工具和日志信息定位和修复问题。 8. 配置和优化操作系统:根据实际需求,对操作系统进行细致的配置和优化,保证系统的性能和稳定性。 9. 发布和更新操作系统:在进行充分测试和验证之后,将操作系统发布到用户,并及时修复和升级系统中的漏洞和问题。 总之,使用 Linux 内核写一个操作系统是一项复杂的任务,需要掌握操作系统和编程技术的知识,并进行详细的计划和设计。同时,要耐心和细心进行开发、测试和调试工作,确保最终的操作系统具备所需的功能和稳定性。 ### 回答3: 使用Linux内核编写一个操作系统是一个庞大的工程,需要深入了解Linux内核的结构和原理,以及操作系统的设计和实现。 首先,我们需要从Linux内核的源代码开始,这可以从Linux官方网站或其他途径获取。然后,我们需要对内核进行分析和理解,包括内核的启动过程、内核空间和用户空间的切换、设备驱动、进程管理等。 接下来,我们可以根据需求对内核进行定制,可以添加或删除某些功能,设计和实现自己的系统调用,以及其他相关的内核模块。这需要深入了解Linux内核的API和接口,如系统调用接口、文件系统接口等。 然后,我们需要编写启动引导程序,将我们的操作系统加载到内存中并启动。 在编写操作系统的过程中,需要注意保护内核的安全性和稳定性,处理各种异常和中断,管理和调度进程,实现进程间通信和同步,支持多线程和多CPU,设计并实现内存管理、文件系统、网络协议栈等核心功能。 此外,对于更高级的功能,如图形界面、设备驱动程序、网络系统等,我们需要进一步的学习和研究。 总之,使用Linux内核编写一个操作系统是一个充满挑战的任务,需要对底层的硬件、操作系统原理和编程技术有深入的了解。通过不断的学习和实践,我们可以逐步掌握这个技能,创建出符合我们需求的定制化操作系统

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Greate AUK

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值