自制操作系统day8 (鼠标数据取得、通往32位模式之路、A20GATE、切换到保护模式、控制寄存器cr0-cr4以及cr8、ALIGNB)

发布于:2025-05-23 ⋅ 阅读:(15) ⋅ 点赞:(0)

day8

鼠标数据取得方法

fifo8_init(&mousefifo, 128, mousebuf);
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();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}
}
}

这段代码是一个处理键盘和鼠标输入事件的主循环,通过FIFO队列缓冲数据,并在屏幕上实时显示输入信息。以下是逐行解释:


一、代码功能概述

  1. 初始化鼠标FIFO队列

    fifo8_init(&mousefifo, 128, mousebuf);  // 初始化鼠标FIFO,容量128字节,缓冲区为mousebuf
    
    
    • 为鼠标数据分配缓冲区,确保中断接收的数据能暂存。
  2. 主事件循环

    for (;;) {
        io_cli();  // 关中断,防止数据竞争
        if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) { // 无数据时
            io_stihlt();  // 开中断并休眠(等待中断唤醒)
        } else {  // 有数据时处理
            // 处理键盘或鼠标事件
        }
    }
    
    

二、核心代码逻辑

1. 关中断与休眠

  • io_cli():关闭CPU中断,确保检查FIFO状态时不会被中断打断。
  • io_stihlt():重新开启中断并执行HLT指令,让CPU休眠直到下一个中断到来。
    • 目的:避免忙等待(Busy Waiting),节省CPU资源。

2. 处理键盘事件

if (fifo8_status(&keyfifo) != 0) {  // 键盘FIFO有数据
    i = fifo8_get(&keyfifo);        // 取出键盘数据
    io_sti();                       // 开中断(允许新中断)
    sprintf(s, "%02X", i);          // 将数据转为十六进制字符串
    // 清空屏幕区域(0,16)到(15,31)
    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
    // 显示十六进制值
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
}

3. 处理鼠标事件

else if (fifo8_status(&mousefifo) != 0) {  // 鼠标FIFO有数据
    i = fifo8_get(&mousefifo);            // 取出鼠标数据
    io_sti();                             // 开中断
    sprintf(s, "%02X", i);                // 转为十六进制字符串
    // 清空屏幕区域(32,16)到(47,31)
    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
    // 显示十六进制值
    putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}


三、关键函数与参数

1. FIFO操作函数

  • fifo8_init(fifo, size, buf):初始化FIFO队列,指定容量和缓冲区地址。
  • fifo8_status(fifo):返回队列中未处理的数据量。
  • fifo8_get(fifo):从队列头部取出一个字节数据。

2. 屏幕操作函数

  • boxfill8(vram, scrnx, color, x0, y0, x1, y1)

    在指定屏幕区域填充颜色。

    • vram:显存地址。
    • scrnx:屏幕水平分辨率(像素)。
    • color:颜色值(如COL8_008484表示蓝绿色)。
    • x0, y0, x1, y1:填充区域的左上角和右下角坐标。
  • putfonts8_asc(vram, scrnx, x, y, color, str)

    在屏幕指定位置绘制ASCII字符串。

    • x, y:文本起始坐标。
    • str:要显示的字符串。

四、代码执行流程

  1. 初始化
    • 鼠标FIFO队列初始化,准备接收数据。
  2. 进入主循环
    • 关中断:防止处理数据时被中断打断。
    • 检查队列:若键盘和鼠标队列均为空,则开中断并休眠。
    • 处理数据:若队列非空,按优先级处理键盘数据(先)或鼠标数据(后)。
  3. 显示数据
    • 将数据转换为十六进制字符串。
    • 清空屏幕局部区域(避免旧数据残留)。
    • 在指定位置绘制新数据。

五、设计细节分析

1. 中断控制

  • 关中断(io_cli

    在检查FIFO状态前关闭中断,确保检查与取数据的操作是原子的,避免竞态条件(Race Condition)。

  • 开中断(io_sti

    在取出数据后立即开启中断,允许新的中断处理程序填充队列。

2. 屏幕刷新优化

  • 局部刷新
    仅刷新显示数据的区域(如0,1615,31),而非全屏刷新,减少显存操作开销。

3. 数据格式转换

  • 十六进制显示
    将原始扫描码或鼠标数据以十六进制显示,便于调试观察(如0x1C代表按键A按下)。

六、性能与扩展性

  • 低功耗休眠

    通过io_stihlt()在无数据时休眠,显著降低CPU占用率。

  • 缓冲区容量

    鼠标FIFO容量为128字节,足够容纳多个鼠标数据包(每个包3字节),避免溢出。

  • 扩展性

    可扩展支持更多输入设备(如游戏手柄),只需增加对应的FIFO队列和处理逻辑。


总结

这段代码实现了一个高效的输入事件处理循环,核心特点包括:

  1. 中断安全:通过io_cli/io_sti确保数据操作的原子性。
  2. 低功耗:无数据时休眠,减少资源消耗。
  3. 实时反馈:即时将输入数据可视化,便于调试或用户交互。

该设计是操作系统输入子系统的典型实现,适用于需要实时响应外设的场景(如嵌入式系统或低级硬件驱动开发)。


鼠标解读(1)(harib05a

struct MOUSE_DEC {
unsigned char buf[3], phase;
};
void enable_mouse(struct MOUSE_DEC *mdec);
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat);
void HariMain(void)
{
(中略)
struct MOUSE_DEC mdec;
(中略)
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) {
/* 3字节都凑齐了,所以把它们显示出来*/
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) {
/* 等待鼠标的0xfa的阶段 */
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; /* 应该不可能到这里来 */
}


以下是代码中关于鼠标处理的详细解释:


一、数据结构与初始化

1. 鼠标解码结构体 MOUSE_DEC

struct MOUSE_DEC {
    unsigned char buf[3], phase; // 存储3字节数据包和解码阶段
};

  • buf[3]:存储PS/2鼠标的3字节数据包。
  • phase:标识当前解码阶段(0-3),用于追踪数据包接收进度。

2. 启用鼠标 enable_mouse()

void enable_mouse(struct MOUSE_DEC *mdec) {
    wait_KBC_sendready();            // 等待键盘控制器就绪
    io_out8(PORT_KEYCMD, 0xD4);      // 发送命令:下一字节发送到鼠标
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, 0xF4);      // 发送命令:启用鼠标数据报告
    mdec->phase = 0;                 // 初始化阶段0(等待ACK)
}

  • 关键步骤
    1. 通过键盘控制器(端口0x64)发送命令0xD4,通知后续数据发送到鼠标。
    2. 发送0xF4到数据端口(0x60),激活鼠标数据报告模式。
    3. 初始化phase=0,等待鼠标返回ACK(0xFA)。

二、鼠标数据解码 mouse_decode()

int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat) {
    if (mdec->phase == 0) {          // 阶段0:等待ACK(0xFA)
        if (dat == 0xFA) {           // 收到ACK
            mdec->phase = 1;         // 进入阶段1(接收字节1)
        }
        return 0;
    } else if (mdec->phase == 1) {   // 阶段1:接收数据包第1字节
        mdec->buf[0] = dat;
        mdec->phase = 2;             // 进入阶段2(接收字节2)
        return 0;
    } else if (mdec->phase == 2) {   // 阶段2:接收数据包第2字节
        mdec->buf[1] = dat;
        mdec->phase = 3;             // 进入阶段3(接收字节3)
        return 0;
    } else if (mdec->phase == 3) {   // 阶段3:接收数据包第3字节
        mdec->buf[2] = dat;
        mdec->phase = 1;             // 重置阶段1(等待下一数据包)
        return 1;                    // 返回1表示完整数据包
    }
    return -1;                       // 错误状态
}

  • 解码流程
    1. 阶段0:等待鼠标返回ACK(0xFA),确认启用成功。
    2. 阶段1-3:依次接收数据包的3个字节,存储到buf数组。
    3. 完成解码:当3字节收齐后,返回1,并重置阶段为1以接收下一数据包。

三、主循环中的鼠标处理

for (;;) {
    io_cli(); // 关中断
    if (fifo8_status(&mousefifo) != 0) {
        i = fifo8_get(&mousefifo);   // 从队列取数据
        io_sti(); 
       // CLI(Clear Interrupt)和STI(Set Interrupt)// 开中断
        if (mouse_decode(&mdec, i) != 0) { // 解码成功
            // 显示3字节数据(十六进制)
            sprintf(s, "%02X %02X %02X", mdec.buf[0], mdec.buf[1], mdec.buf[2]);
            boxfill8(...);           // 清空屏幕区域
            putfonts8_asc(...);      // 显示数据
        }
    }
}

  • 处理流程
    1. 从鼠标FIFO队列中读取数据字节。
    2. 调用mouse_decode解码,若返回1(完整数据包),则显示三个字节的十六进制值。

四、PS/2鼠标数据包格式

标准3字节数据包

字节 位7-0 说明
Byte 0 Y溢出 X溢出
Byte 1 X轴移动量(8位补码,-128~127) 水平移动量(左/右)
Byte 2 Y轴移动量(8位补码,-128~127) 垂直移动量(下/上)
  • 溢出处理:若X/Y溢出位为1,表示移动量超过8位范围(需特殊处理)。
  • 符号位:X/Y符号位为1表示负向移动(左/下)。

五、关键硬件交互

1. 键盘控制器(KBC)端口

  • PORT_KEYCMD (0x64):发送命令到键盘控制器。
  • PORT_KEYDAT (0x60):读写数据(键盘/鼠标)。

2. 中断与FIFO

  • IRQ12:鼠标中断,触发时将数据存入mousefifo
  • FIFO队列:缓冲中断接收的数据,主循环异步处理。

六、注意事项

  1. ACK处理:启用鼠标后需等待0xFA确认,否则后续数据可能错位。
  2. 数据包顺序:需严格按顺序接收3字节,否则解析错误。
  3. 符号与溢出:需正确处理补码和溢出标志,以准确计算鼠标移动。

总结

这段代码通过以下步骤实现鼠标功能:

  1. 初始化:激活鼠标并等待ACK。
  2. 数据接收:通过中断和FIFO缓冲原始字节。
  3. 数据解码:按阶段拼装3字节数据包。
  4. 数据显示:将数据包内容输出到屏幕。

理解PS/2协议和状态机管理是处理输入设备的核心,此代码为操作系统输入子系统的基础实现。


鼠标解读(2)(harib05c


struct MOUSE_DEC {
unsigned char buf[3], phase;
int x, y, btn;
};
/*
结构体里增加的几个变量用于存放解读结果。这几个变量是x、y和btn,分别用于
存放移动信息和鼠标按键状态。*/
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~3的范围内;同时还要判断第一字节对点击有反应的
部分是否在8~F的范围内。如果这个字节的数据不在以上范围内,它就会被舍去。
虽说基本上不这么做也行,但鼠标连线偶尔也会有接触不良、即将断线的可能,这
时就会产生不该有的数据丢失,这样一来数据会错开一个字节。数据一旦错位,就
不能顺利解读,那问题可就大了。而如果添加上对第一字节的检查,就算出了问
题,鼠标也只是动作上略有失误,很快就能纠正过来,*/
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];
/***标准3字节数据包**

| **字节** | **位7-0** | **说明** |
| --- | --- | --- |
| Byte 0 | Y溢出 | X溢出 |
| Byte 1 | X轴移动量(8位补码,-128~127) | **水平移动量**(左/右) |
| Byte 2 | Y轴移动量(8位补码,-128~127) | **垂直移动量**(下/上) |
- **溢出处理**:若X/Y溢出位为1,表示移动量超过8位范围(需特殊处理)。
- **符号位**:X/Y符号位为1表示负向移动(左/下)。*/
if ((mdec->buf[0] & 0x10) != 0) {
mdec->x |= 0xffffff00;
/*|= 是按位或赋值操作符:
等价于 mdec->x = mdec->x | 0xffffff00
作用是将x变量的高24位全部置为1,保持低8位不变
将原始的8位有符号位移量(-128~127)
转换为32位有符号整数(-2147483648~2147483647)
保持数值不变的同时扩展存储空间
这个操作在底层设备驱动中很常见,
用于将硬件返回的补码(two's complement)有符号数扩展为CPU架构的标准整数格式。*/
}
if ((mdec->buf[0] & 0x20) != 0) {
mdec->y |= 0xffffff00;
}
mdec->y = - mdec->y; /* 鼠标的y方向与画面符号相反 */
return 1;
}
return -1; /* 应该不会到这儿来 */
}

} 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';
/*如果mdec.btn的最低位是1,就把s的第2个字符(注:第1个字
符是s[0] )换成‘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);
}
}

一、3字节数据包结构

PS/2鼠标通过3字节数据包上报移动和按键信息,每个字节的位定义如下:

1. 字节0(状态字节)

名称 说明
7 Y溢出(YV) 1表示Y轴移动量超出8位补码范围(-127~127),需要特殊处理。
6 X溢出(XV) 1表示X轴移动量超出8位补码范围。
5 Y符号位(YS) 1表示Y轴负方向移动(向上),0表示正方向(向下)。
4 X符号位(XS) 1表示X轴负方向移动(向左),0表示正方向(向右)。
3 保留 固定为0。
2 中键(MB) 1表示中键按下。
1 右键(RB) 1表示右键按下。
0 左键(LB) 1表示左键按下。

2. 字节1(X轴移动量)

  • 8位补码:表示X轴偏移量,范围-128~127。
    • 正数:向右移动。
    • 负数:向左移动(通过符号位扩展为32位)。

3. 字节2(Y轴移动量)

  • 8位补码:表示Y轴偏移量,范围-128~127。
    • 正数:向下移动。
    • 负数:向上移动(需取反适配屏幕坐标系)。

移动鼠标指针(harib05d

这一步就是将鼠标显示在显示器上

先隐藏之前的鼠标,然后在鼠标指针的坐标上,加上解读得到的位移量

但是隐藏鼠标时填充的背景色需要考虑一下

} 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位模式之路(asmhead.nas代码解释)

关闭中断

; PIC关闭一切中断
; 根据AT兼容机的规格,如果要初始化PIC,
; 必须在CLI之前进行,否则有时会挂起。
; 随后进行PIC的初始化。
MOV AL,0xff
OUT 0x21,AL
NOP ; 如果连续执行OUT指令,有些机种会无法正常运行
OUT 0xa1,AL
CLI ; 禁止CPU级别的中断

等同于

io_out(PIC0_IMR, 0xff); /* 禁止主PIC的全部中断 */
io_out(PIC1_IMR, 0xff); /* 禁止从PIC的全部中断 */
io_cli(); /* 禁止CPU级别的中断*/

防止cpu进行模式转换的时候有中断发生,同样PIC初始化时也不允许有中断发生,所以要屏蔽全部的中断

NOP指令什么都不做,只是让CPU休息一个时钟长的时间

为了让CPU能够访问1MB以上的内存空间,设定A20GATE

; 为了让CPU能够访问1MB以上的内存空间,设定A20GATE
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
;waitkbdout,等同于wait_KBC_sendready

上面这段程序等同于c语言

// 键盘控制器命令定义
#define KEYCMD_WRITE_OUTPORT 0xd1   // 写输出端口的命令
#define KBC_OUTPORT_A20G_ENABLE 0xdf // 启用A20 Gate的位掩码

/* A20GATE的设定流程 */
wait_KBC_sendready();               // 等待KBC准备就绪
io_out8(PORT_KEYCMD, KEYCMD_WRITE_OUTPORT); // 发送写输出端口命令
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_OUTPORT_A20G_ENABLE); // 设置A20启用标志
wait_KBC_sendready();               // 确保命令执行完成 /* 这句话是为了等待完成执行指令 */

程序的基本结构与init_keyboard完全相同,功能仅仅是往键盘控制电路发送指令。

关键数值解析:

  1. 0xd1:键盘控制器命令,表示要写输出端口(Output Port)

D1h,准备写Output端口。随后通过60h端口写入的字节,会被放置在Output Port中。

输出0xdf所要完成的功能,是让A20GATE信号线变成ON的状态。

  1. 0xdf:输出端口的数据配置,二进制形式为11011111,其中:
    • 第1位(bit0):系统复位控制(保持0)
    • 第2位(bit1):A20 Gate使能位(1=启用)
    • 其他位:保留原有配置(如键盘/鼠标中断使能)

执行过程解析:

  1. 等待KBC准备好接收命令(wait_KBC_sendready
  2. 发送0xd1命令告诉KBC要设置输出端口
  3. 发送0xdf数据实际配置输出端口,启用A20地址线
  4. 最后等待确保配置完成

完整位掩码示意图:

0xdf = 1101 1111
        │││
        ││└─ 保持复位信号不变(0)
        │└── A20 Gate使能(1)
        └─── 保留原有中断设置(键盘/鼠标中断)

这个操作允许CPU访问超过1MB的内存地址空间,是进入32位保护模式的必要步骤。

常见的键盘控制器命令(8042兼容指令):

0xD1 写输出端口       - 用于设置系统标志(如A20 Gate)
0x60 写命令字节      - 修改控制器的配置参数
0xAE 启用键盘接口     - 允许键盘输入
0xAF 禁用键盘接口     - 禁止键盘输入
0x20 读命令字节      - 读取当前配置
0xDD 禁用A20线       - 关闭高位地址线
0xDF 启用A20线       - 打开高位地址线(与0xD1配合使用)
0xD0 读输出端口      - 获取当前输出端口状态
0xE0 读测试输入      - 读取测试端口(P1/P2)状态
0xF0-0xFF 自检指令   - 执行控制器自检
; 设置A20 Gate示例
CALL    waitkbdout      ; 等待控制器就绪
MOV     AL,0xD1         ; 写输出端口命令
OUT     0x64,AL         ; 发送到命令端口
CALL    waitkbdout  
MOV     AL,0xDF         ; 输出端口数据(A20使能)
OUT     0x60,AL         ; 发送到数据端口

这些命令通过两个I/O端口操作:

  • 0x64: 命令端口(写命令)
  • 0x60: 数据端口(读/写数据)

关于A20GATE信号线:

这条信号线的作用是什么呢?它能使内存的1MB以上的部分变成可使用状态。最初出现电脑的
时候,CPU只有16位模式,所以内存最大也只有1MB。后来CPU变聪明了,可以使用很大的内存了。但为了兼容旧版的操作系统,在执行激活指令之前,电路被限制为只能使用1MB内存。和鼠标的情况很类似哟。A20GATE信号线正是用来使这个电路停止从而让所有内存都可以使用的东西。

切换到保护模式

; 切换到保护模式
[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(control register 0)这一特殊的32位寄存器的值代入EAX,并将最高位置为0,最低位置为1,再将这个值返回给CR0寄存器。这样就完成了模式转换,进入到不用颁的保护模式。

通过代入CR0而切换到保护模式时,要马上执行JMP指令。所以我们也执行这一指令。为什么要执行JMP指令呢?因为变成保护模式后,机器语言的解释要发生变化。CPU为了加快指令的执行速度而使用了管道(pipeline)这一机制,就是说,前一条指令还在执行的时候,就开始解释下一条甚至是再下一条指令。因为模式变了,就要重新解释一遍,所以加入了JMP指令。

进入保护模式以后,段寄存器的意思也变了(不再是乘以16后再加算的意思了),除了CS以外所有段寄存器的值都从0x0000变成了0x0008。

控制寄存器cr0-cr4以及cr8:

一、CR0(Control Register 0)

核心功能:控制 CPU 的基本运行模式与内存管理。

名称 功能
0 PE (Protection Enable) 保护模式开关:1=启用保护模式(支持分段内存管理)。
1 MP (Monitor Coprocessor) 浮点协处理器监控:与 TS 位配合,控制浮点指令是否触发异常。
2 EM (Emulation) 浮点模拟:1=强制浮点指令触发异常(由软件模拟 FPU)。
3 TS (Task Switched) 任务切换标记:1=任务切换后未保存 FPU 状态,触发 #NM 异常。
4 ET (Extension Type) 协处理器类型:已弃用(现代 CPU 固定为 1)。
5 NE (Numeric Error) 浮点错误处理:1=浮点错误触发 #MF 异常,0=通过中断控制器处理。
16 WP (Write Protect) 写保护:1=禁止内核写用户只读页(防止篡改代码段)。
18 AM (Alignment Mask) 对齐检查:与 EFLAGS.AC 位配合,启用内存对齐检查。
31 PG (Paging Enable) 分页开关:1=启用分页机制(需同时设置 PE=1)。

典型操作示例

; 启用保护模式和分页
mov eax, cr0
or eax, 0x80000001; 设置 PE(位0)和 PG(位31)
mov cr0, eax

二、CR1(Control Register 1)

保留寄存器:在 x86/x64 架构中未定义具体功能,通常不使用。


三、CR2(Control Register 2)

核心功能:存储触发页面错误(#PF)的线性地址。

  • 用途:当发生缺页异常时,CR2 保存导致异常的访问地址。

  • 示例:在缺页处理程序(Page Fault Handler)中,可通过读取 CR2 定位错误地址:复制下载

    c

    void page_fault_handler(void) {
        uintptr_t fault_addr;
        asm("mov %%cr2, %0" : "=r"(fault_addr));
    // 处理缺页...}
    

四、CR3(Control Register 3)

核心功能:存储当前页表结构的基地址(物理地址)。

  • 分页模式
    • 32 位分页:CR3 指向页目录基地址(Page Directory Base)。
    • PAE 分页:CR3 指向页目录指针表(PDPT)。
    • 64 位分页:CR3 指向 PML4 表(4 级页表)。
功能
31:12 页表基地址(对齐到 4KB 边界)
3 PCD (Page Cache Disable)
4 PWT (Page Write Through)
63:5 保留(64 位模式下使用高 32 位)

示例

; 设置页表基地址(假设页目录物理地址为 0x1000)
mov eax, 0x1000
mov cr3, eax

五、CR4(Control Register 4)

核心功能:控制扩展功能(如虚拟化、安全特性)。

名称 功能
5 PAE (Physical Address Extension) 物理地址扩展:1=启用 36 位物理地址(支持 64GB 内存)。
7 PGE (Page Global Enable) 全局页表项:1=允许 TLB 缓存全局页(标记为 Global 的页表项)。
9 OSFXSR SSE/浮点支持:1=启用 SSE 指令和 FXSAVE/FXRSTOR 指令。
10 OSXMMEXCPT SSE 异常处理:1=允许 SSE 指令触发 #XM 异常。
13 VMXE Intel VT-x 虚拟化:1=启用 CPU 虚拟化扩展。
14 SMXE Safer Mode Extensions:与 SMEP/SMAP 配合的安全扩展。
17 PCIDE 进程上下文 ID:1=启用 PCID(减少 TLB 刷新)。
20 SMEP (Supervisor Mode Execution Prevention) 内核执行保护:1=禁止内核执行用户空间代码。
21 SMAP (Supervisor Mode Access Prevention) 内核访问保护:1=禁止内核访问用户空间内存(需配合 EFLAGS.AC)。

典型操作示例

; 启用 PAE 和 SSE 支持
mov eax, cr4
or eax, (1 << 5) | (1 << 9); 设置 PAE(位5)和 OSFXSR(位9)
mov cr4, eax

六、CR8(Control Register 8,仅 x64)

核心功能:控制任务优先级(Task Priority Level, TPL),用于管理中断屏蔽。

  • 用途:在 x64 中替代传统 PIC/APIC 的中断优先级控制。
  • 位定义:低 4 位表示 TPL(0-15),数值越小优先级越高。

七、实际应用场景

1. 操作系统启动

  • 启用保护模式:设置 CR0.PE=1。
  • 启用分页:设置 CR0.PG=1,并配置 CR3 指向页表。
  • 启用 SSE:设置 CR4.OSFXSR=1。

2. 虚拟化

  • 启用 VT-x:设置 CR4.VMXE=1,并配置 VMCS 结构。

3. 安全防护

  • 防止内核漏洞:设置 CR4.SMEP=1 和 CR4.SMAP=1,阻止内核执行或访问用户空间数据。

总结

  • CR0:控制基础模式(保护模式、分页、写保护)。
  • CR2:定位缺页异常地址。
  • CR3:管理分页结构的基地址。
  • CR4:启用高级功能(虚拟化、安全扩展、SSE)。
  • CR8(x64):中断优先级管理。

bootpack的转送

; 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
191
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; 从柱面数变换为字节数/4
SUB ECX,512/4 ; 减去 IPL
CALL memcpy

简单来说,这部分程序只是在调用memcpy函数。

大致相当于c语言

memcpy(bootpack, BOTPAK, 512*1024/4);
memcpy(0x7c00, DSKCAC, 512/4 );
memcpy(DSKCAC0+512, DSKCAC+512, cyls * 512*18*2/4 - 512/4);
//memcpy(转送源地址, 转送目的地址, 转送数据的大小);
//转送数据大小是以双字为单位的,所以数据大小用字节数除以4来指定。

memcpy(0x7c00, DSKCAC, 512/4);

DSKCAC是0x00100000,所以上面这句话的意思就是从0x7c00复制512字节到0x00100000。这正好是将启动扇区复制到1MB以后的内存去的意思。

memcpy(DSKCAC0+512, DSKCAC+512, cyls * 512182/4-512/4);

将始于0x00008200的磁盘内容,复制到0x00100200那里。

转送数据大小”的计算有点复杂,因为它是以柱面数来计算的,所以需要减去启动区的那一部分长度。这样始于0x00100000的内存部分,就与磁盘的内容相吻合了

IMUL”integer multipule”(整数乘法)是乘法运算,

SUB“substract”(减法)“是减法运算。

bootpack是asmhead.nas的最后一个标签。haribote.sys是通过asmhead.bin和bootpack.hrb连接起来而生成的(可以通过Makefile确认),所以asmhead结束的地方,紧接着串连着bootpack.hrb最前面的部分。

memcpy(bootpack, BOTPAK, 512*1024/4);

→ 从bootpack的地址开始的512KB内容复制到0x00280000号地址去。

AI详细解释内存分配和连接机制:

以下是haribOS 05b版本的内存分配与连接机制详解:

asmhead.nas
; 内存布局关键定义
BOTPAK  EQU     0x00280000      ; 内核加载地址
DSKCAC  EQU     0x00100000      ; 磁盘缓存区
DSKCAC0 EQU     0x00008000      ; 临时缓存区

; 启动流程内存分配
1. 引导扇区(ipl10.bin)
   - 加载地址:0x7c00 (BIOS标准位置)
   - 大小:512字节
   - 职责:加载asmhead.bin和内核

2. 启动头(asmhead.bin)
   - 加载地址:0xc200
   - 职责:
     * 设置VGA模式(显存地址0x000a0000)
     * 收集硬件信息到0x0ff0-0x0fff
     * 初始化GDT/IDT
     * 启用保护模式

3. 内核(bootpack.hrb)
   - 加载地址:0x00280000 (BOTPAK)
   - 职责:包含操作系统的所有核心功能

内存连接实现:
; 内核复制过程
MOV     ESI,bootpack    ; 源代码位置(编译时确定)
MOV     EDI,BOTPAK      ; 目标地址0x00280000
MOV     ECX,512*1024/4 ; 复制512KB
CALL    memcpy          ; 执行内存块复制

; 磁盘数据缓存
MOV     ESI,0x7c00      ; 引导扇区原始位置
MOV     EDI,DSKCAC      ; 复制到0x00100000
MOV     ECX,512/4       ; 复制512字节
CALL    memcpy

内存管理关键技术:
1. 分段机制:通过GDT将物理地址映射为线性地址
```nasm
LGDT    [GDTR0]         ; 加载全局描述符表
MOV     EAX,CR0
OR      EAX,0x00000001  ; 启用保护模式
MOV     CR0,EAX

  1. 物理内存映射:
0x00000000-0x000fffff : 实模式可用内存
0x00100000-0x00267fff : 磁盘缓存区
0x00280000-0x003fffff : 内核区域
0x000a0000-0x000bffff : VGA显存区域

该设计通过分阶段加载和精确的内存地址控制,实现了从实模式到保护模式的无缝衔接。

bootpack的启动

; 必须由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

; 内核加载最终阶段
MOV EBX,BOTPAK         ; EBX = 0x00280000(内核基地址)
MOV ECX,[EBX+16]       ; 读取需要复制的数据长度(字节数)
ADD ECX,3              ; 对齐处理:字节数+3
SHR ECX,2              ; 转换为双字数(除以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 ; 跳转到内核入口点
  1. 内核结构定义
    • [BOTPAK+12]:加载地址(物理内存地址)
    • [BOTPAK+16]:需要复制的数据长度
    • [BOTPAK+20]:数据在文件内的偏移量
  2. 地址计算
    • 源地址 = BOTPAK基地址(0x00280000) + 文件内偏移量
    • 目标地址 = 直接指定的物理地址
  3. 复制优化
    • 通过ADD ECX,3SHR ECX,2将字节数转换为4字节对齐的双字数,提高复制效率
  4. 内存复制准备
    • 通过内核头部的元数据(偏移量+12/+16/+20)确定需要复制的数据位置和大小
    • 对非4字节对齐的数据进行向上取整处理
  5. 控制权移交
    • 2*8 表示GDT中代码段选择子(第二个描述符,特权级0)
    • 0x0000001b 是内核入口点的偏移地址
    • 该跳转同时完成从引导程序到内核、从汇编到C代码的转换

内存布局示意图:

0x00280000 (BOTPAK)
+-------------------+
| 内核头部信息       | → 包含加载地址、数据长度等元数据
+-------------------+
| 实际内核代码       | → 复制到指定内存位置
+-------------------+
        ↓
0x0000001b (入口点)

逐行分析:

1. MOV EBX,BOTPAK:BOTPAK定义为0x00280000,这是内核加载的基地址。EBX现在指向内核的起始位置。

2. MOV ECX,[EBX+16]:从EBX+16处读取值到ECX。这里EBX是内核基址,+16可能对应内核头部中的某个字段,比如代码段长度。

3. ADD ECX,3 和 SHR ECX,2:将ECX加3后右移两位,相当于将字节数转换为4字节对齐的双字数。例如,若原长度是5字节,加3得8,右移两位得2,即复制2个双字(8字节)。

4. JZ skip:如果ECX为0,跳转到skip,避免执行无意义的复制操作。

5. MOV ESI,[EBX+20]:从EBX+20读取源地址偏移量。加上EBX得到实际的源地址(文件内的位置)。

6. ADD ESI,EBX:计算源地址的绝对位置,即BOTPAK + 偏移量。

7. MOV EDI,[EBX+12]:从EBX+12读取目标地址,这是代码段应加载的内存地址。

8. CALL memcpy:调用内存复制函数,将数据从源复制到目标地址,复制ECX次,每次4字节。

9. skip::跳过复制的标签。

10. MOV ESP,[EBX+12]:设置栈指针为内核中指定的初始值,通常指向代码段加载地址,可能内核在此处预设了栈空间。

11. JMP DWORD 2*8:0x0000001b:长跳转到内核入口点。2*8是代码段选择子(GDT中第二个描述符,特权级0),0x1b是偏移量。这可能对应内核的入口函数。

关键点解释:

  • 内核头部结构:EBX+12、+16、+20的偏移量对应内核的不同元数据,如加载地址、长度、文件偏移,这些在编译时由链接器生成。
    - 段选择子28:在GDT中,每个描述符占8字节。28表示选择第二个描述符,对应代码段,其基址可能为0,因此入口点0x1b是实际的物理地址。
    - 控制权转移:通过JMP指令切换到内核的代码段,开始执行内核代码。

总结:这段代码完成了从引导程序到内核的交接,包括复制内核代码到正确位置,设置栈指针,并跳转到内核入口点,从而启动操作系统。

内存分布图

0x00000000 - 0x000fffff : 虽然在启动中会多次使用,但之后就变空。(1MB

0x00100000 - 0x00267fff : 用于保存软盘的内容。(1440KB

0x00268000 - 0x0026f7ff : 空(30KB

0x0026f800 - 0x0026ffff : IDT2KB

0x00270000 - 0x0027ffff : GDT64KB

0x00280000 - 0x002fffff : bootpack.hrb512KB

0x00300000 - 0x003fffff : 栈及其他(1MB

0x00400000 - :

waitkbdout

waitkbdout:
IN AL,0x64
AND AL,0x02
IN AL,0x60 ; 空读(为了清空数据接收缓冲区中的垃圾数据)
JNZ waitkbdout ; AND的结果如果不是0,就跳到waitkbdout
RET

它与wait_KBC_sendready相同,但也添加了部分处理,就是从OX60号设备进行IN的处理。也就是说,如果控制器里有键盘代码,或者是已经累积了鼠标数据,就顺便把它们读取出来。

ALIGNB

ALIGNB指令的意思是,一直添加DBO,直到时机合适的时候为止ALIGNB 16的情况下,地址能被16整除的时候,就称为“时机合适”。如果最初的地址能被16整除,则ALIGNB指令不作任何处理。

ALIGNB 16
GDT0:
RESB 8 ; NULL selector
DW 0xffff,0x0000,0x9200,0x00cf ; 可以读写的段(segment)32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 可以执行的段(segment)32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:

这是操作系统中全局描述符表(GDT)的初始化代码,用于进入保护模式时的内存分段管理:


ALIGNB 16          ; 16字节对齐
GDT0:
    RESB 8         ; 空描述符(必须存在但不可用)

    ; 数据段描述符(32位可读写)
    DW 0xffff      ; 段界限低16位
    DW 0x0000      ; 段基址低16位
    DB 0x00        ; 段基址中间8位
    DB 0x92        ; 访问权限(P=1, DPL=0, S=1, Type=读写数据段)
    DB 0x00        ; 段界限高4位 + 标志(G=0, D/B=0)
    DB 0xcf        ; 段基址高8位 + 标志(G=1, D/B=1,表示4KB粒度)

    ; 代码段描述符(32位可执行,bootpack专用)
    DW 0xffff      ; 段界限低16位
    DW 0x0000      ; 段基址低16位
    DB 0x28        ; 段基址中间8位(0x00280000)
    DB 0x9a        ; 访问权限(P=1, DPL=0, S=1, Type=执行代码段)
    DB 0x47        ; 段界限高4位 + 标志(G=0, D/B=0)
    DB 0x00        ; 段基址高8位

GDTR0:
    DW 8*3-1       ; GDT界限(3个描述符*8字节 -1)
    DD GDT0        ; GDT物理地址

ALIGNB 16
bootpack:          ; 内核代码开始位置

关键参数解析:

  1. 数据段描述符
    • 基地址:0x00000000
    • 段界限:0xfffff(4GB空间)
    • 权限:0x92(存在、特权级0、可读写)
  2. 代码段描述符
    • 基地址:0x00280000(对应BOTPAK地址)
    • 段界限:0xfffff(4GB空间)
    • 权限:0x9a(存在、特权级0、可执行)

这个GDT配置使内核代码运行在0x00280000开始的线性地址空间,数据段覆盖整个4GB内存空间,为后续内存管理提供基础。

GDT0也是一种特定的GDT。0号是空区域(null sector),不能够在那里定义段。1
195
号和2号分别由下式设定。
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW); set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);


网站公告

今日签到

点亮在社区的每一天
去签到