Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく。
riscv-isa-sim/riscv/sim.cc
内のリセットベクタが最初に動作する。
riscv-isa-sim/riscv/sim.cc
void sim_t::make_dtb() { ... uint32_t reset_vec[reset_vec_size] = { 0x297, // auipc t0,0x0 0x28593 + (reset_vec_size * 4 << 20), // addi a1, t0, &dtb 0xf1402573, // csrr a0, mhartid get_core(0)->get_xlen() == 32 ? 0x0182a283u : // lw t0,24(t0) 0x0182b283u, // ld t0,24(t0) 0x28067, // jr t0 0, (uint32_t) (start_pc & 0xffffffff), (uint32_t) (start_pc >> 32) };
その直後にコンパイルしたDTB(Device Tree Blob)が挿入される。
riscv-isa-sim/riscv/sim.cc
void sim_t::make_dtb() { ... dts = s.str(); std::string dtb = dts_compile(dts); rom.insert(rom.end(), dtb.begin(), dtb.end()); const int align = 0x1000; rom.resize((rom.size() + align - 1) / align * align);
ここから先はpk(Proxy Kernel)のエントリポイントに移動する。エントリポイントではまずdo_reset
にジャンプする。
${RISCV}/riscv64-unknown-elf/bin/pk.dmp
0000000080000000 <_ftext>: 80000000: 1e80006f j 800001e8 <do_reset>
${RISCV}/riscv64-unknown-elf/bin/pk.dmp
00000000800001e8 <do_reset>: 800001e8: 00000093 li ra,0 800001ec: 00000113 li sp,0 800001f0: 00000193 li gp,0 800001f4: 00000213 li tp,0 ... 800002a8: 00070463 beqz a4,800002b0 <do_reset+0xc8> 800002ac: 6f60206f j 800029a2 <init_first_hart> 800002b0: 00800613 li a2,8 800002b4: 30461073 csrw mie,a2
init_first_hart()
にジャンプする。init_first_hart()
はriscv-pk/machine/minit.c
に定義されている。
void init_first_hart(uintptr_t hartid, uintptr_t dtb) { // Confirm console as early as possible query_uart(dtb); query_htif(dtb); hart_init(); hls_init(0); // this might get called again from parse_config_string ...
init_first_hart()
での仕事
init_first_hart()
は以下の関数が並んでいる。
query_uart(dtb)
fdt_scan()
を呼び出して設定する。そもそもFDT(Flatten Device Tree)とは。
Device Treeのバイナリ表現がFDT。
fdt_cb
構造体の表現riscv-pk/machine/fdt.h
struct fdt_cb { void (*open)(const struct fdt_scan_node *node, void *extra); void (*prop)(const struct fdt_scan_prop *prop, void *extra); void (*done)(const struct fdt_scan_node *node, void *extra); // last property was seen int (*close)(const struct fdt_scan_node *node, void *extra); // -1 => delete the node + children void *extra; };
- ソースコード
riscv-pk/machine/fdt.h
void query_uart(uintptr_t fdt) { struct fdt_cb cb; struct uart_scan scan; memset(&cb, 0, sizeof(cb)); cb.open = uart_open; cb.prop = uart_prop; cb.done = uart_done; cb.extra = &scan; fdt_scan(fdt, &cb); }
query_htif(dtb)
htif_done()
関数では、グローバル変数htif<0x80015100>
に対して1を設定している。
static void htif_done(const struct fdt_scan_node *node, void *extra) { struct htif_scan *scan = (struct htif_scan *)extra; if (!scan->compat) return; htif = 1; }
hart_init()
hartの初期化を行う。具体的には以下の通り。
minit.c
static void hart_init() { mstatus_init(); fp_init(); delegate_traps(); }
fp_init():minit.c
static void fp_init() { if (!supports_extension('D') && !supports_extension('F')) return; assert(read_csr(mstatus) & MSTATUS_FS); #ifdef __riscv_flen for (int i = 0; i < 32; i++) init_fp_reg(i); write_csr(fcsr, 0); #else uintptr_t fd_mask = (1 << ('F' - 'A')) | (1 << ('D' - 'A')); clear_csr(misa, fd_mask); assert(!(read_csr(misa) & fd_mask)); #endif }
delegate_traps():minit.c
// send S-mode interrupts and most exceptions straight to S-mode static void delegate_traps() { if (!supports_extension('S')) return; uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; uintptr_t exceptions = (1U << CAUSE_MISALIGNED_FETCH) | (1U << CAUSE_FETCH_PAGE_FAULT) | (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_LOAD_PAGE_FAULT) | (1U << CAUSE_STORE_PAGE_FAULT) | (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_USER_ECALL); write_csr(mideleg, interrupts); write_csr(medeleg, exceptions); assert(read_csr(mideleg) == interrupts); assert(read_csr(medeleg) == exceptions); }
hls_init(0); // this might get called again from parse_config_string
riscv-pk/machine/minit.c
hls_t* hls_init(uintptr_t id) { hls_t* hls = OTHER_HLS(id); memset(hls, 0, sizeof(*hls)); return hls; } #define OTHER_HLS(id) ((hls_t*)((void*)HLS() + RISCV_PGSIZE * ((id) - read_const_csr(mhartid))))
query_finisher(dtb)
finisherのアドレスを記録しておくらしい。finisherはPowerOffボタンのようなものなのか?
riscv-pk/machine/finisher.c
static void finisher_done(const struct fdt_scan_node *node, void *extra) { struct finisher_scan *scan = (struct finisher_scan *)extra; if (!scan->compat || !scan->reg || finisher) return; finisher = (uint32_t*)(uintptr_t)scan->reg; }
query_mem(dtb)
メモリの構成を記述している。
riscv-pk/machine/fdt.c
static void mem_prop(const struct fdt_scan_prop *prop, void *extra) { struct mem_scan *scan = (struct mem_scan *)extra; if (!strcmp(prop->name, "device_type") && !strcmp((const char*)prop->value, "memory")) { scan->memory = 1; } else if (!strcmp(prop->name, "reg")) { scan->reg_value = prop->value; scan->reg_len = prop->len; } }
riscv-pk/machine/fdt.c
別に何かが設定された様子も無いけど、どうなっているんだろう?っていか、下記のSpikeのDTSだと0x0-0x80000000までを2回設定しているんだけど、これはいかに?
static void mem_done(const struct fdt_scan_node *node, void *extra) { struct mem_scan *scan = (struct mem_scan *)extra; const uint32_t *value = scan->reg_value; const uint32_t *end = value + scan->reg_len/4; uintptr_t self = (uintptr_t)mem_done; if (!scan->memory) return; assert (scan->reg_value && scan->reg_len % 4 == 0); while (end - value > 0) { uint64_t base, size; value = fdt_get_address(node->parent, value, &base); value = fdt_get_size (node->parent, value, &size); if (base <= self && self <= base + size) { mem_size = size; } } assert (end == value); }
query_harts(dtb)
CPUのコンフィグレーションについて設定を行っている。"device_type=cpu"の部分のDTSを読み取っている。riscv-pk/machine/fdt.c
static void hart_prop(const struct fdt_scan_prop *prop, void *extra) { struct hart_scan *scan = (struct hart_scan *)extra; if (!strcmp(prop->name, "device_type") && !strcmp((const char*)prop->value, "cpu")) { assert (!scan->cpu); scan->cpu = prop->node; } else if (!strcmp(prop->name, "interrupt-controller")) { assert (!scan->controller); scan->controller = prop->node; } else if (!strcmp(prop->name, "#interrupt-cells")) { scan->cells = bswap(prop->value[0]); } else if (!strcmp(prop->name, "phandle")) { scan->phandle = bswap(prop->value[0]); } else if (!strcmp(prop->name, "reg")) { uint64_t reg; fdt_get_address(prop->node->parent, prop->value, ®); scan->hart = reg; } }
static void hart_done(const struct fdt_scan_node *node, void *extra) { struct hart_scan *scan = (struct hart_scan *)extra; if (scan->cpu == node) { assert (scan->hart >= 0); } if (scan->controller == node && scan->cpu) { assert (scan->phandle > 0); assert (scan->cells == 1); if (scan->hart < MAX_HARTS) { hart_phandles[scan->hart] = scan->phandle; hart_mask |= 1 << scan->hart; hls_init(scan->hart); } } }
query_clint(dtb)
query_plic(dtb)
wake_harts()
CPUを起動する。はずなのだが、HLS
というのは何なのか良く分からない。
static void wake_harts() { for (int hart = 0; hart < MAX_HARTS; ++hart) if ((((~disabled_hart_mask & hart_mask) >> hart) & 1)) *OTHER_HLS(hart)->ipi = 1; // wakeup the hart }
plic_init()
hart_plic_init()
//prci_test()
memory_init()
メモリの初期化なのだが、実際にはメモリサイズの設定をしているように見える。riscv-pk/machine/minit.c
static void memory_init() { mem_size = mem_size / MEGAPAGE_SIZE * MEGAPAGE_SIZE; }
boot_loader(dtb)
boot_loader()
はriscv-pk/pk/pk.c
に定義されている。riscv-pk/bbl/bbl.c
にも定義されているがこれは別物らしい。オブジェクトをダンプしてみると中身が異なっていた。riscv-pk/pk/pk.c
void boot_loader(uintptr_t dtb) { extern char trap_entry; write_csr(stvec, &trap_entry); write_csr(sscratch, 0); write_csr(sie, 0); set_csr(sstatus, SSTATUS_SUM); file_init(); enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0); }
file_init()
stdin
,stdout
,stderr
を作成する。files_t
という型名でグローバル領域に領域が確保されている。file_t
の型は以下の通り。riscv-pk/pk/file.h
typedef struct file { int kfd; // file descriptor on the host side of the HTIF uint32_t refcnt; } file_t;
riscv-pk/pk/file.c
:file_get_free()
参照カウントがまだ0になっている構造体を取得する。dupはインクリメントの回数を増やす?
static file_t* file_get_free() { for (file_t* f = files; f < files + MAX_FILES; f++) if (atomic_read(&f->refcnt) == 0 && atomic_cas(&f->refcnt, 0, 2) == 0) return f; return NULL; }
int file_dup(file_t* f) { for (int i = 0; i < MAX_FDS; i++) { if (atomic_cas(&fds[i], 0, f) == 0) { file_incref(f); return i; } } return -1; }
enter_supervisor_mode()
ではプログラムをロードして実行するのだが、mepc
に関数の先頭アドレスをロードして、mret
で実行する。enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0)
としており、rest_of_boot_loader()
の内容についてみていく。riscv-pk/pk/pk.c
static void rest_of_boot_loader(uintptr_t kstack_top) { arg_buf args; size_t argc = parse_args(&args); if (!argc) panic("tell me what ELF to load!");
// load program named by argv[0] long phdrs[128]; current.phdr = (uintptr_t)phdrs; current.phdr_size = sizeof(phdrs); load_elf(args.argv[0], ¤t); run_loaded_program(argc, args.argv, kstack_top); }
fdt_scan()
の実装
Flatten Device Tree の構造しかParseしない。
まずはFTDのヘッダを見ていくと、構造体としては以下のように定義されている。この辺りは、以下のサイトが非常に詳細に解説しており、役に立った。
http://masahir0y.blogspot.com/2014/05/device-tree.html
riscv-pk/machine/fdt.h
struct fdt_header { uint32_t magic; uint32_t totalsize; uint32_t off_dt_struct; uint32_t off_dt_strings; uint32_t off_mem_rsvmap; uint32_t version; uint32_t last_comp_version; /* <= 17 */ uint32_t boot_cpuid_phys; uint32_t size_dt_strings; uint32_t size_dt_struct; };
riscv-pk/machine/fdt.c
void fdt_scan(uintptr_t fdt, const struct fdt_cb *cb) { struct fdt_header *header = (struct fdt_header *)fdt; // Only process FDT that we understand if (bswap(header->magic) != FDT_MAGIC || bswap(header->last_comp_version) > FDT_VERSION) return; const char *strings = (const char *)(fdt + bswap(header->off_dt_strings)); uint32_t *lex = (uint32_t *)(fdt + bswap(header->off_dt_struct)); fdt_scan_helper(lex, strings, 0, cb); }
そこから先はFDTのバイナリを読み取ってひたすらfdt_scan_helper()
を実行する。
lex
にはそのDevice Treeの構造を示す先頭のポインタが入り、string
にはDevice Treeの内容を示す文字列が入る。fdt_scan_helper()
はデータ構造の役割を定義してあり、
riscv-pk/machine/fdt.h
#define FDT_BEGIN_NODE 1 #define FDT_END_NODE 2 #define FDT_PROP 3 #define FDT_NOP 4 #define FDT_END 9
となっている。たとえば、FDT_BEGIN_NODE
の場合には、やたらとopen()
、close()
をしているけどこれは何だろう。
riscv/pk/machine/fdt.c
static uint32_t *fdt_scan_helper( uint32_t *lex, const char *strings, struct fdt_scan_node *node, const struct fdt_cb *cb) { ... case FDT_BEGIN_NODE: { uint32_t *lex_next; if (!last && node && cb->done) cb->done(node, cb->extra); last = 1; child.name = (const char *)(lex+1); if (cb->open) cb->open(&child, cb->extra); lex_next = fdt_scan_helper( lex + 2 + strlen(child.name)/4, strings, &child, cb); if (cb->close && cb->close(&child, cb->extra) == -1) while (lex != lex_next) *lex++ = bswap(FDT_NOP); lex = lex_next; break; } ... }
uartの場合、それぞれ、cb->open()
、cb->prop()
、cb->done()
で使用されている関数は以下となる。
riscv-pk/machine/uart.c
static void uart_open(const struct fdt_scan_node *node, void *extra) { struct uart_scan *scan = (struct uart_scan *)extra; memset(scan, 0, sizeof(*scan)); }
static void uart_prop(const struct fdt_scan_prop *prop, void *extra) { struct uart_scan *scan = (struct uart_scan *)extra; if (!strcmp(prop->name, "compatible") && !strcmp((const char*)prop->value, "sifive,uart0")) { scan->compat = 1; } else if (!strcmp(prop->name, "reg")) { fdt_get_address(prop->node->parent, prop->value, &scan->reg); } }
static void uart_done(const struct fdt_scan_node *node, void *extra) { struct uart_scan *scan = (struct uart_scan *)extra; if (!scan->compat || !scan->reg || uart) return; // Enable Rx/Tx channels uart = (void*)(uintptr_t)scan->reg; uart[UART_REG_TXCTRL] = UART_TXEN; uart[UART_REG_RXCTRL] = UART_RXEN; }
prop()
では、Device Tree内にUARTを示す記述が存在するかどうかをチェックしているようだ。最後にuart[...]
で何かの設定をしている。uart
はグローバル変数として定義されている。
riscv-pk/machine/uart.c
volatile uint32_t* uart;
compatible
の意味は良く分からない。reg
が指定してあると、割り当てられているアドレスを探索するようだ。
riscv-pk/machine/fdt.c
const uint32_t *fdt_get_address(const struct fdt_scan_node *node, const uint32_t *value, uint64_t *result) { *result = 0; for (int cells = node->address_cells; cells > 0; --cells) *result = (*result << 32) + bswap(*value++); return value; }
おまけ Proxy Kernelのグローバル変数の配置領域
pk.dmp
000000008000c000 <fds>: 000000008000c400 <magic_mem.2181>: 000000008000d000 <stacks>: 0000000080015000 <hart_phandles>: 0000000080015020 <lock.2182>: 0000000080015028 <free_pages>: 0000000080015030 <next_free_page>: 0000000080015038 <first_free_page>: 0000000080015040 <vmrs>: 0000000080015048 <vm_lock>: 0000000080015050 <htif_lock>: 0000000080015058 <disabled_hart_mask>: 0000000080015060 <current>: 00000000800150d0 <first_free_paddr>: 00000000800150d8 <plic_ndevs>: 00000000800150e0 <plic_priorities>: 00000000800150e8 <mem_size>: 00000000800150f0 <mtime>: 00000000800150f8 <root_page_table>: 0000000080015100 <htif>: 0000000080015108 <htif_console_buf>: 0000000080015110 <uart>: 0000000080015118 <finisher>: 0000000080015120 <hart_mask>:
おまけ Spikeが生成するDevice Tree Source
$ spike --dump-dts hoge /dts-v1/; / { #address-cells = <2>; #size-cells = <2>; compatible = "ucbbar,spike-bare-dev"; model = "ucbbar,spike-bare"; cpus { #address-cells = <1>; #size-cells = <0>; timebase-frequency = <10000000>; CPU0: cpu@0 { device_type = "cpu"; reg = <0>; status = "okay"; compatible = "riscv"; riscv,isa = "rv64imafdc"; mmu-type = "riscv,sv48"; clock-frequency = <1000000000>; CPU0_intc: interrupt-controller { #interrupt-cells = <1>; interrupt-controller; compatible = "riscv,cpu-intc"; }; }; }; memory@80000000 { device_type = "memory"; reg = <0x0 0x80000000 0x0 0x80000000>; }; soc { #address-cells = <2>; #size-cells = <2>; compatible = "ucbbar,spike-bare-soc", "simple-bus"; ranges; clint@2000000 { compatible = "riscv,clint0"; interrupts-extended = <&CPU0_intc 3 &CPU0_intc 7 >; reg = <0x0 0x2000000 0x0 0xc0000>; }; }; htif { compatible = "ucb,htif0"; }; };