Linuxのpriviledge levelを切り替える辺りのコードを見てみる。
Linux 2.6.18
fs/binfmt_elf.c 1017行目あたり
start_thread(regs, elf_entry, bprm->p); if (unlikely(current->ptrace & PT_PTRACED)) { if (current->ptrace & PT_TRACE_EXEC) ptrace_notify ((PTRACE_EVENT_EXEC << 8) | SIGTRAP); else send_sig(SIGTRAP, current, 0); } retval = 0;
include/asm-i386/processor.h
#define start_thread(regs, new_eip, new_esp) do { \ __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \ set_fs(USER_DS); \ regs->xds = __USER_DS; \ regs->xes = __USER_DS; \ regs->xss = __USER_DS; \ regs->xcs = __USER_CS; \ regs->eip = new_eip; \ regs->esp = new_esp; \ } while (0)
これはexecveシステムコール時にelf形式の実行ファイルを読み込むときに呼ばれる関数で、ここでstart_threadにより各セグメントレジスタにユーザプロセス用のレジスタ(__USER_CS = 0x23, __USER_DS = 0x2B)をセットしており、以後このプロセスはこのセグメント(Priviledge Level)で動作する。
以下のコードは実際にスケジューリングされている各プロセスが割り込みハンドラによりスケジューラが呼ばれ、そのスケジューラ処理が終了した時のコードで、iretする前にユーザプロセス用のcs, eip, eflags, ds, ssなどがセットされ割り込みハンドラを呼出す側には戻らずにスケジューリングされたプロセスの(cs, eip)に戻るように加工されている。
arch/i386/kernel/entry.S
ENTRY(interrupt) .text vector=0 ENTRY(irq_entries_start) RING0_INT_FRAME .rept NR_IRQS ALIGN .if vector CFI_ADJUST_CFA_OFFSET -4 .endif 1: pushl $~(vector) CFI_ADJUST_CFA_OFFSET 4 jmp common_interrupt .data .long 1b .text vector=vector+1 .endr /* * the CPU automatically disables interrupts when executing an IRQ vector, * so IRQ-flags tracing has to follow that: */ ALIGN common_interrupt: SAVE_ALL TRACE_IRQS_OFF movl %esp,%eax call do_IRQ jmp ret_from_intr CFI_ENDPROC
do_IRQで各割り込みハンドラを呼び出している。(この中にはスケジュール処理やシステムコールによる処理も含まれる)
そのハンドラ処理が終わるとret_from_intrへjmp。
/* * Return to user mode is not as complex as all this looks, * but we want the default path for a system call return to * go as quickly as possible which is why some of this is * less clear than it otherwise should be. */ # userspace resumption stub bypassing syscall exit tracing ALIGN RING0_PTREGS_FRAME ret_from_exception: preempt_stop ret_from_intr: GET_THREAD_INFO(%ebp) check_userspace: movl EFLAGS(%esp), %eax # mix EFLAGS and CS movb CS(%esp), %al testl $(VM_MASK | 3), %eax jz resume_kernel ENTRY(resume_userspace) cli # make sure we don't miss an interrupt # setting need_resched or sigpending # between sampling and the iret movl TI_flags(%ebp), %ecx andl $_TIF_WORK_MASK, %ecx # is there any work to be done on # int/exception return? jne work_pending jmp restore_all
restore_all: movl EFLAGS(%esp), %eax # mix EFLAGS, SS and CS # Warning: OLDSS(%esp) contains the wrong/random values if we # are returning to the kernel. # See comments in process.c:copy_thread() for details. movb OLDSS(%esp), %ah movb CS(%esp), %al andl $(VM_MASK | (4 << 8) | 3), %eax cmpl $((4 << 8) | 3), %eax CFI_REMEMBER_STATE je ldt_ss # returning to user-space with LDT SS restore_nocheck: TRACE_IRQS_IRET restore_nocheck_notrace: RESTORE_REGS addl $4, %esp CFI_ADJUST_CFA_OFFSET -4 1: iret
この時点でスタックの内容は書き換わっており、その値をチェックしているだけのようです。
最後にiretをし、仕込まれたss, esp, cs, eip, eflagsがセットされ、指定したプロセスのeip部分から実行が再開される。
※CPLが変化するiret時はcs, eip, eflagsだけでなく、ss, espも復帰される。(CPUが自動で行う)
自作OS界隈ですと、Mona OSは大体これと同じ処理をしており、AntosはTSSを使っているようでした。
sodexでは、プロセスの切り替えに現時点ではCPLの変化が発生しないのでjmpのみで行ってます。csを変える方法にはセグメント間ジャンプ、割り込み復帰(iret)、コールゲートを使う方法があり、最初はコールゲートを使ってみようかと思いましたが、コールゲートでcsを変えると、eipを変える箇所が別々になり、カーネルコード中にUSER_CSで動作させる箇所が出てきてしまい、整合性が取れなくなる可能性が考えられるのでLinuxと同じ方式でやってみようと思います。