這幾天我們處理了文字模式畫面與硬碟(INT 13h)讀寫。若要讓 loader 能把資料搬進記憶體、再順利切換到保護模式甚至長模式(IA-32e),我們必須提供 2 個記憶體服務: 1.A20 gate 與 E820,A20 gate 可以讓實模式的地址線不會在 1MB 處回繞。而 E820 則可提供真實的記憶體布局給 bootloader。
A20 gate 是為了相容性的歷史遺留。在 8088 時代,CPU 只有 20 條地址線,尋址範圍僅 2 ^ 20 = 1 MB,一旦位址超過 1 MB,就會在第 21 位 (A20) 被截斷而回繞道 0。到了 80286 開始,地址線擴充,CPU 具備了超過 1 MB 的尋址能力,但為了維持既有軟體行為,IBM 在鍵盤控制器中加入了 AND 邏輯閘,來控制是否允許訪問超過 1MB 這就是 A20 gate。這個開關後來演變成更快的 0x92 (Fast A20)。
開啟 A20 gate 只能保證記憶體尋址不回繞,若想讓實模式下訪問高地址,常見做法是一種叫 big real mode 的技巧,做法是先短暫切到保護模式,準備一份含有 base=0、limit = 4 GB 的段描述符,把它載入段暫存器如 fs,接著把 CR0.PE 清除回到實模式,由於段暫存器的隱藏部分還保留著 4 GB 的界限。此時我們可以用 32 位暫存器在高位址做資料搬運,但注意如果重載了這些暫存器將失去這個尋址能力,回到 1MB 的限制。
由於我們在 KVM 上實作,不需要真的還原這個細節,可以是為 A20 gate 永遠開啟,因始只要攔截到 0x92 或是 8042 鍵盤控制器操作 0x60 0x64 寫入都直接默認開啟(成功)。
int a20_register(struct a20_device *dev, struct io_bus *bus)
{
if (!dev || !bus)
return -1;
struct io_device controller = {
.port_start = 0x60u,
.port_end = 0x64u,
.in = a20_io_in,
.out = a20_io_out,
.opaque = dev,
};
int ret = io_bus_register(bus, &controller);
if (ret < 0)
return ret;
struct io_device fast = {
.port_start = 0x92u,
.port_end = 0x92u,
.in = a20_io_in,
.out = a20_io_out,
.opaque = dev,
};
ret = io_bus_register(bus, &fast);
if (ret < 0)
return ret;
return 0;
}
在這裡我們分別註冊兩個塊路徑與慢路徑,其中 0x60 與 0x64 的寫入是透過鍵盤控制器處理,而 0x92 則是快路徑。
開啟 A20 gate 的程式碼如下:
call wait_keyboard ; 等 8042 可寫(等 IBF=0)
mov al,0xd1 ; 告訴 8042 : 下一個寫到 0x60 的值
out 0x64,al
call wait_keyboard ; 再等一次 IBF=0
mov AL,0xdf ; 要寫入的輸出埠值 : bit1=1 -> A20 = ON
out 0x60,al
call wait_keyboard ; 寫完再等 IBF 清掉
0xd1 是 write output port 指令,他會告訴 8042 接下來會把直寫入 0x60。而在 0x60 中寫入 0xdf 則相當於輸入 Enable A20 address line 命令。而 wait_keyboard 可以是任何的延遲程式碼,目的只是為了讓鍵盤控制器清空。
可以參考這裡
in al,0X92 ;
or al,10b ; 0x92的 bit1 控制A20 gate
out 92h, al ; 開啟A20 gate
快路徑則簡單很多。
為了模擬這個行為我們將針對各自 60h 64h 92h, 3 個端口各自準備函數,92h 默認直接寫入值。
static enum exit_handle_status a20_io_out(void *opaque, const struct io_port_access *access) {
struct a20_device *dev = opaque;
if (!dev || !access || !access->data || access->size == 0)
return EXIT_HANDLE_ERROR;
switch (access->port) {
case 0x60u:
a20_handle_write_60(dev, access);
break;
case 0x64u:
a20_handle_write_64(dev, access);
break;
case 0x92u:
a20_handle_write_92(dev, access);
break;
default:
break;
}
return EXIT_HANDLE_CONTINUE;
}
以下說明 0x60 與 0x64 兩個 port。
static void a20_handle_write_64(struct a20_device *dev, const struct io_port_access *access)
{
size_t stride = access->size;
for (uint32_t i = 0; i < access->count; ++i) {
const unsigned char *src = access->data + (size_t)i * stride;
uint8_t value = src[0];
switch (value) {
case 0xd0u:
dev->pending_output_read = 1;
dev->status |= KBC_STATUS_OBF;
dev->status &= (uint8_t)~KBC_STATUS_IBF;
break;
case 0xd1u:
dev->expecting_output_write = 1;
dev->status |= KBC_STATUS_IBF;
break;
default:
dev->status &= (uint8_t)~KBC_STATUS_IBF;
break;
}
}
}
在 io port 0x64 中我們只模擬兩個命令: 0xd0(read output port)與 0xd1(write output oort)。
OBF = 1,則告訴 CPU 0x60 有資料可讀,而 IBF = 1 則表示鍵盤控制器正在等待 CPU 將資料送入 0x60。
static void a20_handle_write_60(struct a20_device *dev, const struct io_port_access *access)
{
if (!dev->expecting_output_write)
return;
const unsigned char *src = access->data;
if (!src)
return;
a20_apply_output_value(dev, src[0]);
dev->expecting_output_write = 0;
dev->status &= (uint8_t)~KBC_STATUS_IBF;
}
在 0x60 port 中,我們會檢查 expecting_output_write 在 0x64 寫入 0xd1 時這個值才是 1,此時才允許寫入 0x60,a20_apply_output_value 會設定 A20 bit1 = 1。