はじめに
xv6-publicとxv6-riscvはUNIX v6を下敷きに、それぞれx86 versionとRISC-V versionに移植された教育用のOSである。
前回、RISC-Vのprivilege modeの遷移について書いたが、本記事では前回よりも少し具体的に、system callがx86 versionとRISC-V versionでどの様に実装されているのかを比較したい。
Reference
[1] https://github.com/mit-pdos/xv6-public/tree/xv6-rev11 x86 versionのxv6
[2] https://github.com/mit-pdos/xv6-riscv/tree/9ead904afef8d060c2cc5cee6bd8e8d223de8c40 RISC-V(RV64; 64bit) versionのxv6
[3] http://cstmize.hatenablog.jp/entry/2019/09/26/RISC-V%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bprivilege_mode%E3%81%AE%E9%81%B7%E7%A7%BB%28xv6-riscv%E3%82%92%E4%BE%8B%E3%81%AB%E3%81%97%E3%81%A6%29 RISC-Vのprivilege modeについて
[4] 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 x86 versionのInterrupt/Exceptionの挙動について
[5] Intel® 64 and IA-32 Architectures Software Developer’s Manual(SDM) vol.3 (Order Number: 325384-067US May 2018) (公式ドキュメント)
[6] https://riscv.org/specifications/privileged-isa/ Volume II: Privileged Architecture(June 8, 2019)
概要
項目\archtecture | x86 | risc-v |
---|---|---|
syscall handlerの登録 | IDT | stvec |
mode遷移 | [ring-3]=>[ring-0] | U-mode=>S-mode |
syscall instruction | int $0x80 |
ecall |
interruptのswitch | EFLAGS.IFはenableのまま | sstatus.SIEは自動でdisableに |
stack | 後述 | 特定のレジスタは個別のCSRに退避される |
modeの帰還 | iret | sret |
syscall handler
xv6(x86)の場合
x86 versionの場合、IDT(Interrupt Descriptor Table) のentryに以下のように定義することでsystem callが有効になる:
// main -> tvinit @ trap.c // istrap=1, segment selector=SEG_KCODE<<3 // offset=vectors[T_SYSCALL](global variable; see vectors.pl), DPL=DPL_USER SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
// mmu.h struct gatedesc { // lower bits uint off_15_0 : 16; // low 16 bits of offset in segment uint cs : 16; // code segment selector // upper bits uint args : 5; // # args, 0 for interrupt/trap gates uint rsv1 : 3; // reserved(should be zero I guess) uint type : 4; // type(STS_{IG32,TG32}) uint s : 1; // must be 0 (system) uint dpl : 2; // descriptor(meaning new) privilege level uint p : 1; // Present uint off_31_16 : 16; // high bits of offset in segment }; #define STS_IG32 0xE // 32-bit Interrupt Gate #define STS_TG32 0xF // 32-bit Trap Gate #define SETGATE(gate, istrap, sel, off, d) \ { \ (gate).off_15_0 = (uint)(off) & 0xffff; \ (gate).cs = (sel); \ (gate).args = 0; \ (gate).rsv1 = 0; \ (gate).type = (istrap) ? STS_TG32 : STS_IG32; \ (gate).s = 0; \ (gate).dpl = (d); \ (gate).p = 1; \ (gate).off_31_16 = (uint)(off) >> 16; \ }
# vectors.pl print "\n# vector table\n"; print ".data\n"; print ".globl vectors\n"; print "vectors:\n"; for(my $i = 0; $i < 256; $i++){ print " .long vector$i\n"; }
DPL(IDT[14:13]; DPL_USER=3)、Segment Selector(GDTの先頭アドレスからのoffset; SEG_KCODE<<3でDPL=0)は権限チェックのために用いられる。詳しくはReference[4]の"Exception発生直後のhardware(x86)の対応"を参照のこと。
権限チェックに問題がなかった場合、system callが呼ばれるが、vectors.plに定義されているvector128
(vectors[T_SYSCALL]にちょうど対応している)がhandlerとしてまず呼び出される。vector128
は同じvector.plで以下のように定義されている:
for(my $i = 0; $i < 256; $i++){ print ".globl vector$i\n"; print "vector$i:\n"; if(!($i == 8 || ($i >= 10 && $i <= 14) || $i == 17)){ # push error code print " pushl \$0\n"; } # each entry point is different because the x86 doesn't provide the trap number to the interrupt handler print " pushl \$$i\n"; print " jmp alltraps\n"; }
vector128の部分だけ抽出すると、以下のようになる:
.globl vector0 vector128: pushl $0 pushl $128 jmp alltraps
最後にpushl $128
(128=0x80)としているのは、x86では、どの番号のint
にてexceptionが発生したのかが分からないので1、stackにint
の番号をpushしている。
Note) IDTR(IDT Register)にIDTのbase address(physical address)を格納する必要がある:
// main -> idtinit @ trap.c void idtinit(void) { // IDTR are loaded with a linear base address lidt(idt, sizeof(idt)); }
xv6-riscvの場合
xv6-riscvの場合、stvecというCSRにexception/interrupt handlerを設定するだけでよい:
xv6-riscvではmode=0とすることで、IDTというvector tableを作成する必要がなくなった。この場合、単一のentry pointを置けばよい。ただし、mode=1とすることでIDTのようにvector tableの先頭アドレスでの管理も可能である。
どこでstvecを設定しているかは少し難しいが、
U-mode => S-modeに遷移する場合: w_stvec(TRAMPOLINE + (uservec - trampoline))と設定; uservec関数に飛ぶ。
S-mode中にinterruptを許す場合 w_stvec(kernelvec)と設定。 kernelvec関数に飛ぶ
となる。(x86のIDTとは違って、状況に応じてstvecの中身を変更する)
Note) x86では0x80をstackにsaveする必要があったが、RISC-Vでは、scause CSRというregisterによって何が原因でexception/interruptが呼び出されたのかがわかる様になっている:
ecall実行後のS-modeの遷移にてscauseの値によって個々の関数にdispatchする事ができる(ecall後のtrap handlerでは確かにinterrupt:0, code:8に対応するコードがsyscall()
となっている)
Note) x86では、結局は全てのinterrupt handlerがalltraps
関数に集約されるので、メモリ上無駄である(がIDTというhardware機能のため、このようにせざるを得ない)。対して、RISC-Vでは単一のentrypointがtrap handlerになっており、メモリ上のムダが省けるという利点があると思う。
system callが生じたときのmode遷移
xv6(x86)の場合
x86では、system call(int 命令)が生じると、IDT(interrupt Descriptor Table)のentry 0x80番の中身を見て、権限チェックの後、ring-0(Kernel mode)に移る。詳しくは、Reference[4]の"Exception発生直後のhardware(x86)の対応"を確認のこと。
xv6-riscvの場合
RISC-Vでは、3種類のモード(privilege順にM-mode(machine mode) > S-mode(Supervisor-mode(kernel modeのこと)) > U-mode(User mode)が存在する。詳細はReference [3] medeleg/mideleg
の節を見てほしいが、RISV-Vではデフォルトで全てのexception/interruptがU-mode -> M-modeに遷移する様になっているが、boot時に以下の設定をすることで、M-modeでなく、S-modeにdelegate(移譲する)形でtrapを発動させることができる:
// start() @ start.c // delegate all exceptions(0-15) to S-mode w_medeleg(0xffff); // asm volatile("csrw medeleg, %0" : : "r" (x)); // delegate all interrupt(0-15) to S-mode w_mideleg(0xffff); // asm volatile("csrw mideleg, %0" : : "r" (x));
Reference[3]の"medeleg/mideleg"に触れたように、medeleg(Machine-mode delegate) CSRのbit8をONにすることで、ecall
がU-modeからS-modeにモード遷移するようになる。
syscall instruction
xv6(x86)の場合
usys.Sに以下のように定義されている:
#define SYSCALL(name) \ .globl name; \ name: \ movl $SYS_ ## name, %eax; \ int $T_SYSCALL; \ ret SYSCALL(fork) // SYSCALL(...)
T_SYSCALL=0x80
だから、x86の場合はint $0x80
にてsystem callを発行できる。interrupt handlerの節で触れるように、IDTIDT(Interrupt Descriptor Table; 配列)のentryに所定の設定をすれば良く、その場所がたまたま0x80番としていしただけなので、0x80
である必然性はない。
xv6-riscvの場合
user/usys.plに以下のように定義されている:
sub entry { my $name = shift; print ".global $name\n"; print "${name}:\n"; print " li a7, SYS_${name}\n"; print " ecall\n"; print " ret\n"; } entry("fork"); # entry("xxx")
これは、Makefileにてusys.Sに出力される:
# Makefile $U/usys.S : $U/usys.pl perl $U/usys.pl > $U/usys.S
RISC-Vでは、ecall instructionを用いることで、system callを発行する。
interruptのswitch
xv6(x86)の場合
Reference[5]のch.6.8.1とch.6.12.1.2より、
When an interrupt is handled through an interrupt gate, the IF flag is automatically cleared, which disables maskable hardware interrupts. (If an interrupt is handled through a trap gate, the IF flag is not cleared.)
--
The only difference between an interrupt gate and a trap gate is the way the processor handles the IF flag in the EFLAGS register. 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.
と書いてあり、int $0x80
の場合はtrap gateなので、IF=1のままである。
xv6-riscvの場合
Reference[3]のU-mode => S-modeの部分にも詳しく書いたが、ecallが生じたとき、自動的にsstatus.SIE(Software Interrupt Enable)がOFFになる。これがOFFになることで、S-mode => S-mode(horizontal traps)へのinterruptによるmode遷移がdisableになる[^vertial_traps]。
このtrap時のhardwareの振る舞いに関しては、Reference[6]のch.3.1.8に
When a trap is delegated to a less-privileged mode x, the x cause register is written with the trap cause; the x epc register is written with the virtual address of the instruction that took the trap; the x tval register is written with an exception-specific datum; the xPP field of mstatus is written with the active privilege mode at the time of the trap; the xPIE field of mstatus is written with the value of the x IE field at the time of the trap; and the x IE field of mstatus is cleared. The mcause and mepc registers and the MPP and MPIE fields of mstatus are not written.
とあるので、これを参考にした2。
ただし、system callの場合は、software側で一時的に
https://github.com/mit-pdos/xv6-riscv/blob/9ead904afef8d060c2cc5cee6bd8e8d223de8c40/kernel/trap.c#L65 .. interrupt ON
https://github.com/mit-pdos/xv6-riscv/blob/9ead904afef8d060c2cc5cee6bd8e8d223de8c40/kernel/trap.c#L96 .. interruptを再度OFF
としている。
stack
xv6(x86)の場合
だいたいのregisterがstackに退避される。
int $0x80
が起こるとhardware側で、以下のようにstackにint $0x80
直前のregisterの中身が保存される:
Note) Error codeはint $0x80
の場合はstackに積まれない。
stackの位置自体はcontext switch(swtch関数)の直前のswitchuvm(p)
関数にて以下のようにts.ss0
(0はring-0の0)とts.esp0
を次のように指定することで実現できる:
// switchuvm(p: struct proc) mycpu()->gdt[SEG_TSS] = SEG16(STS_T32A, &mycpu()->ts, sizeof(mycpu()->ts)-1, 0); mycpu()->gdt[SEG_TSS].s = 0; // 0 = system mycpu()->ts.ss0 = SEG_KDATA << 3; mycpu()->ts.esp0 = (uint)p->kstack + KSTACKSIZE; // reload TR
その後、vector128で
vector128: pushl $0 // error code pushl $128 // trapno(0x80) jmp alltraps
とスタックを積んで、alltraps
("syscall handler"の節を参照)にて、
// alltraps @ trapasm.S pushl %ds pushl %es pushl %fs pushl %gs pushal
で更に、d/s/f/g segment registerとGeneral-Purpose Registersをpushすることで、struct trapframe
を形成する。
この後、trap関数に飛んで、interrupt handlerの大元の処理を行う:
// alltraps @ trapasm.S # Set up data segments. movw $(SEG_KDATA<<3), %ax movw %ax, %ds movw %ax, %es # Call trap(tf), where tf=%esp pushl %esp call trap // void trap(struct trapframe *tf)
Note) 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; // push by alltraps ushort gs; ushort padding1; ushort fs; ushort padding2; ushort es; ushort padding3; ushort ds; ushort padding4; // vector128 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; };
一番下からuint eip;
メンバまでがx86によって自動的にstackにpushされ、それから上は、vector128, alltrapsによってstackにpushしたメンバである。
xv6-riscvの場合
xv6-riscvの場合、U-modeでのregisterがCSRに(hardwaretの意味で)自動的に退避されるものと、sscratch
によってスタック上に管理されるものの2種類に分かれる。x86と比較すると次のようになる:
x86 | RISC-V(遷移先がS-mode) | remarks |
---|---|---|
%ss(stack) | - | RISC-Vではsegmentationはない |
%esp(stack) | x | RISC-Vの場合、手動でsscratchにて管理する |
%eflags.IF(stack) | sstatus.SPIE | Machine/Supervisor Privious Interrupt Enable |
%cs(stack) | sstatus.SPP | Machine/Supervisor Privious Privilege |
%eip(stack) | sepc | Machine/Supervisor Exception Program Counter |
error code(stack) | scause | Machine/Supervisor Cause Register |
trapno(stack) | scauseにて確認する | |
ds, es, fs, gs(stack) | - | RISC-Vではsegmentationはない |
integer and floating-point registers(General-Purpose Registers)(stack) | stackに管理し、先頭アドレスをsscratchにsave/restoreする |
図の通り、x86で自動的にhardwareが積まれるものは、RISC-Vでは大体CSR(register)に退避される。具体的には上表の一番上から7つ。
最後のinteger and floating-point registers
は
https://github.com/mit-pdos/xv6-riscv/blob/riscv/kernel/trampoline.S#L29https://github.com/mit-pdos/xv6-riscv/blob/036b5edf12fccc06a11287ad58565f253ab2eef6/kernel/trampoline.S#L29 にてuser modeでのa0を保存、TRAPFRAME(=
MAXVA-2*PGSIZE
)のaddress(stackの先頭アドレス)を復元し、https://github.com/mit-pdos/xv6-riscv/blob/036b5edf12fccc06a11287ad58565f253ab2eef6/kernel/trampoline.S#L137 にてuser modeでのa0を復元、TRAPFRAMEのaddress(stackの先頭アドレス)を保存する。
TRAPFRAMEは https://github.com/mit-pdos/xv6-riscv/blob/036b5edf12fccc06a11287ad58565f253ab2eef6/kernel/trap.c#L127 の第一引数(=a0)が元になってる
ret
xv6(x86)の場合
x86の場合はiretにてkernel modeからuser modeに戻る3。
.globl trapret trapret: popal popl %gs popl %fs popl %es popl %ds addl $0x8, %esp # trapno and errcode iret
個々でやっているのは、stackの節にて構成されたtrapframe構造体を引き上げる。
iretの直前は
のようになっている(一番下が現在の%esp)ので、iretにてmode遷移とともにこれらのレジスタを復元する。
xv6-riscvの場合
RISC-Vの場合はsretを用いてS-mode(Supervisor mode; kernel mode) => U-mode(User mode)に戻る。
以前の記事のU-mode -> S-mode
の節でも触れたように、
- sret実行
- sstatus.SIE <- sstatus.SPIE(=1)
- U-modeに遷移する4
- sstatus.SPIE <~ 1 [always]
- sstatus.SPP <~ 00(U-mode) [always]
- pc(program counter) <~ sepc CSR5
にて無事U-modeに戻ることができる。
感想
比較した感想としては、x86ではuser mode->kernel modeに昇格する際に権限のチェックを行うので、それがかなりややこしい(今回はチェックは省略したが)。対してRISC-Vでは、U-modeで生じたtrapは、sie registerの各ビットをONにしている限りS-modeに遷移するので、権限チェックの煩雑さがなくなり嬉しい。
登場するresiger自体もx86に比べてRISC-Vでは減っている(例えば、x86でのsegmentationに関係するregisterやGDT, LDTはRISC-Vでは登場しない概念)ので、少しシンプルな設計になっている。
trapの仕組み自体は両者で一見かなり違うように見えるが、hardwareがtrap直後に自動でやる処理はx86でもRISC-Vでも存在しているし、退避させるreigsterの方法もstackかCSRかという違いだけでやりたいことは同じだと思うので、ただ単に細部の差異だけのように見える。
-
ソースコードコメントに"x86 doesn’t provide the trap number to the interrupt handler"と書いてある。↩
-
mstatusとsstatusの同名のbitは同一視できる。これはReference[3]の"各モードのCSRのbitの解釈について"という節で詳しく書いたので参考にされたい。↩
-
ただし、本来の
iret
の意味合いはmodeの遷移とは厳密に異なる。これに関しては、http://jamesmolloy.co.uk/tutorial_html/10.-User%20Mode.html が参考になると思う。↩ -
trap handler(ecall)発生直後、sstatus.SPP(S-mode Privious Privilege) <~ U-mode(00)に設定されてるため。↩
-
trap handler(ecall)発生直後、sepcにはtrap発生直前のprogram counterが入る。ecallならば、ecall instructionの直前のaddress.↩