はじめに
OpenSBIとはRISC-V向けに提供されたSBI(Supervisor Binary Interface)仕様の実装で、M-modeでの挙動が実装されている(linux kernelはS-modeとU-modeでの挙動が実装されている)。前回の記事でlinux kernel(v5.3.6)の下でOpenSBIを導入した手順を書いたが、今回はOpenSBIの内部実装(version: v0.5とする)がどの様になっているのかを追っていきたい。本記事では、OpenSBIのboot~mret~linux kernelを実行するまでをまとめた。次の記事では、trap handlerの内部実装を覗くつもりだ。
なお、xv6というMIT発の教育用OSでもrisc-v実装が公開されている。こちらは、self-containedな実装で、(当然)linux kernelとその周辺のコンポーネント実装よりも簡素となっているので、OSの内部実装全体を俯瞰しやすい。また、これについては、Reference[3]、Reference[4]にて記事にしているので参照されたい。
Reference
[1] http://cstmize.hatenablog.jp/entry/2019/10/14/_QEMU%2BOpenSBI%28boot_loader%29%E3%81%A7linux_kernel%E3%81%AE%E8%B5%B7%E5%8B%95 OpenSBI+Linux Kernel(v5.3.6)の環境構築
[2] https://riscv.org/specifications/privileged-isa/ The RISC-V Instruction Set Manual(Volume 2)
[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 および、http://cstmize.hatenablog.jp/entry/2019/10/01/RISC-V%E3%81%A8x86%E3%81%AEsystem_call%E3%81%AE%E5%86%85%E9%83%A8%E5%AE%9F%E8%A3%85%E3%81%AE%E9%81%95%E3%81%84%28xv6%E3%82%92%E4%BE%8B%E3%81%AB%29 RISC-Vのprivilegeに関する記事2つ
[5] https://github.com/riscv/opensbi/blob/v0.5/docs/platform/qemu_virt.md "QEMU RISC-V Virt Machine Platform"におけるbuild手順
[6] 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の遷移"。前半にRISC-V architecture(privileged)の基本事項をまとめている
[7] https://static.dev.sifive.com/U54-MC-RVCoreIP.pdf SIFIVE社のRISC-V multiprocessors 仕様書(https://github.com/torvalds/linux/blob/v4.19/drivers/irqchip/irq-sifive-plic.c#L23から)
build処理
前回の記事の環境構築では、以下のコマンドにてOpenSBIをbuildした1:
git clone -b v0.5 https://github.com/riscv/opensbi.git cd opensbi # in opensbi directory ## build make CROSS_COMPILE=riscv64-unknown-elf- PLATFORM=qemu/virt FW_PAYLOAD_PATH=../linux-stable/arch/riscv/boot/Image ## run qemu-system-riscv64 -M virt -m 256M -nographic -kernel build/platform/qemu/virt/firmware/fw_payload.elf -drive file=../rootfs.img,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 -append "root=/dev/vda rw console=ttyS0
../linux-stable/arch/riscv/boot/Image
(linux_build_directory)と../rootfs.img
(path_to_linux_rootfs)の作成は前回で行ったので、ここではbuild/platform/qemu/virt/firmware/fw_payload.elf
(ELF file)がどの様に作成されているかをmake
のコマンド出力(https://gist.github.com/knknkn1162/dab88ab196a1b915469a9e3c03e536a0)をもとに追う。だいたい以下のようになっている:
- オブジェクトファイル群をまとめて
libplatsbi.a
を作成 - fw_payload.S -> fw_payload.oにassemble。このとき、
.incbin FW_PAYLOAD_PATH
として、linux kernelのimage(raw file)も取り込まれる。 - fw_payload.o + libplatsbi.a -> fw_payload.elf(ELF file)を作成(
ld
)。linker scriptはfw_payload.elf.ldS。
2.のFW_PAYLOAD_PATH
はMakefileでの処理にてgccコマンドのオプションとして、-DFW_PAYLOAD_PATH
のかたちで定義されている。-D***
はマクロ***
を定義する(define)ことを意味する。.incbin FW_PAYLOAD_PATH
の定義元であるpayload_bin
関数については後で言及する(mepc CSRがpayload_bin関数と指定されている、つまり、遷移先mode(S/U-mode)の入り口がpayload_binであり、linux kernelのentrypointに移動するということです)。
3のfw_payload.elf.ldS
の中身で、#include "fw_base.ldS"
となっており、ほぼほぼfw_base.ldS
が実体である。
また、ENTRY(_start)
とあるのでentrypointはここ。
Note) やや細かいが、2.のFW_PAYLOAD_PATH=../linux-stable/arch/riscv/boot/Image
がraw binary file(not ELF file)であることを確認しておく。これは、readelf
コマンドでもreadelf: Error: Not an ELF file
でELF fileでないことがチェックできるし、arch/riscv/boot/Makefile
の以下のコードと OBJCOPYFLAGSに関する記述(https://github.com/torvalds/linux/blame/v5.3/Documentation/kbuild/makefiles.rst#L888-L906)をみてもgenerate raw binaries on vmlinux
と書いてあるので、raw fileで有ることが確認できる。
// @ arch/riscv/Makefile OBJCOPYFLAGS := -O binary // Copy binary. Uses OBJCOPYFLAGS usually specified in arch/$(ARCH)/Makefile // @ arch/riscv/boot/Makefile // OBJCOPYFLAGS_$@ may be used to set additional options. OBJCOPYFLAGS_Image :=-O binary -R .note -R .note.gnu.build-id -R .comment -S targets := Image $(obj)/Image: vmlinux FORCE $(call if_changed,objcopy) # if_changed, exec objcopy
なお、OBJCOPYFLAGS_**
についてもここに記述がある。
OpenSBIの内部実装
前章では主にMakefileの構造を探ることで、各種ファイル/パラメタが何を意味しているのかを説明した。この章では、entrypoint(fw_payload.elf.ldS
のENTRY(_start)
からfw_base.Sの_start関数だった)から順に処理を追っていく。OpenSBIはM-modeにおける実装であることに留意する。
見るべきポイントは3つあると思う。:
entrypoint(
_start
)からmret
でS-modeに遷移するまでの流れbootが終わると、S-mode<->U-modeを行き来するが、どのtrapがトリガーとなり、M-modeに遷移するのか
trap handlerが終了した後、どうなるのか?
上2つはboot時に設定されるのでboot時の流れを見れば良い。最後の1つは実際にtrapが起こりM-modeに遷移したことを想定してコードリーディングしていけば良さそうだ。trap handlerについては次の記事に回したい(この記事が長くなりすぎたため)。
Note) RISC-V architecture(特にprivilege周り)の基本的な仕様については本記事では触れないので、Reference[6]の準備と概要をざっと読んでください。
boot時の流れ(entrypointからmret
まで)
fw_base.S
まず、_start
関数のあるfw_base.Sから。hart_id(cpuidみたいなもの)2が0のやつがmemory上の構成を行い(cold_start)、1以上のやつがそれを待つ様になっている(warm_start)。cold_startでは、mret
で飛ぶ際の仕掛けを作っているのでかなり大事な部分である。
お互い_boot_status
という変数を介してsyncした後、全てのhartが_start_warm
に合流し、最終的にcall sbi_init
を呼び出す。この関数は__noreturn
=__attribute__((noreturn))
がついている3ので戻ってこない。
warm_startの処理とcold_startの処理をかんたんに見ていく。両者の処理がfw_base.S
内に混じっていてやや読みにくいので、こちらの方で見やすいように分離したコードを挙げている。
Note) fw_base.Sは2者のアドレス比較を頻繁に行うので、linker scriptに不慣れなら、linker mapを作成するとよい(ld
なら-M
option, gcc
なら、-Wl,-M
optionで作成される。Makefileをいじればよい)。せっかくなのでhttps://gist.github.com/knknkn1162/f62cca53305911ddb1d844678cd6bbf3 にて作成してみた。
hart_id >= 1の場合
hart_id>=1のhartがやることは_boot_status=BOOT_STATUS_BOOT_HART_DONE=2
となるまでhart_id=0の処理を待ち(同期する)、_start_warm
関数を実行し、sbi_init
を実行しているだけである:
.align 3 .section .entry, "ax", %progbits .globl _start .globl _start_warm _start: csrr a6, CSR_MHARTID // assume that `hart_id != 0` // if hart_id>=1, prepare for warm-boot blt zero, a6, _wait_relocate_copy_done // skip _wait_relocate_copy_done: la t0, _start la t1, _link_start // = _fw_start REG_L t1, 0(t1) // In this case, _start = _link_start. In fact, DFW_TEXT_START=0x8000_0000 in qemu/virt, so already meets the condition, `. = ALIGN (0x1000)`. beq t0, t1, _wait_for_boot_hart // yes and jump // skip _wait_for_boot_hart: // #define BOOT_STATUS_BOOT_HART_DONE 2 li t0, BOOT_STATUS_BOOT_HART_DONE la t1, _boot_status // parameter for sync REG_L t1, 0(t1) /* Reduce the bus traffic so that boot hart may proceed faster */ nop nop nop bne t0, t1, _wait_for_boot_hart // sync with cpu with hart_id=0 _start_warm: // skip call sbi_init // sbi_init(struct sbi_scratch *scratch)
ここで、beq t0, t1, _wait_for_boot_hart
を考えよう。beq
はt0 -eq t1
のときに_wait_for_boot_hart
にjumpすることを意味する。t0
は_start
のaddress, t1
は_link_start
のアドレスである。
_start
と_link_start=_fw_start
が同じアドレスになるのはplatform/qemu/virt/config.mk
にてFW_TEXT_START=0x80000000
としているのと、firmware/fw_base.ldS
にて以下のようなlinker scriptの記述になっているからである:
/* FW_TEXT_START=0x80000000 in virt*/ . = FW_TEXT_START; PROVIDE(_fw_start = .); . = ALIGN(0x1000); /* Need this to create proper sections */ /* Beginning of the code section */ .text : { PROVIDE(_text_start = .); *(.entry) // _start *(.text) . = ALIGN(8); PROVIDE(_text_end = .); } // skip
0x80000000はすでにALIGN(0x1000)
を満たしているので、_text_start
=_fw_start
である。_startは.entry
sectionに配置されるから、_fw_start
=_start
となる。
hart_id=0の場合
やや長いので、全体はhttps://gist.github.com/knknkn1162/016b8989da16e2151bedde0fa5fff252 においた。
hart_id=0の場合、大きく分けて2つのことをやっている:
struct sbi_scratch
を構成- FDT(flattened Device Tree)の構成
2.は今回扱うQEMU RISC-V Virt Machine Platform
ではスキップされるので飛ばす(FW_PAYLOAD_FDT_PATH
がセットされていないので、beqz a1, _fdt_reloc_done
のa1=0
となり、真になるため)。なので、実質やっていることは1のみだ。
1で作ったstruct sbi_scratch
はmscratch CSRにセットされる。また、_start_warm
関数のsbi_init
の引数(struct sbi_scratch *scratch
)にてこの構造体(のポインタ)がそのまま渡される。
struct sbi_scratch
を構成する処理は以下の部分。主要なポイントだけおさえる:
1: la a4, platform
のplatform
はconst struct sbi_platform platform
のこと:
const struct sbi_platform platform = { .opensbi_version = OPENSBI_VERSION, .platform_version = SBI_PLATFORM_VERSION(0x0, 0x01), .name = "QEMU Virt Machine", .features = SBI_PLATFORM_DEFAULT_FEATURES, .hart_count = VIRT_HART_COUNT, .hart_stack_size = VIRT_HART_STACK_SIZE, .disabled_hart_mask = 0, .platform_ops_addr = (unsigned long)&platform_ops };
2: hart_id = kにセットする。初期値はk=0
。hartは最大8つまで設定する用になっており、2-5までがループで回る。
3: hart_id=kのtp
(thread pointer)を下図の網掛けの部分の下部にセットする。ここから上の領域にはstruct sbi_scratch
のメンバが入ることになる。
4: struct sbi_scratch
を以下のように埋める:
struct sbi_scratch { /** Start (or base) address of firmware linked to OpenSBI library */ unsigned long fw_start; // _fw_start function /** Size (in bytes) of firmware linked to OpenSBI library */ unsigned long fw_size; // (_fw_end+stacksize)-_fw_end(=size of used region) /** Arg1 (or 'a1' register) of next booting stage for this HART */ unsigned long next_arg1; // fw_next_arg1 /** Address of next booting stage for this HART */ unsigned long next_addr; // payload_bin function address /** Priviledge mode of next booting stage for this HART */ unsigned long next_mode; // PRV_S(S-mode) /** Warm boot entry point address for this HART */ unsigned long warmboot_addr; // _start_warm function address /** Address of sbi_platform */ unsigned long platform_addr; // (const struct sbi_platform)platform /** Address of HART ID to sbi_scratch conversion function */ // need for send_ipi unsigned long hartid_to_scratch; // _hartid_to_scratch function address /** Temporary storage */ unsigned long tmp0; // zero; /** Options for OpenSBI library */ unsigned long options; // zero. don't care } __packed;
5: hart_idをincrementして、hart_id>=8なら終了、それ以外なら2~を繰り返す
特に4のnext_addr, next_arg1, next_mode(=S-mode)メンバが重要で、最終的にmepcにnext_addrメンバを指定することでS-modeに遷移したとき、next_addrに着地する。
next_addrはpayload_bin
が指定されているが、この関数はbuild処理にも登場した.incbin FW_PAYLOAD_PATH
(where FW_PAYLOAD_PATH=../linux-stable/arch/riscv/boot/Image
)であった。つまり、M-mode -> S-modeに移るやいなや、linux kernelのentry pointにjumpすることになる。
// firmware/fw_payload.S payload_bin: #ifndef FW_PAYLOAD_PATH wfi j payload_bin #else .incbin FW_PAYLOAD_PATH #endif
_start_warm
残りは_start_warm
関数だが、これは各種CSRを初期化している:
_start_warm: /* Reset all registers for non-boot HARTs */ li ra, 0 call _reset_regs /* Disable and clear all interrupts */ csrw CSR_MIE, zero csrw CSR_MIP, zero la a4, platform lwu s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4) lwu s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4) /* HART ID should be within expected limit */ csrr s6, CSR_MHARTID bge s6, s7, _start_hang /* find the scratch space for this hart */ la tp, _fw_end // PROVIDE(_fw_end = .); mul a5, s7, s8 add tp, tp, a5 mul a5, s8, s6 // s6 sub tp, tp, a5 li a5, SBI_SCRATCH_SIZE sub tp, tp, a5 /* update the mscratch */ csrw CSR_MSCRATCH, tp /* Setup stack */ // platform add sp, tp, zero /* Setup trap handler */ la a4, _trap_handler csrw CSR_MTVEC, a4 /* Make sure that mtvec is updated */ 1: csrr a5, CSR_MTVEC bne a4, a5, 1b /* Initialize SBI runtime */ csrr a0, CSR_MSCRATCH // set arg0 call sbi_init // sbi_init(struct sbi_scratch *scratch)
stack pointerをstruct sbi_scratch
のすぐ下(memory mapの網掛けの下部)にセットしてる(x86と同様にstack pointerはgrow downする)ことと、mtvec=_trap_handler
としているので、kernel起動後のtrapにてM-modeに遷移する場合は_trap_handler
がentrypointになることに注意すればいいかな。
注意) fw_base.S
の中身については説明したが、OpenSBIのentry pointのaddressが0x8000_0000で指定されているのは違和感がある。もし、物理メモリが2GB(=0x8000_000)以下の場合、そもそもentry pointに飛べないと言う事態が起こるのではないか?
これは、qemuの方で、0x8000_0000をDRAMの開始地点に設定しているからのようだ。これについては、https://qiita.com/tomoyuki-nakabayashi/items/76f912adb6b7da6030c7#bootloader を参考にした。qemuについては、12月頃にどのような実装になっているかAdvent Calenderに合わせて記事にする予定。
sbi_init関数~
fw_base.Sの解説が長かったが、ここからはC言語で書かれているので、読みやすくなっている:
void __noreturn sbi_init(struct sbi_scratch *scratch) { bool coldboot = FALSE; u32 hartid = sbi_current_hartid(); const struct sbi_platform *plat = sbi_platform_ptr(scratch); if (sbi_platform_hart_disabled(plat, hartid)) sbi_hart_hang(); if (atomic_add_return(&coldboot_lottery, 1) == 1) coldboot = TRUE; if (coldboot) init_coldboot(scratch, hartid); else init_warmboot(scratch, hartid); }
どれか一つの(最初に到着した)hartをcoldと選出され、その他のhartをwarmとする。cold処理では、global変数を初期化したり、memory mapped I/Oの初期化をしたりしている(色々やっているが、煩瑣になるので必要なとき以外は飛ばす)。
coldの処理の終盤でwarmの担当のhartとIPI(InterProcess Interrupt)によって同期される。同期が取れた後は、全てのhartがsbi_hart_switch_mode
関数を実行し、mretによって、M-mode ~ S-modeに遷移する:
init_coldboot: assume that atomic_add_return(&coldboot_lottery, 1) == 1 - inititlize global variable: such as `trap_info_offset`, `ipi_data_off` or `time_delta_off` - initialize registers and device systems - sbi_ipi_init(scratch, TRUE): initialize IPI part in CLINT - sbi_hart_wake_coldboot_harts(scratch, hartid): wake warm_id: 1~7 - sbi_platform_ipi_send(plat, target_hart=i): for all other harts - sbi_platform_ops(plat)->ipi_send(target_hart): clint_ipi_send - writel(1, &clint_ipi[target_hart]); - sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr, scratch->next_mode, FALSE) - configure and initialize CSRs - __asm__ __volatile__("mret" : : "r"(a0), "r"(a1)): payload_bin - .incbin FW_PAYLOAD_PATH: ../linux-stable/arch/riscv/boot/Image
init_warmboot: assume that atomic_add_return(&coldboot_lottery, 1) > 1 - sbi_hart_wait_for_coldboot(scratch, hartid); - csr_set(CSR_MIE, MIP_MSIP): Machine Software Interrupt Pending; to permit to receive IPI - wfi(): wait until sending IPI from coldboot hart - sbi_platform_ipi_clear(plat, hartid); - sbi_platform_ops(plat)->ipi_clear(target_hart): clint_ipi_clear - writel(0, &clint_ipi[target_hart]) - sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr, scratch->next_mode, false); - configure and initialize CSRs - __asm__ __volatile__("mret" : : "r"(a0), "r"(a1)); - .incbin FW_PAYLOAD_PATH: ../linux-stable/arch/riscv/boot/Image
IPI処理の中心となる関数が、ipi_clearとipi_send関数ポインタであるが、QEMU RISC-V Virt Machine Platform
の場合、clint_ipi_clear/clint_ipi_send関数4が実行される。
void clint_ipi_send(u32 target_hart) { if (clint_ipi_hart_count <= target_hart) return; /* Set CLINT IPI */ writel(1, &clint_ipi[target_hart]); } void clint_ipi_clear(u32 target_hart) { if (clint_ipi_hart_count <= target_hart) return; /* Clear CLINT IPI */ writel(0, &clint_ipi[target_hart]); }
とてもシンプルで、他のhart(target_hart
)にinterruptを送りたい場合は、clint_ipi[target_hart]
を1と指定する。対して、interruptの受け手は、clint_ipi[target_hart]
として、0と指定する5。受け手の場合はinterruptを受け取れるようにするために、mie.MSIP(Machine Software Interrupt Pending)をONにする必要がある。
clint_ipi
はglobal変数で以下のように初期化されている:
- init_coldboot: - sbi_ipi_init(scratch, TRUE); - ipi_data_off = sbi_scratch_alloc_offset(sizeof(*ipi_data), "IPI_DATA"); - ipi_data = sbi_scratch_offset_ptr(scratch, ipi_data_off); - ipi_data->ipi_type = 0x00; - sbi_tlb_fifo_init(scratch, cold_boot): configure queue for IPI - csr_set(CSR_MIE, MIP_MSIP); - sbi_platform_ipi_init(sbi_platform_ptr(scratch), cold_boot) - sbi_platform_ops(plat)->ipi_init(cold_boot): virt_ipi_init - clint_cold_ipi_init(VIRT_CLINT_ADDR, VIRT_HART_COUNT)
int clint_cold_ipi_init(unsigned long base, u32 hart_count) { /* Figure-out CLINT IPI register address */ clint_ipi_hart_count = hart_count; // VIRT_HART_COUNT=8 clint_ipi_base = (void *)base; // VIRT_CLINT_ADDR=0x2000000 clint_ipi = (u32 *)clint_ipi_base; return 0; }
memory mapped I/Oとなっており、CLINT(Core Local INTerruptor)を操作するメモリの先頭アドレスは0x200_0000
である。これは、Reference[7]に記載がある:
sbi_hart_switch_mode関数~mretまで
すべてのhartの合流先がsbi_hart_switch_mode
で最後にmretを実行するので、この処理を追っていく。なお、この関数はH-mode(Hypervisor mode)の処理も実装されているが、本記事の範囲を超えるので省略する:
ソースコード: https://gist.github.com/knknkn1162/5c547113940b3afebb39cedb03c294f7
sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr, scratch->next_mode, false)
が呼び出し元だが、scratch
変数は「boot時の流れ」の節で説明したこの部分が元になっていることを思い出そう。というわけで、scratch->next_mode=PRV_S
, next_addr=payload_bin
である。また、各種CSRが以下のように設定されている。
- mstatus.MPPをnext_mode=PRV_S=(01), mstatus.MPIE=0(Machine Privious Interrupt Enable)に設定
- mepcをnext_addr=
payload_bin
に設定 - stvec, sscratch, sie, satpを初期化(linux kernelのboot時に設定される)
MPP(Machine Privious Priviledge)はmret
を実行したときの遷移先を決めるbitであった。mepc(Machine Exception Program Counter)はmret
が発生したときに飛ぶprogram counter(instruction pointerのこと)だった。この辺り不慣れならば、xv6-riscvを読むか、自身のReference[6]のprivilege modeの遷移の記事を参照してください。
さて、mret
を実行した後は、RISC-Vのhardware的な挙動は以下のようになる:
- sbi_hart_switch_mode関数にてmret実行
- mstatus.MIE <- mstatus.MPIE(=0) [MIEをrestore]
- S-modeに遷移する(5でmstatus.SPP=1としているため)
- mstatus.MPIE <~ 1 [always]
- sstatus.MPP <~ 00(U-mode) [always]
- pc(program counter) <~ mepc CSR(=payload_bin)
- software処理の開始
この辺りはReference[6]の「xv6-riscvを例にしたprivilegeの遷移例」の章に書いた。もしくは、Reference[2]のch.3.1.6.1を参照のこと。
ということでpayload_bin関数に移る。この関数は.incbin FW_PAYLOAD_PATH
であり、FW_PAYLOAD_PATHはlinux kernelのraw fileのfilepathであったので、めでたくlinux kernelのentrypointに移ることができた。当然linux kernel実行開始時のmodeはS-modeになっていることを確認しておく。
delegate_traps関数
前節までで、OpenSBIのbootからmretを経てS-modeに遷移し、linux kernelのentrypointを実行するまでを見た。 続いて、
bootが終わると、S-mode<->U-modeを行き来するが、どのtrapがトリガーとなり、M-modeに遷移するのか
を確認していきたい。これは、init_coldboot/init_warmboot => sbi_hart_init => delegate_trapsにて実装されているので、この関数を観察することになる。
// make simple static int delegate_traps(struct sbi_scratch *scratch, u32 hartid) { const struct sbi_platform *plat = sbi_platform_ptr(scratch); unsigned long interrupts, exceptions; /* Send M-mode interrupts and most exceptions to S-mode */ // SSIP(Supervisor Software Interrupt Pending) // STIP(Supervisor Timer Interrupt Pending) // SEIP(Supervisor External Interrupt Pending) interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; exceptions = (1U << CAUSE_MISALIGNED_FETCH) | (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_USER_ECALL) | (1U << CAUSE_FETCH_PAGE_FAULT) | (1U << CAUSE_LOAD_PAGE_FAULT) | (1U << CAUSE_STORE_PAGE_FAULT); csr_write(CSR_MIDELEG, interrupts); csr_write(CSR_MEDELEG, exceptions); return 0; }
mideleg(Machine Interrupt Delegation)とmedeleg(Machine Exception Delegation)の詳細はReference[6]の"medeleg/mideleg instruction"を見てほしいが、本来すべてのtrap handlerはM-modeに遷移するのだが、mideleg/medeleg CSRのbitを立てるとそのbitに対応するtrap発動時にS-modeに遷移させることができる。
traps: set by `delegate_traps` function - interrupt: - USI[User Software Interrupt](bit 0): 0 - SSI[Supervisor Software Interrupt](bit 1): 1 - MSI(bit 3): 0 - UTI[User Timer Interrupt](bit 4): 0 - STI[Supervisor Timer Interrupt](bit 5): 1 - MTI(bit 7): 0 - UEI[User External Interrupt](bit 8): 0 - SEI[Supervisor External Interrupt](bit 9): 1 - MEI[Machine External Interrupt](bit 11): 0 - exception: - 0x00(CAUSE_MISALIGNED_FETCH): S-mode - 0x01(CAUSE_FETCH_ACCESS): sbi_trap_redirect(regs, scratch, regs->mepc, mcause, mtval) - 0x02(CAUSE_ILLEGAL_INSTRUCTION): sbi_illegal_insn_handler(hartid, mcause, regs, scratch) - 0x03(CAUSE_BREAKPOINT): S-mode - 0x04(CAUSE_MISALIGNED_LOAD): sbi_misaligned_load_handler(hartid, mcause, regs, scratch) - 0x05(CAUSE_LOAD_ACCESS): sbi_trap_redirect(regs, scratch, regs->mepc, mcause, mtval) - 0x06(CAUSE_MISALIGNED_STORE): sbi_misaligned_store_handler(hartid, mcause, regs, scratch); - 0x07(CAUSE_STORE_ACCESS): sbi_trap_redirect(regs, scratch, regs->mepc, mcause, mtval); - 0x08(CAUSE_USER_ECALL): S-mode - 0x09(CAUSE_SUPERVISOR_ECALL): impossible - 0x0a(reserved) - 0x0b(CAUSE_MACHINE_ECALL): impossible - 0x0c(CAUSE_FETCH_PAGE_FAULT): S-mode - 0x0d(CAUSE_LOAD_PAGE_FAULT): S-mode - 0x0e(Reserved for future standard use) - 0x0f(CAUSE_STORE_PAGE_FAULT): S-mode - 0x10-0x40(Reserved)
interruptに関しては、S-modeで起こるSoftware/Timer/Enable InterruptはM-modeでなく、S-modeに遷移させるようにしている。 exceptionに関しては、ecall(x86のsyscallのこと), page fault exceptionはS-modeで処理するようにしている。
-
Reference[5] をもとにしている↩
-
正確にはcompilerに関数が戻ってこないことを通知するので、compilerが命令を最適化できるgccのoptionである。例えば、https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html とかを参照。↩
-
clintはCLINT(Core Local INTerruptor)でx86のlocal interruptに相当するもの。↩
-
これは、Reference[7]のch.9.2に"The msip register is a 32-bit wide WARL register, where the LSB is reflected in the msip bit of the mip register. Other bits in the msip registers are hardwired to zero. On reset, the msip registers are cleared to zero.“とある。↩