ハイパーバイザーの勉強をしている中で、2段ページテーブルの実装の中でそもそも私は普通のスーパーバイザーでのページテーブルの仕組みをしっかり理解できていないことに気が付いた。
ここでは、riscv_tests
に実装されているページテーブルの実装と仮想アドレスを読みながら、その仕組みを理解していこうと思う。
l1pt[0] = ((pte_t)user_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
l1pt
とl2pt
というのが定義されているが、これはpt
の配列の1つの目と2つ目が指定されているらしい。
#define l1pt pt[0]
#define user_l2pt pt[1]
pte_t pt[NPT][PTES_PER_PT] __attribute__((aligned(PGSIZE)));
#if __riscv_xlen == 64
# define NPT 4
#define kernel_l2pt pt[2]
# define user_l3pt pt[3]
#else
# define NPT 2
# define user_l3pt user_l2pt
#endif
typedef unsigned long pte_t;
#define LEVELS (sizeof(pte_t) == sizeof(uint64_t) ? 3 : 2)
#define PTIDXBITS (PGSHIFT - (sizeof(pte_t) == 8 ? 3 : 2))
#define VPN_BITS (PTIDXBITS * LEVELS)
#define VA_BITS (VPN_BITS + PGSHIFT)
#define PTES_PER_PT (1UL << RISCV_PGLEVEL_BITS)
#define MEGAPAGE_SIZE (PTES_PER_PT * PGSIZE)
RV64の場合は、
pte_t pt[NPT][PTES_PER_PT] __attribute__((aligned(PGSIZE)));
unsigned long pt[4][1UL << 9] __attribute__((aligned(PGSIZE)));
という構成になる。pt
が4つから構成されており、それぞれがVPNのサイズだけ定義されている。pt
の1つのエントリに1つのPTEが格納されるという感じだろうか。ダンプしてみるとpt
は16KiBだけアサインされているので、1ブロックが4KiBで、512エントリ、ということは1エントリが8バイトということになる。
12: 0000000000000000 0 FILE LOCAL DEFAULT ABS vm.c
13: 0000000080002230 44 FUNC LOCAL DEFAULT 3 terminate
14: 000000008000218c 28 FUNC GLOBAL HIDDEN 3 strcpy
15: 00000000800021a8 136 FUNC GLOBAL HIDDEN 3 atol
16: 0000000080003000 16384 OBJECT GLOBAL HIDDEN 5 pt
17: 0000000080002000 92 FUNC GLOBAL HIDDEN 3 memcpy
18: 0000000080007000 1008 OBJECT GLOBAL HIDDEN 5 freelist_nodes
19: 00000000800028d4 500 FUNC GLOBAL HIDDEN 3 vm_boot
20: 000000008000259c 824 FUNC GLOBAL HIDDEN 3 handle_trap
4ブロックがそれぞれ
l1pt
user_l2pt
kernel_l2pt
user_l3pt
となっているが、今のところ意味が分からない。
という所でマップをもう一度読み直してみると、最初のマップは、user_l2pt
へのポインタとなっているので、これは普通にジャンプするためのページテーブルとなっている。
l1pt[0] = ((pte_t)user_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
user_l2pt
の先頭はuser_l3pt
へのジャンプとなっている。
l1pt
の最後のエントリは、kernel_l2pt
のへのジャンプとなっている。
さらにkernel_l2pt
の最後のエントリはDRAM_BASE
(つまり0x80000000)へのテーブルとなっており、これはLeafテーブルとなっている。
#if __riscv_xlen == 64
l1pt[PTES_PER_PT-1] = ((pte_t)kernel_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
kernel_l2pt[PTES_PER_PT-1] = (DRAM_BASE/RISCV_PGSIZE << PTE_PPN_SHIFT) | PTE_V | PTE_R | PTE_W | PTE_X | PTE_A | PTE_D;
user_l2pt[0] = ((pte_t)user_l3pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
uintptr_t vm_choice = SATP_MODE_SV39;
#else
SATP(sptbr)にl1pt
の先頭アドレスを格納されることで仮想アドレス変換の先頭アドレスとして使用されるようになる。
write_csr(sptbr, ((uintptr_t)l1pt >> PGSHIFT) |
(vm_choice * (SATP_MODE & ~(SATP_MODE<<1))));
スーパーバイザー向けのトラップは以下のようになっていた。これはどういう計算だ?
write_csr(stvec, pa2kva(trap_entry));
write_csr(sscratch, pa2kva(read_csr(mscratch)));
write_csr(medeleg,
(1 << CAUSE_USER_ECALL) |
(1 << CAUSE_FETCH_PAGE_FAULT) |
(1 << CAUSE_LOAD_PAGE_FAULT) |
(1 << CAUSE_STORE_PAGE_FAULT));
write_csr(mstatus, MSTATUS_FS | MSTATUS_XS);
write_csr(mie, 0);
#define pa2kva(pa) ((void*)(pa) - DRAM_BASE - MEGAPAGE_SIZE)
#define MEGAPAGE_SIZE (PTES_PER_PT * PGSIZE)
trap_entry
は0x0800000c4に配置されているので、0x800000c4 - 0x80000000 - 512 x 4KiB = 0x8000000c4 - (0x80000000 + 512 x 16KiB)
となり、 0xFFFF_FFFF_FFE0_00C4
となる。この計算の仕組みはどういうことだろう?0x8000_0000から512番目のページテーブルにページを配置するということかな?
次に、malloc()
で割り当てるメモリマップを確保しているものと思われる。
freelist_head = pa2kva((void*)&freelist_nodes[0]);
freelist_tail = pa2kva(&freelist_nodes[MAX_TEST_PAGES-1]);
for (long i = 0; i < MAX_TEST_PAGES; i++)
{
freelist_nodes[i].addr = DRAM_BASE + (MAX_TEST_PAGES + random)*PGSIZE;
freelist_nodes[i].next = pa2kva(&freelist_nodes[i+1]);
random = LFSR_NEXT(random);
}
freelist_nodes[MAX_TEST_PAGES-1].next = 0;
最後にtrapframe
にtest_addr - DRAM_BASE
を設定している。test_addr
はuserstart
に設定されているので、0x80002ac8 - 0x80000000 = 0x2ac8
が設定されてトラップフレームを経由してスーパーバイザーモードに移行する。
22: 000000008000225c 16 FUNC GLOBAL HIDDEN 3 wtf
23: 00000000800077e0 8 OBJECT GLOBAL HIDDEN 5 freelist_tail
24: 0000000080002ac8 0 NOTYPE GLOBAL DEFAULT 3 userstart
25: 00000000800077e8 8 OBJECT GLOBAL HIDDEN 5 freelist_head
26: 0000000080003000 0 NOTYPE GLOBAL DEFAULT 4 begin_signature