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);
まずは、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
への置き換えが必要となる)。