FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (3. RISC-Vのブートシーケンス)

Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく。

f:id:msyksphinz:20180613232411p:plain

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;
      };
    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)

    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, &reg);
    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], &current);

  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";
  };
};

Elfファイルからシンボルを取り出してシミュレータでトレースを表示する機能の実装

RISC-Vシミュレータにはelfファイルを読み込ませているのだが、elfファイルにはいろんな情報が取り込まれており、例えば

  • テキスト領域の関数のヘッダアドレス
  • グローバルデータが格納されているアドレス

などの情報が格納されている。

シミュレータは、テキスト領域の命令と、データ領域の初期値をロードするのだが、それ以外にも、関数の先頭アドレスや、シンボルのアドレスをロードすることによってより多くのデバッグ情報を出力することができる。 今回、RISC-Vシミュレータに追加したのは、関数が配置されているアドレス一覧を読み込んで、プログラムがどのような順番で関数をジャンプしていったのかをトレースすることができる「サブルーチントレース」(自称)の機能だ。

f:id:msyksphinz:20180612022439p:plain
図. サブルーチントレースの実行結果。関数がどのようにしてジャンプし、どこから戻ってきたかをトレースする。このプログラムでは外部I/Oの問題でプログラムが最終的にクラッシュしている...

どのようにしてサブルーチントレースを追加するか

サブルーチントレースの機能は、主に2つに分けられている。

  • 関数ジャンプを検出する部分
  • 関数から戻ってきたことを検出する部分

関数ジャンプの検出

まず、関数ジャンプをする部分の検出は2つの手法が考えられる。

  1. ジャンプ命令を検出する。
  2. ラベル一覧と比較し、新たなラベルに到達するとそれを関数ジャンプとみなす。

  3. の方法は、アーキテクチャ毎に、どの命令が関数ジャンプなのかを登録する必要がある。これはアーキテクチャ毎の項目が多くなり汎用的ではない。 一方で2. の方法は、命令がラベル付きのアドレスに到達するとそれは関数ジャンプとみなす。命令に限定しないので、より汎用的だといえる。 一方で2. の問題点は、関数コールのつもりはないのに、ラベルを踏むとそれだけで関数コールと判定されてしまう可能性がある。 したがって、ある程度のフィルタをかけなければならず。RISCプロセッサの場合は対外関数コールに使われる命令というのは、

  4. プログラムカウンタをアップデートする。

  5. プログラムカウンタ以外のレジスタをアップデータする(戻り先アドレスの保存)

となるので、この条件を満たし、さらにラベルを踏んだ命令は関数ジャンプとみなす。

  • 実装した関数ジャンプ検出のコード。
void TraceInfo::HierFunctionCall (Addr_t fetch_pc)
{
  if (m_hier_func_in_skip == true) {
    return;
  }

  // Jump to Function

  Addr_t jump_pc;
  bool is_find_jump_pc = FindPCUpdate (&jump_pc);

  std::string func_symbol;
  if (is_find_jump_pc) {
    if (m_pe_thread->FindSymbol (jump_pc, &func_symbol) == true) {
      for (uint32_t i = 0; i < GetHierDepth (); i++) {
        fprintf (GetTraceHierFp(), "  ");
      }
      std::stringstream str;
      str << "<FunctionCall " << GetStep() << " " << func_symbol << "(0x"
          << std::hex << std::setw(8) << std::setfill('0') << jump_pc << ")";
      fprintf (GetTraceHierFp(), "%s", str.str().c_str());
      SetHierDepth (GetHierDepth()+1);
      PairStackTrace *stack_trace = new PairStackTrace();
#ifdef ARCH_RISCV
      Addr_t next_pc = fetch_pc + RiscvDec::GetInstLength (GetInstIdx()) / 8;
#endif // ARCH_RISCV
      *stack_trace = std::make_pair (next_pc, func_symbol);
      m_hier_stack.push_back (stack_trace);

      // find skip list
      for (auto it = m_hier_skip_func.begin ();
           it != m_hier_skip_func.end ();
           it++) {
        if ((*it)->first == func_symbol) {
          m_hier_func_in_skip = true;
          if ((*it)->second == InstSkip_t::InstSkip) {
            m_hier_debug_in_skip = true;
          }
          fprintf (GetTraceHierFp(), " ...");
          break;
        }
        it++;
      }
      fprintf (GetTraceHierFp(), ">\n");
    }
  }
}

関数から戻ってきたことを検出する

これは、関数ジャンプ検出時にヒントを挿入しておく。関数から戻ってくるときは、そのジャンプ命令の次の命令に戻ってくることが大半だ(そうでない場合もあるが...)。

そこで、関数ジャンプが発生した際に、「関数ジャンプが実行された命令のプログラムアドレス+関数ジャンプ命令の命令長」戻り先の候補アドレスになり、それを戻り先候補アドレスとして登録する。 そしてシミュレーション上で、戻り先候補アドレスを踏むとそれを関数から戻ってきたこととみなす。

これらの情報をもとに、トレースファイルを作成すれば、上記のように関数のジャンプ履歴を取得することができる。

  • 実装した関数から戻ってくることを検出するコード
void TraceInfo::HierReturn (Addr_t fetch_pc)
{
  // Return from Function

  if (!m_hier_stack.empty ()) {
    bool is_found_pc = false;
    int32_t pop_count = 0;
    auto trace_it = m_hier_stack.end();
    trace_it--;
    for (; trace_it != m_hier_stack.begin(); trace_it--) {
      if ((*trace_it)->first == fetch_pc) {
        is_found_pc = true;
        break;
      }
      pop_count++;
    }
    if (is_found_pc == true) {
      while (pop_count-- >= 0) {
        m_hier_stack.pop_back();
        SetHierDepth (GetHierDepth()-1);
      }
      for (uint32_t i = 0; i < GetHierDepth (); i++) {
        fprintf (GetTraceHierFp(), "  ");
      }
      fprintf (GetTraceHierFp(), "<Return: %s>\n", (*trace_it)->second.c_str());

      m_hier_func_in_skip  = false;
      m_hier_debug_in_skip = false;
    }
  }
}

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (2. Device Tree Blobと Proxy Kernel)

RISC-Vのシミュレータは、シミュレーション対象のプログラムのElfファイル以外に、いくつかの外部ライブラリをロードしている。

  • RISC-V の Device Tree (SpikeのビルドにDevice Tree Compiler が必要なのはこのためだ)
  • RISC-V の Proxy Kernel (I/Oなどの本来OSが行う仕事を肩代わりしている)
f:id:msyksphinz:20180611010051p:plain
図. RISC-V Spike Instruction Set Simulatorの入出力ファイル

まず、Proxy Kernelについては libpk.a ではなく ${RISCV}/riscv-unknown-elf/bin/pk を観察する。 Proxy Kernel のエントリポイントは0x8000_0000 に設定されているため、この場所からプログラムの実行がスタートする。

の前に、本当に最初に始まるのはデバッグコードの実行だ。これはSpikeに以下のように記述されている。 見てわかる通り、このコードは最初にstart_pcのコードにジャンプする。これが上記の${RISCV}/riscv-unknown-elf/bin/pkに相当する。

  • ./riscv-isa-sim/riscv/sim.cc
  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)
  };

次にDevice Treeだが、これは何とSpikeが自身でDevice Tree Compilerを動作させて生成するようになっている。 例えば、デフォルトのSpikeを立ち上げたときに生成されるDTSは以下のようになる。これはSpike の --dump-dts で確認できる。

$ spike --dump-dts test_output_c
/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";
  };
};

これをコンパイルしてDTBを作成し、これをブートコードの後ろに挿入している。

  • ./riscv-isa-sim/riscv/sim.cc
  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);

  boot_rom.reset(new rom_device_t(rom));
  bus.add_device(DEFAULT_RSTVEC, boot_rom.get());

分散システムのコンセンサスアルゴリズムRaftについて調査

ビットコインの勉強の最中に、コンセンサスアルゴリズムについていろいろと調べていたのだが、じゃあ一般的に分散システムでコンセンサスってどうやってとるんだろう?ということに興味をもってコンセンサスアルゴリズムというものを知った。

最初は、ちょっと簡単そう(と言っても難しそうだが)Raftについて。

Raftの前にPaxosというアルゴリズムもあるらしいのだが、これはとても難しいらしいのでまずはRaftから。

論文とか読めばよかったのだけど、ものぐさなのでアニメーションだけ見ていた。っていうかこのページ凄いな!かなり分かりやすい。

Raft

f:id:msyksphinz:20180619235125p:plain
図. Raftについて解説するアニメーション。

Raftの論文はこちら。

  • In Search of an Understandable Consensus Algorithm (Extended Version)

https://raft.github.io/raft.pdf

Paxosについては、超絶頭の良い人たちの分かりやすい資料を見ないとさっぱりだ。

  • Paxos

www.slideshare.net

TensorFlow+Kerasに入門(3. Keras2cppでCIFAR10のモデルを変換してみる)

f:id:msyksphinz:20180701195704p:plain

FPGAの部屋のmarseeさんの記事を見て、TensorFlow+Kerasに入門してみた。 というかmarseeさんの記事で掲載されているソースコードをほとんどCopy & Pasteして実行してみているだけだが...

TensorFlow+KerasでCifar10を学習するサンプルプログラムを実行して、そこから得られたモデルを使ってKeras2cppでモデルの変換を行ってみたい。

最終的な目標は、Keras2cppを使ってC++のコードを出力し、それをネイティブC++環境で実行することだ。

まずは Kerasのサンプルコードを使用してcifar10のCNNのトレーニングを実行した。VirtualBox上のJupyter Notebook、さらにGPUを使わずに実行したので数時間はかかってしまった。 生成したモデルとトレーニングデータはファイルに保存しておくことにした。

github.com

f:id:msyksphinz:20180701193943p:plain

学習済みのモデルは、モデルファイルはJSON形式、重みパラメータはh5ファイルとして保存した。

from keras.models import load_model

model.save('cifar10_cnn_model.h5') # creates a HDF5 file 'my_model.h5'
with open('cifar10_cnn_model.json', 'w') as fout:
    fout.write(model.to_json())

保存したモデルを使って、keras2cppで動作するためのモデルに変換する。これでdumped.nnetが生成される。

$ python ./dump_to_simple_cpp.py -a ../keras_model/cifar10_cnn_model.json -w ../keras_model/cifar10_cnn_model.h5 -o dumped.nnet
Using TensorFlow backend.
Read architecture from ../keras_model/cifar10_cnn_model.json
Read weights from ../keras_model/cifar10_cnn_model.h5
Writing to dumped.nnet
2018-07-01 19:42:53.619547: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2

MNISTのモデルでもそうだが、keras2cppで推論を実行するためにはテストデータを用意しなければならない。これはcifar10のデータをダウンロードして生成した。

    tr_data10, tr_labels10, te_data10, te_labels10, label_names10 = get_cifar10(datapath)

    print("3 32 32")
    print("%f. " % (te_data10[0][0] / 10.0))
    for dim in range(3):
        for y in range(32):
            print("["),
            for x in range (32):
                print("%f. " % (int(te_data10[0][dim * 32 * 32 + y * 32 + x]) / 256.0)),
            print("]")

以下のようにしてcifar10_test_data.datを生成する。

python input_cifar.py > cifar10_test_data.dat

中身は以下のようになっている。3×32×32の入力データの配列を作成した。

3 32 32
[ 0.617188.  0.621094.  0.644531.  0.648438.  0.625000.  0.609375.  0.632812.  0.621094.  0.617188.  0.621094.  0.628906.  0.625000.  0.628906.  0.648438.  0.660156.  0.664062.  0.652344.  0.632812.  0.625000.  0.625000.  0.609375.  0.582031.  0.585938.  0.578125.  0.582031.  0.558594.  0.546875.  0.550781.  0.558594.  0.535156.  0.492188.  0.453125.  ]
[ 0.593750.  0.589844.  0.621094.  0.648438.  0.632812.  0.625000.  0.640625.  0.632812.  0.636719.  0.609375.  0.605469.  0.621094.  0.636719.  0.664062.  0.667969.  0.667969.  0.660156.  0.625000.  0.601562.  0.589844.  0.566406.  0.542969.  0.546875.  0.550781.  0.582031.  0.574219.  0.566406.  0.554688.  0.558594.  0.531250.  0.488281.  0.464844.  ]
[ 0.589844.  0.589844.  0.617188.  0.652344.  0.625000.  0.636719.  0.644531.  0.644531.  0.636719.  0.632812.  0.617188.  0.613281.  0.628906.  0.648438.  0.652344.  0.660156.  0.664062.  0.621094.  0.566406.  0.472656.  0.429688.  0.382812.  0.394531.  0.445312.  0.468750.  0.523438.  0.558594.  0.546875.  0.554688.  0.542969.  0.507812.  0.468750.  ]
[ 0.605469.  0.605469.  0.625000.  0.679688.  0.652344.  0.652344.  0.660156.  0.660156.  0.644531.  0.644531.  0.652344.  0.746094.  0.691406.  0.613281.  0.632812.  0.640625.  0.617188.  0.582031.  0.406250.  0.402344.  0.382812.  0.359375.  0.312500.  0.289062.  0.335938.  0.324219.  0.441406.  0.515625.  0.546875.  0.546875.  0.531250.  0.496094.  ]
[ 0.605469.  0.609375.  0.628906.  0.664062.  0.660156.  0.636719.  0.660156.  0.648438.  0.640625.  0.640625.  0.675781.  0.960938.  0.761719.  0.589844.  0.570312.  0.554688.  0.433594.  0.304688.  0.332031.  0.441406.  0.437500.  0.414062.  0.378906.  0.363281.  0.289062.  0.328125.  0.332031.  0.410156.  0.500000.  0.539062.  0.519531.  0.503906.  ]
[ 0.578125.  0.519531.  0.507812.  0.574219.  0.628906.  0.644531.  0.652344.  0.652344.  0.636719.  0.644531.  0.636719.  0.703125.  0.613281.  0.500000.  0.378906.  0.257812.  0.269531.  0.257812.  0.347656.  0.460938.  0.476562.  0.464844.  0.445312.  0.367188.  0.386719.  0.355469.  0.226562.  0.261719.  0.421875.  0.546875.  0.539062.  0.523438.  ]
[ 0.496094.  0.425781.  0.183594.  0.343750.  0.597656.  0.664062.  0.656250.  0.664062.  0.660156.  0.648438.  0.640625.  0.574219.  0.503906.  0.496094.  0.390625.  0.265625.  0.304688.  0.281250.  0.324219.  0.515625.  0.570312.  0.484375.  0.410156.  0.417969.  0.449219.  0.332031.  0.246094.  0.179688.  0.308594.  0.515625.  0.550781.  0.523438.  ]
[ 0.511719.  0.386719.  0.164062.  0.273438.  0.558594.  0.652344.  0.644531.  0.656250.  0.667969.  0.628906.  0.546875.  0.468750.  0.507812.  0.562500.  0.453125.  0.343750.  0.355469.  0.332031.  0.300781.  0.484375.  0.636719.  0.531250.  0.398438.  0.414062.  0.390625.  0.332031.  0.210938.  0.191406.  0.222656.  0.417969.  0.539062.  0.531250.  ]
[ 0.664062.  0.402344.  0.210938.  0.484375.  0.597656.  0.628906.  0.636719.  0.648438.  0.644531.  0.679688.  0.441406.  0.488281.  0.613281.  0.609375.  0.472656.  0.335938.  0.320312.  0.328125.  0.312500.  0.316406.  0.539062.  0.570312.  0.441406.  0.339844.  0.324219.  0.335938.  0.277344.  0.218750.  0.156250.  0.289062.  0.519531.  0.535156.  ]
[ 0.703125.  0.523438.  0.367188.  0.601562.  0.679688.  0.617188.  0.609375.  0.597656.  0.808594.  0.925781.  0.808594.  0.609375.  0.679688.  0.578125.  0.488281.  0.363281.  0.335938.  0.289062.  0.230469.  0.296875.  0.535156.  0.558594.  0.519531.  0.414062.  0.335938.  0.339844.  0.328125.  0.292969.  0.195312.  0.156250.  0.371094.  0.515625.  ]
[ 0.714844.  0.421875.  0.554688.  0.644531.  0.691406.  0.605469.  0.621094.  0.476562.  0.832031.  0.925781.  0.859375.  0.640625.  0.714844.  0.609375.  0.488281.  0.468750.  0.304688.  0.312500.  0.175781.  0.355469.  0.683594.  0.613281.  0.605469.  0.417969.  0.339844.  0.402344.  0.343750.  0.304688.  0.230469.  0.160156.  0.230469.  0.406250.  ]
[ 0.734375.  0.390625.  0.527344.  0.664062.  0.730469.  0.648438.  0.675781.  0.523438.  0.457031.  0.757812.  0.777344.  0.664062.  0.722656.  0.738281.  0.523438.  0.457031.  0.398438.  0.328125.  0.148438.  0.488281.  0.820312.  0.625000.  0.570312.  0.363281.  0.324219.  0.367188.  0.406250.  0.332031.  0.285156.  0.214844.  0.242188.  0.296875.  ]
[ 0.738281.  0.351562.  0.496094.  0.683594.  0.679688.  0.648438.  0.695312.  0.621094.  0.378906.  0.656250.  0.656250.  0.535156.  0.726562.  0.843750.  0.625000.  0.480469.  0.468750.  0.449219.  0.195312.  0.585938.  0.757812.  0.605469.  0.480469.  0.355469.  0.328125.  0.328125.  0.371094.  0.335938.  0.328125.  0.285156.  0.308594.  0.285156.  ]
...

これを読み込ませてkeras2cppのサンプルコードをコンパイルし、実行する。実行するとSegmentation Faultが出てしまった。

$ g++ -g -Wall -O0 -std=c++11 keras_model.cc example_main.cc && ./a.out
This is simple example with Keras neural network model loading into C++.
Keras model will be used in C++ for prediction only.
3
Reading model from ./dumped.nnet
Layers 18
Layer 0 Conv2D
Layer is empty, maybe it is not defined? Cannot define network.
DataChunk2D 3x32x32
Segmentation fault (core dumped)

デバッグしてみると、Kerasのモデルにおいて、Convolutional2DとConv2Dの名前の違いがあるらしい?

diff --git a/keras_model.cc b/keras_model.cc
index 6c65550..1159839 100644
--- a/keras_model.cc
+++ b/keras_model.cc
@@ -420,6 +420,8 @@ void keras::KerasModel::load_weights(const string &input_fname) {
     Layer *l = 0L;
     if(layer_type == "Convolution2D") {
       l = new LayerConv2D();
+    } else if(layer_type == "Conv2D") {
+      l = new LayerConv2D();
     } else if(layer_type == "Activation") {
       l = new LayerActivation();
     } else if(layer_type == "MaxPooling2D") {

上記の修正を入れて再度実行してみる。実行結果は、、、メモリ不足?対策を考えなければ。

$ g++ -g -Wall -O0 -std=c++11 keras_model.cc example_main.cc && ./a.out
This is simple example with Keras neural network model loading into C++.
Keras model will be used in C++ for prediction only.
3
Reading model from ./dumped.nnet
Layers 18
Layer 0 Conv2D
Layer 0 Conv2D
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Aborted (core dumped)

RISC-VのCompressed命令のISS実装 (2. RV64/RV32の両デコード対応)

RISC-VのCompressed命令について理解したので、自作ISSへの実装を進めた。

Compressed 命令の仕様については以下のまとめた。

msyksphinz.hatenablog.com

前回のRV64版のCompressed命令の実装についてはとりあえず終わったので、次はRV32版のCompressed命令の実装を検討していた。

f:id:msyksphinz:20180608002111p:plain

RV64とRV32で命令エンコーディングがかぶっている部分があるのでこれをどうしようかなと思っていたのだが、よく考えてみると 別にすべて自動で生成する必要は無くて、被っている2命令だけはモードに応じて手動でデコードしてやればいいんだ。 なんだ、単純。

というわけで、最後までデコードした後に、かぶっている命令については手動でデコードする関数を追加した。

    // Remaining Instruction is 2
    // c.fswsp    f[6:2],u[9:7]|u[12:10]<<3
    // c.sdsp     r[6:2],u[9:7]|u[12:10]<<3
      return DecodeInst_LD_10_F3_110_R3_00000_F2_00_R2_00000_R1_00001_RD_00000_OP_00000 (inst); break;

で、最後のDecodeInst_LD_10_F3_110_R3_00000_F2_00_R2_00000_R1_00001_RD_00000_OP_00000という関数は自分で書いてやる。 ここだけは特殊なデコードを許す。

InstId_t RiscvDec::DecodeInst_LD_10_F3_110_R3_00000_F2_00_R2_00000_R1_00001_RD_00000_OP_00000 (InstWord_t inst_hex)
{
  if (FLAGS_bit_mode == 32) {
    return InstId_t::INST_ID_C_FSWSP;
  } else if (FLAGS_bit_mode == 64) {
    return InstId_t::INST_ID_C_SDSP;
  } else {
    return InstId_t::INST_ID_SENTINEL_MAX;
  }
}

というわけで、無事にRV32/RV64のどちらでもテストがPassするようになった。とりあえずCompressed命令の実装はこれで終わり。

RISC-VのCompressed命令のISS実装 (1)

RISC-VのCompressed命令について理解したので、自作ISSへの実装を進めた。

Compressed 命令の仕様については以下のまとめた。

msyksphinz.hatenablog.com

というわけで、実装自体はテンプレートに従って追加していったのだが、いくつか注意しなければならないのが、上記のエントリにも書いたように命令のエンコーディングがかぶっているところだ。 RV32 と RV64/RV128 で、同じエンコーディングでも命令が異なっている部分がある。 ここについては少しケアしないとすべてのテストパタンを動作させることはできない。 とりあえずRV64のみ対応させ、RV32 / RV64の切り替えについては別の機構を追加することにした。

テストパタンについては、2種類用意されており、これをどちらもパスさせなければならないのが最終目標だ。 とりあえず、RV64のテストパタンだけ通すことにした。

  • rv64uc-v-rvc : RV64 Compressed命令テスト、仮想アドレスモード
  • rv64uc-p-rvc : RV64 Compressed命令テスト、物理アドレスモード
  • rv32uc-v-rvc : RV32 Compressed命令テスト、仮想アドレスモード
  • rv32uc-p-rvc : RV32 Compressed命令テスト、物理アドレスモード

とりあえず RV64UC-V-RVC については最後までテストが通るようになった。

f:id:msyksphinz:20180606233716p:plain
図. RV64 RVCのテストパタン実行結果。RVC命令が正しくデコードされ実行されている。

次は、RV32モードでのテストパタンをパスさせよう。