自制操作系统日志——第五天
今天是该系列的第五天。今天的任务就是1、采用结构体形式;2、利用像素矩阵的方法进行字符串的显示;3、GDT/IDT的初始化。
一、利用结构体表示变量的集合
在前一天中,为了方便学习我们都是直接将数字写入程序当中的。但是,对于我们来说,这种习惯是极其不好的,因为这都是我们在设定了采用VRAM画面模式下已知了屏幕分辨率的情况下进行设定的。如果画面进行了更改,那么我们就要把用到的部分都要修改,实在是过于麻烦。因此最好直接从asmhead.asm当中,取得这些关于VRAM的值是更加好的做法!
但是,如果我们直接利用在主程序中定义一个变量来进行获取asmhead中的值的话,那我们需要声明的变量也太多了。看起来也不够简洁,因此,建议使用结构体的形式进行读取。如下述:
//与asmhead中的bootinfo内容对齐
struct BOOTINFO /* 0x0ff0-0x0fff */
{
char cyls;/* 启动区读磁盘读到此为止 */
char leds;/* 启动时键盘的LED的状态 */
char vmode;/* 显卡模式为多少位彩色 */
char reserve;
short scrnx,scrny;/* 画面分辨率 */
char *vram; /*显卡入口地址*/
};//将变量声明集中起来,该变量一共12字节,作为一个整体的新变量BootInfo,可以给其他进行声明;记住按照asmhead顺序写变量
这样子就很简单明了的知道了这些变量的含义,而且我们还可以利用将这个结构体进行声明,并赋予这个结构体一个初始的地址值,方便指向我们在asmhead当中用到的那12个字节的地址空间,具体如下:
void HariMain(void)
{
char *vram;
int xsize,ysize;
struct BootInfo *binfo;
//为了从asmhead.nas中保存下来的值中取出,以防止当画面模式改变后出现异常。因为直接使用320 这些固定的可能会出错,而我们在.nas中会预先设定号模式,直接从这取比较保险
binfo = ( struct BootInfo *) 0x0ff0;
.......
}
这里,我们就把bootinfo这个结构体的起始地址指向了0x0ff0 ,这里也是我们在asmhead当中使用到的起始空间。至于结构体里的变量就按samhead里的地址顺序进行声明:
这里,当我们使用上述的方式后,我们可以使用以下两种,来取得结构体里的值:
(*binfo).scrnx
binfo->scrnx
二、像素字符阵
对于在OS的屏幕上如何显示字符?这里可能是一个比较困难的地方,因为这里我们已经不能调用bios进行显示了,那么我们就换一个思路进行开发测试一下看看。
我们可以采用一个8x16的字符矩阵进行显示256个字符(ascii),如下图所示:
比如,上述的A字符,我们可以就用以下这个16进制的数组来表达:(对应上图的二进制)
static char font_A[16] = {
0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
};
然后,在写下这个函数进行显示输出:
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; } //p代表着字符像素点阵一行的那8位
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;
}
这里的几个if 是根据a字符的8x16这个矩阵中需要呈现的像素点进行判断的!举例来说:
例如0x80 换算成二进制就是 1000 000 ,任意的二进制数与之相与的结果,都只可能是最高位上是1 。因此,当我们已经建立字符数组font_A后,font_A中的每一个8位数据都会与之相与,只有当font_A[13]=0xe7(a字符中只有这个最高位是1)时,结果才会为0,此时就要在这个地方填入颜色。
让我们试着输出一下吧!
当然,为了能够将ascii中的字符串都显示出来,我们可以一个个这样子设置,但是如果这样子搞,可能会花去我们很多的时间。因此以下我们使用kiyoto先生开发好的hankaku.txt字符文本文件来装载在系统里。
我们需要将文本文件读入进来,并于bootpac.obj进行链接,因此这里我们修改以下makefile文件,方便输出对应的obj以及进行连接:
#增加部分:
hankaku.bin : hankaku.txt Makefile
$(MAKEFONT) hankaku.txt hankaku.bin
hankaku.obj : hankaku.bin Makefile
$(BIN2OBJ) hankaku.bin hankaku.obj _hankaku
#修改部分:
bootpack.bim : bootpack.obj naskfunc.obj hankaku.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj naskfunc.obj hankaku.obj
然后,我们再在c的主函数下添加:
extern char hankaku[4096];
由于该文件是依照ascii码进行编制的,因此我们想要找到某个字符数据的话需要找到对应的地址,例如,a字符的ascii码为0x41,则地址为 “hankaku+0x41*16”
继续进一步的为了方便显示字符串,制作如下代码(注意,这里字符串调用的显示字符的函数名一定要对应,我这里因为之前没对应上,总是出现莫名其妙的bug):
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, char *s) //s指向字符串开头的位置,因此可以直接使用s进行读取字符串的每一个单独字符; 以ascii编码
{
extern char hankaku[4096];
for(;*s != 0x00; s++)
{
putfont8(vram, xsize, x, y, c, hankaku + *s*16); //c语言中,函数都是以0x00结尾的!!
x += 8;
}
return;
}
然后,让我们看看主函数中的调用:
putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 123");
putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Yuan OS.");
putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Yuan OS.");
make run 以下:
很好,接下来进一步!我们试试看能不能将变量显示出来呢?
这里,我们调用编译器自带的sprintf函数。在该编译器中,sprintf不像printf函数需要调用到操作系统的大小(我们本来就是建操作系统,怎么可能要用到那种调用操作系统的函数),sprintf函数作用是将数据作为字符串输出到内存当中。
void HariMain(void)
{
char s[40];
int mx,my;
struct BootInfo *binfo = ( struct BootInfo *) 0x0ff0 ;
//extern char hankaku[4096]; //16x256 共256个字符
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, "Yuan OS.");
putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Yuan OS.");
sprintf(s, "scrnx = %d",binfo->scrnx);
putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);
//sprintf(s, "scrnx = %d ", binfo->scrnx);//sprintf(地址,格式,值, 值...) 地址就是该字符串存放在内存当中的地址; 格式就是想要存放的字符串,如果当中有%d之类的则会自动进行置换
for(;;)
{
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有hlt这个命
}
}
三、像素鼠标阵
在前面我们利用了矩阵形式成功的显示了字符,那么接下来我们进一步的利用像素矩阵进行显示鼠标:
如图所示,建立了一个16x16的鼠标矩阵。使用原理字符矩阵一样,于是编写一个函数先进行初始化:
void init_mouse_cursor8(char *mouse, char bc) //准备鼠标指针 16x16 ;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[x+y*16] = COL8_000000;
}
if(cursor[y][x] == 'O'){
mouse[x+16*y] = COL8_FFFFFF;
}
if(cursor[y][x] == '.'){
mouse[x+16*y] = bc;
}
}
}
return;
}
这里我们将颜色信息等等,都放在了传进来的char *mouse指向的内存数据当中,要想写入VRAM显示器当中,我们还需要编写一个填入的函数:
void putblock8_8(char *vram, int vxsize, int pxize,
int pysize, int px0, int py0, char *buf, int bxsize)
/*将鼠标的背景色显示出来;
vram与vxsize是关于VRAM信息的,值分别是0xa000与320;
pxsize与pysize是显示图形的大小,即鼠标像素阵,16x16;
px0与py0是指定图形在画面上显示的位置;
buf与bxsize是指定图形的存放地址和每一行含有的像素数(为后面准备,这里是与pxsize一样)
*/
{
int x,y;
for(y=0; y < pysize ; y++)
{
for(x=0; x < pxize; x++)
{
vram[(py0+y)*vxsize+(px0+x)] = buf[y*bxsize+x];
}
}
return;
}
然后,我们在主函数调用一下就行:
void HariMain(void)
{
char s[40], mcursor[256];
int mx,my;
struct BootInfo *binfo = ( struct BootInfo *) 0x0ff0 ;
//extern char hankaku[4096]; //16x256 共256个字符
init_palette();
init_screen( binfo->vram, binfo->scrnx, binfo->scrny);
/* 显示鼠标 */
mx = (binfo->scrnx - 16) / 2; /* 计算画面的中心坐标*/
my = (binfo->scrny - 28 - 16) / 2;
init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
....
}
当然,由于这里还没有做其他的设定,因此之做出来个中看不中用的鼠标。为了让他动起来我们还需要再多做一些事情。
四、GDT和IDT的初始化
为了方便后续的代码理解,这里我会提前介绍以下GDT与IDT,如果不需要的可以直接跳过该部分。。。
**GDT:**全局段号记录表。其就是指当计算机采用段式存储时,计算机会将整个内存分为一个个的段方便进行管理。然后,GDT就是用于存储每一个段信息的记录表,通过此记录表计算机能够轻易的了解到每一个段的具体信息。每一个段都主要有以下几个信息部分:
- 段地址大小
- 段地址的起始地址
- 段的管理属性
cpu中规定要用8字节的数据来表示这些信息。 按照上述分类,我们进行如下解释:
段地址: 段的地址空间大小为32位(4字节),在cpu中被视为段的基址。因此在下面的结构体中我们使用了base这个变量名。在这个结构体中base分为low(2字节)、mid(1字节)、high(1字节)三个段组成。因此,在下述的初始化中,只需按顺序填入对应的数值即可!!(程序代码中利用移位来达到填入对应位置)
这里可能会有人问,为什么要分成三个段?害,这当然都是80286兼容(老古董了)!
**段大小(段上限):**一个段按理来说最大有4GB,如果直接用此表示,那么我们又要使用4字节,这明显是不可取的毕竟我们还有管理属性没有填入! 因此,我们利用20位作为段的上限。
但是这里如果只采用20位的话,那么我们只能最大寻址到1mb,实在是有点小。因此,在段的属性中我们设置了一个属性G的标志位,当G=1时,这个上限的地址limit的单位就不在是字节了,而是以4kb为单位。至此,就可以有1MB*4KB = 4GB
在代码的结构体中,limit(上限)分为了limit_low与limit_high总共3字节,共24位,因此我们会将limit_high的高四位作为段属性填入!!
段的管理属性: 最后会有12位作为段的属性。再加上前面的4位一共16位,因此代码中,可以利用ar这个变量把他们当作16位进行处理!
ar的高四位称之为拓展访问权:这四位就是"GD" 其中G就是前面说的单位问题 。 D则是指段的模式:当D=1时是32位,当D=0时是16位。
ar的低八位是:
0x00:未使用记录表
0x92:系统专用,可读写不可执行
0x9a:系统专用,可执行可读,不可写
0xf2:应用程序用,可读写不可执行
0xfa:应用程序用,可执行可读,不可写
所谓系统专用,就是不给运行在系统之上的应用程序调用的段。
然后,我们的程序呢,就是先将这些数据整齐有序的放在内存的某个地方,然后将内存的起始地址,和设定的有效个数放在cpu的专用寄存器GDTR当中。
IDT与GDT是大同小异的具体可以看后续的代码注释了。
代码:boorpack.c新增:
struct SEGMENT_DESCRIPTOR{
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
}; //存放8字节的GDT内容
struct GATE_DESCRIPTOR
{
short offset_low, selector;
char dw_count, access_right;
short offset_high;
}; //存放8字节的IDT内容
void init_gdtidt(void)
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000; //使用0x00270000~0x0027ffff
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800; //使用0x0026f800~0x0026ffff
int i;
//GDT初始化
for (i = 0; i < 8192; i++)
{
set_segmdesc(gdt + i , 0, 0, 0); //这里gdt由于表示的是8字节的结构体,因此i每增加一次,地址就+8
}
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);//设定gdt的第一个段为全局,方便cpu掌管内存
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);//设定2号段大小为512kb,用于bootpack.hrb
load_gdtr(0xffff, 0x00270000);//利用汇编给GDTR寄存器存储数值
/* 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;
}
总结
今天的任务到此结束,明天我们加吧劲继续吧IDT与GDT搞定!!内容可能会比较难,大家结合书本再查询资料吧。