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

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

https://weread.qq.com/web/reader/38732220718ff5cf3877215k34132fc02293416a75f431d

1 接收启动信息(harib02a)
在bootpack.c里的,都是将0xa0000呀,320、200等数字直接写入程序,而本来这些值应该从asmhead.nas先前保存下来的值中取。如果不这样做的话,当画面模式改变时,系统就不能正确运行。

所以我们就试着用指针来取得这些值。顺便说一下,binfo是bootinfo的缩写,scrn是screen(画面)的缩写。

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

这里出现的0x0ff4之类的地址到底是从哪里来的呢?其实这些地址仅仅是为了与asmhead. nas保持一致才出现的。

另外,我们把显示画面背景的部分独立出来,单独做成一个函数init_screen。独立的功能做成独立的函数,这样程序读起来要容易一些。
---------------------
2 试用结构体(harib02b)
struct BOOTINFO {
    char cyls, leds, vmode, reserve;
    short scrnx, scrny;
    char *vram;
};
    struct BOOTINFO *binfo;
    binfo = (struct BOOTINFO *) 0x0ff0;
    xsize = binfo->scrnx;//*binfo_scrnx;
    ysize = binfo->scrny;//*binfo_scrny;
    vram = binfo->vram; 
binfo = (struct BOOTINFO *)0x0ff0;本来想写“binfo =0x0ff0; ”的,但由于总出警告,很讨厌,所以我们就进行了类型转换。
设定了指针地址以后,这12个字节的结构体用起来就没问题了。这样我们可以不再直接使用内存地址,而是使用*binfo来表示这个内存地址上12字节的结构体。这与“char *p; ”中的*p表示p地址的1字节是同样道理。
---
3 试用箭头记号(harib02c)
使用箭头,可以将“xsize = (*binfo).scrnx; ”写成“xsize = binfo->scrnx; ”,简单又方便。不过我们还想更简洁些,即连变量xsize都不用,而是直接以binfo->scrnx来代替xsize。
------
4 显示字符(harib02d)
到昨天为止,我们算是画出了一幅稍微像样的画,今天就来在画面上写字。以前我们显示字符主要靠调用BIOS函数,但这次是32位模式,不能再依赖BIOS了,只能自力更生。

那么怎么显示字符呢?字符可以用8×16的长方形像素点阵来表示。想象一个下图左边的数据,然后按下图右边所示的方法置换成0和1,这个方法好像不错。然后根据这些数据在画面上打上点就肯定能显示出字符了。8“位”是一个字节,而1个字符是16个字节。

大家可能会有各种想法,比如“我觉得8×16的字太小了,想显示得更大一些”、“还是小点儿的字好”等。不过刚开始我们就先这样吧,一上来要求太多的话,就没有办法往前进展了。

static char font_A[16] = {
        0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
        0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
    };
其实这仅仅是将刚才的0和1的排列,重写成十六进制数而已。C语言无法用二进制数记录数据,只能写成十六进制或八进制。嗯,读起来真费劲呀。嫌字体不好看,想手动修正一下,都不知道到底需要修改哪儿。但是暂时就先这样吧,以后再考虑这个问题。
-------
5 增加字体(harib02e)
虽然字符“A”显示出来了,但这段程序只能显示“A”而不能显示别的字符。所以我们需要很多别的字体来显示其他字符。英文字母就有26个,分别有大写和小写,还有10个数字,再加上各种符号肯定超过30个了。

这里沿用的OSASK的字体,其作者不是笔者,而是平木敬太郎先生和圣人(Kiyoto)先生。事先已经从他们那里得到了使用许可权

我们这次就将hankaku.txt这个文本文件加入到我们的源程序大家庭中来。这个文件的内容如下:
这比十六进制数和只有0和1的二进制数都容易看一些

当然,这既不是C语言,也不是汇编语言,所以需要专用的编译器。新做一个编译器很麻烦,所以我们还是使用在制作OSASK时曾经用过的工具(makefont.exe)。说是编译器,其实有点言过其实了,只不过是将上面这样的文本文件(256个字符的字体文件)读进来,然后输出成16×256=4096字节的文件而已。

编译后生成hankaku.bin文件,但仅有这个文件还不能与bootpack.obj连接,因为它不是目标(obj)文件。所以,还要加上连接所必需的接口信息,将它变成目标文件。这项工作由bin2obj.exe来完成。它的功能是将所给的文件自动转换成目标程序,就像将源程序转换成汇编那样。也就是说,好像将下面这两行程序编译成了汇编:

如果在C语言中使用这种字体数据,只需要写上以下语句就可以了。
extern char hankaku[4096];
像这种在源程序以外准备的数据,都需要加上extern属性。这样,C编译器就能够知道它是外部数据,并在编译时做出相应调整。

OSASK的字体数据,依照一般的ASCII字符编码,含有256个字符。A的字符编码是0x41,所以A的字体数据,放在自“hankaku+0x41 * 16”开始的16字节里。C语言中A的字符编码可以用’A’来表示,正好可以用它来代替0x41,所以也可以写成“hankaku +‘A' * 16”。
putfont8(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, hankaku + 'A' * 16);

我们使用以上字体数据,向bootpack.c里添加了很多内容,请大家浏览一下。如果顺利的话,会显示出“ABC 123”。下面就来“make run”一下吧。很好,运行正常。


makefile
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
# 3MB+64KB=3136KB
-------------------------
compile error:
../z_tools/makefont.exe hankaku.txt hankaku.bin
hankaku.bin hankaku.obj _hankaku
process_begin: CreateProcess(D:\myos\30days\tolset\harib02a\hankaku.bin, hankaku
.bin hankaku.obj _hankaku, ...) failed.
make (e=193): Error 193make.exe[2]: *** [hankaku.obj] Error 193
make.exe[2]: Leaving directory `D:/myos/30days/tolset/harib02a'
make.exe[1]: *** [img] Error 2    
lose line:
BIN2OBJ  = $(TOOLPATH)bin2obj.exe
---------------------
6 显示字符串(harib02f)
仅仅显示6个字符,就要写这么多代码,实在不太好看。
所以笔者打算制作一个函数,用来显示字符串。既然已经学到了目前这一步,做这样一个函数也没什么难的。嗯,开始动手吧……好,做完了。
C语言中,字符串都是以0x00结尾的,所以可以这么写。函数名带着asc,是为了提醒笔者字符编码使用了ASCII。
这里还要再说明一点,所谓字符串是指按顺序排列在内存里,末尾加上0x00而组成的字符编码。所以s是指字符串前头的地址,而使用*s就可以读取字符编码。这样,仅利用下面这短短的一行代码就能够达到目的了。
putfonts8_asc(binfo->vram, binfo->scrnx,  8,  8, COL8_FFFFFF, "ABC 123");
-------------------
7 显示变量值(harib02g)
现在可以显示字符串了,那么这一节我们就来显示变量的值。能不能显示变量值,对于操作系统的开发影响很大。这是因为程序运行与想象中不一致时,将可疑变量的值显示出来是最好的方法。

在开始的时候,我们曾提到过,自制操作系统中不能随便使用printf函数,但sprintf可以使用。因为sprintf不是按指定格式输出,只是将输出内容作为字符串写在内存中。这个sprintf函数,是本次使用的名为GO的C编译器附带的函数。它在制作者的精心设计之下能够不使用操作系统的任何功能。或许有人会认为,什么呀,那样的话,怎么不做个printf函数呢?这是因为输出字符串的方法,各种操作系统都不一样,不管如何精心设计,都不可避免地要使用操作系统的功能。而sprintf不同,它只对内存进行操作,所以可以应用于所有操作系统。

我们这就来试试这个函数吧。要在C语言中使用sprintf函数,就必须在源程序的开头写上#include <stdio.h>,我们也写上这句话。这样以后就可以随便使用sprintf函数了。接下来在HariMain中使用sprintf函数。

sprintf函数的使用方法是:sprintf(地址,格式,值,值,值,……)。这里的地址指定所生成字符串的存放地址。格式基本上只是单纯的字符串,如果有%d这类记号,就置换成后面的值的内容。除了%d,还有%s, %x等符号,它们用于指定数值以什么方式变换为字符串。%d将数值作为十进制数转化为字符串,%x将数值作为十六进制数转化为字符串。
-------------------------
8 显示鼠标指针(harib02h)
估计后面的开发速度会更快,那就赶紧趁着这势头再描画一下鼠标指针吧。思路跟显示字符差不多,程序并不是很难。

首先,将鼠标指针的大小定为16×16。这个定下来之后,下面就简单了。先准备16×16=256字节的内存,然后往里面写入鼠标指针的数据。我们把这个程序写在init_mouse_cursor8里。
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;
}

变量bc是指back-color,也就是背景色。

要将背景色显示出来,还需要作成下面这个函数。其实很简单,只要将buf中的数据复制到vram中去就可以了。
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的信息。他们的值分别是0xa0000和320。pxsize和pysize是想要显示的图形(picture)的大小,鼠标指针的大小是16×16,所以这两个值都是16。px0和py0指定图形在画面上的显示位置。最后的buf和bxsize分别指定图形的存放地址和每一行含有的像素数。bxsize和pxsize大体相同,但也有时候想放入不同的值,所以还是要分别指定这两个值。--?
------------------
9 GDT与IDT的初始化(harib02i)
要怎么样才能让mouse动呢?……(思考中)……有办法了!首先要将GDT和IDT初始化。不过在此之前,必须说明一下什么是GDT和IDT。

GDT也好,IDT也好,它们都是与CPU有关的设定。为了让操作系统能够使用32位模式,需要对CPU做各种设定。不过,asmhead.nas里写的程序有点偷工减料,只是随意进行了一些设定。如果这样原封不动的话,就无法做出使用鼠标指针所需要的设定,所以我们要好好重新设置一下

那为什么要在asmhead.nas里偷工减料呢?最开始就规规矩矩地设定好不行吗?……嗯,这个问题一下子就戳到痛处了。这里因为笔者希望尽可能地不用汇编语言,而用C语言来写,这样大家更容易理解。所以,asmhead.nas里尽可能少写,只做了运行bootpack.c所必需的一些设定。这次为了使用这个文件,必须再进行设定。如果大家有足够能力用汇编语言编写程序,就不用模仿笔者了,从一开始规规矩矩地做好设定更好

从现在开始,学习内容的难度要增加不小。以后要讲分段呀,中断什么的,都很难懂,很多程序员都是在这些地方受挫的。从难度上考虑,应该在20天以后讲而不是第5天。但如果现在不讲,几乎所有的装置都不能控制,做起来也没什么意思。笔者不想让大家做没有意思的操作系统。

先来讲一下分段。回想一下仅用汇编语言编程时,有一个指令叫做ORG。如果不用ORG指令明确声明程序要读入的内存地址,就不能写出正确的程序来。如果写着ORG 0x1234,但程序却没读入内存的0x1234号,可就不好办了

发生这种情况是非常麻烦的。最近的操作系统能同时运行多个程序,这一点也不稀奇。这种时候,如果内存的使用范围重叠了怎么办?这可是一件大事。必须让某个程序放弃执行,同时报出一个“因为内存地址冲突,不能执行”的错误信息。但是,这种错误大家见过吗?没有。所以,肯定有某种方法能解决这个问题。这个方法就是分段。

所谓分段,打个比方说,就是按照自己喜欢的方式,将合计4GB[插图]的内存分成很多块(block),每一块的起始地址都看作0来处理。这很方便,有了这个功能,任何程序都可以先写上一句ORG (paging),也能解决问题。不过我们目前还不讨论分页,可以暂时不考虑它。

需要注意的一点是,我们用16位的时候曾经讲解过的段寄存器。这里的分段,使用的就是这个段寄存器。但是16位的时候,如果计算地址,只要将地址乘以16就可以了。但现在已经是32位了,不能再这么用了。如果写成“MOV AL, [DS:EBX]”,CPU会往EBX里加上某个值来计算地址,这个值不是DS的16倍,而是DS所表示的段的起始地址。即使省略段寄存器(segment register)的
地址,也会自动认为是指定了DS。这个规则不管是16位模式还是32位模式,都是一样的

按这种分段方法,为了表示一个段,需要有以下信息。
--段的大小是多少
--段的起始地址在哪里
--段的管理属性(禁止写入,禁止执行,系统专用等)

CPU用8个字节(=64位)的数据来表示这些信息。但是,用于指定段的寄存器只有16位。或许有人会猜想在32位模式下,段寄存器会扩展到64位,但事实上段寄存器仍然是16位。

那该怎么办才好呢?可以模仿图像调色板的做法。也就是说,先有一个段号,存放在段寄存器里。然后预先设定好段号与段的对应关系。调色板中,色号可以使用0~255的数。段号可以用0~8191的数。因为段寄存器是16位,所以本来应该能够处理0~65535范围的数,但由于CPU设计上的原因,段寄存器的低3位不能使用。因此能够使用的段号只有13位,能够处理的就只有位于0~8191的区域了

段号怎么设定呢?这是对于CPU的设定,不需要像调色板那样使用io_out(由于不是外部设备,当然没必要)。但因为能够使用0~8191的范围,即可以定义8192个段,所以设定这么多段就需要8192×8=65536字节(64KB)。大家可能会想,CPU没那么大存储能力,不可能存储那么多数据,是不是要写入到内存中去呀。不错,正是这样。这64KB(实际上也可以比这少)的数据就称为GDT。

GDT是“global(segment)descriptor table”的缩写,意思是全局段号记录表。将这些数据整齐地排列在内存的某个地方,然后将内存的起始地址和有效设定个数放在CPU内被称作GDTR[插图]的特殊寄存器中,设定就完成了。

另外,IDT是“interrupt descriptor table”的缩写,直译过来就是“中断记录表”。当CPU遇到外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理这种突发事件。这就是中断功能。

讲了这么长,其实总结来说就是:要使用鼠标,就必须要使用中断。所以,我们必须设定IDT。IDT记录了0~255的中断号码与调用函数的对应关系,比如说发生了123号中断,就调用〇×函数,其设定方法与GDT很相似(或许是因为使用同样的方法能简化CPU的电路)。

如果段的设定还没顺利完成就设定IDT的话,会比较麻烦,所以必须先进行GDT的设定。

虽然说明很长,但程序并没那么长。本次的*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;    
}
----------
SEGMENT_DESCRIPTOR中存放GDT的8字节的内容,它无非是以CPU的资料为基础,写成了结构体的形式。同样,GATE_DESCRIPTOR中存放IDT的8字节的内容,也是以CPU的资料为基础的。

变量gdt被赋值0x00270000,就是说要将0x270000~0x27ffff设为GDT。至于为什么用这个地址,其实那只是笔者随便作出的决定,并没有特殊的意义。从内存分布图可以看出这一块地方并没有被使用

变量idt也是一样,IDT被设为了0x26f800~0x26ffff。顺便说一下,0x280000~0x2fffff已经有了bootpack.h。“哎?什么时候?我可没听说过这事哦!”大家可能会有这样的疑问,其实是后面要讲到的“asmhead.nas”帮我们做了这样的处理。

    for (i = 0; i < 8192; i++) {
        set_segmdesc(gdt + i, 0, 0, 0);
    }
请注意一下以上几行代码。gdt是0x270000, i从0开始,每次加1,直到8191。这样一来,好像gdt+i最大也只能是0x271fff。但事实上并不是那样。C语言中进行指针的加法运算时,内部还隐含着乘法运算。变量gdt已经声明为指针,指向SEGMENT_DESCRIPTOR这样一个8字节的结构体,所以往gdt里加1,结果却是地址增加了8。因此这个for语句就完成了对所有8192个段的设定,将它们的上限(limit,指段的字节数-1)、基址(base)、访问权限都设为0。

    set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
    set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
以上语句是对段号为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赋值,所以要借助汇编语言的力量,仅此而已。

在set_segmdesc和set_gatedesc中,使用了新的运算符,下面来介绍一下。首先看看语句“ar |=0x8000; ”,它是“ar = ar |0x8000; ”的省略表现形式。同样还有“limit /= 0x1000; ”,它是“limit =limit/0x1000; ”的省略表现形式。“|”是前面已经出现的或(OR)运算符。“/”是除法运算符。

“>>”是右移位运算符。比如计算00101100>>3,就得到00000101。移位时,舍弃右边溢出的位,而左边不足的3位,要补3个0。

现在haribote.sys变成多少字节了呢?哦,光字体就有4KB,增加了不少,到7632字节了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值