JF Linux Kernel 2.6 Documentation: /usr/src/linux/Documentation/exception.txt

exception.txt

不正アドレスにアクセスした際の例外処理について [プレインテキスト版]


     Linux 2.1.8 におけるカーネルレベル例外処理
  Joerg Pommnitz <joerg@raleigh.ibm.com> による解説

  カーネルモードで動作しているとき、プロセスはしばしば、信用できない
プログラムから渡されたアドレスにあるユーザモードメモリにアクセスする
必要があります。カーネルは自身を保護するため、このアドレスの正当性を
調べなければなりません。

  古いバージョンの Linux では、この作業は

    int verify_area(int type, const void * addr, unsigned long size)

という関数でおこなわれていました。

  この関数は、アドレス addr で始まり、size サイズを持つメモリ領域を
type で指定した操作 (read もしくは write) でアクセスすることが可能か
どうかを確認していました。これをおこなうため、verify_read はアドレス
addr を含む仮想メモリ領域 (vma) を探さなければなりませんでした。通常
の場合 (プログラムが正しく動作している場合)、このテストは成功していま
した。失敗するのは、バグを含むプログラムを対象としているときだけでした。
幾つかのカーネルプロファイリングテストにおいて、通常は必要とされない
この確認作業は、かなり多くの時間を消費していました。

  この状況を克服するため、Linus は、Linux が動作可能な CPU の全てが
持っている仮想メモリ用ハードウェアに、このテストをおこなわせることに
決めました。

  これはどのように動作するのでしょうか?

  カーネルがその時点でアクセス不可であるアドレスにアクセスを試みると、
CPU は必ずページフォールト例外を生成し、arch/i386/mm/fault.c にある
ページフォールトハンドラ

    void do_page_fault(struct pt_regs *regs, unsigned long error_code)

を呼びます。スタック上のパラメータ群は arch/i386/kernel/entry.S 内の
低レベルアセンブリコードによって生成されます。パラメータ regs は
スタック上に保存されたレジスタ群を指すポインタで、error_code は例外の
理由を示すコードを含んでいます。
(訳注:do_page_fault のコメントより
   error_code:
        bit 0 == 0 は ページがない、1 は保護によるフォールト
        bit 1 == 0 は read, 1 は write
        bit 2 == 0 は カーネル、1 はユーザモード
)

  do_page_fault は最初に、CPU コントロールレジスタ CR2 からアクセス
不能であったアドレスを取得します。もし、そのアドレスが当該プロセスの
仮想アドレス空間内のものであれば、フォールトはおそらく、ページが
スワップインされていなかった、書込み不可であった、というような理由で
起こったのでしょう。しかしながら、私たちが関心を持っているのは、そう
ではない場合 ――アドレスが有効でない場合、つまり、このアドレスを含む
仮想メモリ領域が存在しない場合、です。このような場合、カーネルは
bad_area ラベルにジャンプします。

  そこで、カーネルは、実行を続けることが可能なアドレス (fixup コード) を
見つけるため、例外を引き起こした命令のアドレス (つまり regs->eip) を使い
ます。この検索が成功すれば、フォールトハンドラはリターンアドレス (再度
regs->eip) を修正し、リターンします。実行は fixup 内のアドレスで継続し
ます。
(訳注:この段落は do_page_fault 内の次のコードを説明したものです。
        if ((fixup = search_exception_table(regs->eip)) != 0) {
                regs->eip = fixup;
                return;
        }
)

  fixup はどこを指しているのでしょうか?

  私たちは fixup 内にジャンプしたのですから、fixup は明らかに実行可能
コードを指しています。このコードはユーザメモリにアクセスするマクロの
中に隠されています。例として、include/asm/unaccess.h 内で定義されている
get_user マクロを取り上げることにしますが、その定義を追いかけるのは
ちょっと大変なので、プリプロセッサとコンパイラによって生成されたコードを
見てみることにしましょう。詳しく考察するために、
drivers/char/console.c 内の get_user の呼出しを選びました。

  console.c 1405 行目 の元のコード ――

        get_user(c, buf);

  プリプロセッサの出力 (少し読みやすくするために編集してあります) ――

(
  {
    long __gu_err = - 14 , __gu_val = 0;
    const __typeof__(*( (  buf ) )) *__gu_addr = ((buf));
    if (((((0 + current_set[0])->tss.segment) == 0x18 )  ||
       (((sizeof(*(buf))) <= 0xC0000000UL) &&
       ((unsigned long)(__gu_addr ) <= 0xC0000000UL - (sizeof(*(buf)))))))
      do {
        __gu_err  = 0;
        switch ((sizeof(*(buf)))) {
          case 1:
            __asm__ __volatile__(
              "1:      mov" "b" " %2,%" "b" "1\n"
              "2:\n"
              ".section .fixup,\"ax\"\n"
              "3:      movl %3,%0\n"
              "        xor" "b" " %" "b" "1,%" "b" "1\n"
              "        jmp 2b\n"
              ".section __ex_table,\"a\"\n"
              "        .align 4\n"
              "        .long 1b,3b\n"
              ".text"        : "=r"(__gu_err), "=q" (__gu_val): "m"((*(struct __large_struct *)
                            (   __gu_addr   )) ), "i"(- 14 ), "0"(  __gu_err  )) ;
              break;
          case 2:
            __asm__ __volatile__(
              "1:      mov" "w" " %2,%" "w" "1\n"
              "2:\n"
              ".section .fixup,\"ax\"\n"
              "3:      movl %3,%0\n"
              "        xor" "w" " %" "w" "1,%" "w" "1\n"
              "        jmp 2b\n"
              ".section __ex_table,\"a\"\n"
              "        .align 4\n"
              "        .long 1b,3b\n"
              ".text"        : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
                            (   __gu_addr   )) ), "i"(- 14 ), "0"(  __gu_err  ));
              break;
          case 4:
            __asm__ __volatile__(
              "1:      mov" "l" " %2,%" "" "1\n"
              "2:\n"
              ".section .fixup,\"ax\"\n"
              "3:      movl %3,%0\n"
              "        xor" "l" " %" "" "1,%" "" "1\n"
              "        jmp 2b\n"
              ".section __ex_table,\"a\"\n"
              "        .align 4\n"        "        .long 1b,3b\n"
              ".text"        : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
                            (   __gu_addr   )) ), "i"(- 14 ), "0"(__gu_err));
              break;
          default:
            (__gu_val) = __get_user_bad();
        }
      } while (0) ;
    ((c)) = (__typeof__(*((buf))))__gu_val;
    __gu_err;
  }
);

  すごい! GCC/アセンブリの黒魔術です。これを追いかけるのは不可能ですので、
gcc が生成するコードを見てみることにしましょう。

 >         xorl %edx,%edx
 >         movl current_set,%eax
 >         cmpl $24,788(%eax)
 >         je .L1424
 >         cmpl $-1073741825,64(%esp)
 >         ja .L1423
 > .L1424:
 >         movl %edx,%eax
 >         movl 64(%esp),%ebx
 > #APP
 > 1:      movb (%ebx),%dl                /* これが実際のユーザアクセスです */
 > 2:
 > .section .fixup,"ax"
 > 3:      movl $-14,%eax
 >         xorb %dl,%dl
 >         jmp 2b
 > .section __ex_table,"a"
 >         .align 4
 >         .long 1b,3b
 > .text
 > #NO_APP
 > .L1423:
 >         movzbl %dl,%esi

  オプティマイザは良い仕事をしてくれます。そして私たちが現実的に理解可能
であるものを提供してくれます。理解できますよね? 実際のユーザメモリへの
アクセスは極めて明白です。統一されたアドレス空間のおかげで、私たちはユーザ
メモリ内のアドレスにアクセスすることができます。しかし、.section は何を
するのでしょうか?????

  これを理解するため、構築後のカーネルを覗いてみる必要があります ――

 > objdump --section-headers vmlinux
 >
 > vmlinux:     file format elf32-i386
 >
 > Sections:
 > Idx Name          Size      VMA       LMA       File off  Algn
 >   0 .text         00098f40  c0100000  c0100000  00001000  2**4
 >                   CONTENTS, ALLOC, LOAD, READONLY, CODE
 >   1 .fixup        000016bc  c0198f40  c0198f40  00099f40  2**0
 >                   CONTENTS, ALLOC, LOAD, READONLY, CODE
 >   2 .rodata       0000f127  c019a5fc  c019a5fc  0009b5fc  2**2
 >                   CONTENTS, ALLOC, LOAD, READONLY, DATA
 >   3 __ex_table    000015c0  c01a9724  c01a9724  000aa724  2**2
 >                   CONTENTS, ALLOC, LOAD, READONLY, DATA
 >   4 .data         0000ea58  c01abcf0  c01abcf0  000abcf0  2**4
 >                   CONTENTS, ALLOC, LOAD, DATA
 >   5 .bss          00018e21  c01ba748  c01ba748  000ba748  2**2
 >                   ALLOC
 >   6 .comment      00000ec4  00000000  00000000  000ba748  2**0
 >                   CONTENTS, READONLY
 >   7 .note         00001068  00000ec4  00000ec4  000bb60c  2**0
 >                   CONTENTS, READONLY

  生成されたオブジェクトファイルには、標準的でない ELF セクションが
明らかにふたつ存在します。しかし、まず始めに、構築後の実行可能カー
ネル内のコードに何が起こったのかを解き明かしたいと思います。

 > objdump --disassemble --section=.text vmlinux
 >
 > c017e785 <do_con_write+c1> xorl   %edx,%edx
 > c017e787 <do_con_write+c3> movl   0xc01c7bec,%eax
 > c017e78c <do_con_write+c8> cmpl   $0x18,0x314(%eax)
 > c017e793 <do_con_write+cf> je     c017e79f <do_con_write+db>
 > c017e795 <do_con_write+d1> cmpl   $0xbfffffff,0x40(%esp,1)
 > c017e79d <do_con_write+d9> ja     c017e7a7 <do_con_write+e3>
 > c017e79f <do_con_write+db> movl   %edx,%eax
 > c017e7a1 <do_con_write+dd> movl   0x40(%esp,1),%ebx
 > c017e7a5 <do_con_write+e1> movb   (%ebx),%dl
 > c017e7a7 <do_con_write+e3> movzbl %dl,%esi

  ユーザメモリへのアクセス処理全体が、10 個の x86 マシン用命令になって
います。.section 疑似命令内にくくられた命令群は、もはや通常の実行パス
ではありません。これらは、実行可能ファイル内の異なるセクションに配置
されています ――

 > objdump --disassemble --section=.fixup vmlinux
 >
 > c0199ff5 <.fixup+10b5> movl   $0xfffffff2,%eax
 > c0199ffa <.fixup+10ba> xorb   %dl,%dl
 > c0199ffc <.fixup+10bc> jmp    c017e7a7 <do_con_write+e3>


  そして最後に ――

 > objdump --full-contents --section=__ex_table vmlinux
 >
 >  c01aa7c4 93c017c0 e09f19c0 97c017c0 99c017c0  ................
 >  c01aa7d4 f6c217c0 e99f19c0 a5e717c0 f59f19c0  ................
 >  c01aa7e4 080a18c0 01a019c0 0a0a18c0 04a019c0  ................

  もしくは、人間が読むことのできるバイトオーダに変換すると ――

 >  c01aa7c4 c017c093 c0199fe0 c017c097 c017c099  ................
 >  c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5  ................
                               ^^^^^^^^^^^^^^^^^
                               ここが面白いところですよ!
 >  c01aa7e4 c0180a08 c019a001 c0180a0a c019a004  ................

  何が起こったのでしょうか? アセンブリ疑似命令である

.section .fixup,"ax"
.section __ex_table,"a"

は、後ろに続くコードを ELF オブジェクトファイル内の指定されたセクションへ
移動させるようにと、アセンブラに指示しました。そのため、

3:      movl $-14,%eax
        xorb %dl,%dl
        jmp 2b

という命令は、最終的にオブジェクトファイルの .fixup セクションに行き、

        .long 1b,3b

というアドレス群は、オブジェクトファイルの __ex_table セクションに
行きます。1b と 3b はローカルラベルです。ローカルラベル 1b (1b は
後ろに戻って次のラベル 1 を意味します) はフォールトするかもしれない
命令のアドレスで、つまり、私たちの場合はラベル 1 のアドレスは
c017e7a5 となります ――

元のアセンブリコード: > 1:      movb (%ebx),%dl
vmlinux にリンク後  : > c017e7a5 <do_con_write+e1> movb   (%ebx),%dl

  ローカルラベル 3 (再度後方に)はフォールトを扱うコードのアドレスで、
私たちの例では、その実際の値は c0199ff5 です ――

元のアセンブリコード: > 3:      movl $-14,%eax
vmlinux にリンク後  : > c0199ff5 <.fixup+10b5> movl   $0xfffffff2,%eax

  アセンブリコード

 > .section __ex_table,"a"
 >         .align 4
 >         .long 1b,3b

は、カーネルの例外テーブル内の値の組

 >  c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5  ................
                               ^これが  ^これが
                               1b       3b

c017e7a5,c0199ff5 となります。

 (訳注:__ex_table セクション内には、「不正アドレスをアクセスする可能性の
ある命令へのポインタ」と「不正アドレス・エラーを処理するための命令への
ポインタ」が、ふたつで一組とされており、幾つも並べられています。そして、
「不正アドレス・エラーを処理するための命令」本体は、__ex_table セクション
内ではなく、.fixup セクション内に存在します。__ex_table セクション、.fixup
セクションのエントリ数は、ユーザメモリを参照するマクロ (get_user など) を
記述するたびに増加していきます)

  さて、適切な仮想メモリ領域を持たないカーネルモードからのフォールトが
発生した場合、実際には何が起こるのでしょうか?

1.) 不正なアドレスにアクセスする ――
 > c017e7a5 <do_con_write+e1> movb   (%eax),%dl
2.) MMU が例外を生成する
3.) CPU が do_page_fault を呼び出す
4.) do_page_fault は search_exception_table を呼び出す (例では、引数
    regs->eip の値は c017e7a5)
5.) search_exception_table は例外テーブル (例外テーブルとは、つまり
    ELF セクション __ex_table の内容のこと) 内で、アドレス c017e7a5 を
    探し、フォールトハンドルコードのアドレス c0199ff5 を返します。
    (訳注:search_exception_table は例外テーブル内で、「不正アドレスを
    アクセスする可能性のある命令へのポインタ」欄の値が c0177e7a5 である
    エントリを探し、そのエントリの「不正アドレス・エラーを処理するための
    命令へのポインタ」欄の値 c0199ff5 を返します)
6.) do_page_fault は自身のリターンアドレスを、フォールトハンドルコードを
    指すように修正し、リターンします。
7.) フォールトを処理するコード内で実行が継続されます。
8.) 8a) EAX が -EFAULT (== -14) になります
    8b) DL  が ゼロ になります (ユーザ空間から "read" した値)
    8c) ローカルラベル 2 (フォールトを起こしているユーザアクセスの
        すぐ後の命令のアドレス) で実行が継続します。

  8a から 8c までのステップは、フォールトを起こした命令をエミュレート
します。

  だいたい以上です。もしあなたが私たちの例を見ているならば、なぜ例外
ハンドラのコードの中で EAX が -EFAULT に設定されているのか質問したく
なるかもしれません。えぇと、実際のところ、ユーザメモリへのアクセスが
成功していれば get_user マクロは 0 を返し、失敗していれば -EFAULT を
返します。私たちのオリジナルコードではこの戻り値をテストしませんでし
たが、get_user マクロ内のインラインアセンブリコードは -EFAULT を返そう
としています。GCC はこの値を返すために EAX を選んだというわけです。

注意:
例外テーブルの構築方法、および、順番に並べるという必要性のため、例外は
.text セクション内のコードでのみ使うようにします。他のどのセクションも、
例外テーブルを正しくソートされていない状態にしてしまうので、例外は失敗
するでしょう。


翻訳:JF プロジェクト
翻訳者:川崎 貴彦

Linux カーネル 2.6 付属文書一覧へ戻る