DAY5 结构体、文字显示与GDT/IDT初始化

2020.10.17-2020.10.18

今天的内容主要学习了如何显示符号,将鼠标指针显示出来后,进而考虑如何让它动起来。

1. 接受启动信息

文档:harib02a

昨天的bootpack.c里的0xa0000,320,200等数字都是直接写入程序,当画面模式改变时,不更改bootpack.c里的值就不能正确运行。所以我们应该从asmhead.nas中把先前保存下来的值取出。

因此,对bootpack.c做如下更改:

; asmhead.nas节选; 有关BOOT_INFO
CYLS    EQU     0x0ff0          ; 设定启动区
LEDS    EQU     0x0ff1
VMODE   EQU     0x0ff2          ; 颜色位数
SCRNX   EQU     0x0ff4          ; 分辨率的X
SCRNY   EQU     0x0ff6          ; 分辨率的Y
VRAM    EQU     0x0ff8          ; 图像缓冲区的开始地址
//bootpack.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();}
}

试用结构体

文档:harib02b

再试着在上面的代码的基础上做些修改,这次我们将使用到结构体。

//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();}}

16行可以直接使用binfo = 0x0ff0 但会报warning所以进行了类型转换。

结构体中char reserve是为了空出一字节地址,使得scrnx以后的变量在对应的地址空间上。

char i是1字节变量,short i是2字节变量,int i是4字节变量。

试用箭头记号

文档:harib02c

在C语言中,(*binfo).scrnx可以用binfo->scrnx的方式表现。

类似的*(a+i)也可以用a[i]。在C语言中关于指针的省略表现形式很丰富。

为了让代码更简洁些,这次我们连xsize = binfo->scrnx里的xsize也直接省略。

所以我们做出如下修改:

//bootpack.c节选
void HariMain(void)
{struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;init_palette();init_screen(binfo->vram, binfo->scrnx, binfo->scrny);for (;;) {io_hlt();}}

2. 显示字符

文档:harib02d

内部处理好了,重新将重心放回到外部显示上。

之前显示字符都靠调用BIOS函数,现在进入32位模式,只能靠自己了。

**那应该怎么显示字符呢?**我们可以用8*16的长方形像素点阵将字符画出来,拿A举例子。

这种描画文字形状的数据称为字体(font)数据。

**为什么用8位作宽呢?**因为一字节数据就是8b,更方便操作。

现在我们将font_A作为数据存入程序中。

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 d; /* data */for (i = 0; i < 16; i++) {d = font[i];if ((d & 0x80) != 0) { vram[(y + i) * xsize + x + 0] = c; }if ((d & 0x40) != 0) { vram[(y + i) * xsize + x + 1] = c; }if ((d & 0x20) != 0) { vram[(y + i) * xsize + x + 2] = c; }if ((d & 0x10) != 0) { vram[(y + i) * xsize + x + 3] = c; }if ((d & 0x08) != 0) { vram[(y + i) * xsize + x + 4] = c; }if ((d & 0x04) != 0) { vram[(y + i) * xsize + x + 5] = c; }if ((d & 0x02) != 0) { vram[(y + i) * xsize + x + 6] = c; }if ((d & 0x01) != 0) { vram[(y + i) * xsize + x + 7] = c; }}return;
}

7-14行的d & 一个8进制数的操作是判断第几位是否为1,为1的话向内存中写入颜色,否则不写入就能将字符画出来了。

再把程序整理一下。

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

增加字体

文档:harib02e

目前我们只能显示‘A’,还有很多字符需要显示。这里为了将重心放在操作系统上,我们使用别人已经编码好的字体。

将hankaku.txt加入到源程序中,对于这个文件我们当然是不能直接用的,还需要专用的编译器。

使用makefont.exe将上面的文件输出成16*256=4096B的hankaku.bin文件(每个字符16B,256个字符,按照ASCII编码所以256个),但bin文件也不能和bootpack.obj连接,所以还要通过bin2obj.exe将其转换成目标程序。

各种工具的使用方法参考Makefile

在C语言中写入extern char hankaka[4196] ;即可使用了。在C语言使用源程序以外的数据需要加上extern属性。

B的字符编码是0x42,放在"hankaku + 0x42 * 16"开始的16字节里,按照字符编码也可以写作"hankaku + ‘B’ * 16"。

下面就写个程序将”YRL nb!“显示出来把

//bootpack.c节选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 + 'Y' * 16);putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'R' * 16);putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'L' * 16);putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + ' ' * 16);putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + ' ' * 16);putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + 'n' * 16);putfont8(binfo->vram, binfo->scrnx, 64, 8, COL8_FFFFFF, hankaku + 'b' * 16);putfont8(binfo->vram, binfo->scrnx, 72, 8, COL8_FFFFFF, hankaku + '!' * 16);for (;;) {io_hlt();}}

显示字符串

文档:harib02f

显示几个字符,就要这么多代码,不太美观。

可以改进一下写一个显示字符串的函数:

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

在C语言中,字符串都是以0x00结尾的。

再来看看改进后的HariMain函数: 看起来舒服多了

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, "Yrl nb!");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();}}

显示变量值

文档:harib02g

已经可以显示字符串了,现在我们看看如何实现实现变量值?

我们可以使用sprintf函数,它是printf的同类,他们之间的功能很相近。但在自制操作系统的过程中,不能随便使用printf函数,而sprintf可以使用。sprintf只是将输出内容作为字符串写到内存中,不使用操作系统的任何功能。而printf是输出字符串的方法,各种操作系统都不一样,不论如何精心设计,都不可避免要使用操作系统的功能。

在使用sprintf函数前,在源程序开头写上#include<stdio.h>。

sprintf使用方法:sprintf(地址,格式,值,值,值,…); 这里的地址是字符串存放地址。

sprintf(s, "scrnx = %d", binfo->scrnx);

3. 显示鼠标指针

文档:harib02h

思路跟显示字符一样。先画出来,然后转成数据输出。

首先将鼠标指针的大小定为16*16,

void init_mouse_cursor8(char *mouse, char bc)
/* 准备鼠标指针(16*16) */
{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;
}

绘制函数:

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

vram和vxsize是VRAM的信息,指VRAM的地址开始处和每行的像素数。pxsize和pyszie指想要的图形的大小,在这里两个都是16。px0和py0指图形在画面上的显示位置。最后的buf和bxszie分别指图形的存放位置和图形每一行含有的像素数。

接下来,调用这两个函数:

init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);

运行成功!

4. GDT与IDT的初始化

文档:harib02i

鼠标指针已经显示出来了,但目前还不能移动。

**要怎么才能让鼠标动起来呢?**首先要将GDT和IDT初始化。

**那GDT和IDT究竟是什么呢?**下面一一道来。

操作系统同时运行多个程序,需要避免内存地址冲突。解决内存地址冲突的方法就是分段。

分段后,为了表示一个段,需要如下信息:

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

CPU用8B的数据来表示这些信息,但用于指定段的寄存器只有16b(32位模式下,段寄存器依然是16b),由于CPU设计的原因,段寄 存器的低3位不能使用,所以真正表示段号的只有13位,能够处理的就只有0-8191的段号了。

**那段号怎么设定呢?**0-8191的区域,意味着8192个段,按调色板那样设定的话,就需要8192*8=65536B(64KB)。显然CPU没有那 么大存储能力,所以我们需要将这些数据写入到内存中,而内存中这64KB的数据就叫GDT。

GDT(Global segment Descriptor Table):全局段号记录表,将内存的起始地址和有效设定个数放在CPU内被称为GDTR(GDT Register)的特殊寄存器中,设定就完成了。

IDT(Interrupt Descriptor Table):中断记录表。记录了0-255的中断号码和调用函数的对应关系。

中断:当CPU遇到外部情况变化,或者是内部偶然发生某些错误时,CPU会临时切换过去处理这种突发事件。

中断的意义:为了响应键盘输入字符的事件,CPU需要对键盘的情况进行查询。如果查询得太慢,就会让用户感觉按键后电脑半天没反应。为了及时响应,CPU就需要频繁的查询,而设备可不只有键盘,还有鼠标,硬盘,网卡,声卡等等都需要定期查看设备的状态。大量时间用于查询的话,CPU就会忙于应付。这时,中断机制就出现了。设备有变化时产生中断告知CPU,CPU停止正在处理的任务转而执行中断程序,处理完后返回处理中的任务。得益于中断,CPU不用再浪费精力再查询设备状态上,可以集中精力处理任务了。

所以,我们想使用鼠标就得使用中断,就得设定IDT。

//bootpack.c节选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;
}

gdt被赋值0x00270000,意味着GDT被设为了0x270000-0x27ffff,这块地址没什么意义,只是在内存分布图上这一块地方没有被使用。

idt被赋值0x0026f800,所以IDT被设为了0x26f800-0x26ffff。(顺便说一下,0x28000-0x2fffff放着bootpack.h)

/* 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);

GDT的上限,基址,访问权限都被初始化为0后,还对段号1和2进行了设定。

段号1的上限值为0xffffffff,即大小为4GB,地址是0,它表示CPU所能管理的全部内存,段的属性设为0x4092留到明天再讨论。

段号2大小是512KB,地址是0x280000,刚好是为bootpack.hrb准备的,用这个段就可以执行bootpack.hrb,因为bootpack.hrb是以ORG 0为前提翻译成的机器语言。

load_gdtr(0xffff, 0x00270000);

C语言不能给寄存器赋值,所以这里调用了汇编语言的函数给GDTR赋值。

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

GDTR是一个很特别的48位寄存器,需要通过指定一个内存地址,从指定的地址读取6个字节,然后赋值给GDTR。

该寄存器的低16位是段上限,高32位是GDT的开始地址。

执行函数前,内存按[FF FF 00 00 00 00 27 00]存放,为了执行LGDT,所以把他们排列成[FF FF 00 00 27 00]。

因为GDTR和IDTR结构体基本上一样,IDTR也用load_gdtr设置。

下面补充说明一下set_segmdesc

struct SEGMENT_DESCRIPTOR{short limit_low, base_low;char base_mid, access_right;char limit_high, base_high;
};
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;}

说到底,这个函数是按照CPU的规格要求,把段的信息归结成8B写入内存。

这8B到底记录什么呢? 段的大小,段的起始地址,段的管理属性。

  • 首先看段的地址,毫无疑问的是32b来表示。在结构体里分为low(2B),mid(1B),high(1B)3段,刚好32b。**为什么要分3段呢?**为了与80286兼容(80286是16位机)。

  • 再看看段的上限,它表示段的大小。段上限最大是4GB,是一个32b的数值,如果直接放进去,段的管理属性都没位置存放了。所以段上限只能使用20b,最大也只能指定到1MB。(Intel的大佬们为了解决这个问题,在段属性设了个标志位Gbit,当其为1时,上限的单位被解释成页,一页是4KB,这样4KB*1M=4GB,就能指定4GB的段了。)那么又有问题来了,**结构体里limit_low(2B)和limit_high(1B)一共3B,为何只能使用20b?**实际上,limit_high的高4位被用来存放段属性了。

  • 最后看看段属性。剩余1B的空间加上limit_high的高4位一共12b。段属性又称”段的访问权属性“,用变量名access_right或ar表示。

    ​ ar的高4位被称为”扩展访问权“,由”GD00“构成,**为什么叫扩展?**因为这高4位的属性,在80286的时代还不存在。G指Gbit,D指段的模式,1指32位模式,0指16位模式。

    ​ ar的低8位我们简单地介绍一下。

    ​ 00000000(0x00):未使用的记录表(descriptor table)。
    ​ 10010010(0x92):系统专用,可读写的段。不可执行。
    ​ 10011010(0x9a):系统专用,可执行的段。可读不可写。
    ​ 11110010(0xf2):应用程序用,可读写的段。不可执行。
    ​ 11111010(0xfa):应用程序用,可执行的段。可读不可写。

    ​ 在32位模式下,CPU有系统模式和应用模式之分。CPU到底处于什么模式,取决于执行中的应用程序位于访问权为0x9a还是0xfa的段。

今天的内容有点多,稳住!

现在的系统因为增加了4KB的字体文件,所以已经到了7632B的大小了。明天见。

自制操作系统5-结构体、文字显示与GDT/IDT初始化相关推荐

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

    第5天 结构体.文字显示与GDT/IDT初始化 https://weread.qq.com/web/reader/38732220718ff5cf3877215k34132fc02293416a75f ...

  2. 30天自制操作系统:第五天 结构体、文字显示与 GDT/IDT初始化

    今天的内容相比前几天多了很多,主要是一些东西用代码写出来更难理解,需要更多的时间去琢磨,因此对于一些较为基础的内容不会进行详细的描述. 1.接收启动信息(harib02a) 之前的程序大都是直接使用0 ...

  3. 操作系统实验第五天:结构体、文字显示与GDT/IDT初始化

    一.实验主要内容 内容1:接受启动信息 之前的bootpack.c中是将数字直接写入程序,但这些数字本身应该是从asmhead.nas先前保存下来的值中取.不然当画面模式改变时,程序就不能正确运行. ...

  4. 30天自制OS学习笔记 (五)结构体、文字显示与GDT/IDT 初始化

    1.接收启动信息 & 2.试用结构体 & 3.试用箭头符号 在第五天之前,我们都是把vram.xsize.ysize这些值直接写在了bootpack.c文件中.而这些值应从asmhea ...

  5. 五、结构体、文字显示与GDT/IDT初始化

    试用结构体:      结构体的好处是,可以将各个参数一股脑传递进去,而没有结构体则需要一个一个传递. 一个灵光一闪:原来一个大的软件,所有源文件全部编译进去成为一个整体存放在内存里,假如文件A中宏定 ...

  6. 【Struct(结构体)杂谈之二】名不正则言不顺---Struct(结构体)的声明、定义及初始化

    Struct(结构体)的声明.定义及初始化 上一篇里我们讲了为什么我们要引入Struct这个数据类型,我们了解到Struct是一种聚合数据类型,是为了用户描述和解释一些事物的方便而提出的,Struct ...

  7. c语言结构体菜单显示框架,请教c语言结构体嵌套问题。field `atItem' has incomplete type...

    //菜单项结构体 typedef struct { BYTE                aucItemName[20];   //菜单项的内容 WORD32              dwItem ...

  8. C++结构体实例和类实例的初始化

    结构体实例(包括共用体)和类实例的初始化方法完全相同,二者都可以应用于继承层次中.不同点是结构体(包括共用体)默认成员为public,而类默认成员是private型的. 一.若类和结构体所有数据成员均 ...

  9. C++结构体实例和类实例的初始化 .

    结构体实例(包括共用体)和类实例的初始化方法完全相同,二者都可以应用于继承层次中.不同点是结构体(包括共用体)默认成员为public,而类默认成员是private型的. 一.若类和结构体所有数据成员均 ...

最新文章

  1. XML Schema用法
  2. Android数据存储之SQLite的操作
  3. 使用Go开发gRPC
  4. [vue-cli]vue-cli3你有使用过吗?它和2.x版本有什么区别?
  5. php解析xml数据格式,PHP解析xml格式数据工具类实例分享
  6. Leetcode-5199 Smallest String With Swaps(交换字符串中的元素)
  7. 一文带你从零认识什么是XLA
  8. Oracle11g链接提示未“在本地计算机注册“OraOLEDB.Oracle”解决方法
  9. c++ assert用法
  10. linux下的文件及目录介绍
  11. 谷歌云盘超大文件快速下载方法
  12. dc持久内存与mysql_英特尔傲腾数据中心级持久内存的五大用例
  13. 【Rust指南】错误的分类与传递|使用kind进行异常处理
  14. 蓝牙通信工作流程讲解
  15. php大写数字转换,php如何实现数字金额转换大写金额(代码示例)
  16. 从两个角度谈谈:什么是产品视角
  17. 电子产品可靠性测试费用及检测项目流程
  18. Activiti6.0(三)实现一个请假流程
  19. SAP CDS UI 常用注解用法
  20. FineReport表格软件-CSS动态切换年月日查询报表

热门文章

  1. 01_机器人坐标系的说明
  2. 探究KVO的底层实现原理
  3. Sqlmap -- POST注入
  4. Adobe Photoshop 输出ICO格式图标文件
  5. 全面质量管理理论中的五个影响产品质量的主要因素
  6. 热那亚中文离线地图App上线
  7. PHP日活10万,小程序日活超4亿,近10万商家开通直播,私域红利已来 !
  8. Galois Field NTT
  9. 【经典】synergy共享鼠标键盘/一套鼠标键盘操作多台电脑
  10. 字符串、列表、字典、元组的基本操作