LLVM Toy RISC-V Backend
Table of Contents
- 1. LLVM Toy RISC-V Backend
- 1.1. toy-1: llc 识别 target
- 1.2. toy-2: LLVMInitializeToyTarget
- 1.3. toy-3: LLVMInitializeToyTargetMC
- 1.4. toy-4: ToyDAGToDAGISel
- 1.5. toy-5: ToyInstPrinter
- 1.6. toy-6: ToyAsmPrinter
- 1.7. toy-7: ToyTargetObjectFile
- 1.8. toy-8: ToySubtarget
- 1.9. toy-9: ToyTargetLowering
- 1.10. toy-10: ToyFrameLowering
- 1.11. toy-11: isel
- 1.12. toy-12: ToyRegisterInfo
- 1.13. toy-13: storeRegToStackSlot
- 1.14. toy-14: eliminateFrameIndex
- 1.15. toy-15: emitInstruction
- 1.16. toy-16: printInst
- 1.17. toy-17: add registers
- 1.18. toy-18: add more insns
- 1.19. toy-19: simplify insn definition
- 1.20. toy-20: global address
- 1.21. toy-21: directly lower to machine code
- 1.22. toy-22: lower MachineOperand to MCOperand
- 1.23. toy-23: store global variable
- 1.24. toy-24: LowerReturn
- 1.25. toy-25: emitPrologue and emitEpilogue
- 1.26. toy-26: LowerCall Pt. 1
- 1.27. toy-27: LowerCall Pt. 2
- 1.28. toy-28: LowerFormalArguments
- 1.29. toy-29: LowerCall Pt. 3
- 1.30. toy-30: LowerReturn Pt. 2
- 1.31. toy-31: LowerCall Pt. 4
- 1.32. toy-32: frameindex with constant offset
- 1.33. toy-33: global address with constant offset
- 1.34. toy-34: setcc
- 1.35. toy-35: br_cc
- 1.36. toy-36: type promotion
- 1.37. toy-37: LowerCall Pt. 5
- 1.38. toy-38: glue
- 1.39. toy-39: soft float
- 1.40. toy-40: return struct
- 1.41. toy-41: hard float Pt. 1
- 1.42. toy-42: hard float Pt. 2
- 1.43. toy-43: builtin Pt. 1
- 1.44. toy-44: write object file Pt. 1
- 1.45. toy-45: write object file Pt. 2
- 1.46. toy-46: write object file Pt. 3
- 1.47. toy-47: write object file Pt. 4
- 1.48. toy-48: li
- 1.49. toy-49: intrinsics
1. LLVM Toy RISC-V Backend
1.1. toy-1: llc 识别 target
1.1.1. 目标
$> ./build/bin/llc --version LLVM (http://llvm.org/): LLVM version 15.0.0git DEBUG build with assertions. Default target: x86_64-unknown-linux-gnu Host CPU: skylake Registered Targets: toy - TOY
1.1.2. 需要的修改
添加 target 到 cmake
set(LLVM_ALL_TARGETS ... Toy ... )
这个 `Toy` 与 build 时指定的 `LLVM_TARGETS_TO_BUILD=Toy` 一致, 且 cmake 会根据这个名字找到 `llvm/lib/Target/Toy` 目录来编译
添加 Triple::ArchType
class Triple { public: enum ArchType { UnknownArch, ... toy, ... };
后面提到的 LLVMInitializeToyTargetInfo 函数会使 llc 的命令行参数 `-march toy` 会对应到 ArchType::toy, 同时 LLVMInitializeToyTargetInfo 还会注册 ArchType::toy 对应的 TheToyTarget, 从而让 llc 找到 TheToyTarget. 后续实现的 Toy 的其它初始化的信息都会与 TheToyTarget 关联
- 添加 llvm/lib/Target/Toy 目录
- 需要实现一个名为 LLVMToyCodeGen 的库 (这个名字是由 cmake 要求的), 并实现 LLVMInitializeToyTarget 函数, 目前实现为空
- 需要实现一个名为 LLVMToyDesc 的库, 实现 LLVMInitializeToyTargetMC 函数, 目前实现为空
- 需要实现一个名为 LLVMToyInfo 的库, 实现 LLVMInitializeToyTargetInfo 函数. 为了 llc 的 `Registered Targets` 能列出 toy, 这里必须实现该函数, 以便把 `toy`, `Triple::toy` 以及 `TheToyTarget` 关联起来
1.1.3. 测试
$> ./build/bin/llc --version LLVM (http://llvm.org/): LLVM version 15.0.0git DEBUG build with assertions. Default target: x86_64-unknown-linux-gnu Host CPU: skylake Registered Targets: toy - Toy RISC-V backend $> clang toy_test/test.c -c -emit-llvm -O0 -o /tmp/test.bc $> ./build/bin/llc /tmp/test.bc -march=toy llc: /home/sunway/source/llvm-toy/llvm/tools/llc/llc.cpp:559: auto compileModule(char **, llvm::LLVMContext &)::(anonymous class)::operator()(llvm::StringRef) const: Assertion `Target && "Could not allocate target machine!"' failed.
报错的原因是:
/* NOTE: 由于 LLVMInitializeToyTarget 没有实现, 导致 * TheTarget.TargetMachineCtorFn 没有定义, TheTarget->createTargetMachine 返回 * NULL */ Target = std::unique_ptr<TargetMachine>(TheTarget->createTargetMachine( TheTriple.getTriple(), CPUStr, FeaturesStr, Options, RM, codegen::getExplicitCodeModel(), OLvl)); assert(Target && "Could not allocate target machine!");
1.2. toy-2: LLVMInitializeToyTarget
实现 LLVMInitializeToyTarget.
RegisterTargetMachine 会设置 TheTarget 的 TargetMachineCtorFn, 使得 TheTarget->createTargetMachine 返回 ToyTargetMachine 实例.
1.2.1. 测试
$> toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/lib/CodeGen/LLVMTargetMachine.cpp:42: void llvm::LLVMTargetMachine::initAsmInfo(): Assertion `MRI && "Unable to create reg info"' failed.
出错的原因是没有调用 RegisterMCRegInfo, 导致 initAsmInfo 时出错.
1.3. toy-3: LLVMInitializeToyTargetMC
LLVMInitializeToyTargetMC 会设置一个回调函数, 这些回调会由 initAsm 时通过 TheTargt 的 createXXX 调用以初始化 TheTarget 的 MRI, MII, STI, AsmInfo 等.
MRI
MCRegisterInfo
寄存器的编号, 名字等, 主要信息由 td 生成
MII
MCInstrInfo
指令的编码, 名字等, 主要信息由 td 生成
STI
MCSubtargetInfo
subtarget 对应调用 llc 时指定的 `-mcpu`, `-mattr` 等信息. llc 会用这些信息调用 STI 对应的回调函数以初始化 STI.
subtarget 的信息是由 td 生成的
AsmInfo
MCAsmInfo
需要包含一些 asm 文件的格式信息, 例如 comment 对应的 `#` 符号
在定义 STI 时使用了 td 文件, td 文件需要在 cmake 中指定 tablegen 命令的参数以及生成头文件的名字, 例如
set(LLVM_TARGET_DEFINITIONS Toy.td) tablegen(LLVM ToyGenSubtargetInfo.inc -gen-subtarget) add_public_tablegen_target(ToyCommonTableGen)
表示 td 的入口是 Toy.td, 使用 `-gen-subtarget` 生成 ToyGenSubtargetInfo.inc
1.3.1. 测试
~/source/llvm-toy#toy[17:43:49]@sunway-t14> ./toy_test.sh ; ModuleID = '/tmp/test.bc' source_filename = "toy_test/test.c" target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-pc-linux-gnu" ; Function Attrs: noinline nounwind optnone uwtable define dso_local void @foo() #0 { %1 = alloca i32, align 4 store i32 255, i32* %1, align 4 ret void } ... !llvm.module.flags = !{!0} !llvm.ident = !{!1} !0 = !{i32 1, !"wchar_size", i32 4} !1 = !{!"clang version 10.0.0-4ubuntu1 "} llc: error: target does not support generation of this file type ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
报错的原因是 Toy 没有指定一个 SelectionDAGISel 实例. SelectionDAGISel 是整个 isel (instruction selection) 的入口
1.4. toy-4: ToyDAGToDAGISel
通过 ToyTargetMachine 的 createPassConfig 函数, 注册一个 ToyDAGToDAGISel pass, 后者继承自 SelectionDAGISel, 需要实现一个 `Select` 函数做为 isel 的入口. 这里的 Select 函数直接调用了 td 根据 patten 生成的 SelectCode 函数. tablegen 的 `gen-dag-isel` 需要 td 中定义一个 RegisterClass
1.4.1. 测试
$> ./toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/lib/MC/MCAsmStreamer.cpp:85: (anonymous namespace)::MCAsmStreamer::MCAsmStreamer(llvm::MCContext &, std::unique_ptr<formatted_raw_ostream>, bool, bool, llvm::MCInstPrinter *, std::unique_ptr<MCCodeEmitter>, std::unique_ptr<MCAsmBackend>, bool): Assertion `InstPrinter' failed.
出错的原因是没有实现 InstPrinter
1.5. toy-5: ToyInstPrinter
ToyInstPrinter 继承自 MCInstPrinter, 需要实现 printInst, printRegName, printOperand 等函数. 它会使用 tablegen 的 `-gen-asm-writer` 生成的函数例如 printInstruction, getRegisterName 等
1.5.1. 测试
$> ./toy_test.sh llc: error: target does not support generation of this file type
出错的原因是没有实现 ToyAsmPrinter.
cmake 通过提供 target 目录是否存在 `*AsmPrinter.cpp` 来决定 llc 是否调用 LLVMInitializeToyAsmPrinter 来初始化到 asm printer.
llvm/CMakeLists.txt: ==================== file(GLOB asmp_file "${td}/*AsmPrinter.cpp") if( asmp_file ) set(LLVM_ENUM_ASM_PRINTERS "${LLVM_ENUM_ASM_PRINTERS}LLVM_ASM_PRINTER(${t})\n") endif() AsmPrinters.def.in: ==================== @LLVM_ENUM_ASM_PRINTERS@ 如果前面找到 AsmPrinter.cpp, 则这里会展开成: LLVM_ASM_PRINTER(Toy) llc: ==================== inline void InitializeAllAsmPrinters() { #define LLVM_ASM_PRINTER(TargetName) LLVMInitialize##TargetName##AsmPrinter(); #include "llvm/Config/AsmPrinters.def" }
所以需要定义一个 ToyAsmPrinter.cpp, 并实现 LLVMInitializeToyAsmPrinter 函数
1.6. toy-6: ToyAsmPrinter
ToyAsmPrinter 操作的是 MachineInstr, 它需要实现 emitStartOfAsmFile, emitFunctionBodyStart, emitInstruction 等, 其中 emitInstruction 需要转换 MachineInstr 到 MCInstr, 然后通过 MC 调用到 ToyInstPrinter
1.6.1. 测试
$> ./toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/tools/llc/llc.cpp:733: int compileModule(char **, llvm::LLVMContext &): Assertion `LLVMTM.getObjFileLowering() && "getObjFileLowering"' failed.
报错的原因是 ToyTargetMachine 没有实现 getObjFileLowering 函数
1.7. toy-7: ToyTargetObjectFile
AsmPrinter 会使用 TargetLoweringObjectFile 决定各种数据所在的 section
1.7.1. 测试
$> ./toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/lib/CodeGen/ExpandLargeDivRem.cpp:119: virtual bool (anonymous namespace)::ExpandLargeDivRemLegacyPass::runOnFunction(llvm::Function &): Assertion `TM->getSubtargetImpl(F)' failed.
出错的原因是不支持 subtarget
1.8. toy-8: ToySubtarget
ToyTargetMachine 需要实现 getSubtargetImpl 返回一个 ToySubtaget, 后续 isel 相关的功能例如 getRegisterInfo, getInstrInfo, getFrameLowering, getTargetLowering 都需要由 subtarget 提供
1.8.1. 测试
$> ./toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/lib/CodeGen/ExpandLargeDivRem.cpp:121: virtual bool (anonymous namespace)::ExpandLargeDivRemLegacyPass::runOnFunction(llvm::Function &): Assertion `TLI && "getTargetLowering is null"' failed.
出错的原因是 ToySubtarget 没有实现 getTargetLowering.
1.9. toy-9: ToyTargetLowering
TargetLowering 在 SelectionDAGBuilder 阶段会被调用, 用来生成最初的 SelectionDAG. 虽然最初的 SelectionDAG 基本是 target 无关, 但涉及到函数调用及其参数, 返回值的处理时需要 target 提供 TargetLowering 类, 并实现 LowerReturn 等函数
1.9.1. 测试
$> ./toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/lib/CodeGen/MachineFunction.cpp:193: void llvm::MachineFunction::init(): Assertion `STI->getFrameLowering()' failed.
出错的原因是 ToySubtarget 没有实现 getFrameLowering
1.10. toy-10: ToyFrameLowering
ToyFrameLowering 需要实现 emitPrologue 和 emitEpilogue, 它们由 PEI (prologue epilogue insertion) 这个 pass 调用.
emitPrologue 会获取 stack size, 然后通过 BuildMI 生成 MachineInstr 来调整 sp. 另外它还会生成 dwarf cfi directive.
PEI 操作的是 MachineInstr, 它发生成 scheduling 和 RA 之后, 因为 RA 之后才知道 stack size
1.10.1. 测试
$> ./toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp:3058: void llvm::SelectionDAGISel::SelectCodeCommon(llvm::SDNode *, const unsigned char *, unsigned int): Assertion `MatcherIndex < TableSize && "Invalid index"' failed.
出错的原因是 td 还没有定义 load pattern 对应的指令
1.11. toy-11: isel
写一个最简单的 ToyInstrInfo.td, 它会把 immediate 这个 pattern 转换为 Toy::ADDI, 并且把用于访问局部变量的 frameindex 转换为 Toy::STORE
===== Instruction selection begins: %bb.0 '' ISEL: Starting selection on root node: t5: ch = store<(store (s32) into %ir.1)> t0, Constant:i32<255>, FrameIndex:i32<0>, undef:i32 ISEL: Starting pattern match Creating constant: t7: i32 = TargetConstant<0> Morphed node: t5: ch = STORE<Mem:(store (s32) into %ir.1)> Constant:i32<255>, TargetFrameIndex:i32<0>, TargetConstant:i32<0>, t0 ISEL: Match complete! ISEL: Starting selection on root node: t1: i32 = Constant<255> ISEL: Starting pattern match Initial Opcode index to 51 Creating constant: t9: i32 = TargetConstant<255> Morphed node: t1: i32 = ADDI Register:i32 $physreg1, TargetConstant:i32<255> ISEL: Match complete! ISEL: Starting selection on root node: t0: ch,glue = EntryToken ===== Instruction selection ends: Selected selection DAG: %bb.0 'foo:' SelectionDAG has 7 nodes: t1: i32 = ADDI Register:i32 $physreg1, TargetConstant:i32<255> t0: ch,glue = EntryToken t5: ch = STORE<Mem:(store (s32) into %ir.1)> t1, TargetFrameIndex:i32<0>, TargetConstant:i32<0>, t0
1.11.1. 测试
$> ./toy_test.sh llc: /home/sunway/source/llvm-toy/llvm/lib/CodeGen/SelectionDAG/ScheduleDAGRRList.cpp:368: virtual void (anonymous namespace)::ScheduleDAGRRList::Schedule(): Assertion `TRI' failed.
出错的原因是 schedule 时需要 subtarget 实现 getRegisterInfo
1.12. toy-12: ToyRegisterInfo
ToyRegisterInfo 需要实现 `getCalleeSavedRegs` 等函数, 后续 RA, PEI 等会使用它
1.12.1. 测试
$> ./toy_test.sh *** Final schedule *** SU(1): t1: i32 = ADDI Register:i32 $zero, TargetConstant:i32<255> SU(0): t5: ch = STORE<Mem:(store (s32) into %ir.1)> t1, TargetFrameIndex:i32<0>, TargetConstant:i32<0>, t0 ... Target didn't implement TargetInstrInfo::storeRegToStackSlot! Stack dump: 0. Program arguments: ./build/bin/llc /tmp/test.bc -march=toy --debug 1. Running pass 'Function Pass Manager' on module '/tmp/test.bc'. 2. Running pass 'Prologue/Epilogue Insertion & Frame Finalization' on function '@foo' ... #9 0x000000000134b9d2 insertCSRSaves(llvm::MachineBasicBlock&, llvm::ArrayRef<llvm::CalleeSavedInfo>) /home/sunway/source/llvm-toy/llvm/lib/CodeGen/PrologEpilogInserter.cpp:602:5 #10 0x0000000001348994 (anonymous namespace)::PEI::spillCalleeSavedRegs(llvm::MachineFunction&) /home/sunway/source/llvm-toy/llvm/lib/CodeGen/PrologEpilogInserter.cpp:681:41 #11 0x000000000134768a (anonymous namespace)::PEI::runOnMachineFunction(llvm::MachineFunction&) /home/sunway/source/llvm-toy/llvm/lib/CodeGen/PrologEpilogInserter.cpp:252:3
出错的原因是 PEI 生成 prologue 时为了把 CSR 保存到栈上, 需要实现 storeRegToStackSlot
1.13. toy-13: storeRegToStackSlot
storeRegToStackSlot 需要生成 MachineInstr, 把 reg (例如 RA) 保存到栈上
1.13.1. 测试
$> ./toy_test.sh Found roots: %bb.0 Skipping pass 'Shrink Wrapping analysis' on function foo alloc FI(1) at SP[-4] alloc FI(0) at SP[-8] STORE killed $ra, %stack.1, 0 STORE killed $ra, %stack.1, 0 STORE killed $ra, %stack.1, 0 STORE killed $ra, %stack.1, 0 ....
程序陷入死循环, 原因是 eliminateFrameIndex 目前实现为空.
1.14. toy-14: eliminateFrameIndex
PEI 会调用 eliminateFrameIndex 把使用了 frameindex 的 MachineInstr (例如 `STORE ra, addr, 0`) 修改成 `STORE ra, sp, N`.
eliminateFrameIndex 需要根据 frameindex 的值以及 stack_size 计算出正确的偏移量, 然后修改 MI 的 operand, 把原来的 (addr, 0)替换成 (sp, offset)
1.14.1. 测试
$> ./toy_test.sh EmitInstruction not implemented UNREACHABLE executed at /home/sunway/source/llvm-toy/llvm/include/llvm/CodeGen/AsmPrinter.h:572! Stack dump: ... #9 0x0000000000c410f6 llvm::AsmPrinter::emitFunctionBody() /home/sunway/source/llvm-toy/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp:1725:13 #10 0x0000000000c10671 llvm::AsmPrinter::runOnMachineFunction(llvm::MachineFunction&) /home/sunway/source/llvm-toy/llvm/include/llvm/CodeGen/AsmPrinter.h:4 ...
出错的原因是没有实现 AsmPrinter 的 emitInstruction
1.15. toy-15: emitInstruction
AsmPrinter 可以重写许多 emit 函数, 例如 emitFunctionBodyStart 等, 但这些都为默认的实现. 但 emitInstruction 是 target 必需实现的.
emitInstruction 的功能是把 MachineInstr 转换为 MCInst, 然后交给 MCStream, 后者会调用到 MCInstPrinter 中的接口, 例如 printInst
1.15.1. 测试
./toy_test.sh Debug Range Extension: foo .globl foo # -- Begin function foo .type foo,@function foo: # @foo # %bb.0: Lfunc_end0: .size foo, Lfunc_end0-foo # -- End function
llc 能正常结束, 但输出的 asm 基本为空, 原因是 ToyInstPrinter 中 printInst 等目前的实现为空
1.16. toy-16: printInst
ToyInstPrinter 需要使用 td 生成的信息来实现 printInst, printRegName 等
1.16.1. 测试
$> ./toy_test.sh Debug Range Extension: foo .globl foo # -- Begin function foo .type foo,@function foo: # @foo # %bb.0: sw ra, 4(sp) addi ra, zero, 255 sw ra, 0(sp) addi ra, zero, 255 sw ra, 0(sp) sw ra, 0(sp) Lfunc_end0: .size foo, Lfunc_end0-foo # -- End function
针对 mem operand 使用了自定义的 printMemOperand, 而不会调用默认的 printOperand.
现在的代码看起来有两个问题:
- addi 不应用使用 ra, 需要定义更多的 register
- 最后两行 `sw ra, 0(sp)` 是什么
1.17. toy-17: add registers
1.17.1. 测试
$> ./toy_test.sh Debug Range Extension: foo .globl foo # -- Begin function foo .type foo,@function foo: # @foo # %bb.0: addi t0, zero, 255 sw t0, 0(sp) Lfunc_end0: .size foo, Lfunc_end0-foo # -- End function
1.18. toy-18: add more insns
添加 store/add/and/or/andi/ori 指令. 由于目前不涉及到 object 文件的生成, 所以去掉了 td 中关于指令格式的内容.
添加指令时 patten 可以有两种写法:
写在 Instruct 的 patten 中, 例如:
def LOAD : ToyInst<(outs GPR:$ra), (ins mem:$addr), "lw \t$ra, $addr", [(set GPR:$ra, (load AddrFI:$addr))], IIAlu>;
使用 Pat, 例如:
def LOAD : ToyInst<(outs GPR:$ra), (ins mem:$addr), "lw \t$ra, $addr", [], IIAlu>; def : Pat<(load AddrFI:$addr), (LOAD AddrFI:$addr)>;
1.19. toy-19: simplify insn definition
定义 ADD, DIV, REM, … 时有许多重复的内容, 可以使用 td 的 class 简化这些指令的定义
1.20. toy-20: global address
目前 toy 还不支持 global address, 所以编译下面的程序会报错:
$> cat toy_test/test.c int x = 0; void foo() { int l = x; } $> ./toy_test.sh LLVM ERROR: Cannot select: t4: i32,ch = load<(dereferenceable load (s32) from @x)> t0, GlobalAddress:i32<ptr @x> 0, undef:i32
对于 global address 的处理发生成 legalize 阶段, ToyTargetLower 需要通过 `setOperationAction` 标记 ISD::GlobalAddress 的 action 为 custom, 同时实现 LowerOperation 来处理这个 node: 生成 `add (lui %hi(x)) %lo(x))` 对应的 node
1.20.1. 测试
$> ./toy_test.sh ===== Instruction selection ends: Selected selection DAG: %bb.0 'foo:' SelectionDAG has 9 nodes: t9: i32 = LUI TargetGlobalAddress:i32<ptr @x> 0 [TF=1] t10: i32 = ADDI t9, TargetGlobalAddress:i32<ptr @x> 0 [TF=2] t0: ch,glue = EntryToken t4: i32,ch = LOAD<Mem:(dereferenceable load (s32) from @x)> t10, t0 t6: ch = STORE<Mem:(store (s32) into %ir.1)> t4, TargetFrameIndex:i32<0>, TargetConstant:i32<0>, t4:1 ... foo: # @foo # %bb.0: unknown operand type UNREACHABLE executed at /home/sunway/source/llvm-toy/llvm/lib/Target/Toy/ToyMCInstLower.cpp:25! ... #8 0x0000000000c26775 llvm::ToyMCInstLower::LowerOperand(llvm::MachineOperand const&) const /home/sunway/source/llvm-toy/llvm/lib/Target/Toy/ToyMCInstLower.cpp:27:33
isel 成功, 但在 AsmPrinter 时出错, 因为 LUI 及 ADDI 的 operand 是 GlobalAddress, 当前的 LowerOperand 只支持 register 和 imm
1.21. toy-21: directly lower to machine code
前面的 LowerOperation 把 global address lower 成了 ISD:ADD, 实际上可以直接 lower 成 Toy::ADDI, 省略后续 isel 时 ISD::ADD 到 Toy::ADDI 的过程. 由于 isel 的输入可能是 machine code, 所以 ToyDAGToDAGISel 的 Select 需要忽略掉 SDNode 已经是 machine code 的情况
1.22. toy-22: lower MachineOperand to MCOperand
前面 LUI 转换成 MacineInstr 后它的 MachineOperand 是 MO_GlobalAddress. ToyMCInstLower 的 LowerOperand 需要把它 lower 成 MCOperand.
这个 MCOperand 的 kind 既不是 register, 也不是 immediate, 因此需要定义ToyMCExpr, 做为 MCOperand 的 expr.
ToyMCExpr 保存着原始的 imm 和 kind (hi/lo) 信息. ToyInstPrinter 会调用到 ToyMCExpr::printImpl 最终根据它保存的 kind 打印出 `%hi` 和 `%lo`
1.23. toy-23: store global variable
前面的代码不支持全局变量的赋值, 因为缺少 `store $rs1, $rs2` 这个 pattern
1.24. toy-24: LowerReturn
return 语句需要通过 ToyISelLowering::LowerReturn 处理. 这个过程发生在最初的 SelectionDAGBuilder 阶段.
LowerReturn 需要分析需要 return 的值, 生成一些指令把这些值按调用约定处理 (放在寄存器, 放在栈上), 最后还生成一个 ToyISD::Ret.
目前的 LowerReturn 只处理了 return void 的情况
ToyISD::Ret 使用了自带的 PseudoInstExpansion 机制, 后者会使用 tablegen 生成和 ToyMCInstLower 类似的代码, 把 Toy::Ret lower 成 JALR 对应的 MCInst.
如果不考虑生成 obj, 也可以直接 def RET : InstI<(outs),(ins), "jalr zero,0(ra)",
[(ToyRET)], IIAlu>;
, 这样使用默认的 ToyMCInstLower 就可以处理.
1.25. toy-25: emitPrologue and emitEpilogue
当前生成的代码是错误的, 例如:
$> cat test.c void foo() { int x = 1; int y = x; int z = y; } $> ./toy_test.sh foo: # @foo # %bb.0: addi t0, zero, 1 sw t0, 8(sp) lw t0, 8(sp) sw t0, 4(sp) lw t0, 4(sp) sw t0, 0(sp) jalr zero, 0(ra)
foo 的入口需要有 `addi sp, sp, -12`, 称为 prologue foo 的返回前需要有 `addi sp, sp, 12`, 称为 epilogue
其中 `8` 是 stack size, 取决于:
- 是否保存 fp, 如果有 fp 且没有使用 omit-frame-pointer 则需要保存
- 是否保存 ra, 如果函数不是 leaf function 则需要保存
- 是否保存 callee saved reg (CSR), 如果函数使用了它们, 则需要保存和恢复
- 局部变量占用的栈空间
- …
emitPrologue 本身并不负责 CSR 的 spill 和 restore, 它们由 PEI 负责 (spillCalleeSavedRegs)
emitPrologue 发生在 schedule 及 RA 之后, 所以它们操作的是 MachineInstr 和物理寄存器
epilogue 与 prologue 对应, 但如果函数不需要返回, 则不需要 epilogue (参考 ToyInstrInfo.td 中的 `isReturn = 1`)
1.26. toy-26: LowerCall Pt. 1
call 需要 ToyISelLowering::LowerCall 来处理, 它负责按调用约定处理函数调用的参数 (例如放在特定物理寄存器或放在栈上), 然后生成跳转指令, 并处理函数返回的结果 (例如从特定物理寄存器或栈上获得结果)
当前的实现只生成了跳转指令, 所以只能支持 `void foo()` 类型的函数调用
1.26.1. 测试
#> ./toy_test.sh .text .file "test.c" .globl foo # -- Begin function foo .type foo,@function foo: # @foo # %bb.0: lui t0, %hi(foo) addi t0, t0, %lo(foo) jalr ra, 0(t0) jalr zero, 0(ra) Lfunc_end0: .size foo, Lfunc_end0-foo # -- End function
能生成 `jalr ra, 0(t0)` , 但有一个问题: 没有针对 ra 做 CSR 的 spill 和 restore
1.27. toy-27: LowerCall Pt. 2
CSR 需要保存的前提是函数不是 leaf function, 通过设置 td 中 CALL 指令 `isCall = 1` 让 llvm 知道函数 `hasCalls()`
由于 RA 寄存器 并非由 register allocator 分配, 所以需要重写 determineCalleeSaves, 把 RA 加到 SavedRegs 里
最后需要实现 loadRegFromStackSlot, 以便生成 restore ra 的指令
1.28. toy-28: LowerFormalArguments
LowerFormalArguments 负责按调用约定从物理寄存器或栈上获得函数的参数, ToyCallingConv.td 中的
def ToyCC : CallingConv<[ CCIfType<[i32], CCAssignToReg<[A0, A1]>> ]>;
会生成一个 ToyCC 函数, LowerFormalArguments 调用它获得参数使用的物理寄存器或栈上的位置, 然后生成一些 SDNode (例如 getCopyFromReg) 获得这些参数
目前的实现只支持通过寄存器传递最多两个 i32 类型的参数
1.29. toy-29: LowerCall Pt. 3
LowerCall 时除了生成跳转指令, 还需要按调用约定把参数放在物理寄存器或栈上, 它使用了和前面 LowerFormalArguments 类似的代码.
目前的实现只支持通过寄存器传递最多两个 i32 类型的参数
1.30. toy-30: LowerReturn Pt. 2
LowerReturn 不仅要生成跳转指令, 还需要按调用用约定把返回值放在物理寄存器或栈上.
目前的实现只支持通过寄存器返回一个 i32 类型的返回值
1.31. toy-31: LowerCall Pt. 4
LowerCall 最后还需要根据调用约定从物理寄存器或栈上获得函数的返回值
1.32. toy-32: frameindex with constant offset
访问栈上的 `a[1]` 时会使用带 offset 的 frameindex
1.33. toy-33: global address with constant offset
访问全局的 `a[1]` 时会使用带 offset 的 global address
1.34. toy-34: setcc
riscv 只支持 slt, 但 slt, sgt, seq, sne 都可以用 slt 实现
1.35. toy-35: br_cc
br_cc 是条件跳转. br_cc 先通过 expand 转换成 brcond, 然后再用 blt 等指令匹配 brcond
1.36. toy-36: type promotion
目前的实现里 add 指令只支持 32 位, 为了支持 8/16 位的加法, llvm 的 DAGTypeLegalizer 会在 load/store 时进行 type promotion, 例如 store 时通过 truncate 把 i32 变成 i8, 或 load 时通过 sext 把 i8 变成 i32.
这里的 truncate, sext 信息包含在 load/store 中, 后端需要匹配这些 patten 生成 lb/sb 等指令.
1.37. toy-37: LowerCall Pt. 5
LowerCall 需要按约定把多余的参数放在栈上, LowerFormalArguments 需要从栈上取这些参数
caller 与 callee 的栈布局为:
caller: callee: local_1 local_1 local_2 local_2 ... ... arg1 arg2 <-- [sp]
即 caller 把参数放在栈顶, callee 直接从 caller 的栈帧取参数, 对应的代码为:
bar: addi sp, sp, -12 # NOTE: bar 的 stack size 为 12, 但它访问了 sp+12 和 sp+16, 即 foo 的栈帧 lw t0, 16(sp) lw t0, 12(sp) sw a0, 8(sp) ... foo: # @foo addi sp, sp, -16 sw ra, 12(sp) addi a0, zero, 1 addi a1, zero, 2 addi a2, zero, 3 addi t0, zero, 4 sw t0, 0(sp) # NOTE: foo 的 stack size 原来为 8, 由于参数会使 # 用 sp+0, sp+4, ..., 导致需要在 # emitPrologue/emitEpilogue 时调整 stack # size. 另外为了避免与原有 local 变量冲突, 会在 # eliminateFrameIndex 加上一个 offset addi t0, zero, 5 sw t0, 4(sp) call bar sw a0, 8(sp) lw a0, 8(sp) lw ra, 12(sp) addi sp, sp, 16 ret
1.38. toy-38: glue
由于 call, ret 隐含着使用 a0, a1 等寄存器, 为了避免寄存器分配时对这些寄存器的错误的优化, 需要把它们也放在指令中, 同时使用 glue 把这些寄存器 glue 在一起, 防止 scheduler 错误的优化. 例如:
测试代码为:
void bar(int a, int b, int c) {} int foo(int a, int b) { bar(1, 2, 3); return 1; }
若 LowerReturn 时没有标记 ret 需要使用 a0, 则会生成下面的代码:
foo: # @foo # %bb.0: addi sp, sp, -16 sw ra, 12(sp) sw s0, 8(sp) sw a0, 4(sp) sw a1, 0(sp) addi s0, zero, 1 addi a1, zero, 2 addi a2, zero, 3 add a0, zero, s0 call bar # NOTE: ret 前对 a0 的赋值消失了 lw s0, 8(sp) lw ra, 12(sp) addi sp, sp, 16 ret
若 LowerCall 时没有记录 call 使用了 a0, 则生成下面的代码:
foo: addi sp, sp, -12 sw ra, 8(sp) sw a0, 4(sp) sw a1, 0(sp) # NOTE: a2 的赋值消失了 call bar addi a0, zero, 1 lw ra, 8(sp) addi sp, sp, 12 ret
只记录寄存器的使用并不能完全正常工作, 还需要 glue 把 ret 和 a0 glue 在一起, 没有 glue 时, 可能会生成这样的代码:
foo: # ... addi a0, zero, 1 lw ra, 8(sp) addi sp, sp, 12 # NOTE: call 应该在 addi 之前, 但 ret 和 a0 间没有 glue 可能导致这种错误的结 # 果 call bar ret
1.39. toy-39: soft float
toy 当前不支持 hard float, 导致 legalize 时会通过 libcall 的形式调用到 libgcc 中的函数
1.40. toy-40: return struct
参考 calling convention, 返回结构体时有两种做法:
- 当结构体较小时使用一个或多个寄存器返回
- 当结构体较大时会传入一个隐式的指向结构体的指针, 即 named return value 优化
clang 在生成 llir 时就会根据传入的 target 信息决定用哪种方法.
当使用第二种方法时, caller 需要实现 `set rd, frameindex` 这个 patten, 用来传递结构体指针 (而不是之前实现的 `set rd, (load frameindex)`
1.41. toy-41: hard float Pt. 1
支持硬浮点需要:
- 通过 `addRegisterClass(MVT::f32, &Toy::FPRRegClass)` 告诉 llvm 在 legalize type (LegalizeTypes.cpp) 时不要使用软浮点
- 添加浮点寄存器
- 在 isel lowering 时处理 constant pool, 因为浮点数常量是使用的 constant pool, 而不是像整数常量那样直接 encode 在指令中
- 添加指令及其 pattern, 当前的 `float.c` 中需要实现 fadd, load, store, brcond 及 setune. 由于 RISC-V 缺少 f32 br_cc (blt, beq, …) 指令, 所以 f32 br_cc 会使用 f32 setcc 和 i32 br_cc 实现
1.42. toy-42: hard float Pt. 2
加入基本的 f64 寄存器和指令.
当处理 `float x=0.1` 时, 后端需要支持 truncstoref32. truncstoref32 实际做了两件事:
- 把 f64 truncate 成 f32
- 保存 f32 到内存
riscv 中没有对应的指令, 需要用到 `setTruncStoreAction(MVT::f64, MVT::f32, Expand)`, 让 llvm 把它 expand 成 fround 和 store 两个指令, 分别对应 riscv 的 fcvt.s.d 和 fsw 指令
1.43. toy-43: builtin Pt. 1
以 fma 和 fmaxf 为例
1.43.1. fma
clang 会发现代码可以转换为 fma, 会把它转换为 fmuladd 的 instrinsic, 然后 SelectionDAGBuilder::visitIntrinsicCall 会负责把它转换为 fma 指令. 但如果 isFMAFasterThanFMulAndFAdd 为 false, 则会转换为 fmul 和 fadd, 而不是 fma 指令.
因为实现 fma 有两种做法:
定义 isFMAFasterThanFMulAndFAdd 返回 true, 并实现 fma 指令
def FMADDS : InstR<(outs FPR:$rd), (ins FPR:$rs1, FPR:$rs2, FPR: $rs3), "fmadd.s\t$rd, $rs1, $rs2, $rs3", [(set FPR:$rd, (fma FPR:$rs1, FPR:$rs2, FPR:$rs3))], IIAlu>;
在 td 中直接匹配复杂的 pattern:
def FMADDS : InstR<(outs FPR:$rd), (ins FPR:$rs1, FPR:$rs2, FPR:$rs3), "fmadd.s\t$rd, $rs1, $rs2, $rs3", [(set FPR:$rd, (fadd (fmul FPR:$rs1,FPR:$rs2), FPR:$rs3))], IIAlu>;
1.43.2. fmaxf
__builtin_fmaxf 在 visitIntrinsicCall 会转换为 fmaxnum, 但 TargetLoweringBase 默认把 fmaxnum 的 action 设置为 expand, 因此需要 setOperationAction 为 Legal 后再实现 fmaxnum 指令
1.44. toy-44: write object file Pt. 1
https://blog.llvm.org/2010/04/intro-to-llvm-mc-project.html
MCInst 在整个 MC 层处于中心的位置:
MCInstPrinter: 把 MCInst 转换为 asm MCCodeEmitter: 把 MCInst 转换为 binary MCTargetAsmParser: 把 asm 转换为 MCInst MCDisassembler: 把 binary 转换为 MCInst
目前的实现都是 stub, `make xxx.o` 可以生成一个空的 obj 文件
1.45. toy-45: write object file Pt. 2
支持 I 指令 (addi, lw…), S 指令 (sw, …) 和 R 指令 (add, sub, …) 的 encoding
去掉了针对 AddrFI 的 mem operand. 由于 I 指令中关于 imm 编码的特殊性, 目前无法实现其 EncoderMethod. 对于通过 ComplexPattern 匹配的 operand, 实际上可以通过 `[(load GPR:$rs2, (AddrFI GPR:$rs1, imm12:$imm))]` 的形式使用, 无需使用 mem 这种 operand
`make arith.o` 产生的 object 除了伪指令 ret, 其它都是正确的.
1.46. toy-46: write object file Pt. 3
目前使用伪指令实现 RET 和 CALL, 在输出 asm 比较容易, 但没有考虑 encoding 的问题.
RET 转换成 jalr 比较容易, 它直接对应了 `jalr zero, 0(ra)`.
CALL 转换 jalr 时由于涉及到 symbol 的问题, 需要转换为 `lui t0, %hi(addr); addi t0, %lo(addr); jarl ra, 0(t0)`, 其中 hi/lo 需要转换为 fixup 信息 (类型, 原指令需要被 patch 的 offset 和长度) 保存在 object 中 Linker Relocation
`make run_arith BINARY=1` 可以正常运行, 但还剩几个伪指令 (j, lea, …) 和 branch 没有处理
1.47. toy-47: write object file Pt. 4
j 指令需要使用 jal 实现, 并使用 jal 类型的 fixup
branch 指令和 jal 类似, 但使用 branch 的 fixup
`make run BINARY=1` 现在都能正常运行.
1.48. toy-48: li
li 伪指令用来把 imm 赋值给 gpr, 如果 imm 为 imm12, 可以用 `addi` 实现, 否则需要用 `lui` 和 `addi` 来实现.
需要注意的是针对 imm 获得 lui 需要的高 20 位并不能直接取高 20 位, 因为低 12 位送给 addi 时有可能被当做负数.
例如 imm = 0x1fff, RISCV_CONST_HIGH_PART(imm) 宏返回 0x2000, 而不是 0x1000, 因为低 12 位 0xfff 在 addi 时会 sext 成负数
1.49. toy-49: intrinsics
为了添加一个 intrinsic, 需要同时修改 clang 和 llvm
clang
clang 需要把 c 代码中的 `__builtin_xxx` 转换成 llir 中的 `@llvm.xxx`
intrinsic 定义在 BUiltins{xxx}.def 中.
clang 的 CGBuiltin.cpp 在处理 intrinsic 时需要决定是否生成 @llvm.<target>.xxx 形式的 intrinsic 调用. c 代码中的 __builtin_xxx 并不一定都会转换为 llvm instrinsic, 有的会转换为 libcall. 例如
# clang/Basic/Builtins.def LIBBUILTIN(sqrt, "dd", "fne", MATH_H, ALL_LANGUAGES)
中通过 `e` 表示 sqrt 可能会出错, 需要返回 errno (例如输入为负数), 这时只能交给 libcall 处理. 所以 c 代码中的 `__builtin_sqrtf` 或 `sqrtf` 不会转换为 `@llvm.sqrt.f32` (除非使用了 -fno-math-error 等 flag, 参考 CGBuiltin.cpp::ConstWithoutErrnoAndExceptions)
llvm
llvm 需要把 `@llvm.xxx` 形式的 llvm intrinsic lower 成最终的指令.
llvm 需要修改 Instrinsics.td, intrinsic 的名字有特定的要求, 例如 td 中定义的 intrinsic 为 int_<target>_xxx, llir 中对应的是 `@llvm.<target>.xxx`.
另外, intrinsic 可以重载, 例如:
def int_fabs : DefaultAttrsIntrinsic<[llvm_anyfloat_ty], [LLVMMatchType<0>]>;
其中的 anyfloat 会导致 llir 中可以使用 `@llvm.fabs.f32`, `@llvm.fabs.f64`
最后需要添加 intrinsic 对应的 lowering, 可以直接在 td 中匹配 `int_toy_getsp`这种 patten, 也可以参考 LowerINTRINSIC_WO_CHAIN 使用 custom 的 lowering