Linux 2.1.8 におけるカーネルレベル例外処理 Joerg Pommnitz による解説 カーネルモードで動作しているとき、プロセスはしばしば、信用できない プログラムから渡されたアドレスにあるユーザモードメモリにアクセスする 必要があります。カーネルは自身を保護するため、このアドレスの正当性を 調べなければなりません。 古いバージョンの 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 xorl %edx,%edx > c017e787 movl 0xc01c7bec,%eax > c017e78c cmpl $0x18,0x314(%eax) > c017e793 je c017e79f > c017e795 cmpl $0xbfffffff,0x40(%esp,1) > c017e79d ja c017e7a7 > c017e79f movl %edx,%eax > c017e7a1 movl 0x40(%esp,1),%ebx > c017e7a5 movb (%ebx),%dl > c017e7a7 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 そして最後に ―― > 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 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 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 プロジェクト 翻訳者:川崎 貴彦