FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V Testに実装されているページテーブルの実装を調査する

ハイパーバイザーの勉強をしている中で、2段ページテーブルの実装の中でそもそも私は普通のスーパーバイザーでのページテーブルの仕組みをしっかり理解できていないことに気が付いた。 ここでは、riscv_testsに実装されているページテーブルの実装と仮想アドレスを読みながら、その仕組みを理解していこうと思う。

f:id:msyksphinz:20210320001918p:plain

  • vm_boot()での動作
  // map user to lowermost megapage
  l1pt[0] = ((pte_t)user_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;

l1ptl2ptというのが定義されているが、これは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

// riscv_test.h
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   // 16KiBアサインされている
    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へのポインタとなっているので、これは普通にジャンプするためのページテーブルとなっている。

  // map user to lowermost megapage
  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))));
f:id:msyksphinz:20210320001951p:plain

スーパーバイザー向けのトラップは以下のようになっていた。これはどういう計算だ?

  // set up supervisor trap handling
  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));
  // FPU on; accelerator on; allow supervisor access to user memory access
  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;

最後にtrapframetest_addr - DRAM_BASEを設定している。test_addruserstartに設定されているので、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