FPGA開発日記

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

RustでELFファイルを開く方法を調査する (3. Section Headerを読み取る)

Program Headerを読み取ることができるようになったので、次はSection Headerを読み取ることにする。Section Headerに関する情報は、ELFファイルの先頭から、ELFヘッダテーブルにおけるSHOFFバイトの位置から開始している。また、各セクションの大きさは、SHENTSIZE分なので、SHNUM分だけセクションを読んでいけばよい。

        for sh_idx in 0..loader.get_e_shnum() {
            let shdr: SectionHeader = loader.get_section_header(sh_idx.into());
            shdr.dump();
            loader.dump_section(shdr.sh_offset, shdr.sh_size);
        }

get_section_header()では、SHOFFからSHENTSIZEバイトずつデータを読み込んで情報を表示してく。Section Headerの構成は以下のようになっているので、そのまま値を書きだしていく仕組みだ。

typedef struct
{
  Elf64_Word    sh_name;        /* Section name (string tbl index) */
  Elf64_Word    sh_type;        /* Section type */
  Elf64_Xword   sh_flags;       /* Section flags */
  Elf64_Addr    sh_addr;        /* Section virtual addr at execution */
  Elf64_Off sh_offset;      /* Section file offset */
  Elf64_Xword   sh_size;        /* Section size in bytes */
  Elf64_Word    sh_link;        /* Link to another section */
  Elf64_Word    sh_info;        /* Additional section information */
  Elf64_Xword   sh_addralign;       /* Section alignment */
  Elf64_Xword   sh_entsize;     /* Entry size if section holds table */
} Elf64_Shdr;
    fn get_section_header(&self, idx: u32) -> SectionHeader {
        let mut sh_off   = self.get_e_shoff();
        let sh_entsize  = self.get_e_shentsize() as u32;

        sh_off += sh_entsize*idx;

        let sh_name      = self.get_4byte_elf(sh_off as usize); sh_off += 4;
        let sh_type      = self.get_4byte_elf(sh_off as usize); sh_off += 4;
        let sh_flags     = self.get_8byte_elf(sh_off as usize); sh_off += 8;
        let sh_addr      = self.get_8byte_elf(sh_off as usize); sh_off += 8;
        let sh_offset    = self.get_8byte_elf(sh_off as usize); sh_off += 8;
        let sh_size      = self.get_8byte_elf(sh_off as usize); sh_off += 8;
        let sh_link      = self.get_4byte_elf(sh_off as usize); sh_off += 4;
        let sh_info      = self.get_4byte_elf(sh_off as usize); sh_off += 4;
        let sh_addralign = self.get_8byte_elf(sh_off as usize); sh_off += 8;
        let sh_entsize   = self.get_8byte_elf(sh_off as usize); // sh_off += 8;

        SectionHeader::new (sh_name, sh_type, sh_flags, sh_addr, sh_offset,
                            sh_size, sh_link, sh_info, sh_addralign, sh_entsize.into())
    }

このようにして最終的に結果をダンプしていく。以下のようになった。

...
== Section Dump ==
  Name      : 1b
  Type      : 1
  Flags     : 6
  Addr      : 80000000
  Offset    : 1000
  Size      : 144
  Link      : 0
  Info      : 0
  AddrAlign : 40
  EntSize   : 0
04c0006f 34202f73 00800f93 03ff0a63
00900f93 03ff0663 00b00f93 03ff0263
80000f17 fe0f0f13 000f0463 000f0067
34202f73 000f5463 0040006f 5391e193
00001f17 fc3f2023 ff9ff06f f1402573
00051063 00000297 01028293 30529073
18005073 00000297 01c28293 30529073
...
72757461 65620065 5f6e6967 6e676973
72757461 735f0065 74726174 6e655f00
72660064 6f686d6f 00007473
== Section Dump ==
  Name      : 11
  Type      : 3
  Flags     : 0
  Addr      : 0
  Offset    : 223b
  Size      : 2e
  Link      : 0
  Info      : 0
  AddrAlign : 1
  EntSize   : 0
79732e00 6261746d 74732e00 62617472
68732e00 74727473 2e006261 74786574
696e692e 742e0074 736f686f 00000074

RustでELFファイルを開く方法を調査する (2. Program Headerを読み取る)

RustでELFファイルを開く続き。次はProgram Headerを読むコードを書いていく。Program Headerはセグメント領域の情報が格納されている。セグメントにはコードセグメントとデータセグメントが存在し、コードセグメントにはプログラム、データセグメントにはデータが格納されている。

前回のヘッダファイルの読み取りを見直してみると、

E_PHOFF     = 40     // プログラムヘッダのファイル先頭からのオフセット
E_SHOFF     = 2270      // セグメントヘッダのファイル先頭からのオフセット
E_FLAGS     = 0
E_EHSIZE    = 64
E_PHENTSIZE = 56        // プログラムヘッダのサイズ
E_PHNUM     = 2         // プログラムヘッダのエントリ数
E_SHENTSIZE = 64        // セグメントヘッダのサイズ
E_SHNUM     = 6         // セグメントヘッダのエントリ数
E_SHSTRNDX  = 5

まずはプログラムヘッダ。プログラムヘッダのサイズは56バイトで、2つのエントリが格納されている。オフセットは0x40から始まっているので、ここからデータを取り出して構造体を作成する。

    fn get_program_header(&self, idx: u32) -> ProgramHeader {
        let ph_off   = self.get_e_phoff();
        let ph_size  = self.get_e_phentsize() as u32;

        let p_type   = self.get_4byte_elf((ph_off + ph_size*idx                             ) as usize);
        let p_flags  = self.get_4byte_elf((ph_off + ph_size*idx + 4                         ) as usize);
        let p_offset = self.get_8byte_elf((ph_off + ph_size*idx + 4 + 4                     ) as usize);
        let p_vaddr  = self.get_8byte_elf((ph_off + ph_size*idx + 4 + 4 + 8                 ) as usize);
        let p_paddr  = self.get_8byte_elf((ph_off + ph_size*idx + 4 + 4 + 8 + 8             ) as usize);
        let p_filesz = self.get_8byte_elf((ph_off + ph_size*idx + 4 + 4 + 8 + 8 + 8         ) as usize);
        let p_memsz  = self.get_8byte_elf((ph_off + ph_size*idx + 4 + 4 + 8 + 8 + 8 + 8     ) as usize);
        let p_align  = self.get_8byte_elf((ph_off + ph_size*idx + 4 + 4 + 8 + 8 + 8 + 8 + 8 ) as usize);

        let phdr_type = match Phdr_Type::from_u64(p_type as u64) {
            Some(phdr_type) => phdr_type,
            None            => panic!("Unknown Phdr Type"),
        };

        ProgramHeader::new (phdr_type, p_flags, p_offset, p_vaddr, p_paddr,
                            p_filesz, p_memsz, p_align)

かなり無理やりだが、PHOFFの先頭から順番にデータを取り出してProgramHeader構造体に格納する。これをダンプすることで情報を表示する。

    pub fn dump_phdr(&self)
    {
        println!("  Entry Type  : {}", self.get_type_string());
        println!("  Flags       : {:x}", self.p_flags);
        println!("  Offset      : {:x}", self.p_offset);
        println!("  VAddr       : {:x}", self.p_vaddr);
        println!("  PAddr       : {:x}", self.p_paddr);
        println!("  File Size   : {:x}", self.p_filesz);
        println!("  Memory Size : {:x}", self.p_memsz);
        println!("  Alignment   : {:x}", self.p_align);
    }

さらに、このセクション情報をもとに各セクションの内容をダンプすることにした。self.p_offsetself.p_memszの間どデータをひたすらダンプする。

    fn dump_section(&self, start: u64, memsz: u64)
    {
        for byte_idx in (start..(start + memsz)).step_by(4) {
            print!("{:08x} ", self.get_4byte_elf(byte_idx as usize));
            if byte_idx % 16 == 16-4 {
                print!("\n");
            }
        }
        print!("\n");
    }

これで、以下のようなELFの情報を抽出できるようになった。

E_TYPE      = ET_EXEC
E_MACHINE   = RISCV
E_VERSION   = 1
E_ENTRY     = 80000000
E_PHOFF     = 40
E_SHOFF     = 2270
E_FLAGS     = 0
E_EHSIZE    = 64
E_PHENTSIZE = 56
E_PHNUM     = 2
E_SHENTSIZE = 64
E_SHNUM     = 6
E_SHSTRNDX  = 5
  Entry Type  : PT_LOAD
  Flags       : 5
  Offset      : 1000
  VAddr       : 80000000
  PAddr       : 80000000
  File Size   : 144
  Memory Size : 144
  Alignment   : 1000
04c0006f 34202f73 00800f93 03ff0a63
00900f93 03ff0663 00b00f93 03ff0263
80000f17 fe0f0f13 000f0463 000f0067
34202f73 000f5463 0040006f 5391e193
00001f17 fc3f2023 ff9ff06f f1402573
00051063 00000297 01028293 30529073
18005073 00000297 01c28293 30529073
fff00293 3b029073 01f00293 3a029073
00000297 01828293 30529073 30205073
30305073 30405073 00000193 00000297
f6828293 30529073 00100513 01f51513
00055863 0ff0000f 00100193 00000073
80000297 f4028293 00028e63 10529073
0000b2b7 1092829b 30229073 30202373
f4629ee3 30005073 00000297 01428293
34129073 f1402573 30200073 0ff0000f
...

RustでELFファイルを開く方法を調査する (1. ELFファイルをRustで開く方法)

非常に久しぶりにRustを触っている。RISC-Vシミュレータ以外にもRustでやってみたいことがあって色々調べているのだが、RustでELFファイルなどのバイナリを扱う方法を少し調べていた。シミュレータの用途でもあるし、別の用途でもELFファイルを扱えるといろいろ便利だ。

以下のブログを参考にした。なるほど、mmapというのを使えばELFをメモリにマッピングしているような状態で扱うことができるらしい。

tomo-wait-for-it-yuki.hatenablog.com

  • elf_loader/src/main.rs
use std::fs::File;
use memmap::Mmap;
use std::env;

// 0x7f 'E' 'L' 'F'
const HEADER_MAGIC: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];

const EM_RISCV: u16 = 243;

pub struct ElfLoader {
    mapped_file: Mmap,
}

impl ElfLoader {
    pub fn try_new(file_path: &str) -> std::io::Result<ElfLoader> {
        let file = File::open(&file_path)?;
        Ok(ElfLoader {
            mapped_file: unsafe { Mmap::map(&file)? },
        })
    }
    ...

main()内で以下のようにELFのファイル名を指定してファイルをオープンする。

fn main() {
    let args: Vec<String> = env::args().collect();

    let filename = &args[1];
    let loader = match ElfLoader::try_new(filename) {
        Ok(loader) => loader,
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error)
        },
    };
...

ロードに成功すれば、次にELFのビットフィールドを読んでいきいろんな情報を収集する。

    if loader.is_elf() {
        println!("OK!");
        println!("EI_CLASS   = {}", loader.get_ei_class());
        println!("EI_DATA    = {}", loader.get_ei_data());
        println!("EI_VERSION = {}", loader.get_ei_version());

        println!("EI_MACHINE = {}", loader.get_ei_machine_string());

    } else {
        println!("Not ELF file!");
    }

これで実行してみる。

$ cargo run ${HOME}/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-p-simple
OK!
EI_CLASS   = 2
EI_DATA    = 1
EI_VERSION = 1
EI_MACHINE = RISCV
$ cargo run target/debug/elf_loader
OK!
EI_CLASS   = 2
EI_DATA    = 1
EI_VERSION = 1
EI_MACHINE = X86_64

とりあえずまずは最初の段階までたどり着いた。

もうすこしELFの読み取りを進めていく。少なくと各セクションのデータを読み込んで、逆アセンブリが出力できる程度にはしておきたいな。

ELFヘッダの位置は、/usr/include/elf.hを確認すると、以下のような順番で格納されている。以下は64ビットの例:

  • /usr/include/elf.h
typedef struct
{
  unsigned char   e_ident[EI_NIDENT]; /* Magic number and other info */
  Elf64_Half    e_type;         /* Object file type */
  Elf64_Half    e_machine;      /* Architecture */
  Elf64_Word    e_version;      /* Object file version */
  Elf64_Addr    e_entry;        /* Entry point virtual address */
  Elf64_Off e_phoff;        /* Program header table file offset */
  Elf64_Off e_shoff;        /* Section header table file offset */
  Elf64_Word    e_flags;        /* Processor-specific flags */
  Elf64_Half    e_ehsize;       /* ELF header size in bytes */
  Elf64_Half    e_phentsize;        /* Program header table entry size */
  Elf64_Half    e_phnum;        /* Program header table entry count */
  Elf64_Half    e_shentsize;        /* Section header table entry size */
  Elf64_Half    e_shnum;        /* Section header table entry count */
  Elf64_Half    e_shstrndx;     /* Section header string table index */
} Elf64_Ehdr;

この通りにフィールドを切り取っていくことにした。まずは定数を定義して簡単に切り取れるようにする。

  • elf_loader/src/main.rs
/* 64bit architectures */
const ELF64_ADDR_SIZE   :usize = mem::size_of::<u64>();
const ELF64_OFF_SIZE    :usize = mem::size_of::<u64>();
const ELF64_SWORD_SIZE  :usize = mem::size_of::<i32>();
const ELF64_WORD_SIZE   :usize = mem::size_of::<u32>();
const ELF64_HALF_SIZE   :usize = mem::size_of::<u16>();

const E_TYPE_START_BYTE      :usize = 16;
const E_TYPE_SIZE_BYTE       :usize = ELF64_HALF_SIZE;
const E_MACHINE_START_BYTE   :usize = E_TYPE_START_BYTE + E_TYPE_SIZE_BYTE;
const E_MACHINE_SIZE_BYTE    :usize = ELF64_HALF_SIZE;
const E_VERSION_START_BYTE   :usize = E_MACHINE_START_BYTE + E_MACHINE_SIZE_BYTE;
const E_VERSION_SIZE_BYTE    :usize = ELF64_WORD_SIZE;
const E_ENTRY_START_BYTE     :usize = E_VERSION_START_BYTE + E_VERSION_SIZE_BYTE;
const E_ENTRY_SIZE_BYTE      :usize = ELF64_ADDR_SIZE;
...

これに基づいてELFの情報を切り取っていく。ダサいが以下のような実装にしている。もうちょっとビットフィールド切り取りを綺麗に作りたいが、まずは正確動作最優先。

    fn get_e_version(&self) -> u64 {
        (self.mapped_file[E_VERSION_START_BYTE + 3] as u64) << 24 |
        (self.mapped_file[E_VERSION_START_BYTE + 2] as u64) << 16 |
        (self.mapped_file[E_VERSION_START_BYTE + 1] as u64) <<  8 |
        (self.mapped_file[E_VERSION_START_BYTE + 0] as u64)
    }

    fn get_e_entry(&self) -> u64 {
        (self.mapped_file[E_ENTRY_START_BYTE + 3] as u64) << 24 |
        (self.mapped_file[E_ENTRY_START_BYTE + 2] as u64) << 16 |
        (self.mapped_file[E_ENTRY_START_BYTE + 1] as u64) <<  8 |
        (self.mapped_file[E_ENTRY_START_BYTE + 0] as u64)
    }

    fn get_e_phoff(&self) -> u64 {
        (self.mapped_file[E_PHOFF_START_BYTE + 3] as u64) << 24 |
        (self.mapped_file[E_PHOFF_START_BYTE + 2] as u64) << 16 |
        (self.mapped_file[E_PHOFF_START_BYTE + 1] as u64) <<  8 |
        (self.mapped_file[E_PHOFF_START_BYTE + 0] as u64)
    }

    fn get_e_shoff(&self) -> u64 {
        (self.mapped_file[E_SHOFF_START_BYTE + 3] as u64) << 24 |
        (self.mapped_file[E_SHOFF_START_BYTE + 2] as u64) << 16 |
        (self.mapped_file[E_SHOFF_START_BYTE + 1] as u64) <<  8 |
        (self.mapped_file[E_SHOFF_START_BYTE + 0] as u64)
    }

    /* Processor-specific flags */
    fn get_e_flags(&self) -> u32 {
        (self.mapped_file[E_FLAGS_START_BYTE + 3] as u32) << 24 |
        (self.mapped_file[E_FLAGS_START_BYTE + 2] as u32) << 16 |
        (self.mapped_file[E_FLAGS_START_BYTE + 1] as u32) <<  8 |
        (self.mapped_file[E_FLAGS_START_BYTE + 0] as u32)
    }
    /* ELF header size in bytes */
    fn get_e_ehsize(&self) -> u32 {
        (self.mapped_file[E_EHSIZE_START_BYTE + 1] as u32) <<  8 |
        (self.mapped_file[E_EHSIZE_START_BYTE + 0] as u32)
    }
    /* Program header table entry size */
    fn get_e_phentsize(&self) -> u32 {
        (self.mapped_file[E_PHENTSIZE_START_BYTE + 1] as u32) <<  8 |
...

main()println!で以下のようにダンプする。

    if loader.is_elf() {
        println!("OK!");
        println!("EI_CLASS   = {}", loader.get_ei_class());
        println!("EI_DATA    = {}", loader.get_ei_data());
        println!("EI_VERSION = {}",   loader.get_ei_version());

        println!("\n");
        println!("E_TYPE      = {}",   loader.get_e_type_string());
        println!("E_MACHINE   = {}",   loader.get_e_machine_string());
        println!("E_VERSION   = {}",   loader.get_e_version());
        println!("E_ENTRY     = {:x}", loader.get_e_entry());
        println!("E_PHOFF     = {:x}", loader.get_e_phoff());
        println!("E_SHOFF     = {:x}", loader.get_e_shoff());
        println!("E_FLAGS     = {}",   loader.get_e_flags());
        println!("E_EHSIZE    = {}",   loader.get_e_ehsize());
        println!("E_PHENTSIZE = {}",   loader.get_e_phentsize());
        println!("E_PHNUM     = {}",   loader.get_e_phnum());
        println!("E_SHENTSIZE = {}",   loader.get_e_shentsize());
        println!("E_SHNUM     = {}",   loader.get_e_shnum());
        println!("E_SHSTRNDX  = {}",   loader.get_e_shstrndx());
...
$ cargo run ${HOME}/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-p-simple
OK!
EI_CLASS   = 2
EI_DATA    = 1
EI_VERSION = 1


E_TYPE      = ET_EXEC
E_MACHINE   = RISCV
E_VERSION   = 1
E_ENTRY     = 80000000
E_PHOFF     = 40
E_SHOFF     = 2270
E_FLAGS     = 0
E_EHSIZE    = 64
E_PHENTSIZE = 56
E_PHNUM     = 2
E_SHENTSIZE = 64
E_SHNUM     = 6
E_SHSTRNDX  = 5

無事に切り取ることができた。

Circuit IRコンパイラのCIRCTを試す

f:id:msyksphinz:20200730230239p:plain

"CIRCT"は"Circuit IR Compiler and Tools"の略称で、MLIRを用いた回路構成を生成するコンパイラツールである。

https://github.com/llvm/circt

ソースコードが構成されており、面白そうなのでダウンロードしてビルドしてみることにした。

まずはソースコードのダウンロードを行う。

$ cd ${HOME}/work/llvm/circt
$ git clone git@github.com:circt/circt.git
$ git submodule init
$ git submodule update

LLVM/MLIRモジュールのビルドを行う。

$ cd circt
$ mkdir llvm/build
$ cd llvm/build
$ cmake -G Ninja ../llvm -DLLVM_ENABLE_PROJECTS="mlir" -DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
    -DLLVM_ENABLE_ASSERTIONS=ON -DCMAKE_BUILD_TYPE=DEBUG
$ ninja
$ ninja check-mlir

CIRCTのビルドを行う。

$ mkdir circt/build
$ cd circt/build
$ cmake -G Ninja .. -DMLIR_DIR=${HOME}/work/llvm/circt/llvm/build/lib/cmake/mlir/ \
    -DLLVM_DIR=${HOME}/work/llvm/circt/llvm/build/lib/cmake/llvm \
    -DLLVM_ENABLE_ASSERTIONS=ON -DCMAKE_BUILD_TYPE=DEBUG
$ ninja
$ ninja check-circt

すでに限定的ではあるが、FIRRTLをParseしてVerilogを生成することができる。試してみよう。

$ cd bin/test/EmitVerilog/Output
ls -1
verilog-basic.fir.script
verilog-errors.mlir.script
verilog-rtl-dialect.mlir.script
verilog-weird.mlir.script

いくつかのFIRファイルが配置している。verilog-basic.fir.scriptがFIRをコンパイルするためのスクリプトになっている。

set -o pipefail;{ : 'RUN: at line 1';   /home/msyksphinz/work/llvm/circt/build/bin/circt-translate -parse-fir --mlir-print-debuginfo /home/msyksphinz/work/llvm/circt/test/EmitVerilog/verilog-basic.fir | /home/msyksphinz/work/llvm/circt/build/bin/circt-translate -emit-verilog -verify-diagnostics | /home/msyksphinz/work/llvm/circt/llvm/build/bin/FileCheck /home/msyksphinz/work/llvm/circt/test/EmitVerilog/verilog-basic.fir --strict-whitespace; }

これに基づいて、自分でもCIRCTを動かしてみよう。

$ ${BUILD}/bin/circt-translate -parse-fir --mlir-print-debuginfo \
    ${CIRCT}/test/EmitVerilog/verilog-basic.fir | \
    ${BUILD}/bin/circt-translate -emit-verilog -verify-diagnostics

おお、Verilogファイルが生成できた。しかも超絶速い。FIRRTLなど話にならないほど高速だ。

...
  always @(posedge _M_write_clk) begin
    if (_M_write_en & _M_write_mask) begin
      _M_id[_M_write_addr] <= _M_write_data_id; // Decoupled.scala:209:24
      _M_other[_M_write_addr] <= _M_write_data_other;   // Decoupled.scala:209:24
    end
  end // always @(posedge)
endmodule

module Attach(
  input a, b, c);


  `ifndef SYNTHESIS
    alias a = b = c;    // Place.scala:101
  `endif
  `ifdef SYNTHESIS
    assign a = b;       // Place.scala:101
    assign a = c;       // Place.scala:101
    assign b = a;       // Place.scala:101
    assign b = c;       // Place.scala:101
    assign c = a;       // Place.scala:101
    assign c = b;       // Place.scala:101
  `endif // SYNTHESIS
endmodule

module IsInvalid(
  output a);

endmodule

module Locations(
  input  [3:0] a,
  output [3:0] b);
...

QEMUのTCG(Tiny Code Generator)を読み解く(2. TCGが生成したx86機械語を読む)

TCGの続き。TCGによるエンコードをもっと詳しく見るために、最適化を抑制すべくいろいろ変更してみた。

git diff tcg/i386/tcg-target.inc.c
diff --git a/tcg/i386/tcg-target.inc.c b/tcg/i386/tcg-target.inc.c
index ec083bddcf..d86ccdf05b 100644
--- a/tcg/i386/tcg-target.inc.c
+++ b/tcg/i386/tcg-target.inc.c
@@ -40,6 +40,8 @@ static const char * const tcg_target_reg_names[TCG_TARGET_NB_REGS] = {
 };
 #endif

+void tgen_arithr(TCGContext *s, int subop, int dest, int src);
+
 static const int tcg_target_reg_alloc_order[] = {
 #if TCG_TARGET_REG_BITS == 64
     TCG_REG_RBP,
@@ -820,7 +822,7 @@ static inline void tcg_out_vex_modrm_pool(TCGContext *s, int opc, int r)
 }

 /* Generate dest op= src.  Uses the same ARITH_* codes as tgen_arithi.  */
-static inline void tgen_arithr(TCGContext *s, int subop, int dest, int src)
+void tgen_arithr(TCGContext *s, int subop, int dest, int src)
 {
     /* Propagate an opcode prefix, such as P_REXW.  */
     int ext = subop & ~0x7;

i386ターゲットのtgen_arithr()static inlineを削除して独立するようにした。QEMUはビルドにWarningが入ると速攻で落ちるので慎重に作業する。

これでビルドしてtgen_arithr()のオブジェクトコードを見てみる。

000000000041f421 <tgen_arithr>:
  41f421:       55                      push   %rbp
  41f422:       48 89 e5                mov    %rsp,%rbp
  41f425:       48 83 ec 30             sub    $0x30,%rsp
  41f429:       48 89 7d e8             mov    %rdi,-0x18(%rbp)
  41f42d:       89 75 e4                mov    %esi,-0x1c(%rbp)
  41f430:       89 55 e0                mov    %edx,-0x20(%rbp)
  41f433:       89 4d dc                mov    %ecx,-0x24(%rbp)
  41f436:       8b 45 e4                mov    -0x1c(%rbp),%eax
  41f439:       83 e0 f8                and    $0xfffffff8,%eax
  41f43c:       89 45 fc                mov    %eax,-0x4(%rbp)
  41f43f:       83 65 e4 07             andl   $0x7,-0x1c(%rbp)
  41f443:       8b 45 e4                mov    -0x1c(%rbp),%eax
  41f446:       8d 14 c5 00 00 00 00    lea    0x0(,%rax,8),%edx
  41f44d:       8b 45 fc                mov    -0x4(%rbp),%eax
  41f450:       01 d0                   add    %edx,%eax
  41f452:       8d 70 03                lea    0x3(%rax),%esi
  41f455:       8b 4d dc                mov    -0x24(%rbp),%ecx
  41f458:       8b 55 e0                mov    -0x20(%rbp),%edx
  41f45b:       48 8b 45 e8             mov    -0x18(%rbp),%rax
  41f45f:       48 89 c7                mov    %rax,%rdi
  41f462:       e8 ec f8 ff ff          callq  41ed53 <tcg_out_modrm>
  41f467:       90                      nop
  41f468:       c9                      leaveq
  41f469:       c3                      retq

なるほど、さっぱりわからんぞ。というかhelpを見ていたらいろいろトレースオプションがあることに気が付いた。

$ qemu-system-riscv64 --machine virt --d help --nographic --trace enable=myriscvx_trap --kernel rv64ui-p-simple
out_asm         show generated host assembly code for each compiled TB
in_asm          show target assembly code for each compiled TB
op              show micro ops for each compiled TB
op_opt          show micro ops after optimization
op_ind          show micro ops before indirect lowering
int             show interrupts/exceptions in short format
exec            show trace before each executed TB (lots of logs)
cpu             show CPU registers before entering a TB (lots of logs)
fpu             include FPU registers in the 'cpu' logging
mmu             log MMU-related activities
pcall           x86 only: show protected mode far calls/returns/exceptions
cpu_reset       show CPU state before CPU resets
unimp           log unimplemented functionality
guest_errors    log when the guest OS does something invalid (eg accessing a
non-existent register)
page            dump pages at beginning of user mode emulation
nochain         do not chain compiled TBs so that "exec" and "cpu" show
complete traces
strace          log every user-mode syscall, its input, and its result
trace:PATTERN   enable trace events

なるほど、ここでは、in_asm,op,op_opt,op_ind,out_asmが有益そうだ。

$ qemu-system-riscv64 --machine virt --d in_asm,op,op_opt,op_ind,out_asm --nographic \
    --trace enable=myriscvx_trap --kernel rv64ui-p-simple 2>&1 | tee qemu.myriscvx64.log
IN:
Priv: 3; Virt: 0
0x0000000000001000:  00000297          auipc           t0,0            # 0x1000
0x0000000000001004:  02028593          addi            a1,t0,32
0x0000000000001008:  f1402573          csrrs           a0,mhartid,zero

OUT: [size=112]
0x7fdeec000100:  8b 5d f0                 movl     -0x10(%rbp), %ebx
0x7fdeec000103:  85 db                    testl    %ebx, %ebx
0x7fdeec000105:  0f 8c 4b 00 00 00        jl       0x7fdeec000156
0x7fdeec00010b:  48 c7 45 28 00 10 00 00  movq     $0x1000, 0x28(%rbp)
0x7fdeec000113:  48 c7 45 58 20 10 00 00  movq     $0x1020, 0x58(%rbp)
0x7fdeec00011b:  48 c7 85 00 01 00 00 08  movq     $0x1008, 0x100(%rbp)
0x7fdeec000123:  10 00 00
0x7fdeec000126:  c7 85 8c f8 ff ff 01 00  movl     $1, -0x774(%rbp)
0x7fdeec00012e:  00 00
0x7fdeec000130:  48 8b fd                 movq     %rbp, %rdi
0x7fdeec000133:  33 f6                    xorl     %esi, %esi
0x7fdeec000135:  ba 14 0f 00 00           movl     $0xf14, %edx
0x7fdeec00013a:  33 c9                    xorl     %ecx, %ecx
0x7fdeec00013c:  ff 15 26 00 00 00        callq    *0x26(%rip)
0x7fdeec000142:  48 89 45 50              movq     %rax, 0x50(%rbp)
0x7fdeec000146:  48 c7 85 00 01 00 00 0c  movq     $0x100c, 0x100(%rbp)
0x7fdeec00014e:  10 00 00
0x7fdeec000151:  e9 c0 fe ff ff           jmp      0x7fdeec000016
0x7fdeec000156:  48 8d 05 e6 fe ff ff     leaq     -0x11a(%rip), %rax
0x7fdeec00015d:  e9 b6 fe ff ff           jmp      0x7fdeec000018
0x7fdeec000162:  90                       nop
0x7fdeec000163:  90                       nop
0x7fdeec000164:  90                       nop
0x7fdeec000165:  90                       nop
0x7fdeec000166:  90                       nop
0x7fdeec000167:  90                       nop
0x7fdeec000168:  .quad  0x00000000004c30fe

なるほど、上記のRISC-V3命令がこのx86命令に変換されるらしい。これはさっぱりわからんぞ。

という訳でもう少し簡単なプログラムを作ってみようと思った。

  • simple_asm.S
    .section    .text
_start:
    addi    x1, x0, 10
    addi    x2, x1, 11
    addi    x3, x2, 12
    addi    x4, x3, 13
    addi    x5, x4, 14
    addi    x6, x5, 15
    addi    x7, x6, 16
    addi    x8, x7, 17
    addi    x9, x8, 18
    addi    x10, x9, 19
    addi    x11, x10, 20
    addi    x12, x11, 21
    addi    x13, x12, 22
    addi    x14, x13, 23
    addi    x15, x14, 24
    addi    x16, x15, 25
    addi    x17, x16, 26
    addi    x18, x17, 27
    addi    x19, x18, 28
    addi    x20, x20, 29

main:

finish_loop:
    j   finish_loop

ひたすら加算を行う。これをコンパイルしたらひたすらx86の加算命令が生成されるはずだ。

$ riscv64-unknown-elf-as simple_asm.S -o simple_asm.o
$ riscv64-unknown-elf-ld simple_asm.o -o simple_asm -T ../../riscv-tools/riscv-tests/benchmarks/common/test.ld

実行してみる。

$ qemu-system-riscv64 --machine virt --d in_asm,op,op_opt,op_ind,out_asm \
    --nographic --trace enable=myriscvx_trap --kernel simple_asm 2>&1 | tee qemu.myriscvx64.log

これが入力アセンブリコード

----------------
IN:
Priv: 3; Virt: 0
0x0000000080000000:  00a00093          addi            ra,zero,10
0x0000000080000004:  00b08113          addi            sp,ra,11
0x0000000080000008:  00c10193          addi            gp,sp,12
0x000000008000000c:  00d18213          addi            tp,gp,13
0x0000000080000010:  00e20293          addi            t0,tp,14
0x0000000080000014:  00f28313          addi            t1,t0,15
0x0000000080000018:  01030393          addi            t2,t1,16
0x000000008000001c:  01138413          addi            s0,t2,17
0x0000000080000020:  01240493          addi            s1,s0,18
0x0000000080000024:  01348513          addi            a0,s1,19
0x0000000080000028:  01450593          addi            a1,a0,20
0x000000008000002c:  01558613          addi            a2,a1,21
0x0000000080000030:  01660693          addi            a3,a2,22
0x0000000080000034:  01768713          addi            a4,a3,23
0x0000000080000038:  01870793          addi            a5,a4,24
0x000000008000003c:  01978813          addi            a6,a5,25
0x0000000080000040:  01a80893          addi            a7,a6,26
0x0000000080000044:  01b88913          addi            s2,a7,27
0x0000000080000048:  01c90993          addi            s3,s2,28
0x000000008000004c:  01da0a13          addi            s4,s4,29
0x0000000080000050:  0000006f          j               0               # 0x80000050
  • 中間のTiny Code。なるほど、新しい変数を定義しては定数加算を繰り返していることが分かる。
OP:
 ld_i32 tmp0,env,$0xfffffffffffffff0
 movi_i32 tmp1,$0x0
 brcond_i32 tmp0,tmp1,lt,$L0

 ---- 0000000080000000
 movi_i64 tmp2,$0x0
 movi_i64 tmp3,$0xa
 add_i64 tmp2,tmp2,tmp3
 mov_i64 x1/ra,tmp2

 ---- 0000000080000004
 mov_i64 tmp2,x1/ra
 movi_i64 tmp3,$0xb
 add_i64 tmp2,tmp2,tmp3
 mov_i64 x2/sp,tmp2

 ---- 0000000080000008
 mov_i64 tmp2,x2/sp
 movi_i64 tmp3,$0xc
 add_i64 tmp2,tmp2,tmp3
 mov_i64 x3/gp,tmp2

 ---- 000000008000000c
 mov_i64 tmp2,x3/gp
 movi_i64 tmp3,$0xd
 add_i64 tmp2,tmp2,tmp3
...
  • 驚いたことにこの後Tiny Codeに最適化が実行される。定数伝搬が検出されすべてのレジスタ割り当てがそのまま即値代入に置き換えられる。
OP after optimization and liveness analysis:
 ld_i32 tmp0,env,$0xfffffffffffffff0      dead: 1  pref=0xffff
 movi_i32 tmp1,$0x0                       pref=0xffff
 brcond_i32 tmp0,tmp1,lt,$L0              dead: 0 1

 ---- 0000000080000000
 movi_i64 tmp2,$0xa                       pref=0xffff
 mov_i64 x1/ra,tmp2                       sync: 0  dead: 0 1  pref=0xffff

 ---- 0000000080000004
 movi_i64 tmp2,$0x15                      pref=0xffff
 mov_i64 x2/sp,tmp2                       sync: 0  dead: 0 1  pref=0xffff

 ---- 0000000080000008
 movi_i64 tmp2,$0x21                      pref=0xffff
 mov_i64 x3/gp,tmp2                       sync: 0  dead: 0 1  pref=0xffff

 ---- 000000008000000c
 ...
  • 生成されたx86機械語。movqって定数移動のことか(x86だから定数移動以外もできるんだろうけど)。綺麗に依存関係が消されている。少し悔しい。
OUT: [size=236]
0x7fbd1c0003c0:  8b 5d f0                 movl     -0x10(%rbp), %ebx
0x7fbd1c0003c3:  85 db                    testl    %ebx, %ebx
0x7fbd1c0003c5:  0f 8c d5 00 00 00        jl       0x7fbd1c0004a0
0x7fbd1c0003cb:  48 c7 45 08 0a 00 00 00  movq     $0xa, 8(%rbp)
0x7fbd1c0003d3:  48 c7 45 10 15 00 00 00  movq     $0x15, 0x10(%rbp)
0x7fbd1c0003db:  48 c7 45 18 21 00 00 00  movq     $0x21, 0x18(%rbp)
0x7fbd1c0003e3:  48 c7 45 20 2e 00 00 00  movq     $0x2e, 0x20(%rbp)
0x7fbd1c0003eb:  48 c7 45 28 3c 00 00 00  movq     $0x3c, 0x28(%rbp)
0x7fbd1c0003f3:  48 c7 45 30 4b 00 00 00  movq     $0x4b, 0x30(%rbp)
0x7fbd1c0003fb:  48 c7 45 38 5b 00 00 00  movq     $0x5b, 0x38(%rbp)
0x7fbd1c000403:  48 c7 45 40 6c 00 00 00  movq     $0x6c, 0x40(%rbp)
0x7fbd1c00040b:  48 c7 45 48 7e 00 00 00  movq     $0x7e, 0x48(%rbp)
0x7fbd1c000413:  48 c7 45 50 91 00 00 00  movq     $0x91, 0x50(%rbp)
0x7fbd1c00041b:  48 c7 45 58 a5 00 00 00  movq     $0xa5, 0x58(%rbp)
0x7fbd1c000423:  48 c7 45 60 ba 00 00 00  movq     $0xba, 0x60(%rbp)
0x7fbd1c00042b:  48 c7 45 68 d0 00 00 00  movq     $0xd0, 0x68(%rbp)
0x7fbd1c000433:  48 c7 45 70 e7 00 00 00  movq     $0xe7, 0x70(%rbp)
0x7fbd1c00043b:  48 c7 45 78 ff 00 00 00  movq     $0xff, 0x78(%rbp)
0x7fbd1c000443:  48 c7 85 80 00 00 00 18  movq     $0x118, 0x80(%rbp)
0x7fbd1c00044b:  01 00 00
0x7fbd1c00044e:  48 c7 85 88 00 00 00 32  movq     $0x132, 0x88(%rbp)
0x7fbd1c000456:  01 00 00
0x7fbd1c000459:  48 c7 85 90 00 00 00 4d  movq     $0x14d, 0x90(%rbp)
0x7fbd1c000461:  01 00 00
0x7fbd1c000464:  48 c7 85 98 00 00 00 69  movq     $0x169, 0x98(%rbp)
0x7fbd1c00046c:  01 00 00
0x7fbd1c00046f:  48 8b 9d a0 00 00 00     movq     0xa0(%rbp), %rbx
0x7fbd1c000476:  48 83 c3 1d              addq     $0x1d, %rbx
0x7fbd1c00047a:  48 89 9d a0 00 00 00     movq     %rbx, 0xa0(%rbp)
0x7fbd1c000481:  66 90                    nop
0x7fbd1c000483:  e9 00 00 00 00           jmp      0x7fbd1c000488
0x7fbd1c000488:  bb 50 00 00 80           movl     $0x80000050, %ebx
0x7fbd1c00048d:  48 89 9d 00 02 00 00     movq     %rbx, 0x200(%rbp)
0x7fbd1c000494:  48 8d 05 65 fe ff ff     leaq     -0x19b(%rip), %rax
0x7fbd1c00049b:  e9 78 fb ff ff           jmp      0x7fbd1c000018
0x7fbd1c0004a0:  48 8d 05 5c fe ff ff     leaq     -0x1a4(%rip), %rax
0x7fbd1c0004a7:  e9 6c fb ff ff           jmp      0x7fbd1c000018
f:id:msyksphinz:20200729013316p:plain

QEMUのTCG(Tiny Code Generator)を読み解く

QEMUは高速なエミュレーションが可能な理由の一つに、TCGを使った高速なバイナリ変換機構がある。TCGの役割は、ターゲットバイナリからTCG(Tiny Code Generator)を用いた中間表現に変換し、ホスト形式のバイナリに変換する2つの機構が存在している。

f:id:msyksphinz:20200727012347p:plain:w500

TCGがどのようにして高速なエミュレーションを実現しているのかについて調査している。ターゲットコードをTCGに変換する手法については、target/riscv/translate.cを解析してきたので何となくわかるとして、TCGからどのようにホストのバイナリに変換する方法について見ていこうと思う。

まずはTCGについてはtcg/tcg.cに多くのオペレーションの定義がなされている。一方で、各ホストのTCGからの変換ポリシについてはtcg/[target-arch]に定義がある。一番安定してそうなtcg/i386/tcg-target.inc.cを見てみることにする。

  • qemu/tcg/i386/tcg-target.inc.c
#ifdef CONFIG_DEBUG_TCG
static const char * const tcg_target_reg_names[TCG_TARGET_NB_REGS] = {
#if TCG_TARGET_REG_BITS == 64
    "%rax", "%rcx", "%rdx", "%rbx", "%rsp", "%rbp", "%rsi", "%rdi",
#else
    "%eax", "%ecx", "%edx", "%ebx", "%esp", "%ebp", "%esi", "%edi",
#endif
...

x86の命令定義についてはあまり詳しくないのだが、おそらくこの辺がx86の機械語に相当しているのだろう。

#define OPC_ARITH_EvIz  (0x81)
#define OPC_ARITH_EvIb (0x83)
#define OPC_ARITH_GvEv (0x03)     /* ... plus (ARITH_FOO << 3) */
#define OPC_ANDN        (0xf2 | P_EXT38)
#define OPC_ADD_GvEv   (OPC_ARITH_GvEv | (ARITH_ADD << 3))
#define OPC_AND_GvEv    (OPC_ARITH_GvEv | (ARITH_AND << 3))
#define OPC_BLENDPS     (0x0c | P_EXT3A | P_DATA16)
...

その証拠に、RISC-V側の定義は機械語に一致している(RISC-Vの機械語なら一瞬で理解できるのだ)。

typedef enum {
    OPC_ADD = 0x33,
    OPC_ADDI = 0x13,
    OPC_AND = 0x7033,
    OPC_ANDI = 0x7013,
    OPC_AUIPC = 0x17,
    OPC_BEQ = 0x63,
    OPC_BGE = 0x5063,
    OPC_BGEU = 0x7063,
...

この知識を前提にして、TCGからバイナリを生成するフローを追いかけていく。

  • qemu/tcg/tcg.c
int tcg_gen_code(TCGContext *s, TranslationBlock *tb)
{
#ifdef CONFIG_PROFILER
    TCGProfile *prof = &s->prof;
#endif
    int i, num_insns;
...
    QTAILQ_FOREACH(op, &s->ops, link) {
        TCGOpcode opc = op->opc;

#ifdef CONFIG_PROFILER
        atomic_set(&prof->table_op_count[opc], prof->table_op_count[opc] + 1);
#endif

        switch (opc) {
        case INDEX_op_mov_i32:
        case INDEX_op_mov_i64:
        case INDEX_op_mov_vec:
...
        default:
            /* Sanity check that we've not introduced any unhandled opcodes. */
            tcg_debug_assert(tcg_op_supported(opc));
            /* Note: in order to speed up the code, it would be much
               faster to have specialized register allocator functions for
               some common argument patterns */
            tcg_reg_alloc_op(s, op);
            break;
        }
#ifdef CONFIG_DEBUG_TCG
...

まあ最初はtcg_reg_alloc_op()から読んでいけばよかろう。これはおそらくレジスタに書き込みを行う命令だ(reg_alloc_opなので)。

で、tcg_reg_alloc()のコードの大半はとりあえず無視して、実際に命令を生成するとところだけ取り出すとここだ。

static void tcg_reg_alloc_op(TCGContext *s, const TCGOp *op)
{
    const TCGLifeData arg_life = op->life;
    const TCGOpDef * const def = &tcg_op_defs[op->opc];
    TCGRegSet i_allocated_regs;
...
    /* emit instruction */
    if (def->flags & TCG_OPF_VECTOR) {
        tcg_out_vec_op(s, op->opc, TCGOP_VECL(op), TCGOP_VECE(op),
                       new_args, const_args);
    } else {
        tcg_out_op(s, op->opc, new_args, const_args);
    }

tcg_out_op()は各アーキテクチャで実装が委ねられている。

  • qemu/tcg/i386/tcg-target.inc.c
static inline void tcg_out_op(TCGContext *s, TCGOpcode opc,
                              const TCGArg *args, const int *const_args)
{
    TCGArg a0, a1, a2;
    int c, const_a2, vexop, rexw = 0;

#if TCG_TARGET_REG_BITS == 64
# define OP_32_64(x) \
        case glue(glue(INDEX_op_, x), _i64): \
            rexw = P_REXW; /* FALLTHRU */    \
        case glue(glue(INDEX_op_, x), _i32)
...
                
    OP_32_64(add2):
        if (const_args[4]) {
            tgen_arithi(s, ARITH_ADD + rexw, a0, args[4], 1);
        } else {
            tgen_arithr(s, ARITH_ADD + rexw, a0, args[4]);
        }
        if (const_args[5]) {

っとなんだかややこしいのでRISC-V側を見てみる。

  • qemu/tcg/i386/tcg-target.inc.c
static void tcg_out_op(TCGContext *s, TCGOpcode opc,
                       const TCGArg *args, const int *const_args)
{
    TCGArg a0 = args[0];
    TCGArg a1 = args[1];
    TCGArg a2 = args[2];
...
    switch (opc) {
            ...
    case INDEX_op_add_i64:
        if (c2) {
            tcg_out_opc_imm(s, OPC_ADDI, a0, a1, a2);
        } else {
            tcg_out_opc_reg(s, OPC_ADD, a0, a1, a2);
        }
        break;

このtcg_out_opc_reg()というのがまさにRISC-V向けバイナリを生成するルーチンだ。

/*
 * RISC-V instruction emitters
 */

static void tcg_out_opc_reg(TCGContext *s, RISCVInsn opc,
                            TCGReg rd, TCGReg rs1, TCGReg rs2)
{
    tcg_out32(s, encode_r(opc, rd, rs1, rs2));
}

このencode_r()が機械語エンコーディングを作り上げている。

/*
 * RISC-V immediate and instruction encoders (excludes 16-bit RVC)
 */

/* Type-R */

static int32_t encode_r(RISCVInsn opc, TCGReg rd, TCGReg rs1, TCGReg rs2)
{
    return opc | (rd & 0x1f) << 7 | (rs1 & 0x1f) << 15 | (rs2 & 0x1f) << 20;
}

これにより機械語バイナリが作られるという訳だ。つまり、このTCGから機械語への変換ルーチンは魔法でもなんでもなく、TCGのオペレーションをパタンマッチングさせているということだ。

QEMUに入門してみる(20. CSRレジスタの追加)

QEMU実装の続き。CSR命令の実装が足りていないので、いくつか主要なものを実装していく。現時点ではmhartidしか実装していないので追加していく。csr.cにはCSR命令追加のテンプレートが用意されているのでこれを使っていく。

  • qemu/target/myriscvx/csr.c
/* Control and Status Register function table */
static myriscvx_csr_operations csr_ops[CSR_TABLE_SIZE] = {
    /* Machine Information Registers */
    [CSR_MVENDORID] =           { any,  read_zero                           },
    [CSR_MARCHID] =             { any,  read_zero                           },
    [CSR_MIMPID] =              { any,  read_zero                           },
    [CSR_MHARTID] =             { any,  read_mhartid                        },

    /* Machine Trap Setup */
    [CSR_MSTATUS] =             { any,  read_mstatus,     write_mstatus     },
    [CSR_MISA] =                { any,  read_misa,        write_misa        },
    [CSR_MIDELEG] =             { any,  read_mideleg,     write_mideleg     },
    [CSR_MEDELEG] =             { any,  read_medeleg,     write_medeleg     },
    [CSR_MIE] =                 { any,  read_mie,         write_mie         },
    [CSR_MTVEC] =               { any,  read_mtvec,       write_mtvec       }
};

CSR読み込み用と、CSR書き込み用のコードが用意されている。

  • qemu/target/myriscvx/csr.c
static int read_mstatus(CPUMYRISCVXState *env, int csrno, target_ulong *val)
{
  *val = env->mstatus;
  return 0;
}
static int write_mstatus(CPUMYRISCVXState *env, int csrno, target_ulong val)
{
  target_ulong mstatus = env->mstatus;
  target_ulong mask = 0;
  int dirty;

  /* flush tlb on mstatus fields that affect VM */
  if ((val ^ mstatus) & (MSTATUS_MXR | MSTATUS_MPP | MSTATUS_MPV |
                         MSTATUS_MPRV | MSTATUS_SUM)) {
    tlb_flush(env_cpu(env));
  }
  mask = MSTATUS_SIE | MSTATUS_SPIE | MSTATUS_MIE | MSTATUS_MPIE |
    MSTATUS_SPP | MSTATUS_FS | MSTATUS_MPRV | MSTATUS_SUM |
    MSTATUS_MPP | MSTATUS_MXR | MSTATUS_TVM | MSTATUS_TSR |
    MSTATUS_TW;
#if defined(TARGET_RISCV64)
  /*
   * RV32: MPV and MTL are not in mstatus. The current plan is to
   * add them to mstatush. For now, we just don't support it.
   */
  mask |= MSTATUS_MTL | MSTATUS_MPV;
#endif

  mstatus = (mstatus & ~mask) | (val & mask);

  dirty = ((mstatus & MSTATUS_FS) == MSTATUS_FS) |
    ((mstatus & MSTATUS_XS) == MSTATUS_XS);
  mstatus = set_field(mstatus, MSTATUS_SD, dirty);
  env->mstatus = mstatus;

  return 0;
}

とりあえずMYRISCVXの実装にそれらを追加していく。RISC-Vの実装からとりあえずパクってくることにした。

これらのCSR命令の操作はmyriscvx_csrrw()から呼び出される。

int myriscvx_csrrw(CPUMYRISCVXState *env, int csrno, target_ulong *ret_value,
                   target_ulong new_value, target_ulong write_mask)
{
  int ret;
  target_ulong old_value;
...
  /* execute combined read/write operation if it exists */
  if (csr_ops[csrno].op) {
    return csr_ops[csrno].op(env, csrno, ret_value, new_value, write_mask);
  }

  /* if no accessor exists then return failure */
  if (!csr_ops[csrno].read) {
    return -1;
  }

  /* read old value */
  ret = csr_ops[csrno].read(env, csrno, &old_value);
  if (ret < 0) {
    return ret;
  }
  ....

ここまででさらにテストベクタが進むようになった。次はシフト演算命令かな?

IN:
Priv: 3; Virt: 0
0x00000000800000a8:  00100513          addi            a0,zero,1
0x00000000800000ac:  01f51513          slli            a0,a0,31

19397@1595696172.703149:myriscvx_trap hart:0, async:0, cause:2, epc:0x800000ac, tval:0x0, desc=illegal_instruction
----------------
IN:
Priv: 3; Virt: 0
0x0000000080000004:  34202f73          csrrs           t5,mcause,zero

19397@1595696172.703218:myriscvx_trap hart:0, async:0, cause:2, epc:0x80000004, tval:0x0, desc=illegal_instruction
19397@1595696172.703246:myriscvx_trap hart:0, async:0, cause:2, epc:0x80000004, tval:0x0, desc=illegal_instruction
19397@1595696172.703253:myriscvx_trap hart:0, async:0, cause:2, epc:0x80000004, tval:0x0, desc=illegal_instruction
19397@1595696172.703259:myriscvx_trap hart:0, async:0, cause:2, epc:0x80000004, tval:0x0, desc=illegal_instruction
19397@1595696172.703265:myriscvx_trap hart:0, async:0, cause:2, epc:0x80000004, tval:0x0, desc=illegal_instruction