レガシーガジェット研究所

気になったことのあれこれ。

Linux Kernel ~ 例外処理 ~

概要

「詳解Linux Kernel」を参考にVersion 2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelx86とする。

今回は例外処理について見ていく。

例外処理

LinuxはCPUが発生させる例外のほとんどをエラーとみなす。除算エラーであれば例外ハンドラが当該プロセスにシグナルを送り、復旧のための処理を走らせるか、ハンドラが設定されていない場合にはアボートする。

またハードウェア資源を効率的に使用するためにも例外を用いる。浮動小数レジスタの遅延切り替えを行う「デバイス使用不可例外」や、実際のページ割り当てを遅延実行するための「ページフォルト」などがある。

例外ハンドラの処理は大まかに次の3つとなる。

trap_init()で各例外に対応する例外ハンドラを適切に初期化する。

// arch/i386/kernel/traps.c
void __init trap_init(void)
{
    :

    set_trap_gate(0,&divide_error);
    set_intr_gate(1,&debug);
    set_intr_gate(2,&nmi);
    set_system_intr_gate(3, &int3); /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_intr_gate(14,&page_fault);
    set_trap_gate(15,&spurious_interrupt_bug);
    set_trap_gate(16,&coprocessor_error);
    set_trap_gate(17,&alignment_check);
#ifdef CONFIG_X86_MCE
    set_trap_gate(18,&machine_check);
#endif
    set_trap_gate(19,&simd_coprocessor_error);

    set_system_gate(SYSCALL_VECTOR,&system_call);
    
    /*
     * Should be a barrier for any external CPU state.
     */
    cpu_init();
    trap_init_hook();
}

ちなみにset_system_gate()の引数であるSYSTEM_VECTORは以下のように定義されている。

// include/asm-i386/mach-default/irq_vectors.h
#define SYSCALL_VECTOR     0x80

ベクタ番号8番の「ダブルフォルト」例外は不正動作を意味するためタスクゲートが使用される。

set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);

引数のGDT_ENTRY_DOUBLEFAULT_TSSでGDT32番のエントリに格納されているTSSセグメントディスクリプタを指しており、そこに格納されているeipespを読み込むことで専用のスタックで例外処理を行うことになる。

レジスタの退避

例外発生時に制御回路がエラーコードをスタックに積まない場合には以下のように0をスタックに積んでから例外ハンドラ名(do_プレフィックスから始まる)をスタックに詰みerror_code:へジャンプする。

// arch/i386/kernel/entry.S
ENTRY(overflow)
    pushl $0
    pushl $do_overflow
    jmp error_code

ENTRY(bounds)
    pushl $0
    pushl $do_bounds
    jmp error_code

ENTRY(invalid_op)
    pushl $0
    pushl $do_invalid_op
    jmp error_code

一方で制御回路がエラーコードをスタックに積む場合には以下のようになる。

// arch/i386/kernel/entry.S
ENTRY(stack_segment)
    pushl $do_stack_segment
    jmp error_code

ENTRY(general_protection)
    pushl $do_general_protection
    jmp error_code

ENTRY(alignment_check)
    pushl $do_alignment_check
    jmp error_code

ENTRY(page_fault)
    pushl $do_page_fault
    jmp error_code

先ほどあったpush $0が無くなっているのがわかる。

jmp命令の引数であるerror_codeからは以下のように続く。

error_code:
    pushl %ds
    pushl %eax
    xorl %eax, %eax
    pushl %ebp
    pushl %edi
    pushl %esi
    pushl %edx
    decl %eax           # eax = -1
    pushl %ecx
    pushl %ebx
    cld
    movl %es, %ecx
    movl ES(%esp), %edi     # get the function address
    movl ORIG_EAX(%esp), %edx   # get the error code
    movl %eax, ORIG_EAX(%esp)
    movl %ecx, ES(%esp)
    movl $(__USER_DS), %ecx
    movl %ecx, %ds
    movl %ecx, %es
    movl %esp,%eax          # pt_regs pointer
    call *%edi
    jmp ret_from_exception

上記では以下のような処理が行われている。

pushl %ds
pushl %eax
xorl %eax, %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
decl %eax           # eax = -1
pushl %ecx
pushl %ebx
  • EFALGレジスタのDF(Derection Flag)フラグをクリア

当該フラグをクリアすることで文字列処理時にEDI及びESIが自動インクリメントされるようになる。

cld
  • 関数アドレスとエラーコードをスタックから取得する
movl %es, %ecx
movl ES(%esp), %edi     # get the function address
movl ORIG_EAX(%esp), %edx   # get the error code
movl %eax, ORIG_EAX(%esp)
movl %ecx, ES(%esp)

上記を見るとコメントにもある通りedxにエラーコード、ediに関数アドレスを取得している。

ESORIG_EAXなどの値は以下のように定義されており、スタックポインタを起点に指定の位置に値を保存しているのがわかる。

EBX      = 0x00
ECX     = 0x04
EDX     = 0x08
ESI     = 0x0C
EDI     = 0x10
EBP     = 0x14
EAX     = 0x18
DS      = 0x1C
ES      = 0x20
ORIG_EAX    = 0x24
EIP     = 0x28
CS      = 0x2C
EFLAGS      = 0x30
OLDESP      = 0x34
OLDSS       = 0x38
movl $(__USER_DS), %ecx
movl %ecx, %ds
movl %ecx, %es
  • pt_regs構造体ポインタをeaxレジスタに保存。
pushl %ds
pushl %eax
xorl %eax, %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
decl %eax           # eax = -1
pushl %ecx
pushl %ebx
:
(省略)
:
movl %esp,%eax          # pt_regs pointer

esppt_regs構造体のポインタになるのは、pushして行く順番を見るとdsからpt_pregs構造体定義の順にpushされているためである。

// include/asm-i386/ptrace.h
struct pt_regs {
    long ebx;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    int  xds;
    int  xes;
    long orig_eax;
    long eip;
    int  xcs;
    long eflags;
    long esp;
    int  xss;
};
  • 例外ハンドラの呼び出し

先ほど取得した関数アドレスを使用している。

call *%edi
  • ハンドラの終了処理
jmp ret_from_exception

例外ハンドラ

do_プレフィックスから始まる関数名が例外ハンドラで、先ほどレジスタに設定した引数を受け取っているがわかる。

// i386/kernel/traps.c
fastcall void do_general_protection(struct pt_regs * regs, long error_code)
{
    :

ちなみにfast_callは以下のように定義されおり、関数の引数をレジスタで渡すように指定するものである。

#define fastcall    __attribute__((regparm(3)))

(i386では最大3個までで第一引数からeaxedxecxの順に引数がレジスタで渡される)

例外ハンドラの最後ではエラー番号とベクタ番号を設定し、プロセスに対してシグナルを送信している。

fastcall void do_general_protection(struct pt_regs * regs, long error_code)
{
    :
    (省略)
    :
    
    current->thread.error_code = error_code;
    current->thread.trap_no = 13;
    force_sig(SIGSEGV, current);
    return;

上記の処理が終了後先ほど見たret_from_exceptionが呼ばれ処理が終了する。

参考文献