回顾:经过第四天,已经可以画出一个桌面了,现在在这个基础上添一些东西吧。
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"字符:
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“的位置不动。搂一眼:
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位大小。
其中保存了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类似。
保护模式的中断表
中断门描述符
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