2020.3.29
1. 接受启动信息(harib02a)
在harib01?中的bootpack.c中xsize、ysize屏幕分辨率等信息是直接写入程序的。正确的获取这些信息的方式是从asmhead.nas先前保存下来的值中获取。
查看asmhead.nas的部分代码:
; 有关BOOT_INFO
CYLS EQU 0x0ff0 ; 设置启动区,从内存中获取CYLS
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色数目的信息。颜色的位数。
SCRNX EQU 0x0ff4 ; 分辨率X(screen x)
SCRNY EQU 0x0ff6 ; 分辨率Y(screen y)
VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址
ORG 0xc200 ; 设置程序装载地址。
MOV AL,0x13 ; VGA显卡,320*200*8位彩色
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; 记录画面模式
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000
从上述代码红可以得知,分辨率X存放在内存地址0x0ff4中,分辨率Y存放在内存地址0x0ff6中,图像缓冲区的开始地址VRAM存放在内存地址的0x0ff8中。
因此,harib02a的bootpack.c的HariMain函数应该修改成:
void HariMain(void)
{
char *vram; /*注意vram是char类型的指针*/
int xsize, ysize;
short *binfo_scrnx, *binfo_scrny;
int *binfo_vram;
init_palette();
binfo_scrnx = (short *) 0x0ff4; /*分辨率X*/
binfo_scrny = (short *) 0x0ff6; /*分辨率Y*/
binfo_vram = (int *) 0x0ff8; /*图像缓冲区的开始地址*/
xsize = *binfo_scrnx;
ysize = *binfo_scrny;
vram = (char *) *binfo_vram; /*加上(char *)是为了防止出现警告*/
init_screen(vram, xsize, ysize);
for (;;) {
io_hlt();
}
}
其中,init_screen(vram, xsize, ysize)是把显示背景的部分独立出来的函数。
void init_screen(char *vram, int x, int y)
{
boxfill8(vram, x, COL8_008484, 0, 0, x - 1, y - 29);
boxfill8(vram, x, COL8_C6C6C6, 0, y - 28, x - 1, y - 28);
boxfill8(vram, x, COL8_FFFFFF, 0, y - 27, x - 1, y - 27);
boxfill8(vram, x, COL8_C6C6C6, 0, y - 26, x - 1, y - 1);
boxfill8(vram, x, COL8_FFFFFF, 3, y - 24, 59, y - 24);
boxfill8(vram, x, COL8_FFFFFF, 2, y - 24, 2, y - 4);
boxfill8(vram, x, COL8_848484, 3, y - 4, 59, y - 4);
boxfill8(vram, x, COL8_848484, 59, y - 23, 59, y - 5);
boxfill8(vram, x, COL8_000000, 2, y - 3, 59, y - 3);
boxfill8(vram, x, COL8_000000, 60, y - 24, 60, y - 3);
boxfill8(vram, x, COL8_848484, x - 47, y - 24, x - 4, y - 24);
boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y - 4);
boxfill8(vram, x, COL8_FFFFFF, x - 47, y - 3, x - 4, y - 3);
boxfill8(vram, x, COL8_FFFFFF, x - 3, y - 24, x - 3, y - 3);
return;
}
2. 试用结构体(harib02b)
struct BOOTINFO {
char cyls, leds, vmode, reserve;
short scrnx, scrny;
char *vram;
};
void HariMain(void)
{
char *vram;
int xsize, ysize;
struct BOOTINFO *binfo;
init_palette();
binfo = (struct BOOTINFO *) 0x0ff0;/*这样写视为防止出现警告*/
xsize = (*binfo).scrnx;
ysize = (*binfo).scrny;
vram = (*binfo).vram;
init_screen(vram, xsize, ysize);
for (;;) {
io_hlt();
}
}
3. 试用箭头记号(harib02c)
xsize = (*binfo).scrnx
等价于 xsize = binfo->scrnx
4. 显示字符(harib02d)
以前显示字符主要靠调用BIOS函数,但是现在是32位模式,不能再以来BIOS了。
harib02d下的bootpack.c中的putfont8函数 :
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];
/*从左到右以此判断d的二进制编码是否为1*/
if ((d & 0x80) != 0) { p[0] = c; }
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;
}
harib02d下的bootpack.c中的HariMain函数 :
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
static char font_A[16] = {
0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
};
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfont8(binfo->vram, binfo->scrnx, 10, 10, COL8_FFFFFF, font_A);
for (;;) {
io_hlt();
}
}
5. 增加字体(harib02e)
char 0x41
........
...**...
...**...
...**...
...**...
..*..*..
..*..*..
..*..*..
..*..*..
.******.
.*....*.
.*....*.
.*....*.
***..***
........
........
写到这里是中午12点多,刚吃完午饭。可能吃坏了东西,辗转厕所与书桌好几次。于是就睡下了,不知道今天的任务能不能完成。
因此,Makefile文件也需要修改。具体的代码不再赘述。
在C语言中使用这种字体,需要加上如下代码:
extern char hankaku[4096]
像这种在源程序以外准备的数据,都需要加上extern属性。这样C编译器就能知道它是外部数据,并在编译时做出相应调整。
extern详解:https://baike.baidu.com/item/extern/4443005?fr=aladdin
extern是计算机语言中的一个关键字,可置于变量或者函数前,以表示变量或者函数的定义在别的文件中。提示编译器遇到此变量或函数时,在其它模块中寻找其定义,另外,extern也可用来进行链接指定。
OSASK的字体数据,依照一般的ASCII字符编码,含有256个字符。A的字符编码是0x41
,A的字体数据放在开始地址为hankaku + 0x41 * 16
的16个字节
里。0x41
刚好是字符A的ASCII码,那么A的字体数据的开始地址可以用C语言写成hankaku + 'A' * 16
。
harib02e下的bootpack.c的HariMain函数:
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
extern char hankaku[4096]; /*使用外部字体数据hankaku*/
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfont8(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, hankaku + 'A' * 16); /*putfont8函数最后的参数需要字符起始地址即可*/
putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'B' * 16);
putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'C' * 16);
putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + '1' * 16);
putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + '2' * 16);
putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + '3' * 16);
for (;;) {
io_hlt();
}
}
6. 显示字符串(harib02f)
显然,上述显示六个字符的代码略显冗长。因此,函数putfont8_asc便是用来显示字符串的。
harib02f下的bootpack.c中的putfont8_asc函数:
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
extern char hankaku[4096];
for (; *s != 0x00; s++) {/*C字符串以'0x00'结尾,结束符\0*/
putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
x += 8;
}
return;
}
所谓字符串,就是字符按顺序排列在内存里,并在末尾加上0x00
的一段数据。
harib02f下的bootpack.c中的HariMain函数
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 123");
putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS.");
putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");
for (;;) {
io_hlt();
}
}
7. 显示变量值(harib02g)
显示变量值可以调试我们写的程序。在没有写出我们自己的调试器之前,只能使用这种方式来确认程序的正确性。
使用C语言中sprintf
函数可以实现将输出内容作为字符串直接写入内存中。
但是我们不能使用printf
函数,因为printf
是指定格式输出,不可避免的需要使用操作系统的功能(我们现在正在做操作系统,因此我们现在还没有操作系统)。
sprintf函数,是本次使用的名为GO的C语言编译器附带的函数。它可以不使用操作系统的任何功能。sprintf只对内存进行操作,所以适合于任何操作系统。
sprintf函数语法 https://baike.baidu.com/item/sprintf/9703430?fr=aladdin
int sprintf(char *string, char *format [,argument,...]);
简单来讲,sprintf和printf差不多。使用方法是:sprintf(内存地址, 格式, 格式对应的值)。这个格式和printf的格式差不多。注意,sprintf可不认识’\n’。
显示变量scrnx的值
sprintf(s, "scrnx = %d", binfo->scrnx);/*此时,s是指向字符串的指针*/
putfont8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);
harib02g下的bootpack.c中的HariMain函数:
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
char s[40];/*注意s的大小要合适*/
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 123");
putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS.");
putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");
/*新添加的代码*/
sprintf(s, "scrnx = %d", binfo->scrnx);
putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);
for (;;) {
io_hlt();
}
}
8. 显示鼠标指针(harib02h)
鼠标的大小为16*16
。先准备16*16=256字节的内存,然后往里面写入鼠标指针的数据即可。
harib02h下的bootpack.c中的init_mouse_cursor8函数:
void init_mouse_cursor8(char *mouse, char bc)
/* 初始化鼠标指针 */
{
static char cursor[16][16] = {
"**************..",
"*OOOOOOOOOOO*...",
"*OOOOOOOOOO*....",
"*OOOOOOOOO*.....",
"*OOOOOOOO*......",
"*OOOOOOO*.......",
"*OOOOOOO*.......",
"*OOOOOOOO*......",
"*OOOO**OOO*.....",
"*OOO*..*OOO*....",
"*OO*....*OOO*...",
"*O*......*OOO*..",
"**........*OOO*.",
"*..........*OOO*",
"............*OO*",
".............***"
};
int x, y;
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; /*背景色*/
}
}
}
return;
}
bc是背景色,mouse是存放鼠标颜色的数组。
harib02h下的bootpack.c中的putblock8_8函数,用于将背景色显示出来。
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;
}
其中,vxsize是屏幕分辨率X的大小;pxsize和pysize是显示图形的大小,这里是鼠标的大小16*16;px0和py0是图形左上角相对于屏幕的位置;buf是图形存放地址;bxsize是图形一行含有的像素数。
harib02h下的bootpack.c中的HariMain函数:
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
char s[40], mcursor[256];
int mx, my;
init_palette();
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
mx = (binfo->scrnx - 16) / 2; /* 设置鼠标的位置 */
my = (binfo->scrny - 28 - 16) / 2;
init_mouse_cursor8(mcursor, COL8_008484); /*初始化鼠标*/
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /*显示鼠标*/
sprintf(s, "(%d, %d)", mx, my);/*显示鼠标的位置*/
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
for (;;) {
io_hlt();
}
}
这里,(mx, my) = (152, 78)。
9. GDT与IDT的初始化(harib02i)
要想移动鼠标,就必须将GDT和IDT初始化。
为了让操作系统能够使用32位模式,需要对CPU进行各种设定。GDT和IDT都是与CPU有关的设定。
这就需要修改asmhead.nas。
原先的asmhead.nas只是做了一些能够运行bootpack.c所必需的一些设定。
分段
ORG:声明程序要读入的内存地址。
操作系统需要能够同时运行多个程序。这时,多个程序执行ORG,可能发生内存使用范围重叠现象。解决这个现象的方法就是分段。
分段:将内存分成很多块(大小不同),每一块的起始地址都看成是0.
分页:将内存分成很多块(大小相同),且也将程序分割成等大的多块。
段寄存器就是用来分段的。
MOV AL,[DS:EBX]
,CPU会往EBX里加上DS所表示的段的起始地址
,而不再是DS的16倍了。表示一个段:
CPU用8个字节(64位) 来表示一个段。 注意,用于指定段的寄存器只有16位。即段寄存器(默认DS)只有16位。
内存被分成很多段。因此需要段号来区分这些段。段号和段一一对应。
段寄存器只有16位,由于CPU设计的原因,段寄存器的低3位不能使用,因此,我们可以使用13位,那么就可以表示2^13=8192个不同的段,即段号范围0~8191.
我们可以设定8192
个段,一个段占8
字节,那么共需要8192*8字节=65536字节(64KB)
。
显然这64KB只能放在内存里。
GDT
IDT
IDT,interrupt descriptor table,中断记录表
。中断,当CPU遇到外部状况发生变化或者内部偶然错误,会临时切换过去处理这种突发情况。
如果想使用鼠标,就必须使用中断机制。因此,我们必须设定IDT。
IDT记录了0~255的终端号码与调用函数的对应关系。 比如,发生了123号中断,那么CPU就去调用指定123号指定的函数。
IDT的设定方法和GDT的设定方法类似(上图)。
注意,需要先设定GDT,再设定IDT。
harib02i下的bootpack.c代码节选:
struct SEGMENT_DESCRIPTOR { /*存放GDT的8字节内容,依据是CPU的资料*/
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
};
struct GATE_DESCRIPTOR { /*存放IDT的8字节内容*/
short offset_low, selector;
char dw_count, access_right;
short offset_high;
};
void init_gdtidt(void) /*初始化GDT和IDT*/
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000; /*0x270000~0x27ffff设置为GDT*/
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800;
int i;
/* 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);
/* IDT初始化 */
for (i = 0; i < 256; i++) {
set_gatedesc(idt + i, 0, 0, 0);
}
load_idtr(0x7ff, 0x0026f800);
return;
}
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
/*存疑1*/
if (limit > 0xfffff) {/*从GDT图可以看出,limit占20位,所以最大是0xfffff。下面的代码因该是越界重置,但是没看懂为什么。*/
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit & 0xffff; /*取低16位*/
sd->base_low = base & 0xffff; /*取低16位*/
sd->base_mid = (base >> 16) & 0xff; /*先右移16位再取低8位*/
sd->access_right = ar & 0xff; /*取低8位*/
/*存疑2*/
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); /*前面半句看懂了,后面的| ((ar >> 8) & 0xf0)有点莫名其妙。*/
sd->base_high = (base >> 24) & 0xff; /*右移24位,再取8位*/
return;
}
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
gd->offset_low = offset & 0xffff; /*取低16位*/
gd->selector = selector; /*我觉得应该还得加上&0xffff*/
/*存疑3*/
gd->dw_count = (ar >> 8) & 0xff; /*从这里看dw_count是在高位*/
gd->access_right = ar & 0xff;
gd->offset_high = (offset >> 16) & 0xffff;
return;
}
CPU有关GDT的设定
(reference:https://www.cnblogs.com/bajdcc/p/8972946.html)
CPU有关IDT的设定
(reference:https://blog.csdn.net/fwqcuc/article/details/5855460 )
为什么设定0x270000~0x27ffff
这65536个字节为GDT,或者说为什么GDT的其实地址是0x270000?
原因:内存分布图上,显示这一块儿没有被使用。
IDT被设定成为0x26f800~0x26ffff
。共256*8字节=2048字节(2KB)。
顺便提一下,内存0x280000~0x2fffff
共524288字节(512KB)已经被bootpack.h使用了。这件事情是asmhead.nas干的。
GDT的初始化:
将8192个段的上限(limit,指段的字节数-1),基址(base),访问权限都设置成为了0.
两句代码
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
0x280000
,这就是位bootpack.hrb准备的,用这个段就可以执行bootpack.hrb,因为bootpack.hrb是以ORG 0
为前提翻译成的机器语言。load_gdtr(0xffff, 0x00270000);
这句话是把特殊寄存器GDTR内容加载到内存里面。
寄存器GDTR
(reference:https://www.cnblogs.com/wanghj-dz/p/3975107.html)
naskfunc.nas添加了两个函数load_gdtr和load_idtr.
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LGDT [ESP+6]
RET
_load_idtr: ; void load_idtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LIDT [ESP+6]
RET
2020.3.30傍晚,在读上述代码的时候突然卡住了,摆在我面前的有三个问题:1.WIN32汇编;2.函数调用过程栈的变化。3. load_gdtr(0xffff, 0x00270000)中参数传递。
代码含义?
MOV AX,[ESP+4]
MOV [ESP+6],AX
我深知,一时半会儿是不能掌握WIN32汇编的。同时花太多时间在学习WIN32汇编也不是制作操作系统的重点。我的重心应该在C语言编程上。
于是,我重新读了第2天有关汇编的相关知识(P36-37)。瞬间觉得豁然开朗了许多。
MOV AX,[ESP+4]
:AX是16位寄存器,由于WIN32汇编中源数据和目的数据必须位数相同,因此上述代码相当于MOV AX,WORD[ESP+4]
, WORD[ESP+4]是指以内存地址ESP+4为起始地址的连续2字节(WORD)的内容。 那么这句话的意思也就显而易见了。
MOV [ESP+6],AX
:类比上面,相当于MOV WORD[ESP+6],AX
。这句话的意思是:把以内存地址为ESP+6为起始地址的连续2字节的内容赋值给寄存器AX。
现在只剩一个问题了,为什么是[ESP+4],[ESP+6]而不是其他呢? 解决这个问题,我们需要知道函数调用过程栈的变化。
这个问题暴露了我大学四年学习:没学到点啥。这绝不是贬低中国每羊大学,而是批判自己的学习态度不端正和治学不严谨。
首先,了解一下程序对内存使用的分区情况:
栈区
函数调用过程栈的变化
Reference:https://www.zhihu.com/question/22444939/answer/22200552
假设有如下代码:
void func_A(arg_A1, arg_A2);
void func_B(arg_B1, arg_B2);
int main(int argc, char *argv[], char **envp){
func_A(arg_A1, arg_A2);
}
void func_A(arg_A1, arg_A2){
var_A;
func_B(arg_B1, arg_B2);
}
void func_B(arg_B1, arg_B2){
var_B1;
var_B2;
}
总体过程:
ESP、EBP、EIP进一步理解
函数调用大致包括以下几个步骤:
参数入栈:将参数从右向左依次压入系统栈(当前栈帧) 中
返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行
代码区跳转:处理器从当前代码区跳转到被调用函数的入口处
栈帧调整:具体包括
保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)
将当前栈帧切换到新栈帧。(将ESP值装入EBP,更新栈帧底部)
给新栈帧分配空间。(把ESP减去所需空间的大小,抬高栈顶,ESP抬到新栈帧的顶部。)
首先明白一点:数据存放在内存中,我们采用的是小端模式(高地址高位)。
调用load_gdtr时:
指令LGDT 地址addr
:将内存地址addr开始的6个字节读入GDTR寄存器中。
load_gdtr函数时用汇编写的,且没有局部变量,只使用了CPU的寄存器AX。所以我猜(笑)ESP=EBP,他们俩都指向返回地址。【暂时,这是我所能做出的最合适的解释了。希望看到这篇文档的大佬能帮忙解惑。】
load_idtr和load_gdtr十分相似,这里不再赘述。
重新回到这里的时候已经是2020.03.30 23:22了。
接下来的IDT的初始化和load_idtr(0x7ff, 0x0026f800)
和GDT的类似。
访问权属性和IDT的详细说明还是留到明天吧,大言不惭的讲,我和当时川合秀实一样,有点累了。
10. 再次刷新两项记录