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隊列緩沖數據,并在屏幕上實時顯示輸入信息。以下是逐行解釋:
一、代碼功能概述
-
初始化鼠標FIFO隊列:
fifo8_init(&mousefifo, 128, mousebuf); // 初始化鼠標FIFO,容量128字節,緩沖區為mousebuf
- 為鼠標數據分配緩沖區,確保中斷接收的數據能暫存。
-
主事件循環:
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
:要顯示的字符串。
四、代碼執行流程
- 初始化:
- 鼠標FIFO隊列初始化,準備接收數據。
- 進入主循環:
- 關中斷:防止處理數據時被中斷打斷。
- 檢查隊列:若鍵盤和鼠標隊列均為空,則開中斷并休眠。
- 處理數據:若隊列非空,按優先級處理鍵盤數據(先)或鼠標數據(后)。
- 顯示數據:
- 將數據轉換為十六進制字符串。
- 清空屏幕局部區域(避免舊數據殘留)。
- 在指定位置繪制新數據。
五、設計細節分析
1. 中斷控制
-
關中斷(
io_cli
):在檢查FIFO狀態前關閉中斷,確保檢查與取數據的操作是原子的,避免競態條件(Race Condition)。
-
開中斷(
io_sti
):在取出數據后立即開啟中斷,允許新的中斷處理程序填充隊列。
2. 屏幕刷新優化
- 局部刷新:
僅刷新顯示數據的區域(如0,16
到15,31
),而非全屏刷新,減少顯存操作開銷。
3. 數據格式轉換
- 十六進制顯示:
將原始掃描碼或鼠標數據以十六進制顯示,便于調試觀察(如0x1C
代表按鍵A按下)。
六、性能與擴展性
-
低功耗休眠:
通過
io_stihlt()
在無數據時休眠,顯著降低CPU占用率。 -
緩沖區容量:
鼠標FIFO容量為128字節,足夠容納多個鼠標數據包(每個包3字節),避免溢出。
-
擴展性:
可擴展支持更多輸入設備(如游戲手柄),只需增加對應的FIFO隊列和處理邏輯。
總結
這段代碼實現了一個高效的輸入事件處理循環,核心特點包括:
- 中斷安全:通過
io_cli
/io_sti
確保數據操作的原子性。 - 低功耗:無數據時休眠,減少資源消耗。
- 實時反饋:即時將輸入數據可視化,便于調試或用戶交互。
該設計是操作系統輸入子系統的典型實現,適用于需要實時響應外設的場景(如嵌入式系統或低級硬件驅動開發)。
鼠標解讀(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)
}
- 關鍵步驟:
- 通過鍵盤控制器(端口
0x64
)發送命令0xD4
,通知后續數據發送到鼠標。 - 發送
0xF4
到數據端口(0x60
),激活鼠標數據報告模式。 - 初始化
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) { // 收到ACKmdec->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; // 錯誤狀態
}
- 解碼流程:
- 階段0:等待鼠標返回ACK(
0xFA
),確認啟用成功。 - 階段1-3:依次接收數據包的3個字節,存儲到
buf
數組。 - 完成解碼:當3字節收齊后,返回1,并重置階段為1以接收下一數據包。
- 階段0:等待鼠標返回ACK(
三、主循環中的鼠標處理
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(...); // 顯示數據}}
}
- 處理流程:
- 從鼠標FIFO隊列中讀取數據字節。
- 調用
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隊列:緩沖中斷接收的數據,主循環異步處理。
六、注意事項
- ACK處理:啟用鼠標后需等待
0xFA
確認,否則后續數據可能錯位。 - 數據包順序:需嚴格按順序接收3字節,否則解析錯誤。
- 符號與溢出:需正確處理補碼和溢出標志,以準確計算鼠標移動。
總結
這段代碼通過以下步驟實現鼠標功能:
- 初始化:激活鼠標并等待ACK。
- 數據接收:通過中斷和FIFO緩沖原始字節。
- 數據解碼:按階段拼裝3字節數據包。
- 數據顯示:將數據包內容輸出到屏幕。
理解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完全相同,功能僅僅是往鍵盤控制電路發送指令。
關鍵數值解析:
- 0xd1:鍵盤控制器命令,表示要寫輸出端口(Output Port)
D1h,準備寫Output端口。隨后通過60h端口寫入的字節,會被放置在Output Port中。
輸出0xdf所要完成的功能,是讓A20GATE信號線變成ON的狀態。
- 0xdf:輸出端口的數據配置,二進制形式為
11011111
,其中:- 第1位(bit0):系統復位控制(保持0)
- 第2位(bit1):A20 Gate使能位(1=啟用)
- 其他位:保留原有配置(如鍵盤/鼠標中斷使能)
執行過程解析:
- 等待KBC準備好接收命令(
wait_KBC_sendready
) - 發送0xd1命令告訴KBC要設置輸出端口
- 發送0xdf數據實際配置輸出端口,啟用A20地址線
- 最后等待確保配置完成
完整位掩碼示意圖:
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] ; 設定臨時GDTMOV EAX,CR0AND EAX,0x7fffffff ; 設bit31為0(為了禁止分頁)OR EAX,0x00000001 ; 設bit0為1(為了切換到保護模式)MOV CR0,EAXJMP pipelineflush
pipelineflush:MOV AX,1*8 ; 可讀寫的段 32bitMOV DS,AXMOV ES,AXMOV FS,AXMOV GS,AXMOV 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
- 物理內存映射:
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 ; 跳轉到內核入口點
- 內核結構定義:
- [BOTPAK+12]:加載地址(物理內存地址)
- [BOTPAK+16]:需要復制的數據長度
- [BOTPAK+20]:數據在文件內的偏移量
- 地址計算:
- 源地址 = BOTPAK基地址(0x00280000) + 文件內偏移量
- 目標地址 = 直接指定的物理地址
- 復制優化:
- 通過
ADD ECX,3
和SHR ECX,2
將字節數轉換為4字節對齊的雙字數,提高復制效率
- 通過
- 內存復制準備:
- 通過內核頭部的元數據(偏移量+12/+16/+20)確定需要復制的數據位置和大小
- 對非4字節對齊的數據進行向上取整處理
- 控制權移交:
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 : IDT (2KB)
0x00270000 - 0x0027ffff : GDT (64KB)
0x00280000 - 0x002fffff : bootpack.hrb(512KB)
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: ; 內核代碼開始位置
關鍵參數解析:
- 數據段描述符:
- 基地址:0x00000000
- 段界限:0xfffff(4GB空間)
- 權限:0x92(存在、特權級0、可讀寫)
- 代碼段描述符:
- 基地址: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);