OpenSBIの内部実装(boot~linux kernelを実行するまで)

はじめに

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

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)をもとに追う。だいたい以下のようになっている:

  1. オブジェクトファイル群をまとめてlibplatsbi.aを作成
  2. fw_payload.S -> fw_payload.oにassemble。このとき、.incbin FW_PAYLOAD_PATHとして、linux kernelのimage(raw file)も取り込まれる。
  3. fw_payload.o + libplatsbi.a -> fw_payload.elf(ELF file)を作成(ld)。linker scriptはfw_payload.elf.ldS

2.のFW_PAYLOAD_PATHMakefileでの処理にて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.ldSENTRY(_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を考えよう。beqt0 -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つのことをやっている:

  1. struct sbi_scratchを構成
  2. FDT(flattened Device Tree)の構成

2.は今回扱うQEMU RISC-V Virt Machine Platformではスキップされるので飛ばす(FW_PAYLOAD_FDT_PATHがセットされていないので、beqz a1, _fdt_reloc_donea1=0となり、真になるため)。なので、実質やっていることは1のみだ。

1で作ったstruct sbi_scratchはmscratch CSRにセットされる。また、_start_warm関数のsbi_initの引数(struct sbi_scratch *scratch)にてこの構造体(のポインタ)がそのまま渡される。


struct sbi_scratchを構成する処理は以下の部分。主要なポイントだけおさえる:

1: la a4, platformplatformconst 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のメンバが入ることになる。

f:id:knknkn11626:20191017171003j:plain
memory map

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]に記載がある:

f:id:knknkn11626:20191020195519j:plain
Reference[7] ch.9.1より

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が以下のように設定されている。

  1. mstatus.MPPをnext_mode=PRV_S=(01), mstatus.MPIE=0(Machine Privious Interrupt Enable)に設定
  2. mepcをnext_addr=payload_binに設定
  3. 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的な挙動は以下のようになる:

  1. sbi_hart_switch_mode関数にてmret実行
  2. mstatus.MIE <- mstatus.MPIE(=0) [MIEをrestore]
  3. S-modeに遷移する(5でmstatus.SPP=1としているため)
  4. mstatus.MPIE <~ 1 [always]
  5. sstatus.MPP <~ 00(U-mode) [always]
  6. pc(program counter) <~ mepc CSR(=payload_bin)
  7. 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で処理するようにしている。


  1. Reference[5] をもとにしている

  2. hartはRISC-V hardware threadのことだが、CPU unitのこと。

  3. 正確にはcompilerに関数が戻ってこないことを通知するので、compilerが命令を最適化できるgccのoptionである。例えば、https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html とかを参照。

  4. clintはCLINT(Core Local INTerruptor)でx86のlocal interruptに相当するもの。

  5. これは、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.“とある。