FPGA開発日記

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

Binary Translation型エミュレータを作る(ロードストア命令のMMU実装)

Binary Translation方式の命令セットエミュレータのRust実装をしている。前回まででデバッグ機能は作り切ったので、いよいよより複雑な機能の実装に着手する。まず手を付けなければならないのは仮想アドレスのサポート。前回までにとりあえずRustによる仮想アドレス変換の実装を行った。仮想アドレス変換はロードストア命令に対しても実装しなければならない。しかし現在のTCGの構成のままロードストア命令にアドレス変換を実装するといきなり実装が複雑になりデバッグが大変になるので、一度ロードストア命令をすべてヘルパー関数による実装に変更し、メモリアクセスをすべてヘルパー関数に置き換えることにした。これによりメモリアクセスのエミュレーション速度は著しく低下するが、確実にデバッグできる環境になるのでこれでまずはテストパタンを通していきたい。

f:id:msyksphinz:20201005124734p:plain

これまで、メモリアクセス命令は以下のようなTCGを生成していた。

    pub fn translate_ld(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_rri(TCGOpcode::LOAD_64BIT, inst)
    }
    pub fn translate_lw(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_rri(TCGOpcode::LOAD_32BIT, inst)
    }

単純なTCGであるが、TCGOpcode::LOAD_64BITなどのオペコードはx86への変換部分でネイティブなコードに落とし込んでいた。

    fn tcg_gen_load(
        emu: &EmuEnv,
        pc_address: u64,
        tcg: &TCGOp,
        mc: &mut Vec<u8>,
        mem_size: MemOpType,
        target_reg: RegisterType,
    ) -> usize {
        let mut gen_size: usize = pc_address as usize;

        let arg0 = tcg.arg0.unwrap();
...
        // Load Guest Memory Head into EAX
        let guestcode_addr = emu.calc_guest_data_mem_address();
        gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RAX, guestcode_addr as u64, mc);

        // Move Guest Memory from EAX to ECX
        gen_size += Self::tcg_modrm_64bit_out(
            X86Opcode::MOV_GV_EV,
            X86ModRM::MOD_11_DISP_RAX,
            X86TargetRM::RCX,
            mc,
        );

        // Load value from rs1
        gen_size += Self::tcg_modrm_64bit_out(
            X86Opcode::MOV_GV_EV,
            X86ModRM::MOD_10_DISP_RBP,
            X86TargetRM::RAX,
            mc,
        );
        gen_size += Self::tcg_out(emu.calc_gpr_relat_address(arg1.value) as u64, 4, mc);
...

これを、以下のようにTCGへの変換部分をヘルパー関数呼び出しに置き換えてしまった。

    pub fn translate_ld(inst: &InstrInfo) -> Vec<TCGOp> {
        let rs1_addr: usize = get_rs1_addr!(inst.inst) as usize;
        let imm_const: u64 = ((inst.inst as i32) >> 20) as u64;
        let rd_addr: usize = get_rd_addr!(inst.inst) as usize;

        let rs1 = Box::new(TCGv::new_reg(rs1_addr as u64));
        let imm = Box::new(TCGv::new_imm(imm_const));
        let rd = Box::new(TCGv::new_reg(rd_addr as u64));

        let op = TCGOp::new_helper_call_arg3(CALL_HELPER_IDX::CALL_LOAD64_IDX as usize, *rd, *rs1, *imm);
        vec![op]
    }

ヘルパー関数は、この例ではCALL_LOAD64_IDXを呼び出している。これは以下のように実装している。

    pub fn helper_func_load16(
        emu: &mut EmuEnv,
        rd: u32,
        rs1: u32,
        imm: u32,
        _dummy: u32,
    ) -> usize {
        let rs1_data = emu.m_regs[rs1 as usize];
        let addr = rs1_data.wrapping_add(imm as i32 as u64);

        #[allow(unused_assignments)]
        let mut guest_phy_addr:u64 = 0;
        match emu.convert_physical_address(addr, MemAccType::Read) {
            Ok(addr) => guest_phy_addr = addr,
            Err(error) => {
                panic!("Read Error: {:?}\n", error);
            }
        };
        emu.m_regs[rd as usize] = emu.read_mem_4byte(guest_phy_addr) as u64;
        return 0;
    }

もろにRust実装を呼び出している。convert_physical_address()は前回実装したものだ。ネイティブコードを使わずRustの実装により物理アドレスを求め、メモリアクセスを行ってその結果をレジスタに代入する。

まずはこの実装でデグレードが無いことを確認する。

$ cargo run -- --debug --dump-gpr --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-p-add
Guest PC Address = 80000040
<Convert_Virtual_Address. virtual_addr=0000000080000040 : vm_mode = 0, priv_mode = 3>
  converted physical address = 80000040
  00001f17 : auipc   t5, 0x1
<Convert_Virtual_Address. virtual_addr=0000000080000044 : vm_mode = 0, priv_mode = 3>
  converted physical address = 80000044
  fc3f2023 : sw      gp, -64(t5)
<Convert_Virtual_Address. virtual_addr=0000000080000048 : vm_mode = 0, priv_mode = 3>
  converted physical address = 80000048
  ff9ff06f : j       pc + 0xffff8
tb_address  = 0x7ffa59cb0000

上手く行っているようだ。次は仮想アドレスの確認。

$ cargo run -- --debug --dump-gpr --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-v-add
x20 = 0000000000000000  x21 = 0000000000000000  x22 = 0000000000000000  x23 = 0000000000000000
x24 = 0000000000000000  x25 = 0000000000000000  x26 = 0000000000000000  x27 = 0000000000000000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

Result: MEM[0x1000] = 00000000
...

あれ、テストが成功しなかった。問題を確認していく。

Binary Translation型エミュレータを作る(MMU機能の実装検討)

Binary Translation方式の命令セットエミュレータのRust実装をしている。前回まででデバッグ機能は作り切ったので、いよいよより複雑な機能の実装に着手する。まず手を付けなければならないのは仮想アドレスのサポート。これが無いとテストパタンの大半をPASSできないし、Linuxを立ち上げることもできない。この機能について実装を考えて行く。

QEMUにおいては、MMUなどの仮想アドレスから物理アドレスへの変換機能については以下のブログ記事で確認した。これを実際に自作QEMUで実装して行くことになる。

まずは簡単に、TLBを実装せずにアドレス変換の結果をキャッシュしないバージョンを作ってみる。簡単なバージョンで正しく動作することを確認して、キャッシュすることで高速化を目指すという訳だ。

仮想アドレスから物理アドレスへの変換をRustで実装する

実はこれはすでに用意してある。大昔にRustでRISC-Vシミュレータを作った時に実装した仮想アドレスから物理アドレスへの変換ルーチンがそのまま使える。

    fn walk_page_table(&mut self, virtual_addr: u64, acc_type: MemAccType, init_level: u32, 
        ppn_idx: Vec<u8>, pte_len: Vec<u8>, pte_idx: Vec<u8>, vpn_len: Vec<u8>, vpn_idx: Vec<u8>, 
        pagesize: u32, ptesize: u32) -> Result<u64, MemResult> {
        let is_write_access = match acc_type {
            MemAccType::Write => true,
            _ => false,
        };
...

        for level in range(0, init_level).rev() {
            let va_vpn_i: u64 =
                (virtual_addr >> vpn_idx[level as usize]) & ((1 << vpn_len[level as usize]) - 1);
            pte_addr += (va_vpn_i * (ptesize as u64)) as u64;

            pte_val = self.read_mem_4byte(pte_addr) as i64;

            println!(
                "<Info: VAddr = 0x{:016x} PTEAddr = 0x{:016x} : PPTE = 0x{:08x}>",
                virtual_addr, pte_addr, pte_val
            );

            // 3. If pte:v = 0, or if pte:r = 0 and pte:w = 1, stop and raise a page-fault exception.

これと、あとは例外が発生した場合に処理をするためのGenerateException()を持ってきておく。

    pub fn generate_exception(&mut self, code: ExceptCode, tval: i64) {
        println!(
            "<Info: Generate Exception Code={}, TVAL={:016x} PC={:016x}>",
            code as u32, tval, self.m_pc[0]
        );

        let epc: u64;
        epc = self.m_pc[0];

...

この2つを使って、まずは命令フェッチのアドレス変換を実装していこう。といっても、フェッチ部分にアドレス変換の関数を挟み込むだけである。

            #[allow(while_true)]
            while true {
                #[allow(unused_assignments)]
                let mut guest_phy_pc = 0;
                match self.convert_physical_address(self.m_pc[0], MemAccType::Read) {
                    Ok(addr) => guest_phy_pc = addr,
                    Err(error) => {
                        print!("Fetch Error: {:?}\n", error);
                        continue;
                    }
                };
                print!("  converted physical address = {:08x}\n", guest_phy_pc);
                let guest_inst = self.read_mem_4byte(guest_phy_pc);
...

これで基本的には実装完了となる。ただしテストパタンを動かすためにはいくつかさらに改造を加えていかなければならない。

まず1つ目は、メモリアクセスのオフセット値の削除だ。現在のテストバイナリは命令の開始位置が0x8000_0000になっており、それよりも下のアドレスには基本的にアクセスしないようになっている。この場合アドレスを32ビット空間ですべて保持してしまうのはもったいないので、メモリテーブルを作るときは0x80000000を引き算して、オフセット値を削減してメモリサイズを抑えている。

これをしてしまった場合、命令としては0x8000_xxxxとして実行しているつもりでも、実際のアクセスアドレスは0x0000_xxxxとなってしまうためいろいろと問題が生じる。命令フェッチの部分はRustで記述しているため単純に0x8000_0000を引き算するコードを書いておけばよいが、ロードストア命令がアクセスする場所についてはそうもいかない。

どうしようかといろいろ考えたのだが、結果的にTCGからx86に変換する際に無理やり0x8000_0000を引き算する命令を挿入することにした。これだとこのTCG変換関数はRISC-V専用になってしまうが、どうせ仮想アドレス→物理アドレスをきちんと実装するとこの辺も作り直しになってしまうのでとりあえずは良いだろう。

        // Execute Load
        // GPR value + Memory Head Address
        Self::tcg_modrm_64bit_out(
            X86Opcode::ADD_GV_EV,
            X86ModRM::MOD_11_DISP_RCX,
            X86TargetRM::RAX,
            mc,
        );

        // Address Calculation : Sub Bias 0x8000_0000
        gen_size += Self::tcg_64bit_out(X86Opcode::ADD_EAX_IV, mc);
        gen_size += Self::tcg_out(0x8000_0000 as u64, 4, mc);

最後の2行が、実計算アドレスに対して0x8000_0000を減算するコードとなっている。実際、確認すると以下のようなx86命令が生成されている。

Guest PC Address = 80002914
<Convert_Virtual_Address. virtual_addr=0000000080002914 : vm_mode = 0, priv_mode = 3>
  converted physical address = 80002914
  6ed8b823 : sd      a3, 1776(a7)
tb_address  = 0x7f02e69a0000
00007F02E69A0000 48B8000099E6027F0000 movabs    $0x7F02_E699_0000,%rax
00007F02E69A000A 488BC8               mov       %rax,%rcx
00007F02E69A000D 488B8D90000000       mov       0x90(%rbp),%rcx
00007F02E69A0014 4803C1               add       %rcx,%rax
00007F02E69A0017 480500000080         add       $0xFFFF_FFFF_8000_0000,%rax
00007F02E69A001D 488B8D70000000       mov       0x70(%rbp),%rcx
00007F02E69A0024 488988F0060000       mov       %rcx,0x6F0(%rax)
00007F02E69A0000 E9DFFF0000           jmp       0x0000_7F02_E69A_FFE4

途中にadd $0xFFFF_FFFF_8000_0000,%raxという命令が挿入された。これが当該命令に相当する。

この辺まで実装できたら、一応rv64ui-v-simpleを動かして動作を確認する。

$ cargo run -- --debug --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-v-simple
Guest PC Address = 800000c0
<Convert_Virtual_Address. virtual_addr=00000000800000c0 : vm_mode = 0, priv_mode = 3>
  converted physical address = 800000c0
  10200073 : sret
tb_address  = 0x7f1fe7920000
00007F1FE7920000 48BFA89FC4C0FF7F0000 movabs    $0x7FFF_C0C4_9FA8,%rdi
00007F1FE792000A FF9558040000         callq     *0x458(%rbp)
00007F1FE7920000 E9FAFF0000           jmp       0x0000_7F1F_E792_FFFF
00007F1FE7920000 E9F5FF0000           jmp       0x0000_7F1F_E792_FFFA
helper_sret(emu) is called! PC = 000000002ac8
x00 = 0000000000000000  x01 = 0000000000000000  x02 = 0000000000000000  x03 = 0000000000000000
x04 = 0000000000000000  x05 = 0000000000000000  x06 = 0000000000000000  x07 = 0000000000000000
x08 = 0000000000000000  x09 = 0000000000000000  x10 = 0000000000000000  x11 = 0000000000000000
x12 = 0000000000000000  x13 = 0000000000000000  x14 = 0000000000000000  x15 = 0000000000000000
x16 = 0000000000000000  x17 = 0000000000000000  x18 = 0000000000000000  x19 = 0000000000000000
x20 = 0000000000000000  x21 = 0000000000000000  x22 = 0000000000000000  x23 = 0000000000000000
x24 = 0000000000000000  x25 = 0000000000000000  x26 = 0000000000000000  x27 = 0000000000000000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

SRETが実行され、スーパーバイザモードに移行する。

Guest PC Address = 00002ac8
<Convert_Virtual_Address. virtual_addr=0000000000002ac8 : vm_mode = 8, priv_mode = 0>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080003000 : PPTE = 0x20001001>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080004000 : PPTE = 0x20001801>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080006010 : PPTE = 0x00000000>
<Page Table Error : 0x0000000080006010 = 0x00000000 is not valid Page Table. Generate Exception>
<Info: Generate Exception Code=13, TVAL=0000000000002ac8 PC=0000000000002ac8>
<Info: Exception. ChangeMode from 0 to 1>
<Info: Set Program Counter = 0xffffffffffe000c4>
Fetch Error: TlbError
<Convert_Virtual_Address. virtual_addr=ffffffffffe000c4 : vm_mode = 8, priv_mode = 1>
<Info: VAddr = 0xffffffffffe000c4 PTEAddr = 0x0000000080003ff8 : PPTE = 0x20001401>
<Info: VAddr = 0xffffffffffe000c4 PTEAddr = 0x0000000080005ff8 : PPTE = 0x200000cf>

アドレス変換が始まった。仮想アドレス0x00002ac8に対してアドレス変換を行ったのだが、3番目のアドレス変換でテーブルの中身が何も入っておらず例外となった。例外により新たなプログラムカウンタが設定されたが、こちらもテーブルが用意されておらず例外となった。最終的に0x8000_00c4にジャンプしてトラップハンドラを起動している。上手く行っているようだ。

次に考えなければならないのはロードストア命令によるメモリアクセスのアドレス変換だ。少しずつやっていこう。

f:id:msyksphinz:20201003011457p:plain

MicrochipのPolarFire SoC Icicle Kitを購入した

ちょくちょく話題に上がっていた、MicrochipのPolarFire Icicle Kitを購入し、先日発送されついに我が家に到着した。

f:id:msyksphinz:20201013010855p:plain

どうも中身はSiFiveのマルチコアSoCのようで、それにFPGAを接続してSoCを構成しているようだ。 しかし詳細については正直全く調べていない。公私共に最近忙しすぎて、これについて事前勉強している暇がなかったのだ。

とりあえずやってみたいこととしては、Linuxのブートとベンチマーキングかな。後はせっかくなのでMircochip(というかMicrosemiか)のFPGAも試したい。 SoCのアクセラレータとして動かせるならば、Chiselで何かしらデザインを書いて、FPGA部にインプリして遊んでみるのも面白そうだ。

とここまで書いてふと思ったのだが、そもそもこのSoC FPGAはどういった分野がターゲットの製品なのかなあ? Brochureを眺めて見ても、なんかいろんな分野のことが書いてあってターゲットを絞っているわけでもなさそうだ。 MicrosemiというなかなかキワモノのFPGAとキワモノのRISC-V、どこがどういった分野で使用されるのだろう?

と思ったらRISC-Vの仕様書翻訳で有名なしばっちさんがMicrosemi FPGAの紹介をされていた。なるほどー、コンパクト低消費電力向け、ということね。 まあXilinxでもIntelでも、それだったら小さなFPGAを買って来ればよい、という話になるんだろうが、そもそもツールが大きすぎて使う気にならないしな... (私のPCのSSDは使用率が95%になってしまっており、もう新規のツールを入れる隙間が無いのだ)

といいつつその裏返しで、Microsemiのツールもあまりいい話を聞いたことがない。RISC-V CPUコンテストの時にMicrosemiのFPGAはツールがボロッカスだったという話も聞いているので、そのあたりどうなのかしら?という所も見てみたい。

www.slideshare.net

あとはあれだな。今私はQEMU実装にハマっているので、RISC-Vターゲット向けに私のRustコードを移植して、Raspberry-PiとRISC-Vとx86の3プラットフォーム対応にするのもありだな。なんという気の長い話だ...

「SystemVerilog入門」を入手しました

f:id:msyksphinz:20201007150457p:plain

共立出版様から「System Verilog入門:設計・仕様・検証のためのハードウェア記述言語」を献本頂きました。共立出版様、ありがとうございます。

Amazonでの発売日は2020/10/09ですが、共立出版のウェブサイトでは発売日が2020/10/12となっています。どっちが正しいのか分からないので、遅い方ということで本日このブログを公開することにしました。

私はデジタル設計が専門のハードウェア設計エンジニアで、現在も主たる業務はデジタル回路設計です。VerilogもしくはSystemVerilogを使いますし、たまにSystemVerilog以外の言語を使って設計することもあります。

しかし私のSystemVerilogの知識はあまり多くはありません。最低限必要な設計のための構文、always_ffだのalways_combだの、あとはインタフェースとかstructとか、そういうのが使える程度です。検証用の様々な機能とかもあまり使ったことがない。プログラミング言語もそうですが、あまり言語自体に興味がないのです。

という、「SystemVerilog普段使っているけどあまり詳細は知らない」という私にとって、この本はある意味で衝撃です。

まず、やたらめったら細かい。まあLRM(SystemVerilogの仕様書?)をベースにして書かれているわけですから、文法的なところまで非常に細かく書かれています。目次を見ただけでも、「なにこれそんな機能あったの」みたいな項目が多い。

とにかく、仕様書の文法に則っての解説がすごい。章立てを見ると分かりますが、「第7章:代入文」とか「第8章:オペレータと式」とか、文法に従って章分けが行われています。つまり1章から順番に読んでいけばいいという訳では無く、必要なところをピックアップしながら読み進めていく必要がありそうです。

各所の説明も淡々としており、辞書としての要素が強いです。私としては、よくよく読んでいると、「え!SystemVerilogのintegerintって違うの!!!?(integerは4値、intは2値らしい)」という驚きの発見があったりしますが、そこは特に強調されていません(あくまで淡々と)。

逆に困ってしまいました。この本を読破すればSystemVerilogの広範囲な機能をカバーできることは間違いありません。しかし、あまりにも詳しすぎてどこから手を付けていいのか分からないのです。

私はあまり頭が良くないので、こういう本を読んでも、実際に手を動かさないと見につかない場合が多いです。この本の場合は仕様の詳細にツッコんでいく場合が多く、あまり手を動かせる場所がありません。さてどうしようか。

私としてはこの本は辞書として使うのが良いかなと思っています。と言っても辞書というほどにはインデックス化されていないので、まずは全体の内容を読み込んで、そのうえで「これってどこに書いてあったかな?」みたいな「思い出しサーチ」をする必要があるでしょう1

本書は「入門」といいつつ初学者向けではない

私が最初にハードウェア記述言語(その時はVHDLだった)を学んだ時は入門書を片手にシミュレータを使ってあーだこーだと設計していましたが、その当時の私が本書を手に取ってSystemVerilogを勉強しようとしたら、間違いなく挫折しているでしょう。本書は「入門書」ではなく「最強のSystemVerilog辞書」だと思います2

SystemVerilogを初めて勉強する人が本書を手に取ることは素晴らしいことだと思います。しかしぜひもう一冊「手を動かしながらステップバイステップで学べるSystemVerilog入門書」みたいなやつをご購入ください。この本は基本を学んだ人がもう一歩(いや二~三歩)ステップアップするのを助けるのには素晴らしい本だと思います。

私のビックリポイント

これだけだとレビューとしてあまりにショボいので、各章をざっと読みながら私のビックリポイントを挙げていきますね。

  • 2.3節:プログラム。programとかいう構文があるの... これまではinitial beginでひたすらシーケンスを書いていたけど、それをサブルーチン化できるということかな。すげえ。

  • 3.6節, 3.7節:integerintって違うの!integerは4値、intは2値。どっちもハードウェアとして使えるのかな?

  • 3.13節:イベントデータタイプeventというのがあるのか。これは異なるプロセス間での同期待ちを行える。これは検証環境とかで、キューへの入力、キューからの取り出しのタイミング合わせとかで使えそうかな?
  • 4.2.2節:タグ付きユニオン。なにそれと思ったら、unionで代入に使ったデータタイプと読み出しに使ったデータタイプが異なる場合にエラーにしてくれるらしい。これ誰がうれしいの...?
    • と思ったけど、CPUのデコーダとか複数の命令種に合わせてpacked unionで作るので、デコードタイプと異なるデータ型で読みだすと変なデコード結果になっちゃうもんな。そういった場合にtaggedを付けておけば間違える事は無いかも。
  • 5.23節:インタフェースクラス。これ自体は知っていたけど、中に関数とかを埋め込んでいろいろできるんだな。バスの定義とかにうまく活用できるかもしれない。
  • 8.1.12 insideオペレータ、ナニコレこんなのあったの...?
  • 8.1.13 ビットストリームオペレータ。ナニコレ複雑すぎて理解できない...
  • 11章 クロッキングブロック。clockingとか使ったことない...これはしっかり勉強しないとダメそうだ...

ここまで来て疲れてきた... もう少し時間を取って勉強しないとダメそうです...


  1. そういう意味では、この本を辞書としてもう一つ「Effective SystemVerilog」みたいなやつを著者様にはお願いしたいところです(笑)

  2. 日本語を勉強したいと言う外国人に、いきなりドカッと国語辞典を渡す愚か者はいないだろう。

「動かして学ぶ 量子コンピュータプログラミング」を買いました

量子コンピュータについては、こういう本が欲しかったんですよ。大学の講義資料とかを読み漁っても、量子コンピュータって複雑な数式が大量に出てきたりして分かりにくい。そこをどうにか突破できたとしても、じゃあどうやって具体的にプログラミングするの?という部分が突破できないことが多い。この本はそのあたりの、具体的な量子コンピュータプログラミングを紹介してくれる本だ(と言ってもまだ最初の方しか読んでないけど)。

もともと私も向学のために量子コンピュータシミュレータを作っていたので、具体的な量子コンピュータプログラミングについては非常に興味がある。 その時はある程度単純なプログラミングもできるようなフレームワークを作っていたのだが、高速フーリエ変換のあたりから本当に私のフレームワークが正しく動いているのか分からなくなってそこで止めてしまっていた。これを機にもう一度勉強してみよう。

まだ最初の方しか読んでいないのだが、本書は量子コンピュータの原理についてはかなりすっ飛ばしているように感じる。つまり理論的な部分や原理的な部分は別に資料を用意して読み込んだ方が良さそうだ。ある程度量子コンピュータの原理を学んだ人が、じゃあ具体的なプログラミングをするためにはどうすれば良いのか?という所をカバーしてくれているように思う。そういう意味では、私にぴったり合っているような気がする。少しずつ読み進めていこう。

f:id:msyksphinz:20201001220131p:plain

「並列コンピュータ 非定量的アプローチ」を買いました

天野英晴先生の新著ということで購入してみました。「非定量的」という何とも面白いサブタイトルを付けたもんだ。

f:id:msyksphinz:20201001214726p:plain

「非定量的アプローチ」という名前の通り、本書にはヘネパタと違って具体的な数値がグラフが全く出てこない。すべてを雰囲気で理解するためのような書籍だと認識した。そういう意味では、いきなり厳密で分かりにくい教科書を読むよりもこういう本から入ってきた方が分かりやすいのかもしれない。

中身は、何だか天野先生の講義資料で見たことのあるような図がたくさん出ているなあ...?まあ講義資料をベースに書いているのだろうからその通りかもしれない。 並列コンピュータの書籍であるので、マルチコアの話から始まり、共有メモリ、分散共有メモリ、クラスタなどと続いている。 結合網の話などは正直あまり得意分野ではないので、これを機に読んでみよう。

さらに最近の話題としてアクセラレータやFPGAGPUなどの話も入っているようだ。書籍自体も小さめで150ページ程度と非常にコンパクトなので、読み物としても面白いかもしれない。コンピュータを専門としている人なら土日でさらっと面白く読むことができると思う。

Binary Translation型エミュレータを作る(RISC-V Disassemblerクレートの作成)

Binary Translation方式の命令セットエミュレータのRust実装、ある程度進んできたが、どんどん複雑なテストパタンを確認していかなければならない。デバッグ機能についてだが、ホスト命令のディスアセンブル機能ができたので、次はゲスト命令をディスアセンブル表示できるようにしたい。ゲスト命令については今回はRISC-Vに絞っているため、RISC-Vのディスアセンブラで既に使用できるRust実装が無いか探したが、うまそうなものが無いため自作することにした。

と言ってもデコーダを1から作ってしまうのは非常に面倒なので、spike-dasmというC++で記述されたRISC-VデコーダをRustのWrapperで囲んでRustのインタフェースを作ることにした。

まずはspike-dasmについて調査する。riscv-isa-simリポジトリに実体がある。

github.com

この中でディスアセンブル機能の実体を探す。diasm.ccとかdisam.hとかが怪しい。

  • riscv-isa-sim/spike_main/spike-dasm.cc
  disassembler_t* disassembler = new disassembler_t(xlen);
...
    
      string dis = disassembler->disassemble(bits);
      s = s.substr(0, start) + dis + s.substr(endp - &s[0] + 1);
      pos = start + dis.length();
    }

このdisassembler_tというものが本質らしい。このクラスをRustで呼び出せるように改造する。

  • spike-dasm-wrapper/src/lib.rs
#[link(name = "spike-dasm", kind="static")]
use std::ffi::CStr;
use std::os::raw::c_char;

pub enum DisasmImpl {}

extern {
    pub fn Disasm_Disasm() -> *mut DisasmImpl;
    pub fn Disasm_disassemble(diasm: *mut DisasmImpl, insn:u32) -> *const c_char;
}

pub struct Disasm {
    raw: *mut DisasmImpl
}

impl Disasm {
    #[inline]
    pub fn new() -> Self {
        unsafe { Disasm { raw: Disasm_Disasm()}}
    }
    #[inline]
    pub fn disassemble(&mut self, insn: u32) ->String {
        let st = unsafe {
            let st_raw = Disasm_disassemble(self.raw, insn);
            CStr::from_ptr(st_raw).to_string_lossy().into_owned()
        };
        return st;
    }
}

ここでは、C言語側とのインタフェースとしてDiasm_Diasm()Disasm_disasemble()を定義している。この関数の実体はこちら。

  • spike-dasm-wrapper/src/helper.cpp
#include <iostream>
#include "disasm.h"

extern "C"
{
    typedef struct
    {
        disassembler_t impl;
    } DisasmImpl;

    DisasmImpl *Disasm_Disasm()
    {
        disassembler_t *diasm = new disassembler_t(64);
        return (DisasmImpl *)diasm;
    }

    const char *Disasm_disassemble(DisasmImpl *di, uint32_t insn)
    {
        auto str = di->impl.disassemble(insn);
        char *str_ptr = new char[100];
        memcpy(str_ptr, str.c_str(), str.length());
        return str_ptr;
    }
}

それぞれ、

このときに、Disasm_disassemble()から文字列を返すのがどうも上手く行かず、必ず文字が崩れてしまうのでもしやと思って明示的にメモリ中に領域を確保してそこに文字列をコピーするようにしたらうまく行った。これはもしかしてspike-dasm自体のメモリリークのバグかもしれない。。。

という訳でこのラッパーをクレートして用意することにした。

https://crates.io/crates/spike-dasm-wrapper

github.com

活用方法は以下だ。単純にDisasmクラスをインスタンス化して呼び出しているだけだ。とりあえずこれでうまく行っているが、これだとディスアセンブルを実行する度に新規インスタンスを作っているので効率が悪い。そのうち何とかしたい。

pub fn disassemble_riscv(inst: u32) -> String {
    let mut disasm = Disasm::new();
    disasm.disassemble(inst)
}

以下のようにしてゲスト命令をフェッチしている最中に呼び出す。

                self.m_tcg_vec.append(&mut tcg_inst);
                if step {
                    let mut exit_tcg = vec![TCGOp::new_0op(TCGOpcode::EXIT_TB)];
                    self.m_tcg_vec.append(&mut exit_tcg);
                }
                if debug {
                    print!("  {:08x} : {}\n",  inst_info.inst, disassemble_riscv(guest_inst));
                }
                if id == RiscvInstId::JALR
                    || id == RiscvInstId::JAL
...

実行結果は以下のようになった。x86ホスト命令のディスアセンブルと同時に呼び出してみる。

cargo run -- --debug --elf-file \
    /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-p-simple
========= BLOCK START =========
Guest PC Address = 00000054
  00000297 : auipc   t0, 0x0
  01028293 : addi    t0, t0, 16
  30529073 : csrw    mtvec, t0
  18005073 : csrwi   satp, 0
  00000297 : auipc   t0, 0x0
  01c28293 : addi    t0, t0, 28
  30529073 : csrw    mtvec, t0
  fff00293 : li      t0, 4095
  3b029073 : csrw    pmpaddr0, t0
  01f00293 : li      t0, 31
  3a029073 : csrw    pmpcfg0, t0
  00000297 : auipc   t0, 0x0
  01828293 : addi    t0, t0, 24
  30529073 : csrw    mtvec, t0
  30205073 : csrwi   medeleg, 0
  30305073 : csrwi   mideleg, 0
  30405073 : csrwi   mie, 0
  00000193 : li      gp, 0
  00000297 : auipc   t0, 0x0
  f6828293 : addi    t0, t0, 3944
  30529073 : csrw    mtvec, t0[f:id:msyksphinz:20200930225648p:plain]
  00100513 : li      a0, 1
  01f51513 : slli    a0, a0, 31
  00055863 : bgez    a0, pc + 16
tb_address  = 0x7fadcfb40000
00007FADCFB40000 48C7853000000054000000 movq      $0x54,0x30(%rbp)
00007FADCFB40000 488B8530000000       mov       0x30(%rbp),%rax
00007FADCFB40007 480510000000         add       $0x10,%rax
00007FADCFB4000D 48898530000000       mov       %rax,0x30(%rbp)
00007FADCFB40000 48BF5034A3F3FF7F0000 movabs    $0x7FFF_F3A3_3450,%rdi
00007FADCFB4000A 48BE0000000000000000 movabs    $0,%rsi
00007FADCFB40014 48BA0500000000000000 movabs    $5,%rdx
00007FADCFB4001E 48B90503000000000000 movabs    $0x305,%rcx

いいね、ディスアセンブル結果がしっかりと表示された。これでデバッグはかなりやりやすくなる。

f:id:msyksphinz:20200930225648p:plain