30天自制操作系统
上QQ阅读APP看书,第一时间看更新

6 色号设定(harib01f)

好了,到现在为止我们的话题都是以C语言为中心的,但我们的目的不是为了掌握C语言,而是为了制作操作系统,操作系统中是不需要条纹图案之类的。我们继续来做操作系统吧。

可能大家马上就想描绘一个操作系统模样的画面,但在此之前要先做一件事,那就是处理颜色问题。这次使用的是320× 200的8位颜色模式,色号使用8位(二进制)数,也就是只能使用0~255的数。我想熟悉电脑颜色的人都会知道,这是非常少的。一般说起指定颜色,都是用#ffffff一类的数。这就是RGB(红绿蓝)方式,用6位十六进制数,也就是24位(二进制)来指定颜色。8位数完全不够。那么,该怎么指定#ffffff方式的颜色呢?

这个8位彩色模式,是由程序员随意指定0~255的数字所对应的颜色的。比如说25号颜色对应#ffffff,26号颜色对应#123456等。这种方式就叫做调色板(palette)。

如果像现在这样,程序员不做任何设定,0号颜色就是#000000,15号颜色就是#ffffff。其他号码的颜色,笔者也不是很清楚,所以可以按照自己的喜好来设定并使用。

笔者通过制作OSAKA知道:要想描绘一个操作系统模样的画面,只要有以下这16种颜色就足够了。所以这次我们也使用这16种颜色,并给它们编上号码0-15。

#000000:黑 #00ffff:浅亮蓝 #000084:暗蓝
#ff0000:亮红 #ffffff:白 #840084:暗紫
#00ff00:亮绿 #c6c6c6:亮灰 #008484:浅暗蓝
#ffff00:亮黄 #840000:暗红 #848484:暗灰
#0000ff:亮蓝 #008400:暗绿
#ff00ff:亮紫 #848400:暗黄

所以我们要给bootpack.c添加很多代码。

■■■■■

本次的bootpack.c

void io_hlt(void);
void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags);

/*就算写在同一个源文件里,如果想在定义前使用,还是必须事先声明一下。*/

void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);

void HariMain(void)
{
    int i; /* 声明变量。变量i是32位整数型 */
    char *p; /* 变量p是BYTE [...]用的地址 */

    init_palette(); /* 设定调色板 */

    p = (char *) 0xa0000; /* 指定地址 */

    for (i = 0; i <= 0xffff; i++) {
        p[i] = i & 0x0f;
    }

    for (; ; ) {
        io_hlt();
    }
}

void init_palette(void)
{
    static unsigned char table_rgb[16 * 3] = {
        0x00, 0x00, 0x00,   /*  0:黑 */
        0xff, 0x00, 0x00, /* 1:亮红 */
        0x00, 0xff, 0x00, /* 2:亮绿 */
        0xff, 0xff, 0x00, /* 3:亮黄 */
        0x00, 0x00, 0xff, /* 4:亮蓝 */
        0xff, 0x00, 0xff, /* 5:亮紫 */
        0x00, 0xff, 0xff, /* 6:浅亮蓝 */
        0xff, 0xff, 0xff, /* 7:白 */
        0xc6, 0xc6, 0xc6, /* 8:亮灰 */
        0x84, 0x00, 0x00, /* 9:暗红 */
        0x00, 0x84, 0x00, /* 10:暗绿 */
        0x84, 0x84, 0x00, /* 11:暗黄 */
        0x00, 0x00, 0x84, /* 12:暗青 */
        0x84, 0x00, 0x84, /* 13:暗紫 */
        0x00, 0x84, 0x84, /* 14:浅暗蓝 */
        0x84, 0x84, 0x84 /* 15:暗灰 */
    };
    set_palette(0, 15, table_rgb);
    return;

    /* C语言中的static char语句只能用于数据,相当于汇编中的DB指令 */
}

void set_palette(int start, int end, unsigned char *rgb)
{
    int i, eflags;
    eflags = io_load_eflags();  /* 记录中断许可标志的值*/
    io_cli();                      /* 将中断许可标志置为0,禁止中断 */
    io_out8(0x03c8, start);
    for (i = start; i <= end; i++) {
        io_out8(0x03c9, rgb[0] / 4);
        io_out8(0x03c9, rgb[1] / 4);
        io_out8(0x03c9, rgb[2] / 4);
        rgb += 3;
    }
    io_store_eflags(eflags);     /* 复原中断许可标志 */
    return;
}

程序的头部罗列了很多的外部函数名,这些函数必须在naskfunc.nas中写。这有点麻烦,但也没办法。先跳过这一部分,我们来看看主函数HariMain。函数里只是增加了一行调用调色板置置的函数,变更并不是太大。我们接着往下看。

■■■■■

函数init_palette开头一段以static开始的语句,虽然很长,但结果无非就是声明了一个常数table_rgb。它太长了,有些晦涩难懂,所以我们来简化一下。

void init_palette(void)
{
    table_rgb的声明;
    set_palette(0, 15, table_rgb);
    return;
}

简而言之,就是这些内容。除了声明之外没什么难点,所以我们仅仅解说声明部分。

char a[3];

C语言中,如果这样写,那么a就成为了常数,以汇编的语言来讲就是标志符。标志符的值当然就意味着地址。并且还准备了“RESB 3”。总结一下,上面的叙述就相当于汇编里的这个语句:

a:
    RESB 3

nask中RESB的内容能够保证是0,但C语言中不能保证所以里面说不定含有某种垃圾数据。

■■■■■

另外,在这个声明的后面加上 “= { … }”,还可以写上数据的初始值。比如:

char a[3]= { 1,2,3 };

这与下面的内容基本等价。

char a[3];
a[0] = 1;
a[1] = 2;
a[2] = 3;

这里,a是表示最初地址的数字,也就是说它被认为是指针。

那么这次,应该代入的值共有16× 3=48个。笔者不希望大家做如此多的赋值语句。每次赋值都至少要消耗3个字节,这样算下来光这些赋值语句就要花费将近150字节,这太不值了。

其实写成下面这样一般的DB形式,不就挺好吗。

table_rgb:
    DB 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, …

只要48字节就够了。所以说,就像在汇编语言中用DB指令代替RESB指令那样,在C语言中也有类似的指示方法,那就是在声明时加上static。这次我们也加上它。

下面来看unsigned。它的意思是:这里所处理的数据是BYTE(char)型,但它是没有符号(sign)的数(0或者正整数)。

char型的变量有3种模式,分别是signed型、unsigned型和未指定型。signed型用于处理-128~127的整数。它虽然也能处理负数,扩大了处理范围,很方便,但能够处理的最大值却减小了一半。unsigned型能够处理0~255的整数。未指定型是指没有特别指定时,可由编译器决定是unsigned还是signed。

在这个程序里,多次出现了0xff这个数值,也就是255,我们想用它来表示最大亮度,如果它被误解成负数(0xff会被误解成-1)就麻烦了。虽然我们不清楚亮度比0还弱会是什么概念,但无论如何不能产生这种误解。所以我们决定将这个数设定为unsigned。顺便提一句,int和short也分signed和unsigned。……好了,关于init_palette的说明就到此为止。

■■■■■

下面要讲的是C语言说明部分最后的函数set_palette。这个函数虽然很短,干的事儿可不少。首先让我们仔细看看以下精简之后的记述吧。

void set_palette(int start, int end, unsigned char *rgb)
{
    int i;
    io_out8(0x03c8, start);
    for (i = start; i <= end; i++) {
        io_out8(0x03c9, rgb[0] / 4);
        io_out8(0x03c9, rgb[1] / 4);
        io_out8(0x03c9, rgb[2] / 4);
        rgb += 3;
    }
    return;
}

程序被如此精简后还可以正确运行。其实可以在一开始就介绍这个程序,但由于想给大家介绍精简之前的正确方法,所以才写了那么长。这个先放一边,我们来说说精简的程序吧。

这个程序所做的事情,仅仅是多次调用io_out8。函数io_out8是干什么的呢?以后在naskfunc.nas中还要详细说明,现在大家只要知道它是往指定装置里传送数据的函数就行了。

■■■■■

我们前面已经说过,CPU的管脚与内存相连。如果仅仅是与内存相连,CPU就只能完成计算和存储的功能。但实际上,CPU还要对键盘的输入有响应,要通过网卡从网络取得信息,通过声卡发送音乐数据,向软盘写入信息等。这些都是设备(device),它们当然也都要连接到CPU上。

既然CPU与设备相连,那么就有向这些设备发送电信号,或者从这些设备取得信息的指令。向设备发送电信号的是OUT指令;从设备取得电气信号的是IN指令。正如为了区别不同的内存要使用内存地址一样,在OUT指令和IN指令中,为了区别不同的设备,也要使用设备号码。设备号码在英文中称为port(端口)。port原意为“港口”,这里形象地将CPU与各个设备交换电信号的行为比作了船舶的出港和进港。

所以,我们执行OUT指令时,出港信号就要挥泪告别CPU了。这就好像它在说:“妈妈,我要走了。我在显卡中,会很好的,不用担心。”我想不用说大家也会感觉得到,在C语言中,没有与IN或OUT指令相当的语句,所以我们只好拿汇编语言来做了。唉,汇编真是关键时刻显身手的语言呀。

■■■■■

如果我们读一读程序的话,就会发现突然蹦出了0x03c8、0x03c9之类的设备号码,这些设备号码到底是如何获得的呢?随意写几个数字行不行呢?这些号码当然不是能随便乱写的。否则,别的什么设备胡乱动作一下,会带来很严重的问题。所以事先必须仔细调查。笔者自己制作了参考网页。

网页的叙述太长了,不好意思(注:这一页也是笔者写的)。网页中有一个项目,叫做“video DA converter”,其中有以下记述。

❏ 调色板的访问步骤。

❏ 首先在一连串的访问中屏蔽中断(比如CLI)。

❏ 将想要设定的调色板号码写入0x03c8,紧接着,按R, G, B的顺序写入0x03c9。如果还想继续设定下一个调色板,则省略调色板号码,再按照RGB的顺序写入0x03c9就行了。

❏ 如果想要读出当前调色板的状态,首先要将调色板的号码写入0x03c7,再从0x03c9读取3次。读出的顺序就是R, G, B。如果要继续读出下一个调色板,同样也是省略调色板号码的设定,按RGB的顺序读出。

❏ 如果最初执行了CLI,那么最后要执行STI。

我们的程序在很大程度上参考了以上内容。

■■■■■

到这里,该说明的部分都说明得差不多了。总结一下就是:

void set_palette(int start, int end, unsigned char *rgb)
{
    int i, eflags;
    eflags = io_load_eflags();  /* 记录中断许可标志的值 */
    io_cli();                      /* 将许可标志置为0,禁止中断 */
    已经说明的部分
    io_store_eflags(eflags);     /* 恢复许可标志的值 */
    return;
}

在“调色板的访问步骤”的记述中,还写着CLI、STI什么的。下面来看看它们可以做些什么。

首先是CLI和STI。所谓CLI,是将中断标志(interrupt flag)置为0的指令(clear interrupt flag)。STI是要将这个中断标志置为1的指令(set interrupt flag)。而标志,是指像以前曾出现过的进位标志一样的各种标志,也就是说在CPU中有多种多样的标志。更改中断标志有什么好处呢?正如其名所示,它与CPU的中断处理有关系。当CPU遇到中断请求时,是立即处理中断请求(中断标志为1),还是忽略中断请求(中断标志为0),就由这个中断标志位来设定。

那到底什么是中断呢?大家可能会有这种疑问,可如果现在来讲这个问题的话,就与我们“描绘一个操作系统模样的画面”这个主题渐行渐远了,所以等以后有机会再讲吧。

■■■■■

下面再来介绍一下EFLAGS这一特别的寄存器。这是由名为FLAGS的16位寄存器扩展而来的32位寄存器。FLAGS是存储进位标志和中断标志等标志的寄存器。进位标志可以通过JC或JNC等跳转指令来简单地判断到底是0还是1。但对于中断标志,没有类似的JI或JNI命令,所以只能读入EFLAGS,再检查第9位是0还是1。顺便说一下,进位标志是EFLAGS的第0位。

空白位没有特殊意义(或许留给将来的CPU用?)

set_palette中想要做的事情是在设定调色板之前首先执行CLI,但处理结束以后一定要恢复中断标志,因此需要记住最开始的中断标志是什么。所以我们制作了一个函数io_load_eflags,读取最初的eflags值。处理结束以后,可以先看看eflags的内容,再决定是否执行STI,但仔细想一想,也没必要搞得那么复杂,干脆将eflags的值代入EFLAGS,中断标志位就恢复为原来的值了。函数io_store_eflags就是完成这个处理的。

估计不说大家也知道了,CLI也好,STI也好,EFLAGS的读取也好,EFLAGS的写入也好,都不能用C语言来完成。所以我们就努力一下,用汇编语言来写吧。

■■■■■

我们已经解释了bootpack.c程序,那么现在就来说说naskfunc.nas。

; naskfunc
; TAB=4
[FORMAT "WCOFF"]                  ; 制作目标文件的模式
[INSTRSET "i486p"]                ; 使用到486为止的指令
[BITS 32]                          ; 制作32位模式用的机器语言
[FILE "naskfunc.nas"]             ; 源程序文件名
        GLOBAL  _io_hlt, _io_cli, _io_sti, io_stihlt
        GLOBAL  _io_in8,  _io_in16,  _io_in32
        GLOBAL  _io_out8, _io_out16, _io_out32
        GLOBAL  _io_load_eflags, _io_store_eflags
[SECTION .text]
_io_hlt:     ; void io_hlt(void);
        HLT
        RET
_io_cli:     ; void io_cli(void);
        CLI
        RET
_io_sti:     ; void io_sti(void);
        STI
        RET
_io_stihlt: ; void io_stihlt(void);
        STI
        HLT
        RET
_io_in8:     ; int io_in8(int port);
        MOV      EDX, [ESP+4]      ; port
        MOV      EAX,0
        IN       AL, DX
        RET
_io_in16:   ; int io_in16(int port);

        MOV      EDX, [ESP+4]      ; port
        MOV      EAX,0
        IN       AX, DX
        RET
_io_in32:   ; int io_in32(int port);
        MOV      EDX, [ESP+4]      ; port
        IN       EAX, DX
        RET
_io_out8:   ; void io_out8(int port, int data);
        MOV      EDX, [ESP+4]      ; port
        MOV      AL, [ESP+8]       ; data
        OUT      DX, AL
        RET
_io_out16:  ; void io_out16(int port, int data);
        MOV      EDX, [ESP+4]      ; port
        MOV      EAX, [ESP+8]      ; data
        OUT      DX, AX
        RET
_io_out32:  ; void io_out32(int port, int data);
        MOV      EDX, [ESP+4]      ; port
        MOV      EAX, [ESP+8]      ; data
        OUT      DX, EAX
        RET
_io_load_eflags:     ; int io_load_eflags(void);
        PUSHFD       ; 指PUSH EFLAGS
        POP      EAX
        RET
_io_store_eflags:   ; void io_store_eflags(int eflags);
        MOV      EAX, [ESP+4]
        PUSH EAX
        POPFD ; 指POP EFLAGS
        RET

到现在为止的说明,想必大家都已经懂了,尚且需要说明的只有与EFLAGS相关的部分了。如果有“MOV EAX, EFLAGS”之类的指令就简单了,但CPU没有这种指令。能够用来读写EFLAGS的,只有PUSHFD和POPFD指令。

■■■■■

PUSHFD是“push flags double-word”的缩写,意思是将标志位的值按双字长压入栈。其实它所做的,无非就是“PUSH EFLAGS”。POPFD是“pop flags double-word”的缩写,意思是按双字长将标志位从栈弹出。它所做的,就是“POP EFLAGS”。

栈是数据结构的一种,大家暂时只要理解到这个程度就够了。往栈登录数据的动作称为push(推),请想象一下往烤箱里放面包的情景。从栈里取出数据的动作称为pop(弹出)。

也就是说,“PUSHFD POP EAX”,是指首先将EFLAGS压入栈,再将弹出的值代入EAX。所以说它代替了“MOV EAX, EFLAGS”。另一方面,PUSH EAX POPFD正与此相反,它相当于“MOV EFLAGS, EAX”。

■■■■■

最后要讲的是io_load_eflags。它对我们而言,是第一个有返回值的函数的例子,但根据C语言的规约,执行RET语句时,EAX中的值就被看作是函数的返回值,所以这样就可以。

另外,虽然还有几个函数是不必要的,但因为将来会用到,所以这里就顺便做了。虽然不知道什么时候用,用于什么目的,但通过到目前为止的讲解也能明白其中的意义。

好了,讲解完了以后执行一下吧。运行“make run”。条纹的图案没有变化,但颜色变了!成功了!