FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (41. objdumpのコードを読み解く)

f:id:msyksphinz:20190425001356p:plain

現状、llvm-objdumpを実行すると以下のようになってしまう。これをサポートする。

% ./bin/llvm-objdump -d if_ctrl.o
if_ctrl.o: file format ELF32-myriscvx

if_ctrl.o': can't find target: : error: unable to get target for 'unknown--', see --version and --triple.

まずは、MYRISCVXのオブジェクトファイルを認識させる必要がある。

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

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

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

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

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

  • build-myriscvx/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だ。

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
  • DecodeSimm12
  • DecodeBranch12Target

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

  • llvm-myriscvx/lib/Target/MYRISCVX/Disassembler/MYRISCVXDisassembler.cpp
....
#include "MYRISCVXGenDisassemblerTables.inc"
...
/* 整数レジスタの場合のデコード */
static DecodeStatus DecodeCPURegsRegisterClass(MCInst &Inst,
                                               unsigned RegNo,
                                               uint64_t Address,
                                               const void *Decoder) {
  if (RegNo > 15)
    return MCDisassembler::Fail;

  /* レジスタインスタンスを作成し、オペランドに登録する */
  RegNo = getReg(Decoder, MYRISCVX::GPRRegClassID, RegNo);

  Inst.addOperand(MCOperand::createReg(RegNo));
  return MCDisassembler::Success;
}

static DecodeStatus DecodeGPRRegisterClass(MCInst &Inst,
                                              unsigned RegNo,
                                              uint64_t Address,
                                              const void *Decoder) {
  return DecodeCPURegsRegisterClass(Inst, RegNo, Address, Decoder);
}
...
/* 分岐命令のPC相対即値アドレス生成 */
static DecodeStatus DecodeBranch12Target(MCInst &Inst,
                                         unsigned Insn,
                                         uint64_t Address,
                                         const void *Decoder) {
  /* RISC-Vの即値命令フィールド切り出しは若干複雑 */
  int BranchOffset = SignExtend32<12>((fieldFromInstruction(Insn, 31, 1) << 12) |
                                      (fieldFromInstruction(Insn, 25, 6) <<  5) |
                                      (fieldFromInstruction(Insn,  8, 4) <<  1) |
                                      (fieldFromInstruction(Insn,  7, 1) << 11));
  /* 即値オペランドを生成して登録する */
  Inst.addOperand(MCOperand::createImm(BranchOffset));
  return MCDisassembler::Success;
}

このようにして実装されたdecodeInstruction()およびデコード用ヘルパー関数は、実際にはより上位のMYRISCVXDisassembler::getInstructionにより呼び出される。

  • llvm-myriscvx/tools/llvm-objdump/llvm-objdump.cpp
static void disassembleObject(const ObjectFile *Obj, bool InlineRelocs) {
...
      for (Index = Start; Index < End; Index += Size) {
        // 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);

では、ここまで実装した状態で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 -stats -debug -filetype=obj if_ctrl.bc -mcpu=simple32 -march=myriscvx32 -target-abi=lp64 -enable-MYRISCVX-tail-calls=false -relocation-model=pic
file if_ctrl.o
ELF 32-bit MSB relocatable, version 1 (SYSV), not stripped
./bin/objdump -d if_ctrl.o
Disassembly of section .text:
0000000000000000 _Z11test_ifctrlv:
       0:       13 01 81 ff     addi    x11, x11, -8
       4:       13 05 00 00     addi    x6, zero, 0
       8:       23 22 a1 00     sw      x6, 4(x11)
       c:       03 25 41 00     lw      x6, 4(x11)
      10:       63 10 05 00     bne     x6, zero, 0
      14:       03 25 41 00     lw      x6, 4(x11)
      18:       13 05 15 00     addi    x6, x6, 1
      1c:       23 22 a1 00     sw      x6, 4(x11)

0000000000000020 $BB0_2:
      20:       03 25 41 00     lw      x6, 4(x11)
      24:       13 01 81 00     addi    x11, x11, 8
      28:       67 80 10 00     ret     x10