FPGA開発日記

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

オリジナルLLVMバックエンド実装をまとめる(30. llvm-litによるテストの追加)

f:id:msyksphinz:20191007020939p:plain:w200

前回の続き。MYRISCVXのテストを追加する。

simple_main.cppをclangでコンパイルすると、以下のようなLLVM中間コードが生成される。

./bin/clang --target=riscv64-unknown-elf -c  ../myriscvx-tests/tests/simple_main.cpp -emit-llvm -o - | ./bin/llvm-dis -o -
...
; Function Attrs: noinline norecurse nounwind optnone
define dso_local signext i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  ret i32 0
}
...

この中間表現をllcで処理してアセンブリ命令を出力するわけだが、上記のコードは以下のように変換されるはずだ。

...
main:                                   # @main

# %bb.0:                                # %entry
        addi    x2, x2, -4
        addi    x10, zero, 0
        sw      x10, 0(x2)
        addi    x2, x2, 4
        ret
...

入力としての中間コードと、出力としての想定するアセンブリ命令を1つにまとめて、テストを作る。

  • llvm-myriscvx80/test/CodeGen/MYRISCVX/simple_main.ll
; 32ビット MYRISCVXでllcを実行。clangで生成したIRは、-O3を指定してある。
; RUN: llc -mtriple=myriscvx32 < %s \
; RUN:   | FileCheck %s -check-prefix=MY32I

define dso_local i32 @main32_O3() local_unnamed_addr #0 {
; MY32I-LABEL: main32_O3:
; MY32I:       # %bb.0:
; MY32I-NEXT:    addi x10, zero, 0
; MY32I-NEXT:    ret
entry:
  ret i32 0
}
  • llvm-myriscvx80/test/CodeGen/MYRISCVX/simple_main.ll
; 64ビット MYRISCVXでllcを実行。clangで生成したIRは、-O3を指定してある。
; RUN: llc -mtriple=myriscvx64 < %s \
; RUN:   | FileCheck %s -check-prefix=MY64I

define dso_local i32 @main64_O3() local_unnamed_addr #0 {
; MY64I-LABEL: main64_O3:
; MY64I:       # %bb.0:
; MY64I-NEXT:    addi x10, zero, 0
; MY64I-NEXT:    ret
entry:
  ret i32 0
}
  • llvm-myriscvx80/test/CodeGen/MYRISCVX/simple_main.ll
; 64ビット MYRISCVXでllcを実行。clangで生成したIRは、-O0を指定してある。
; RUN: llc -mtriple=myriscvx32 < %s \
; RUN:   | FileCheck %s -check-prefix=MY32I

define dso_local signext i32 @main32() #0 {
; MY32I-LABEL: main32:
; MY32I:        addi    x2, x2, -4
; MY32I:        addi    x10, zero, 0
; MY32I:        sw      x10, 0(x2)
; MY32I:        addi    x2, x2, 4
; MY32I:        ret
entry:
  %retval = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  ret i32 0
}
  • llvm-myriscvx80/test/CodeGen/MYRISCVX/simple_main.ll
; 64ビット MYRISCVXでllcを実行。clangで生成したIRは、-O0を指定してある。
; RUN: llc -mtriple=myriscvx64 < %s \
; RUN:   | FileCheck %s -check-prefix=MY64I

define dso_local signext i32 @main64() #0 {
; MY64I-LABEL: main64:
; MY64I:        addi    x2, x2, -4
; MY64I:        addi    x10, zero, 0
; MY64I:        sw      x10, 0(x2)
; MY64I:        addi    x2, x2, 4
; MY64I:        ret
entry:
  %retval = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  ret i32 0
}

-check-prefix=で指定したラベルを元に生成されたコードとの比較を行っていく。上記の4つのテストが実行され、最終結果が表示される仕組みだ。実行してみる。

$ ./bin/llvm-lit -v ../llvm-myriscvx80/test/CodeGen/MYRISCVX/simple_main.ll
-- Testing: 1 tests, 1 threads --
PASS: LLVM :: CodeGen/MYRISCVX/simple_main.ll (1 of 1)
Testing Time: 0.45s
  Expected Passes    : 1

なんだかあまり変わり映えののない結果になったが、Passしているので良しとする。万が一テストに失敗した場合は以下のようにエラーログが表示される。

  • llvm-myriscvx80/test/CodeGen/MYRISCVX/simple_main.ll
; 32ビット MYRISCVXでllcを実行。clangで生成したIRは、-O3を指定してある。
; RUN: llc -mtriple=myriscvx32 < %s \
; RUN:   | FileCheck %s -check-prefix=MY32I

define dso_local i32 @main32_O3() local_unnamed_addr #0 {
; MY32I-LABEL: main32_O3:
; MY32I:       # %bb.0:
; MY32I-NEXT:    addi x10, zero, 0
; MY32I-NEXT:    rett   # 間違えてretをrettとしてしまった。
entry:
  ret i32 0
}

実行すると以下のようにエラーが表示された。故意に間違えたエラーではあるが、正しく指摘されている。

Exit Code: 1

Command Output (stderr):
--
/home/msyksphinz/work/llvm/llvm-myriscvx80/test/CodeGen/MYRISCVX/simple_main.ll:51:10: error: MY64I: expected string not found in input
; MY64I: rett
         ^
<stdin>:60:2: note: scanning from here
 ret
 ^

オリジナルLLVMバックエンド実装をまとめる(29. llvm-litによるテストの調査)

f:id:msyksphinz:20191007020939p:plain:w200

LLVMのオリジナルのバックエンドを実装してきたが、いろんなソースコードコンパイルして、いろんなテストプログラムを動かしてきた。

これだけ多くのテストパタンを実行するのなら、テスト環境とリグレッションテストを用意しておくのが良い。LLVMは、テストパタンとリグレッションテストの環境が用意されている。

ここでは、これまで作成したオリジナルバックエンドをテストするための環境について調査する。

llvm-lit

llvm-litは"LLVM Integrated Tester"で、LLVMとClangのテストスイートを実行するためのツールだ。テストセットを記述してLLVMに食わせ、その結果が想定したものかどうかまでチェックしてくれる。

llvm-litは以下のようにして使用する。

$ ./bin/llvm-lit -h
usage: llvm-lit [-h] [--version] [-j N] [--config-prefix NAME] [-D NAME=VAL]
                [-q] [-s] [-v] [-vv] [-a] [-o PATH] [--no-progress-bar]
                [--show-unsupported] [--show-xfail] [--path PATH] [--vg]
                [--vg-leak] [--vg-arg ARG] [--time-tests] [--no-execute]
                [--xunit-xml-output XUNIT_OUTPUT_FILE]
                [--timeout MAXINDIVIDUALTESTTIME] [--max-failures MAXFAILURES]
                [--max-tests N] [--max-time N] [--shuffle] [-i]
                [--filter REGEX] [--num-shards M] [--run-shard N] [--debug]
                [--show-suites] [--show-tests] [--single-process]
                [test_paths [test_paths ...]]

では、コードジェネレータ(LLVMバックエンド)のテストを見てみる。LLVMリポジトリ内に、test/CodeGen内にテストが入っている。

tree -L 1 test/CodeGen
test/CodeGen
├── AArch64
├── AMDGPU
├── ARC
├── ARM
├── AVR
├── BPF
├── Generic
├── Hexagon
├── Inputs
├── Lanai
├── MIR
├── MSP430
├── Mips
├── NVPTX
├── Nios2
├── PowerPC
├── RISCV
├── SPARC
├── SystemZ
├── Thumb
├── Thumb2
├── WebAssembly
├── WinCFGuard
├── WinEH
├── X86
└── XCore

26 directories, 0 files

ターゲットアーキテクチャ毎に、テストパタンが並んでいる。さらに、RISCVの中身をのぞいてみる。

tree -L 1 test/CodeGen/RISCV
test/CodeGen/RISCV
├── addc-adde-sube-subc.ll
├── align.ll
├── alloca.ll
├── alu32.ll
├── analyze-branch.ll
├── arith-with-overflow.ll
...
├── tail-calls.ll
├── vararg.ll
├── wide-mem.ll
└── zext-with-load-is-free.ll

0 directories, 76 files

Clangで処理された中間形式LLVM IRのファイル群が格納されている。

llvm-litでリグレッションテスト流す

リグレッションテストを体験するために、まずは一度RISC-Vターゲットでリグレッションテストを流してみる。llvm-litコマンドでディレクトリを指定すれば、自動的にすべてのテストを流してくれる。

./bin/llvm-lit -v ../llvm-myriscvx80/test/CodeGen/RISCV
-- Testing: 94 tests, 8 threads --
PASS: LLVM :: CodeGen/RISCV/addc-adde-sube-subc.ll (1 of 94)
PASS: LLVM :: CodeGen/RISCV/alloca.ll (2 of 94)
PASS: LLVM :: CodeGen/RISCV/analyze-branch.ll (3 of 94)
PASS: LLVM :: CodeGen/RISCV/align.ll (4 of 94)
...
PASS: LLVM :: CodeGen/RISCV/sext-zext-trunc.ll (91 of 94)
PASS: LLVM :: CodeGen/RISCV/rv64m-exhaustive-w-insts.ll (92 of 94)
PASS: LLVM :: CodeGen/RISCV/rv64i-exhaustive-w-insts.ll (93 of 94)
PASS: LLVM :: CodeGen/RISCV/atomic-rmw.ll (94 of 94)
Testing Time: 11.82s
  Expected Passes    : 94

流し終わった。所要時間はわずか10数秒で、94個のテストを流してすべてPassだ。RISC-Vのバックエンドはまだ開発段階なのでわずかに94本しかテストはないが、X86を見てみると何と3368本ものテストが用意されている。

ls -1 ../llvm-myriscvx80/test/CodeGen/X86
2003-08-03-CallArgLiveRanges.ll
2003-08-23-DeadBlockTest.ll
2003-11-03-GlobalBool.ll
...
zext-sext.ll
zext-shl.ll
zext-trunc.ll
zlib-longest-match.ll

コード生成テストの構成を確認してみる

では、テストの内容を確認する。test/CodeGen/RISCV/alu32.llを見てみる。

; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py
; RUN: llc -mtriple=riscv32 -verify-machineinstrs < %s \
; RUN:   | FileCheck %s -check-prefix=RV32I

; These tests are each targeted at a particular RISC-V ALU instruction. Other
; files in this folder exercise LLVM IR instructions that don't directly match a
; RISC-V instruction

; Register-immediate instructions

define i32 @addi(i32 %a) nounwind {
; RV32I-LABEL: addi:
; RV32I:       # %bb.0:
; RV32I-NEXT:    addi a0, a0, 1
; RV32I-NEXT:    ret
  %1 = add i32 %a, 1
  ret i32 %1
}

RUN:の行に、テストの内容が書かれている。このテストでは、llc -mtriple=riscv32 -verify-machineinstrs < [テストファイル名] | FileCheck [テストファイル名] -check-prefix=RV32Iが実行される、ということになる。

llcで生成したターゲットアセンブリコードを、FileCheckコマンドで結果を比較する、ということになる。実行してみる。

$ ./bin/llvm-lit ../llvm-myriscvx80test/CodeGen/RISCV/alu32.ll
-- Testing: 1 tests, 1 threads --
PASS: LLVM :: CodeGen/RISCV/alu32.ll (1 of 1)
Testing Time: 0.87s
  Expected Passes    : 1

LLVM IRのコード:

define i32 @addi(i32 %a) nounwind {
  %1 = add i32 %a, 1
  ret i32 %1
}

に対して、以下のコードが生成されることを想定している。これが一致すると、テストがPassとなる。

addi:
# %bb.0:
    addi a0, a0, 1
    ret

複数のGitHubアカウントに対して複数のSSHキーを設定するための方法

人によってはGitHubのアカウントを2つ以上持っていたり、社内で複数のGitHubアカウントを使い分ける必要があったりする場合がある。 例えば仕事用と趣味用でGitHubアカウントを2つ用意していたり、その場合は1つのPCから複数のGitHubに対して制御を行う必要がある。

この場合、SSHキーをどうすればよいのかという問題だが、単純に同じSSHキーを登録しておけばいいんじゃない?と思っていたら、それはGitHub的に使用できないらしい。 実際、別のアカウントでGitHubに登録しているSSH公開鍵をもう1つのGitHubのアカウントで登録しようと売ると、「このキーすでに使われてるぞ」と怒られてしまう。

f:id:msyksphinz:20191023222033p:plain

そこでどうするかというと、手元のPCでもう一つのSSHキーを生成する。例えばこれをid_rsa_selfとする。 すると、新たに~/.ssh/id_rsa_self.pubが生成されるので、これをGitHubに登録するわけだ。

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/msyksphinz/.ssh/id_rsa): /home/msyksphinz/.ssh/id_rsa_self
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa_self.
Your public key has been saved in id_rsa_self.pub.
The key fingerprint is:
...

そしてこの2つをどのようにして使い分けるかという話だが、このためには~/.ssh/configに設定を記述することになる。

  • ~/ssh/config
# Personal GitHub account
Host msyksphinz
 HostName github.com
 User git
 AddKeysToAgent yes
 IdentityFile ~/.ssh/id_rsa

# Work GitHub account
Host msyksphinz-self
 HostName github.com
 User git
 AddKeysToAgent yes
 IdentityFile ~/.ssh/id_rsa_self

configファイルはパーミッションを600にしないと警告されるので気を付けること。

2つのコンフィグレーションを用意した。どちらともHostNamegithub.comに設定しているのだが、IdentityFileをそれぞれ別のファイルに設定している。

そしてこのコンフィグレーションをどのように使い分けるのかということだが、これはgit cloneなどの時にアドレスとして使える。 つまり、上記の場合でmsyksphinz-selfのキーを使ってcloneしたい場合、以下のようにすればよい。

git clone git@msyksphinz-self:msyksphinz-self/rumy-make.git

msyksphinz-self:で、msyksphinz-selfのアカウントでgithub.comへのアクセスを行ってくれる。 .git/configを見てみると、URLとしてはmsyksphinz-selfを使ったアドレスになっているので、既存のClone済みのリポジトリについても同じようにURLを変更すれば、複数のGitHubアカウントを使い分けることができるようになる。

[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[remote "origin"]
        url = git@msyksphinz-self:msyksphinz-self/rumy-make.git
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
        remote = origin
        merge = refs/heads/master

オリジナルLLVMバックエンド実装をまとめる(28. llvm-objdumpを実装する2)

前回の続き。命令ダンプのオペランド表記を一部改造する

このままでLLVMコンパイルして実行しても良いのだが、例えば以下のような命令はダンプの結果がおかしくなる。

        addi    x2, x2, -4
-->
        addi    x2, x2, 4092

マイナス値の表記がうまく行えない。オペランドの出力についてはprintOperand()が担っているのだが、実装部分を確認すると以下のようになっており、符号の有り無しを区別していない。

  • llvm-myriscvx80/lib/Target/MYRISCVX/InstPrinter/MYRISCVXInstPrinter.cpp
void MYRISCVXInstPrinter::printOperand(const MCInst *MI, unsigned OpNo,
                                       raw_ostream &O) {
  const MCOperand &Op = MI->getOperand(OpNo);
  if (Op.isReg()) {
    printRegName(O, Op.getReg());
    return;
  }

  // 即値のプリントについては、符号の有り無しを考慮していない。
  if (Op.isImm()) {
    O << Op.getImm();
    return;
  }

  assert(Op.isExpr() && "unknown operand kind in printOperand");
  Op.getExpr()->print(O, &MAI, true);
}

これではダンプ結果として少し読みにくいので、符号ありのオペランドについては出力関数を切り替えて、符号を扱えるようにしたい。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
class ArithLogicI<bits<7> opcode, bits<3> funct3,
                  string instr_asm, SDNode OpNode,
                  Operand Od, PatLeaf imm_type, RegisterClass RC> :
  MYRISCVX_I<opcode, funct3, (outs RC:$rd), (ins RC:$rs1, Od:$imm12),
  !strconcat(instr_asm, "\t$rd, $rs1, $imm12"),
  [(set RC:$rd, (OpNode RC:$rs1, imm_type:$imm12))], IIAlu> {
    let isReMaterializable = 1;
}
...
def ADDI : ArithLogicI<0b0010011, 0b000, "addi", add, simm12, immSExt12, GPR>;

オペランドにはsimm12ノードを使用しているので、simm12ノードのDecodeMethod()を変更する。

// Signed Operand
def simm12 : Operand<XLenVT>, ImmLeaf<XLenVT, [{return isInt<12>(Imm);}]> {
  let DecoderMethod = "decodeSImmOperand<12>";
}
def simm20 : Operand<XLenVT>, ImmLeaf<XLenVT, [{return isInt<20>(Imm);}]> {
  let DecoderMethod = "decodeSImmOperand<20>";
}

decodeSImmOperand()MYRISCVXInstPrinter()に作り込んでいく。

  • llvm-myriscvx80/lib/Target/MYRISCVX/Disassembler/MYRISCVXDisassembler.cpp
template <unsigned N>
static DecodeStatus decodeSImmOperand(MCInst &Inst, uint64_t Imm,
                                      int64_t Address, const void *Decoder) {
  assert(isUInt<N>(Imm) && "Invalid immediate");
  // Sign-extend the number in the bottom N bits of Imm
  Inst.addOperand(MCOperand::createImm(SignExtend64<N>(Imm)));
  return MCDisassembler::Success;
}

即値を64ビット幅に拡張し、オペランドを追加する。

f:id:msyksphinz:20191019221631p:plain
符号付即値のノードについては、DecodeMethodを変更してDecodeSIMMOperand()を呼び出す。

コンパイルと実行

では、ここまで実装した状態でLLVMをリコンパイルし、実際にobjdumpを実行してみる。

  • if_ctrl.cpp
int test_ifctrl()
{
  unsigned int a = 0;

  if (a == 0) {
    a++; // a = 1
  }

  return a;
}
./bin/clang if_ctrl.cpp -emit-llvm
./bin/llc -mtriple=myriscvx32 -filetype=asm -o if_ctrl.my32.s

生成されたif_ctrl_my32.sは以下のようになった。

_Z11test_ifctrlv:                       # @_Z11test_ifctrlv

# %bb.0:                                # %entry
        addi    x2, x2, -4
        addi    x10, zero, 0
        sw      x10, 0(x2)
        lw      x10, 0(x2)
        bne     x10, zero, $BB0_2
# %bb.1:                                # %if.then
        lw      x10, 0(x2)
        addi    x10, x10, 1
        sw      x10, 0(x2)
$BB0_2:                                 # %if.end
        lw      x10, 0(x2)
        addi    x2, x2, 4
        ret

次は、オブジェクトファイルを生成してみる。

./bin/llc -mtriple=myriscvx32 -filetype=obj -o if_ctrl.my32.o
./bin/llvm-objdump -triple=myriscvx32 -d  if_ctrl.my32.o
Disassembly of section .text:
0000000000000000 _Z11test_ifctrlv:
       0:       13 01 c1 ff     addi    x2, x2, -4
       4:       13 05 00 00     addi    x10, zero, 0
       8:       23 20 a1 00     sw      x10, 0(x2)
       c:       03 25 a1 00     lw      x10, 10(x2)
      10:       63 10 00 00     bne     zero, zero, 0
      14:       03 25 a1 00     lw      x10, 10(x2)
      18:       13 05 15 00     addi    x10, x10, 1
      1c:       23 20 a1 00     sw      x10, 0(x2)
      20:       03 25 a1 00     lw      x10, 10(x2)
      24:       13 01 41 00     addi    x2, x2, 4
      28:       67 80 00 00     ret

バイトの表記が全部逆になっているので少し驚くかもしれない。 左から順に0バイト目、1バイト目、2バイト目、3バイト目となる。 つまり最初のaddi命令はffc10113であるのが正解で、念のためriscv-toolsのディスアセンブラで確認しておくと、

echo "DASM(ffc10113)" | spike-dasm
addi    sp, sp, -4

となり、命令として正しくダンプできていることが確認できると思う。

オリジナルLLVMバックエンド実装をまとめる(27. llvm-objdumpを実装する1)

ELFファイルの生成とディスアセンブラの作成

LLVMには、ツールセットとして生成したオブジェクトファイルを逆アセンブルするためのllvm-ojbdumpというコマンドが用意されている。 基本的に生成したオブジェクトファイルはこのコマンドを用いてダンプすることが可能だ。

しかし、MYRISCVXの実装ではDisassemblerを実装していないので、llvm-objdumpコマンドを実行するとエラーになる。

% ./bin/llvm-objdump -triple=myriscvx32 -d simple_main.o
simple_main.o:  file format ELF32-unknown

./bin/llvm-objdump: error: 'simple_main.o': no disassembler for target myriscvx32.

まずは、MYRISCVXのオブジェクトファイルを認識させる必要がある。そして、LLVMにMYRISCVXのディスアセンブラを実装していいく。

命令定義を作ったのでディスアセンブラも自動で作られるのじゃないか?と思うかもしれないが、命令のデコードや命令の表示のルーチンは基本的にすでに生成されている。 これを使うためのWrapperに関する実装を付け加えていくことになる。

MYRISCVXのディスアセンブラを実装する

ディスアセンブルをサポートするためには、lib/Target/MYRISCVXの下に、さらにDisassemblerディレクトリを掘る。その中にディスアセンブルに必要なコードを実装していく。

まず、llvm-objdumpがオブジェクトファイルをダンプするまでの手順だが、llvm-objdump.cppにその実装がある。

  • llvm-myriscvx80/tools/llvm-objdump/llvm-objdump.cpp
static void disassembleObject(const ObjectFile *Obj, bool InlineRelocs) {
  if (StartAddress > StopAddress)
    error("Start address should be less than stop address");

...
        // 命令の取得。オブジェクトから命令を切り取ってデコードする。
        // Disassemble a real instruction or a data when disassemble all is
        // provided
        bool Disassembled = DisAsm->getInstruction(Inst, Size, Bytes.slice(Index),
                                                   SectionAddr + Index, DebugOut,
                                                   CommentStream);
...
        // デコードした情報に基づいて命令の出力を行う。
        PIP.printInst(*IP, Disassembled ? &Inst : nullptr,
                      Bytes.slice(Index, Size), SectionAddr + Index, outs(), "",
                      *STI, &SP, &Rels);
        outs() << CommentStream.str();
        Comments.clear();
...

ディスアセンブラが命令を取得し、その命令をデコード、そして出力する、という流れだ。 まず、DisAsmはディスアセンブラで、MYRISCVXのサブターゲットを取得して生成されている。

  • llvm-myriscvx80/tools/llvm-objdump/llvm-objdump.cpp
  // ディスアセンブラDisAsmを作成。
  std::unique_ptr<MCDisassembler> DisAsm(
    TheTarget->createMCDisassembler(*STI, Ctx));
  if (!DisAsm)
    report_error(Obj->getFileName(), "no disassembler for target " +
                 TripleName);
f:id:msyksphinz:20191019205308p:plain
llvm-objdumpの大まかな流れ。

まずは、Disassemblerを生成するようにMYRISCVXのサブディレクトリを追加する。

  • llvm-myriscvx80/lib/Target/MYRISCVX/CMakeLists.txt
...
tablegen(LLVM MYRISCVXGenDisassemblerTables.inc -gen-disassembler)
...
add_subdirectory(Disassembler)
  • llvm-myriscvx80/lib/Target/MYRISCVX/LLVMBuild.txt
[common]
subdirectories =
  MCTargetDesc
  TargetInfo
  InstPrinter
  Disassembler    /* これを追加 */
...
has_asmprinter = 1
has_disassembler = 1  /* これを追加 */
...

まず、上記のtablegenの追加により、新たにTarget Descriptionからディスアセンブラが生成される。 Disassemblerの中では、このディスアセンブラのテンプレートに不足しているものを追加していく。

まずは、MYRISCVXDisassembler::getInstruction()の実装から入る。ここでは、命令の切り出しとデコードを行う。

  • llvm-myriscvx80/lib/Target/MYRISCVX/Disassembler/MYRISCVXDisassembler.cpp
#include "MYRISCVXGenDisassemblerTables.inc"

DecodeStatus
MYRISCVXDisassembler::getInstruction(MCInst &Instr, uint64_t &Size,
                                     ArrayRef<uint8_t> Bytes,
                                     uint64_t Address,
                                     raw_ostream &VStream,
                                     raw_ostream &CStream) const {
  uint32_t Insn;

  DecodeStatus Result;
  // 32ビット長の命令の切り出し。
  Insn = support::endian::read32le(Bytes.data());

  // 命令のデコード
  // Calling the auto-generated decoder function.
  Result = decodeInstruction(DecoderTableMYRISCVX32, Instr, Insn, Address,
                             this, STI);
  if (Result != MCDisassembler::Fail) {
    Size = 4;
    return Result;
  }

  return MCDisassembler::Fail;
}

support::endian::read32leによって命令を切り出している。今回は16ビット長の短縮命令については省略し、RISC-Vはリトルエンディアンのみなのでこのような単純な実装になっている。

切り出された命令はInsnに格納され、デコードされる。 デコードを行うのはdecodeInstructionだ。 これは、MCInstオブジェクトを生成する。また、デコードテーブルとしてMYRISCVXGenDisassemblerTables.incが生成したDecoderTableMYRISCVX32を使用している。

生成されたディスアセンブラのテンプレートを見てみる。

  • build-myriscvx80/lib/Target/MYRISCVX/MYRISCVXGenDisassemblerTables.inc
static const uint8_t DecoderTableMYRISCVX32[] = {
/* 0 */       MCD::OPC_ExtractField, 0, 7,  // Inst{6-0} ...
/* 3 */       MCD::OPC_FilterValue, 3, 48, 0, 0, // Skip to: 56
/* 8 */       MCD::OPC_ExtractField, 12, 3,  // Inst{14-12} ...
...

template<typename InsnType>
static DecodeStatus decodeInstruction(const uint8_t DecodeTable[], MCInst &MI,
                                      InsnType insn, uint64_t Address,
                                      const void *DisAsm,
                                      const MCSubtargetInfo &STI) {
...

insnで渡される命令をデコードして、その結果をMCInst MIに返す。DecodeTable[]経由でMYRISCVXのデコードテーブルDecodeTableMYRISCVX32を渡す。

  while (true) {
    ptrdiff_t Loc = Ptr - DecodeTable;
    switch (*Ptr) {
    default:
      errs() << Loc << ": Unexpected decode table opcode!\n";
      return MCDisassembler::Fail;
    case MCD::OPC_ExtractField: {
      /* 命令フィールドの切り出し */
      CurFieldValue = fieldFromInstruction(insn, Start, Len);
...
    case MCD::OPC_FilterValue: {
       /* フィールドのデコード */
...
    case MCD::OPC_CheckField: {
...
    case MCD::OPC_CheckPredicate: {
...
    case MCD::OPC_Decode: {
      unsigned Len;
      // Decode the Opcode value.
      unsigned Opc = decodeULEB128(++Ptr, &Len);
      Ptr += Len;
      unsigned DecodeIdx = decodeULEB128(Ptr, &Len);
      Ptr += Len;
      ...
      S = decodeToMCInst(S, DecodeIdx, insn, MI, Address, DisAsm, DecodeComplete);
      ...
      LLVM_DEBUG(dbgs() << Loc << ": OPC_Decode: opcode " << Opc
                   << ", using decoder " << DecodeIdx << ": "
                   << (S != MCDisassembler::Fail ? "PASS" : "FAIL") << "\n");
      return S;   

decodeInstructionの内部では、デコードテーブルを先頭から読んでいき、命令フィールドを切り出しながら、次々とテーブルをジャンプする。最終的に1つの命令に到達すれば、それがデコード結果となる。

MCD::OPC_Decodeまで到達すると命令を1つに特定できる(Opcに命令のデコードインデックスが格納される)。decodeToMCInstでは特定した命令をMCInstに変換し、decodeInstructionから抜ける。 MCInstでは命令の形状に応じてレジスタオペランドなどの情報を組み立てる。 そのためにはDecodeIdxでインデックスされる命令フォーマットを元にオペランドを組み立てる。それがdecodeToMCInstだ。

  • build-myriscvx80/lib/Target/MYRISCVX/MYRISCVXGenDisassemblerTables.inc
template<typename InsnType>
static DecodeStatus decodeToMCInst(DecodeStatus S, unsigned Idx, InsnType insn, MCInst &MI,
                                   uint64_t Address, const void *Decoder, bool &DecodeComplete) {
...
  switch (Idx) {
  default: llvm_unreachable("Invalid index!");
  case 0:
    return S;
  case 1:
    /* Reg, Reg, Imm12 フォーマットの場合 */
    tmp = fieldFromInstruction(insn, 7, 5);
    if (DecodeGPRRegisterClass(MI, tmp, Address, Decoder) == MCDisassembler::Fail) { return MCDisassembler::Fail; }
    tmp = fieldFromInstruction(insn, 15, 5);
    if (DecodeGPRRegisterClass(MI, tmp, Address, Decoder) == MCDisassembler::Fail) { return MCDisassembler::Fail; }
    tmp = fieldFromInstruction(insn, 20, 12);
    if (DecodeSimm12(MI, tmp, Address, Decoder) == MCDisassembler::Fail) { return MCDisassembler::Fail; }
    return S;
...
  case 5:
    /* Reg, Reg, Reg フォーマットの場合 */
    tmp = fieldFromInstruction(insn, 7, 5);
    if (DecodeGPRRegisterClass(MI, tmp, Address, Decoder) == MCDisassembler::Fail) { return MCDisassembler::Fail; }
    tmp = fieldFromInstruction(insn, 15, 5);
    if (DecodeGPRRegisterClass(MI, tmp, Address, Decoder) == MCDisassembler::Fail) { return MCDisassembler::Fail; }
    tmp = fieldFromInstruction(insn, 20, 5);
    if (DecodeGPRRegisterClass(MI, tmp, Address, Decoder) == MCDisassembler::Fail) { return MCDisassembler::Fail; }
    return S;

decodeToMCInst内で、定義されていない関数がある。各オペランドのデコードを行う関数群だ。例えば、

  • DecodeGPRRegisterClass

などが相当する。これらをlib/Target/MYRISCVX/MYRISCVXDisassembler.cppで実装することになる。

  • llvm-myriscvx80/lib/Target/MYRISCVX/Disassembler/MYRISCVXDisassembler.cpp
....
#include "MYRISCVXGenDisassemblerTables.inc"
...
const unsigned int GPRDecodeTable [] = {
  MYRISCVX::ZERO, MYRISCVX::RA, MYRISCVX::SP,  MYRISCVX::GP,
  MYRISCVX::TP,   MYRISCVX::T6, MYRISCVX::T1,  MYRISCVX::T2,
  MYRISCVX::FP,   MYRISCVX::S1, MYRISCVX::A0,  MYRISCVX::A1,
  MYRISCVX::A2,   MYRISCVX::A3, MYRISCVX::A4,  MYRISCVX::A5,
  MYRISCVX::A6,   MYRISCVX::A7, MYRISCVX::S2,  MYRISCVX::S3,
  MYRISCVX::S4,   MYRISCVX::S5, MYRISCVX::S6,  MYRISCVX::S7,
  MYRISCVX::S8,   MYRISCVX::S9, MYRISCVX::S10, MYRISCVX::S11,
  MYRISCVX::T0,   MYRISCVX::T3, MYRISCVX::T4,  MYRISCVX::T5 };

static DecodeStatus DecodeCPURegsRegisterClass(MCInst &Inst,
                                               unsigned RegNo,
                                               uint64_t Address,
                                               const void *Decoder) {
  if (RegNo > sizeof(GPRDecodeTable))
    return MCDisassembler::Fail;

  unsigned int Reg = GPRDecodeTable[RegNo];
  Inst.addOperand(MCOperand::createReg(Reg));
  return MCDisassembler::Success;
}

static DecodeStatus DecodeGPRRegisterClass(MCInst &Inst,
                                           unsigned RegNo,
                                           uint64_t Address,
                                           const void *Decoder) {
  return DecodeCPURegsRegisterClass(Inst, RegNo, Address, Decoder);
}

GPRのデコードの部分について実装を行った。DecodeCPURegisterClass()からDecodeGPRRegisterClass()を呼び出し(これは後で浮動小数レジスタも追加したかったので切り分け)、そしてGPRDecodeTableに基づいてレジスタのデコードを行う。

GPRDecodeTableを用意しなければならに理由としては、レジスタの定義としてレジスタ番号と同じ順序に定義がされているわけではないので、「レジスタ番号」→「MYRISCVXのレジスタ定義のEnum」に置き換えるためのテーブルが必要になっている(実際、MYRISCVX::ZEROはゼロ番ではないので、配列の0番目からMYRISCVX::ZEROへの置き換えが必要となる)。

f:id:msyksphinz:20191019205337p:plain
decodeInstruction()の大まかな流れ。フィールドを切り出してMCInstを組み立てる。

CQ出版 インタフェース 12月号「注目オープンソースRISC-Vマイコン」に寄稿しました

f:id:msyksphinz:20191024012004p:plain:w300

CQ出版さんが出版している雑誌インタフェースの12月号で、「注目オープンソースRISC-Vマイコン」という特集記事に寄稿しました。

RISC-Vマイコンの話がメインなんですが、よく見ると私はマイコンの話はほとんど書いていないですね。 なんか仕様だのクラウドだの、普段実機をあまり触っていないことがばれてしまうような内容構成になっています。

私が寄稿した内容としては、

  • イントロダクション
  • RISC-Vの仕様
  • RISC-Vプロセッサ辞典
  • HiFive1
  • FireSim

あたりです。本当は

  • Chisel
  • HiFive Unleashed

も書いて寄稿したのだけれど、ページの都合上削除されたようです。まあブログの内容とあまり変わらないのだけれども。

くしくもRISC-V Day Tokyo やDesign Solution Forumなど、RISC-V系の発表が多く行われる時期にぶつかってきたわけですが、よく見たらトランジスタ技術RISC-V特集やってますね。本当はもう少し早く出して欲しかったのだけれども、雑誌の出版の予定というのは予定通りにはいかないもので、この時期になったようです。

CQ出版RISC-Vとホビイスト

CQ出版がらみと言えば、Twitter上で少し目を止めてしまった以下の情報。

別にCQ出版がホビイスト向けということはご指摘の通りなのかもしれないけど、自分としては少しイメージが違った。 というか、別のツイートを見てもCQ出版馬鹿にしてる?と一瞬思ったけどそういうことじゃなくて、ちゃんとFPGAマガジン読んでるのね。

「ふざけるな俺は忙しいんだ」の意味は正直あまり悪くは捉えていなくて、そうだよね人手足りないもんね。 確かに当時はRV32Iの実装が多く、Rocketくらいしかまともな実装は殆ど無かったのだけれども、でもRISC-Vについて特集を組んだのはFPGAマガジンが日本初だし、先見の明があるというのは異論はないと思う。

これは一般論とは全く関係ないのだけれども、私個人の思いとしては、学生時代からCQ出版というのはあこがれの存在で、よく先生から「CQ出版に寄稿できれば一流」と煽られていた。当時の学校で先輩の優秀な一握りがCQ出版に原稿を出して(そして落ちたらしいが)、すごいなーと思っていたのが今や私が特集に寄稿するようになってしまったのだから、私のレベルが上がったのか、全体ユーザ数が減ったのか(笑)。

で、技術系界隈はトップダウンボトムアップの筋が必要だと思っていて、CQ出版はその中でボトムアップの非常に重要なメディアであると思っている。ボトムアップというのはどういうことかというと、技術者自身が見たり聞いたり体験したものをベースに、「これ良いんじゃない?」というものを上に持ち上げていって、最終的に製品化する流れ。一方トップダウンは経営者とかが「これやるぞ!」って言ってそれを技術者が作り込んで行く流れ、という(私の)定義。トップダウンベンチャーでありがち。一方で大企業のトップがフラフラとして一本筋を示していないと、周りから「大丈夫かな?」と思われがち。 ボトムアップは草の根活動も含め、生の体験をしている技術者の声が上がっていくタイプ。これも技術の未来を予測するための重要な判断材料だと思う。

CQ出版は、確かに雑誌なので完璧に推敲されている訳ではない(編集者ではなく著者=私の方が)し、私も「こんな適当なこと書いていいかしら」みたいなのも載せて下さったりで非常にフランクな良い雑誌だと思う。技術者が草の根で「これ良いんじゃない?」という感覚が無いと、上からトップダウンで指示を出しても技術者がちゃんと動いてくれないことがあるんじゃないか。CQ出版は基本的に自分で手を動かした結果を各人が寄稿している雑誌なので、その辺かなり信ぴょう性がある、と思っている。

あと先見性があるのも重要。組み込みの雑誌とはいえ、量子コンピュータの特集を組んでみたり、ドローンの特集を計画してみたり、かなり未来を走ってるんじゃないか。情報収集としては非常に良い雑誌だと感じている。

そしてトップダウン、というかきちっとした情報源としてRISC-V Readerも重要な位置づけだし、「これを読めば全て分かるぜ!」という意味で”Reader" を「原典」(=聖書?)と訳したのも何となく意図が分かるように思う。実際Amazonでベストセラーらしいし、みんなが手に取って読みたくなる「本!」って感じの本になっているし、これを見れば全部分かるぜ、という気にさせるのは、トップダウンのフローとして上手くできていると思う。

だから、ボトムアップの方向はホビイスト向けと思うんじゃなくて、草の根活動の重要な情報源として見てはどうだろう?別にRISC-V Readerが無くても興味のある人は触り始めるし、そのための重要な情報源だ。RISC-V Readerは英語版、日本語版、中国語版、ポルトガル語版、スペイン語版とあるけど、それ以外の国でもしっかりやっているし、言い訳がましいことを言って新しいものに手を出さないのは日本人っぽくて個人的に面白くない。

今年のRISC-V Day Tokyoには参加できなかったのだけれども、まあ気になるところとしては日本国内では新しい技術に対して相変わらず日和見な感じが続いているのは分かるし、アジェンダを見ても誰も手を動かしていないのが分かるし、ポンチ絵でも良いのでモノを見せることができたのはデンソーの子会社くらいか。そんな中、実際に手を動かして、さらに面倒な文書化までやってくれているCQ出版の出版陣、執筆陣はもう少し評価されてもいいんじゃないか。

私は技術を先導する立場にはおらず、面白そうなものをかじりながら生活している立場なので偉そうなことは全く言えないのだが、トップダウンからとボトムアップから、うまく融合して、日本の半導体を盛り上げてくれると嬉しい。

雑誌だからと言ってバカにしてると、そのうち雑誌の方から馬鹿にされて、御社の製品の特集組んでくれなくなると思うよ。日本企業の半導体製品、どこいった?

オリジナルLLVMバックエンド実装をまとめる(26. LLVM IRをサポートするためのIntrinsicsのサポート2. )

前回の続き。

データのバイトスワップを行う。C言語では、__builtin_bswap16, __builtin_bswap32, __builtin_bswap64により取得することができる。 これらはそのまま命令に落とし込めないので、IRの生成を抑制する。

  • func_bswap.cpp
int test_bswap16() {
  volatile int a = 0x1234;
  int result = (__builtin_bswap16(a) ^ 0x3412);

  return result;
}

int test_bswap32() {
  volatile int a = 0x1234;
  int result = (__builtin_bswap32(a) ^ 0x34120000);

  return result;
}

int test_bswap64() {
  volatile int a = 0x1234;
  int result = (__builtin_bswap64(a) ^ 0x3412000000000000);

  return result;
}

int test_bswap() {
  int result = test_bswap16() + test_bswap32() + test_bswap64();

  return result;
}

これも、LLVM中間言語を生成してみる。

./bin/clang func_bswap.cpp -c -emit-llvm --target=riscv32-unknown-elf -o - | ./bin/llvm-dis -o -
define dso_local i32 @_Z12test_bswap16v() #0 {
entry:
  %a = alloca i32, align 4
  %result = alloca i32, align 4
  store volatile i32 4660, i32* %a, align 4
  %0 = load volatile i32, i32* %a, align 4
  %conv = trunc i32 %0 to i16
  %1 = call i16 @llvm.bswap.i16(i16 %conv)
  %conv1 = zext i16 %1 to i32
  %xor = xor i32 %conv1, 13330
  store i32 %xor, i32* %result, align 4
  %2 = load i32, i32* %result, align 4
  ret i32 %2
}
...
define dso_local i32 @_Z12test_bswap32v() #0 {
  entry:
  %1 = call i32 @llvm.bswap.i32(i32 %0)
}
...
define dso_local i32 @_Z12test_bswap64v() #0 {
entry:
  %1 = call i64 @llvm.bswap.i64(i64 %conv)
}

それぞれ、llvm.bswap.i16, llvm.bswap.i32, llvm.bswap.i64が呼ばれていることが分かる。 それぞれ、16ビット、32ビット、64ビットの長さでバイトスワップを行うノードである。

https://llvm.org/docs/LangRef.html#llvm-bswap-intrinsics

このノードも、直接命令として落とし込むことはできない。このノードは、setOperationAction()の設定で生成を禁止し、より一般的なノードへの置き換えを行う。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM,
                                               const MYRISCVXSubtarget &STI)
...
  setOperationAction(ISD::BSWAP, MVT::i16, Expand);
  setOperationAction(ISD::BSWAP, MVT::i32, Expand);
  setOperationAction(ISD::BSWAP, MVT::i64, Expand);
...
}

BSWAPのコンパイル結果である。バイトスワップの操作が展開されていることが分かる。

_Z12test_bswap16v:                      # @_Z12test_bswap16v

# %bb.0:                                # %entry
        addi    x2, x2, -8
        lui     x10, 1
        ori     x10, x10, 564
        sw      x10, 4(x2)
        lw      x10, 4(x2)
        slli    x11, x10, 8
        lui     x12, 4080
        and     x11, x11, x12
        slli    x10, x10, 24
        or      x10, x10, x11
        srli    x10, x10, 16
        lui     x11, 3
        ori     x11, x11, 1042
        xor     x10, x10, x11
        sw      x10, 0(x2)
        lw      x10, 0(x2)
        addi    x2, x2, 8
        ret
f:id:msyksphinz:20191017003130p:plain
bswap16で生成された命令によるバイトスワップの様子。