CPU架构的llvm后端

摘要:
对于Cpu0SExx类的名称,表示标准32位类。遵循llvm3.5Mips后端风格。图15继承自TableGen生成文件的Cpu0类Fig.15Cpu0classesinheritedfromTableGengeneratedfiles由于llvm有很深的继承树,这里就不深挖了。TheCpu0Target用于大端,TheCpu0elTarget用于小端。Cpu0MCAsmInfo派生自MCAsmInfo,一个llvm内置类。))ISel:指令选择RVR:重写虚拟寄存器,删除CopyToRegAsmP:Cpu0Asm打印Post-RA:Post-RA伪指令扩展pass从上面的llc-print-before-all-print-after-all显示,ret在stageOptimizedlegalizedselectionDAG中,翻译成Cpu0ISD::Ret,最后翻译成Cpu0指令ret。Chapter3_4/Cpu0ISelLowering.cpp在LowerReturn()中创建Cpu0ISD::Ret节点,当llvm系统遇到C的return关键字时调用。添加序言/尾声功能概念以下来自tricore_llvm.pdf部分“4.4.2非静态寄存器信息”。
Creating an LLVM Backend for the Cpu0 Architecture

Backend structure

CPU架构的llvm后端第1张

Fig. 14Cpu0 backend class access link

图 14Cpu0 后端类访问链接

添加了大多数 Cpu0 后端类,代码可以概括为图14。类 Cpu0Subtarget 提供接口 getInstrInfo(),getFrameLowering(),...,获取其它 Cpu0 类。大多数类(如 Cpu0InstrInfo,Cpu0RegisterInfo 等),都有 Subtarget 引用成员,允许通过 Cpu0Subtarget 接口,访问其它类。如果后端模块没有 Subtarget 引用,这些类仍然可以通过 static_cast<Cpu0TargetMachine &>(TM).getSubtargetImpl(),通过 Cpu0TargetMachine(通常使用 TM 作为符号)访问 Subtarget 类。一旦获取到 Subtarget 类,后端代码就可以访问其它类。对于 Cpu0SExx 类的名称,表示标准32 位类。遵循 llvm 3.5 Mips 后端风格。Mips 后端使用 Mips16,MipsSE 和 Mips64 文件/类名称,分别为 16,32 和 64 位架构定义类。

图15显示了 Cpu0 TableGen 的继承关系。后端类可以包含 TableGen 生成的类并从中继承。Cpu0后端的所有TableGen生成的类,都在build/lib/Target/Cpu0/*.inc中。通过 C++ 继承机制,TableGen 为后端程序员,提供了一种灵活的方式,使用生成的代码。如果需要,程序员有机会覆盖此功能。

CPU架构的llvm后端第2张

图 15继承自 TableGen 生成文件的 Cpu0 类

Fig. 15Cpu0 classes inherited from TableGen generated files

由于llvm有很深的继承树,这里就不深挖了。受益于继承树结构,不需要在指令,帧/堆栈和选择 DAG 类中,实现太多代码,很多代码是由父类实现的。llvm-tblgen 根据Cpu0InstrInfo.td 的信息,生成 Cpu0GenInstrInfo.inc。Cpu0InstrInfo.h 通过定义“#define GET_INSTRINFO_HEADER”,从 Cpu0GenInstrInfo.inc 中,提取需要的代码。使用TabelGen,通过编译器开发的模式匹配理论,减少了后端的代码量。这在 “DAG”和“指令选择”中,都有解释。

To make the registration clearly, summary as the following diagram,Fig. 16.

CPU架构的llvm后端第3张

图 16Tblgen 为 Cpu0 后端生成文件

Fig. 16Tblgen generate files for Cpu0 backend

createCpu0MCAsmInfo() 为目标 TheCpu0Target 和 TheCpu0elTarget,注册了类 Cpu0MCAsmInfo 的对象。TheCpu0Target 用于大端,TheCpu0elTarget 用于小端。Cpu0MCAsmInfo 派生自 MCAsmInfo,一个 llvm 内置类。大多数代码在父级中实现,后端通过继承重用这些代码。

createCpu0MCInstrInfo() 实例化 MCInstrInfo 对象X,通过 InitCpu0MCInstrInfo(X) ,进行初始化。由于 InitCpu0MCInstrInfo(X) 是在 Cpu0GenInstrInfo.inc 中定义的,所以这个函数会添加指定的 Cpu0InstrInfo.td 中的信息。

createCpu0MCInstPrinter() 实例化 Cpu0InstPrinter,支持打印功能的说明。

createCpu0MCRegisterInfo()类似于“MC指令信息的注册函数”,初始化了Cpu0RegisterInfo.td中,指定的寄存器信息。共享来自指令/寄存器 td 描述的一些值,如果与 td 描述文件一致,无需在 Initialize 例程中,再次指定。

createCpu0MCSubtargetInfo() 实例化 MCSubtargetInfo 对象,使用 Cpu0.td 信息,进行初始化。

根据“目标注册部分”,可以通过动态注册机制,在 LLVMInitializeCpu0TargetMC() 按需注册 Cpu0 后端类,如上述函数 LLVMInitializeCpu0TargetMC()。

现在,可以使用 AsmPrinter,如下所示,

Summary above translation into Table: Chapter 3 .bc IR instructions.

CPU架构的llvm后端第4张
下层:初始选择 DAG(Cpu0ISelLowering.cpp,LowerReturn(…))

  • ISel:指令选择
  • RVR:重写虚拟寄存器,删除 CopyToReg
  • AsmP:Cpu0 Asm 打印
  • Post-RA:Post-RA 伪指令扩展pass

从上面的llc-print-before-all-print-after-all显示,ret在stage Optimizedlegalizedselection DAG中,翻译成Cpu0ISD::Ret,最后翻译成Cpu0指令ret。由于 ret 使用常量 0(在此示例中为ret i32 0),因此常量 0通过Cpu0InstrInfo.td 定义的以下模式,转换为“addiu $2, $zero, 0”

Cpu0ISelLowering.cpp 的函数LowerReturn() 正确处理返回变量。Chapter3_4/Cpu0ISelLowering.cpp在LowerReturn()中创建Cpu0ISD::Ret节点,当llvm系统遇到C的return关键字时调用。创建 DAG(Cpu0ISD::Ret (CopyToReg %X, %V0, %Y), %V0, Flag)。由于 V0 寄存器,在 CopyToReg 中分配,Cpu0ISD::Ret 使用 V0,带有 V0 寄存器的 CopyToReg,继续存在,不会在任何后续优化步骤中删除。如果使用“return DAG.getNode(Cpu0ISD::Ret, DL, MVT::Other, Chain, DAG.getRegister(Cpu0::LR, MVT::i32));”,不是“返回 DAG.getNode (Cpu0ISD::Ret, DL, MVT::Other, &RetOps[0], RetOps.size());”,V0 寄存器将不会生效,DAG(CopyToReg %X, %V0, %Y)将在以后的优化步骤中删除。

添加序言/尾声功能

概念

以下来自 tricore_llvm.pdf 部分“4.4.2 非静态寄存器信息”。

对于某些目标架构,目标架构的寄存器集的某些方面,取决于可变因素,必须在运行时确定。不能从 TableGen,描述静态生成——尽管在 TriCore 后端,大部分是可能的。有以下几点:

  • 调用者保存的寄存器。通常,ABI 指定一组寄存器,如果内容在执行期间可能被修改,函数必须在进入时,保存这些寄存器,在返回时,恢复这些寄存器。
  • 保留寄存器。尽管 TableGen 文件中,已经定义了一组不可用的寄存器,TriCoreRegisterInfo 包含一个方法,用于在位向量中,标记所有不可分配的寄存器编号。

实现了以下方法:

  • emitPrologue() 在函数的开头,插入序言代码。由于 TriCore 的上下文模型,这是一项微不足道的任务,不需要手动保存任何寄存器。唯一需要做的,通过递减堆栈指针,为函数的堆栈帧保留空间。如果函数需要一个帧指针,帧寄存器 %a14 被预先设置为堆栈指针的旧值。
  • emitEpilogue() 旨在发出指令,在从函数返回之前,销毁堆栈帧,恢复所有先前保存的寄存器。由于 %a10(堆栈指针),%a11(返回地址)和 %a14(帧指针,如果有),都是上层上下文的一部分,根本不需要结尾代码。所有清理操作,都由 ret 指令隐式执行。
  • 对于引用堆栈槽中,一个数据字的每条指令,都调用消除帧索引()。代码生成器之前的所有过程,都通过抽象帧索引和立即偏移量,寻址堆栈槽。此函数的目的,将这样的引用转换为寄存器-偏移对。根据包含指令的机器函数,是否具有固定或可变堆栈帧,使用堆栈指针 %a10,或帧指针 %a14,作为基址寄存器。相应计算偏移量。图 17展示了两种情况下,堆栈槽的寻址方式。

如果受影响指令的寻址模式,由于偏移量太大,无法处理该地址(偏移字段对于 BO 寻址模式,有 10 位,对于 BOL 模式,有 16 位),发出一系列指令,显式计算有效地址。临时结果,放入一个未使用的地址寄存器。如果没有可用的,清除已占用的地址寄存器。LLVM 的框架提供了一个名为 RegScavenger 的类,负责处理所有细节。

able 11Handle return register lr

表 11处理返回寄存器 lr

CPU架构的llvm后端第5张

图 17位于堆栈上的变量 a 的寻址。如果堆栈帧具有可变大小,必须相对于帧指针寻址槽

Fig. 17 Addressing of a variable a located on the stack. If the stack frame has a variable size, slot must be addressed relative to the frame pointer

CPU架构的llvm后端第6张

Table 12Backend functions called in PrologEpilogInserter.cpp

表 12PrologEpilogInserter.cpp 中调用的后端函数

CPU架构的llvm后端第7张

File PrologEpilogInserter.cpp includes the calling of backend functions spillCalleeSavedRegisters(), emitProlog(), emitEpilog() and eliminateFrameIndex() as follows,

文件 PrologEpilogInserter.cpp,包括调用后端函数,spillCalleeSavedRegisters(), emitProlog(), emitEpilog() ,eliminateFrameIndex()。

Table 13Cpu0 stack adjustment instructions before replace addiu and shl with lui instruction

Cpu0AnalyzeImmediate.cpp递归方式编写,逻辑上有点复杂。不过前端编译,用到了递归技巧。不跟踪代码,列出“表:用lui指令替换addiu和shl之前的Cpu0堆栈,调整指令”和“表:用lui指令替换addiu和shl之后的Cpu0堆栈,调整指令”中的堆栈大小和指令。

Table 14Cpu0 stack adjustment instructions after replace addiu and shl with lui instruction
CPU架构的llvm后端第8张

CPU架构的llvm后端第9张

由于 Cpu0 堆栈是 8 字节对齐,从 0x7ff9 到 0x7fff 的地址,不可能存在的。

假设 sp = 0xa0008000,stack size = 0x90008000, (0xa0008000 - 0x90008000) => 0x10000000。使用 Cpu0 Prologue 说明,进行验证,如下所示,

  1. “addiu $1, $zero, -9” => ($1 = 0 + 0xfffffff7) => $1 = 0xfffffff7.
  2. “shl $1, $1, 28;” => $1 = 0x70000000.
  3. “addiu $1, $1, -32768” => $1 = (0x70000000 + 0xffff8000) => $1 = 0x6fff8000.
  4. “addu $sp, $sp, $1” => $sp = (0xa0008000 + 0x6fff8000) => $sp = 0x10000000.

使用 sp = 0x10000000,堆栈大小stack size = 0x90008000 的 Cpu0 Epilogue 指令,进行验证。

  1. “addiu $1, $zero, -28671” => ($1 = 0 + 0xffff9001) => $1 = 0xffff9001.
  2. “shl $1, $1, 16;” => $1 = 0x90010000.
  3. “addiu $1, $1, -32768” => $1 = (0x90010000 + 0xffff8000) => $1 = 0x90008000.
  4. “addu $sp, $sp, $1” => $sp = (0x10000000 + 0x90008000) => $sp = 0xa0008000.

Cpu0AnalyzeImmediate::GetShortestSeq() ,将调用 Cpu0AnalyzeImmediate:: ReplaceADDiuSHLWithLUi() ,仅用单个指令 lui,替换 addiu 和 shl。

假设 sp = 0xa0008000 和堆栈大小 = 0x90008000,那么 (0xa0008000 - 0x90008000) => 0x10000000。使用 Cpu0 Prologue 说明进行验证,如下所示,

  1. “lui $1, 28671” => $1 = 0x6fff0000。
  2. “ori $1, $1, 32768” => $1 = (0x6fff0000 + 0x00008000) => $1 = 0x6fff8000。
  3. “addu $sp, $sp, $1” => $sp = (0xa0008000 + 0x6fff8000) => $sp = 0x10000000。

使用 sp = 0x10000000 和堆栈大小 = 0x90008000 的 Cpu0 Epilogue 指令进行验证,如下所示,

  1. “lui $1, 36865” => $1 = 0x90010000。
  2. “addiu $1, $1, -32768” => $1 = (0x90010000 + 0xffff8000) => $1 = 0x90008000。
  3. “addu $sp, $sp, $1” => $sp = (0x10000000 + 0x90008000) => $sp = 0xa0008000。

表 15llvm 后端阶段的函数

able 15Functions for llvm backend stages

CPU架构的llvm后端第10张

在“添加 Cpu0DAGToDAGISel 类”部分的指令,添加了一个pass。可以将代码嵌入到其它类似的pass中。有关信息,请查看 CodeGen/Passes.h。根据llc-debug-pass=Structure指示的功能单元,调用pass。

已经完成了一个简单的 cpu0 编译器,只支持ldstaddiuoriluiaddushlret8 条指令。

可能会想“在编写了这么多代码之后,只需得到这 8 条指令!”。重点是已经为 Cpu0 目标机,创建了一个框架(llvm 后端结构类继承树)。有超过 3000 行带有注释的源代码,包括文件 *.cpp,*.h,*.td 和 CMakeLists.txt。可以通过命令计数wc`finddir-name*.cpp`对于文件 *.cpp,*.h,*.td,*.txt。LLVM 前端,总共有 700 行源代码,没有注释。实际上,编写后端是启动缓慢,但运行很快。Clang 在 clang/lib 目录中,有超过 500,000 行,带有注释的源代码,包括 C++ 和 Obj C 支持。llvm 3.1 的 Mips 后端,只有 15,000 行,带有注释。即使是复杂的X86 CPU,外有CISC,内有RISC(微指令),在llvm 3.1中,也只有45000行注释。

CPU架构的llvm后端第11张

Fig. 20Code generation and execution flow

图20的上半部分,生成和执行计算机程序,工作流程和软件包。IR代表中间表示。中间部分是工作流程。除了clang,其它块都需要扩展,进行新的后端开发(许多后端也扩展clang,Cpu0后端没有这个需求)。实现了黄框部分。该图的绿色部分,用于 Cpu0 后端的 lld 和 elf2hex,可以在http://jonathan2251.github.io/lbt/index.html上找到。十六进制是 ascii 文件格式,使用“0”到“9”和“a”到“f”,表示十六进制值,因为 Verilog 语言机器,用作输入文件。

参考链接:

http://jonathan2251.github.io/lbd/ctrlflow.html

免责声明:文章转载自《CPU架构的llvm后端》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇GBK 编码Hbase常用命令下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

逆向so文件调试工具ida基础知识点

1.界面介绍 https://www.freebuf.com/column/157939.html 2.IDA常用快捷键 切换文本视图与图表视图 空格键 返回上一个操作地址 ESC 搜索地址和符号 G 对符号进行重命名 N 常规注释 '冒号键 可重复注释 分号键 添加标签 Alt+M 查看标签 Ctrl+M 查看段的信息 Ctrl+S 查看交叉应用...

DSP Bios记忆

DSP/BIOS中的线程 (转) DSP/BIOS中的线程和电脑中的线程有很大区别。关于DSP/BIOS的详细介绍请参考TMS320 DSP/BIOS User's Guide。下面简单地介绍一下DSP/BIOS的线程。为了让DSP能够同时处理多个任务,DSP/BIOS提供了如下几种类型的线程。HWI(硬件中断),SWI(软件中断),TSK(任务),IDL...

用Tinkercad学arduino之 74HC595寄存器控制8个led跑马灯

项目地址:https://www.tinkercad.com/things/5nsSWyQOAkI-chenillard // Arduino Pattern Creator by PhilCam // http://my.free.time.free.fr/ // Use with Uno R3 + 74HC595 + 8 leds int d...

linux c 用户态调试追踪函数调用堆栈以及定位段错误[转载]

一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的。 在glibc头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈。 int backtrace(void **buffer,int size) 该...

[转] 函数调用栈

http://kingj.iteye.com/blog/1555017 http://www.cnblogs.com/rain-lei/p/3622057.html   函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实...

Linux高级编程--04.GDB调试程序(查看数据)

查看栈信息 当程序被停住了,你需要做的第一件事就是查看程序是在哪里停住的。当你的程序调用了一个函数,函数的地址,函数参数,函数内的局部变量都会被压入“栈”(Stack)中。你可以用GDB命令来查看当前的栈中的信息。 下面是一些查看函数调用栈信息的GDB命令: backtrace / bt :打印当前的函数调用栈的所有信息。如: (gdb) bt #0...