【读书笔记-《30天自制操作系统》-4】Day5

今天的内容继续以显示为主,将实现文字的显示并描绘鼠标箭头,以及为了之后实现鼠标的移动,初步引入GDT与IDT。
在这里插入图片描述

1. 结构体的引入

同属于一类的变量,在处理时一个一个传入函数太过于麻烦,如果能打个包作为一个参数传入,在使用时再分别引用,代码会简洁不少。而这个打成的包就是结构体。

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;

	init_screen(vram, xsize, ysize);

	for (;;) {
		io_hlt();
	}
}

这里将前面通过BIOS获取并已经保存在内存里的一些信息打包成了结构体。从前面的内容(【读书笔记-《30天自制操作系统》-2】Day3)可以看到,从0x0ff0开始恰好依次存放了CYLS,LEDS等信息,通过结构体的成员就可以直接访问。
这里binfo是BOOTINFO类型的指针,访问成员时需要通过(*binfo).vram的形式。而获取结构体指针中成员变量的值,更简洁的写法可以使用"->"符号:

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;

	init_palette();
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);

	for (;;) {
		io_hlt();
	}
}

而对于一个结构体变量,如struct BOOTINFO binfo,要获取其成员变量的值,只要直接使用"."运算符就可以了。

binfo.vram, binfo.scrnx, binfo.scrny

2. 从显示字符到显示变量

2.1 单个字符的显示

接下来实现文字的显示,先从单个字符的显示开始。思路是这样:在8x16的矩形内,用0表示不填充颜色,1表示填充颜色,用填充1的区域来构成一个字符,如下图所示:
在这里插入图片描述
根据以上的填充方式,每一行可以用一个十六进制数来表示,定义了字符“A”:

	static char font_A[16] = {
		0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
		0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
	};

而用下面这个函数来将“A”显示出来:

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

可以看出函数putfont8中,每执行一次for循环体,其实是检查这一行需要填充的颜色。将整个8x16方格中需要填充颜色的部分填充之后,字符A也就显示出来了。

理解了显示字符的原理之后,其实可以使用定义好的字符库,比如作者在代码中提供的可以自由使用的OSASK字体。这套字体实现了ASCII字符的编码,共256个字符,每个字符需要16个字节,因此共4096字节。在C语言中可以这样引用:

extern char hankaku[4096]

在ASCII编码中,"A"的字符编码是0x41,要使用字体库中的A字符可以写成

hankaku + 0x41*16

或者直接写成

hankaku + 'A'*16

这样实现单个字符显示的代码如下:

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
	extern char hankaku[4096];

	init_palette();
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	putfont8(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, hankaku + 'A' * 16);
	putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'B' * 16);
	putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'C' * 16);
	putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + '1' * 16);
	putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + '2' * 16);
	putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + '3' * 16);

	for (;;) {
		io_hlt();
	}
}

执行的结果如下,分别显示了字符ABC与123。
在这里插入图片描述
2.2 字符串的显示

完成了单个字符的显示,也就可以顺理成章地实现字符串的显示了。C语言中的字符串其实是以’\0’结尾的char型数组,而‘\0’的字符编码是0x00,于是有下面的字符串显示程序:

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

这个函数的入参是一个char*指针,for循环中其实是把"\0"前的字符一个一个显示出来,原理上还是很简单的。这样在程序中就可以直接显示一个完整的句子:

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;

	init_palette();
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	putfonts8_asc(binfo->vram, binfo->scrnx,  8,  8, COL8_FFFFFF, "ABC 123");
	putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS.");
	putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");

	for (;;) {
		io_hlt();
	}
}

最后的两条putfonts8_asc语句是为了展示出不同的文字效果:
在这里插入图片描述
这样显示出的文字带一点阴影,更有立体感。看来作者对于美化显示效果也颇费了一番心思。

2.3 变量的显示

接下来再显示变量。这里需要用到sprintf。为什么C语言中的prinf不能使用呢?因为printf用于输出,而输出字符串的方式,各个操作系统有所不同。sprintf只对内存进行操作,因此可以应用于所有的操作系统。新增代码如下:

	sprintf(s, "scrnx = %d", binfo->scrnx);
	putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);

sprintf的使用方法:sprintf(地址, 格式,值,值,值,……),其中%d代表十进制数。运行的结果如图:

在这里插入图片描述
同样的方式,鼠标箭头也可以描绘出来:

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

与显示单个字符稍有不同的是这里使用的是16x16的方框。其中"bc"指的是背景色。16x16的方框内除了显示鼠标箭头的区域,还需要将其他部分填充为背景色。为了实现背景色的填充,需要增加下面这个函数:

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

运行以上两个函数:

	init_mouse_cursor8(mcursor, COL8_008484);
	putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
	sprintf(s, "(%d, %d)", mx, my);
	putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);

显示的效果如下:
在这里插入图片描述
3. GDT与IDT初步

鼠标箭头是有了,但要想让它真正动起来,需要的工作还不少,不是简单一篇的内容能够说完的。本篇里先引入GDT与IDT的概念,为后续实现鼠标箭头的移动打基础。

3.1 GDT
GDT是什么,又为什么要引入这个概念呢?
这还要从内存分段管理来说起。对于多进程的操作系统,可以同时运行多个程序。为了防止各个程序使用的内存范围发生重叠,分段是一种解决方法。每个程序都在分给自己的一段内存上运行,并且将这一段内存的起始地址看作0来进行处理,这样编写程序也更为方便,分割成的段被成为segment。
通过分段的方式管理内存,定位一个内存地址仍然使用的是起始地址+偏移的形式。执行

MOV AL, [DS:EBX]

指令时,这里的起始地址使用的是DS所表示的段的起始地址,EBX表示的则是在段内的偏移。

为了表示一个段,需要有以下信息:

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

CPU用8个字节,64位来存放这样一个段的信息,但段寄存器仍然是那个16位的寄存器,无法存放这么多信息。这时候,就需要参考上一篇【读书笔记-《30天自制操作系统》-3】Day4中调色板的使用方法。设定一个段号,存放在段寄存器里,并且预先设置号段与段号的对应关系。
8位的调色板模式下能够使用的色号是0-255,作为16位的寄存器,段寄存器理论上能够存储的段号应该是0-65535。但实际上由于CPU设计的限制,段寄存器的低3位不能用来表示段号,因此实际能够存储的段号只有0-8191,共8192个段。8192个段,所需要的空间是8192*8=65536字节,也就是64KB。这64KB的数据也就是GDT:global (segment) descriptor table,意思是全局段号记录表。CPU无力存储这么大的数据,因此这64KB的数据仍然是存放在内存里。将这段内存的起始地址和有效设定个数存放在CPU的GDTR寄存器中,GDT的设定就完成了。

3.2 IDT
再来简单说说IDT。IDT是interrupt descriptor table的缩写,也就是中断记录表的意思。
所谓的中断,是为了使CPU能够及时对外部设备的动作做出响应,又不影响CPU正常工作处理的一种机制。CPU的运行速度极快,对于主频100MHz的CPU,每秒钟可以执行100000000条指令。对于鼠标这样的设备,并不确定用户在什么时间会触发鼠标的动作。采用定时查询的机制,时间间隔短了,会占用CPU大量的时间用于查询,无法完成正常的工作;时间间隔长了,用户的响应又不及时。中断机制就是用来解决这一问题的。
在CPU正常工作状态下,如果产生了中断,这时CPU就会停下当前处理的工作并将当前的工作状态保存,处理中断。中断处理结束后,CPU重新获取中断前的工作状态,继续工作。所以使用鼠标时,需要使用中断,也就需要设置IDT。
IDT中记录的是中断号码与调用函数之间的关系,这样产生了某一号码的中断,CPU就可以执行相应的调用函数,完成中断处理。而为了设置IDT,必须先设置好GDT,这也是引入GDT的原因。
关于GDT与IDT的设置,代码如下:

struct SEGMENT_DESCRIPTOR {
	short limit_low, base_low;
	char base_mid, access_right;
	char limit_high, base_high;
};

struct GATE_DESCRIPTOR {
	short offset_low, selector;
	char dw_count, access_right;
	short offset_high;
};

void init_gdtidt(void)
{
	struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;
	struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) 0x0026f800;
	int i;

	/* GDT初始化 */
	for (i = 0; i < 8192; i++) {
		set_segmdesc(gdt + i, 0, 0, 0);
	}
	set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
	set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
	load_gdtr(0xffff, 0x00270000);

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

	return;
}

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

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

结构体SEGMENT_DESCRIPTOR 用8个字节来存放段信息。从代码中可以看出,GDT地址从0x00270000开始(这是作者选取未被使用的内存随意设定的)。
set_segmdesc的入参包括段信息SEGMENT_DESCRIPTOR *sd,段地址上限limit,段基址base以及段属性ar。
以下语句

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

设置了两个段。段1的基址为0,上限为4G,是所能管理的全部内存;段2则是从0x00280000地址开hi的512KB内存。至于属性0x4092和0x409a所代表的含义,仍然要留待下一篇来讲解了。
完成两个段的设置之后,再通过

load_gdtr(0xffff, 0x00270000);

设置好GDTR寄存器,GDT的设置就基本完成了。

IDT的设置与GDT的设置类似,当前的代码中只是将IDT整体都初始化为0。

下一篇中,会对中断与IDT的设置进行更为详细的讲解,敬请期待。

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值