第8天 鼠标控制与32位模式切换
2020.4.2
1. 鼠标解读(1)(harib05a)
- 现在,我们让鼠标动起来。
- 先对bootpack.c中的HariMain函数进行修改。
unsigned char mouse_dbuf[3], mouse_phase;
……
enable_mouse();
mouse_phase = 0; /* 进入等待鼠标的0xfa状态 */
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
io_stihlt();
} else {
if (fifo8_status(&keyfifo) != 0) {
i = fifo8_get(&keyfifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
if (mouse_phase == 0) {
/* 等待鼠标进入0xfa的状态 */
if (i == 0xfa) {
mouse_phase = 1;
}
} else if (mouse_phase == 1) {
/* 等待鼠标的第1字节 */
mouse_dbuf[0] = i;
mouse_phase = 2;
} else if (mouse_phase == 2) {
/* 等待鼠标的第2字节 */
mouse_dbuf[1] = i;
mouse_phase = 3;
} else if (mouse_phase == 3) {
/* 等待鼠标的第3字节 */
mouse_dbuf[2] = i;
mouse_phase = 1;
/* 鼠标的三个字节都齐了,显示出来。 */
sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
}
}
- 首先,要把鼠标准备好了的答复消息
0xfa
舍弃掉。 - 鼠标一次会发送的数据都是3个字节一组的。(鼠标动一下,会产生3次鼠标中断,每次中断发送1字节数据)
- mouse_phase用来记住接收鼠标的工作进展到了什么阶段。
- 接收的3个字节的数据存放在mouse_dbuf数组中。
make run
以后,点击鼠标或移动鼠标:
- 上图中红框里面的3字节数据
28 21 B3
就是mouse_dbuf[0-2]中存的数据。 - 如果移动鼠标,
28
(mouse_dbuf[0])中的数字2
会变化,变化范围是0~3. - 如果只是移动鼠标
28
中的8
并不会变化,只有当点击鼠标的时候才会变化。不管怎么点击鼠标,这个值的变化的范围是8~F
. - 21(mouse_dbuf[1])与鼠标左右移动有关
- B3(mouse_dbuf[2])与鼠标上下移动有关
- 2. 稍事整理(harib05b)
- HariMain函数显得有点乱,整理一下。
- bootpack.c中的代码(节选):
struct MOUSE_DEC {
unsigned char buf[3], phase;
};
……
void enable_mouse(struct MOUSE_DEC *mdec); /*注意,这里的enable_mouse函数加了一个参数*/
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat);
……
void HariMain(void)
{
……
enable_mouse(&mdec);
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
io_stihlt();
} else {
if (fifo8_status(&keyfifo) != 0) {
i = fifo8_get(&keyfifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); /*用背景色覆盖掉原先的值*/
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
if (mouse_decode(&mdec, i) != 0) {
sprintf(s, "%02X %02X %02X", mdec.buf[0], mdec.buf[1], mdec.buf[2]);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31); /*用背景色覆盖掉原先的值*/
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
}
}
}
void enable_mouse(struct MOUSE_DEC *mdec)
{
/* 鼠标有效 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
/* 顺利的话,ACK(0xfa)会发送过来 */
mdec->phase = 0; /* 等待0xfa的阶段 */
return;
}
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{
if (mdec->phase == 0) {
if (dat == 0xfa) {
mdec->phase = 1;
}
return 0;
}
if (mdec->phase == 1) {
mdec->buf[0] = dat;
mdec->phase = 2;
return 0;
}
if (mdec->phase == 2) {
mdec->buf[1] = dat;
mdec->phase = 3;
return 0;
}
if (mdec->phase == 3) {
mdec->buf[2] = dat;
mdec->phase = 1;
return 1;
}
return -1; /* 应该不可能到这里来 */
}
- 创建了一个新的结构体MOUSE_DEC,DEC是decode的缩写,用于存储解读鼠标所需要的的所有变量。
- 因此,enable_mouse函数也相应地添加了一个函数。
- 我们把鼠标解读的过程整合成了一个函数mouse_decode。这样HariMain函数看起来又就比较清爽了。
make run
应该也没什么问题。
3. 鼠标解读(2)(harib05c)
- 再次解读鼠标
- bootpack.c中的mouse_decode函数。
struct MOUSE_DEC {
unsigned char buf[3], phase;
int x, y, btn;
};
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{
if (mdec->phase == 0) {
if (dat == 0xfa) {
mdec->phase = 1;
}
return 0;
}
if (mdec->phase == 1) {
if ((dat & 0xc8) == 0x08) {
/* 如果鼠标的第1字节正确 */
mdec->buf[0] = dat;
mdec->phase = 2;
}
return 0;
}
if (mdec->phase == 2) {
mdec->buf[1] = dat;
mdec->phase = 3;
return 0;
}
if (mdec->phase == 3) {
mdec->buf[2] = dat;
mdec->phase = 1;
mdec->btn = mdec->buf[0] & 0x07;
mdec->x = mdec->buf[1];
mdec->y = mdec->buf[2];
if ((mdec->buf[0] & 0x10) != 0) {
mdec->x |= 0xffffff00;
}
if ((mdec->buf[0] & 0x20) != 0) {
mdec->y |= 0xffffff00;
}
mdec->y = - mdec->y; /* 鼠标的y方向和屏幕的y方向相反 */
return 1;
}
return -1; /* 应该不会到这里来 */
}
- 结构体MOUSE_DEC新增变量:
- x代表鼠标移动的X方向位移。
- y代表鼠标移动的Y方向位移。
- btn代表按键状态。
- 当
mdec->phase = 1
时,代码if ((dat & 0xc8) == 0x08)
用于判断第一个字节对移动有反应的部分(56位)是否在03的范围内,同时判断第一个字节对点击有反应的部分(13位)是否在8F的范围内。
- 只有当dat=00xx1xxx的时候if语句才为真。此时,dat的对应位必然在对应范围内。
- 这样做是因为,防止偶尔鼠标出错。
- 当
mdec->phase = 3
时,鼠标按键的状态存放在mdec->buf[0]的低3位。因此只需要按位与0x07即可得到btn。 - 对于x和y的获得,直接使用mdec->buf[1]和mdec->buf[2]即可。同时,需要根据第1字节中对鼠标移动有反应的几位信息,将x和y的第8位以及第8位以后全部设成1,或者全部保留为0.
- mdec->buf[0]:00xx1xxx。高4位的
00xx
是有关鼠标移动的。其中第5位的x如果是1,代表鼠标在X方向有变化,即x值需要更新。第6位的x如果是1,代表鼠标在Y方向上有变化,即y值需要更新。
- 最后,再注意一下,鼠标的Y方向和屏幕的Y方向是相反的,因此y的值需要取其相反数。
- 修改bootpack.c中的HariMain函数(节选):
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
io_stihlt();
} else {
if (fifo8_status(&keyfifo) != 0) {
i = fifo8_get(&keyfifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
if (mouse_decode(&mdec, i) != 0) {
/* 鼠标的3字节数据都准备好了 */
sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
if ((mdec.btn & 0x01) != 0) {
s[1] = 'L';
}
if ((mdec.btn & 0x02) != 0) {
s[3] = 'R';
}
if ((mdec.btn & 0x04) != 0) {
s[2] = 'C';
}
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
}
}
- mdec.btn的低3位abc:
- 如果第1位是1,代表鼠标左键被按下。
- 如果第2位是1,代表鼠标右键被按下。
- 如果第3位是1,代表鼠标中键被按下。
- 这里再提一下boxfill8函数,它的作用是用特定的颜色画一个矩形。
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;
}
return;
}
- 矩形范围是(x0, y0)-(x1, y1)。
make run
:
- 移动鼠标:
- 点击鼠标左键:
还不错哦。
4. 移动鼠标指针(harib05d)
- 因为鼠标解读已经完成了,现在只需要改一下图形显示,这样就能让鼠标在屏幕上动起来了。
- bootpack.c中的HariMain函数(节选):
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
if (mouse_decode(&mdec, i) != 0) {
/* 鼠标3字节已经准备好 */
sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
if ((mdec.btn & 0x01) != 0) {
s[1] = 'L';
}
if ((mdec.btn & 0x02) != 0) {
s[3] = 'R';
}
if ((mdec.btn & 0x04) != 0) {
s[2] = 'C';
}
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
/* 鼠标指针的移动 */
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15, my + 15); /* 隐藏鼠标 */
mx += mdec.x;
my += mdec.y;
if (mx < 0) {
mx = 0;
}
if (my < 0) {
my = 0;
}
if (mx > binfo->scrnx - 16) {
mx = binfo->scrnx - 16;
}
if (my > binfo->scrny - 16) {
my = binfo->scrny - 16;
}
sprintf(s, "(%3d, %3d)", mx, my);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 0, 79, 15); /* 隐藏坐标 */
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); /* 显示坐标 */
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /* 描画鼠标 */
}
}
- 因为鼠标要移动,所以现需要将鼠标用boxfill8函数隐藏,然后重新计算鼠标的位置(不能超出屏幕范围),然后重新绘制鼠标。由于鼠标位置也得更新,所以坐标显示也得先隐藏后显示。
- 这里说明一下,
make run
使用QEMU运行系统时会产生一个错误(这个错误从第7天的harib04a就已经产生了),就是按下一个按键字母(比如A),会一直产生键盘中断。使用VMware便不会产生这样的情况。所以,在不用到键盘按键的时候我们使用QEMU运行,否则使用VMware。 - 使用VMware运行:
鼠标终于能动起来了。
经过了伊凡‘苦战’:完成了GDT/IDT/PIC的初始化,使用了FIFO缓冲区,学会了处理键盘中断。 - 由于没有考虑到叠加处理,我们在移动鼠标的时候回产生如下‘尴尬’情形。
5. 通往32位模式之路
现在是2020.4.7 10:03,我又重新开始进行毕业设计相关的工作了。
从4.2到今天过去四天了。从4.2号起,腰痛得厉害,感觉要断了,贴了膏药也无济于事。椅子太差劲,坐在上面导致腰痛是必然。于是连夜买了桌椅套装和按摩器。趁着清明放假,我也给自己放个假期。昨天桌椅到了,傍晚趁着晚霞,装起来了桌椅。现在,坐在新桌椅上,用按摩器按摩着腰。
- 第3天中,进入32位模式时,源代码asmhead.nas并未解释。
- harib05d中的asmhead.nas:
; haribote-os
; TAB=4
BOTPAK EQU 0x00280000
DSKCAC EQU 0x00100000
DSKCAC0 EQU 0x00008000
; 有关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
; 用BIOS获取键盘上各种LED指示灯的状态。
MOV AH,0x02
INT 0x16 ; keyboard BIOS
MOV [LEDS],AL
; PIC关闭一切中断
; 根据AT兼容机的规格,如果初始化PIC
; 必须在CLI之前进行,否则有时会挂起
; 随后进行PIC的初始化
MOV AL,0xff
OUT 0x21,AL
NOP ; 如果连续执行OUT指令,有些机种会无法正常运行
OUT 0xa1,AL
CLI ; 禁止CPU级别的中断
; 为了让CPU能够访问1MB以上的内存空间,设定A20GATE
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
; 切换到保护模式
[INSTRSET "i486p"] ; 想要使用486指令的叙述
LGDT [GDTR0] ; 临时设定GDT
MOV EAX,CR0
AND EAX,0x7fffffff ; 设bit31为0(为了禁止分页)
OR EAX,0x00000001 ; 设bit0为1(为了切换到保护模式)
MOV CR0,EAX
JMP pipelineflush
pipelineflush:
MOV AX,1*8 ; 可读写的段 32bit
MOV DS,AX
MOV ES,AX
MOV FS,AX
MOV GS,AX
MOV SS,AX
; bootpack的转送
MOV ESI,bootpack ; 转送源
MOV EDI,BOTPAK ; 转送目的地
MOV ECX,512*1024/4
CALL memcpy
; 磁盘数据转送到它本来的位置去
; 首先从启动扇区开始
MOV ESI,0x7c00 ; 转送源
MOV EDI,DSKCAC ; 转送目的地
MOV ECX,512/4
CALL memcpy
; 所有剩下的
MOV ESI,DSKCAC0+512 ; 转送源
MOV EDI,DSKCAC+512 ; 转送目的地
MOV ECX,0
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; 从柱面数变换为字节数/4
SUB ECX,512/4 ; 减去IPL
CALL memcpy
; 必须由asmhead来完成的工作,至此全部完毕
; 以后就交给bootpack来完成
; bootpack的启动
MOV EBX,BOTPAK
MOV ECX,[EBX+16]
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
JZ skip ; 没有要转送的东西时
MOV ESI,[EBX+20] ; 转送源
ADD ESI,EBX
MOV EDI,[EBX+12] ; 转送目的地
CALL memcpy
skip:
MOV ESP,[EBX+12] ; 栈初始值
JMP DWORD 2*8:0x0000001b
waitkbdout:
IN AL,0x64
AND AL,0x02
IN AL,0x60 ; 空读(为了清空数据接收缓冲区中的垃圾数据)
JNZ waitkbdout ; AND的结果如果不是0,就跳到waitkdbout
RET
memcpy:
MOV EAX,[ESI]
ADD ESI,4
MOV [EDI],EAX
ADD EDI,4
SUB ECX,1
JNZ memcpy ; 减法运算的结果如果不是0,就跳到memcpy
RET
ALIGNB 16
GDT0:
RESB 8 ; NULL selector
DW 0xffff,0x0000,0x9200,0x00cf ; 可读写的段 32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 可执行的段 32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
- 代码:
; PIC关闭一切中断
; 根据AT兼容机的规格,如果初始化PIC
; 必须在CLI之前进行,否则有时会挂起
; 随后进行PIC的初始化
MOV AL,0xff
OUT 0x21,AL
NOP ; 如果连续执行OUT指令,有些机种会无法正常运行
OUT 0xa1,AL
CLI ; 禁止CPU级别的中断
相当于代码:
io_out8(PIC0_IMR, 0xff); /* 禁止主PIC的全部中断 */
io_out8(PIC1_IMR, 0xff); /* 禁止从PIC的全部中断 */
io_cli(); /* 禁止CPU级别的中断 */
- 在CPU进行模式切换的时候,需要禁止一切中断。同样,在后来进行的PIC初始化过程中,也不允许发生中断。因此,直接把全部中断屏蔽掉。
NOP指令:什么也不做,让CPU休息一个时钟长的时间。
- 代码:
; 为了让CPU能够访问1MB以上的内存空间,设定A20GATE
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
相当于代码:
#define KEYCMD_WRITE_OUTPORT 0xd1
#define KBC_OUTPORT_A20G_ENABLE 0xdf
/* A20GATE的设定 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_OUTPORT);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_OUTPORT_A20G_ENABLE);
wait_KBC_sendready();
- 这里的waitkbdout等同于wait_KBC_sendready【以后还会说明】。
- waitkbdout的代码待会儿有解释。
- 代码:
; 切换到保护模式
[INSTRSET "i486p"] ; 想要使用486指令的叙述
LGDT [GDTR0] ; 临时设定GDT
MOV EAX,CR0
AND EAX,0x7fffffff ; 设bit31为0(为了禁止分页)
OR EAX,0x00000001 ; 设bit0为1(为了切换到保护模式)
MOV CR0,EAX
JMP pipelineflush
pipelineflush:
MOV AX,1*8 ; 可读写的段 32bit
MOV DS,AX
MOV ES,AX
MOV FS,AX
MOV GS,AX
MOV SS,AX
- INSTRSET指令,是为了能够使用386以后的LGDT,EAX,CR0等关键字。
- LGDT指令,把随意准备的GDT读进来。对于这个暂定的GDT,我们以后还要重新设置。
- 然后将CR0这一特殊的32位寄存器的值代入EAX中,并将最高位设置为0,最低位设置为1,再将这个值写回CR0寄存器。这样就完成了模式转换,进入了不用分页的保护模式。 CR0,control register 0, 是一个非常重要的寄存器,只有OS能操作它。
- 代码:
; bootpack的转送
MOV ESI,bootpack ; 转送源
MOV EDI,BOTPAK ; 转送目的地
MOV ECX,512*1024/4
CALL memcpy
; 磁盘数据转送到它本来的位置去
; 首先从启动扇区开始
MOV ESI,0x7c00 ; 转送源
MOV EDI,DSKCAC ; 转送目的地
MOV ECX,512/4
CALL memcpy
; 所有剩下的
MOV ESI,DSKCAC0+512 ; 转送源
MOV EDI,DSKCAC+512 ; 转送目的地
MOV ECX,0
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; 从柱面数变换为字节数/4
SUB ECX,512/4 ; 减去IPL
CALL memcpy
- 简单来说,这部分程序只是在调用
memcpy
函数。 - 程序相当于代码:
memcpy(bootpack, BOTPAK, 512*1024/4);
memcpy(0x7c00, DSKCAC, 512/4);
memcpy(DSKCAC0+512, DSKCAC+512, cyls*512*18*2/4 - 512/4);
- 函数
memcpy
是复制内存的函数。语法是:
memcpy(转送内存源地址,转送内存目的地址,转送数据的大小)
- 转送数据的大小是以双字(DOWRD),所以数据大小要用字节数除以4来指定。
- ESI中存放源地址
- EDI中存放目的地址
- ECX中存放转送数据大小
- 常量定义:
BOTPAK EQU 0x00280000
DSKCAC EQU 0x00100000
DSKCAC0 EQU 0x00008000
- 先看
memcpy(0x7c00, DSKCAC, 512/4);
- DSKCAC是0x00100000,这句话的意思是,从内存地址0x7c00复制512字节到内存地址0x100000中去。 也就是说,将启动扇区的512字节复制到1MB以后的内存中去。
- 再看
memcpy(DSKCAC0+512, DSKCAC+512, cyls*512*18*2/4 - 512/4);
- DSKCAC0是0x8000,这句代码的意思是:从内存地址为0x8200复制x字节到0x100200去。
- x是(cyls51218*2/4 - 512/4)*4。
- 这样,始于0x100000的内存,就和磁盘的内容吻合了。
- IMUL是乘法运算,SUB是减法运算。
- 最后看
memcpy(bootpack, BOTPAK, 512*1024/4);
- bootpack是asmhead.nas的最后一个标签。通过Makefile文件可以知道,haribote.sys是由asmhead.bin和bootpack.hrb连接起来的,所以asmhead结束的地方,紧接着就是bootpack.hrb最前面的部分。
- 这句代码的意思是:从内存地址bootpack复制512*1024个字节(512KB)到内存地址0x280000中去。
- 512字节比bootpack.hrb大得多,这是因为多一点防止出错。
- 代码: 存疑5
; 必须由asmhead来完成的工作,至此全部完毕
; 以后就交给bootpack来完成
; bootpack的启动
MOV EBX,BOTPAK
MOV ECX,[EBX+16]
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
JZ skip ; 没有要转送的东西时
MOV ESI,[EBX+20] ; 转送源
ADD ESI,EBX
MOV EDI,[EBX+12] ; 转送目的地
CALL memcpy
skip:
MOV ESP,[EBX+12] ; 栈初始值
JMP DWORD 2*8:0x0000001b
- 我们依然是在做memcpy。它对bootpack.hrb的header进行解析,将执行所必须的数据传送过去。EBX中带入的是BOTPAK,所以值如下:
这些值因harib的版本不同而不同。
比如,在harib05d中,这些值分别是:
- [EBX+16]-----bootpack.hrb之后的第16号地址,值为0x000011a8.(高地址高位,小端模式。)
- [EBX+20]-----bootpack.hrb之后的第20号地址,值为0x00001104.
- [EBX+12]-----bootpack.hrb之后的第12号地址,值为0x00310000.
SHR指令是右移位指令
JZ, jump if zero,根据前一个计算结果是否为0来决定是否跳转。如果是0,则跳转,如果不是0则不跳转。
- 那么,ESI=0x00001104,EDI=0x00310000,ECX=0x000011a8。
- 所以,skip之前的代码的意思是:将bootpack.hrb第0x1104字节开始的0x11a8字节复制到0x310000号地址去。
- 这里显得很奇怪,后面还会解释。
- skip代码:
- 将0x3100000带入ESP中
- 使用特殊的JMP指令,将2*8代入到CS中,同时移到0x1b号地址。这里的0x1b号地址是指第2个段的0x1b号地址。第2个段的基地址是0x280000,所以实际上是从0x28001b开始执行的。这也就是bootpack.hrb的0x1b号地址。
- 我们在设计时,bootpack使用的就是第2个段,(第1个段是全部内存),起始地址就是0x280000。
- 内存分布图:
- 0x00000000 - 0x000fffff : 在启动时多次使用,之后变空。(1MB)
- 0x00100000 - 0x00267fff : 用于保存软盘内容。(1440KB)
- 0x00268000 - 0x0026f7ff : 空。(30KB)
- 0x0026f800 - 0x0026ffff : IDT。(2KB)
- 0x00270000 - 0x0027ffff : GDT。(64KB)
- 0x00280000 - 0x002fffff : bootpack.hrb。(512KB)
- 0x00300000 - 0x003fffff : 栈及其他。(1MB)
- 0x00400000 - : 空。
其中,0x00000000 - 0x000fffff部分的细分:
- 0x00000000 - 0x00007bff : 被占用,但是具体是干什么的现在未知,但有一部分是关于VRAM的。(31KB)
- 0x00007c00 - 0x00007dff : 用于启动区。(512B)
- 0x00008000 - 0x000081ff : 启动区使用。(512B)
- 0x00008200 - 0x00034fff : 软盘内容,10个柱面。(180KB)
- 0x00034fff - 0x000fffff : 未知。(812KB)
- 代码:
waitkbdout:
IN AL,0x64
AND AL,0x02
IN AL,0x60 ; 空读(为了清空数据接收缓冲区中的垃圾数据)
JNZ waitkbdout ; AND的结果如果不是0,就跳到waitkdbout
RET
- waitkbdout和wait_KBC_sendready差不多。但是添加了从设备号0x60的设备上进行IN的处理,也就是说,如果控制器里有键盘代码或者积累了鼠标数据,就顺便将它们读出来。
#define PORT_KEYSTA 0x0064
#define KEYSTA_SEND_NOTREADY 0x02
void wait_KBC_sendready(void)
{
/* 等待键盘控制电路准备就绪 */
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
break;
}
}
return;
}
JNZ,和JZ相反,意思是jump if not zero.
- 代码:
memcpy:
MOV EAX,[ESI]
ADD ESI,4
MOV [EDI],EAX
ADD EDI,4
SUB ECX,1
JNZ memcpy ; 减法运算的结果如果不是0,就跳到memcpy
RET
- 复制内存的程序。
- ESI中存放源开始地址;EDI中存放目的开始地址;ECX中存放的是数据大小(因为ECX是32位寄存器,所以字节数/4).
- 代码:
ALIGNB 16
GDT0:
RESB 8 ; NULL selector
DW 0xffff,0x0000,0x9200,0x00cf ; 可读写的段 32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 可执行的段 32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
ALIGNB
:align是排整齐的意思。ALIGNB的意思是,一直添加DBO(空指令,还是DB 0
?),存疑6
直到‘时机合适’为止。
ALIGNB 16的情况下,地址能被16整除的时候,就称为“时机合适”。如果最初的地址能被16整除,那么ALIGNB指令不做任何处理。- GDT0也是一种特定的GDT。0号是空区域(null sector),
不能够在那里定义段。1号和2号分别由下面代码设定:
#define LIMIT_BOTPAK 0x0007ffff
#define ADR_BOTPAK 0x00280000
#define AR_DATA32_RW 0x4092
#define AR_CODE32_ER 0x409a
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
- 相当于代码:
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
- 查看函数set_segment:
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit & 0xffff;
sd->base_low = base & 0xffff;
sd->base_mid = (base >> 16) & 0xff;
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
sd->base_high = (base >> 24) & 0xff;
return;
}
- 查看CPU有关GDT的设定图
-
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
注意:0xffffffff > 0xfffff, 那么limit = 0xfffff, ar = 0xc092。具体8字节怎么填,按照查看CPU有关GDT的设定图填即可。最终应该写的8字节数据就是:0xffff 0x0000 0x9200 0x00cf
。 - 同理,
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
按照查看CPU有关GDT的设定图,最终的8字节数据就是0xffff 0x0000 0x9a28 0047
。 - 这样,DW后面的数据也就明朗了。
- GDTR0是LGDT指令,意思是通知GDT0说:“有GDT了哦。”代码的意思是,写入了16位的上限(8*3-1),和32位的段起始地址(GDT0)
- 再熟悉一下DB,DW,DD:
- DB:往内存中写入8位数据(1字节)。
- DW:往内存中写入16位数据(2字节,1字)
- DD:往内存中写入32位数据(4字节,双字)
- 到此为止,asmhead.nas的解释就完成了。也就是说,最初状态时,GDT在asmhead.nas中,而不在0x00270000-0x0027ffff的范围里。 IDT连设定都没设定,所以仍然处于中断禁止的状态。 应当趁着硬件上积累过多数据而产生误动作之前,尽快开放中断,接收数据。
- 因此应该在bootpack.c的HariMain里,应该在进行调色板(palette)的初始化以及画面的准备之前,先赶紧重新创建GDT和IDT,初始化PIC,并执行io_sti()开放中断。
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
char s[40], mcursor[256], keybuf[32], mousebuf[128];
int mx, my, i;
struct MOUSE_DEC mdec;
init_gdtidt();
init_pic();
io_sti(); /* IDT/PIC的初始化已经完成,因此开放CPU的中断 */
fifo8_init(&keyfifo, 32, keybuf);
fifo8_init(&mousefifo, 128, mousebuf);
io_out8(PIC0_IMR, 0xf9); /* 开放PIC1和键盘中断(11111001) */
io_out8(PIC1_IMR, 0xef); /* 开放鼠标中断(11101111) */
init_keyboard();
init_palette();
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
……
6. 腰痛
- 腰痛不是一个好信号。
- 缺乏锻炼,一坐就是几个小时,椅子不好。这些都是腰痛的原因。
- 今晚的膏药起了作用,腰间凉凉的,有点舒服的感觉。
- 在手机上设了闹钟,每45min响一次,提醒自己改休息一下。
- 现在是2020.4.7 22:30。夜已经深了,父母早已睡下。
- 加油,明天继续。第9天,内存管理。
- 这次Markdown代码的行数是825行,又’长’了一点儿(笑)。