LoginSignup
10
9

More than 5 years have passed since last update.

Linux(x86-32bit)のページフォルトハンドラを読んでみる(その3)

Posted at

今回やること

前回はページフォルトハンドラの中心となっているdo_page_fault()の前半部分を読んでみました。
今回も引き続きdo_page_fault()を読みます。

arch/x86/mm/fault.c
  vma = find_vma(mm, address);
  if (unlikely(!vma)) {
    bad_area(regs, error_code, address);
    return;
  }

まずはアクセス対象の仮想アドレス領域を見つける

find_vma()は以下のとおりです。

mm/mmap.c
/* Look up the first VMA which satisfies  addr < vm_end,  NULL if none. */
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)

まさにコメントのとおりです。この関数のチェックに外れたらそれは該当プロセスのメモリ空間にない領域へのアクセスであることが決定するので、bad_area()を呼びます。

arch/x86/mm/fault.c
  if (likely(vma->vm_start <= address))
    goto good_area;

しかし、先のfind_vma()のコメントにもあるとおり、find_vma()ではあくまでもaddress < vm_end(領域の最後)のみしかチェックしていません。
正当な領域であれば

vma->vm_start <= address < vma->vm_end

のはずです。
このチェックに成功すると、仮想アドレス領域として存在する空間へのアクセスであることが確定します。

「仮想アドレス領域として存在しないケース」とは

以下は仮想アドレス領域として存在しないケースとなります。
念のため、このケースの例を図にしてイメージしやすいようにします。

find_vma.jpg

上記の例だと、確かにvma->vm_end < addressなvmaはあり、find_vma()ではvma(vm_start=0x5000/vm_end=0x7000)を得られます。
しかし、アクセスしようとしているaddressには何もありません。
この場合に該当するケースを以下でさばきます。

arch/x86/mm/fault.c
  if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
    bad_area(regs, error_code, address);
    return;
  }

VM_GROWSDOWNは「該当仮想アドレス領域がアドレスの若い方に向かって伸びていく」領域であることを示します。
普通、アドレス空間はアドレス値が大きな方に向かって伸びていきますが、スタックのような領域だと小さな方に向かって伸びていきます。
そして、小さな方に向かって伸びていく(=vma->vm_startの値が変わる)ことで、結果として存在する仮想アドレス空間へのアクセスになるケースもありえます。

今回の判定にも該当しない場合、該当アクセスが正当になる可能性は消えるので、bad_area()が呼ばれます。

arch/x86/mm/fault.c
  if (error_code & PF_USER) {
    /*
     * Accessing the stack below %sp is always a bug.
     * The large cushion allows instructions like enter
     * and pusha to work. ("enter $65535, $31" pushes
     * 32 pointers and then decrements %sp by 65535.)
     */
    if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
      bad_area(regs, error_code, address);
      return;
    }
  }

これはスタックフレームを明らかにはみ出るようなaddressを弾く判定です。
Intel SDMによると、コメントで例として挙げられているenterは以下の命令となっています。太字のところが、上記直値の65536,32につながってくると思われます。
(ちなみにpushaは「Pushes the contents of the general-purpose registers onto the stack.」とのことであり、65536byteものスタックを消費するとは考えにくいです。enter命令がスタックを大量に消費する可能性があるということでしょう。)

ENTER imm16, imm8
(略)
Creates a stack frame for a procedure. The first operand (size operand) specifies the size of the stack frame (that is, the number of bytes of dynamic storage allocated on the stack for the procedure). The second operand (nesting level operand) gives the lexical nesting level (0 to 31) of the procedure.

スタックを拡張します。拡張に失敗した場合、bad_area()を呼ぶのだと推定できます。

arch/x86/mm/fault.c
  if (unlikely(expand_stack(vma, address))) {
    bad_area(regs, error_code, address);
    return;
  }

保護権限チェック

さて、いよいよ、アクセスしようとしている仮想アドレス空間が存在することを前提とした処理に入ります。

arch/x86/mm/fault.c
  /*
   * Ok, we have a good vm_area for this memory access, so
   * we can handle it..
   */
good_area:
  if (unlikely(access_error(error_code, vma))) {
    bad_area_access_error(regs, error_code, address);
    return;
  }

access_error()は以下のとおりです。
アクセス対象の仮想アドレス領域に対するアクセス方式(ReadとかWrite)が保護属性的に許されているか調べる関数です。

arch/x86/mm/fault.c
static inline int
access_error(unsigned long error_code, struct vm_area_struct *vma)
{
  if (error_code & PF_WRITE) {
    /* write, present and write, not present: */
    if (unlikely(!(vma->vm_flags & VM_WRITE)))
      return 1;
    return 0;
  }

  /* read, present: */
  if (unlikely(error_code & PF_PROT))
    return 1;

  /* read, not present: */
  if (unlikely(!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE))))
    return 1;

  return 0;
}

ちなみに、access_error()内に出てくるPF_PROTですが、この値は以下のenum値で表現されています。

arch/x86/mm/fault.c
/*
 * Page fault error code bits:
 *
 *   bit 0 ==  0: no page found 1: protection fault
 *   bit 1 ==  0: read access   1: write access
 *   bit 2 ==  0: kernel-mode access  1: user-mode access
 *   bit 3 ==       1: use of reserved bit detected
 *   bit 4 ==       1: fault was an instruction fetch
 */
enum x86_pf_error_code {

  PF_PROT   =   1 << 0,
  PF_WRITE  =   1 << 1,
  PF_USER   =   1 << 2,
  PF_RSVD   =   1 << 3,
  PF_INSTR  =   1 << 4,
};

前回引用したIntel SDMのerror_codeのフォーマットのbit0と上記enum値から、このビットはページフォルト例外の要因を表します。

ページフォルトの実際の処理

ここまでのチェックをくぐり抜けてページフォルトの実際の処理に入ります。
それを行っているのが、handle_mm_fault()と推定します。

arch/x86/mm/fault.c
  /*
   * If for any reason at all we couldn't handle the fault,
   * make sure we exit gracefully rather than endlessly redo
   * the fault:
   */
  fault = handle_mm_fault(mm, vma, address, flags);

以後は、handle_mm_fault()の戻り値を見て、エラー処理を行っています。
単なるエラー(VM_FAULT_ERROR)だけでなく、「リトライを要する」(VM_FAULT_RETRY)もあります。それがどのようなケースで起こりうるかはhandle_mm_fault()の実装を見ることで明らかになるでしょう。

arch/x86/mm/fault.c
  /*
   * If we need to retry but a fatal signal is pending, handle the
   * signal first. We do not need to release the mmap_sem because it
   * would already be released in __lock_page_or_retry in mm/filemap.c.
   */
  if (unlikely((fault & VM_FAULT_RETRY) && fatal_signal_pending(current)))
    return;

  if (unlikely(fault & VM_FAULT_ERROR)) {
    mm_fault_error(regs, error_code, address, fault);
    return;
  }

  /*
   * Major/minor page fault accounting is only done on the
   * initial attempt. If we go through a retry, it is extremely
   * likely that the page will be found in page cache at that point.
   */
  if (flags & FAULT_FLAG_ALLOW_RETRY) {
    if (fault & VM_FAULT_MAJOR) {
      tsk->maj_flt++;
      perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
              regs, address);
    } else {
      tsk->min_flt++;
      perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
              regs, address);
    }
    if (fault & VM_FAULT_RETRY) {
      /* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk
       * of starvation. */
      flags &= ~FAULT_FLAG_ALLOW_RETRY;
      flags |= FAULT_FLAG_TRIED;
      goto retry;
    }
  }

最後に、check_v8086_mode()を呼んで、セマフォを解除してページフォルト処理を終了します。

arch/x86/mm/fault.c
  check_v8086_mode(regs, address, tsk);

  up_read(&mm->mmap_sem);
}

気になるので、check_v8086()を覗いてみました。

arch/x86/mm/fault.c
/*
 * Did it hit the DOS screen memory VA from vm86 mode?
 */
static inline void
check_v8086_mode(struct pt_regs *regs, unsigned long address,
     struct task_struct *tsk)
{
  unsigned long bit;

  if (!v8086_mode(regs))
    return;

  bit = (address - 0xA0000) >> PAGE_SHIFT;
  if (bit < 32)
    tsk->thread.screen_bitmap |= 1 << bit;
}

ちょっとだけ仮想8086モード

これは、仮想8086モードに関する関数と推察できます。
仮想8086モードとは、「プロテクトモードのタスク保護の管理下で8086のコードを実行することによってその仮想機械の実装をハードウエア的に支援する(Wikipediaより抜粋)」機能です。

そして、PCアーキテクチャで0xA00000と言えばVideo RAM(VGA display)の領域です。この領域の値を変えると、画面への出力を変えることができます。
(メモリマップはOSDevの記事を参照されるとわかりやすいです。これ、どこかに公式な規格書あったりするのかいな?)

上記ソースの

tsk->thread.screen_bitmap |= 1 << bit;

は仮想8086モード下で動くソフトウェアが提供する仮想的な画面への出力を変更していると推定できます。

やっと、__do_page_fault()が終わりました

長かったですが、__do_page_fault()をざっと眺めてきました。
しかし、まだ先は長い。これからが本番です。

次回の予定

次回はページフォルトの実際的な処理を行っているhandle_mm_fault()を追ってみます。

10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9