30天自制操作系统(第4-6天)

4C语言与画面显示的练习

4.1 用C语言实现内存写入

_write_mem8:         ; void write_mem8(int addr, int data);
    MOV ECX,[ESP+4]  ; [ESP + 4]中存放的是地址,将其读入ECX
    MOV AL,[ESP+8]   ; [ESP + 8]中存放的是数据,将其读入AL
    MOV [ECX],AL
    RET

由于函数void write_mem8(int addr, int data);的输入项为addr和data,且该两项数据类型为int,占4个字节,所以使用32位寄存器进行存储。将addr读到ECX寄存器,data的低八位读到AL寄存器(AL寄存器占8位,即一个字节),并将AL寄存器中的数据赋值给ECX寄存器中的地址处,总结该函数的功能就是将data低8位数据写入addr地址。在3.8节中提到,VRAM的地址为0xa0000~0xaffff的64KB,所以下面函数的功能就是VRAM全部写入了15,而颜色种类也是2^8=256种,这可能就是为何要取低8位的原因。

void HariMain(void)
{
    int i; /*变量声明:i是一个32位整数*/
    for (i = 0xa0000; i <= 0xaffff; i++) {
        write_mem8(i, 15); /* 15=0x00001111,即地址0xa0000~0xaffff范围内均赋值15 */
    }
    for (;;) {
        io_hlt();
    }
}

4.2 条纹图案

条纹图案是指变更VRAM中地址的元素,按照某种规律进行变换。由于之前窗口设定的长度为320像素,考虑到后续程序中的颜色共有16种,所以某行的像素分别为(00,01,02,03,04,05,06,07,08,09,0A,0B,0C,0D,0E,0F,00,....)。该处用到了C语言中的 ‘&’ 操作,在位运算中经常遇到,这里就不作详细介绍。

修改前:write_mem8(i, 15); /* 15=0x00001111,即地址0xa0000~0xaffff范围内均赋值15 */

修改后:write_mem8(i, i & 0x0f); /* 地址0xa0000~0xaffff范围内均赋值(i & 0x0f) */

4.3 挑战指针

把指针说成地址变量,更好理解。

void HariMain(void)
{
    int i; /*变量声明。变量i是32位整数*/
    char *p; /*变量p,用于BYTE型地址*/
    for (i = 0xa0000; i <= 0xaffff; i++) {
        p = i; /* 代入地址,假设p=i=0xa1234 */
        *p = i & 0x0f;/* 向0xa1234处地址(VRAM中的某一处地址)写入数值(i & 0x0f) */
        /*这可以替代write_mem8(i, i & 0x0f);*/
    }
    for (;;) {
        io_hlt();
    }
}

4.4 指针的应用

p = (char *) 0xa0000; /*给地址变量赋值*/
for (i = 0; i <= 0xffff; i++) {
    /* 下述两种说法相同,均是将地址0xa0000向前偏移i位后赋值(i & 0x0f)
    与(*p+i)有本质区别,前者是地址的前移,后者是加减p地址上的值 */
    *(p + i) = i & 0x0f;
    // p[i] = i & 0x0f;
}

4.5 色号设定(代码中为何rgb元素要除4)

/* 初始化调色板 */
void init_palette(void){/*RGB(红绿蓝)方式,用6位十六进制数,也就是24位指定颜色*/
	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;
}
/* 设置调色板:将rgb数组中的数据写入到start~end调色板中(参考调色板访问步骤) */
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);/* 哪位大佬能够解答一下该处为何要除4 */
		io_out8(0x03c9,rgb[1]/4);
		io_out8(0x03c9,rgb[2]/4);
		rgb+=3;
	}
	io_store_eflags(eflags);		/* 恢复许可标志的值 */
	return;
}
void io_out8(int port, int data);
;port=0x03c9,data=rgb[0]/4=0xff/4=0x3f;   第一个数字port的存放地址:[ESP + 4],
;第二个数字data的存放地址:[ESP + 8]
_io_out8:					; void io_out8(int port,int data);
		MOV		EDX,[ESP+4]	; port端口 	EDX=0x03c9
		MOV		AL,[ESP+8]	; 数据		AL =0x3f
		OUT		DX,AL		; 将数据0x3f写入DX端口0x03c9
		RET

调色板的访问步骤:

1、首先在一连串的访问中屏蔽中断(比如 CLI )。
2、将想要设定的调色板号码写入 0x03c8 ,紧接着,按 R G B的顺序写入 0x03c9 。如果还想继续设定下一个调色板,则省略调色板号码,再按照 RGB的 顺序写入 0x03c9 就行了。
3、如果想要读出当前调色板的状态,首先要将调色板的号码写入 0x03c8,再从 0x03c9 读取 3 次。读出的顺序就是 R、 G B。如果要继续读出下一个调色板,同样也是省略调色板号码的设定,按 RGB 的顺序读出。
4、如果最初执行了 CLI ,那么最后要执行 STI

4.6 绘制矩形

boxfill8(p, 320, COL8_FF0000, 20, 20, 120, 120);
boxfill8(p, 320, COL8_00FF00, 70, 50, 170, 150);
boxfill8(p, 320, COL8_0000FF, 120, 80, 220, 180);

函数void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);  输入值:vram为VRAM位置,xsize为屏幕长度像素(在3.8节设置过,为320),c为颜色,坐标(x0,y0)和(x1,y1)。该函数功能为将坐标(x0,y0)和(x1,y1)围成的长方形内的像素颜色变成c。

4.7 今天的成果

/* 绘制320*200内的像素屏幕框架 */
void init_screen(char *vram, int xsize, int ysize){
	boxfill8(vram, xsize, COL8_008484,  0, 			0, xsize - 1, ysize - 29);// 矩形
	boxfill8(vram, xsize, COL8_C6C6C6,  0, ysize - 28, xsize - 1, ysize - 28);// 横长条
	boxfill8(vram, xsize, COL8_FFFFFF,  0, ysize - 27, xsize - 1, ysize - 27);// 横长条
	boxfill8(vram, xsize, COL8_C6C6C6,  0, ysize - 26, xsize - 1, ysize -  1);// 矩形
	
	boxfill8(vram, xsize, COL8_FFFFFF,  3, ysize - 24, 		  59, ysize - 24);// 横长条
	boxfill8(vram, xsize, COL8_FFFFFF,  2, ysize - 24, 		   2, ysize -  4);// 竖长条
	boxfill8(vram, xsize, COL8_848484,  3, ysize -  4, 		  59, ysize -  4);// 横长条
	boxfill8(vram, xsize, COL8_848484, 59, ysize - 23, 		  59, ysize -  5);// 竖长条
	boxfill8(vram, xsize, COL8_000000,  2, ysize -  3, 		  59, ysize -  3);// 横长条
	boxfill8(vram, xsize, COL8_000000, 60, ysize - 24, 		  60, ysize -  3);// 竖长条
	
	boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 24, xsize -  4, ysize - 24);// 长横条
	boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 23, xsize - 47, ysize -  4);// 竖长条
	boxfill8(vram, xsize, COL8_FFFFFF, xsize - 47, ysize -  3, xsize -  4, ysize -  3);// 长横条
	boxfill8(vram, xsize, COL8_FFFFFF, xsize -  3, ysize - 24, xsize -  3, ysize -  3);// 竖长条
	return;
}

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

5.1 接收启动信息

按照3.8节中,SCRNX=0x0ff4,SCRNY=0x0ff6,VRAM=0x0ff8。该节将读取上述几个地址中的值,并将画面背景显示部分独立封装成init_screen函数。

5.2 使用结构体

将3.8节中的0x0ff0~0x0ffb地址内元素整合成一个BOOTINFO结构体,包含cyls, leds, vmode,reserve 等元素(reserve在3.8节中没有,VMODE为一个字节,而VMODE的地址为0x0ff2,相邻地址0x0ff3没有命名,所以在此就用reserve 表示地址0x0ff3处)。

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

cyls, leds, vmode,reserve这几个元素分别对应0x0ff0,0x0ff1,0x0ff2,0x0ff3,所以都是char类型。scrnx, scrny元素地址为0x0ff4,0x0ff6,均为占两个字节,所以是short类型。而vram只有一个开始位置0x0ff8,没有结束位置,所以该处使用指针类型。当创建结构体时,需要对结构体中的元素进行初始化。struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0,就是将binfo指向地址0x0ff0处,与3.8节中的元素相对应。

5.3 试用箭头记号

struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;binfo->srcnx与(*binfo).scrnx的操作是相同的。

5.4 显示字符

该节将字符构造出16*8像素点阵,如下图字符‘A‘。

30天自制操作系统(第4-6天)_第1张图片

字符’A‘可以构造成数组进行读取,下面应该构造函数将该字符进行绘制到VRAM中。函数void putfont8(char *vram, int xsize, int x, int y, char c, char *font) ;的功能就是将font数组绘制到VRAM中以坐标(x, y)为起点处,字符颜色为c。由于字符每行8个像素,所以for循环16行加上每层循环8个判断改变VRAM中的特定位置颜色。

5.5 增加字体

在文件hankaku.txt中存在256个常用字符,只要明白下述函数就是实现将该.txt文件转换成机器语言即可,如果想了解请自行检索。

TOOLPATH = ../z_tools/
MAKEFONT = $(TOOLPATH)makefont.exe
BIN2OBJ  = $(TOOLPATH)bin2obj.exe

#使用makefont.exe从hankaku.txt生成hankaku.bin
hankaku.bin : hankaku.txt Makefile
	$(MAKEFONT) hankaku.txt hankaku.bin
	
#使用bin2obj.exe从hankaku.bin生成hankaku.obj
hankaku.obj : hankaku.bin Makefile
	$(BIN2OBJ) hankaku.bin hankaku.obj _hankaku
由于每个字符为8*16像素,8位为一个字节,共256个字符,所以就是16*256=4096字节。extern char hankaku[4096];就是将这256字符写入到hankaku数组中,相邻的16字节就是一个字符。
hankaku + 'A' * 16,而在ASCII中'A'=0x41=65,所以就是将hankaku首字符的地址向后移动'A' * 16=1040个位置,即在hankaku数组中从下标1040开始的16个元素构成了字符'A'。通过上述分析,调用putfont8函数,指定输入项font即可打印出字符,多次调用putfont8函数可打印出字符串。

5.6 显示字符串

在5.5中需要多次调用putfont8函数才可以打印字符串,是否可以将需要打印的字符串整体作为输入项传入函数进行打印呢?改进函数void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s) ,可将字符串s绘制到VRAM中以坐标(x, y)为起点处,字符颜色为c。逐个读入字符串中的字符,调用putfont8函数进行打印,打印完字符后需要偏移x,不然就打印在同一位置上了。

5.7 显示变量值

该节主要介绍了函数sprintf,该函数在头文件stdio.h中,所以要使用该函数,就需要包含该头文件。sprintf(s, "scrnx = %d", binfo->scrnx); 的功能为binfo->scrnx的值替换到%d处,并将中间双引号的字符串写入到s中。由于binfo->scrnx=320,所以s="scrnx = 320"。

5.8 显示鼠标指针

将鼠标做成16*16像素的形状,void init_mouse_cursor8(char *mouse, char bc)函数中,mouse中存放了按照鼠标形状确定的颜色,bc为背景色。现在鼠标相关信息已准备好,需要将鼠标信息打印到屏幕上。参考putfont8函数,构造函数void putblock8_8(char *vram, int vxsize, int pxsize, int pysize, int px0, int py0, char *buf, int bxsize)。pxsize,pysize为想要显示的图形(picture)的大小,px0,py0为图形显示为位置,buf和bxsize分别指定图形的存放地址和每一行含有的像素数,功能为从buf读出pxsize*pysize个图形数据(bxsize指定读出哪些连续数据的),并将该图形写到vram的(px0,py0)坐标处。

5.9 GDTIDT的初始化

该节涉及到linux内核的GDT( 按照《深入理解linux内核》P44页 )和IDT( 按照《深入理解linux内核》P144页 ),创建SEGMENT_DESCRIPTOR和GATE_DESCRIPTOR结构体。

// GDT  global(segment)descriptor table,8字节
struct SEGMENT_DESCRIPTOR{
	short limit_low, base_low;
	char base_mid, access_right;
	char limit_high, base_high;
};

// IDT  interrupt descriptor table,8字节
struct GATE_DESCRIPTOR{
	short offset_low, selector;
	char dw_count, access_right;
	short offset_high;
};

结构体均为8字节,GDT的索引值为13位,所以GDT有2^13=8192个,而IDT的索引值为8位,所以IDT有2^8=256个。struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000; struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800;由于idt的地址为0x0026f800,而IDT有256*8=2048字节,所以0x0026f800+0x800=0x00270000。并按照《深入理解linux内核》中的介绍,初始化结构体中的元素。

; limit=0x0000ffff   addr=0x00270000  存放道ESP的顺序为ffff0000 00007200
_load_gdtr:					; void load_gdtr(int limit, int addr);
		MOV		AX,[ESP+4]	; limit 存放的是段上限   limit的低16位写进AX寄存器,即AX=0xffff
		MOV		[ESP+6],AX	; 将ESP寄存器中的第3 4位赋值为第1 2位数据  ESP:ffffffff 00007200
		LGDT	[ESP+6]		; 将0x00270000ffff装入GDTR寄存器
		RET

第6天 分割编译与中断处理

6.1 初始化PIC(这个位置说实话并不懂,加上了些自己的理解)

// 该程序是以INT 0x20~0x2f接收中断信号IRQ0~15而设定的
void init_pic(void){
	io_out8(PIC0_IMR, 0xff);/*IMR为8位寄存器,对应8路IRQ信号,禁止主PIC的全部中断*/
	io_out8(PIC1_IMR, 0xff);/*禁止从PIC的全部中断*/
	
	io_out8(PIC0_ICW1, 0x11);/* 边沿触发模式(edge trigger mode) */
	io_out8(PIC0_ICW2, 0x20);/* IRQ0-7由INT20-27接收 */
	io_out8(PIC0_ICW3, 1<<2);/* PIC1由IRQ2连接 */
	io_out8(PIC0_ICW4, 0x01);/* 无缓冲区模式 */
	
	io_out8(PIC1_ICW1, 0x11);/* 边沿触发模式(edge trigger mode) */
	io_out8(PIC1_ICW2, 0x28);/* IRQ8-15由INT28-2f接收 */
	io_out8(PIC1_ICW3,    2);/* PIC1由IRQ2连接 */
	io_out8(PIC1_ICW4, 0x01);/* 无缓冲区模式 */
	
	io_out8(PIC0_IMR, 0xfb); /* 11111011 对应IRQ2=0 主PIC0以外全部禁止中断 */
	io_out8(PIC1_IMR, 0xff); /* 11111111 禁止从PIC所有中断 */
	
	return;
}

由于PIC通过第2IRQ与主PIC相连,所以IRQ2应该置为0。ICW(initial control word):初始化控制数据。ICW有4个,分别编号为1~4,共有4个 字 节的数据。ICW1和ICW4与PIC主板配线方式、中断信号的电气特性等有关。ICW3是有关主 —从连接的设定,对主PIC而言,第几号IRQ与从PIC相连,是用8位来设定的,所以就设 定成00000100;对从PIC来说,该从PIC与主PIC的第几号相连,用3位来设定。

6.2 中断处理程序的制作

鼠标是 IRQ12 ,键盘是 IRQ1,所以编写了用于 INT 0x2c INT 0x21 的中断处理程序。
_asm_inthandler21:
		PUSH 	ES
		PUSH 	DS
		PUSHAD				;相当于使所有32位寄存器入栈,PUSH EAX->PUSH EDI
		MOV 	EAX,ESP
		PUSH 	EAX			;相当于ADD ESP,-4  MOV [SS:ESP],EAX
		MOV 	AX,SS
		MOV 	DS,AX
		MOV 	ES,AX
		CALL 	_inthandler21
		POP 	EAX			;相当于MOV EAX,[SS:ESP]  ADD ESP,4  
		POPAD				;与PUSHAD指令相反,使所有32位寄存器出栈
		POP 	DS
		POP 	ES
		IRETD

中断处理程序待我二刷《深入理解Linux内核》后再来添加自己的理解。

你可能感兴趣的:(算法)