LinuxカーネルのマネージドPageFault

例外処理は、ソフトウェアシステムの機能において重要な位置を占めます。 実際、異常なイベントに対するタイムリーで正しい応答を保証することは、オペレーティングシステム、特にそのコアによって実行される重要なタスクの1つです。 最新のLinuxカーネルは、例外処理のプロセスを制御する機能を提供しますが、そのインターフェイスの制限により、このメカニズムはカーネルモジュールの開発者の間では一般的ではありません。



さらに、PageFaultの例では、例外処理プロセスのいくつかの機能と、x86アーキテクチャ用のLinuxカーネルモジュールを開発する際にこの機能を使用できる方法の説明が考慮されます。







コア例外





カーネルで例外が使用される場所と方法の例として、カーネル空間とユーザー空間の間でデータをコピーすることを検討してください。 通常、関数copy_from_usercopy_to_userがこれを担当します。memcpyとは異なる特徴は、異なるアドレススペース間のデータ転送中に発生する例外を正しく処理することです。



実際、データがカーネルからユーザーにコピーされる状況( copy_to_user



関数)を考慮すると、書き込みが試行されるユーザープロセスのページがスワップ内にあるか、プロセスにまったくアクセスできない場合に状況が発生する可能性があります。 そして、最初のケースで問題の正しい解決策がこのページをロードしてコピーを継続することである場合、2番目のケースでは操作を中断してエラーコードをユーザーに返す必要があります(たとえば-EINVAL



)。



明らかに、欠落しているページに対応するアドレスをアドレス指定するコマンドを実行すると、例外、つまりページ障害例外 、またはページ#PF



#PF



)が発生します。 この時点で、カーネルは現在のタスクのコンテキストを保存し、対応するハンドラーのコードdo_page_faultを実行します。 何らかの方法で問題を修正し、カーネルは中断されたタスクのコンテキストを復元します。 ただし、例外の処理結果によっては、戻りアドレスが例外の原因となった命令のアドレスと異なる場合があります。 つまり、カーネルで提供されるメカニズムのおかげで、潜在的に「危険な」命令に対して、実行時に例外がスローされた場合に作業を続行するアドレスを指定することができます。



例外処理インターフェース





示されたメカニズムの実装方法を理解するには、カーネルからユーザーに4バイトをコピーするプリミティブの実装( __put_user_4関数)を検討する価値があります。



 62 ENTRY(__put_user_4) 63 ENTER 64 mov TI_addr_limit(%_ASM_BX),%_ASM_BX 65 sub $3,%_ASM_BX 66 cmp %_ASM_BX,%_ASM_CX 67 jae bad_put_user 68 ASM_STAC 69 3: movl %eax,(%_ASM_CX) <-     70 xor %eax,%eax 71 EXIT 72 ENDPROC(__put_user_4) ... 89 bad_put_user: 90 CFI_STARTPROC 91 movl $-EFAULT,%eax 92 EXIT ... 98 _ASM_EXTABLE(3b,bad_put_user)
      
      







ご覧のとおり、アドレス範囲のチェックに加えて、この関数はデータを直接転送します(69行目のmovl



命令)。 これは、例外が予想される場所です。なぜなら、 ターゲットアドレスが実際にユーザー空間アドレスの範囲に属しているという事実に加えて、それに関する情報はこれ以上ありません。 次に、次の_ASM_EXTABLEマクロに注意する必要があります。



 43 # define _ASM_EXTABLE(from,to) \ 44 .pushsection "__ex_table","a" ; \ 45 .balign 8 ; \ 46 .long (from) - . ; \ 47 .long (to) - . ; \ 48 .popsection
      
      







このマクロのアクションは、 from



およびto



2つの値を特別な__ex_table



セクションに追加することです。これは、 __ex_table



とおり、69行目の「疑わしい」命令のアドレスと、例外が処理された後に実行を続ける命令に対応します。すなわちbad_put_user



__ex_table



エントリを追加すると、障害点を管理しやすくなります。 このテーブルは、例外を処理するときにカーネルによって使用されます。



例外テーブルとその処理





そのため、前述したように、例外テーブルはこれらの命令に関する情報が保存される中心的な場所であり、その間のエラーは個別に処理する必要があります。 今後、カーネル自体のテーブルに加えて、モジュールごとに個別のテーブルも提供されることに注意してください。 ただし、 exception_table_entry構造によって記述される要素の構造を考慮する価値があります。



 97 struct exception_table_entry { 98 int insn, fixup; 99 };
      
      







ご覧のとおり、テーブル要素の形式は、マクロ_ASM_EXTABLE



調べたときに明らかになったものに対応しています。 最初の要素は命令を記述し、2番目の要素は例外の場合に制御が転送されるコードを記述します。 ページ障害例外が発生するたびに、Linuxカーネルは、とりわけ、この例外の原因となったコマンドのアドレスがカーネル__ex_table



テーブルに__ex_table



いるか、ロードされたモジュールのテーブルに__ex_table



いるかを__ex_table



ます。 そのようなレコードが見つかった場合、対応するアクションが実行されます。 そうでない場合、カーネルはいくつかの標準ロジックを実行して例外処理を完了します。



カーネルモジュールの個々の例外テーブルに関しては、これらのテーブルの要素の形式は標準であり、カーネルの形式に対応しています。 各モジュールのこのようなテーブルへのリンクは、 THIS_MODULE->extable



で利用できますが、テーブル要素の数はTHIS_MODULE->num_exentries



含まれていTHIS_MODULE->num_exentries



THIS_MODULEマクロ自体は、モジュール記述子構造へのリンクを提供します。



 223 struct module 224 { ... 276 /* Exception table */ 277 unsigned int num_exentries; 278 struct exception_table_entry *extable; ... 378 };
      
      







以下は、例外の原因となった命令に一致するハンドラーを検索する主要なカーネル関数です。 彼女のコードは次のとおりです



  50 /* Given an address, look for it in the exception tables. */ 51 const struct exception_table_entry *search_exception_tables(unsigned long addr) 52 { 53 const struct exception_table_entry *e; 54 55 e = search_extable(__start___ex_table, __stop___ex_table-1, addr); 56 if (!e) 57 e = search_module_extables(addr); 58 return e; 59 }
      
      







ご覧のとおり、まず最初に、 __ex_table



カーネルのベーステーブルで検索が実行され、その後、結果がない場合にのみ、モジュール例外テーブル間で検索が続行されます。 命令アドレスに一致するハンドラーがない場合、この関数を実行するカーネルの結果はNULL



になりNULL



。 それ以外の場合、結果は例外テーブルの対応する要素へのポインタになります。



カーネルモジュールでの例外処理





したがって、例外を処理するための一般的な手順が明確であれば、トレーニングのために、例外を作成して処理することを目的とするモジュールを作成できます。 すでに書いたコードはgithubで入手できます。 次に、コードの簡単な説明とコメントを提供します。



したがって、通常のNULLポインターの逆参照を行う関数でPageFault例外の生成を処理します。



 static void raise_page_fault(void) { debug(" %s enter\n", __func__); ((int *)0)[0] = 0xdeadbeef; debug(" %s leave\n", __func__); }
      
      







明らかに、nullポインターに書き込もうとすると、クラッシュが発生します。 そして、これはまさにあなたが必要とするものです。 適切に対応するには、次のことを行う必要があります。







以下は、 udis86を使用した逆アセンブリを使用して上記のアクションを実行する関数です。



 static int fixup_page_fault(struct exception_table_entry * entry) { ud_t ud; ud_initialize(&ud, BITS_PER_LONG, \ UD_VENDOR_ANY, (void *)raise_page_fault, 128); while (ud_disassemble(&ud) && ud.mnemonic != UD_Iret) { if (ud.mnemonic == UD_Imov && \ ud.operand[0].type == UD_OP_MEM && ud.operand[1].type == UD_OP_IMM) { unsigned long address = \ (unsigned long)raise_page_fault + ud_insn_off(&ud); extable_make_insn(entry, address); extable_make_fixup(entry, address + ud_insn_len(&ud)); return 0; } } return -EINVAL; }
      
      







ご覧のとおり、最初のステップは逆raise_page_fault



を構成することです(分析の始まりはraise_page_fault



)。 次に、指定された検索深度で、コマンドを繰り返し処理します。 目的のコマンド(操作((int *)0)[0] = 0xdeadbeef;



)は、タイプUD_OP_MEM



第1オペランドとタイプUD_OP_MEM



の第2オペランドを持つ通常のmovl $0xdeadbeef, 0



UD_OP_IMM



。 コマンドアドレスが見つかるとすぐに、テーブル要素が形成されます。 同時に、機能が実行されます。



 static void extable_make_insn(struct exception_table_entry * entry, unsigned long addr) { #if LINUX_VERSION_CODE >= KERNEL_VERSION(3,5,0) entry->insn = (unsigned int)((addr - (unsigned long)&entry->insn)); #else entry->insn = addr; #endif } static void extable_make_fixup(struct exception_table_entry * entry, unsigned long addr) { #if LINUX_VERSION_CODE >= KERNEL_VERSION(3,5,0) entry->fixup = (unsigned int)((addr - (unsigned long)&entry->fixup)); #else entry->fixup = addr; #endif }
      
      







それらの最初は、構造内の命令のアドレスを形成します。 2番目はfixapアドレス、つまり 制御が転送されるコマンド。 カーネル3.5から、 exception_table_entry



構造に小さな変更が発生したことに注意することが重要です。つまり、フィールドの次元が縮小されました-64ビットアーキテクチャのinsn



およびfixup



です。 これにより、アドレスの保存に必要なメモリを削減できましたが、計算ロジックはわずかに変更されました。 したがって、カーネル3.5以降、 insn



およびfixup



、これらの要素に関連するアドレスオフセットに対応する32ビット値が格納されます。 706276543b699d80f546e45f8b12574e7b18d952をすべて台無しにするコミットをもたらすことに興味がある人のために。



おわりに





この例は、カーネルモジュールを使用してLinuxカーネルの例外処理を管理する機能を示しています。 このテスト例では、事前に準備された環境、つまり構成されたモジュールexables



テーブルで例外(PageFault)が呼び出されました。 後者の状況により、異常終了を解消し、緊急指示に続くコマンドでプログラムの実行を継続することができました。



さらに、準備されたテストケースにより、 除算エラー (#DE)や未定義オペコード (#UD)などの他の例外を処理する可能性を評価できます。



 struct { const char * name; int (* fixup)(struct exception_table_entry *); void (* raise)(void); } exceptions[] = { { .name = "0x00 - div0 error (#DE)", .fixup = fixup_div0_error, .raise = raise_div0_error, }, { .name = "0x06 - undefined opcode (#UD)", .fixup = fixup_undefined_opcode, .raise = raise_undefined_opcode, }, { .name = "0x14 - page fault (#PF)", .fixup = fixup_page_fault, .raise = raise_page_fault, }, };
      
      






All Articles