30天自制操作系统笔记

以前学操作系统总是学一些算法,关于什么进程调度算法,内存何如分配,怎样提高缓存命中等。而对于操作系统本身还是只有一个模糊的概念。受到https://www.zhihu.com/column/c_1193254878150045696的激励,我就学习了30天自制操作系统这本书。看完这本书,对操作系统是什么的神秘感破除了,但也产生了更多疑问,一方面书中很多东西讲的不多,自己一下也理解不了:比如向32位切换的部分。而且本书是像水流那样一步一步,不断写好,再改进,最终实现的。所以在看完后,我又把我阅读时的笔记对照书中的代码,把它按不同部分结合起来又整理了一下,便于理解。

另外确实推荐这位大佬,他在这本书之外进行了很多扩展https://www.zhihu.com/column/c_1193254878150045696

直观展示

用二进制写一个操作系统

新建img磁盘文件,通过二进制编辑器在磁盘文件前写入如下512B内容

EB 4E 90 48 45 4C 4C 4F  49 50 4C 00 02 01 01 00
02 E0 00 40 0B F0 09 00  12 00 02 00 00 00 00 00
40 0B 00 00 00 00 29 FF  FF FF FF 48 45 4C 4C 4F
2D 4F 53 20 20 20 46 41  54 31 32 20 20 20 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
B8 00 00 8E D0 BC 00 7C  8E D8 8E C0 BE 74 7C 8A
04 83 C6 01 3C 00 74 09  B4 0E BB 0F 00 CD 10 EB
EE F4 EB FD 0A 0A 68 65  6C 6C 6F 2C 20 4F 53 00
00 00 0A 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 AA

使用quem虚拟软件模拟启动,可以看到显示hello,OS内容
img

用汇编写操作系统

BIOS:是计算机厂家在主板的ROM中预先写入的程序,其中就有INT
INT软件中断指令:通过INT+数字可以调用不同的中断指令对应的函数,比如显示一个字符的调用过程为:

AH=0x0e
AL=字符串地址
BH=0
BL=字符设定的颜色
INT 0x10
;FAT12格式软盘所要定义的内容,包括扇区大小,簇大小等
db	0xeb,0x4e,0x90
db	"HELLOIPL"
dw	512
db	1
dw	1
db	2
dw	224
dw	2880
db	0xf0
dw	9
dw	18
dw	2
dd	0
dd	2880
db	0,0,0x29
dd	0xffffffff
db 	"HELLO-OS   "
db	"FAT12   "
RESB	18 ;RESB X指预留X个字节的位置,即写入X个00

;以下程序主要为了显示msg中的信息
ORG	0x7c00 ;将程序放入内存中的0x7c00的位置,因为内存不同位置有不同的功能,这个就是启动区内容装载的地址
JMP	entry
DB	0x90
entry:
MOV	AX,0
MOV	SS,AX
MOV	SP,0X7C00
MOV	DS,AX
MOV	ES,AX
MOV	SI,msg
putloop:
MOV	AL,[SI]
ADD	SI,1
CMP	AL,0
JE	fin
MOV	AH,0X0E
MOV	BX,15
INT	0X10
JMP	putloop
;让CPU停下来
fin:
HLT
JMP fin
;要显示的信息
msg:
db	0x0a,0x0a
db	"hello,OS   "
db	0x0a
db	0

RESB 0x7dfe-$ ;$指将要读入的内存地址
db 0x55,0xaa

上述代码编译后写入img文件,同样可以启动,编译后二进制内容与上相同,除了最后结尾时因为汇编编写时打印的话写成了"hello,OS "(末尾有三个空格),所以二进制中有三个00 00 00 变为了20 20 20

EB 4E 90 48 45 4C 4C 4F  49 50 4C 00 02 01 01 00
02 E0 00 40 0B F0 09 00  12 00 02 00 00 00 00 00
40 0B 00 00 00 00 29 FF  FF FF FF 48 45 4C 4C 4F
2D 4F 53 20 20 20 46 41  54 31 32 20 20 20 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
B8 00 00 8E D0 BC 00 7C  8E D8 8E C0 BE 74 7C 8A
04 83 C6 01 3C 00 74 09  B4 0E BB 0F 00 CD 10 EB
EE F4 EB FD 0A 0A 68 65  6C 6C 6F 2C 20 4F 53 20
20 20 0A 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 AA

汇编代码与二进制对应关系如下

     1 00000000                                 ;FAT12格式软盘
     2 00000000                                 
     3 00000000 EB 4E 90                        db	0xeb,0x4e,0x90
     4 00000003 48 45 4C 4C 4F 49 50 4C         db	"HELLOIPL"
     5 0000000B 0200                            dw	512
     6 0000000D 01                              db	1
     7 0000000E 0001                            dw	1
     8 00000010 02                              db	2
     9 00000011 00E0                            dw	224
    10 00000013 0B40                            dw	2880
    11 00000015 F0                              db	0xf0
    12 00000016 0009                            dw	9
    13 00000018 0012                            dw	18
    14 0000001A 0002                            dw	2
    15 0000001C 00000000                        dd	0
    16 00000020 00000B40                        dd	2880
    17 00000024 00 00 29                        db	0,0,0x29
    18 00000027 FFFFFFFF                        dd	0xffffffff
    19 0000002B 48 45 4C 4C 4F 2D 4F 53 20 20   db 	"HELLO-OS   "
       00000035 20 
    20 00000036 46 41 54 31 32 20 20 20         db	"FAT12   "
    21 0000003E 00 00 00 00 00 00 00 00 00 00   RESB	18
       00000048 00 00 00 00 00 00 00 00 
    22 00000050                                 
    23                                          ORG	0x7c00
    24 00007C00 EB 01                           JMP	entry
    25 00007C02 90                              DB	0x90
    26 00007C03                                 entry:
    27 00007C03 B8 0000                         MOV	AX,0
    28 00007C06 8E D0                           MOV	SS,AX
    29 00007C08 BC 7C00                         MOV	SP,0X7C00
    30 00007C0B 8E D8                           MOV	DS,AX
    31 00007C0D 8E C0                           MOV	ES,AX
    32 00007C0F BE 7C27                         MOV	SI,msg
    33 00007C12                                 putloop:
    34 00007C12 8A 04                           MOV	AL,[SI]
    35 00007C14 83 C6 01                        ADD	SI,1
    36 00007C17 3C 00                           CMP	AL,0
    37 00007C19 74 09                           JE	fin
    38 00007C1B B4 0E                           MOV	AH,0X0E
    39 00007C1D BB 000F                         MOV	BX,15
    40 00007C20 CD 10                           INT	0X10
    41 00007C22 EB EE                           JMP	putloop
    42 00007C24                                 fin:
    43 00007C24 F4                              HLT
    44 00007C25 EB FD                           JMP fin
    45 00007C27                                 msg:
    46 00007C27 0A 0A                           db	0x0a,0x0a
    47 00007C29 68 65 6C 6C 6F 2C 4F 53 20 20   db	"hello,OS   "
       00007C33 20 
    48 00007C34 0A                              db	0x0a
    49 00007C35 00                              db	0

这就是操作系统的直观展示,其实这只用汇编写了一个启动区boot。
因为计算机总是会读取磁盘的第一个扇区,检查其末尾是否为0x55 0xaa,如果是就要执行这个扇区上的启动程序。
但是真正的操作系统不可能只有512B,所以一般只在前512B放入一个操作系统的加载程序,通过这个程序来加载放在别处的操作系统。

使其能加载一个程序

过程是,通过boot中程序读入其它扇区的程序到内存中,然后跳转到指定内存位置。
所以对上面的汇编程序改写,使其能读取其它扇区的内容,也就是为了以后能读入真正的操作系统。这里读入之后10个柱面的内容。
CPU寻址:如果只用一个16位寄存器来寻址,只能包括64KB的内存,所以采专用段寄存器ES:BX的方式,寻址范围变为了ES*16+BX,这样能够包括1MB的地址
磁盘:磁头和柱面从0开始编号,扇区从1开始编号。读取磁盘内容的中断INT13:

AH=02(读盘)03(写盘)04(校验)0c(寻道)
AL=要处理的连续扇区数
CH=柱面号
CL=扇区号
DH=磁头号
DL=驱动器号
ES:BX=读到的内存地址
返回值
FLACS.CF\==0
AH\==0表示没有错误
FLACS.CF\==1
AH==错误号码
CYLS	EQU		10				;声明CYLS=10

		ORG		0x7c00			;加载到内存0x7c00处
;FAT12格式软盘
		JMP		entry
		DB		0x90
		DB		"HARIBOTE"		
		DW		512				
		DB		1				
		DW		1				
		DB		2				
		DW		224				
		DW		2880			
		DB		0xf0			
		DW		9				
		DW		18				
		DW		2				
		DD		0				
		DD		2880			
		DB		0,0,0x29		
		DD		0xffffffff		
		DB		"HARIBOTEOS "	
		DB		"FAT12   "		
		RESB	18				
entry:
		MOV		AX,0			
		MOV		SS,AX
		MOV		SP,0x7c00
		MOV		DS,AX
;读取磁盘数据
;以下是使用INT0x13中断读取磁盘的设置
;0x0820指读取后放到内存0x08200处
		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			;指读磁盘,也是INT0x13的必要设置
		MOV		AL,1			;1个扇区
		MOV		BX,0
		MOV		DL,0x00			;指读第几个盘,这里只有一个,就是读第一个
		INT		0x13			
		JNC		next			;读取成功就跳转
		ADD		SI,1			;失败次数加一,并与5比较
		CMP		SI,5			
		JAE		error			;5次读取都没成功就跳转到error
		MOV		AH,0x00         ;重置后再跳转retry尝试读取
		MOV		DL,0x00			
		INT		0x13			
		JMP		retry
next:
		MOV		AX,ES			;读一个扇区512B,再读下一个扇区,ES要移动512/16
		ADD		AX,0x0020
		MOV		ES,AX			
		ADD		CL,1			;读扇区的CL加一
		CMP		CL,18			
		JBE		readloop		;继续读该柱面
		MOV		CL,1
		ADD		DH,1            ;磁头数加一
		CMP		DH,2
		JB		readloop		;继续读该磁头
		MOV		DH,0
		ADD		CH,1            ;柱面加一
		CMP		CH,CYLS         
		JB		readloop		;要读10个柱面

;跳转到一个后面加入的程序,因为读取磁盘到了0x8200处,读取的程序在img的0x4200处,而img的前0x200加载到了内存0x7c00处,所以这个程序实际在内存的0x8200+0x4200-0x200=0xc200
		MOV		[0x0ff0],CH
		JMP		0xc200
;如果读取失败,通过如下代码显示msg中的错误提示		
error:
		MOV		SI,msg
putloop:
		MOV		AL,[SI]
		ADD		SI,1			
		CMP		AL,0
		JE		fin
		MOV		AH,0x0e			
		MOV		BX,15			
		INT		0x10		;显示的中断	
		JMP		putloop
msg:
		DB		0x0a, 0x0a		
		DB		"load error"
		DB		0x0a			
		DB		0

		RESB	0x7dfe-$		

		DB		0x55, 0xaa

并再写一个程序,该程序使显示黑屏,并将其导入img盘中,可以看到该程序放在磁盘0x4200处,让前面的程序在读入扇区完毕后,跳转到这里来
img
其中是设置显卡模式的中断

AH=0x00
AL=模式
0x03:80*25的16色模式
0x12:640*480*4
0x13:320*200*8
0x6a:800*600*4

跳转到的程序内容

		ORG		0xc200			;因为第一个扇区读到了内存0x7c00处,从第二个扇区开始是读到0x8200开始的,所以该程序会被加载到内存0x8200+0x4200-0x200=0xc200处

		MOV		AL,0x13			
		MOV		AH,0x0E
		INT		0x10
fin:
		HLT
		JMP		fin

运行操作系统,它就首先读取磁盘第一个扇区,然后通过第一个扇区中的程序读取磁盘10个柱面的内容(其中就有黑屏程序的内容),并在读取完后跳转到这个程序并执行这个黑屏程序
img

引入C语言

对上述的黑屏程序混合使用C与汇编来改写
使用汇编编写一个函数naskfunc.nas
使用C语言编写函数,并且调用汇编的函数bootpack.c
C语言程序先转为汇编语言,然后转化为obj目标文件,同时汇编语言程序也转为obj目标文件,然后链接再导入到img磁盘中
naskfunc.nas

[FORMAT "WCOFF"]				;目标文件模式
[BITS 32]						;32位机器语言
[INSTRSET "i486p"]	            ;告诉nask这个程序给486用
[FILE "naskfunc.nas"]			;源文件名

		GLOBAL	_io_hlt,_write_mem8			;文件中包含的函数名


;函数定义
[SECTION .text]		

_io_hlt:	;void io_hlt(void);
		HLT
		RET
_write_mem8:	; void write_mem8(int addr, int data);
		MOV		ECX,[ESP+4]		
		MOV		AL,[ESP+8]	
		MOV		[ECX],AL
		RET

bootpack.c

;函数声明
void io_hlt(void);
void write_mem8(int addr, int data);

void HariMain(void)
{

	int i;

	for (i = 0xa0000; i <= 0xaffff; i++) {
		write_mem8(i, i & 0x0f); /* MOV BYTE [i],15 */
	}

    /*
    其实也可以使用指针,直接操作内存,而不用汇编所写的程序
    char *p;
	for (i = 0xa0000; i <= 0xaffff; i++)
    {
		p = i; 
		*p = i & 0x0f;
	}*/

	for (;;) {
		io_hlt();
	}

}

CPU有16位模式和32位模式,不同模式的机器语言的命令代码不同,使用32位模式就不能使用BIOS,因为BIOS使用16位机器语言写的,所以需要增加一个新的程序,使其可以保存BIOS信息

; haribote-os boot asm
; TAB=4

BOTPAK	EQU		0x00280000		; bootpack偺儘乕僪愭
DSKCAC	EQU		0x00100000		
DSKCAC0	EQU		0x00008000		

;BOOT_INFO信息
CYLS	EQU		0x0ff0			;设定启动区
LEDS	EQU		0x0ff1
VMODE	EQU		0x0ff2			;颜色的位数
SCRNX	EQU		0x0ff4			;分辨率x大小
SCRNY	EQU		0x0ff6			;分辨率y大小
VRAM	EQU		0x0ff8			;图像缓冲区的开始地址

		ORG		0xc200			;因为第一个扇区读到了内存0x7c00处,从第二个扇区开始是读到0x8200开始的,所以该程序会被加载到内存0x8200+0x4200-0x200=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

;关闭一切中断
		MOV		AL,0xff
		OUT		0x21,AL         ;禁止主PIC中断
		NOP						;如果连续执行两次out命令可能错误,所以停一会
		OUT		0xa1,AL         ;禁止从PIC中断

		CLI						;禁止CPU中断

;设定A20GATE信号线变为ON状态,使CPU能访问1MB以上内存空间
;设定方法是往键盘控制电路发送指令0xdf
;因为以前CPU只能使用1MB内存,为了兼容,只有在指令激活后才能使用1MB以上内存

		CALL	waitkbdout      ;等同于wait_KBC_sendready
		MOV		AL,0xd1
		OUT		0x64,AL
		CALL	waitkbdout
		MOV		AL,0xdf			; enable A20
		OUT		0x60,AL
		CALL	waitkbdout

;切换到保护模式

[INSTRSET "i486p"]				;使用486指令

		LGDT	[GDTR0]			;设定临时GDT
		MOV		EAX,CR0
		AND		EAX,0x7fffffff	;设bit31为0(为了禁止分页)
		OR		EAX,0x00000001	;设bbit0为1(为了切换到保护模式)
		MOV		CR0,EAX         ;这样就进行了模式切换,CR0为32位寄存器
		JMP		pipelineflush   ;通过CR0切换到保护模式时,要马上执行JMP指令。进入保护模式后,机器语言的解释要发生变化,CPU为了加快指令执行速度使用管道pipline这一机制,前一条指令执行时就要解释下一条指令。进入保护模式后段寄存器的意思就变了,除CS外的所有段寄存器的值都存0x0000变为0x0008
pipelineflush:
		MOV		AX,1*8			;
		MOV		DS,AX
		MOV		ES,AX
		MOV		FS,AX
		MOV		GS,AX
		MOV		SS,AX

; bootpack的转送

		MOV		ESI,bootpack	;转送源
		MOV		EDI,BOTPAK		;转送目的地
		MOV		ECX,512*1024/4
		CALL	memcpy

;磁盘数据转送到它本来的位置去

;从启动扇区开始
;转送数据大小以双字为单位,所以除以4
;haribote.sys是通过asmhead.bin与bootpack.hrb连接起来生成的,所以asmhead结束部分紧接着bootpack.hrb前面部分
		MOV		ESI,0x7c00		;转送源
		MOV		EDI,DSKCAC		;转送目的地
		MOV		ECX,512/4
		CALL	memcpy

;剩下的扇区

		MOV		ESI,DSKCAC0+512	;转送源
		MOV		EDI,DSKCAC+512	;转送目的地
		MOV		ECX,0
		MOV		CL,BYTE [CYLS]
		IMUL	ECX,512*18*2/4	;从柱面数变换为字节数/4
		SUB		ECX,512/4		;减去IPL
		CALL	memcpy

;bootpack启动

		MOV		EBX,BOTPAK
		MOV		ECX,[EBX+16]
		ADD		ECX,3			; ECX += 3;
		SHR		ECX,2			;右移位指令 ECX /= 4;
		JZ		skip			;没有要转送的东西时跳转
		MOV		ESI,[EBX+20]	;转送源
		ADD		ESI,EBX
		MOV		EDI,[EBX+12]	;转送目的地
		CALL	memcpy
skip:
		MOV		ESP,[EBX+12]	;栈初始值
		JMP		DWORD 2*8:0x0000001b

waitkbdout:
		IN		 AL,0x64
		AND		 AL,0x02
		JNZ		waitkbdout		
		RET

memcpy:
		MOV		EAX,[ESI]
		ADD		ESI,4
		MOV		[EDI],EAX
		ADD		EDI,4
		SUB		ECX,1
		JNZ		memcpy			
		RET

		ALIGNB	16	;db 0 使得16字节对齐
;临时的段
GDT0:
		RESB	8				;保留8字节,0号段不能定义
		DW		0xffff,0x0000,0x9200,0x00cf	;1号段定义为可读写的段
		DW		0xffff,0x0000,0x9a28,0x0047	;2号段定义为可执行的段

		DW		0
;相当于LGDT指令,通知GDT0有了GDT
GDTR0:
		DW		8*3-1
		DD		GDT0

		ALIGNB	16
bootpack:

查看磁盘文件对应位置的信息,其中没有阴影部分为写入的保存BIOS信息的程序,阴影部分为混合使用C与汇编链接后的程序,黑线和红线为C与汇编各自的部分。
img
运行结果
img

图形界面

分辨模式的设定

首先在asmhead.nas中先检查该画面模式是否可以使用,如果不支持采用前面代码展示的调用INT10中断设定为VGA显卡,320x200x8位彩色模式

		MOV		AL,0x13			;VGA显卡,320*200*8位彩色
		MOV		AH,0x00
		INT		0x10
		MOV		BYTE [VMODE],8	;记录画面模式
		MOV		WORD [SCRNX],320
		MOV		WORD [SCRNY],200

如果支持通过调用INT10中断设定为1024x768x8bit的画面模式

		MOV		BX,VBEMODE+0x4000 ;VBEMODE为0x105
		MOV		AX,0x4f02
		INT		0x10
		MOV		BYTE [VMODE],8
		MOV		AX,[ES:DI+0x12]
		MOV		[SCRNX],AX
		MOV		AX,[ES:DI+0x14]
		MOV		[SCRNY],AX
		MOV		EAX,[ES:DI+0x28]
		MOV		[VRAM],EAX

并且将boot信息进行保存,以供之后使用

;BOOT_INFO信息
CYLS	EQU		0x0ff0			;设定启动区
LEDS	EQU		0x0ff1
VMODE	EQU		0x0ff2			;颜色的位数
SCRNX	EQU		0x0ff4			;分辨率x大小
SCRNY	EQU		0x0ff6			;分辨率y大小
VRAM	EQU		0x0ff8			;图像缓冲区的开始地址

设定调色盘

因为之前设定使用1024x768x8的画面模式,其中可以用8位代表颜色,所以可以指定0-255个色号,默认0号对应黑色等(这就是默认的调色板),但是可以自己设定,这里设定16种颜色和对应的号码,每三个字节R,G,B对应一种颜色。
并且进行扩展,扩展方式是对每个RGB分6个亮度,比如会产生(0,0,0),(51,0,0),(102,0,0)……(255,0,0),(0,0,51)共216种颜色,加上原来的16中共232种(当然有一些重复)
在graphic.c

void init_palette(void)
{
	//16种颜色
	static unsigned char table_rgb[16 * 3] = {
		0x00, 0x00, 0x00,	/*  0:黑 */
		0xff, 0x00, 0x00,	/*  1:亮红 */
		0x00, 0xff, 0x00,	/*  2:亮绿 */
		0xff, 0xff, 0x00,	/*  3:亮黄 */
		0x00, 0x00, 0xff,	/*  4:亮蓝 */
		0xff, 0x00, 0xff,	/*  5:亮紫 */
		0x00, 0xff, 0xff,	/*  6:浅亮蓝 */
		0xff, 0xff, 0xff,	/*  7:白 */
		0xc6, 0xc6, 0xc6,	/*  8:亮灰 */
		0x84, 0x00, 0x00,	/*  9:暗红 */
		0x00, 0x84, 0x00,	/* 10:暗绿 */
		0x84, 0x84, 0x00,	/* 11:暗黄 */
		0x00, 0x00, 0x84,	/* 12:暗青 */
		0x84, 0x00, 0x84,	/* 13:暗紫 */
		0x00, 0x84, 0x84,	/* 14:浅暗蓝 */
		0x84, 0x84, 0x84	/* 15:暗灰 */
	};
	//扩展颜色
	unsigned char table2[216 * 3];
	int r, g, b;
	set_palette(0, 15, table_rgb);
	for (b = 0; b < 6; b++) {
		for (g = 0; g < 6; g++) {
			for (r = 0; r < 6; r++) {
				table2[(r + g * 6 + b * 36) * 3 + 0] = r * 51;
				table2[(r + g * 6 + b * 36) * 3 + 1] = g * 51;
				table2[(r + g * 6 + b * 36) * 3 + 2] = b * 51;
			}
		}
	}
	set_palette(16, 231, table2);
	return;

	/* static char 相当于汇编的db */
}

产生颜色后就是设定调色盘,即设定好这些颜色对应的号码,方法是使用OUT指令将调色板的号码写入端口号0x03c8,然后将对应的RGB写入端口号0x03c9

void set_palette(int start, int end, unsigned char* rgb)
{
	int i, eflags;
	eflags = io_load_eflags();	//记录中断
	io_cli(); 					//屏蔽中断
	//要写入调色板,需要将调色板的号码写入0x03c8,然后将对应的RGB写入0x03c9,下一个号码可以省略输入
	io_out8(0x03c8, start);
	for (i = start; i <= end; i++) {
		io_out8(0x03c9, rgb[0] / 4);
		io_out8(0x03c9, rgb[1] / 4);
		io_out8(0x03c9, rgb[2] / 4);
		rgb += 3;
	}
	io_store_eflags(eflags);	//恢复中断
	return;
}

其中调用的函数用汇编在naskfunc.nas定义,其中的EFLAGS寄存器是FLAGS的16位寄存器的扩展
用于存储进位和中断标志和跳转的判断,中断标志位是第9位
其中io_out8为向端口输出数据的函数

_io_out8:	; void io_out8(int port, int data);
		MOV		EDX,[ESP+4]		; port
		MOV		AL,[ESP+8]		; data
		OUT		DX,AL
		RET
;保存标志寄存器的值
_io_load_eflags:	; int io_load_eflags(void);
		PUSHFD		;先将寄存器的值压入栈,再弹出给EAX
		POP		EAX
		RET
;恢复标志寄存器的值
_io_store_eflags:	; void io_store_eflags(int eflags);
		MOV		EAX,[ESP+4]
		PUSH	EAX
		POPFD		
		RET
_io_cli:	; void io_cli(void);
		CLI	;中断标志置为0的指令,屏蔽中断
		RET

绘制桌面

本质就是将像素点变为某种颜色,这样就组成了一幅图片,也就绘制了桌面。
而只要向显卡内存VRAM中写入对应的颜色完成了上述操作,而VRAM在内存中的映射地址为0xa0000-0xaffff,所以只要向这个内存区域写入颜色数据即可
graphic.c

//绘制图形
//对于1024x768的画面模式,像素下(x,y)对应的vram地址为0xa0000+x+y*1024
//这里vram就是vram的地址,xsize就是在启动时保存的信息1024,c为颜色色号,后面的参数为绘制的矩形的左上角坐标和右下角坐标
void boxfill8(unsigned char* vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
	int x, y;
	for (y = y0; y <= y1; y++) {
		for (x = x0; x <= x1; x++)
			vram[y * xsize + x] = c;
	}
	return;
}

之后就是调用这个函数然后去设计桌面
graphic.c

void init_screen8(char* vram, int x, int y)
{
	boxfill8(vram, x, COL8_008484, 0, 0, x - 1, y - 29);
	boxfill8(vram, x, COL8_C6C6C6, 0, y - 28, x - 1, y - 28);
	boxfill8(vram, x, COL8_FFFFFF, 0, y - 27, x - 1, y - 27);
	boxfill8(vram, x, COL8_C6C6C6, 0, y - 26, x - 1, y - 1);

	boxfill8(vram, x, COL8_FFFFFF, 3, y - 24, 59, y - 24);
	boxfill8(vram, x, COL8_FFFFFF, 2, y - 24, 2, y - 4);
	boxfill8(vram, x, COL8_848484, 3, y - 4, 59, y - 4);
	boxfill8(vram, x, COL8_848484, 59, y - 23, 59, y - 5);
	boxfill8(vram, x, COL8_000000, 2, y - 3, 59, y - 3);
	boxfill8(vram, x, COL8_000000, 60, y - 24, 60, y - 3);

	boxfill8(vram, x, COL8_848484, x - 47, y - 24, x - 4, y - 24);
	boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y - 4);
	boxfill8(vram, x, COL8_FFFFFF, x - 47, y - 3, x - 4, y - 3);
	boxfill8(vram, x, COL8_FFFFFF, x - 3, y - 24, x - 3, y - 3);
	return;
}

在bootpack.c调用,其中binfo->scrnx, binfo->scrny为启动时保存的boot信息,即分辨率1024x768init_screen8(buf_back, binfo->scrnx, binfo->scrny);
以下为画出的桌面
img

显示字符

英文字符

字符其实也是一幅图片,这里采用16*8的字符大小,如图所示A,只要在特定位置涂黑就可以组成这个字符,也就需要字体文件规定在哪里涂黑,这个字体文件在书中提供了hankaku.txt,只需要经过转化在程序编译生成时链接进去即可
在bootpack.c中声明使用extern char hankaku[4096];
img
绘制时,对于每一个字符,只有传入其在字体文件hankaku[4096]中的位置char* font
然后遍历数据中的“16行”,通过8个与操作取得每一行的每一位,对不是0的每一位写入对应的颜色数据
graphic.c

//显示一个字符,一个字符占16*8个像素
void putfont8(char* vram, int xsize, int x, int y, char c, char* font)
{
	int i;
	char* p, d /* data */;
	for (i = 0; i < 16; i++) {
		p = vram + (y + i) * xsize + x;
		d = font[i];
		if ((d & 0x80) != 0) { p[0] = c; }
		if ((d & 0x40) != 0) { p[1] = c; }
		if ((d & 0x20) != 0) { p[2] = c; }
		if ((d & 0x10) != 0) { p[3] = c; }
		if ((d & 0x08) != 0) { p[4] = c; }
		if ((d & 0x04) != 0) { p[5] = c; }
		if ((d & 0x02) != 0) { p[6] = c; }
		if ((d & 0x01) != 0) { p[7] = c; }
	}
	return;
}

显示中文字符

书中的是显示日文字符,显示中文字符只需要进行一些小改动
首先中文是全角字符,且是上下结构,所以不同与英文字符采用的16*8的字符大小,中文字符需要16*16的大小,这里照抄了https://zhuanlan.zhihu.com/p/148126660的代码将上下两部分的8*16转为左右的两部分16*8,然后分别显示左右两部分
另外就是HZK16字库,在https://blog.twofei.com/embedded/hzk.html提供了字库的下载链接
然后按照书中译者提供的方法,将hankaku.txt生成的字库hankaku.bin复制一份,其包含0x1000大小的数据,通过winhex,将上面下载的字库的165440个字节复制,并粘贴到后面,改名就是之后用到的字库文件nihongo.fnt,如下所示
img
然后需要将字体文件导入到img,之后通过程序加载字库
其中是一些分配内存,读取磁盘文件的操作,在之后会解释,大致流程就是:先分配一段内存存储字体,在磁盘中读取字体文件,如果没有读取到,填充为英文字库和无意义的0xff(这里其实有些多余,但为了尽量减少对原来的修改)
最后字体文件的地址保存在0x0fe8
其中和原文不一样的就是分配内存改为了16 * 256 + 32 * 94 * 55,因为我们制作的字体文件nihongo.fnt包含复制来的256个英文字符,每个占16字节,和后面复制来的55个区,每个区94个字符,内个字符32字节共165440个字节(区的概念可以参考上面给出的两个链接)
bootpack.c

	//载入字库
	nihongo = (unsigned char*)memman_alloc_4k(memman, 16 * 256 + 32 * 94 * 55);
	fat = (int*)memman_alloc_4k(memman, 4 * 2880);
	file_readfat(fat, (unsigned char*)(ADR_DISKIMG + 0x000200));
	finfo = file_search("nihongo.fnt", (struct FILEINFO*)(ADR_DISKIMG + 0x002600), 224);
	if (finfo != 0) {
		i = finfo->size;
		nihongo = file_loadfile2(finfo->clustno, &i, fat);
	}
	else {

		for (i = 0; i < 16 * 256; i++) {
			nihongo[i] = hankaku[i]; //如果没有字库,直接复制英文字库
		}
		for (i = 16 * 256; i < 16 * 256 + 32 * 94 * 55; i++) {
			nihongo[i] = 0xff; //其它部分用0xff填充
		}
	}
	*((int*)0x0fe8) = (int)nihongo;
	memman_free_4k(memman, (int)fat, 4 * 2880);

显示字符串

字符串的显示就是用上面的函数将字符一个一个显示,但是为了显示汉字,这里稍微增加了一些逻辑
这里task是得到当前任务,在之后说
通过task中的字段langmode判断,如果是0为英文输入法,为1是中文输入法。
因为中文每个字符是由两个字节表示的(因为这里是GBK编码),task->langbyte1默认是0,通过判断,如果这个字节在这个范围,那么这是一个中文字符的字节,再让task->langbyte1指向这个中文字符的首字节,然后再下次循环就要通过这个字节找字符在字文件中的位置。
位置的计算公式为,原理在上面两个链接中有介绍

k = task->langbyte1 - 0xa1;
t = *s - 0xa1;
font = nihongo + 256 * 16 + (k * 94 + t) * 32;

找到位置就是上下两半变为左右两半,然后分开显示
graphic.c

//显示s字符串
void putfonts8_asc(char* vram, int xsize, int x, int y, char c, unsigned char* s)
{
	extern char hankaku[4096];//字体
	struct TASK* task = task_now();
	char* nihongo = (char*)*((int*)0x0fe8), * font;
	int k, t;

	if (task->langmode == 0) {
		for (; *s != 0x00; s++) {
			putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
			x += 8;
		}
	}
	if (task->langmode == 1) {
		for (; *s != 0x00; s++) {
			if (task->langbyte1 == 0) {
				if (0xa1 <= *s && *s <= 0xfe) {
					task->langbyte1 = *s;
				}
				else {
					putfont8(vram, xsize, x, y, c, nihongo + *s * 16);
				}
			}
			else {
				k = task->langbyte1 - 0xa1;
				t = *s - 0xa1;
				task->langbyte1 = 0;
				font = nihongo + 256 * 16 + (k * 94 + t) * 32;

				unsigned char New[16] = { 0,0,0,0,0,0,0,0,   0,0,0,0,0,0,0,0 };
				unsigned char New2[16] = { 0,0,0,0,0,0,0,0,   0,0,0,0,0,0,0,0 };
				int i;

				for (i = 0; i <= 7; i++)                /* 取上半边数据 */
				{
					New[i] = font[i * 2];        /* 左半角取偶数位置字节 */
					New2[i] = (font)[2 * i + 1];	  /* 右半角取奇数位置字节 */
				}

				for (i = 8; i <= 15; i++)              /* 取下半边数据 */
				{
					New[i] = (font + 16)[(i - 8) * 2];
					New2[i] = (font + 16)[(i - 8) * 2 + 1];
				}

				putfont8(vram, xsize, x - 8, y, c, New);	/* 左半角 */
				putfont8(vram, xsize, x, y, c, New2);
			}
			x += 8;
		}
	}
	return;
}

img

窗口绘制

和桌面绘制原理都一致,只要调整好大小,颜色等布局即可
window.c
相关函数有
绘制窗口:void make_window8(unsigned char* buf, int xsize, int ysize, char* title, char act)窗口
绘制窗口的标题栏:void make_wtitle8(unsigned char* buf, int xsize, char* title, char act)
绘制文本框:void make_textbox8(struct SHEET* sht, int x0, int y0, int sx, int sy, int c)
改变标题栏的颜色:void change_wtitle8(struct SHEET* sht, char act)
其中act参数和改变标题栏的颜色主要用于表示该窗口是否被选中,选中就改变颜色

图层叠加处理

当多个窗口叠加在一起,如何显示
当选中底层的窗口,它如何显示在上层
当窗口移动,如何快速刷新界面,重新显示
这些都需要使用图层的概念:即界面中每一个窗口,包括桌面,鼠标的显示,它们都可看做是一个图层,图层有顶部和底部的关系,桌面肯定始终处于最底部,鼠标始终处于最顶部,界面的显示就是这些图层叠加在一起的效果。
首先是图层的数据结构的定义

#define MAX_SHEETS		256//最多保存256个图层
//每个图层的数据结构
//保存每个图层内容的地址* buf,图层的bxsize,bysize大小,图层在画面的(vx0,vy0)坐标,图层颜色透明度col_inv,图层的高度(在画面的前后)height,图层是否显示flags,图层保存的SHTCTL结构*ctl,图层所属的任务*task,并且flags还通过0x10和0x20处是否为1标识该图层属于的任务是命令行任务还是应用程序任务
struct SHEET {
	unsigned char* buf;
	int bxsize, bysize, vx0, vy0, col_inv, height, flags;
	struct SHTCTL* ctl;
	struct TASK* task;
};
struct SHTCTL {
	unsigned char* vram, * map;//*map用于提高界面显示速度
	int xsize, ysize, top;//画面大小和最高图层高度
	struct SHEET* sheets[MAX_SHEETS];//保存图层从低到高排列后的图层地址
	struct SHEET sheets0[MAX_SHEETS];//保存每个图层
};

之后初始化上述结构,主要是给其指定保存数据地址
bootpack.c
shtctl = shtctl_init(memman, binfo->vram, binfo->scrnx, binfo->scrny);

比如要显示背景桌面,先要分配一个图层sht_back = sheet_alloc(shtctl); //bootpack.c也就是遍历SHTCL的sheets0,得到flags为0的(未分配),并把它设置为1(已分配)
然后为图层分配保存图层颜色数据的缓存地址,并设置图层的大小透明度等

//bootpack.c
buf_back = (unsigned char*)memman_alloc_4k(memman, binfo->scrnx * binfo->scrny);
sheet_setbuf(sht_back, buf_back, binfo->scrnx, binfo->scrny, -1);

而颜色数据是先写在这个图层的缓存中,之后再写在显存对应的varm地址中

当图层准备好后,调用sheet_refresh写入到varm中显示出来
sheet.c

//绘制图层
void sheet_refresh(struct SHEET* sht, int bx0, int by0, int bx1, int by1)
{
	//判断图层是否被隐藏,隐藏值为-1
	if (sht->height >= 0) {
		sheet_refreshsub(sht->ctl, sht->vx0 + bx0, sht->vy0 + by0, sht->vx0 + bx1, sht->vy0 + by1, sht->height, sht->height);
	}
	return;
}

因为有时候窗口移动,只有局部有变化,局部需要重新绘制,所以不必全部重新绘制,提高速度
另外由于很多窗口重叠,如果绘制了底层的像素,再绘制上面的像素,相当于重复做无用功,所以在SHTCTL加入了map这一结构
通过一个额外存储保存map信息(表明当前画面展示的图层的部分信息),每个图层对应1个号码
当一个图层的区域需要更新时,查看该图层的号码余map中该区域的号码是否一致,如果一致说明要更新,不一致,说明这个图层这一部分是在底层没有显示,不必更新。
img
每次更新时先更新map数据,在通过map更新界面
先通过判断,防止绘制画面外的区域
之后从需要更新的一层逐层向上遍历
每一次先计算出要更新的区域,更新时为了再次提高速度,一次更新不只写入1字节,而是在没有透明图层时异常更新4字节
sheet.c

void sheet_refreshmap(struct SHTCTL* ctl, int vx0, int vy0, int vx1, int vy1, int h0)
{
	int h, bx, by, vx, vy, bx0, by0, bx1, by1,sid4,*p;
	unsigned char* buf, sid, * map = ctl->map;
	struct SHEET* sht;
	//如果重新绘制的区域超过了画面就修正
	if (vx0 < 0) { vx0 = 0; }
	if (vy0 < 0) { vy0 = 0; }
	if (vx1 > ctl->xsize) { vx1 = ctl->xsize; }
	if (vy1 > ctl->ysize) { vy1 = ctl->ysize; }
	for (h = h0; h <= ctl->top; h++) {
		sht = ctl->sheets[h];
		sid = sht - ctl->sheets0;//用作这一图层的代表号码
		buf = sht->buf;
		//提前计算要更新的区域
		bx0 = vx0 - sht->vx0;
		by0 = vy0 - sht->vy0;
		bx1 = vx1 - sht->vx0;
		by1 = vy1 - sht->vy0;
		if (bx0 < 0) { bx0 = 0; }
		if (by0 < 0) { by0 = 0; }
		if (bx1 > sht->bxsize) { bx1 = sht->bxsize; }
		if (by1 > sht->bysize) { by1 = sht->bysize; }
		if (sht->col_inv == -1) {
			
			if ((sht->vx0 & 3) == 0 && (bx0 & 3) == 0 && (bx1 & 3) == 0) {
				//没有透明图层,4字节一起
				bx1 = (bx1 - bx0) / 4; //计算需要填几个4字节
				sid4 = sid | sid << 8 | sid << 16 | sid << 24;
				for (by = by0; by < by1; by++) {
					vy = sht->vy0 + by;
					vx = sht->vx0 + bx0;
					p = (int*)&map[vy * ctl->xsize + vx];//转为32位来填充
					for (bx = 0; bx < bx1; bx++) {
						p[bx] = sid4;
					}
				}
			}
			else {
				//没有透明图层,1字节
				for (by = by0; by < by1; by++) {
					vy = sht->vy0 + by;
					for (bx = bx0; bx < bx1; bx++) {
						vx = sht->vx0 + bx;
						map[vy * ctl->xsize + vx] = sid;
					}
				}
			}
		}
		else {
			//有透明图层
			for (by = by0; by < by1; by++) {
				vy = sht->vy0 + by;
				for (bx = bx0; bx < bx1; bx++) {
					vx = sht->vx0 + bx;
					if (buf[by * sht->bxsize + bx] != sht->col_inv) {
						map[vy * ctl->xsize + vx] = sid;
					}
				}
			}
		}
	}
	return;
}

更新时主要就是利用上面更新的map数据写入到vram中vram[vy * ctl->xsize + vx] = buf[by * sht->bxsize + bx];
sheet.c

//局部绘制图层
void sheet_refreshsub(struct SHTCTL* ctl, int vx0, int vy0, int vx1, int vy1, int h0, int h1)
{
	int h, bx, by, vx, vy, bx0, by0, bx1, by1, bx2, sid4, i, i1, * p, * q, * r;
	unsigned char* buf, * vram = ctl->vram, * map = ctl->map, sid;
	struct SHEET* sht;
	//如果重新绘制的区域超过了画面就修正
	if (vx0 < 0) { vx0 = 0; }
	if (vy0 < 0) { vy0 = 0; }
	if (vx1 > ctl->xsize) { vx1 = ctl->xsize; }
	if (vy1 > ctl->ysize) { vy1 = ctl->ysize; }
	for (h = h0; h <= h1; h++) {
		sht = ctl->sheets[h];
		buf = sht->buf;
		sid = sht - ctl->sheets0;
		bx0 = vx0 - sht->vx0;
		by0 = vy0 - sht->vy0;
		bx1 = vx1 - sht->vx0;
		by1 = vy1 - sht->vy0;
		if (bx0 < 0) { bx0 = 0; }
		if (by0 < 0) { by0 = 0; }
		if (bx1 > sht->bxsize) { bx1 = sht->bxsize; }
		if (by1 > sht->bysize) { by1 = sht->bysize; }
		//4字节
		if ((sht->vx0 & 3) == 0) {
			i = (bx0 + 3) / 4;
			i1 = bx1 / 4;
			i1 = i1 - i;
			sid4 = sid | sid << 8 | sid << 16 | sid << 24;
			for (by = by0; by < by1; by++) {
				vy = sht->vy0 + by;
				for (bx = bx0; bx < bx1 && (bx & 3) != 0; bx++) {	//前面被四除多余的部分写入
					vx = sht->vx0 + bx;
					if (map[vy * ctl->xsize + vx] == sid) {
						vram[vy * ctl->xsize + vx] = buf[by * sht->bxsize + bx];
					}
				}
				vx = sht->vx0 + bx;
				p = (int*)&map[vy * ctl->xsize + vx];
				q = (int*)&vram[vy * ctl->xsize + vx];
				r = (int*)&buf[by * sht->bxsize + bx];
				for (i = 0; i < i1; i++) {							
					if (p[i] == sid4) {
						q[i] = r[i];
					}
					else {
						bx2 = bx + i * 4;
						vx = sht->vx0 + bx2;
						if (map[vy * ctl->xsize + vx + 0] == sid) {
							vram[vy * ctl->xsize + vx + 0] = buf[by * sht->bxsize + bx2 + 0];
						}
						if (map[vy * ctl->xsize + vx + 1] == sid) {
							vram[vy * ctl->xsize + vx + 1] = buf[by * sht->bxsize + bx2 + 1];
						}
						if (map[vy * ctl->xsize + vx + 2] == sid) {
							vram[vy * ctl->xsize + vx + 2] = buf[by * sht->bxsize + bx2 + 2];
						}
						if (map[vy * ctl->xsize + vx + 3] == sid) {
							vram[vy * ctl->xsize + vx + 3] = buf[by * sht->bxsize + bx2 + 3];
						}
					}
				}
				for (bx += i1 * 4; bx < bx1; bx++) {				//后面被四除多余的部分写入
					vx = sht->vx0 + bx;
					if (map[vy * ctl->xsize + vx] == sid) {
						vram[vy * ctl->xsize + vx] = buf[by * sht->bxsize + bx];
					}
				}
			}
		}
		//1字节
		else {
			for (by = by0; by < by1; by++) {
				vy = sht->vy0 + by;
				for (bx = bx0; bx < bx1; bx++) {
					vx = sht->vx0 + bx;
					if (map[vy * ctl->xsize + vx] == sid) {
						vram[vy * ctl->xsize + vx] = buf[by * sht->bxsize + bx];
					}
				}
			}
		}
	}
	return;
}

图层上下左右移动

上下移动

//对图层高度信息的重新设置
void sheet_updown(struct SHEET *sht, int height)
{
	struct SHTCTL* ctl = sht->ctl;
	int h, old = sht->height; //保存旧高度

	//对设定的height进行检查
	if (height > ctl->top + 1) {
		height = ctl->top + 1;
	}
	if (height < -1) {
		height = -1;
	}
	sht->height = height; //设定高度

	if (old > height) {	//如果设定的高度比原先低
		if (height >= 0) {//如果图层没被隐藏
			//则把所有图层原先比它低的现在比它高的往后移
			for (h = old; h > height; h--) {
				ctl->sheets[h] = ctl->sheets[h - 1];
				ctl->sheets[h]->height = h;
			}
			ctl->sheets[height] = sht;
			sheet_refreshmap(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height + 1);
			sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height + 1, old);
		} else {	//如果图层被隐藏
			if (ctl->top > old) {//如果被隐藏的图层不是最高图层
				//那么原先比它高的图层的高度都应该减一
				for (h = old; h < ctl->top; h++) {
					ctl->sheets[h] = ctl->sheets[h + 1];
					ctl->sheets[h]->height = h;
				}
			}
			ctl->top--;
			sheet_refreshmap(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, 0);
			sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, 0, old - 1);
		}
	} else if (old < height) {	//如果设定的高度比原先高
		if (old >= 0) {//如果图层原来没被隐藏
			//则把所有图层原先比它高的现在比它低的往前移
			for (h = old; h < height; h++) {
				ctl->sheets[h] = ctl->sheets[h + 1];
				ctl->sheets[h]->height = h;
			}
			ctl->sheets[height] = sht;
		} else {//如果图层原来被隐藏,现在显示了
			//那么原先比它高的图层的高度都应该加一
			for (h = ctl->top; h >= height; h--) {
				ctl->sheets[h + 1] = ctl->sheets[h];
				ctl->sheets[h + 1]->height = h + 1;
			}
			ctl->sheets[height] = sht;
			ctl->top++;
		}
		sheet_refreshmap(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height);
		sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height, height);
	}
	return;
}

左右移动

//左右移动图层
void sheet_slide(struct SHEET *sht, int vx0, int vy0)
{
	struct SHTCTL* ctl = sht->ctl;
	int old_vx0 = sht->vx0, old_vy0 = sht->vy0;
	sht->vx0 = vx0;
	sht->vy0 = vy0;
	if (sht->height >= 0) {
		sheet_refreshmap(ctl, old_vx0, old_vy0, old_vx0 + sht->bxsize, old_vy0 + sht->bysize, 0);
		sheet_refreshmap(ctl, vx0, vy0, vx0 + sht->bxsize, vy0 + sht->bysize, sht->height);
		//移动前的地方把原图层下的图层重绘
		sheet_refreshsub(ctl, old_vx0, old_vy0, old_vx0 + sht->bxsize, old_vy0 + sht->bysize, 0, sht->height - 1);
		//移动后的地方只要重绘移动过去的图层
		sheet_refreshsub(ctl, vx0, vy0, vx0 + sht->bxsize, vy0 + sht->bysize, sht->height, sht->height);
	}
	return;
}

内存管理

内存的分布规划如下,主要管理0x00400000之后的内存

内存分布图
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-              : 空

定义了如下数据结构,FREEINFO表示一个空闲区域的地址和大小,MEMMAN记录这些空闲区域,lostsize, losts表示回收内存失败的内存大小和回收失败的次数,frees, maxfrees表示当前记录的最大空闲区域数和可以保存的最大空闲区域数
在0x003c0000处保存这些管理内存的数据结构

#define MEMMAN_FREES		4090	//可以保存的空闲区域的数量,大约可以管理32KB
#define MEMMAN_ADDR			0x003c0000
//空闲区域的数据结构
struct FREEINFO {
	unsigned int addr, size;
};
struct MEMMAN {
	int frees, maxfrees, lostsize, losts;
	struct FREEINFO free[MEMMAN_FREES];
};

内存检查

为了解决CPU与内存之间虚度不匹配的问题,在它们中间也就是CPU中加入了缓存模块(从486开始)。所以在进行内存检查时,要先关闭缓存,否则CPU一直读取的是缓存
内存检查首先要关闭CPU与内存间的缓存功能,关闭方法是对CR0的31和30位设为1来关闭缓存

unsigned int memtest(unsigned int start, unsigned int end)
{
	char flg486 = 0;
	unsigned int eflg, cr0, i;

	//首先检查CPU是386还是486。检查方法是看标志寄存器中第18位是否为0,386没有这一位,这一位一直都是0,通过向这一位写入1然后再读取来判断
	eflg = io_load_eflags();
	eflg |= EFLAGS_AC_BIT;
	io_store_eflags(eflg);
	eflg = io_load_eflags();
	if ((eflg & EFLAGS_AC_BIT) != 0) {
		flg486 = 1;
	}
	eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */
	io_store_eflags(eflg);

	//对CR0的31和30位设为1来关闭缓存
	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;
}

其中load_cr0和store_cr0就是用来读入和吸入cr0数据的

_load_cr0:		; int load_cr0(void);
		MOV		EAX,CR0
		RET

_store_cr0:		; void store_cr0(int cr0);
		MOV		EAX,[ESP+4]
		MOV		CR0,EAX
		RET

检查方法是向内存中写入数据,再反转数据后读出看是否和之前写入的数据反转后一样
但是下面的代码c编译器会自动优化导致无法实现功能,所以书中用汇编实现了下述函数功能
最后函数会返回指定范围内可用的内存的总量

//但编译器会有优化,造成功能无法实现
////内存的检查
//unsigned int memtest_sub(unsigned int start, unsigned int end)
//{
//	unsigned int i, * p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
//	//为了加快速度,每0x1000内存大小检查一次其ffc处
//	for (i = start; i <= end; i += 0x1000) {
//		p = (unsigned int*)(i + 0xffc);
//		old = *p;	
//		//写入内存,再反转后读出看是否一样
//		*p = pat0;			
//		*p ^= 0xffffffff;	
//		if (*p != pat1) {	
//		not_memory:
//			*p = old;
//			break;
//		}
//		*p ^= 0xffffffff;	
//		if (*p != pat0) {	
//			goto not_memory;
//		}
//		*p = old;			
//	}
//	return i;
//}

内存分配

方法有很多,假设以1KB为单位进行管理,可以以1位是否为1或0表示这1KB是否被占用
分配内存时,查看连续的足够大小的0,将其标记为1,然后使用这些位对应的地址
回收时,通过地址找到对应的位,将其标记为0

也可以通过一个数据结构记录当前空闲区域的数量,以及每个区域的起始地址和大小
分配时通过查找满足大小的空闲区域,然后减去分配的区域,更新这个空闲区域的大小和起始地址
回收时,还要查看回收的区域是否可以和相邻的区域合并为大的空闲区域
下面采用的是第二种
memory.c

//内存分配
unsigned int memman_alloc(struct MEMMAN* man, unsigned int size)
{
	unsigned int i, a;
	for (i = 0; i < man->frees; i++) {
		//找到符合大小的内存
		if (man->free[i].size >= size) {
			a = man->free[i].addr;
			//更新这个空余区域的信息
			man->free[i].addr += size;
			man->free[i].size -= size;
			//如果这个空域区域恰好被分配完,要删除
			if (man->free[i].size == 0) {
				man->frees--;
				//将后面的空域区域信息前移
				for (; i < man->frees; i++) {
					man->free[i] = man->free[i + 1];
				}
			}
			return a;//返回分配的地址
		}
	}
	return 0;
}

为了避免内存碎片,主要通过4K为单位进行分配,不足4K的部分也要向上舍入
memory.c

//按4K大小分配内存
unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size)
{
	unsigned int a;
	size = (size + 0xfff) & 0xfffff000;//以0x1000字节为单位即4KB向上舍入
	a = memman_alloc(man, size);
	return a;
}

内存回收

memory.c

//内存回收
int memman_free(struct MEMMAN* man, unsigned int addr, unsigned int size)
{
	int i, j;
	//遍历查看这一回收的区域应该放在哪里
	for (i = 0; i < man->frees; i++) {
		if (man->free[i].addr > addr) {
			break;
		}
	}
	if (i > 0) {
		//如果正好能和前面空域区域拼在一起
		if (man->free[i - 1].addr + man->free[i - 1].size == addr) {
			man->free[i - 1].size += size;
			if (i < man->frees) {
				//如果正好能和后面区域拼在一起
				if (addr + size == man->free[i].addr) {
					man->free[i - 1].size += man->free[i].size;
					man->frees--;
					//将后面的区域信息前移
					for (; i < man->frees; i++) {
						man->free[i] = man->free[i + 1];
					}
				}
			}
			return 0;
		}
	}
	//如果不能和前面的区域拼在一起
	if (i < man->frees) {
		//但正好能和后面的区域拼在一起
		if (addr + size == man->free[i].addr) {
			man->free[i].addr = addr;
			man->free[i].size += size;
			return 0;
		}
	}
	//如果和前面和后面的区域都不能拼在一起,而可以保存的空闲区域也没有被占满
	if (man->frees < MEMMAN_FREES) {
		for (j = man->frees; j > i; j--) {
			man->free[j] = man->free[j - 1];
		}
		man->frees++;
		if (man->maxfrees < man->frees) {
			man->maxfrees = man->frees;
		}
		man->free[i].addr = addr;
		man->free[i].size = size;
		return 0;
	}
	//如果以上都没满足,则这次回收失败,进行记录
	man->losts++;
	man->lostsize += size;
	return -1;
}

memory.c

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;
}

缓冲区

之后的鼠标键盘传送的数据,各程序间传递的数据都是保存在定义好的缓冲区内
缓冲区就是特别划定一块内存,其数据结构定义为:buf为缓冲区地址,p是下一个数据写入位置,q为下一个读取位置,size为缓冲区大小,free为缓冲区空域大小,*task为缓冲区属于的任务程序,flag为0为正常,1为溢出

struct FIFO32 {
	int* buf;
	int p, q, size, free, flags;
	struct TASK* task;
};

缓冲区存取数据,其中任务功能部分主要针对唤醒休眠的任务,这在之后任务部分介绍
fifo.c

//往缓冲区保存数据
int fifo32_put(struct FIFO32 *fifo, int data)
{
	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--;
	//如果有任务功能
	if (fifo->task != 0) {
		//当处于休眠状态,进行唤醒
		if (fifo->task->flags != 2) {
			task_run(fifo->task, -1, 0);
		}
	}
	return 0;
}
//从缓冲区取数据
int fifo32_get(struct FIFO32 *fifo)
{
	int data;
	if (fifo->free == fifo->size) {
		//缓冲区为空
		return -1;
	}
	data = fifo->buf[fifo->q];
	fifo->q++;
	if (fifo->q == fifo->size) {
		fifo->q = 0;
	}
	fifo->free++;
	return data;
}

鼠标与键盘

鼠标的显示

鼠标本质也是和桌面一样,画一个鼠标的形状即可

//画鼠标,bc指背景颜色
void init_mouse_cursor8(char* mouse, char bc)
{
	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;
}

同样是定义图层,定义保存图层内容的内存(鼠标是16*16的,所以定义buf_mouse[256]),设定好后,通过init_mouse_cursor8将内容画在buf_mouse所指内存中
最后通过sheet_slide显示出鼠标,并用sheet_updown设定其图层高度为2(因为开始桌面在底层高度为1,命令行窗口在中间设定为1)

unsigned char* buf_mouse[256]
sht_mouse = sheet_alloc(shtctl);//鼠标图层
sheet_setbuf(sht_mouse, buf_mouse, 16, 16, 99);
init_mouse_cursor8(buf_mouse, 99);

mx = (binfo->scrnx - 16) / 2;
my = (binfo->scrny - 28 - 16) / 2;
sheet_slide(sht_mouse, mx, my);
sheet_updown(sht_mouse, 2);

键盘与鼠标中断

鼠标和键盘在点击时,会通过PIC发送中断信号,PIC再通知CPU,根据中断号在IDT中找到对应的处理函数,然后调用内存中的函数来进行处理。
所以需要初始化GDT,IDT,PIC,编写中断处理函数,并且初始化键盘控制电路(鼠标控制电路的设定包含在键盘控制电路里,键盘控制电路初始化完成,鼠标电路控制器激活也就完成了
),另外鼠标需要先执行激活鼠标的指令,使其能产生中断信号,并且要使鼠标控制电路生效,可以向CPU发出中断

GDT与IDT设置

GDT全局段记录表
CPU段寄存器只有16位,但是其中第一位表示特权级RPL,第二到三位表示描述符类型GDT和LDT,所以只能有13位来表示段号。
每个段号对应的内容保存在内存中的GDT中,每个GDT项包含段的大小,段的起始地址,段的属性(写入,执行等)8字节的信息
GDT的起始地址存放在CPU的GDTR寄存器中

IDT中断记录表
记录了0-255的中断号码与调用函数的对应关系

通过以下两个函数初始化每个段表和中断表项

void set_segmdesc(struct SEGMENT_DESCRIPTOR* sd, unsigned int limit, int base, int ar)
{
	/*
	因为段上限最大为4GB,但是为了表示4GB需要32位的值,这样8字节的GDT项不够用,
	所以只用20位来表示,可以表示1MB,并设置一个标志位,当标志位为1时,段上限的单位是页,CPU中一页4KB
	limit_high的高四位里表示段属性,这四位中第一位就是这个标志,第二位指段的模式,1为32位模式,0指80286的16模式
	段属性的八位是系统模式专用段ring0还是应用模式专用段ring3,ring1,2是设备驱动器使用,是否可读,可写,可执行
	*/
	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;
}

void set_gatedesc(struct GATE_DESCRIPTOR* gd, int offset, int selector, int ar)
{
	gd->offset_low = offset & 0xffff;
	gd->selector = selector;
	gd->dw_count = (ar >> 8) & 0xff;
	gd->access_right = ar & 0xff;
	gd->offset_high = (offset >> 16) & 0xffff;
	return;
}

然后初始化段表和中断表,其中load_gdtr和load_idtr是将两个表的地址保存在对应的寄存器上
asm_inthandler21等为对应的中断函数

//初始化GDT和IDT
void init_gdtidt(void)
{
	//这里人为设置它的起始地址
	struct SEGMENT_DESCRIPTOR* gdt = (struct SEGMENT_DESCRIPTOR*)ADR_GDT;
	struct GATE_DESCRIPTOR* idt = (struct GATE_DESCRIPTOR*)ADR_IDT;
	int i;

	//初始化GDT
	for (i = 0; i < 8192; i++) {
		set_segmdesc(gdt + i, 0, 0, 0);
	}
	//设置第一个第二个段,第一个段的大小为0xffffffff,起始地址为0x00000000,属性为0x4092
	set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
	//这个段就是bootpack整个程序段,其中段属性AR_CODE32_ER为0x008e表示用于中断
	set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
	load_gdtr(LIMIT_GDT, ADR_GDT);

	//初始化IDT
	for (i = 0; i < 256; i++) {
		set_gatedesc(idt + i, 0, 0, 0);
	}
	load_idtr(LIMIT_IDT, ADR_IDT);

	//初始化键盘鼠标等中断记录表,其中2<<3和2*都一样,都表示段号为2
	set_gatedesc(idt + 0x0c, (int)asm_inthandler0c, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x0d, (int)asm_inthandler0d, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x20, (int)asm_inthandler20, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x21, (int)asm_inthandler21, 2<<3, AR_INTGATE32);
	set_gatedesc(idt + 0x27, (int)asm_inthandler27, 2<<3, AR_INTGATE32);
	set_gatedesc(idt + 0x2c, (int)asm_inthandler2c, 2<<3, AR_INTGATE32);
	set_gatedesc(idt + 0x40, (int)asm_hrb_api, 2 * 8, AR_INTGATE32 + 0x60);
	return;
}

PIC可编程中断控制器

CPU只能处理一个中断,通过增加PIC可以处理更多的中断信号.PIC被集成在主板上的芯片组里,与CPU相连的为主PIC,另一个为从PIC
PIC中的寄存器:
8位寄存器IMR中断屏蔽寄存器
如果某一位值为1,则改路IRQ信号被屏蔽,在进行中断设定时,为避免其它路的中的中断引起混乱,要屏蔽中断。没有设备连接时也要屏蔽所有中断。
ICW初始化控制数据,有4个,每个1字节
ICW1和ICW4与主板电气特性有关
ICW3有关主从连接的设定,主从PIC第几号IRQ相连,对应位就要设为1,所以设为00000100
ICW2决定IRQ以哪一号中断通知CPU
当中断发生,如果CPU可以处理,PIC就会用相连的数据线传送2字节的数据,为0xcd 0x,0xcd就是INT指令,0x为设定的中断号
因为CPU会自动产生INT0x00-0x1f的中断,所以这里用INT0x20-0x2f接受中断信号IRQ0-15
img

//初始化PIC
void init_pic(void)
{
	//PIC0与PIC1指主从PIC
	io_out8(PIC0_IMR,  0xff  ); //禁止所有中断
	io_out8(PIC1_IMR,  0xff  ); //禁止所有中断

	io_out8(PIC0_ICW1, 0x11  ); //边沿触发模式
	io_out8(PIC0_ICW2, 0x20  ); //IRQ0-7由INT20-27接收
	io_out8(PIC0_ICW3, 1 << 2); //主PIC由IRQ2连接从PIC
	io_out8(PIC0_ICW4, 0x01  ); //无缓冲区模式

	io_out8(PIC1_ICW1, 0x11  ); //边沿触发模式
	io_out8(PIC1_ICW2, 0x28  ); //IRQ8-15由INT28-2f接收
	io_out8(PIC1_ICW3, 2     ); //主PIC由IRQ2连接从PIC
	io_out8(PIC1_ICW4, 0x01  ); //无缓冲区模式

	io_out8(PIC0_IMR,  0xfb  ); //11111011 主PIC除IQR2以外全部禁止
	io_out8(PIC1_IMR,  0xff  ); //11111111 禁止所有中断

	return;
}

初始化键盘控制电路

void wait_KBC_sendready(void)
{
	//等待键盘控制电路准备完毕
	//如果键盘准备好了,可以接受CPU指令了,那么0x0064处读取的数据的倒数第二位应该是0
	for (;;) {
		if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
			break;
		}
	}
	return;
}
//初始化键盘控制电路
void init_keyboard(struct FIFO32* fifo, int data0)
{
	keyfifo = fifo;
	keydata0 = data0;
	//一边等待键盘控制电路是否准备完成,一边发送模式设定指令,指令为0x60,鼠标模式的模式号码为0x47
	wait_KBC_sendready();
	io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
	wait_KBC_sendready();
	io_out8(PORT_KEYDAT, KBC_MODE);
	return;
}

激活鼠标

//激活鼠标
void enable_mouse(struct MOUSE_DEC* mdec)
{
	wait_KBC_sendready();
	//如果往键盘控制电路发送指令0xd4,下一个数据自动发送给鼠标
	io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
	wait_KBC_sendready();
	io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
	mdec->phase = 0;
	return;
	//如果成功,鼠标会返回0xfa
}

中断函数的编写

鼠标中断函数,其连接在IRQ12
在向缓存中存放数据时加入了人为设计的mousedata0值,其设定为512,因为之后设计中鼠标数据和键盘数据都存放在同一个缓存中,为了区分是谁的数据

//处理鼠标中断
void inthandler2c(int* esp)
{
	int data;
	io_out8(PIC1_OCW2, 0x64);	//通知PIC1"IRQ-12已经知道发生了中断
	io_out8(PIC0_OCW2, 0x62);	//通知PIC0"IRQ-02已经知道发生了中断
	data = io_in8(PORT_KEYDAT);
	fifo32_put(mousefifo, data + mousedata0);
	return;
}

键盘中断函数
在向缓存中存放数据时加入了人为设计的keydata0值,其设定为256,

void inthandler21(int* esp)
{
	int data;
	io_out8(PIC0_OCW2, 0x61);	//通知PIC"IRQ-01已经知道发生了中断,即0x60+IRQ号码输出给OCW2即可
	data = io_in8(PORT_KEYDAT);
	fifo32_put(keyfifo, data + keydata0);
	return;
}

但是中断首先保存寄存器的值,调用中断处理函数,再恢复寄存器,并IRETD

;中断处理后不能执行return对应RET指令。必须执行IRET指令
;PUSHAD指EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI全部入栈
;对于c语言,SS,DS,ES必须指同一个段
;该程序首先保存寄存器的值,调用中断处理函数,再恢复寄存器,并IRETD
_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

_asm_inthandler2c:
		PUSH	ES
		PUSH	DS
		PUSHAD
		MOV		EAX,ESP
		PUSH	EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL	_inthandler2c
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		IRETD

鼠标数据的解析

收到的鼠标数据和键盘数据都放在同一个缓存中。
其中鼠标每一次移动点击会产生三字节的信息,鼠标按键的状态(点了哪个键)在第一个字节的低3位,第二字节和鼠标左右移动有关,第三字节和鼠标上下移动有关,根据这些信息可以解析鼠标向哪里移动了多少,然后在新位置画出鼠标就实现了鼠标的移动

int mouse_decode(struct MOUSE_DEC* mdec, unsigned char dat)
{
	if (mdec->phase == 0) {
		//鼠标准备完成后发送的0xfa
		if (dat == 0xfa) {
			mdec->phase = 1;
		}
		return 0;
	}
	if (mdec->phase == 1) {
		//鼠标的第一字节
		if ((dat & 0xc8) == 0x08) {
			//判读第一个字节对移动的部分是否在0-3范围,对点击的部分是否在8-f的范围
			mdec->buf[0] = dat;
			mdec->phase = 2;
		}
		return 0;
	}
	if (mdec->phase == 2) {
		//鼠标的第二字节
		mdec->buf[1] = dat;
		mdec->phase = 3;
		return 0;
	}
	if (mdec->phase == 3) {
		//鼠标的第三字节
		mdec->buf[2] = dat;
		mdec->phase = 1;
		mdec->btn = mdec->buf[0] & 0x07;//鼠标按键的状态在第一个字节的低3位
		mdec->x = mdec->buf[1];//第二字节和鼠标左右移动有关
		mdec->y = mdec->buf[2];//第三字节和鼠标上下移动有关
		if ((mdec->buf[0] & 0x10) != 0) {
			mdec->x |= 0xffffff00;
		}
		if ((mdec->buf[0] & 0x20) != 0) {
			mdec->y |= 0xffffff00;
		}
		mdec->y = -mdec->y; //鼠标与屏幕的y方向相反
		return 1;
	}
	return -1;
}

每次从缓存中取出数据进行判断,因为在存入数据时加入了512,所以鼠标的数据在512到767之间
然后更新鼠标新的坐标new_mx和new_my
判断是否点击了左键
如果点击了左键,判断mmx是否小于零(mmx初始值为-1,表示鼠标没有正在点击移动窗口,如果之后判断为正在移动,就为当前鼠标坐标的值)

如果mmx小于0了
就从上到下遍历此处图层,如果鼠标点击在该图层上,且点击的不是透明部分,进行如下操作:
先将该图层移到最高处(只比鼠标图层低)
然后判断是否为当前正在活动的命令行窗口,如果是,调用keywin_off关闭原来的窗口,调用keywin_on设置当前窗口为活动窗口。在这两个函数中会首先改变窗口的标题栏颜色,并根据图层的flags判断是否为命令行窗口,如果是,向该任务的缓存中存入数据2或3,在命令行程序中会跟据这些数据进行光标的隐藏和显示
并且判断是否点击的区域是否为标题栏,如果是就进入了移动模式,mmx,mmy就变成了当前鼠标的坐标
最后判断点击的是否为叉号,如果是,再判断该窗口是否为应用程序,如果是就要向应用程序所属的命令行打印程序结束的信息,并且将程序下一条指令指向一个程序结束的函数asm_end_app,然后立马启动该应用程序来执行来结束程序。如果是命令行程序就向该任务的缓存中存入数据4,在命令行程序中会执行cmd_exit函数,该函数再向操作系统主程序的缓存发送范围768到1023的数据,释放内存空间等

如果mmx不小于0
就是移动模式要计算出鼠标新的坐标new_wx,new_wy
为了加快速度,当鼠标和键盘的缓存中没有数据时立马进行更新画面
其中和鼠标一起移动的窗口也是这样的更新思路,并且它要一退出移动模式就要立马更新
另外为了更快刷新画面,以便使用上面关于图层4字节写入的部分,这里通过new_wx = (mmx2 + x + 2) & ~3;对坐标强制舍入为4字节的整数倍

else if (512 <= i && i <= 767) {
				if (mouse_decode(&mdec, i - 512) != 0) {
					//鼠标移动显示
					mx += mdec.x;
					my += mdec.y;
					//鼠标不能移出屏幕外的判断
					if (mx < 0) {
						mx = 0;
					}
					if (my < 0) {
						my = 0;
					}
					if (mx > binfo->scrnx - 1) {
						mx = binfo->scrnx - 1;
					}
					if (my > binfo->scrny - 1) {
						my = binfo->scrny - 1;
					}
					new_mx = mx;
					new_my = my; 
					//移动主窗口
					if ((mdec.btn & 0x01) != 0) {
						//普通模式
						if (mmx < 0) {
							//寻找鼠标点击的图层
							for (j = shtctl->top - 1; j > 0; j--) {
								sht = shtctl->sheets[j];
								x = mx - sht->vx0;
								y = my - sht->vy0;
								if (0 <= x && x < sht->bxsize && 0 <= y && y < sht->bysize) {
									if (sht->buf[y * sht->bxsize + x] != sht->col_inv) {
										sheet_updown(sht, shtctl->top - 1);
										//窗口的切换
										if (sht != key_win) {
											keywin_off(key_win);
											key_win = sht;
											keywin_on(key_win);
										}
										//如果点击了窗口工具栏
										if (3 <= x && x < sht->bxsize - 3 && 3 <= y && y < 21) {
											mmx = mx;
											mmy = my;
											mmx2 = sht->vx0;
											new_wy = sht->vy0;
										}
										//如果点击了叉号
										if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {
											//如果是应用程序窗口,通过0x10处是否为1判断是否为应用程序
											if ((sht->flags & 0x10) != 0) {
												task = sht->task;
												cons_putstr0(task->cons, "\nBreak(mouse) :\n");
												io_cli();
												task->tss.eax = (int)&(task->tss.esp0);
												task->tss.eip = (int)asm_end_app;
												io_sti();
												task_run(task, -1, 0);
											}
											//向命令行程序发送数据
											else {
												task = sht->task;
												sheet_updown(sht, -1);//暂时隐藏图层
												keywin_off(key_win);
												key_win = shtctl->sheets[shtctl->top - 1];
												keywin_on(key_win);
												io_cli();
												fifo32_put(&task->fifo, 4);
												io_sti();
											}
										}
										break;
									}
								}
							}
						}
						//移动模式
						else {
							x = mx - mmx;
							y = my - mmy;
							new_wx = (mmx2 + x + 2) & ~3;
							new_wy = new_wy + y;
							mmy = my;
						}
					}
					//没有按下左键,设置为普通模式
					else {
						mmx = -1;
						if (new_wx != 0x7fffffff) {
							sheet_slide(sht, new_wx, new_wy);	/* 堦搙妋掕偝偣傞 */
							new_wx = 0x7fffffff;
						}
					}
				}
			}

键盘数据的解析

收到的鼠标数据和键盘数据都放在同一个缓存中。
键盘中每个键按下和弹起式都会传输一个数据

字符数据
首先设置两个数组,分别存储按下和不按下shift按键时键盘对应的数据

	static char keytable0[0x80] = {
		0,   0,   '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '^', 0x08, 0,
		'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '@', '[', 0x0a, 0, 'A', 'S',
		'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', ':', 0,   0,   ']', 'Z', 'X', 'C', 'V',
		'B', 'N', 'M', ',', '.', '/', 0,   '*', 0,   ' ', 0,   0,   0,   0,   0,   0,
		0,   0,   0,   0,   0,   0,   0,   '7', '8', '9', '-', '4', '5', '6', '+', '1',
		'2', '3', '0', '.', 0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
		0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
		0,   0,   0,   0x5c, 0,  0,   0,   0,   0,   0,   0,   0,   0,   0x5c, 0,  0
	};
	static char keytable1[0x80] = {
		0,   0,   '!', 0x22, '#', '$', '%', '&', 0x27, '(', ')', '~', '=', '~', 0x08, 0,
		'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '`', '{', 0x0a, 0, 'A', 'S',
		'D', 'F', 'G', 'H', 'J', 'K', 'L', '+', '*', 0,   0,   '}', 'Z', 'X', 'C', 'V',
		'B', 'N', 'M', '<', '>', '?', 0,   '*', 0,   ' ', 0,   0,   0,   0,   0,   0,
		0,   0,   0,   0,   0,   0,   0,   '7', '8', '9', '-', '4', '5', '6', '+', '1',
		'2', '3', '0', '.', 0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
		0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
		0,   0,   0,   '_', 0,   0,   0,   0,   0,   0,   0,   0,   0,   '|', 0,   0
	};

然后将键盘传入的数据对应上面的数组进行转换,并存入s[0]
之后进行大小写的判断,如果大写锁定键和shift键只有一个按下,则要转为大写

if (((key_leds & 4) == 0 && key_shift != 0) ||((key_leds & 4) != 0 && key_shift == 0)) {
	s[0] += 0x20;	//大写字母转为小写字母
}

最后将这个数据存入当前任务的缓存中,以供其后续处理

shift键
有左右两个shift键,当两个shift键都没按下时,key_shift设置为0

对锁定键numlock,capslock等的支持

可以通过bios中保存的键盘信息获得,在启动时就进行了保存

keystatus:
		MOV		AH,0x02
		INT		0x16 			; keyboard BIOS
		MOV		[LEDS],AL

binfo->leds第4位为scrollLock状态
binfo->leds第5位为numlock状态
binfo->leds第6位为capslock状态

对于NumLock和CapsLock等LED的控制,可采用下面的方法向键盘发送指令和数据。
读取状态寄存器,等待bit 1的值变为0。
向数据输出(0060)写入要发送的1个字节数据。
等待键盘返回1个字节的信息,这和等待键盘输入所采用的方法相同(用IRQ等待或者用轮询状态寄存器bit 1的值直到其变为0都可以)。
返回的信息如果为0xfa,表明1个字节的数据已成功发送给键盘。如为0xfe则表明发送失败,需要返回第1步重新发送。
要控制LED的状态,需要按上述方法执行两次,向键盘发送EDxx数据。其中,xx的bit 0代表ScrollLock, bit 1代表NumLock, bit 2代表CapsLock(0表示熄灭,1表示点亮)。bit 3~7为保留位,置0即可

这里就可以根据自己需要赋予各种按键的功能,书中定义了强制结束功能为shift+f1:实现方法就是将这个任务的下一条指令指向asm_end_app的函数,然后执行

打开新命令行功能为shift+f2:实现方法主要调用pen_console,分配图层,分配图层缓存,画出界面,设定任务(在之后任务部分解释)

struct TASK* open_constask(struct SHEET* sht, unsigned int memtotal)
{
	struct MEMMAN* memman = (struct MEMMAN*)MEMMAN_ADDR;
	struct TASK* task = task_alloc();
	int* cons_fifo = (int*)memman_alloc_4k(memman, 128 * 4);
	task->cons_stack = memman_alloc_4k(memman, 64 * 1024);
	task->tss.esp = task->cons_stack + 64 * 1024 - 12;
	task->tss.eip = (int)&console_task;
	task->tss.es = 1 * 8;
	task->tss.cs = 2 * 8;
	task->tss.ss = 1 * 8;
	task->tss.ds = 1 * 8;
	task->tss.fs = 1 * 8;
	task->tss.gs = 1 * 8;
	*((int*)(task->tss.esp + 4)) = (int)sht;
	*((int*)(task->tss.esp + 8)) = memtotal;
	task_run(task, 2, 2); /* level=2, priority=2 */
	fifo32_init(&task->fifo, 128, cons_fifo, task);
	return task;
}

struct SHEET* open_console(struct SHTCTL* shtctl, unsigned int memtotal)
{
	struct MEMMAN* memman = (struct MEMMAN*)MEMMAN_ADDR;
	struct SHEET* sht = sheet_alloc(shtctl);
	unsigned char* buf = (unsigned char*)memman_alloc_4k(memman, 256 * 165);
	sheet_setbuf(sht, buf, 256, 165, -1); 
	make_window8(buf, 256, 165, "console", 0);
	make_textbox8(sht, 8, 28, 240, 128, COL8_000000);
	sht->task = open_constask(sht, memtotal);
	sht->flags |= 0x20;	//0x20处为1就是有光标
	return sht;
}

键盘的判断逻辑

if (256 <= i && i <= 511) {
				if (i < 0x80 + 256) {
					if (key_shift == 0) {
						s[0] = keytable0[i - 256];
					}
					else {
						s[0] = keytable1[i - 256];
					}
				}
				else {
					s[0] = 0;
				}
				if ('A' <= s[0] && s[0] <= 'Z') {
					if (((key_leds & 4) == 0 && key_shift == 0) ||((key_leds & 4) != 0 && key_shift != 0)) {
						s[0] += 0x20;	//大写字母转为小写字母
					}
				}
				if (s[0] != 0 && key_win != 0) {
						fifo32_put(&key_win->task->fifo, s[0] + 256);
				}
				//tab键切换窗口
				if (i == 256 + 0x0f && key_win != 0) {
					keywin_off(key_win);
					j = key_win->height - 1;
					if (j == 0) {
						j = shtctl->top - 1;
					}
					key_win = shtctl->sheets[j];
					keywin_on(key_win);
				}
				//左右两个shift键
				if (i == 256 + 0x2a) {
					key_shift |= 1;
				}
				if (i == 256 + 0x36) {
					key_shift |= 2;
				}
				//抬起关闭
				if (i == 256 + 0xaa) {
					key_shift &= ~1;
				}
				if (i == 256 + 0xb6) {
					key_shift &= ~2;
				}
				if (i == 256 + 0x3a) {	/* CapsLock */
					key_leds ^= 4;
					fifo32_put(&keycmd, KEYCMD_LED);
					fifo32_put(&keycmd, key_leds);
				}
				if (i == 256 + 0x45) {	/* NumLock */
					key_leds ^= 2;
					fifo32_put(&keycmd, KEYCMD_LED);
					fifo32_put(&keycmd, key_leds);
				}
				if (i == 256 + 0x46) {	/* ScrollLock */
					key_leds ^= 1;
					fifo32_put(&keycmd, KEYCMD_LED);
					fifo32_put(&keycmd, key_leds);
				}
				//强制结束键
				if (i == 256 + 0x3b && key_shift != 0 && key_win != 0) {
					task = key_win->task;
					if (task != 0 && task->tss.ss0 != 0) {	/* Shift+F1 */
						cons_putstr0(task->cons, "\nBreak(key) :\n");
						io_cli();
						task->tss.eax = (int)&(task->tss.esp0);
						task->tss.eip = (int)asm_end_app;
						io_sti();
						task_run(task, -1, 0);
					}
				}
				//打开新命令行
				if (i == 256 + 0x3c && key_shift != 0) {	/* Shift+F2 */
					if (key_win != 0) {
						keywin_off(key_win);
					}
					key_win = open_console(shtctl, memtotal);
					sheet_slide(key_win, 32, 4);
					sheet_updown(key_win, shtctl->top);
					keywin_on(key_win);
				}
				if (i == 256 + 0x57) {	/* F11 */
					sheet_updown(shtctl->sheets[1], shtctl->top - 1);
				}
				if (i == 256 + 0xfa) {	//键盘成功接收到返回的数据
					keycmd_wait = -1;
				}
				if (i == 256 + 0xfe) {	//键盘成功接收到返回的数据
					wait_KBC_sendready();
					io_out8(PORT_KEYDAT, keycmd_wait);
				}
			}

多任务

通过让CPU不断快速切换,处理多个任务,使得看似CPU在同一时间在处理多个任务(实际上同一时间只处理一个任务)。
实现:

  1. 首先需要一个定时器,负责计时,然后计时结束进行任务切换
  2. 其次需要保存任务的数据结构。当CPU进行切换时将寄存器中的内容写到内存中,然后把下一个任务的内容从内存中读到寄存器中。在内存中通过GDT的一种数据结构TSS(任务状态段)来保存,每个正在执行的任务的段号会以段号*8的形式保存在任务寄存器TR中
  3. 然后切换时通过jmp命令跳转到下一任务的TSS处
    JMP命令有near模式如jmp 地址,只改写EIP
    有far模式如jmp 地址1:地址2,改写CS(地址1)与EIP(地址2)
    TSS使用的就是far的jmp来进行任务切换,只要指定jmp 下一个任务段TSS地址:0即可
    CPU每次执行带段地址的指令(如far的jmp),都回去GDT中判断是普通的跳转,还是任务切换
  4. 最后还要有任务的休眠,任务的优先级设定等

定时器

管理定时器需要对PIT(可编程的间隔型定时器)进行设定。PIT连接着PIC中的IRQ0,这样就能设定它多少秒产生一次中断,设定方法为

AL=0x34 OUT(0x43,AL)
AL=中断周期低8位 OUT(0x40,AL)
AL=中断周期高8位 OUT(0x40,AL)

中断产生的频率=主频/设定的中断周期数
定时器数据结构设定为

//计时结构,向fifo中写入数据通知超时
#define MAX_TIMER		500
struct TIMER {
	struct TIMER* next;//记录下一个timer的地址,是一个链式结构,便于插入和删除
	unsigned int timeout;//超时的时刻
	char flags,flags2;//flags2用于区分定时器是否需要在应用程序结束时自动取消,flags为0表示未被使用,为1表示配置好了,为2表示正在运行
	struct FIFO32* fifo;//主要是为了光标实现的,设置为光标所在任务的缓存的地址,当光标一闪一闪时,需要向里面写入数据
	int data;//data向缓存里面写入的数据
};
struct TIMERCTL {
	unsigned int count, next;//count为现在的时刻,每次中断,这个值就加1
	struct TIMER* t0;//按timeout从少到多排序,来记录计时器,链式结构,第一个timer的地址
	struct TIMER timers0[MAX_TIMER];
};

设定一个定时器
如果新设定的定时器的超市时刻早于现在的最早的超时时刻,就应将其插在头部,否则插在中间
这里进行了一个优化:初始化时就设置好一个始终位于尾部的定时器,这样不用考虑插在尾部的情况,减少代码复杂度

//设置定时器超时时刻
void timer_settime(struct TIMER* timer, unsigned int timeout)
{
	int e;
	struct TIMER* t, * s;
	timer->timeout = timeout + timerctl.count;//系统当前总的时刻+超时时间=超时的时刻
	timer->flags = TIMER_FLAGS_USING;
	e = io_load_eflags();
	io_cli();
	t = timerctl.t0;
	//在头部插入时
	t = timerctl.t0;
	if (timer->timeout <= t->timeout) {
		timerctl.t0 = timer;
		timer->next = t;
		timerctl.next = timer->timeout;
		io_store_eflags(e);
		return;
	}
	//在中间插入时
	for (;;) {
		s = t;
		t = t->next;
		if (timer->timeout <= t->timeout) {
			s->next = timer; 
			timer->next = t;
			io_store_eflags(e);
			return;
		}
	}
	return;
}

关于中断的设置和键盘鼠标的中断设置流程一致,先写中断函数,然后在中断表中进行注册
这里中断函数流程是
每一次中断,当前总时刻都要加一,然后检查现在总时刻是不是已经到了第一个超时时刻,如果到了就要遍历,对所有已经超时的计时器处理,其中针对光标的处理特殊,需要向其所在的缓存写入数据,否则就需要调用task_switch进行任务切换(在之后任务部分解释)

void inthandler20(int* esp)
{
	struct TIMER* timer;
	char ts = 0;
	io_out8(PIC0_OCW2, 0x60);
	timerctl.count++;//当前走过的总时间
	//如果下一个超时时刻在总时间之后,没必要处理
	if (timerctl.next > timerctl.count) {
		return;
	}
	timer = timerctl.t0;
	for (;;) {
		//超时时刻在总时间之后,没必要处理
		if (timer->timeout > timerctl.count) {
			break;
		}
		//超时
		timer->flags = TIMER_FLAGS_ALLOC;
		if (timer != task_timer){
			fifo32_put(timer->fifo, timer->data);
		}
		else {
			ts = 1; //关于任务切换的task_timer超时时不写入缓存,而将ts设为1
		}
		timer = timer->next;
	}
	//进行移位重新排序
	timerctl.t0 = timer;
	//timerctl.next的设置
	timerctl.next = timer->timeout;
	//用于任务切换
	if (ts != 0) {
		task_switch();
	}
	return;
}
_asm_inthandler20:
		PUSH	ES
		PUSH	DS
		PUSHAD
		MOV		EAX,ESP
		PUSH	EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL	_inthandler20
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		IRETD
set_gatedesc(idt + 0x20, (int)asm_inthandler20, 2 * 8, AR_INTGATE32);

光标

算是定时器的一个小应用
实现思路是先后设置两个定时器:一个定时器设置向所属任务缓存中写0,时间到后,读取缓冲区的0,将光标颜色设一个值,然后设置向所属任务缓存中写1的定时器,时间到后,读取缓冲区的1,将光标颜色设另一个值,如此往复。

任务数据结构定义

TASKCTL主要保存着任务按层规划的结构TASKLEVEL(主要为了任务的优先级),这里分了10个层
每一层的TASKLEVEL结构主要保存这一层的任务
每一个任务由TASK数据结构定义

#define MAX_TASKS		1000	//最大任务数量
#define TASK_GDT0		3		//从GDT的几号开始分配给TSS
#define MAX_TASKS_LV	100	//每个level最多100个任务
#define MAX_TASKLEVELS	10 //最多10个level
struct TSS32 {
	int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
	int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
	int es, cs, ss, ds, fs, gs;
	int ldtr, iomap;
};
struct TASK {
	int sel, flags; //sel保存GDT号,flag=0没被使用,flag=1正在使用,可能在休眠,flag=2正在运行
	int level, priority;//level任务的层号,任务优先级也就是任务运行时间
	struct FIFO32 fifo;
	struct TSS32 tss;
	struct SEGMENT_DESCRIPTOR ldt[2];//这个主要用于操作系统保护,之后在这一部分介绍
	struct CONSOLE* cons;//任务所属的命令行的结构
	int ds_base, cons_stack;//数据段地址,栈地址
	struct FILEHANDLE* fhandle;//任务对应的文件操作结构
	int* fat;//读取文件用的fat表,在之后文件部分介绍
	char* cmdline;//保存的命令
	unsigned char langmode, langbyte1;//保存的显示中文英文结构
};
struct TASKLEVEL {
	int running; //正在运行的任务数量
	int now; //现在运行的是哪个任务
	struct TASK* tasks[MAX_TASKS_LV];
};
struct TASKCTL {
	int now_lv; //现在活动中的level
	char lv_change; //下次任务切换时是否需要改变level
	struct TASKLEVEL level[MAX_TASKLEVELS];
	struct TASK tasks0[MAX_TASKS];
};

任务初始化

//taskctl初始化
struct TASK* task_init(struct MEMMAN* memman)
{
	int i;
	struct TASK* task, * idle;
	struct SEGMENT_DESCRIPTOR* gdt = (struct SEGMENT_DESCRIPTOR*)ADR_GDT;
	taskctl = (struct TASKCTL*)memman_alloc_4k(memman, sizeof(struct TASKCTL));
	for (i = 0; i < MAX_TASKS; i++) {
		taskctl->tasks0[i].flags = 0;
		taskctl->tasks0[i].sel = (TASK_GDT0 + i) * 8;
		taskctl->tasks0[i].tss.ldtr = (TASK_GDT0 + MAX_TASKS + i) * 8;
		set_segmdesc(gdt + TASK_GDT0 + i, 103, (int)&taskctl->tasks0[i].tss, AR_TSS32);
		set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15, (int)taskctl->tasks0[i].ldt, AR_LDT);
	}
	for (i = 0; i < MAX_TASKLEVELS; i++) {
		taskctl->level[i].running = 0;
		taskctl->level[i].now = 0;
	}
	//分配一个任务,将调用这个程序的程序作为这个任务加入到任务管理数据结构中
	task = task_alloc();
	task->flags = 2; //活动中
	task->priority = 2; //优先级为2则是0.02秒切换
	task->level = 0;	//初始化任务所在level
	task_add(task);
	task_switchsub();	
	load_tr(task->sel);
	task_timer = timer_alloc();
	timer_settime(task_timer, task->priority);

	idle = task_alloc();
	idle->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
	idle->tss.eip = (int)&task_idle;
	idle->tss.es = 1 * 8;
	idle->tss.cs = 2 * 8;
	idle->tss.ss = 1 * 8;
	idle->tss.ds = 1 * 8;
	idle->tss.fs = 1 * 8;
	idle->tss.gs = 1 * 8;
	task_run(idle, MAX_TASKLEVELS - 1, 1);
	return task;
}
//初始化一个任务
struct TASK* task_alloc(void)
{
	int i;
	struct TASK* task;
	for (i = 0; i < MAX_TASKS; i++) {
		if (taskctl->tasks0[i].flags == 0) {
			task = &taskctl->tasks0[i];
			task->flags = 1; //正在使用
			task->tss.eflags = 0x00000202; /* IF = 1; */
			task->tss.eax = 0;
			task->tss.ecx = 0;
			task->tss.edx = 0;
			task->tss.ebx = 0;
			task->tss.ebp = 0;
			task->tss.esi = 0;
			task->tss.edi = 0;
			task->tss.es = 0;
			task->tss.ds = 0;
			task->tss.fs = 0;
			task->tss.gs = 0;
			task->tss.iomap = 0x40000000;
			task->tss.ss0 = 0;
			return task;
		}
	}
	return 0;
}

设定一个任务,要设定其层level和优先级(每次运行的时间)。如果level相比之前有了改变,先要将任务从该层休眠,再设定新的level,并添加到新层上,之后再将其状态flags设为2,即运行状态,并在下次任务切换时检查level状况。

//添加运行一个任务
void task_run(struct TASK* task, int level, int priority)
{
	if (level < 0) {
		level = task->level;
	}
	//如果是0并不改变其优先级
	if (priority > 0) {
		task->priority = priority;
	}
	//如果改变了任务的level
	if (task->flags == 2 && task->level != level) {
		task_remove(task);//先把任务从当前level移除(只是休眠),通过从下面的判断从休眠状态唤醒,并设置新level
	}
	//从休眠状态唤醒
	if (task->flags != 2) {
		task->level = level;
		task_add(task);
	}
	taskctl->lv_change = 1;//下次任务切换时检查level
	return;
}

任务优先级

优先任务每次运行时间长,同一优先时间的任务通过分层来决定哪一个被优先调用
主要有向层上添加任务,从层上移除任务和层的切换。当taskctl->lv_change不为0时进行层的切换,就是从第一层向之后遍历,如果这一层有正在运行的任务,就运行这一层的任务,思想也就是优先前面的层的任务。

//添加任务到level上
void task_add(struct TASK* task)
{
	struct TASKLEVEL* tl = &taskctl->level[task->level];
	tl->tasks[tl->running] = task;
	tl->running++;
	task->flags = 2;
	return;
}
//从level上移除任务(并没有移走,只是标记休眠)
void task_remove(struct TASK* task)
{
	int i;
	struct TASKLEVEL* tl = &taskctl->level[task->level];
	//找到任务所在位置
	for (i = 0; i < tl->running; i++) {
		if (tl->tasks[i] == task) {
			break;
		}
	}
	//运行的任务数减一,现在正在运行的任务的now也应减一,但后面还需要进行移动
	tl->running--;
	if (i < tl->now) {
		tl->now--;
	}
	//now值修正
	if (tl->now >= tl->running) {
		tl->now = 0;
	}
	task->flags = 1;//标记位不活动,但是在使用
	//移位操作
	for (; i < tl->running; i++) {
		tl->tasks[i] = tl->tasks[i + 1];
	}
	return;
}
//level层的切换
void task_switchsub(void)
{
	int i;
	for (i = 0; i < MAX_TASKLEVELS; i++) {
		if (taskctl->level[i].running > 0) {
			break;
		}
	}
	taskctl->now_lv = i;
	taskctl->lv_change = 0;
	return;
}

任务切换

主要就是一遍遍循环遍历当前这一层的所有任务,对遍历到的任务设定计时器,然后调用farmap去运行任务。其中如果taskctl->lv_change不为0,就改变层,在新的层上不断循环遍历。

//负责任务的切换
void task_switch(void)
{
	struct TASKLEVEL* tl = &taskctl->level[taskctl->now_lv];
	struct TASK* new_task, * now_task = tl->tasks[tl->now];
	tl->now++;
	//如果下一个任务与总运行的任务数量相同,表示所有任务循环过一遍
	if (tl->now == tl->running) {
		tl->now = 0;
	}
	//如果lv_change不为0则需要层的切换
	if (taskctl->lv_change != 0) {
		task_switchsub();
		tl = &taskctl->level[taskctl->now_lv];
	}
	new_task = tl->tasks[tl->now];
	timer_settime(task_timer, new_task->priority);
	if (new_task != now_task) {
		farjmp(0, new_task->sel);
	}
	return;
}

任务休眠

当进程没有操作时,可以让其休眠,通过标记其暂时不需要运行
当内存有数据输入时,再唤醒,将其加入到运行队列中

//任务睡眠
void task_sleep(struct TASK* task)
{
	struct TASK* now_task;
	if (task->flags == 2) {
		now_task = task_now();
		task_remove(task); //使其休眠
		//如果休眠的是自己则进行任务切换
		if (task == now_task) {
			task_switchsub();
			now_task = task_now();
			farjmp(0, now_task->sel);
		}
	}
	return;
}

文件系统

书中一开始用到的就是FAT文件系统结构,在img的特定偏移保存着FAT表,可以知道文件数据保存在哪几个扇区中,FAT表中保存文件每一个项都指向了保存文件的下一项,是一个链式结构。所以可以通过FAT表知道文件保存在哪里。另外在特定偏移保存着文件系统中保存的所有文件,每个文件的内容是有32个字节

struct FILEINFO {
    unsigned char name[8], ext[3], type;
    char reserve[10];
    unsigned short time, date, clustno;
    unsigned int size;
};

开始的8个字节是文件名。文件名不足8个字节时,后面用空格补足,所有的文件名都是大写的。
如果文件名的第一个字节为0xe5,代表这个文件已经被删除了。从磁盘映像img的0x004200就开始存放文件haribote.sys了,因此文件信息最多可以存放224个。接下来3个字节是扩展名,也全部使用了大写字母。后面1个字节存放文件的属性信息:

0x01……只读文件(不可写入)
0x02……隐藏文件
0x04……系统文件
0x08……非文件信息(比如磁盘名称等)
0x10……目录

接下来的10个字节为保留。下面2个字节为WORD整数,存放文件的时间。再下面2个字节存放文件的日期。接下来的2个字节也是WORD整数,代表这个文件的内容从磁盘上的哪个扇区开始存放。最后的4个字节为DWORD整数,存放文件的大小。
这本书中对文件的操作主要有
读取fat表:void file_readfat(int* fat, unsigned char* img)
读取文件内容:void file_loadfile(int clustno, int size, char* buf, int* fat, char* img)
查找磁盘img中是否有该文件:struct FILEINFO* file_search(char* name, struct FILEINFO* finfo, int max)

系统命令

在命令行窗口输入系统提供的命令,对操作系统进行管理

命令的读取

与之前介绍的相同,系统命令任务也是通过自己的缓冲区接受键盘鼠标的数据。当输入普通字符时不断记录这些字符,最后接收到回车时,调用执行命令的函数cons_runcmd根据已经记录的数据执行对应的命令

void cons_runcmd(char* cmdline, struct CONSOLE* cons, int* fat, int memtotal)
{
	if (strcmp(cmdline, "mem") == 0 && cons->sht != 0) {
		cmd_mem(cons, memtotal);
	}
	else if (strcmp(cmdline, "cls") == 0 && cons->sht != 0) {
		cmd_cls(cons);
	}
	else if (strcmp(cmdline, "dir") == 0 && cons->sht != 0) {
		cmd_dir(cons);
	}
	else if (strncmp(cmdline, "type ", 5) == 0 && cons->sht != 0) {
		cmd_type(cons, fat, cmdline);
	}
	else if (strcmp(cmdline, "exit") == 0) {
		cmd_exit(cons, fat);
	}
	else if (strncmp(cmdline, "start ", 6) == 0) {
		cmd_start(cons, cmdline, memtotal);
	}
	else if (strncmp(cmdline, "ncst ", 5) == 0) {
		cmd_ncst(cons, cmdline, memtotal);
	}
	else if (strncmp(cmdline, "langmode ", 9) == 0) {
		cmd_langmode(cons, cmdline);
	}
	else if (cmdline[0] != 0) {
		//如果找到就执行程序
		if (cmd_app(cons, fat, cmdline) == 0) {
			cons_putstr0(cons, "Bad command.\n\n");
		}
	}
	return;
}

mem

显示内存使用状况

void cmd_mem(struct CONSOLE* cons, int memtotal)
{
	struct MEMMAN* memman = (struct MEMMAN*)MEMMAN_ADDR;
	char s[60];
	sprintf(s, "total   %dMB\nfree %dKB\n\n", memtotal / (1024 * 1024), memman_total(memman) / 1024);
	cons_putstr0(cons, s);
	return;
}

cls

清屏,原理是将屏幕图层的数据全部填充成命令行背景使用的颜色

void cmd_cls(struct CONSOLE* cons)
{
	int x, y;
	struct SHEET* sheet = cons->sht;
	for (y = 28; y < 28 + 128; y++) {
		for (x = 8; x < 8 + 240; x++) {
			sheet->buf[x + y * sheet->bxsize] = COL8_000000;
		}
	}
	sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128);
	cons->cur_y = 28;
	return;
}

dir

读取磁盘内所有文件名字
原理是读取FAT目录所在位置,将其中的文件名显示出来

void cmd_dir(struct CONSOLE* cons)
{
	struct FILEINFO* finfo = (struct FILEINFO*)(ADR_DISKIMG + 0x002600);
	int i, j;
	char s[30];
	for (i = 0; i < 224; i++) {
		if (finfo[i].name[0] == 0x00) {
			break;
		}
		if (finfo[i].name[0] != 0xe5) {
			if ((finfo[i].type & 0x18) == 0) {
				sprintf(s, "filename.ext   %7d", finfo[i].size);
				//这里的文件名只设置最大为8
				for (j = 0; j < 8; j++) {
					s[j] = finfo[i].name[j];
				}
				s[9] = finfo[i].ext[0];
				s[10] = finfo[i].ext[1];
				s[11] = finfo[i].ext[2];
				cons_putstr0(cons, s);
			}
		}
	}
	cons_newline(cons);
	return;
}

type

读取文件内容
通过扇区偏移找出文件所在位置并读取

void cmd_type(struct CONSOLE* cons, int* fat, char* cmdline)
{
	struct MEMMAN* memman = (struct MEMMAN*)MEMMAN_ADDR;
	struct FILEINFO* finfo = file_search(cmdline + 5, (struct FILEINFO*)(ADR_DISKIMG + 0x002600), 224);
	char* p;
	if (finfo != 0) {
		p = (char*)memman_alloc_4k(memman, finfo->size);//文件内容所在地址
		file_loadfile(finfo->clustno, finfo->size, p, fat, (char*)(ADR_DISKIMG + 0x003e00));//文件内容导入到该地址
		//读取文件内容
		cons_putstr1(cons, p, finfo->size);
		memman_free_4k(memman, (int)p, finfo->size);
	}
	else {
		cons_putstr0(cons, "File not found.\n");
	}
	cons_newline(cons);
	return;
}

exit

退出命令(也是任务结束的函数)
其中结束自己不能自己结束自己,通过向操作系统主程序发送数据,让它来结束
其中有两种不同的数据发送,第一种是该命令行程序有自己的图层,发送自己图层的地址+768的数据。第二种该命令行没有自己的图层(它可能是启动了一个应用,但没有自己的图层),就发送自己TASK结构的地址+1024的数据

void cmd_exit(struct CONSOLE* cons, int* fat)
{
	struct MEMMAN* memman = (struct MEMMAN*)MEMMAN_ADDR;
	struct TASK* task = task_now();
	struct SHTCTL* shtctl = (struct SHTCTL*)*((int*)0x0fe4);
	struct FIFO32* fifo = (struct FIFO32*)*((int*)0x0fec);
	timer_cancel(cons->timer);
	memman_free_4k(memman, (int)fat, 4 * 2880);
	io_cli();
	//发送数据让主程序关闭自己
	if (cons->sht != 0) {
		fifo32_put(fifo, cons->sht - shtctl->sheets0 + 768);
	}
	else {
		fifo32_put(fifo, task -> taskctl->tasks0 + 1024);
	}
	io_sti();
	for (;;) {
		task_sleep(task);
	}
}

start

打开一个新窗口并运行后面跟的应用程序
原理是:先打开应个命令行窗口,然后将保存的命令复制过去,再传送过去回车键,这相当于在这个命令行窗口执行了这个命令。

void cmd_start(struct CONSOLE* cons, char* cmdline, int memtotal)
{
	struct SHTCTL* shtctl = (struct SHTCTL*)*((int*)0x0fe4);
	struct SHEET* sht = open_console(shtctl, memtotal);
	struct FIFO32* fifo = &sht->task->fifo;
	int i;
	sheet_slide(sht, 32, 4);
	sheet_updown(sht, shtctl->top);
	
	for (i = 6; cmdline[i] != 0; i++) {
		fifo32_put(fifo, cmdline[i] + 256);
	}
	fifo32_put(fifo, 10 + 256);	/* Enter */
	cons_newline(cons);
	return;
}

ncst

不打开一个新窗口,但运行后面跟的应用程序
这个就是在exit关闭时没有图层的情况

void cmd_ncst(struct CONSOLE* cons, char* cmdline, int memtotal)
{
	struct TASK* task = open_constask(0, memtotal);
	struct FIFO32* fifo = &task->fifo;
	int i;

	for (i = 5; cmdline[i] != 0; i++) {
		fifo32_put(fifo, cmdline[i] + 256);
	}
	fifo32_put(fifo, 10 + 256);	/* Enter */
	cons_newline(cons);
	return;
}

langmode

中英文显示切换命令

void cmd_langmode(struct CONSOLE* cons, char* cmdline)
{
	struct TASK* task = task_now();
	unsigned char mode = cmdline[9] - '0';
	if (mode <= 1) {
		task->langmode = mode;
	}
	else {
		cons_putstr0(cons, "mode number error.\n");
	}
	cons_newline(cons);
	return;
}

系统调用API

操作系统提供接口,应用程序可以调用来完成某种功能,这个接口其实就是函数
但是随着系统代码的修改,这个函数的地址会一直发生变化,在书中的解决办法是将这个函数注册为一个中断处理函数。但是不能一个函数就使用一个中断号,这样中断号不够用,在书中只使用了0x40这个中断号,然后通过传入不同的值(即设置不同的中断的功能号)调用不同的函数
同样是编写中断函数int* hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)(具体代码在之后解释)和下面的汇编代码

_asm_hrb_api:
		STI		;虽然通过INT调用会禁止中断,但这里作为普通函数不想禁止中断
		PUSH	DS
		PUSH	ES
		PUSHAD	;之后调用函数会导致寄存器值改变,先全部保存
		PUSHAD	;将调用函数所需要的参数全部压入栈
		MOV		AX,SS
		MOV		DS,AX		;将操作系统用的段地址存入ds和es
		MOV		ES,AX
		CALL	_hrb_api
		CMP		EAX,0		; EAX不为0的时候程序结束
		JNE		_asm_end_app
		ADD		ESP,32
		POPAD			;寄存器值还原
		POP		ES
		POP		DS
		;RETF				;far call调用需要用RETF不能简单的用RET
		IRETD			;中断处理对应的返回

然后在中断表里注册,但是在区分操作系统段和应用程序段的时候(之后在操作系统安全部分解释),应用程序试图调用未经操作系统授权的中断时,CPU会产生异常,所以需要在IDT中将INT 0x40设置为可供应用程序作为API调用的中断,所以在段注册时加了0x60这个属性

set_gatedesc(idt + 0x40, (int)asm_hrb_api, 2 * 8, AR_INTGATE32 + 0x60);

下面代码中的edx保存的就是传来的功能号,通过功能号判断,调用不同的函数

int* hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
	int i;
	struct TASK* task = task_now();
	struct CONSOLE* cons = task->cons;
	struct SHTCTL* shtctl = (struct SHTCTL*)*((int*)0x0fe4);
	int ds_base = task->ds_base;
	struct SHEET* sht;
	struct FIFO32* sys_fifo = (struct FIFO32*)*((int*)0x0fec);
	int* reg = &eax + 1;	/* eax后面的地址 */
		//强行修改通过PUSHAD保存的值
		/* reg[0] : EDI,   reg[1] : ESI,   reg[2] : EBP,   reg[3] : ESP */
		/* reg[4] : EBX,   reg[5] : EDX,   reg[6] : ECX,   reg[7] : EAX */
	struct FILEINFO* finfo;
	struct FILEHANDLE* fh;
	struct MEMMAN* memman = (struct MEMMAN*)MEMMAN_ADDR;

	if(edx==1)
	{
		//省略,下面分开解释
	}
	else if (……)
	{
		//……
	}
}

显示单个字符的API

其中2号和3号都是打印字符串,1号是打印字符,cons_putstr0和cons_putstr1也是调用的cons_putchar,cons_putchar的功能就是将字符打印在命令行窗口上

if (edx == 1) {
		cons_putchar(cons, eax & 0xff, 1);
	}
else if (edx == 2) {
		cons_putstr0(cons, (char*)ebx + ds_base);
	}
else if (edx == 3) {
		cons_putstr1(cons, (char*)ebx + ds_base, ecx);
	}

如果将API封装为可供c调用的函数,那么就可以用C语言编写能够使这个操作系统运行的应用程序了,比如下面代码就是传入功能号edx=1,传入参数,然后调用中断

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api001.nas"]

		GLOBAL	_api_putchar

[SECTION .text]

_api_putchar:	; void api_putchar(int c);
		MOV		EDX,1
		MOV		AL,[ESP+4]		; c
		INT		0x40
		RET

那么我们就能使用这个API,在下面代码中声明上面汇编定义的void api_putchar(int c)这个函数,其中api_end()也是一个API函数,用来结束程序。
编写好这样一个调用系统API的函数,并编译连接后,比如生成的文件名就叫a.hrb(书中的规定,当然其它也可,类似于windows上x.exe),就可以保存在磁盘img中(相当于我们平常向磁盘安装程序)

void api_putchar(int c);
void api_end(void);

void HariMain(void)
{
	api_putchar('A');
	api_end();
}

然后在系统中就可以直接运行这个程序
img

操作系统中应用程序如何执行

依然通过file_search在磁盘中寻找要执行的程序的位置,如果找到了就读取文件的内容。在这本书中可执行程序的格式是作者自定义的0x0000存放数据段的大小,0x0004存放Hari的标记,判断是不是可执行文件,就是看这个文件是不是大于36子节,并且)0x0004处为Hari的标记,0x000c存放应用程序启动时esp寄存器的初始值,0x0010存放向数据段传送的字节数,0心4存放向数据段传送部分在文件中的起始起止等等。
之后注册两个段,一个代码段,一个数据段,调用start_app执行程序

//执行文件程序
int cmd_app(struct CONSOLE* cons, int* fat, char* cmdline)
{
	struct MEMMAN* memman = (struct MEMMAN*)MEMMAN_ADDR;
	struct FILEINFO* finfo;
	char name[18], * p, * q;
	struct TASK* task = task_now();
	int i, segsiz, datsiz, esp, dathrb, appsiz;
	struct SHTCTL* shtctl;
	struct SHEET* sht;
	//得到程序名字
	for (i = 0; i < 13; i++) {
		if (cmdline[i] <= ' ') {
			break;
		}
		name[i] = cmdline[i];
	}
	name[i] = 0;

	//找程序文件
	finfo = file_search(name, (struct FILEINFO*)(ADR_DISKIMG + 0x002600), 224);
	if (finfo == 0 && name[i - 1] != '.') {
		//如果文件名找不到,就加上后缀再找
		name[i] = '.';
		name[i + 1] = 'H';
		name[i + 2] = 'R';
		name[i + 3] = 'B';
		name[i + 4] = 0;
		finfo = file_search(name, (struct FILEINFO*)(ADR_DISKIMG + 0x002600), 224);
	}
	if (finfo != 0) {
		//找到了执行,返回1
		appsiz = finfo->size;
		p = file_loadfile2(finfo->clustno, &appsiz, fat);
		if (appsiz >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {//为了确认是不是hrb
			segsiz = *((int*)(p + 0x0000));
			esp = *((int*)(p + 0x000c));
			datsiz = *((int*)(p + 0x0010));
			dathrb = *((int*)(p + 0x0014));
			q = (char*)memman_alloc_4k(memman, segsiz);
			task->ds_base = (int)q;
			set_segmdesc(task->ldt + 0, finfo->size - 1, (int)p, AR_CODE32_ER + 0x60);
			set_segmdesc(task->ldt + 1, segsiz - 1, (int)q, AR_DATA32_RW + 0x60);
			for (i = 0; i < datsiz; i++) {
				q[esp + i] = p[dathrb + i];
			}
			start_app(0x1b, 0 * 8 + 4, esp, 1 * 8 + 4, &(task->tss.esp0));//加四表示LDT段号

			//关闭窗口
			shtctl = (struct SHTCTL*)*((int*)0x0fe4);
			for (i = 0; i < MAX_SHEETS; i++) {
				sht = &(shtctl->sheets0[i]);
				if ((sht->flags & 0x11) == 0x11 && sht->task == task) {
					sheet_free(sht);
				}
			}
			//将未关闭的文件关闭
			for (i = 0; i < 8; i++) {
				if (task->fhandle[i].buf != 0) {
					memman_free_4k(memman, (int)task->fhandle[i].buf, task->fhandle[i].size);
					task->fhandle[i].buf = 0;
				}
			}
			timer_cancelall(&task->fifo);
			memman_free_4k(memman, (int)q, segsiz);
			task->langbyte1 = 0;
		}
		else {
			cons_putstr0(cons, ".hrb file format error.\n");
		}
		memman_free_4k(memman, (int)p, appsiz);
		cons_newline(cons);
		return 1;
	}
	//找不到返回0
	return 0;
}

start_app执行程序时书中是先将要低调用的程序的地址存入栈中,再调用RETF指令来执行程序
程序结束是通过asm_end_app返回调用它的函数cmd_app
因为上面调用函数的方法特别,所以程序执行后无法用RETF的方式结合并返回,所以asm_end_app直接传入调用程序的栈的地址,来强行返回
返回后如果有窗口,都要关闭,并且要通过timer_cancelall将程序使用的定时器都取消,防止程序结束了,定时器还在运行

_start_app:		; void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
		PUSHAD		;将所有寄存器的值保存下来
		MOV		EAX,[ESP+36]	; 应用程序EIP
		MOV		ECX,[ESP+40]	; 应用程序CS
		MOV		EDX,[ESP+44]	; 应用程序ESP
		MOV		EBX,[ESP+48]	; 应用程序DS/SS
		MOV		EBP,[ESP+52]	; tss.esp0的地址
		MOV		[EBP  ],ESP		; 保存操作系统用ESP
		MOV		[EBP+4],SS		; 保存操作系统SS
		MOV		ES,BX
		MOV		DS,BX
		MOV		FS,BX
		MOV		GS,BX
;	调整栈避免RETF跳转到应用程序
		OR		ECX,3			; RETF调用应用程序的设置
		OR		EBX,3			
		PUSH	EBX				; 应用程序SS
		PUSH	EDX				; 应用程序ESP
		PUSH	ECX				; 应用程序CS
		PUSH	EAX				; 应用程序EIP
		RETF
;	应用程序结束不会回到这里

_asm_end_app:
;	EAX为tss.esp0地址
		MOV		ESP,[EAX]
		MOV		DWORD [EAX+4],0
		POPAD
		RET					; 返回cmd_app

结束程序API

和上面程序结束asm_end_app是一样的,因为执行程序时书中是先将要低调用的程序的地址存入栈中,再调用RETF指令来执行程序,所以程序执行后无法用RETF的方式结合并返回,所以直接传入调用程序的栈的地址,来强行返回。
这个API也是这个思路

else if (edx == 4) {
		return &(task->tss.esp0);
}

其它API

API的功能就是基本是将以前实现的功能封装一下,封装过程也和上述一样,所以不详细写了
功能号5,6,7的为显示窗口等画界面的API
功能号8,9,10的与内存分配有关,分别为初始化内存,分配内存,释放内存。关于分配内存:应用程序只能对操作系统为它准备好的数据段的内存进行分配。现在的操作系统会在malloc时根据分配的大小调整应用程序的段的大小。但是书中的系统只是在编写应用程序的时候指定出来需要多大的内存,指定malloc的大小后和栈的大小累加写入.hrb开头的四个字节(这个在前面程序的格式中提到过,0x0000处是数据段大小)
功能号11-14为画点,画线等界面功能的API
功能号15为接收键盘数据的API,当收到数据后放入reg[7]然后返回,也就是通过这个API返回接收到的键盘的数据
功能号16-19为定时器相关的API
功能号20为蜂鸣器API
功能号21-25为打开,读取文件等相关API
功能号26为获取命令行内容API,方法就是将命令行task结构中保存的命令行数据放到```reg[7]``即EAX,然后返回
功能号27为设定中英文

在使用这些API时要将这些API与编写的程序连接在一起,才能生成最终运行的程序。所以将将他们统一打包成一个库文件,书中使用了工具将他们全部打包成apilib.lib库,然后编写一个头文件apilib.h,声明这些函数。之后要调用这些API,只需要在我们的源代码中加入```#inlcude "apilib.h"即可,不用向显示字符里示例的一样要在前面声明要使用的函数了。

操作系统保护

区分操作系统与应用程序

为了阻止应用程序擅自访问了操作系统的内存空间,解决方法:创建应用程序专用的数据段,并在在段定义的地方的访问权限加入0x60,就可将段设为应用程序用,那么当存入操作系统用的段地址时就会产生异常
另外也要保护应用程序不被别的应用程序破坏,解决方法:通过设置LDT局部描述表,LDT和tss一样都是一种段
所以对这些段的规划如下

1 操作系统数据段
2 操作系统代码段
3-1002 tss使用的段
1003- 应用程序代码段和数据段

无论哪种类型段,设置方法都一样,其中tss用的段和应用程序用的段都已经在taskctl初始化函数中初始化过

taskctl->tasks0[i].tss.ldtr = (TASK_GDT0 + MAX_TASKS + i) * 8;//ldt段,将段的地址保存在tss.ldtr寄存器中,这样CPU知道哪个任务使用哪个段
set_segmdesc(gdt + TASK_GDT0 + i, 103, (int)&taskctl->tasks0[i].tss, AR_TSS32);//这个是初始化tss段
set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15, (int)taskctl->tasks0[i].ldt, AR_LDT);//初始化ldt段

在cmd_app中将代码段和数据段出案件在LDT中

set_segmdesc(task->ldt + 0, finfo->size - 1, (int)p, AR_CODE32_ER + 0x60);
set_segmdesc(task->ldt + 1, segsiz - 1, (int)q, AR_DATA32_RW + 0x60);

不像其它类型的段在调用时需要段号*8来指定,LDT段要段号*8+4

start_app(0x1b, 0 * 8 + 4, esp, 1 * 8 + 4, &(task->tss.esp0));//加四表示LDT段号

对异常的支持

当应用程序产生异常时,会产生0x0d中断,当以应用程序模式运行时,执行IN和OUT,CLI,STI,HLT指令也会产生一般保护异常0x0d
可以在这个中断实现函数,强制结束程序

_asm_inthandler0d:
		STI
		PUSH	ES
		PUSH	DS
		PUSHAD
		MOV		EAX,ESP
		PUSH	EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL	_inthandler0d
		CMP		EAX,0		
		JNE		_asm_end_app		
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		ADD		ESP,4			; INT 0x0d需要这样
		IRETD
int* inthandler0d(int* esp)
{
	
	struct TASK* task = task_now();
	struct CONSOLE* cons = task->cons;
	char s[30];
	cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
	sprintf(s, "EIP = %08X\n", esp[11]);
	cons_putstr0(cons, s);
	return &(task->tss.esp0);
}

IDT注册

	set_gatedesc(idt + 0x0d, (int)asm_inthandler0d, 2 * 8, AR_INTGATE32);

栈异常

栈异常产生的中断号是0c,中断的实现方式和之前一样,中断函数的功能这里主要就是结束程序

_asm_inthandler0c:
		STI
		PUSH	ES
		PUSH	DS
		PUSHAD
		MOV		EAX,ESP
		PUSH	EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL	_inthandler0c
		CMP		EAX,0
		JNE		_asm_end_app
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		ADD		ESP,4			
		IRETD
int* inthandler0c(int* esp)
{
	struct TASK* task = task_now();
	struct CONSOLE* cons = task->cons;
	char s[30];
	cons_putstr0(cons, "\nINT 0C :\n Stack Exception.\n");
	sprintf(s, "EIP = %08X\n", esp[11]);
	cons_putstr0(cons, s);
	return &(task->tss.esp0);
}

代码中```esp[11]``为esp中11号元素,它的值就是寄存器EIP的值,其它寄存器的值也可以像这样的得到,如下所示

esp[ 0] : EDI
esp[ 1] : ESI        esp[0~7]为_asm_inthandler中PUSHAD的结果
esp[ 2] : EBP
esp[ 4] : EBX
esp[ 5] : EDX
esp[ 6] : ECX
esp[ 7] : EAX
esp[ 8] : DS         esp[8~9]为_asm_inthandler中PUSH的结果
esp[ 9] : ES
esp[10] : 错误编号(基本上是0,显示出来也没什么意思)
esp[11] : EIP
esp[12] : CS         esp[10~15]为异常产生时CPU自动PUSH的结果
esp[13] : EFLAGS
esp[14] : ESP (应用程序用ESP)
esp[15] : SS  (应用程序用SS)

IDT注册

	set_gatedesc(idt + 0x0c, (int)asm_inthandler0c, 2 * 8, AR_INTGATE32);

alloca

C语言编译器规定,如果栈中的变量超过4KB,则需要调用__alloca这个函数。这个函数的主要功能是根据操作系统的规格来获取栈中的空间。在Windows和Linux中,如果不调用这个函数,而是仅对ESP进行减法运算的话,貌似无法成功获取内存空间(小于4KB时只要对ESP进行减法运算即可)。在这本书的系统中,对于栈的管理并没有什么特殊的设计,因此也用不着去调用__alloca函数,可C语言的编译器会擅自去调用那个函数。为了解决这个问题,作者自己写了一个__alloca函数,只对ESP进行减法运算,而不做其他任何多余的操作,放在apilib中用于之后程序编译。

应用程序

书中除了使用上面的API写了很多小程序,还实现了图片浏览器,文本浏览器等程序。但是这些和操作系统本身不太相关,最后就没细看。

最后书中给出了制作ISO文件的方法,因为书中都是通过qemu这个运行的,所以我也尝试了一下,使用vmware打开,还是比较激动的
img

posted @ 2022-08-20 10:56  启林O_o  阅读(223)  评论(0编辑  收藏  举报