第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])与鼠标上下移动有关
- 上图中红框里面的3字节数据
-
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值需要更新。
- mdec->buf[0]:00xx1xxx。高4位的
-
最后,再注意一下,鼠标的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)。
- mdec.btn的低3位abc:
-
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。
- 我们依然是在做memcpy。它对bootpack.hrb的header进行解析,将执行所必须的数据传送过去。EBX中带入的是BOTPAK,所以值如下:
-
内存分布图:
- 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.
- waitkbdout和wait_KBC_sendready差不多。但是添加了从设备号0x60的设备上进行IN的处理,也就是说,如果控制器里有键盘代码或者积累了鼠标数据,就顺便将它们读出来。
-
代码:
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行,又’长’了一点儿(笑)。