システムコールを呼べる様にするまで
この記事は、自作OS Advent calendar 23 日目です。
今回はsystem callについて書きました。sysenter/sysexitではなくintによる割込みゲートを使った実装です。
自分の自作OSは、IA-32,pentium 4とそれ以降のアーキテクチャ*1を想定しています。
なので今回もこれを前提に書きます。
idtについての簡単な知識があることを前提に書きますが、自分が理解していないところがあるかもしれせん。
記事の内容に関して、不明な点や間違いがあれば指摘してもらえればと思います。
自分のコードは、30日自作OS入門に影響をいろいろと受けていて、idtのエントリの登録もこの本に従っていました。 しかし、xv6やminixの割り込みの設定を見ると、idtに直接handlerを登録しておらず、 この理由として、ハードウェア依存を減らす為だと思っています。*2
基本的にxv6の実装を参考にしているので、その部分の説明をしたいと思います。
xv6の実装は、
vectors.plで生成されるvectors.Sに登録するhandlerがひたすらあって、
これらが、idtに登録されています。
vectors.Sの一部
.globl alltraps .globl vector0 vector0: pushl $0 pushl $0 jmp alltraps .globl vector1 vector1: pushl $0 pushl $1 jmp alltraps . . . .globl vector255 vector255: pushl $0 pushl $255 jmp alltraps # vector table .data .globl vectors vectors: .long vector0 .long vector1 . . . .long vector255
そのhandlerで何をしているかというと、
はじめの2個のpushで、trapframeの一部をstackを使って作っているのですが、
このtrapframeとはx86.hの中にある
struct trapframe { struct trapframe { // registers as pushed by pusha uint edi; uint esi; uint ebp; uint oesp; // useless & ignored uint ebx; uint edx; uint ecx; uint eax; // rest of trap frame ushort gs; ushort padding1; ushort fs; ushort padding2; ushort es; ushort padding3; ushort ds; ushort padding4; uint trapno; // below here defined by x86 hardware uint err; uint eip; ushort cs; ushort padding5; uint eflags; // below here only when crossing rings, such as from user to kernel uint esp; ushort ss; ushort padding6; };
で、vectorの一つ目のpushは
uint err;
の部分で、エラーコードがある例外の割り込みの際にはpushしていません。
例として14番のpage faultの例外はエラーコードが積まれるので
vector14: pushl $14 jmp alltraps
となっています。
uint err;
より下に定義してある、ものはCPUによって自動でスタックに積まれるので*3
ここでは特に触らない。
そして,alltrapsの実装はtrapasm.Sにあり
#include "mmu.h" # vectors.S sends all traps here. .globl alltraps alltraps: # Build trap frame. pushl %ds pushl %es pushl %fs pushl %gs pushal # Set up data and per-cpu segments. movw $(SEG_KDATA<<3), %ax movw %ax, %ds movw %ax, %es movw $(SEG_KCPU<<3), %ax movw %ax, %fs movw %ax, %gs # Call trap(tf), where tf=%esp pushl %esp call trap addl $4, %esp # Return falls through to trapret... .globl trapret trapret: popal popl %gs popl %fs popl %es popl %ds addl $0x8, %esp # trapno and errcode iret
trapframeの続きをスタックに積んでいき、
call trap の前にespをスタックにPushすることで、今まで積んだものをtrapframeとして
trapの引数として渡しています。
そして、実際の割り込みの動作をしているtrap関数です。
void trap(struct trapframe *tf) { if(tf->trapno == T_SYSCALL){ if(proc->killed) exit(); proc->tf = tf; syscall(); if(proc->killed) exit(); return; } switch(tf->trapno){ case T_IRQ0 + IRQ_TIMER: if(cpu->id == 0){ acquire(&tickslock); ticks++; wakeup(&ticks); release(&tickslock); } lapiceoi(); break; case T_IRQ0 + IRQ_IDE: ideintr(); lapiceoi(); break; case T_IRQ0 + IRQ_IDE+1: // Bochs generates spurious IDE1 interrupts. break; case T_IRQ0 + IRQ_KBD: kbdintr(); lapiceoi(); break; case T_IRQ0 + IRQ_COM1: uartintr(); lapiceoi(); break; case T_IRQ0 + 7: case T_IRQ0 + IRQ_SPURIOUS: cprintf("cpu%d: spurious interrupt at %x:%x\n", cpu->id, tf->cs, tf->eip); lapiceoi(); break; //PAGEBREAK: 13 default: if(proc == 0 || (tf->cs&3) == 0){ // In kernel, it must be our mistake. cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n", > tf->trapno, cpu->id, tf->eip, rcr2()); panic("trap"); } // In user space, assume process misbehaved. cprintf("pid %d %s: trap %d err %d on cpu %d " "eip 0x%x addr 0x%x--kill proc\n", proc->pid, proc->name, tf->trapno, tf->err, cpu->id, tf->eip, rcr2()); proc->killed = 1; } // Force process exit if it has been killed and is in user space. // (If it is still executing in the kernel, let it keep running // until it gets to the regular system call return.) if(proc && proc->killed && (tf->cs&3) == DPL_USER) exit(); // Force process to give up CPU on clock tick. // If interrupts were on while locks held, would need to check nlock. if(proc && proc->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER) yield(); // Check if the process has been killed since we yielded if(proc && proc->killed && (tf->cs&3) == DPL_USER) exit(); }
あとは、引数でもらったtrapframeのtrapnoを使ってそれぞれの処理に振り分けてます。
system_call()では中でeaxを使ってどのシステムコールなのかを特定していました。
自分の割込みの基本的な実装は同じなのですが、userとkernelのどちらからの割込みでも同じstackframeを
使っていることが気に入らなかったので、minixを参考に以下の様にしました。
#define IS_INT_IN_KERNEL(displ, label) \ cmpl $2, displ(%esp) ;\ je label
このマクロを使って
.globl system_call system_call: IS_INT_IN_KERNEL(12, system_call_by_kernel) system_call_by_user: #割込みの際に特権が変わった場合の処理(esp, ssが余分にstackに積まれる) system_call_by_kernel: push %eax call system_call_handler iret
今のところこのようにsystem callを実装しました。
実は、現状ユーザープロセスを動かせていないので、特権が変わるような割込みのテストができてないので
問題を抱えているかもしれません。
当初はsysenterだけ実装しようと思っていたのですが、intによるソフトウェア割込みと話が違いすぎたので
別の記事としていつか上げたられたらいいなと思います。
余談
qemuのi386-systemってCR4のpage-size extensionが使えることに驚きました。
参考
IA-32 インテル® アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、下巻: システム・プログラミング・ガイド http://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol3_i.pdf
Operating Systems Design and Implementation