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”。条纹的图案没有变化,但颜色变了!成功了!