FPGA開発日記

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

オリジナル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を組み立てる。