第5天:结构体、文字显示与GDT/IDT初始化

本文介绍了如何在自制操作系统中使用C语言实现图形界面的基础功能,包括初始化内存地址、使用结构体、显示字符和字符串、显示变量值以及初始化GDT和IDT。通过结构体优化内存地址的访问,设计函数显示字符和字符串,并展示了如何显示鼠标。同时,详细阐述了GDT和IDT的作用以及初始化过程,为实现鼠标动态功能奠定基础。
摘要由CSDN通过智能技术生成

回顾:经过第四天,已经可以画出一个桌面了,现在在这个基础上添一些东西吧。

5.1、接收启动信息

还记得在第三天吗,在准备进入32位时,准备了很多内存地址,为以后使用,现在就是时候了,在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;
	
	init_screen(vram, xsize, ysize);

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

这些地址是不是很熟悉?并且,把第四天绘制背景画面的一串函数调用整合成一个函数init_screen

5.2、试用结构体

这个部分主要介绍C语言的结构使用了,很简单。
对于5.1中使用内存地址也可以,但是不够优雅,可以把他们整合成一个结构体。关于结构体的讨论有下面两种,主要是使用方式。

5.2.1、点“.”方式使用结构体成员

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;

	init_screen(vram, xsize, ysize);

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

这里定义了结构体BOOTINFO,并定义了该结构体的指针binfo,通过点的方式调用成员。

5.2.2、箭头“->”方式调用结构体成员

struct BOOTINFO {
	char cyls, leds, vmode, reserve;
	short scrnx, scrny;
	char *vram;
};

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

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

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

这里采用箭头的方式调用,看着更优雅了。

5.3、显示字符

之前显示字符串都是采用BIOS,现在使用32位,C语言,所以显示字符串就要自己来实现了。

5.3.1、显示单个字符

假设要显示字符"A",设计“A”所占的区域大小为8x16像素的矩形区域,有图像显示的地方为1,没有的为0,如图:
字符A
一行八位,可以用两个十六进制数表示,一个字符就用十六组数字表示出来,例如"A"字符:

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

表示出来了,怎么显示呢?看代码:

//vram:内存起始地址,xsize:屏幕x方向的分辨率,x y:绘图起始位置,c:颜色序号,font:字符表示数组
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;
}

通过与计算方式,找到表示为“1”的位置,内存中写入颜色c,为”0“的位置不动。搂一眼:
打印A

5.3.2、显示其他字符

现在只有一个字符A数组,但是要想显示其他字符怎么办呢?当然可以像打印 A 一样,给每个字符设计一个数组,但是这太慢了,作者提供一个字体,保存在txt中,长这样:

char 0x41
........
...**...
...**...
...**...
...**...
..*..*..
..*..*..
..*..*..
..*..*..
.******.
.*....*.
.*....*.
.*....*.
***..***
........
........

这个字体文件有256的字符,每个都是8x16像素大小,作者使用了一个工具把字体文件编译成了一个4096的数组(16x256)。
在c程序文件中可以声明后使用,常用字符都是按照ASCII的顺序编制的。

extern char hankaku[4096];

传参时字体数据这么传:

putfont8(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, hankaku + 'A' * 16);
//等价
//putfont8(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, hankaku + 0x41 * 16);

5.3.3、显示字符串

上面我们已经可以显示很多字符了,但是显示一个字符就要调用一次putfont8函数。

感觉挺麻烦的,再设计一个 putfont8_asc 函数,循环打印字符。

//s:字符串
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;
}

调用

putfonts8_asc(binfo->vram, binfo->scrnx,  8,  8, COL8_FFFFFF, "ABC 123");

5.4、显示变量值

刚开始可能会想,这还不简单,使用printf 就好了,其实是不行的,因为printf函数使用操作系统的功能,各个操作系统还不同。这里使用sprintf 函数,该函数不是指定格式输出,只是将输出内容作为字符串写在内存中
参考解释 link有人会问,既然是在用标准的C语言开发内核,我们都知道C语言提供了标准打印函数pintf,为什么不直接使用?原因:1.C语言标准函数库需要归属的操作系统支撑,我们现在是自制操作系统,当然无法支持其函数应有的功能。

(不太明白,sprintf也用到%d之类的格式化,怎么不是指定格式输出了?
自答:sprintf只是将字符存入指定内存中,内有调用系统的输出显示功能。),

这个函数被作者改造过了,能够不使用操作系统功能显示字符。

//s:字符串数组
sprintf(s, "scrnx = %d", binfo->scrnx);
putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);

5.5、显示鼠标

//mouse:256大小的数组,bc:背景色
//O:白色,鼠标主体;*:鼠标的黑边;.:背景色
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;
}

绘制函数:

//vram:内存起始地址;vxsize:屏幕x大小;pxsize pysize:绘制鼠标大小;px0 py0:绘制的起始位置;buf:256的数组(init_mouse_cursor8函数设置好的);bxsize:鼠标每一行大小,起始和pxsize表示内容差不多
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);

聪明的你,这就很简单了吧

5.6、GDT与IDT的初始化

现在鼠标还无法动起来,要让它动起来,首先需要初始化GDT和IDT。
GDT(global [segment] descriptor table)全局段描述表。
IDT (interrupt descriptor table)中断描述表。
这两个表是操作系统在32位模式下很重要的表。

5.6.1、GDT与分段

操作系统要执行多个程序的时候,可能内存会发生重叠,这时候就需要要其中一个程序停止运行,并报出内存冲突的错误。解决方法就是分段。

还记得在16位模式下,有说段寄存器,是段寄存器左移4位+偏移=地址。现在到32位不需要了,但是,段寄存器也不是直接参与寻址了,段寄存器用来选择段,也叫段选择子。

我说的还不全面,其实段选择子还包括影子寄存器,只不过是对程序员不可见的。
段选择子
上图中影子寄存器是靠硬件来操作的,对系统程序员不可见,是硬件为了减少性能损耗而设计的一个段描述符的高速缓存,不然每次内存访问都要去内存中查表,那性能损失是巨大的,影子寄存器也正好是 64 位,里面存放了 8 字节段描述符数据。

低三位之所以能放 TI 和 RPL,是因为段描述符 8 字节对齐,每个索引低 3 位都为 0,我们不用关注 LDT,只需要使用 GDT 全局描述符表,所以 TI 永远设为 0。

段选择子有13位进行选择段,2的13次方有0-8191个段可以选择,所以也会有8192个段描述符,一个段描述符32位(8字节),所以段描述符表一共有64K大小,寄存器是存不下的,需要存放在内存中。这片内存要求是连续的。这个表的起始地址就存放在GDTR(global [segment] descriptor table register)中,这是CPU的一个寄存器,48位大小。
GDTR其中保存了GDT的基地址和界限(或者说GDT的长度),高32位为GDT的基地址,低16位为界限。还记得保护模式中的段寄存器也是16位的吗,它们和gdtr中的界限是对应的啊。

补充
32位分段机制
参考: 保护模式下的寻址
分段
段描述符

段描述符段描述符拆开成两个32位:
段描述符

总结一下,分段模式下找内存地址的过程:

1、寻址时,先找到gdtr寄存器,从中得到GDT的基址
2、有了GDT的基址,又有段寄存器中保存的索引,可以得到段寄存器“所指”的那个表项,既所指的那个描述符
3、得到了描述符,就可以从描述符中得到该描述符所描述的那个段的起始地址
4、有了段的起始地址,将偏移地址拿过来与之相加,便能得到最后的线性地址
5、 有了线性地址(虚拟地址),经过变换,即可得到相应的物理地址

5.6.2、IDT

当CPU遇到外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理这种突发事件。这就是中断功能。
如果没有中断,CPU一直要查询外部状态,还要处理当前程序任务,有些设备或许就没发生变化,根本不需要查询,这就显得很浪费了,只有发生中断的时候再去处理外部情况,其他时间只需要一直安心处理程序任务就好了。

IDT和GDT类似。

保护模式的中断表
IDT中断门描述符
中断门描述符

5.6.3、代码

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;
}
//1号和2号设定一下上限、基址和权限
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
//保存gdtr
load_gdtr(0xffff, 0x00270000);

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值