iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
自我挑戰組

30 天 hypervisor 入門系列 第 15

Day 15 實現簡易記憶體管理結構

  • 分享至 

  • xImage
  •  

這一階段我們將會在 KVM 的 API 之上,劃分 Guest 的 GPA 空間並賦予不同語意,用於控制哪些地址被直接映射、哪些地址必須回到 Host 處理。
我們把 GPA 切為以下三類,他們差別只在是否被 KVM_SET_USER_MEMORY_REGION 註冊到 KVM。

  • RAM:可讀寫、已註冊的 memslot,用來放程式與資料(有註冊)。
  • MMIO:用於周邊暫存器;我們它保留為「未註冊的洞」,任何資料存取都回到 host 處理。
  • Guard:保護區同樣是「未註冊的洞」,不與於 RAM 、 MMIO 的區塊。
    當 Guest 訪問記憶體命中 RAM 時,KVM 會直接透過 EPT 存取,而命中 MMIO 或 Guard 時,因為沒有註測,將產生 EPT violation,KVM 會把它轉成 KVM_EXIT_MMIO 交回 Host。我們只需要看 GPA 落在哪個區間,就能決定要做設備模擬(MMIO)還是當成越界處理(Guard)。

在這個階段先做 3 個函式分別處理記憶體區塊的註冊、註銷、同步到 KVM。
另外由於註冊的記憶體區塊不會太多,因此用固定表 + bitset 管理就夠。

資料介面

enum mem_slot_kind {
    MEM_SLOT_RAM,   // 註冊到 KVM
    MEM_SLOT_MMIO,  // 給 VM-exit 做 MMIO
    MEM_SLOT_GUARD, // 用於抓越界
    MEM_SLOT_RESERVED,
};

enum mem_slot_flags {
    MEM_SLOT_F_LOG_DIRTY = 1u << 0,
    MEM_SLOT_F_READONLY  = 1u << 1, // 這裡的 flag 跟 kvm 相同
};

struct mem_slot {
    uint32_t id;
    uint64_t gpa_start;
    uint64_t size;
    void *hva;
    enum mem_slot_kind kind;
    uint32_t flags;
};

struct mem_layout {
    struct mem_slot slots[MEM_LAYOUT_MAX_SLOTS]; // 最多 32 個
    uint32_t free_slots; // 1 = free, 0 = occupied
};

管理結構是 mem_layout。最多 32 個 slot,每個 slot 用 free_slots 的 1 bit 表示。
mem_slot_kind 決定是否註冊到 KVM 以及後續 VM-exit 的處理邏輯。其餘欄位直接對應 KVM 的 kvm_userspace_memory_region。

函式介面

enum mem_layout_result mem_layout_add_slot(struct mem_layout *layout,
                                           uint32_t slot_id,
                                           uint64_t gpa_start,
                                           uint64_t size,
                                           void *hva,
                                           enum mem_slot_kind kind,
                                           uint32_t flags)
{
    if (!layout)
        return MEM_LAYOUT_ERR_UNKNOWN_SLOT;

    if (slot_id >= MEM_LAYOUT_MAX_SLOTS)
        return MEM_LAYOUT_ERR_INVALID_SLOT_ID; // 不支援此 ID

    if (layout->free_slots == 0)
        return MEM_LAYOUT_ERR_FULL; // 沒有多餘的 slot 可註冊

    if (!TEST_BIT32(layout->free_slots, slot_id))
        return MEM_LAYOUT_ERR_SLOT_OCCUPIED; // 目標 slot 已被使用

    if (!size)
        return MEM_LAYOUT_ERR_ALIGN; // 大小不可為 0

    if (!IS_ALIGNED(gpa_start, MEM_LAYOUT_PAGE_SIZE) ||
        !IS_ALIGNED(size, MEM_LAYOUT_PAGE_SIZE))
        return MEM_LAYOUT_ERR_ALIGN; // 註冊記憶體區塊須滿足對齊要求

    bool needs_hva = (kind == MEM_SLOT_RAM); // 需註冊到 kvm

    if (needs_hva && !hva)
        return MEM_LAYOUT_ERR_UNKNOWN_SLOT;

    if (hva && !IS_ALIGNED((uintptr_t)hva, MEM_LAYOUT_PAGE_SIZE))
        return MEM_LAYOUT_ERR_ALIGN; // 由於 EPT 也是以頁為單位 HVA 也需要對齊

    if (gpa_start > MEM_LAYOUT_MAX_GPA)
        return MEM_LAYOUT_ERR_RANGE;

    uint64_t max_len = (MEM_LAYOUT_MAX_GPA - gpa_start) + 1u;
    if (size > max_len)
        return MEM_LAYOUT_ERR_RANGE; // 越界

    for (uint32_t i = 0; i < MEM_LAYOUT_MAX_SLOTS; ++i) {
        if (TEST_BIT32(layout->free_slots, i))
            continue;

        const struct mem_slot *other = &layout->slots[i];
        if (ranges_overlap(gpa_start, size, other->gpa_start, other->size))
            return MEM_LAYOUT_ERR_OVERLAP; // 檢查註冊記憶體區塊是否重疊
    }

    struct mem_slot *slot = &layout->slots[slot_id];
    slot->id = slot_id;
    slot->gpa_start = gpa_start;
    slot->size = size;
    slot->hva = hva;
    slot->kind = kind;
    slot->flags = flags;

    layout->free_slots &= ~BIT32(slot_id);

    return MEM_LAYOUT_OK;
}

mem_layout_add_slot 的用途很簡單,只是把一段 GPA 範圍掛進 Host 維護的 mem_layout。掛上時並不會註冊到 kvm,僅僅是紀錄如何處理這塊記憶體,等之後由 mem_layout_sync_kvm() 一次性同步到 KVM。
函式進來先做基本防呆,檢查 layout 是否有效、slot_id 是否在可用範圍、整個表是否還有空位可填,並且確認該 slot_id 目前是空的。接著檢查是否滿足對齊,對於 RAM 區塊也需檢查 hva 是否為頁對齊。同時函是會檢查註冊的記憶體區塊是否與其他區塊重疊。當所有檢查都通過時才會寫入 slots。

enum mem_layout_result mem_layout_remove_slot(struct mem_layout *layout, uint32_t slot_id)
{
    if (!layout)
        return MEM_LAYOUT_ERR_UNKNOWN_SLOT;

    if (slot_id >= MEM_LAYOUT_MAX_SLOTS)
        return MEM_LAYOUT_ERR_INVALID_SLOT_ID;

    if (TEST_BIT32(layout->free_slots, slot_id))
        return MEM_LAYOUT_ERR_UNKNOWN_SLOT;

    layout->slots[slot_id] = (struct mem_slot) {
        .id = slot_id,
    };

    layout->free_slots |= BIT32(slot_id);

    return MEM_LAYOUT_OK;
}

mem_layout_remove_slot 負責將某個 slot_id 從管理結構中移除,並把 free_slots 對應 bit 設回可用。

enum mem_layout_result mem_layout_sync_kvm(const struct mem_layout *layout, int vm_fd)
{
    if (!layout || vm_fd < 0)
        return MEM_LAYOUT_ERR_UNKNOWN_SLOT;

    enum mem_layout_result res = mem_layout_validate(layout);
    if (res != MEM_LAYOUT_OK) {
        errno = EINVAL;
        return res;
    }

    for (uint32_t i = 0; i < MEM_LAYOUT_MAX_SLOTS; ++i) {
        const struct mem_slot *slot = &layout->slots[i];

        if (slot->kind != MEM_SLOT_RAM)
            continue; // MEM_SLOT_RAM 的值是 0

        struct kvm_userspace_memory_region region = {
            .slot = slot->id,
            .flags = mem_layout_to_kvm_flags(slot->flags),
            .guest_phys_addr = slot->gpa_start,
            .memory_size = slot->size,
            .userspace_addr = (uint64_t)slot->hva,
        };

        if (ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &region) < 0)
            return MEM_LAYOUT_ERR_UNKNOWN_SLOT;
    }

    return MEM_LAYOUT_OK;
}

mem_layout_sync_kvm 會將註冊為 RAM 的記憶體區塊套用到 KVM 中,首先調用 mem_layout_validate 做完整性檢查,確認對齊、範圍與重疊都沒問題。接著掃過所有屬性為 RAM 的 SLOT 並註冊就組成一筆 kvm_userspace_memory_region 並註冊到 KVM 中。而對於那些要從 KVM 註銷的區塊由於在 mem_layout_remove_slot 會將 size 設為 0,調用 ioctl 填入 kvm_userspace_memory_region 時等同於註銷效果。


上一篇
Day 14 Intel EPT 與 2D page walk
下一篇
Day 16 Bootloader 建立最小可啟動開機扇區
系列文
30 天 hypervisor 入門20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言