這一階段我們將會在 KVM 的 API 之上,劃分 Guest 的 GPA 空間並賦予不同語意,用於控制哪些地址被直接映射、哪些地址必須回到 Host 處理。
我們把 GPA 切為以下三類,他們差別只在是否被 KVM_SET_USER_MEMORY_REGION 註冊到 KVM。
在這個階段先做 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, ®ion) < 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 時等同於註銷效果。