想要讓 boot 動起來,必須在我們的系統裡實作一個最小的 BIOS 模組。而這些 bios 的入口地址被記錄在 0x0 ~ 0x3FF 的中斷向量表(IVT)中,而 BIOS 則會放在 0xF0000 ~ 0xFFFFF 的 ROM 區。另外,x86 在上電後由 0xFFFF0(reset vector)進入韌體,完成初始流程後把開機扇區載入到 0x7C00,跳轉給我們的 boot 程式。
要讓 boot loader 跑起來,我們會模擬開機扇區載入、基本文字輸出、以及磁碟讀取。在 Real mode 下,這些都會透過 INT n 進入 BIOS 服務。為了把 hypervisor 的責任邊界切乾淨,中斷服務程式只做簡單封裝,能在 Guest 本地完成的就地處理(例如 INT 10h 的 teletype 直接寫 0xB8000 文字緩衝),而需要外部裝置的(如 INT 13h 讀取扇區) 則以過 out 命令觸發 VM-exit,轉交給 Host 的 exit handler 完成 I/O,回填狀態後由 stub 修補返回 FLAGS 並以 iret 收尾。
為了方便插拔與擴充,可以把 BIOS 打包成模組,介面如下。以後要切換不同韌體實作(例如改走 UEFI)也只需替換實作,而不動調用點。
struct bios_firmware {
uint64_t rom_gpa; // 0xF0000 後
size_t rom_size; // 64 KB (0xF0000 ~ 0xFFFFF)
const char *image_path; // 韌體模組路徑
};
void bios_firmware_init_defaults(struct bios_firmware *fw)
int bios_firmware_load(struct bios_firmware *fw, struct mem_layout *layout);
void bios_firmware_init_defaults(struct bios_firmware *fw)
{
if (!fw)
return;
fw->rom_gpa = 0xF0000;
fw->rom_size = 0x10000; /* 64 KB */
fw->image_path = NULL;
}
這個函式會將 bios_firmware 設定成預設版本,當 image_path = NULL 會讀取預設版本(目前尚未實作)。
而 rom_gpa = 0xF0000 是將把 BIOS ROM 放在 0xF0000 中,rom_size 設定為 64 KB。
此函式的功能很簡單,把 BIOS ROM 映像貼到 Guest 的 [rom_gpa, rom_gpa+rom_size)區間,把 IVT(0x0000–0x03FF) 全部指到 rom_segment:0000,若沒有 ROM 檔就放一個最小可存活的 ROM,讓 reset vector 在 0xFFFF0 做 far jump 到 seg:0000,而 seg:0000 放 cli; hlt; jmp $ 以安全停下。
int bios_firmware_load(struct bios_firmware *fw, struct mem_layout *layout)
{
const size_t ivt_bytes = 0x400;
const uint16_t default_offset = 0;
if (!fw || !layout)
return -EINVAL; // 必須確保配置了 memory slot
if (!fw->rom_size)
return -EINVAL;
uint64_t rom_end = fw->rom_gpa + fw->rom_size;
if (rom_end < fw->rom_gpa)
return -ERANGE; // 防止 64-bit 加法溢位
if (fw->rom_gpa & 0xF)
return -EINVAL; // 16 byte 對齊
if (fw->rom_size < 0x10)
return -ERANGE; // 0xFFFF0 為入口至少需要 0X10
const struct mem_slot *rom_slot = mem_layout_find_by_gpa(layout, fw->rom_gpa);
if (!rom_slot || !rom_slot->hva)
return -ENOENT; // slot 需配置成 RAM
uint64_t slot_end = rom_slot->gpa_start + rom_slot->size;
if (slot_end < rom_slot->gpa_start)
return -ERANGE;
if (rom_end > slot_end)
return -ERANGE;
uint64_t rom_offset = fw->rom_gpa - rom_slot->gpa_start;
uint8_t *rom_ptr = (uint8_t *)rom_slot->hva + rom_offset;
memset(rom_ptr, 0xFF, fw->rom_size); // 先用 0xFF 擦掉整段表示未燒入 ROM
int fd = -1;
int have_image = 0;
if (fw->image_path && fw->image_path[0]) {
fd = open(fw->image_path, O_RDONLY | O_CLOEXEC); /
if (fd < 0)
return -errno;
struct stat st;
if (fstat(fd, &st) < 0) {
int err = -errno;
close(fd);
return err;
}
if (st.st_size < 0) {
close(fd);
return -EINVAL;
}
size_t to_copy = (size_t)st.st_size;
if (to_copy > fw->rom_size) {
close(fd);
return -EFBIG;
}
uint8_t *dst = rom_ptr;
while (to_copy > 0) {
ssize_t n = read(fd, dst, to_copy);
if (n < 0) {
if (errno == EINTR)
continue;
int err = -errno;
close(fd);
return err;
}
if (n == 0)
break;
dst += (size_t)n;
to_copy -= (size_t)n; // 把韌體複製到 Guest 的 ROM 中。
}
if (to_copy != 0) {
close(fd);
return -EIO;
}
close(fd);
have_image = 1; // 成功寫入 ROM
}
const struct mem_slot *ivt_slot = mem_layout_find_by_gpa(layout, 0u);
if (!ivt_slot || !ivt_slot->hva)
return -ENOENT;
if (ivt_slot->gpa_start > 0u)
return -ERANGE;
if (ivt_slot->size < ivt_bytes)
return -ERANGE;
uint16_t rom_segment = (uint16_t)(fw->rom_gpa >> 4);
uint16_t *ivt = (uint16_t *)ivt_slot->hva;
memset(ivt, 0, ivt_bytes);
for (size_t vec = 0; vec < 256u; ++vec) {
ivt[vec * 2] = default_offset;
ivt[vec * 2 + 1] = rom_segment; // 把 IVT 設定為預設值,之後需要使用的 IVT 會在韌體中寫入覆蓋
}
if (!have_image) {
rom_ptr[0] = 0xFA; /* cli */
rom_ptr[1] = 0xF4; /* hlt */
rom_ptr[2] = 0xEB; /* jmp $ */
rom_ptr[3] = 0xFE;
size_t reset_offset = fw->rom_size - 0x10u;
uint8_t *reset_ptr = rom_ptr + reset_offset;
reset_ptr[0] = 0xEA; /* jmp far imm16:16 */
reset_ptr[1] = 0x00;
reset_ptr[2] = 0x00;
reset_ptr[3] = (uint8_t)(rom_segment & 0xFFu);
reset_ptr[4] = (uint8_t)(rom_segment >> 8);
// 沒 ROM 檔時的預設處理
}
return 0;
}