一、鼠标解读
鼠标收到激活指令后,发送的第一个按键编码是0xfa,之后,每次从鼠标发送过来的数据都是3个字节一组的。
移动鼠标时,第一个字节的高四位会在0~3的范围内变化,也就是说第七位和第八位始终为0,鼠标向左移动时第五位为1,向下移动时第六位为1;点击鼠标时,第一个字节的低四位会在8~F之间变化,也就是说第四位始终为1,左键点击时第一位为1,右键点击时第二位为1,滚轮键点击时第三位为1。
第二个字节与鼠标的左右移动有关,如果把一个字节当作整数的补码来看,向右移动时为正,向左移动为负,且移动速度越快绝对值越大,范围为-128~127。
第三个字节与鼠标的上下移动有关,如果也把它作为整数的补码来解析,向上为正,向下为负。
bootpack.c节选
struct MOUSE_DEC { //解读鼠标所需要的变量
unsigned char buf[3], phase; //缓冲区,鼠标阶段标志
int x, y, btn; //x和y方向移动信息,鼠标按键状态
};
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{
if (mdec->phase == 0) {
/* 等待鼠标的0xfa的阶段 */
if (dat == 0xfa) {
mdec->phase = 1;
}
return 0;
}
if (mdec->phase == 1) {
/* 等待鼠标第一字节的阶段 */
if ((dat & 0xc8) == 0x08) {
/* 判断第一字节的第七位、第八位是否为0,第四位是否为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; //取出buf[0]的低三位
mdec->x = mdec->buf[1];
mdec->y = mdec->buf[2];
if ((mdec->buf[0] & 0x10) != 0) { //需要注意的是x和y是int型,占四个字节,而buf[0]只占一个字节,如果鼠标向左移动,则buf[0]解释为一个字节的整数为负数,扩展为四字节整数,补码要保证值不变,需要将高三个字节全部补1,也就是并0xffffff00
mdec->x |= 0xffffff00;
}
if ((mdec->buf[0] & 0x20) != 0) { //如果鼠标向下移动,将y的高三个字节全部补1
mdec->y |= 0xffffff00;
}
mdec->y = - mdec->y; /* 鼠标与屏幕的y方向相反 */
return 1;
}
return -1; /* 应该不会到这儿来 */
}
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) { //第一位是1,左键被按下
s[1] = 'L';
}
if ((mdec.btn & 0x02) != 0) { //第二位是1,右键被按下
s[3] = 'R';
}
if ((mdec.btn & 0x04) != 0) { //第三位是1,滚轮被按下
s[2] = 'C';
}
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 25 * 8 - 1, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
二、移动鼠标指针
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); /* 描画鼠标 */
}
}
三、通往32位模式之路
第一次提到使用32位模式是在第三天,所以就重新复习一下之前的内容,将知识结合起来弄懂asmhead.nas到底做了些什么。
所谓的32位模式,指的是CPU模式,CPU有16位和32位两种模式,以16位模式启动时,用AX和CX等16位寄存器会非常方便,相反,使用EAX和ECX等32位寄存器会非常麻烦,除此之外,16位和32位模式的机器语言的命令代码不一样,同样的机器语言解释的方法也不一样,所以16位模式的机器语言在32位模式下不能运行,反之亦然。
16位模式下,如果能用一个寄存器来表示内存地址的话,由于BX只能表示0~0xffff的值,也就是只有0~65535,最大64KB,为了能访问更多内存地址,当时设计了一个起辅助作用的段寄存器,在指定内存地址时,可以使用这个段寄存器,使用段寄存器时以ES:BX这种方式来表示地址,写成“MOV AL,[ES:BX]”,代表ES×16+BX的内存地址,如果在ES和BX里都代入0xffff,也就是65535×16+65535=1M+64K-16Bytes,也就是可以指定1M以内的内存地址。
32位模式下可以使用的内存容量远远大于1MB,另外32位下可以使用CPU的自我保护功能(识别出可疑的机器语言并屏蔽,以免破坏系统),但是在32位模式下不能调用BIOS(Basic Input Output System,基础输入输出系统)功能,这是因为BIOS是用16位机器语言写的,如果我们需要用到BIOS来做什么事情,就全部放在进入32位模式之前来做。
我们需要BIOS做什么呢?设定画面模式、设置键盘状态、初始化段表和中断记录表,也就是第三天至今天所做的事情。
asmhead.nas
; haribote-os boot asm
; TAB=4
BOTPAK EQU 0x00280000 ; bootpack的传送目的地
DSKCAC EQU 0x00100000 ; 磁盘传送目的地
DSKCAC0 EQU 0x00008000 ; 磁盘传送源
; 有关BOOT_INFO
CYLS EQU 0x0ff0 ; cylinders,启动区柱面数,没找到在哪里赋值过
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 颜色的位数
SCRNX EQU 0x0ff4 ; 分辨率的x
SCRNY EQU 0x0ff6 ; 分辨率的y
VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址
ORG 0xc200 ; 这个程序要被装载到的内存地址
; 设置画面模式
MOV AL,0x13 ; VGA显卡,320×200×8位彩色
MOV AH,0x00
INT 0x10 ; 调用BIOS中断设置画面模式,详情见 https://en.wikipedia.org/wiki/BIOS_interrupt_call
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 ; 将BIOS调用的返回值保存到[LEDS]
; PIC(programmable interrupt controller,可编程中断控制器)关闭一切中断
; 根据AT兼容机的规格,如果要初始化PIC,必须在CLI之前进行,否则有时会挂起。
; 随后进行PIC的初始化
MOV AL,0xff ; PIC的中断屏蔽寄存器为8位寄存器,8位对应着8路IRQ信号,如果某一位的值是1,则该值对应的IRQ信号被屏蔽,PIC就忽视该路信号
OUT 0x21,AL ; 屏蔽主PIC的全部中断
NOP ; 让CPU休息一个时钟长的时间,如果连续执行OUT指令,有些机种会无法正常运行
OUT 0xa1,AL ; 屏蔽从PIC的全部中断
CLI ; 禁止CPU级别的中断
; 为了让CPU能够访问1MB以上的内存空间,设定A20GATE
CALL waitkbdout ; 调用函数waitkbdout(在这段代码的末尾有定义)
MOV AL,0xd1
OUT 0x64,AL ; 下一个命令传送给键盘控制电路的附属端口
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL ; 向这个附属端口发送0xdf,让A20Gate信号线变成ON的状态
CALL waitkbdout
; 切换到保护模式
[INSTRSET "i486p"] ; “想要使用486指令”的叙述,之后可以使用LGDT,EAX,CR0等关键字
LGDT [GDTR0] ; 设定临时GDT,从[GDTR0](在这段代码的末尾有定义)读取6个字节赋值给GDTR寄存器
MOV EAX,CR0 ; 将CR0(特殊的32位寄存器,只有操作系统才能操作它)的值代入EAX
AND EAX,0x7fffffff ; 将最高位置为0
OR EAX,0x00000001 ; 将最低位置为1
MOV CR0,EAX ; 将EAX的值返回给CR0寄存器,进入保护模式,在这种模式下,应用程序既不能随便改变段的设定,又不能使用操作系统专用的段,操作系统受到CPU的保护
JMP pipelineflush ; 进入保护模式后机器语言的解释要发生变化,CPU使用流水线技术,在前一条命令执行的时候,就开始解释之后的命令了,变成保护模式后,要马上执行JMP指令,将解释但还未执行的命令重新解释一遍
pipelineflush:
MOV AX,1*8 ; 进入保护模式后,段寄存器的意思不再是乘16再加算了,除了CS以外的所有段寄存器的值都从0x0000变成了0x0008,CS如果变化会导致混乱,所以要放到后面再处理。0x0008相当于gdt+1的段,即段表中第1个表项的初始地址,0号是空区域,不能定义段
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 ; 传送数据大小,以4字节为单位,所以数据大小要除以4
CALL memcpy ; 调用函数memcpy(在这段代码的末尾有定义),将从bootpack的地址开始的512KB内容复制到0x00280000号地址去,这里还不理解为什么要把bootpack复制到0x00280000,它不属于磁盘吗,当前最纠结的问题就是这些文件的关系以及操作系统安装的过程
; 磁盘数据最终传送到它本来的位置去
; 首先从启动扇区开始
MOV ESI,0x7c00 ; 传送源
MOV EDI,DSKCAC ; 传送目的地
MOV ECX,512/4 ; 传送数据大小为一个扇区
CALL memcpy ; 将始于0x7c00的512字节复制到0x00100000
; 所有剩下的
MOV ESI,DSKCAC0+512 ; 传送源
MOV EDI,DSKCAC+512 ; 传送目的地
MOV ECX,0
MOV CL,BYTE [CYLS] ; 乘数
IMUL ECX,512*18*2/4 ; CYLS*512*18*2/4,CYLS为柱面数,与CL相乘之后的结果储存在ECX
SUB ECX,512/4 ; 减去IPL(Initial Program Loader,启动区)的大小
CALL memcpy ; 将始于0x00008200的CYLS*512*18*2-512字节复制到0x00100200
; 必须由asmhead来完成的工作,至此全部结束
; 以后就交由bootpack来完成
; bootpack的启动
MOV EBX,BOTPAK ; EBX赋值为bootpack.hrb的位置
MOV ECX,[EBX+16] ; bootpack.hrb的第16号地址,值是0x11a8
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
JZ skip ; 如果ECX>1,那么(ECX+1)/4必不为0,如果为0跳转到skip
MOV ESI,[EBX+20] ; 传送源,bootpack.hrb的第20号地址,值是0x10c8
ADD ESI,EBX
MOV EDI,[EBX+12] ; 传送目的地,bootpack.hrb的第12号地址,值是0x00310000
CALL memcpy
skip:
MOV ESP,[EBX+12] ; 栈初始值
JMP DWORD 2*8:0x0000001b ; 2*8即CS中的第二个段,跳转到第二个段的0x1b号地址,第二个段的基址为0x280000,所以实际上是从0x28001b开始执行的,也就是bootpack.hrb的0x1b号地址
; 后面的四个定义都是前面代码用到的
waitkbdout: ; 等待键盘控制电路可以接受CPU的指令
IN AL,0x64
AND AL,0x02 ; 空读(为了清空数据接收缓冲区中的垃圾数据)
JNZ waitkbdout ; AND的结果如果不是0,就不断循环waitkbout直至为0
RET
memcpy:
MOV EAX,[ESI]
ADD ESI,4
MOV [EDI],EAX
ADD EDI,4
SUB ECX,1
JNZ memcpy ; 减法运算的结果如果不是0,就不断循环memcpy
RET
; memcpyはアドレスサイズプリフィクスを入れ忘れなければ、ストリング命令でも書ける
ALIGNB 16
GDT0:
RESB 8 ; 空区域,不能定义段
DW 0xffff,0x0000,0x9200,0x00cf ; 可以读写的段
DW 0xffff,0x0000,0x9a28,0x0047 ; 可以执行的段(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
; 接下来就开始执行bootpack.hrb了
bootpack:
关于A20 GATE的补充
很多稀奇古怪的东西都是由于系统升级时,为了保持向下兼容而产生的,A20 Gate就是其中之一。
在8086/8088中,只有20根地址总线,所以可以访问的地址是2^20=1M,但由于8086/8088是16位地址模式,能够表示的地址范围是0-64K,所以为了在8086/8088下能够访问1M内存,Intel采取了分段的模式:16位段基地址:16位偏移。其绝对地址计算方法为:16位基地址左移4位+16位偏移=20位地址。
但这种方式引起了新的问题,通过上述分段模式,能够表示的最大内存为:FFFFh:FFFFh=FFFF0h+FFFFh=10FFEFh=1M+64K-16Bytes(1M多余出来的部分被称做高端内存区HMA)。但8086/8088只有20位地址线,如果访问100000h~10FFEFh之间的内存,则必须有第21根地址线。所以当程序员给出超过1M(100000H-10FFEFH)的地址时,系统并不认为其访问越界而产生异常,而是自动从重新0开始计算,也就是说系统计算实际地址的时候是按照对1M求模的方式进行的,这种技术被称为wrap-around。
到了80286,系统的地址总线发展为24根,这样能够访问的内存可以达到2^24=16M。Intel在设计80286时提出的目标是,在实模式下,系统所表现的行为应该和8086/8088所表现的完全一样,也就是说,在实模式下,80286以及后续系列,应该和8086/8088完全兼容。但最终,80286芯片却存在一个BUG:如果程序员访问100000H-10FFEFH之间的内存,系统将实际访问这块内存,而不是象过去一样重新从0开始。
为了解决上述问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根),被称为A20 Gate:如果A20 Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;如果A20 Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式。绝大多数IBM PC兼容机默认的A20 Gate是被禁止的。由于在当时没有更好的方法来解决这个问题,所以IBM使用了键盘控制器来操作A20 Gate,但这只是一种黑客行为,毕竟A20 Gate和键盘操作没有任何关系。在许多新型PC上存在着一种通过芯片来直接控制A20 Gate的BIOS功能。从性能上,这种方法比通过键盘控制器来控制A20 Gate要稍微高一点。
上面所述的内存访问模式都是实模式,在80286以及更高系列的PC中,即使A20 Gate被打开,在实模式下所能够访问的内存最大也只能为10FFEFH,尽管它们的地址总线所能够访问的能力都大大超过这个限制。为了能够访问10FFEFH以上的内存,则必须进入保护模式。(其实所谓的实模式,就是8086/8088的模式,这种模式存在的唯一理由就是为了让旧的程序能够继续正常的运行在新的PC体系上)
引自:https://wenku.baidu.com/view/2eae56bff121dd36a32d828e.html