linux2.6の場合のExceptionと`int $0x80`(system call)の挙動
linux2.6の場合のExceptionとint $0x80
の挙動
前の記事でxv6ではどうなのかを述べた。今回はlinux kernel(2.6.11)の場合のexceptionとint $0x80
の挙動についてまとめる。今回も長いので、I/O APIC, local APICがからんだinterruptは後回しにしたい。
referenceは前の記事を参考にしてね。
IDTの初期設定
x86(32-bit)では、arch/i386/kernel/head.Sのstartup_32
がprotected modeのentrypointになっており、registerの設定をいろいろした後に、start_kernelに飛ぶ。この関数は、init/main.cにある。
最初に
arch/i386/kernel/head.S
でIDTを初期化して、とりあえずロードまで済ませている。start_kenel -> trap_initの
set_**_gate
でIDTのentryを設定する。sys_call(0x80)とexceptionの設定start_kernel -> trap_init -> cpu_initで再度IDTをロード
start_kernel -> init_IRQにて、IDTのentryをinterrupt gateに初期化する。syscall(0x80)の部分は
trap_init
で設定されているのでskipしていることに注意。
// init_IRQ in arch/i386/kernel/i8259.c // init_IRQ // skip for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) { if (i >= NR_IRQS) break; // void set_intr_gate(unsigned int n, void *addr) // // _set_gate(idt_table+n,14,0,addr,__KERNEL_CS); if (FIRST_EXTERNAL_VECTOR + i != SYSCALL_VECTOR) set_intr_gate(FIRST_EXTERNAL_VECTOR + i, interrupt[i]); } // skip
ここで、interrupt
はarch/i386/kernel/entry.S
で定義されているcallbackを要素に持つ配列。次回以降で詳しくみていく。
exception
divide_error exception(#0)を例に取る。 User landから呼ばれるので%csのRPL(CPL)は3である。
hardware側の処理 ~ do_divide_error
まで
前節で、start_kenel -> trap_init -> set_trap_gate(0,÷_error);
と設定した1。
hardware側の処理の概要は、http://cstmize.hatenablog.jp/entry/2019/03/20/xv6%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AEException%E3%81%A8Interrupt%E3%81%AE%E6%8C%99%E5%8B%95 で書いたが、ここでは、TSSの設定だけ見る。
// start_kernel -> trap_init -> cpu_init(void) // skip // t->esp0 = current->thread->esp0; load_esp0(t, thread); // #define set_tss_desc(cpu,addr) __set_tss_desc(cpu, GDT_ENTRY_TSS, addr) GDT_ENTRY_TSS=GDT_ENTRY_KERNEL_BASE + 4 set_tss_desc(cpu,t); load_TR_desc(); // #define load_TR_desc() __asm__ __volatile__("ltr %%ax"::"a" (GDT_ENTRY_TSS*8))
なので、TSSにloadされるesp0はprocのstack(thread_info)内にあることがわかる。
例外が発生した場合、権限周りは大丈夫として、スタックにレジスタが積まれる。後ほどみていくが、struct pt_regs
が形成されていく。
hareware側では
struct pt_regs { long ebx; long ecx; long edx; long esi; long edi; long ebp; long eax; int xds; int xes; // ES long orig_eax; // ORIG_EAX // these below register is stored by hardware automatically(the previous values) long eip; int xcs; long eflags; long esp; int xss; };
でregisterがpushされた結果、long eip;
まで自動的にstackがgrow downする。(下に伸びるので、struct pt_regs
の要素はxss(stack segment)の方向から構築されていく。ES
やORIG_EAX
はconstantとして指定されているが、すぐ下のコードに出てくる。
その後、arch/i386/kernel/entry
にて
ENTRY(divide_error) pushl $0 // pt_regs.orig_eax pushl $do_divide_error // pt_regs.xes ALIGN error_code: pushl %ds // pt_regs.xds pushl %eax // pt_regs.eax xorl %eax, %eax // reset %eax = 0 pushl %ebp // pt_regs.ebp pushl %edi // pt_regs.edi pushl %esi // pt_regs.esi pushl %edx // pt_regs.edx decl %eax // %eax = -1 pushl %ecx // pt_regs.ecx pushl %ebx // pt_regs.ebx # cld instruction to clear the direction flag eflags.DF cld # NOW, %esp(current stack pointer) is at &pt_regs, which type is `struct pt_regs`. movl %es, %ecx # %ecx <= %es # TODO: why? movl ES(%esp), %edi # get the function address($do_divide_error) movl ORIG_EAX(%esp), %edx # set the error code in %edx # pt_regs.orig_eax <= -1 to separate from syscall instruction(0x80) movl %eax, ORIG_EAX(%esp) # %ecx <= 0x20(%esp) # TODO: why? movl %ecx, ES(%esp) # designate user data segment in %ds, %es movl $(__USER_DS), %ecx movl %ecx, %ds movl %ecx, %es movl %esp,%eax # set the stack pointer(&pt_regs) in %edx // call do_divide_error call *%edi jmp ret_from_exception
と処理が走る。個々でやっていることは、struct pt_regs
の構築とsegment registerの設定だ。
%edi
にはdo_divide_error
関数へのpointerが入っているので、call *%edi
でdo_divide_error
が動く。
これが終わったら、ret_from_exception
に移動する。
Note) xv6では、vectors.plの
# vector0: # pushl $0 # pushl $0 # jmp alltraps
および、trapasm.Sの https://github.com/mit-pdos/xv6-public/blob/b818915f793cd20c5d1e24f668534a9d690f3cc8/trapasm.S#L4-L20 に対応する。struct pt_regs
はstruct trapframe
に対応していることが見て取れる。
do_divide_error
do_divide_error
そのものはコードになくて、arch/i386/kernel/traps.c
にてDO_VM86_ERROR_INFO( 0, SIGFPE, "divide error", divide_error, FPE_INTDIV, regs->eip)
というマクロで一般化されているので、ばらしてみる:
// #define fastcall __attribute__((regparm(3))) fastcall void do_divide_error(struct pt_regs * regs, long error_code) { siginfo_t info; info.si_signo = signr; info.si_errno = 0; info.si_code = sicode; info.si_addr = (void __user *)siaddr; if (notify_die(DIE_TRAP, str, regs, error_code, trapnr, signr) == NOTIFY_STOP) return; do_trap(trapnr, signr, str, 1, regs, error_code, &info); }
雰囲気的には、do_trap
関数を呼ぶようだ。更にこの関数を除くと、regs->xcs
は3(exception呼び出し前のCPL=3)なので、trap_signal
labelに遷移する...
fastcallとはなんぞや? これは、関数の引数とregisterを直接対応させることができる。regparmはGCC拡張で、
On x86-32 targets, the
regparm
attribute causes the compiler to pass arguments number one to number if they are of integral type in registers EAX, EDX, and ECX instead of on the stack. Functions that take a variable number of arguments continue to be passed all of their arguments on the stack. https://gcc.gnu.org/onlinedocs/gcc/x86-Function-Attributes.html
とある。通常は、関数呼び出しの前にスタックに積んで置く準備をする必要があるが、今回の場合、
movl ES(%esp), %edi # get the function address($do_divide_error) movl %esp,%eax # set the stack pointer(&pt_regs) in %edx
の用に、%eax, %ediをセットしているので、これらが、(struct pt_regs * regs, long error_code)
に対応する。stack取り出さないで、registerを直接使うという点でfastcallなんだろう。
notify_die
はkprobe_exceptions_notify
がcallbackとして呼ばれ、notify_die
の第一引数で戻り値が決定するようだ。今回はDIE_TRAP
で、NOTIFY_DONE
となるので、条件分岐はfalseとなる。したがって、どのみちdo_trap
が呼ばれる。
do_trap
infoはstruct siginfo
でmemberが.si_signo: SIGFPE, .si_code: FPE_INTDIV
となる。force_sig_info -> specific_send_sig_info -> send_signalと続くようだ。user processにSIGFPE
を投げるのが仕事っぽい。
struct siginfo
はuser landでも出てくる構造体で、sigactionでキャッチするSIGNALを設定してstruct siginfo
で受け取れるようにできる。ココらへんは The Linux Programming Interfaceのch21.4あたりで詳しく解説されている。
do_trapが終わったので、ret_from_exception
を見る。
ret_from_exception
ret_from_exception
については、Understanding the Linux Kernelのch4.6が圧倒的に詳しいので、2つだけこの記事で触れる。
1つ目が一番最初のところ。
ret_from_exception:
preempt_stop # #define preempt_stop cli
exceptionの場合、trap gateが入口になる事があるため、eflags.IF=ONのママの場合がある。このため、exceptionの部分だけ、cli
命令が付加されている。以降、iret命令が呼ばれるまで2はinterrupt disableの状態が続く。
2つ目がexceptionの一番最後のところ。最終的にrestore_allラベルに飛んで、User landに戻る。
restore_all: RESTORE_ALL #define RESTORE_ALL \ RESTORE_REGS \ addl $4, %esp; \ 1: iret; \ .section .fixup,"ax"; \ 2: sti; \ movl $(__USER_DS), %edx; \ movl %edx, %ds; \ movl %edx, %es; \ movl $11,%eax; \ call do_exit; \ .previous; \ # swaps current section (code) with previous section (data) .section __ex_table,"a";\ .align 4; \ .long 1b,2b; \ .previous # // swaps curent section (data) with previous section (code)
RESTORE_REGSをバラすと、
restore_all: popl %ebx; popl %ecx; popl %edx; popl %esi; popl %edi; popl %ebp; popl %eax; popl %ds; popl %es; addl $4, %esp; iret;
再びstruct pt_regs
を見る:
struct pt_regs { long ebx; long ecx; long edx; long esi; long edi; long ebp; long eax; int xds; int xes; // ES // pop above value long orig_eax; // addl $4, %esp; long eip; int xcs; long eflags; long esp; int xss; };
iret
も http://cstmize.hatenablog.jp/entry/2019/03/20/xv6%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AEException%E3%81%A8Interrupt%E3%81%AE%E6%8C%99%E5%8B%95 の最後で触れているが、hardware側で%eip
やら%esp
やら%ss
やらを復元していく。%eip
はexceptionが生じた直後のinstruction pointerだったから、exceptionの処理終了後はここに飛んで作業を再開する。(ただ、SIGFPE
は"重い"signalなので、このuser processはabortして死ぬことになるだろう)
Note) .section .fixup
はよくわかんないので、TODOに回す。
Note) xv6では、 https://github.com/mit-pdos/xv6-public/blob/b818915f793cd20c5d1e24f668534a9d690f3cc8/trapasm.S#L21-L32 に対応する。同様にiret
命令があることがポイント。
int 0x80
つづいてsoftware-generated なinterruptが発生した場合どうなっているのかをみていく。まず、set_system_gate(SYSCALL_VECTOR,&system_call);
がstart_kernel -> trap_initで設定されていることに注意する。
syscall interruptが発生した時、hardware側で権限周りを確認後、registerをstackにpushする。その後、arch/i386/kernel/entry.S
内のENTRY(system_call)
からソフトウェア的な処理がスタートする。
%eax
にはsystem callのnumberを保存しているので、call *sys_call_table(,%eax,4)
が呼ばれ、所要の関数に飛ぶ。この処理が終われば、syscall_exit
ラベル、restore_all
ラベルと進んでいきiret
でUser spaceに引き戻される。
ただし、sys_execve
の場合は、最終的にuser processに飛んだままになるかもしれない(処理が帰ってこない)ので、違う記事にて処理を追いたい。(xv6では、http://cstmize.hatenablog.jp/entry/2019/03/14/GDT%E3%81%A8IDT%E5%91%A8%E8%BE%BA%E3%81%AE%E7%90%86%E8%A7%A3%28xv6%E3%82%92%E4%BE%8B%E3%81%AB%29 の記事の"forkからinitまで"のフローに簡単にまとめている)
syscallの詳細はUnderstanding Linux kernelのch10にあるみたいだが、まだ読んでないので読んだらまとめたい。
exception中のinterruptの挙動
exception中にinterruptが生じるか否かは実はexceptionの種類によって異なる。interrupt gateの場合はeflags.IFがOFFになるので、手動でONにしない限り、interruptは発生しない。しかし、trap gateの場合はeflags.IFフラグはexceptionの前後でそのまま3。(TSSにあるeflagsエントリはch.7.2.1によれば、EFLAGS register field — State of the EFAGS register prior to the task switch.
なので、前の状態を保存するためのもの)
Note) Trap GateとInterrupt Gateの形式的な違いは、Descriptorのtype[43:40]が0b1110だったらinterrupt Gateで、0b1111だったらTrap Gateであるという部分だけである。
divide_error
の場合、したのようにtrap_gate
(type: 0b1111とセットされてる)なので、IFはクリアされない。set_system_gate
もtypeが0x1111なのでIFはクリアされない。
// arch/i386/kernel/traps.c // void __init trap_init(void) set_trap_gate(0,÷_error); set_intr_gate(1,&debug); set_intr_gate(2,&nmi); set_system_intr_gate(3, &int3); 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); // in x86, yes #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); // SYSCALL_VECTOR=0x80
なので、例えば、int $0x80
が発生中にinterruptが生じる場合ことももちろんある。例えば、
[syscall開始 -> [local APIC timerが0に -> interrupt開始 -> 処理.. -> interrupt終了 ] -> syscall処理の続行 -> syscallの終了]
とか。
local APIC timerが0
になるprocessは別プロセスであってもよく、その場合は、interruptを開始するprocessはexceptionが起こったprocessと別になる。
みたいなことが起こりうる。
exception中のschedulingについて
こちらも起こりうる。arch/i386/kernel/entry.S
の中の
ret_from_exception -> resume_userspace -> work_pending ->
check thread_info.flags & _TIF_NEED_RESCHED
-> work_resched -> call schedule()
となり、scheduleはprocess switchのentry pointだから、現在exceptionの処理中にもかかわらず、別のprocessに移動することが有りうる。
_TIF_NEED_RESCHED
がいつ付加されるかは、自分がまだ把握しきっていないので、別の機会に。
Note) ちなみに、上2つの例のように、exceptionやinterruptなどKernel Modeにいるときに別のprocessに移動できる状態のことをpreempt
というみたい。通常はexceptionがinterruptが起こったときは発生前のprocessと同じprocessで作業する(exceptionの場合は、stack pointerを先頭に持ってくるのだったし、別で触れるが、external deviceによるinterruptは別途違うstack pointerに切り替えて作業する。)のだが、上記のようにそうならないパターンがいくつかある4
exceptionの入れ子について
exceptionの最中にexceptionが起こった場合は、Double Fault Exception (#DF)
に分類され、abortとなる。(同じexceptionが重なることはないというところがポイント)
interruptの最中にexceptionは起こるか?
こちらも起こりうるが、これはinterruptについて解説するときにまとめるのが良いだろう。
-
_set_gate(idt_table+n: GDT's index, 15: Trap Gate Descriptor, 0: dpl, divide_error: offset, __KERNEL_CS: segment selector);
となっている。↩ -
厳密に言えば、call命令で呼び出される関数内でeflags.IFがONになることはあるがassembly内では終了処理中はdisableのまま。↩
-
ch.6.12.1.2に
When accessing an exception- or interrupt-handling procedure through an interrupt gate, the processor clears the IF flag to prevent other interrupts from interfering with the current interrupt handler. A subsequent IRET instruction restores the IF flag to its value in the saved contents of the EFLAGS register on the stack. Accessing a handler procedure through a trap gate does not affect the IF flag.
とある。↩ -
Understanding Linux Kernel のch.1の"Reentrant Kernels"の4つの箇条書されたやつがそう。↩