009-彩色的显示器

       上次讲到了引用C语言开发的原因以及如何使用C语言与之前的汇编头结合,这次,正式开始C语言的开发。

       由于我们已经指定了C语言编写的入口函数为OSMain(),那么,就从这个函数开始写吧。既然要写一个函数,我们首先得确定这个函数的参数和返回值类型。如何确定呢?那得看谁会来调用这个函数,又想从这个函数里获得点什么。由于我们是将编译好的这个程序和之前写的汇编头相拼接的,那么,调用这个函数的一定就是我们之前写的汇编指令了,更准确的来说,是一条JMP语句。但是,汇编语言之间的函数参数传递都是通过寄存器来实现的,因为对于汇编语言来说,不存在什么局部变量,所有的程序公用寄存器和内存,所有,它们之间可以随意使用,自然就可以很顺畅的传递参数。但是这一点在C语言不可以,由于C语言在编译时会给内存来划分相对应的堆区栈区,函数之间的数据传递一定要通过接口,这里的接口就体现为参数和返回值。我们在写一个纯C程序时不必考虑这些问题,因为只要提供好接口,并且合适地调用就万事大吉了,至于接口处数据如何传递,这是编译器的事。但是现在不行,现在我们的C函数是要用汇编语言来调用的,我们就必须明白这些参数到底是存放在了哪里。

       我们可以写一段简单的C程序,通过反汇编来查看里面的数据传递逻辑。通过研究我们可以知道,C语言会在函数调用的时候,将函数指针传入esp寄存器中,而在实际内存中,函数指针的后面紧跟着的就是参数的指针,由于我们在32位环境下编写代码,所有的指针都应该是32位的,也就是4字节,所以,esp+4, esp+8……就分别是第一个,第二个……参数的指针。写过变参函数的程序员都知道,变参函数中函数参数的类型是未知的,个数也是无限制的,之所以会这样就是由于C语言函数将参数依次放在了内存中,当你写一个普通函数的时候,我们通过函数指针类型可以知道这个范围到哪里,所以不允许参数越界(这完全是编译器的限制),但是当你写一个变参函数的时候,这个限制就被放开,自然也就无法确定参数个数了。那么,函数的返回值会放在哪里呢?会放在EAX中(当然,这会根据你实际声明的变量类型来决定究竟是读取EAX还是AX还是AL)。所以总结一下就是,当我们在C语言中调用一个函数表达式时,编译器要做的事情是这样的,首先,根据函数指针找到函数对应的内存位置,并且把这个地址放到esp中,其次,以esp为基址依次向后读取参数,然后执行函数主体,最后会读取EAX中的值作为函数返回值,替换函数调用在C语言表达式中的位置。这就是为什么如果你声明了函数返回值,但是在函数中没有写return语句,它就会返回一个任意值的原因,因为EAX中的值是随机的。了解了这个以后再来考虑,OSMain函数的参数和返回值问题。我们在调用这个函数的时候,将需要的信息都保存在了内存的固定位置,因此只要知道这些位置就好,暂时还不需要通过参数传递(当然,有序组织起来直接传参不妨视为一个更好的方式,目前暂时搁置,以后在优化代码的时候会重新考虑),而至于返回值,好的习惯是,在C语言主体程序结束后,给寄存器里留下一个返回值,用于表示程序是否正常运行,如果非正常退出,可以给出错误码,我们在之后还可以去进行相关处理。暂时还不需要特别关注这个错误码,先留出一个接口就好,因此决定,OSMain函数是一个int返回值的无参数函数,并且设定正常退出的返回值为0,这样,程序框架就出来了,是我们非常熟悉的形式。

int OSMain() {

    return 0;
}

       接下来的任务就是在屏幕上显示出内容来了。我们知道在进入OSMain函数之前,我们就已经开启了VGA彩色显示模式,这是一个320×200分辨率的256色。换句话说,我们的屏幕上现在有640,000个像素点,每个像素点你可以在256种颜色中选择一种来填充,由此来绘制我们的界面。在VGA模式下,显卡支持RGB配色,红、绿、蓝三种颜色的配比各有256个等级,通常情况下我们用两位十六进制表示一个颜色分量的比例。例如纯白色就表示为#FFFFFF,纯黄色就表示为#00FFFF。但是这样的话我们来算一下,所有能够显示得颜色应该有256×256×256=16,777,216种颜色,这显然已经远远超出了256种颜色,因此,我们就需要从这一千六百多万种颜色里选出256种颜色作为我们要使用的颜色。这256种颜色慢慢选,慢慢试,机械性的选美工作就让大家萝卜青菜了。这里为了把操作系统界面差不多显示出来,就只选择了16种颜色,我们先来做一个16色的操作系统,至于其他颜色,以后慢慢加就好了。

       所以,我们选择了16种配色,写出了以下basicDisplay.h文件:

#ifndef basicDisplay_h
#define basicDisplay_h

#include "asmfunc.h"
#include "global.h"

#define PORTID_PALETTE 	        ((void *)0x03c8)
#define PORTID_RGB		((void *)0x03c9)
#define VGA_MEM_END		((void *)0xafa00)

typedef struct {
	unsigned char red, green, blue;
} colorRGB;
colorRGB colorRGBMake(const char r, const char g, const char b);

#define COLOR_RGB(rgb) (colorRGBMake(rgb))
#define BLACK		0x00, 0x00, 0x00
#define RED 		0xff, 0x00, 0x00
#define GREEN		0x00, 0xff, 0x00
#define YELLOW		0xff, 0xff, 0x00
#define BLUE		0x00, 0x00, 0xff
#define PERPLE		0xff, 0x00, 0xff
#define CYAN		0x00, 0xff, 0xff
#define WHITE		0xff, 0xff, 0xff
#define GRAY		0xc6, 0xc6, 0xc6
#define DARK_RED 	0x85, 0x00, 0x00
#define DARK_GREEN	0x00, 0x85, 0x00
#define DARK_YELLOW	0x85, 0x85, 0x00
#define DARK_BLUE	0x00, 0x00, 0x85
#define DARK_PERPLE	0x85, 0x00, 0x85
#define DARK_CYAN	0x00, 0x85, 0x85
#define DARK_GREY	0x85, 0x85, 0x85

enum {
	id_black = 0,
	id_red = 1,
	id_green = 2,
	id_yellow = 3,
	id_blue = 4,
	id_perple = 5,
	id_cyan = 6,
	id_white = 7,
	id_gray = 8,
	id_darkRed = 9,
	id_darkGreen = 10,
	id_darkYellow = 11,
	id_darkBlue = 12,
	id_darkPerple = 13,
	id_darkCyan = 14,
	id_darkGray = 15
};

void setPalette();
#endif


       解释一下这个代码,4-8行暂时略过,一会会解释。第9行这里定义了一个指针常量,指向显存。在CPU的寻址空间中(目前是32位字长4GB寻址空间),绝大部分由内存条提供,但是还有一部分是由显卡来提供的,这部分RAM的作用就是用于存储关于显示的数据,就叫做显存。如果你使用独立显卡,那这部分存储空间就在你的显卡上,这时对于CPU来说,虽然你有内存和显存这两个物理起见,但由于它们都被接在了CPU的直接寻址线上,因此对于CPU来说它们是一样的。(这里我们考虑的是两种物理设备的情况,而在真实的计算机设备上,通常不只这两种,CPU还会将一根寻址线接到硬盘,通过虚拟内存技术将外存的一部分作为内存来使用,从而提升内存空间,缓解内存压力)这也就是当年量产了4GB内存条以后,大家都吵着浪费内存空间的原因,因为CPU要去访问显存,自然就要有一部分内存永远无法被访问到了,大致上只能用到3.2GB-3.6GB左右。而如果你使用的是集成显卡,那么显存就得让内存来充当了,这时候虽然你可以访问4GB的内存空间,但是你就必须划分出来一部分内存空间来作为显存使用。但是无论你使用哪种具体的物理设备作为显存,它都在CPU的寻址空间中,因此我们在编写程序的时候就不需要考虑这个问题了,我们只需要知道显存的地址是多少就可以了。在早期黑白显示的时候,对于屏幕上每一个像素只能有两种状态,亮或者灭,所以一个像素只需要一个二进制位的显存就可以了,这个时候一个地址的显存会对应屏幕上8个像素点的亮暗信息。(这里顺带一提,计算机刚加电的时候,屏幕会被初始化为80×25的文本模式,利用320KB的显存可以显示2000个字符。)不过我们由于进入了VGA模式,每一个像素点都需要表示一个8位色,所以,一个像素点就需要对应8位,也就是1字节的显存。那么我们有640,000个像素点,所以0xafa00-0xbf400的地址空间就是我们的显存了,在每一个像素里添一个颜色代号就可以显示对应的颜色了。

      11-14行定义了RGB色结构体,并声明了与之配套的结构体构造函数。16-32行用于配合RGB构造函数,定义了16种颜色。34-51行分别定义了这些颜色的代号。53行定义的函数,接下来就要重点进行说明。下面给出basicDisplay.c的代码:

#include "basicDIsplay.h"

colorRGB colorRGBMake(const char r, const char g, const char b) {
	colorRGB temp;
	temp.red = r;
	temp.green = g;
	temp.blue = b;
	return temp;
}

void setColor(const int colorId, const colorRGB color) {
	outportB(PORTID_PALETTE, colorId);
	outportB(PORTID_RGB, color.red / 4);
	outportB(PORTID_RGB, color.green / 4);
	outportB(PORTID_RGB, color.blue / 4);
}

void setPalette() {
	int nEflags = loadEflags();
	cleanInterrupt();
	setColor(0, COLOR_RGB(BLACK));
	setColor(1, COLOR_RGB(RED));
	setColor(2, COLOR_RGB(GREEN));
	setColor(3, COLOR_RGB(YELLOW));
	setColor(4, COLOR_RGB(BLUE));
	setColor(5, COLOR_RGB(PERPLE));
	setColor(6, COLOR_RGB(CYAN));
	setColor(7, COLOR_RGB(WHITE));
	setColor(8, COLOR_RGB(GRAY));
	setColor(9, COLOR_RGB(DARK_RED));
	setColor(10, COLOR_RGB(DARK_GREEN));
	setColor(11, COLOR_RGB(DARK_YELLOW));
	setColor(12, COLOR_RGB(DARK_BLUE));
	setColor(13, COLOR_RGB(DARK_PERPLE));
	setColor(14, COLOR_RGB(DARK_CYAN));
	setColor(15, COLOR_RGB(DARK_GREY));
	storeEflags(nEflags);
	setInterrupt();
}

      
       3-9行用于创建结构体变量创建的构造函数,不再解释。重点解释11-16行的setColor()函数。我们刚才说到,需要在很多颜色中选出256中颜色,对于我们这里来说,就是要选出16中颜色,给它表上0-15号,这样,你给显存里存放0-15中的数字的时候,显卡就能知道到底把哪个颜色显示出去了。那如此说来,就应该有一个“东西”,能够让我们写一个表格(就有点类似于GDT和IDT那样),里面要放着色号和颜色的对应关系,我们把这个“东西”就叫做调色盘。那么究竟什么是调色盘,它其实是显卡上的一个部件,类似于寄存器又类似于内存,这么纠结是因为它的存储空间要比寄存器大得多,但是性质上来说又很像寄存器。具体的电子芯片结构的问题在这里就没有讨论的意义的,总之这是显卡中的一部分。那么既然它不再内存中,我们也就不能通过寻址的方式来访问里面的数据了,因为它并不在CPU的寻址空间当中。那么如何访问到它呢?CPU要想访问内存之外的东西,就必须通过I/O总线,这是CPU与外界输入输出设备连接的接口,在I/O总线上的每个设备都有一个自己的编号,CPU正是通过这个设备编号找到对应的设备的。setColor()函数的任务就是向调色盘对应的编号发送数据。向0x03c8发送数据表示将要设定的颜色编号,然后再向0x03c9发送三次数据来分辨表示R、G、B的分量。这样就设置好了一种颜色。为什么要除以4呢?这是因为,虽然RGB用了6位十六进制表示一个颜色,照理说应该能够表示16,777,216中颜色,但是其实VGA显卡还支持不了这么多中颜色,而是每种颜色分量只支持64种,也就是一共64×64×64=262,144种颜色,那么这与真正的RGB色号的对应关系是什么呢?就是每4个一跳,也就是说(0x00, 0x00, 0x00)表示#000000,(0x01, 0x01, 0x01)表示#040404, (0x02, 0x03, 0x40)表示#080CFF,以此类推,所以,我们把RGB的颜色分量除以4就可以得到实际的编码。21-36行就是通过调用16次setClolor()函数来完成16种颜色的编选。

       接下来介绍刚才没有介绍到的代码,比如19,20,37,38行代码的含义以及outportB函数的由来。这些莫名其妙出现的函数是哪里来的呢?这里需要注意的是,由于我们在编写非常底层的操作系统代码,这是没有任何集成开发环境给我们使用的,更不会有什么函数库给我们用,所有的一切都是我们自己写上去的。所谓C语言编程,也不过是仅仅使用了C语言语法特性而已,那些我们所熟悉的标准C语言库是一概没有的,因为谁也不会知道我们即将开发出的操作系统是什么样的,自然也就不会有与之相匹配的库文件了。那么,这些函数并不是库函数,它一定是我们自己来实现的。outportB函数是想给I/O设备发送信息,这种功能C语言是做不到的,所以,得靠外部力量来实现。这个功能用汇编语言来写非常容易,一句OUT指令就可以搞定,所以,我们创建了asmfunc.asm文件,专门用于写C语言无法实现的函数的函数体。

[format "WCOFF"]		; 由于该文件编译后还需要链接,所以声明文件格式
[instrset "i486p"]
[bits 32]				; 生成32位模式机器码

[file "asmfunc.asm"]	; 文件名
	global _halt		; 包含的函数
	global _cleanInterrupt, _setInterrupt
	global _inportB, _inportW, _inportD
	global _outportB, _outportW, _outportD
	global _loadEflags, _storeEflags
	global _loadGDTR, _loadIDTR
	global _preInterruptHandler21, _preInterruptHandler2c, _preInterruptHandler27
	extern _interruptHandler21, _interruptHandler2c, _interruptHandler27
	[section .text]			; 实际的函数
_halt:
	hlt
	ret

_cleanInterrupt:
	cli
	ret
	
_setInterrupt:
	sti
	ret
	
_inportB:
	mov edx, [esp + 4]
	mov eax, 0
	in al, dx
	ret
	
_inportW:
	mov edx, [esp + 4]
	mov eax, 0
	in ax, dx
	ret
	
_inportD:
	mov edx, [esp + 4]
	mov eax, 0
	in eax, dx
	ret
	
_outportB:
	mov edx, [esp + 4]
	mov al, [esp + 8]
	out dx, al
	ret
	
_outportW:
	mov edx, [esp + 4]
	mov ax, [esp + 8]
	out dx, ax
	ret
	
_outportD:
	mov edx, [esp + 4]
	mov eax, [esp + 8]
	out dx, eax
	ret
	
_loadEflags:
	pushfd
	pop eax
	ret
	
_storeEflags:
	mov eax, [esp + 4]
	push eax
	popfd
	ret
	
_loadGDTR:
	mov ax, [esp + 4]
	mov [esp + 6], ax
	lgdt [esp + 6]
;	mov eax, [esp + 8]
;	mov [esp + 6], eax
;	lgdt [esp + 4]
	ret
	
_loadIDTR:
	mov ax, [esp + 4]
	mov [esp + 6], ax
	lidt [esp + 6]
	ret
	
_preInterruptHandler21:
	push es
	push ds
	pushad
	mov eax, esp
	push eax
	mov ax, ss
	mov ds, ax
	mov es, ax
	call _interruptHandler21
	pop eax
	popad
	pop ds
	pop es
	iretd
	
_preInterruptHandler2c:
	push es
	push ds
	pushad
	mov eax, esp
	push eax
	mov ax, ss
	mov ds, ax
	mov es, ax
	call _interruptHandler2c
	pop eax
	popad
	pop ds
	pop es
	iretd
	
_preInterruptHandler27:
	push es
	push ds
	pushad
	mov eax, esp
	push eax
	mov ax, ss
	mov ds, ax
	mov es, ax
	call _interruptHandler27
	pop eax
	popad
	pop ds
	pop es
	iretd

       现在给出的这个文件里有很多函数是以后要用的,先给出,以后还会来讲解。这里主要讲解outportB函数。46行接收传来的第一个参数,放入edx中,显然这应该是32位的数据,47行接收传来的第二个参数,放入al中,显然这应该是一个8位的数据。在48行执行out指令,edx中的数据作为设备编号,al中的数据作为发送的数据。如此看来,给这个函数传递的参数,应该是一个指针(用于指向设备),还有一个数据(用于向设备发送),函数名中的B的意思就是发送的这个数据应该是一个字节的。由于最后没有向eax里保存什么数据,所以说明这个函数不需要返回值。那么我们由此就可以写出它在C语言中的函数声明:

void outportB(void *pPort, char data);

       这样就解释清楚了outportB()函数的来历。下面给出完整的asmfunc.h文件:

#ifndef asmfunc_h
#define asmfunc_h

void halt();

void cleanInterrupt();
void setInterrupt();

char inportB(void *pPort);
short inportW(void *pPort);
long inportD(void *pPort);

void outportB(void *pPort, char data);
void outportW(void *pPort, short data);
void outPortD(void *pPort, long data);

long loadEflags();
void storeEflags(long Eflag);

void loadGDTR(long limit, void *addr);
void loadIDTR(long limit, void *addr);

void preInterruptHandler21();
void preInterruptHandler2c();
void preInterruptHandler27();

#endif

       这样我们也能参照看出loadEflags(),storeEflags(nEflags),cleanInterrupt()和setInterrupt()函数的实现。这4个函数又是干什么的呢?之前介绍过CPU的中断机制,中断信号在什么时候发过来是不知道的,在接收到中断指令后CPU会停下手中的活转去执行中断,但是,就像之前进行模式转换时候那样,如果在某些重要步骤中发生了中断,是不能够直接去处理的,否则将会发生严重错误,我们把这样的操作叫做原子操作,意思就是这些操作不可分,你必须一次性处理完毕,不能因为中断就把它拆开处理。设定调色盘的过程也是原子操作,所以需要在之前关闭中断,之后再开启中断。cleanInterrupt()其实就是执行了CTI,setInterrupt()其实就是执行了STI,而另外那两个函数的目的就在于,关闭中断前保存一些数据,开启中断后再恢复,因为有些中断发出的数据会在原子操作中被改掉,等再去执行中断的时候就会发生错误,为了保险起见,我们把当前的状态先保存下来,等恢复中断以后再恢复它。

       这样,我们只需要在OSMain()函数中调用initPalette()函数就可以完成调色盘的初始化,接下来的任务就是给显存里写东西了,但是如果你每一个像素点都专门写一遍的话,那真的就类似了,所以我们略微封装一下用于显示各种界面的函数,比如说最容易想到的就是画出一个矩形了。
       要想画出一个矩形,我们应该了解矩形的位置以及大小。那么,既然我们的研究单位是屏幕上的每一个像素点,所以,我们这里的座标也好,长度也好,都是以像素为单位。下面给出用于绘制图形的代码draw.h:

#ifndef draw_h
#define draw_h

#include "basicDisplay.h"

#define SCR_CENTER (posMake(bootInfo -> scr_x / 2, bootInfo -> scr_y / 2))

typedef struct {
	unsigned x, y;
} Position;
Position posMake(const unsigned x, const unsigned y);

typedef struct {
	unsigned x, y, height, width;
} Rect;
Rect rectMake(const unsigned x, const unsigned y, const unsigned width, const unsigned height);

void drawRect(Rect rect, unsigned char colorId);
#endif

        两个结构体分别用于定义位置和矩形,当然有与之配套的结构体构造函数。而16行的函数用于绘制矩形,下面给出其实现文件draw.c:

#include "draw.h"

Position posMake(const unsigned x, const unsigned y) {
	Position temp;
	temp.x = x;
	temp.y = y;
	return temp;
}

Rect rectMake(const unsigned x, const unsigned y, const unsigned width, const unsigned height) {
	Rect temp;
	temp.x = x;
	temp.y = y;
	temp.height = height;
	temp.width = width;
	return temp;
}

void drawRect(Rect rect, unsigned char colorId) {
	int x, y;
	for (x = rect.x; x <= rect.x + rect.width; x++) {
		for (y = rect.y; y < rect.y + rect.height; y++) {
			int position = y * bootInfo -> scr_x + x;
			*(unsigned char *)(bootInfo -> vga_mem_strat + position) = colorId;
		}
	}
}


       两个结构体构造函数不再解释,62-71行的函数就是根据rect的信息,计算出需要更改的左边点,然后把与之对应的颜色信息写到相应的显存中,非常简单。有了这个工具以后,我们就可以绘制出一个差不多的图形了,下面是initDesktop.h

#ifndef initDesktop_h
#define initDesktop_h

#include "basicDisplay.h"
#include "draw.h"

void initDesktop();

#endif


       一下是initDesktop.c:

#include "initDesktop.h"

void initDesktop() {
	setPalette();
	drawRect(rectMake(0, 0, bootInfo -> scr_x, bootInfo -> scr_y - 29), id_darkCyan);
	drawRect(rectMake(0, bootInfo -> scr_y - 28, bootInfo -> scr_x, 1), id_gray);
	drawRect(rectMake(0, bootInfo -> scr_y - 28, 1, 28), id_gray);
	drawRect(rectMake(0, bootInfo -> scr_y - 1, bootInfo -> scr_x, 1), id_gray);
	drawRect(rectMake(bootInfo -> scr_x - 2, bootInfo -> scr_y - 28, 1, 28), id_gray);
	drawRect(rectMake(1, bootInfo -> scr_y - 27, bootInfo -> scr_x - 3, 26), id_darkGray);
}


       其实就是绘制了一个看起来比较像桌面的界面而已,不再过多解释。最后再OSMain()里调用initDesktop()就大功告成。附上效果图:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章