以下の内容はhttps://msyksphinz.hatenablog.com/entry/2025/07/14/040000より取得しました。


riscv-testsの仮想アドレスサポートのための実装を読み解く (2. handle_faultの処理)

ここで、 DRAM_BASE が 0x4_0000_0000_0000 であると仮定して、どのようにアドレス変換が行われるかを考えてみよう。 仮想アドレス 0x12345 がどのように変換されるかを考える。

アドレスと、VPNへのマッピングは図のようになる

  • VPN[2] = 0x0
  • VPN[1] = 0x0
  • VPN[0] = 0x12
  • Page Offset = 0x678

仮想アドレスの変換は、上位のVPNから順番に変換していく仕組みだ。SATPの指すl1ptをベースとしてVPN[2]=0x0としてl1pt[0]アクセスされる。

l1pt[0]user_l2ptへのポインタを指しており、VPN[1]を使った変換に移っていく。

VPN[1]=0x0なので、 user_l2pt[0x0] にアクセスされる。

user_l2pt[0x0]user_llptへのリンクだ。 しかし、user_llpt[0x12] はPTEを特に何も設定してないのでここでページ・テーブル例外が発生する。

handle_fault()が呼ばれるのだが、引数causeで、addrには元の仮想アドレスが挿入される。

void handle_fault(uintptr_t addr, uintptr_t cause)
{
  assert(addr >= PGSIZE && addr < MAX_TEST_PAGES * PGSIZE);
  addr = addr/PGSIZE*PGSIZE;

  if (user_llpt[addr/PGSIZE]) {
    if (!(user_llpt[addr/PGSIZE] & PTE_A)) {
      user_llpt[addr/PGSIZE] |= PTE_A;
    } else {
      assert(!(user_llpt[addr/PGSIZE] & PTE_D) && cause == CAUSE_STORE_PAGE_FAULT);
      user_llpt[addr/PGSIZE] |= PTE_D;
    }
    flush_page(addr);
    return;
  }

  freelist_t* node = freelist_head;
  assert(node);
  freelist_head = node->next;
  if (freelist_head == freelist_tail)
    freelist_tail = 0;

  uintptr_t new_pte = (node->addr >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V | PTE_U | PTE_R | PTE_W | PTE_X;
  user_llpt[addr/PGSIZE] = new_pte | PTE_A | PTE_D;
  flush_page(addr);

  assert(user_mapping[addr/PGSIZE].addr == 0);
  user_mapping[addr/PGSIZE] = *node;

  uintptr_t sstatus = set_csr(sstatus, SSTATUS_SUM);
  memcpy((void*)addr, uva2kva(addr), PGSIZE);
  write_csr(sstatus, sstatus);

  user_llpt[addr/PGSIZE] = new_pte;
  flush_page(addr);

  asm volatile ("fence.i");
}

具体的にはこうだろう。

1. まずページオフセットの部分を切り落とす。

  assert(addr >= PGSIZE && addr < MAX_TEST_PAGES * PGSIZE);
  addr = addr/PGSIZE*PGSIZE;

2. user_llpt[addr/PGSIZE]user_llptにページが存在しているかをチェックし、 - 存在していた場合 - PTE_Aが設定されていなければ設定する。(AはAccessed Bitを意味する) - そうでなければ、PTE_Dを設定する (DはDirtyを意味する) - (ちなみに、"A"はそのページにアクセスされた(読取/書込/実行)ことがある、Dはそのページに対して書き込みが発生したことがある、の意味) - そのままページをフラッシュしてリターン

そこから先はページが存在しない場合の処理になる。

3. freelist_headから新しいページのフリーリストを取得する。フリーリストのポインタを更新する。

  freelist_t* node = freelist_head;
  assert(node);
  freelist_head = node->next;
  if (freelist_head == freelist_tail)
    freelist_tail = 0;

4. 新たなPTEを作成する。node->addrが新たなページのアドレスとなり、PTEのV/U/R/W/X/A/Dが設定され、user_llptの当該場所に設定される。

  uintptr_t new_pte = (node->addr >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V | PTE_U | PTE_R | PTE_W | PTE_X;
  user_llpt[addr/PGSIZE] = new_pte | PTE_A | PTE_D;
  flush_page(addr);

5. user_mapping に新たな作成したページをマッピングしておく。これはページのEvictionの時に使うらしい。

  assert(user_mapping[addr/PGSIZE].addr == 0);
  user_mapping[addr/PGSIZE] = *node;

6. sstatus.SUMを設定して、スーパーバイザモードからユーザモードへのページのアクセスを許可する。 - ちなみに、このページ処理はMEDELEGによりユーザモードで処理されるようになっている。

7. さらに、memcpyでもって、addrからuva2kva(addr)へのデータ転送を行う。

  uintptr_t sstatus = set_csr(sstatus, SSTATUS_SUM);
  memcpy((void*)addr, uva2kva(addr), PGSIZE);
  write_csr(sstatus, sstatus);

uva2kva(addr)の実装はこうだ。マクロの定義ではpaとなっているが、これは仮想アドレスのハズである。 MEGAPAGE_SIZEの定義はMEGAPAGE_SIZE = (PTES_PER_PT * PGSIZE) = ((1UL << RISCV_PGLEVEL_BITS) * PGSIZE) = ((1 << 9) * (1 << 12)) = 512 * 4096 = 0x20_0000 となる。これの実装はちょっと良く分からない。

#define uva2kva(pa) ((void*)(pa) - MEGAPAGE_SIZE)

8. 最後に、user_llpt[addr/PGSIZE] にPTEを設定して終わりだ。

  user_llpt[addr/PGSIZE] = new_pte;
  flush_page(addr);

flush_page(addr) の実装は以下だ。これは要するにsfence.vmaを実行する。

#define flush_page(addr) asm volatile ("sfence.vma %0" : : "r" (addr) : "memory")



以上の内容はhttps://msyksphinz.hatenablog.com/entry/2025/07/14/040000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14