《30天自制操作系统》第5天

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

1.接收启动信息

我们在asmhead.nas中保存了一些数值,比如SCRNX、SCRNY,但是不取出来使用的话就没有意义了,就让我们来读取这些数据吧。

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

这段代码取出数值的方法比较麻烦。下面介绍一个更简单的方法。


2.试用结构体

如标题所示,使代码变简单的方法就是使用结构体,结构体也是属于C语言的基础知识,所以你懂的,我不会讲它。

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

...

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

代码看起来清爽了不少。

其中,把结构体指针指向0x0ff0地址,结构体内部的元素会对应到之后的地址上,即cyls对应CYLS,leds对应LEDS,以此类推。本来作者把数据存到一起就是为了用结构体。另外,reserve并没有什么用,它只是用来占据一个字节,使结构体内的数据字节对齐,虽然不加的话编译器也会让它自动对齐,但加了看起来更容易理解。


3.试用箭头记号

使用上一节内容的方式表示指针太麻烦了,下面变得更简单一点。

    struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;

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

箭头符号才是指针的标配。我寻思上一节里面也不需要赋值,为什么搞得那么麻烦呢?难道是为了让这一节和上一节形成对比?

这样讲起来真轻松啊,不加硬件设施的代码讲起来就是真么惬意。


4.显示字符

虽然我们之前使用BIOS功能显示了字符,但这次使用的是32位模式,用不了BIOS功能,只能自力更生了。

如下图所示,这是一个8*16的像素点阵,并将每个方格置换为0和1,得到右边的字节,1个字符在屏幕上的显示就可以通过8个字节表示出来。

如下是要添加的代码:

...

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

font_A数组的内容就是上图右侧的字节,putfont8用于显示字符,首先找到显示字符的位置,对字符进行与操作,在相应位上填上颜色,如0x18(00011000),就是在第3和第4位上填充颜色,之后换一行继续填充颜色,直至16行填充完毕。

显示字符的实现还是比较简单的。由于我曾经学过STM32在屏幕上显示字符,所以很快就理解了,相信大家解决起来不困难。

接着看看运行的结果。

感觉还不错,就是单单显示一个A有些单调了点。


5.增加字体

光显示一个A肯定不够的,最少要把ASCII码能显示的字符都能够显示出来。但如果每个字符的16个字节都需要我们自己手打,那作者肯定会被骂得够惨。基于这些原因,作者提供了一个hankaku.txt,其内容如下所示。

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

虽然看不太懂,但是没关系,作者把它处理成能用的形式,具体操作看书吧,顺便在Makefile里面添加关于hankaku.txt的处理代码。

最后hankaku.txt被处理成了一个4096字节大小的数组,在C文件里使用:

extern char hankaku[4096];

就能够使用了。hankaku数组中每16个元素代表了一个字符的字体数据,只需要用要显示的字符的ASCII码值乘以16就能找到字符在hankaku数组中的位置。

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

    while (1)
    {
        io_hlt();
    }
}

用这段代码来显示看看效果吧。应该会在屏幕上显示“ABC 123”。


6.显示字符串

上面显示字符串的操作太复杂了,写代码嘛,自然主体里面越少看起来越赏心悦目。现在就来改改。

void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
    extern char hankaku[4096];
    for ( ; *s != 0; s++)
    {
        putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
        x += 8;
    }
}

这个函数的实现方法就是把每个字符提取出来分别调用putfont8函数,最后x += 8是为了换位置显示字符,不然字符串只会原地显示,本来显示的是一个字符串就变成显示一个字符了。

    putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 123");
    putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "April-OS");
    putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "April-OS");

再在主函数加上这段代码,最后运行得到的结果如下。

作者这些带阴影的字符不错啊。


7.显示变量值

能看见系统中的变量值自然是好的,这样就可以通过变量的数值,明确地知道系统中是否发生了什么错误。

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

使用这两行代码进行变量值的显示。由于我们写的显示函数只能显示字符串,所以我们要先把数值转换为字符串,然后再调用显示函数。sprintf是C语言的库函数,书上都介绍有,我就不介绍了。

最后的结果为:


8.显示鼠标指针

代码如下:

void init_mouse_cursor8(char *mouse, char 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[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;
        }
    }
}

首先要定义鼠标的数据,使用一个16*16大小的数组保存下来,并用另一个数组记录鼠标的颜色信息。让为‘ * ’的部位显示黑色,为‘O’的部分显示白色,为‘ . ’的部分显示背景色。

/* @param
 * vxsize: 屏幕长度
 * pxsize: 填充块在x方向的长度
 * pysize: 填充块在y方向的长度
 * px0: 填充块的起始x坐标
 * py0: 填充块的起始y坐标
 * buf: 鼠标的颜色信息数组
 * bxsize: 鼠标的颜色信息数组的列数
*/
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];
        }
    }
}

这是用来显示鼠标的函数,将显存的相应位置填充鼠标颜色。

在主函数中也更改添加了如下代码:

    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);
    sprintf(s, "(%d, %d)", mx, my);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);

最后得到的结果如下。

可惜丑了点,没办法,谁叫咱的像素低。


9.GDT与IDT的初始化

GDT是全局段号记录表(Global segment Description Table),什么是段?这和操作系统的内存分配有关,不懂的去看看操作系统相关章节的知识吧(一开始我还没想到和GDT和内存分配有关,直到我考研专业课复习到了内存管理那一章)。由于段寄存器的低3位不能用,段号只有13位,段号有0~8191共8192个,每个段号有8B大小,所以段号一共需要8192*8B=64KB大小的内存(要是用不了8192个段,需要的内存自然可以减少)。

IDT是中断记录表(Interrupt Descriptor Table)。外部设备的速度要远远小于CPU的速度,当一个外部设备要工作了,CPU会放下手上的活运行外部设备,而大部分时间CPU都处于空闲状态,CPU的使用效率会大大减少,为此引入中断机制。IDT中有0~255共256个号码记录中断服务函数。

要理解这节的代码并不容易,因为作者把一部分解释写在了后面的章节,而且作者没有给出GDT和IDT的结构,虽然搜一搜GDT和IDT结构皆可以在CSDN上找到相关的博客,但我第一次学的时候就没有想过要去找博客看,结果弄得似懂非懂的,所以下面会对GDT和IDT进行介绍,篇幅可能会有点多,还请耐心看完。

以下是关于GDT的介绍。

如图是GDT的结构图。

基地址段在内存中的起始地址
段界限段的长度
TYPE由于介绍过长,放在下面解释
S

用于指定描述符的类型。

S=0,表示这是一个系统段;

S=1,表示这是一个代码段或数据段(堆栈段也是特殊的数据段)。

DPL表示描述符的特权级。处理器支持的特权级别有4种,分别是0,1,2,3,其中0是最高特权级别。
P

段存在位,用于表示描述符所对应的段是否存在。

P=0,表示段不在内存中。

P=1,表示段在内存中。

AVL软件可利用位。80386对该位未作规定,且与80386兼容的处理器都不会对该位的使用做任何规定。
L64位代码段标志,保留此位给64位处理器使用,目前将此位置0即可。
D/B

默认的操作数大小。主要是为了能够在32位处理器上兼容运行16位保护模式的程序。

D/B=0,表示指令中的偏移地址或者操作数是16位的;

D/B=1,表示偏移地址或者操作数是32位的。

G

粒度位,用于解释段界限的含义。

G=0,段界限以字节为单位,段的扩展范围为1B~·1MB(描述符的界限值为20位);

G=1,段界限以4KB为单位,段的扩展范围为4KB~·4GB

TYPE的介绍如下:

数据段代码段
XEWA含义XCRA含义
000X只读100X只执行
001X读和写101X读和可执行
010X只读,向下扩展110X只执行,依从的代码段
011X读写,向下扩展111X可执行,读,依从的代码段

其中X、E、W、A、C、R代表的意思分别为:

X

表示是否可执行。

X=0,表示不可执行,数据段总是不可执行的;

X=1,表示可执行,代码段总是可执行的。

E

段的扩展方向。

E=0,向上扩展,即向高地址方向扩展,是普通的数据段;

E=1,向下扩展,即向低地址方向扩展,通常是堆栈段。

W

段的写属性。

W=0,不允许写入,此时写入的话会引发异常中断;

W=1,允许写入。

A

已访问位。用于表示它指向的段最近是否被访问过。在描述符穿件的时候应该清0。之后每当该段被访问时,处理器将该位置1。

C

段特权级依从。

C=0,表示非依从的代码段,可以与特权级相同的代码段调用,或者通过门调用;

C=1,表示运行从低特权级的程序转移到该段执行。

R

段是否允许读出。

R=0,表示代码段不能读出,此时读出会引发处理器异常中断;

R=1,表示代码段可以读出。

我们现在就可以来理解下面这几段代码了。

...

    /* GDT初始化 */
    for (i = 0; i < 8192; i++)
        set_segmdesc(gdt + i, 0, 0, 0);
    set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);  /* 4092的意思是系统专用,可读写的数据段 */
    set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);  /* 4092的意思是系统专用,可读可执行的代码段 */

...

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
    if (limit > 0xfffff)
    {
        ar |= 0x8000;       /* G_bit = 1,此时段界限以4KB为单位 */
        limit /= 0x1000;    /* 0x1000 = 4096 = 4KB */
    }
    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;
}

set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);进入到函数内部后,检查段界限是否大于1MB,若大于则将G位置1,表示段界限以4KB为单位,并将段界限除以4KB得到新的段界限。这里的段界限为0xffffffff,明显超过了1MB,将G位置1并将段界限除以4KB。参数ar是int类型,是一个32位数据,作者应该是想将它的低8位保存TYPE、S、DPL、P,11~15位保存AVL、L、D/B和G。传入0x4092的含义可以通过上面查到,意思是这段的段界限以4KB为单位,偏移地址、操作数是16位的,存在于内存中,是一个可读写的数据段。同样0x409a的含义可以查到,这段的段界限以4KB为单位,偏移地址、操作数是16位的,存在于内存中,是一个可读可执行的代码段。

剩下的关于段基址和段界限的赋值操作就没什么可将的了。

讲完GDT,我们还要讲讲IDT,以下是IDT的内容。

如图是IDT的结构图。

段选择器中断处理程序所在的段选择符
偏移中断处理程序所在段的段内偏移
TYPE

描述符的类型。

1110:中断描述符;

0101:任务门描述符;

1111:陷阱门描述符

P与DPL的意义与GDT的一样,就不介绍了。再来看看有关IDT的代码吧。

...

    /* IDT初始化 */
    for (i = 0; i < 256; i++)
        set_gatedesc(idt + i, 0, 0, 0);
    load_idtr(0x7ff, 0x0026f800);   /* 将0x26f800~0x26ffff设为GDT */

...

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

由于参数都是0,没什么好讲的,想知道的就看看上面的介绍。

另外,在naskfunc.nas中也增添了新的函数,代码如下:

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

_load_idtr:         ; void load_idtr(int limit, int addr);
    MOV AX, [ESP + 4]       ; limit
    MOV [ESP + 6], AX
    LIDT    [ESP + 6]
    RET

LGDT/LIDT指令是将源操作数中的值加载到全局描述符表格寄存器(GDTR)或中段描述符表格寄存器(IDTR)。源操作数指定6字节内存位置,它包括了GDT或IDT的基址和界限。如果操作数大小属性是32位,则将16位限制(6字节数据操作数的2个低位字节)与32位基址(操作数的4个高位字节)加载到寄存器。

所以以上代码就是按照这个原则将GDT或IDT的基址和界限加载到GDTR或IDTR中。

不要忘记在HariMain中调用函数哦。下面来看看运行的效果吧。

然而并没有什么变化。。。╮(╯﹏╰)╭等到把键盘中断设置好就能看到些变化了。今天就结束啦。


终于写完了,这工作量不输第3天的工作量了,在我学第5天的内容的时候,上面的这些资料还是花了我几天才收集好,毕竟有些博客介绍的信息不完整,但我发现我的就算完整了也好像没什么用,有些尴尬。。。希望大家能够顺利地学习下去!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值