自制操作系统日志——第四天
今天是该系列的第四天。今天的任务就是利用指针以及汇编,实现本操作系统的画面显示。
一、利用c直接写入内存
从之前的划分内存那块可以知道,我们要想让系统的画面进行显示就需要向VARM所在的内存中写入数据。
⇒ VARM 是320x200,且于内存中是由0xa0000开始的。因此,我们可以认为屏幕的右上角是(0,0)左下角就是(319,199)。这里假设一个内存地址在屏幕上就算一个点,则内存地址的增加就是按着屏幕从左到右,按行递增的顺序进行的。故有以下的内存地址计算方法:
0xa0000+x+y*320
好了,在了解这个以后,我们接下来正式开始进行编写吧!
这里啊,由于c没有直接向内存地址输入的语句(当然你可以用指针写,后面我也会写用指针的方法),这里我们写一个汇编程序形成c的库函数,以此来实现写入指定的内存地址空间,修改naskfunc.nas:
naskfunc.nas:(新增了以下内容)
[INSTRSET "i486p"] ;告诉汇编编译器,这个是给486使用的,而不是8086 即可以识别到寄存器EAX
GLOBAL _write_mem8
[SECTION .text]
_write_mem8: ; void wirte_mem8(int addr, int data);
mov ECX,[esp+4] ;[ESP+4]中存放的是地址,将其读入搭配ecx中
mov al,[esp+8] ;[esp+8]中存放的是数据,将其放入al中
mov [ECX],al
RET
然后,我们就可以在bootpack.c,新增以下内容:
void write_mem8(int addr, int data); //向VARM内存显卡地址写入
// 这里如果看过编写的汇编代码的话,那么可能会疑惑,这里传入的参数貌似没有再汇编中使用到?
// 这里,我做出自己的理解:
// 1、函数的参数是通过栈进行传递的,而且是从右往左依次入栈,所以地址才会在栈指针指向的低地址空间!!
// 2、调用函数的前后堆栈要保持一致,即函数返回后,栈指针要恢复到进入函数前
// 3、函数接受的形参都是从栈中取的
// 这里VRAM的显存空间位于0xa0000~0xaffff
void HariMain(void)
{
int i ;
for(i=0xa000; i <= 0xaffff ; i++)
{ write_mem8(i, 15);//mov byte [i],15; 15代表这将像素15全部写入,即白色
}
for(;;)
{
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有hlt这个命
}
}
然后,make run即可:
看!!此时画面变白了!
这里,解释一下上面的函数传参在汇编里的情况:
二、制作条纹,以及利用c语言制作
为了显得有成就一点,这里我们继续进一步的修改一下,值得页面能显示条纹状:
修改bootpack.c:
for(i=0xa000; i <= 0xaffff ; i++)
{ write_mem8(i, i & 0x0f);//mov byte [i],15; 15代表这将像素15全部写入,即白色
这里,让地址值全部与15进行与操作。解释以下为什么这样子就能显示条纹了:这是因为0x0f的二进制值为 0000 1111 而由于与的特性因此,只要是超过四位以上的将全部都变为了0,只有低四位会随着地址的增加呈现周期性的变化。而一行有320 ,320/4=80 因此,一行会重复80次,当到达下一行时变化也是和第一行一样的,因此就会出现条纹状。
解释完了后,让我们make run以下:
好的,接下来我们使用c语言的指针完成上述的操作:
void HariMain(void)
{
int i ;
char *p;
for(i=0xa000; i <= 0xaffff ; i++)
{
p = (char *) i; //由于c中将数据和内存地址进行了区分,因此在赋值的过程中将数据进行类型转化是最好的!!
*p = i & 0x0f;
//代替了write_mem8(i, i & 0x0f);
}
for(;;)
{
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有hlt这个命
}
}
这里,我们可以以汇编角度来解释一下指针,即p代表着一个地址,而*p则在汇编中就相当于[p]的意思,即代表着一个内存单元的地址。因此,p可以说是内存地址的变量,而char p这个声明,其实就只有一个变量,即p变量。因为放到汇编中,至始至终都只有一个p可变而已,毕竟p = [p] 。
除此之外,再来说明一下声明的含义:
char *P; //用于BYTE类的地址,指向的内存单元数据为1字节,al
short *P; //用于word类的地址,指向的内存单元数据为2字节,ax
int *P; //用于用于Dword类的地址,指向的内存单元数据为4字节,eax
//p本身因为代表的是地址,因此p本身是4字节的
上述的指针还可以改成:
void HariMain(void)
{
int i ;
char *p;
p = (char *) 0xa0000;
for(i=0; i <= 0xffff ; i++)
{
p[i] = i $ 0x0f;
}
for(;;)
{
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有hlt这个命
}
}
这里的p[i]就与*(p+i)是一个意思,即p的地址+i的地址后,再指向该地址的内存空间!! 由于加法是可互换的因此其实i[p]也是可以的!!!!
make run一下后,也会出现上述的彩色条纹!
三、制作调色板
我们使用的是VGA模式的320x200x8,至于320x200就是代表着屏幕的矩形框的像素之类的,这里不再赘述了(上面有)。然后,我们来解释一下8: 这里的8其实是指8位的颜色模式,即我们采用色号为8位2进制的数来表示整个系统!
这里熟悉色彩的人可能就会疑惑,因为我们的电脑一般采用RGB(红绿蓝)的方式,即用6位16进制数表示 #ffffff ,也就是24位2进制数,那么我们这个8是不是有点太少了?
再前面我们调用bios时,那里有介绍说我们这个是调色板模式。也就是说,我们可以利用这8位二进制数随意的进行指定RGB的色彩,即我们可以设定15号颜色对应的是#ffffff ,26号对应的是#8c8c8c 号颜色。
再了解这些后,我们去修改一下我们的bootpack.c文件,增加调色板模式(以下代码可能含有未知的函数声明,暂时不用管,讲到时候再说):
void io_hlt(void); //暂停
void io_cli(void); //设置IF标志寄存器为0
void io_out8( int port, int data ); //向端口写入数据,按rgb格式写入
int io_load_eflags(void);
void io_store_eflags(int eflags ) ;
void write_mem8(int addr, int data); //向VARM内存显卡地址写入
void init_palette(void); //设定调色板
void set_palette(int start , int end, unsigned char *rgb); //向bios提供的调色板写入
void HariMain(void)
{
int i ; //i是一个32位的整数
char *p ;//在汇编中变量p是一个地址本身是四字节的,但是其指向的内存单元的byte类型(因为char就是1字节)
init_palette();//设定调色板
p = (char *) 0xa0000 ;
for(i=0; i<= 0xffff; i++)
{
p[i] = i & 0x0f ;
}
for(;;)
{
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有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); //进行设置
}
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 = rgb+3;
}
io_store_eflags(eflags); //恢复中断许可标志
}
这里,解释一部分:static 这一句话,对应于汇编其实就是db指令(占1字节)。而如果不用static的话,直接char a[3]这种,就相当于赋值了,在汇编中可以看出resb 3 (占3字节) 。我们上面定义了48个数据,如果用赋值的话大概是150字节左右了,而db则至于48 字节!
除此之外,这里面使用的io_in 于io_out 其实调用的是汇编里的in指令与out指令,方便向连接的设备输送信息的。
然后还有一个0x03c8、0x03c9这是什么呢? 这其实是,VGA显示设备的设备号码,其具体功能如下:
调色板设备的饭问步骤:
1、首先在一连串的访问中屏蔽中断,(使用CLI将IF设置为0)禁止中断请求;
2、将想要设定调色板的号码写入0x03c8 , 然后接着按R、G、B的顺序写入0x03c9,如果想要继续设定下一个调色板,直接继续写入即可。
3、想要读取当前调色板状态,则先将调色板号码写入0x03c7 ,然后就会按R、G、B的顺序从0x03c9中读出即可,如果想要继续读下一个调色板,直接继续读;
4、执行sti
至此,上述代码中大部分已经解读完,下面看一下naskfunc.nas:
[FORMAT "WCOFF"] ;制作的目标文件的模式
[INSTRSET "i486p"] ;告诉汇编编译器,这个是给486使用的,而不是8086 即可以识别到寄存器EAX
[BITS 32] ;制作32位的机器语言模式
[FILE "naskfunc.nas"] ;源文件名信息
;制作具体的函数库
GLOBAL _write_mem8 ;程序中包含的函数名,一定要以_开头,为了衔接c语言的函数库
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] ;目标文件中写了这些之后再写程序
_write_mem8: ; void wirte_mem8(int addr, int data);
mov ECX,[esp+4] ;[ESP+4]中存放的是地址,将其读入搭配ecx中
mov al,[esp+8] ;[esp+8]中存放的是数据,将其放入al中
mov [ECX],al
RET
_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_stihld(void)
STI
HLT
RET
_io_in8: ;int io_in8(int port)
mov edx,[esp+4]
mov eax,0
in al,dx ;将dx端口的数据读8字节到al中,下面的同理
RET
_io_in16: ;int io_in16(int port)
mov edx,[esp+4]
mov eax,0
in ax,dx
RET
_io_in32: ;int io_in32(int port)
mov edx,[esp+4]
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 ;将al的数据写到dx这个端口
RET
_io_out16: ;void io_out16(int port ,int data)
mov edx,[esp+4] ;port
mov ax,[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 flag寄存器
pop eax ;这里解释一下:函数的返回值,在汇编中是由:char型 AL ; int型 AX
RET
_io_store_eflags: ; void io_store_flags(int flags)
mov eax,[esp+4]
push eax
popfd
RET
然后,make run:
三、制作矩形框,以及今天最终成果的框
由于在第一部分已经讲了,VRAM显卡在内存中像素地址是如何对应到屏幕的,因此我们可以直接利用这个公式制作矩形框:
0xa0000+x+y*320
bootpack.c:(新增一些内容,主要是boxfill8这个函数)
//按着下面的init_palette 定义的颜色
#define COL8_000000 0 //黑
#define COL8_FF0000 1 //亮红
#define COL8_00FF00 2 //亮绿
#define COL8_FFFF00 3 //亮黄
#define COL8_0000FF 4 //亮蓝
#define COL8_FF00FF 5 //亮紫
#define COL8_00FFFF 6 //浅亮蓝
#define COL8_FFFFFF 7 //白
#define COL8_C6C6C6 8 //亮灰
#define COL8_840000 9 //暗红
#define COL8_008400 10//暗绿
#define COL8_848400 11//暗黄
#define COL8_000084 12//暗青
#define COL8_840084 13//暗紫
#define COL8_008484 14//浅暗蓝
#define COL8_848484 15//暗灰
void io_hlt(void); //暂停
void io_cli(void); //设置IF标志寄存器为0
void io_out8( int port, int data ); //向端口写入数据,按rgb格式写入
int io_load_eflags(void);
void io_store_eflags(int eflags ) ;
void write_mem8(int addr, int data); //向VARM内存显卡地址写入
void init_palette(void); //设定调色板
void set_palette(int start , int end, unsigned char *rgb);
void HariMain(void)
{
char *p ;//在汇编中变量p是一个地址本身是四字节的,但是其指向的内存单元的byte类型(因为char就是1字节)
init_palette();//设定调色板
p = (char *) 0xa0000 ;
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);
for(;;)
{
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有hlt这个命
}
}
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
int x,y;
for(y = y0; y <= y1; y++)
{
for(x = x0; x <= x1; x++)
vram[y*xsize+x]=c; //按着屏幕从左往右一行行读,则需要填入的vram = 0xa0000+x+y*320 。因为本身vram大小是320*200
}
return;
}
make run:
很好,最后来来看一下最终成果吧:(就更改以下主函数)
void HariMain(void)
{
char *vram;
int xsize,ysize;
init_palette();//设定调色板
vram = (char *) 0xa0000 ;
xsize = 320;
ysize = 200;
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); //设置右边矩形框的右边
for(;;)
{
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有hlt这个命
}
}
make run:
总结
:以上就是第四天的内容,感觉内容还是很多的,已经能够显示框框了,兴奋!!