实验日期 | 实验项目 |
---|---|
2020.10.29 | 第5天 结构体,文字显示和GDT/IDT初始化 |
文章目录
一、实验主要内容
1、 内容1 接收启动信息和使用结构体
(1).内容概要
- 实验内容:从asmhead.nas中取和画面相关的信息,并将功能模块化;使用结构体将具有相同意义的数据归纳到一起;使用箭头标记方式简化代码的书写。
- 实验重点::理解从asmhead.nas中直接取相关信息的设计优势;学会使用结构体对具有类似属性的变量进行归类处理。
(2).关键代码分析
struct BOOTINFO {
char cyls, leds, vmode, reserve;
short scrnx, scrny;
char *vram;
};
这部分代码定义了结构体,scrnx,scrny表示画面模式的x和y,vram表示图形缓冲区的首地址,这里定义的变量的地址位置和汇编代码中变量的保持一致。结构体指针变量的地址指向0x0ff0,对于scrnx,来说相对于结构体的首指针偏移4个字节,即对应地址为0x0ff0+0x4=0x0ff4。下面的图是每个变量对应的地址。
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;//结构体变量指向对应的地址
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);//这里使用箭头对应起图形缓冲区的写入地址,图形显示模式的X,Y
for (;;) {
io_hlt();
}
}
在C语言中常常会使用到类似于(*binfo).scrnx的编写格式,于是产生了binfo->srcnx的箭头标记方式。这可以理解为两者是等价的,只是在C语言中的不同表现方式。为了简化代码量,可以不用声明多余的变量,在init_screen()函数中直接使用结构体中的成员变量传参。
2、 内容2 显示字符
(1).内容概要
- 实验内容:通过控制不同像素点的颜色来显示字符。
- 实验重点:理解字符显示的原理。显示字符的关键在于将字符用一个矩形框表示出来,有字符点的位置为黑色,其余的为白色,将其转换为一个0,1的二进制矩阵。转换关系如下图所示:
在图中每行8位对应一个字节,一个16个字节,可以和16个16进制的数字对应起来,根据之前的绘制图案的原理即可在屏幕上显示字符。
(2).关键代码分析
static char font_A[16] = {
0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
};
这部分是描画文字形状的数据。
void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
int i;
char *p, d /* data */;
for (i = 0; i < 16; i++) {
//每次对每行的数据进行判断。
p = vram + (y + i) * xsize + x;
d = font[i];
if ((d & 0x80) != 0) {
p[0] = c; }//0x80对应二进制为1000 0000,如果d最左边的位为1,则整个结果为1,否则其他情况均为0.同理,下一句中的0x40是用来判断左边第2位是否为1。
if ((d & 0x40) != 0) {
p[1] = c; }
if ((d & 0x20) != 0) {
p[2] = c; }
if ((d & 0x10) != 0) {
p[3] = c; }
if ((d & 0x08) != 0) {
p[4] = c; }
if ((d & 0x04) != 0) {
p[5] = c; }
if ((d & 0x02) != 0) {
p[6] = c; }
if ((d & 0x01) != 0) {
p[7] = c; }
}
return;
}
putfont8函数将字体数据描画到画面上的函数。字符被描画到一个8*16的矩形区域内,矩形的左上角的坐标为(x,y),指针p指向的是这个二维矩形区域每个点的地址。使用一个for,每次对每行数据中的1进行判断,如果为1则描画该点;这里需要使用了8个if语句,和8个不同的数字相与,目的就是为了判断每位上的数字是否为1。
3、 内容3 增加字体,显示字符串和显示变量值
(1).内容概要
- 实验内容: 使用OSASK的字体;编写一个字体显示函数,用循环来依次显示字符串中的字符;使用不依赖任何底层操作系统的sprintf来输出显示变量值。
- 实验重点:理解字体显示的原理;学会使用循环去依次显示字符;能够显示变量值,明白显示变量值对代码调试的意义。
如何使用OSASK字体数据:首先使用专门的编译器将字体编码转化为二进制文件,接着使用bin2obj.exe工具将所给的二进制文件转换成为目标程序。在C语言程序中对声明的数组加上extern属性,就可以使用在源程序以外准备的数据了。
sprintf函数的功能和printf很类似,它可以将输出内容作为字符串写在内存中,接着使用我们之前编写的字符串显示函数就可以将写在内存中的字符串打印出来。printf在使用过程中会涉及到系统调用,而我们的自制的操作系统是不能基于任何底层操作系统的,sprintf只对内存操作,与操作系统无关刚好满足这一点的要求。下表是对sprintf的格式总结:
sprintf(地址,格式,值,值,值,…) | |
---|---|
%d | 十进制数 |
%5d | 5位十进制数,不足会加空格补全5位 |
%05d | 5位十进制数,不足会加前导0补全5位 |
%x | 十六进制数 |
%5x | 5位十六进制数,不足会加空格补全5位 |
%05x | 5位十六进制数,不足会将前导0补全5位 |
%s | 字符串格式 |
(2).关键代码分析
putfont8(binfo->vram, binfo->scrnx, 8,8, COL8_FFFFFF, hankaku+'A'*16);
这行代码中hankaku+’A’16的写法解释:OSASK的字体数据,依照一般的ASCII字符编码,含有256个字符。A的字符编码是0x41,所以A的字体数据,放在自“hankaku+0x4116”开始的16字节里。C语言中A的字符编码可以用’A’来表示,正好可以用它来代替0x41,所以也可以写成“hankaku +‘A’*16”。
void putfont8_asc(char *vram,int xsize,int x,int y,char c,unsigned char *s)
{
extern char hankaku[4096];
for(;*s!=0x00;s++)
{
putfont8(vram,xsize,x,y,c,hankaku+ *s *16);
x+=8;
}
return ;
}
这部分代码是显示字符串的代码。C语言中,字符串都是以0x00结尾的,所以可以这么写。。这里还要再说明一点,所谓字符串是指按顺序排列在内存里,末尾加上0x00而组成的字符编码。所以s是指字符串前头的地址,而使用*s就可以读取字符编码。这样,利用上面的循环就能够达到目的了。
sprintf(s,"scrnx = %d",binfo->scrny);
putfont8_asc(binfo->vram,binfo->scrnx,16,64,COL8_FFFFFF,s);
这两句代码就是显示变量值的程序。Sprintf先按照指定格式将要打印的内容和显示的变量作为字符串写入内容中,接着调用putfont8_asc函数打印出写入内存的字符串。
4、 内容4 显示鼠标指针
(1).内容概要
- 实验内容: 根据显示字符的思路,实现一个显示鼠标的功能。
(2).关键代码分析
static char cursor[16][16] = {
"**************..",
"*OOOOOOOOOOO*...",
"*OOOOOOOOOO*....",
"*OOOOOOOOO*.....",
"*OOOOOOOO*......",
"*OOOOOOO*.......",
"*OOOOOOO*.......",
"*OOOOOOOO*......",
"*OOOO**OOO*.....",
"*OOO*..*OOO*....",
"*OO*....*OOO*...",
"*O*......*OOO*..",
"**........*OOO*.",
"*..........*OOO*",
"............*OO*",
".............***"
};
这部分是使用字符编码的方式作出一个16*16个像素大小的鼠标图案。
for (y = 0; y < 16; y++) {
for (x = 0; x < 16; x++) {
if (cursor[y][x] == '*') {
mouse[y * 16 + x] = COL8_000000;
}
if (cursor[y][x] == 'O') {
mouse[y * 16 + x] = COL8_FFFFFF;
}
if (cursor[y][x] == '.') {
mouse[y * 16 + x] = bc;
}
}
}
这部分使用二重循环将鼠标图案中的每个编码替换为相应的颜色,包括16*16区域中去除鼠标后的背景色。这里注意一下,如果不对背景色进行初始化,则在显示图形颜色的时候,由于没有初始化,背景将会是黑色,显得比较突兀。
void putblock8_8(char *vram, int vxsize, int pxsize,int pysize, int px0, int py0, char *buf, int bxsize)
{
int x, y;
for (y = 0; y < pysize; y++) {
for (x = 0; x < pxsize; x++) {
vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x];
}
}
return;
}
这部分代码是将初始化后的数据复制到图形缓冲区vram中去。其中函数中vram和vxsize是关于VRAM的信息。他们的值分别是Oxa0000和320。pxsize和pysize是想要显示的图形的大小,鼠标指针的大小是16×16,所以这两个值都是16。px0和py0指定图形在画面上的显示位置。最后的buf和bxsize分别指定图形的存放地址和每一行含有的像素数。bxsize和pxsize大体相同,但也有时候想放人不同的值,所以还是要分别指定这两个值。
5、 内容5 GDT和IDT的初始化
(1).内容概要
- 实验内容: 对GDT和IDT进行初始化。
- 实验重点:学会对GDT(全局段号记录表)和IDT(中断记录表)的初始化,并了解相应的结构。
段的相关知识总结
a. 将固定大小的内存分为很多段。每段的起始地址看做是0来进行处理,分割的块就
称为段。
b. 在16位机器中,段寄存器可以用来扩展地址的表示,地址表示为段寄存器的值*16+原地址。而在32位中DS则是表示段的起始地址,实际的地址为段寄存器中的值+原地址。
c. 表示一个段,需要的信息:段的大小;段的起始地址;段的管理属性(如禁止写入,禁止执行,系统专用等等)
d. 段号的设定:由于CPU设计上的原因,段寄存器的低3位不能使用,故只能处理0-8191的区域,段号自然只能使用0-8191的数。
e. 段的存储:设定8192个段,需要的大小为8192*8=64kb,这里引入GDT,将这些数据整齐地排列在内存中的某个地方,然后将内存的起始地址和有效设定个数放在CPU中的GDTR的特殊寄存器中。
中断:当CPU遇到外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理
这种突发事件。这就是中断功能。中断的出现减少了CPU对设备的访问次数,以便CPU能更加集中精力在处理任务上,而不是一直在检测设备的请求状态。
(2).关键代码分析
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;
/* GDT的初始化 */
for (i = 0; i < 8192; i++) {
set_segmdesc(gdt + i, 0, 0, 0);
}
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
load_gdtr(0xffff, 0x00270000);
这部分代码是对GDT的初始化。gdt是定义的SEGMENT_DESCRIPTOR结构体变量,gdt的起始的地址为0x00270000,一直到0x0027ffff的区域都用来设置gdt。set_segmdesc函数的第2个参数表示段的大小,第3个参数表示段的基址,第4个参数表示段的管理属性。for循环一开始对所有段的相关信息都初始化为0。接下来两条语句是对段为1,2的两个段的设定,段号为1的段,上限值为Oxffffffff即大小正好是4GB),地址是0,它表示的是CPU所能管理的全部内存本身。段的属性设为0x4092;段号为2的段,它的大小是512KB,地址是0x280000,这是为bootpack.hrb而准备的。最后一句是使用汇编语言对寄存器GDTR赋值。
/* IDT的初始化 */
for (i = 0; i < 256; i++) {
set_gatedesc(idt + i, 0, 0, 0);
}
load_idtr(0x7ff, 0x0026f800);
这部分是对IDT的初始化,其设置方式也是和GDT类似的。
二、遇到的问题及解决方法
1、 描述问题1
- 问题描述
OSASK数据转换为obj目标文件后,是如何做成映像文件的,被存储在extern声明的hankaku数组中?
- 解决方法
书中也没有对之后的过程进行详细的说明。于是查看Makefile文件,发现下面的语句
将hankaku.obj文件和之前的obj文件一起生成了bootpack.bim文件。最终生成的映像文件在启动时被加载到内存中,hankaku数据也对应内存中的一段位置,和extern定义的数据建立起映射关系。
三、程序设计创新点
1、 描述创新点1,关键代码及结果截图
- 创新点1
一开始的实验是在屏幕上显示字符,但是不能显示汉字。到了今天的实验,既然字体数据可以编写,于是想通过编写字体来显示中文。
- 关键代码
在hankaku.txt字体数据文件中加入0x100号和0x101号字体
将对应字体数据的存储数组大小改变为4128=4096+32*2。接着调用putfont8函数输出字体即可。
- 结果截图
在屏幕上成功显示我自己加入的字体数据,所以只要将汉字进行编码处理就能实现在屏幕上显示中文汉字的功能。
四、实验心得体会
- 本次实验是自制操作系统的第5天,这次实验又增减一些C语言知识的学习,如结构体的使用,实验的重点是实现字符和字符串的显示,显示原理有点类似于描点的方式,实现机制还是基于第4天的内容(图案显示)。另外,在实验末尾绘制了鼠标,并对实现鼠标移动做了一些准备工作,内容比较枯燥,涉及了段和中断的相关知识。之前在小学期学习过中断机制,这里对这些术语并不是很陌生,在实验过程中可以侧重于去理解为什么要这样做,使用这些机制有什么好处。学习到这里,马上就可以实现可以动起来的鼠标了,还是比较激动的,从0基础开始,从BIOS中断显示字符到绘制动画效果,学习到了不少的知识。