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 を選んだという わけです。