指令教程LLVM(目标函数机器指令寄存器)「指令中目标寄存器和操作数都只有1个」

超越指令选择在前面的章节中,我们学习了使用LLVM的SelectionDAG和GlobalISel框架进行指令选择
现在,我们可以探索指令选择之外的其他有趣概念
本章涵盖了一些编译器后端之外的高级主题,这些主题对于高度优化的编译器来说可能很有趣
例如,一些传递在指令选择之后运行,可以对各种指令执行不同的优化,这意味着开发者在这个阶段可以引入自己的传递来执行有意义的特定于目标的任务
最终,在本章中,我们将深入探讨以下概念:向LLVM添加新的机器函数传递将新目标集成到clang前端如何针对不同的CPU架构进行目标编译向LLVM添加新的机器函数传递在本节中,我们将探索如何在LLVM中实现一个新的机器函数传递,该传递在指令选择之后运行
具体来说,将创建一个MachineFunctionPass类,它是LLVM中原始FunctionPass类的一个子集,可以通过opt运行
这个类适应原始基础设施,允许实现操作后端MachineFunction表示的传递,通过llc使用
重要的是要注意,后端传递的实现利用了遗留传递管理器的接口,而不是新的传递管理器
这是因为LLVM当前在后端中没有新传递管理器的完整工作实现
因此,本章将遵循在遗留传递管理器管道中添加新传递的方法
在实际实现方面,与函数传递类似,机器函数传递一次优化单个(机器)函数,但它们重写了runOnMachineFunction()方法,而不是runOnFunction()方法
本节将实现的机器函数传递是一个检查零除何时发生的传递,具体来说,是在后端插入引发陷阱的代码
这种类型的传递对于M88k目标很重要,因为MC88100 CPU在硬件上无法可靠地检测零除情况
继续从上一章的后端实现开始,让我们看看如何实现后端机器函数传递
实现M88k目标的顶层接口首先,在llvm/lib/Target/M88k/M88k.h中,让我们在llvm命名空间声明内部添加两个原型,稍后将使用它们:将实现的机器函数传递称为M88kDivInstrPass
我们将添加一个函数声明来初始化这个传递,并传入传递注册表,这是一个管理所有传递的注册和初始化的类:void initializeM88kDivInstrPass(PassRegistry &);接下来,声明实际创建M88kDivInstr传递的函数,以M88k目标机器信息作为其参数:FunctionPass createM88kDivInstr(const M88kTargetMachine &);添加机器函数传递的TargetMachine实现接下来,我们将分析在llvm/lib/Target/M88k/M88kTargetMachine.cpp中所需的一些更改:在LLVM中,通常给用户提供打开或关闭传递的选项
因此,让我们为用户提供与我们的机器函数传递相同的灵活性
我们将首先声明一个名为m88k-no-check-zero-division的命令行选项,并将其初始化为false,这意味着除非用户明确关闭,否则总是会检查零除
我们将此选项添加到llvm命名空间声明下,并且是llc的一个选项:using namespace llvm; static cl::opt<bool> NoZeroDivCheck("m88k-no-check-zero-division", cl::Hidden, cl::desc("M88k: Don't trap on integer division by zero."), cl::init(false));同样,习惯性地创建一个正式的方法返回命令行值,以便我们可以查询它来确定传递是否会运行
我们将原始的命令行选项包装在noZeroDivCheck()方法中,以便稍后使用命令行结果:M88kTargetMachine::~M88kTargetMachine() {} bool M88kTargetMachine::noZeroDivCheck() const { return NoZeroDivCheck; }接下来,在LLVMInitializeM88kTarget()中,我们注册和初始化M88k目标和传递,我们将插入对在llvm/lib/Target/M88k/M88k.h中声明的initializeM88kDivInstrPass()方法的调用:extern "C" LLVM_EXTERNAL_VISIBILITY void LLVMInitializeM88kTarget() { RegisterTargetMachine<M88kTargetMachine> X(getTheM88kTarget()); auto &PR = PassRegistry::getPassRegistry(); initializeM88kDAGToDAGISelPass(PR); initializeM88kDivInstrPass(PR); }M88k目标还需要覆盖addMachineSSAOptimization()方法,这是一个在机器指令处于SSA形式时添加通过以优化它们的函数
本质上,我们的机器函数通过被添加为一种机器SSA优化类型
此方法被声明为一个需要被覆盖的函数
我们将在M88kTargetMachine.cpp的末尾添加完整实现:bool addInstSelector() override;void addPreEmitPass() override;void addMachineSSAOptimization() override;...void M88kPassConfig::addMachineSSAOptimization() { addPass(createM88kDivInstr(getTM<M88kTargetMachine>())); TargetPassConfig::addMachineSSAOptimization();}我们在M88kTargetMachine.h中也声明了返回命令行选项以开启和关闭机器函数通过的方法(noZeroDivCheck()方法):~M88kTargetMachine() override;bool noZeroDivCheck() const;开发机器函数通过的具体细节 现在M88k目标机器的实现已经完成,下一步将是开发机器函数通过本身
实现包含在新文件中,llvm/lib/Target/M88k/M88kDivInstr.cpp:首先添加我们机器函数通过所需的头文件
这包括提供我们访问M88k目标信息的头文件,以及允许我们操作机器函数和机器指令的头文件:#include "M88k.h"#include "M88kInstrInfo.h"#include "M88kTargetMachine.h"#include "MCTargetDesc/M88kMCTargetDesc.h"#include "llvm/ADT/Statistic.h"#include "llvm/CodeGen/MachineFunction.h"#include "llvm/CodeGen/MachineFunctionPass.h"#include "llvm/CodeGen/MachineInstrBuilder.h"#include "llvm/CodeGen/MachineRegisterInfo.h"#include "llvm/IR/Instructions.h"#include "llvm/Support/Debug.h"之后,我们将添加一些代码来准备我们的机器函数通过
首先是DEBUG_TYPE定义,命名为m88k-div-instr,用于调试时的细粒度控制
具体来说,定义这个DEBUG_TYPE允许用户指定机器函数通过名称,并在启用调试信息时查看与通过相关的任何调试信息:#define DEBUG_TYPE "m88k-div-instr"我们还指定正在使用的llvm命名空间,并且声明了我们机器函数的STATISTIC值
这个统计量,称为InsertedChecks,跟踪编译器插入了多少除零检查
最后,声明了一个匿名命名空间来封装随后的机器函数通过实现:using namespace llvm;STATISTIC(InsertedChecks, "Number of inserted checks for division by zero");namespace {如前所述,这个机器函数通过旨在检查除零情况,并插入将导致CPU陷入困境的指令
这些指令需要条件代码,所以我们定义了一个名为CC0的枚举值,其中包含对M88k目标有效的条件代码及其编码:enum class CC0 : unsigned { EQ0 = 0x2, NE0 = 0xd, GT0 = 0x1, LT0 = 0xc, GE0 = 0x3, LE0 = 0xe};接下来,让我们创建我们机器函数通过的实际类,称为M88kDivInstr
首先,我们将其创建为继承并属于MachineFunctionPass类型的实例
接下来,我们声明M88kDivInstr通过将需要的各种必要实例
这包括我们将创建并稍后详细说明的M88kBuilder,以及包含目标指令和寄存器信息的M88kTargetMachine
此外,我们还需要在发出指令时的寄存器银行信息和机器寄存器信息
还添加了一个AddZeroDivCheck布尔值,以表示先前的命令行选项,该选项打开或关闭我们的通过:class M88kDivInstr : public MachineFunctionPass { friend class M88kBuilder; const M88kTargetMachine TM; const TargetInstrInfo TII; const TargetRegisterInfo TRI; const RegisterBankInfo RBI; MachineRegisterInfo MRI; bool AddZeroDivCheck;对于M88kDivInstr类的公共变量和方法,我们声明了一个LLVM将用于识别我们通过的识别号,以及接受M88kTargetMachine的M88kDivInstr构造函数
接下来,我们覆盖了getRequiredProperties()方法,该方法表示MachineFunction在优化期间可能具有的属性,我们还覆盖了runOnMachineFunction()方法,这将是我们通过在检查任何除零时运行的主要方法之一
公开声明的第二个重要函数是runOnMachineBasicBlock()函数,它将从runOnMachineFunction()内部执行:public: static char ID; M88kDivInstr(const M88kTargetMachine TM = nullptr); MachineFunctionProperties getRequiredProperties() const override; bool runOnMachineFunction(MachineFunction &MF) override; bool runOnMachineBasicBlock(MachineBasicBlock &MBB);最后,声明了私有方法并结束了类
我们在M88kDivInstr类中声明的唯一私有方法是addZeroDivCheck()方法,该方法在任何除法指令之后插入除零检查
正如我们稍后将看到的,MachineInstr需要指向M88k目标上特定的除法指令:private: void addZeroDivCheck(MachineBasicBlock &MBB, MachineInstr DivInst);};接下来创建了一个名为 M88kBuilder 的类,这是一个专门用于创建 M88k 特定指令的构建器实例
此类保留 MachineBasicBlock(及相应的迭代器)的实例,并使用 DebugLoc 跟踪此构建器类的调试位置
其他必要的实例包括目标指令信息、目标寄存器信息以及 M88k 目标的寄存器银行信息:class M88kBuilder { MachineBasicBlock MBB; MachineBasicBlock::iterator I; const DebugLoc &DL; const TargetInstrInfo &TII; const TargetRegisterInfo &TRI; const RegisterBankInfo &RBI;对于 M88kBuilder 类的公共方法,我们必须为这个构建器实现构造函数
在初始化时,我们的专用构建器需要一个 M88kDivInstr 通过的实例来初始化目标指令、寄存器信息以及寄存器银行信息,以及 MachineBasicBlock 和调试位置:public: M88kBuilder(M88kDivInstr &Pass, MachineBasicBlock MBB, const DebugLoc &DL) : MBB(MBB), I(MBB->end()), DL(DL), TII(Pass.TII), TRI(Pass.TRI), RBI(Pass.RBI) {}接下来,创建一个设置 M88k 构建器内部 MachineBasicBlock 的方法,并相应地设置 MachineBasicBlock 迭代器: void setMBB(MachineBasicBlock NewMBB) { MBB = NewMBB; I = MBB->end(); }接下来需要实现的 constrainInst() 函数,在处理 MachineInstr 实例时需要用到
对于给定的 MachineInstr,我们检查 MachineInstr 实例的操作数的寄存器类是否可以通过现有函数 constrainSelectedInstRegOperands() 进行限制
如这里所示,此机器功能通过要求机器指令的寄存器操作数可以被限制: void constrainInst(MachineInstr MI) { if (!constrainSelectedInstRegOperands(MI, TII, TRI, RBI)) llvm_unreachable("Could not constrain register operands"); }此通过插入的一个指令是 BCND 指令,如 M88kInstrInfo.td 中定义的,这是 M88k 目标上的条件分支
为了创建此指令,我们需要一个条件代码,这些是我们在 M88kDivInstr.cpp 开头实现的 CC0 枚举值——即一个寄存器和 MachineBasicBlock
创建 BCND 指令后,创建并检查新创建的指令是否可以被限制,然后返回它: MachineInstr bcnd(CC0 Cc, Register Reg, MachineBasicBlock TargetMBB) { MachineInstr MI = BuildMI(MBB, I, DL, TII.get(M88k::BCND)) .addImm(static_cast<int64_t>(Cc)) .addReg(Reg) .addMBB(TargetMBB); constrainInst(MI); return MI; }类似地,我们的机器功能通过还需要一个陷阱指令,即 TRAP503 指令
这个指令需要一个寄存器,并在寄存器的第 0 位未设置时触发向量为 503 的陷阱,这将在零除法后触发
创建 TRAP503 指令后,在返回之前检查 TRAP503 是否可以被限制
此外,这结束了 M88kBuilder 类的实现,并完成了前面声明的匿名命名空间: MachineInstr trap503(Register Reg) { MachineInstr MI = BuildMI(MBB, I, DL, TII.get(M88k::TRAP503)).addReg(Reg); constrainInst(MI); return MI; }};// 结束匿名命名空间我们现在可以开始实现在机器功能通过中执行实际检查的函数了
首先,让我们探索如何实现 addZeroDivCheck()
这个函数简单地在当前机器指令之间插入除零检查,当前机器指令预计指向 DIVSrr 或 DIVUrr;这些分别是有符号和无符号除法的助记符
插入 BCND 和 TRAP503 指令,并增加 InsertedChecks 统计量以指示这两个指令的添加:void M88kDivInstr::addZeroDivCheck(MachineBasicBlock &MBB, MachineInstr DivInst) { assert(DivInst->getOpcode() == M88k::DIVSrr || DivInst->getOpcode() == M88k::DIVUrr && "Unexpected opcode"); MachineBasicBlock TailBB = MBB.splitAt(DivInst); M88kBuilder B(this, &MBB, DivInst->getDebugLoc()); B.bcnd(CC0::NE0, DivInst->getOperand(2).getReg(), TailBB); B.trap503(DivInst->getOperand(2).getReg()); ++InsertedChecks;}接下来实现的是 runOnMachineFunction(),这是在 LLVM 中创建函数通过类型时需要覆盖的重要函数之一
这个函数根据机器功能通过期间是否进行了任何更改返回 true 或 false
此外,对于给定的机器功能,我们收集所有相关的 M88k 子目标信息,包括目标指令、目标寄存器、寄存器银行和机器寄存器信息
用户是否打开或关闭 M88kDivInstr 机器功能通过的细节也在此查询并存储在 AddZeroDivCheck 变量中
此外,分析机器功能中的所有机器基本块以检查除零
执行机器基本块分析的函数是 runOnMachineBasicBlock();我们将在接下来实现这个函数
最后,如果机器功能发生了变化,这由返回的 Changed 变量指示:bool M88kDivInstr::runOnMachineFunction(MachineFunction &MF) { const M88kSubtarget &Subtarget = MF.getSubtarget<M88kSubtarget>(); TII = Subtarget.getInstrInfo(); TRI = Subtarget.getRegisterInfo(); RBI = Subtarget.getRegBankInfo(); MRI = &MF.getRegInfo(); AddZeroDivCheck = !TM->noZeroDivCheck(); bool Changed = false; for (MachineBasicBlock &MBB : reverse(MF)) Changed |= runOnMachineBasicBlock(MBB); return Changed;}对于 runOnMachineBasicBlock() 函数,也返回一个 Changed 布尔标志以指示机器基本块是否已更改;但是,它最初被设置为 false
此外,在机器基本块内,我们需要分析所有机器指令并检查指令是否分别为 DIVUrr 或 DIVSrr 操作码
除了检查操作码是否为除法指令外,我们还需要检查用户是否打开了我们的机器功能通过或关闭
如果满足所有这些条件,则通过先前实现的 addZeroDivCheck() 函数相应地添加带有条件分支和陷阱指令的除零检查
bool M88kDivInstr::runOnMachineBasicBlock(MachineBasicBlock &MBB) { bool Changed = false; for (MachineBasicBlock::reverse_instr_iterator I = MBB.instr_rbegin(); I != MBB.instr_rend(); ++I) { unsigned Opc = I->getOpcode(); if ((Opc == M88k::DIVUrr || Opc == M88k::DIVSrr) && AddZeroDivCheck) { addZeroDivCheck(MBB, &I); Changed = true; } } return Changed;}之后,我们需要实现构造函数来初始化我们的函数通过并设置适当的机器功能属性
这可以通过在 M88kDivInstr 类的构造函数内使用 PassRegistry 实例调用 initializeM88kDivInstrPass() 函数来实现,并且还可以通过设置机器功能属性来指示我们的通过要求机器功能处于 SSA 形式:M88kDivInstr::M88kDivInstr(const M88kTargetMachine TM) : MachineFunctionPass(ID), TM(TM) { initializeM88kDivInstrPass(PassRegistry::getPassRegistry());}MachineFunctionProperties M88kDivInstr::getRequiredProperties() const { return MachineFunctionProperties().set( MachineFunctionProperties::Property::IsSSA);}接下来,我们需要初始化我们的机器功能通过的 ID 并实例化 INITIALIZE_PASS 宏,用我们机器功能通过的详细信息
这需要通过实例、命名信息以及两个布尔参数来指示通过是否仅检查 CFG 和通过是否是分析通过
由于 M88kDivInstr 两者都不执行,所以在通过初始化宏中指定了两个 false 参数:char M88kDivInstr::ID = 0;INITIALIZE_PASS(M88kDivInstr, DEBUG_TYPE, "Handle div instructions", false, false)最后,createM88kDivInstr() 函数创建一个新的 M88kDivInstr 通过实例,带有一个 M88kTargetMachine 实例
这被封装到 llvm 命名空间中,在完成此函数后结束命名空间:namespace llvm{FunctionPass createM88kDivInstr(const M88kTargetMachine &TM) {return new M88kDivInstr(&TM);}} // 结束 llvm 命名空间我们即将完成实现我们的新机器功能通过
现在,我们需要确保 CMake 知道 M88kDivinstr.cpp 中的新机器功能通过
然后,将此文件添加到 llvm/lib/Target/M88k/CMakeLists.txt 中:add_llvm_target(M88kCodeGen M88kAsmPrinter.cpp M88kDivInstr.cpp M88kFrameLowering.cpp M88kInstrInfo.cpp M88kISelDAGToDAG.cpp最后一步是使用以下命令使用我们的新机器功能通过实现构建 LLVM
我们需要 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=M88k CMake 选项来构建 M88k 目标:$ cmake -G Ninja ../llvm-project/llvm -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=M88k -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="llvm"$ ninja通过这些,我们已经实现了机器功能通过,但是看到它是如何工作的不是很有趣吗?我们可以通过将 LLVM IR 传递给 llc 来展示此通过的结果
使用 llc 运行机器功能通过的一瞥 我们有以下 IR,其中包含除零:$ cat m88k-divzero.lltarget datalayout = "E-m:e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-a:8:16-n32"target triple = "m88k-unknown-openbsd"@dividend = dso_local global i32 5, align 4define dso_local i32 @testDivZero() #0 { %1 = load i32, ptr @dividend, align 4 %2 = sdiv i32 %1, 0 ret i32 %2}让我们将其输入 llc:$ llc m88k-divzero.ll通过这样做,我们将看到,在生成的汇编中,默认情况下,由我们的新机器功能通过插入的除零检查,由 bcnd.n (BCND) 和 tb0 (TRAP503) 表示:| %bb.1: subu %r2, %r0, %r2 bcnd.n ne0, %r0, .LBB0_2 divu %r2, %r2, 0 tb0 0, %r3, 503. . ..LBB0_3: bcnd.n ne0, %r0, .LBB0_4 divu %r2, %r2, 0 tb0 0, %r3, 503但是,让我们看看当我们指定 --m88k-no-check-zero-division 给 llc 时会发生什么:$ llc m88k-divzero.ll –m88k-no-check-zero-division此选项告诉后端 llc 不要运行检查除零的通过
生成的汇编将不包含任何 BCND 或 TRAP503 指令
这里是一个例子:| %bb.1: subu %r2, %r0, %r2 divu %r2, %r2, 0 jmp.n %r1 subu %r2, %r0, %r2正如我们所看到的,实现机器功能通过需要几个步骤,但这些程序可以作为您实现任何适合您需求的机器功能通过的指南
由于我们在本节中广泛探索了后端,让我们改变方向,看看如何让前端了解 M88k 目标
将新目标集成到 clang 前端 在前几章中,我们在 LLVM 中开发了 M88k 目标的后端实现
为了完成 M88k 目标的编译器实现,我们将研究通过为我们的 M88k 目标添加 clang 实现来连接我们的新目标到前端
在 clang 中实现驱动程序集成 让我们从为 M88k 添加 clang 的驱动程序集成开始:我们将要做的第一个更改在 clang/include/clang/Basic/TargetInfo.h 文件中
BuiltinVaListKind 枚举列出了每种目标的 __builtin_va_list 类型的不同种类,这些类型用于可变参数函数支持,因此为 M88k 添加了相应的类型:enum BuiltinVaListKind {. . . // typedef struct __va_list_tag { // int __va_arg; // int __va_stk; // int __va_reg; //} va_list; M88kBuiltinVaList };接下来,我们必须添加一个新的头文件,clang/lib/Basic/Targets/M88k.h
此文件是前端中 M88k 目标特性支持的头文件
第一步是定义一个新的宏,以防止多次包含相同的头文件、类型、变量等
我们还必须包括我们需要的各种头文件以供后续实现:#ifndef LLVM_CLANG_LIB_BASIC_TARGETS_M88K_H#define LLVM_CLANG_LIB_BASIC_TARGETS_M88K_H#include "OSTargets.h"#include "clang/Basic/TargetInfo.h"#include "clang/Basic/TargetOptions.h"#include "llvm/Support/Compiler.h"#include "llvm/TargetParser/Triple.h"我们将声明的方法将相应地添加到 clang 和 targets 命名空间中,就像 llvm-project 中的其他目标一样:namespace clang {namespace targets {现在让我们声明实际的 M88kTargetInfo 类,并让它扩展原始的 TargetInfo 类
此类被标记为 LLVM_LIBRARY_VISIBILITY,因为如果此类链接到一个共享库,这个属性允许 M88kTargetInfo 类只在库内部可见,并且外部无法访问:class LLVM_LIBRARY_VISIBILITY M88kTargetInfo: public TargetInfo {此外,我们必须声明两个变量 - 一个字符数组来表示寄存器名称,以及一个枚举值,包含可以在 M88k 目标中选择的 CPU 类型
我们默认设置的 CPU 是 CK_Unknown CPU
稍后我们将看到,这可以被用户选项覆盖: static const char const GCCRegNames[]; enum CPUKind { CK_Unknown, CK_88000, CK_88100, CK_88110 } CPU = CK_Unknown;之后,我们开始声明我们类实现中需要的公共方法
除了我们类的构造函数,我们定义了各种获取器方法
这包括获取目标特定 #define 值的方法,获取目标支持的内置列表的方法,返回 GCC 寄存器名称及其别名的方法,最后,返回我们之前添加到 clang/include/clang/Basic/TargetInfo.h 中的 M88k BuiltinVaListKind 的方法:public: M88kTargetInfo(const llvm::Triple &Triple, const TargetOptions &); void getTargetDefines(const LangOptions &Opts, MacroBuilder &Builder) const override; ArrayRef<Builtin::Info> getTargetBuiltins() const override; ArrayRef<const char > getGCCRegNames() const override; ArrayRef<TargetInfo::GCCRegAlias> getGCCRegAliases() const override; BuiltinVaListKind getBuiltinVaListKind() const override { return TargetInfo::M88kBuiltinVaList; }在获取器方法之后,我们还必须定义执行 M88k 目标上各种检查的方法
第一个检查 M88k 目标是否具有以字符串形式提供的特定目标特性
其次,我们添加了一个函数,用于验证内联汇编使用时的约束
最后,我们有一个函数,用于检查特定 CPU 是否对 M88k 目标有效,也以字符串形式提供: bool hasFeature(StringRef Feature) const override; bool validateAsmConstraint(const char &Name, TargetInfo::ConstraintInfo &info) const override; bool isValidCPUName(StringRef Name) const override;接下来,让我们声明 M88kTargetInfo 类的设置器方法
第一个简单地设置我们想要针对的特定 M88k CPU,而第二个方法设置一个向量,包含 M88k 的所有有效支持的 CPU: bool setCPU(const std::string &Name) override; void fillValidCPUList(SmallVectorImpl<StringRef> &Values) const override;};为了完成我们驱动程序实现的头文件,让我们结束我们在开始时添加的命名空间和宏定义:} // namespace targets} // namespace clang#endif // LLVM_CLANG_LIB_BASIC_TARGETS_M88K_H现在我们已经完成了 clang/lib/Basic/Targets 中的 M88k 头文件,我们必须添加相应的 TargetInfo C++ 实现在 clang/lib/Basic/Targets/M88k.cpp 中
我们将从包含所需的头文件开始,特别是我们刚刚创建的新的 M88k.h头文件:#include "M88k.h"#include "clang/Basic/Builtins.h"#include "clang/Basic/Diagnostic.h"#include "clang/Basic/TargetBuiltins.h"#include "llvm/ADT/StringExtras.h"#include "llvm/ADT/StringRef.h"#include "llvm/ADT/StringSwitch.h"#include "llvm/TargetParser/TargetParser.h"#include <cstring>正如我们之前在头文件中所做的,我们从 clang 和 targets 命名空间开始,然后也开始实现 M88kTargetInfo 类的构造函数:namespace clang {namespace targets {M88kTargetInfo::M88kTargetInfo(const llvm::Triple &Triple, const TargetOptions &) : TargetInfo(Triple) {在构造函数中,我们为 M88k 目标设置了数据布局字符串
正如您之前可能看到的,这个数据布局字符串位于发出的 LLVM IR 文件的顶部
数据布局字符串每个部分的说明在此处: std::string Layout = ""; Layout += "E"; // M68k 是大端序 Layout += "-m:e"; Layout += "-p:32:32:32"; // 指针是 32 位
// 所有标量类型自然是对齐的
Layout += "-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64"; // 浮点数和双精度浮点数也是自然对齐的
Layout += "-f32:32:32-f64:64:64"; // 我们更喜欢所有全局变量都对齐为 16 位;参见上文
Layout += "-a:8:16"; Layout += "-n32"; // 整数寄存器是 32 位
resetDataLayout(Layout);
构造函数通过设置各种变量类型为有符号 long long、无符号 long 或有符号 int 来结束: IntMaxType = SignedLongLong; Int64Type = SignedLongLong; SizeType = UnsignedLong; PtrDiffType = SignedInt; IntPtrType = SignedInt;}之后,实现了设置目标 CPU 的函数
这个函数采用一个字符串,并在 llvm::StringSwitch 中将 CPU 设置为用户在 LLVM 中提供的特定 CPU 字符串,这本质上只是一个针对字符串的常规 switch,专门用于 LLVM
我们可以看到 M88k 目标支持三种 CPU 类型,如果提供的字符串与任何预期类型都不匹配,就有一个 CK_Unknown 类型:bool M88kTargetInfo::setCPU(const std::string &Name) { StringRef N = Name; CPU = llvm::StringSwitch<CPUKind>(N) .Case("generic", CK_88000) .Case("mc88000", CK_88000) .Case("mc88100", CK_88100) .Case("mc88110", CK_88110) .Default(CK_Unknown); return CPU != CK_Unknown;}先前提到 M88k 目标支持三种有效且有效的 CPU 类型:mc88000、mc88100 和 mc88110,其中通用类型只是 mc88000 CPU
我们必须实现以下函数,以在 clang 中强制执行这些有效 CPU:static constexpr llvm::StringLiteral ValidCPUNames[] = { {"generic"}, {"mc88000"}, {"mc88100"}, {"mc88110"}};void M88kTargetInfo::fillValidCPUList( SmallVectorImpl<StringRef> &Values) const { Values.append(std::begin(ValidCPUNames), std::end(ValidCPUNames));}bool M88kTargetInfo::isValidCPUName(StringRef Name) const { return llvm::is_contained(ValidCPUNames, Name);}接下来,实现了 getTargetDefines() 方法
这个函数为前端定义了必要的宏,例如有效的 CPU 类型
除了 m88k 和 __m88k 宏之外,我们还必须为有效的 CPU 定义相应的 CPU 宏:void M88kTargetInfo::getTargetDefines(const LangOptions &Opts, MacroBuilder &Builder) const { using llvm::Twine; Builder.defineMacro("__m88k__"); Builder.defineMacro("__m88k"); switch (CPU) { // 对子架构 case CK_88000: Builder.defineMacro("__mc88000__"); break; case CK_88100: Builder.defineMacro("__mc88100__"); break; case CK_88110: Builder.defineMacro("__mc88110__"); break; default: break; }}接下来的几个函数是存根函数,但对于前端的基本支持是必需的
这包括从目标获取内置函数的函数,以及查询目标是否支持目标的特定特性的函数
现在,我们将这些函数留作未实现状态,并为这些函数设置默认返回值,以便以后可以实施:ArrayRef<Builtin::Info> M88kTargetInfo::getTargetBuiltins() const { return std::nullopt;}bool M88kTargetInfo::hasFeature(StringRef Feature) const { return Feature == "M88000";}接下来,我们将为 M88k 添加寄存器名称的实现
通常,特定平台支持的寄存器名称列表及其用途可以在该平台的 ABI 中找到
在此实现中,我们将实现主要的通用寄存器 0-31,并且还将创建一个数组来存储此信息
就寄存器别名而言,请注意,我们目前实现的寄存器没有别名:const char const M88kTargetInfo::GCCRegNames[] = { "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", "r39", "r31"};ArrayRef<const char > M88kTargetInfo::getGCCRegNames() const { return llvm::makeArrayRef(GCCRegNames);}ArrayRef<TargetInfo::GCCRegAlias> M88kTargetInfo::getGCCRegAliases() const { return std::nullopt; // 没有别名
}
我们将实现的最后一个函数是验证目标上的内联汇编约束的函数
这个函数简单地采用一个字符,它代表内联汇编约束,并相应地处理约束
实现了一些内联汇编寄存器约束,例如地址、数据和浮点寄存器,还考虑了一些常数的约束:bool M88kTargetInfo::validateAsmConstraint( const char &Name, TargetInfo::ConstraintInfo &info) const { switch (Name) { case 'a': // 地址寄存器 case 'd': // 数据寄存器 case 'f': // 浮点寄存器 info.setAllowsRegister(); return true; case 'K': // 常量 1 case 'L': // 常量 -1^20 .. 1^19 case 'M': // 常量 1-4: return true; } return false;}我们通过结束最初在文件开始时启动的 clang 和 targets 命名空间来结束该文件:} // namespace targets} // namespace clang完成 clang/lib/Basic/Targets/M88k.cpp 的实现后,需要在 clang/include/clang/Driver/Options.td 中实现添加 M88k 特性组和有效的 CPU 类型
回想一下,我们之前定义了 M88k 目标的三种有效 CPU 类型:mc88000、mc88100 和 mc88110
这些 CPU 类型也需要在 Options.td 中定义,因为此文件是定义所有选项和标志的中心位置,这些选项和标志将被 clang 接受:首先,我们必须添加 m_m88k_Features_Group,它表示一组将对 M88k 目标可用的特性:def m_m88k_Features_Group: OptionGroup<"<m88k features group>">, Group<m_Group>, DocName<"M88k">;接下来,我们必须将三种有效的 M88k CPU 类型定义为 M88k 特性组中的特性:def m88000 : Flag<["-"], "m88000">, Group<m_m88k_Features_Group>;def m88100 : Flag<["-"], "m88100", Group<m_m88k_Features_Group>;def m88110 : Flag<["-"], "m88110">, Group<m_m88k_Features_Group>;通过这些,我们已经实现了将 M88k 目标与 clang 连接的驱动程序集成部分
在 clang 中为 M88k 实现 ABI 支持现在,我们需要为 clang 中的 M88k 目标添加 ABI 支持,这允许我们从前端为 M88k 目标生成特定代码:让我们首先通过添加以下代码到 clang/lib/CodeGen/TargetInfo.h 开始
这是一个原型,它为 M88k 目标创建了代码生成信息:std::unique_ptr<TargetCodeGenInfo> createM88kTargetCodeGenInfo(CodeGenModule &CGM);我们还需要在 clang/lib/Basic/Targets.cpp 中添加以下代码,这将帮助 clang 了解 M88k 的可接受目标三元组
正如我们所看到的,对于 M88k 目标,可接受的操作系统是 OpenBSD
这意味着 clang 接受 m88k-openbsd 作为目标三元组:#include "Targets/M88k.h"#include "Targets/MSP430.h". . . case llvm::Triple::m88k: switch (os) { case llvm::Triple::OpenBSD: return std::make_unique<OpenBSDTargetInfo<M88kTargetInfo>>(Triple, Opts); default: return std::make_unique<M88kTargetInfo>(Triple, Opts); } case llvm::Triple::le32:. . .现在,我们需要创建一个名为 clang/lib/CodeGen/Targets/M88k.cpp 的文件,以便我们可以继续为 M88k 实现代码生成信息和 ABI
在 clang/lib/CodeGen/Targets/M88k.cpp 中,我们必须添加以下必要的头文件,其中之一是我们刚刚修改的 TargetInfo.h 头文件
然后,我们必须指定我们正在使用的 clang 和 clang::codegen 命名空间:#include "ABIInfoImpl.h"#include "TargetInfo.h"using namespace clang;using namespace clang::CodeGen;接下来,我们必须声明一个新的匿名命名空间,并在其中放置我们的 M88kABIInfo
M88kABIInfo 继承自现有的 clang ABIInfo,并包含 DefaultABIInfo
对于我们的目标,我们大量依赖现有的 ABIInfo 和 DefaultABIInfo,这大大简化了 M88kABIInfo 类:namespace {class M88kABIInfo final : public ABIInfo { DefaultABIInfo defaultInfo;此外,除了添加 M88kABIInfo 类的构造函数之外,还添加了几个方法
computeInfo() 实现了默认的 clang::CodeGen::ABIInfo 类
还有一个 EmitVAArg() 函数,该函数生成代码以从传入的指针中检索参数;这将在之后更新
这主要用于可变参数函数支持:public: explicit M88kABIInfo(CodeGen::CodeGenTypes &CGT) : ABIInfo(CGT), defaultInfo(CGT) {} void computeInfo(CodeGen::CGFunctionInfo &FI) const override {} CodeGen::Address EmitVAArg(CodeGen::CodeGenFunction &CGF, CodeGen::Address VAListAddr, QualType Ty) const override { return VAListAddr; }};接下来添加 M88kTargetCodeGenInfo 类的构造函数,它扩展了原始的 TargetCodeGenInfo
之后,我们必须关闭最初创建的匿名命名空间:class M88kTargetCodeGenInfo final : public TargetCodeGenInfo {public: explicit M88kTargetCodeGenInfo(CodeGen::CodeGenTypes &CGT) : TargetCodeGenInfo(std::make_unique<DefaultABIInfo>(CGT)) {} };}最后,我们必须添加实现以创建实际的 M88kTargetCodeGenInfo 类作为 std::unique_ptr,它接受一个单独的 CodeGenModule,该模块生成 LLVM IR 代码
这直接对应于最初添加到 TargetInfo.h 的内容:std::unique_ptr<TargetCodeGenInfo>CodeGen::createM88kTargetCodeGenInfo(CodeGenModule &CGM) { return std::make_unique<M88kTargetCodeGenInfo>(CGM.getTypes());}这结束了 M88k 在前端的 ABI 支持
为 M88k 在 clang 中实现工具链支持 M88k 目标集成到 clang 中的最后部分将是为我们的目标实现工具链支持
像以前一样,我们需要为工具链支持创建一个头文件
我们称之为 clang/lib/Driver/ToolChains/Arch/M88k.h:首先,我们必须定义 LLVM_CLANG_LIB_DRIVER_TOOLCHAINS_ARCH_M88K_H 以防止稍后多次包含,并且还要添加任何必要的头文件以供后续使用
然后,我们必须声明 clang、driver、tools 和 m88k 命名空间,每个命名空间都嵌套在另一个命名空间内:#ifndef LLVM_CLANG_LIB_DRIVER_TOOLCHAINS_ARCH_M88K_H#define LLVM_CLANG_LIB_DRIVER_TOOLCHAINS_ARCH_M88K_H#include "clang/Driver/Driver.h"#include "llvm/ADT/StringRef.h"#include "llvm/Option/Option.h"#include <string>#include <vector>namespace clang {namespace driver {namespace tools {namespace m88k {接下来,我们必须声明一个枚举值,它描述了浮点 ABI,这是软和硬浮点的
这意味着浮点计算可以由浮点硬件本身完成,这很快,或者通过软件仿真完成,这会更慢:enum class FloatABI { Invalid, Soft, Hard, };在此之后,我们必须添加定义以通过驱动程序获取浮点 ABI,并通过 clang 的 -mcpu= 和 -mtune= 选项获取 CPU
我们还必须声明一个函数,从驱动程序中检索目标特性:FloatABI getM88kFloatABI(const Driver &D, const llvm::opt::ArgList &Args);StringRef getM88kTargetCPU(const llvm::opt::ArgList &Args);StringRef getM88kTuneCPU(const llvm::opt::ArgList &Args);void getM88kTargetFeatures(const Driver &D, const llvm::Triple &Triple, const llvm::opt::ArgList &Args, std::vector<llvm::StringRef> &Features);最后,我们通过结束最初定义的命名空间和宏来结束头文件:} // end namespace m88k} // end namespace tools} // end namespace driver} // end namespace clang#endif // LLVM_CLANG_LIB_DRIVER_TOOLCHAINS_ARCH_M88K_H我们即将实现的是工具链支持的 C++ 实现,位于 clang/lib/Driver/ToolChains/Arch/M88k.cpp 中:再次,我们将通过包含必要的头文件和命名空间开始实现,这些命名空间我们将在后面使用
我们还必须包括我们之前创建的 M88k.h 头文件:#include "M88k.h"#include "ToolChains/CommonArgs.h"#include "clang/Driver/Driver.h"#include "clang/Driver/DriverDiagnostic.h"#include "clang/Driver/Options.h"#include "llvm/ADT/SmallVector.h"#include "llvm/ADT/StringSwitch.h"#include "llvm/Option/ArgList.h"#include "llvm/Support/Host.h"#include "llvm/Support/Regex.h"#include <sstream>using namespace clang::driver;using namespace clang::driver::tools;using namespace clang;using namespace llvm::opt;接下来实现的是 normalizeCPU() 函数,该函数将 CPU 名称处理为 clang 中的 -mcpu= 选项
正如我们所看到的,每个 CPU 名字有几种可接受的变化
此外,当用户指定 -mcpu=native 时,它允许他们为当前主机的 CPU 类型编译:static StringRef normalizeCPU(StringRef CPUName) { if (CPUName == "native") { StringRef CPU = std::string(llvm::sys::getHostCPUName()); if (!CPU.empty() && CPU != "generic") return CPU; } return llvm::StringSwitch<StringRef>(CPUName) .Cases("mc88000", "m88000", "88000", "generic", "mc88000") .Cases("mc88100", "m88100", "88100", "mc88100") .Cases("mc88110", "m88110", "88110", "mc88110") .Default(CPUName);}接下来,我们必须实现 getM88kTargetCPU() 函数,其中给定我们在 clang/include/clang/Driver/Options.td 中早先实现的 clang CPU 名称,我们获取我们所针对的 M88k CPU 的相应 LLVM 名称:StringRef m88k::getM88kTargetCPU(const ArgList &Args) { Arg A = Args.getLastArg(options::OPT_m88000, options::OPT_m88100, options::OPT_m88110, options::OPT_mcpu_EQ); if (!A) return StringRef(); switch (A->getOption().getID()) { case options::OPT_m88000: return "mc88000"; case options::OPT_m88100: return "mc88100";case options::OPT_m88110:return "mc88110";case options::OPT_mcpu_EQ:return normalizeCPU(A->getValue());default:llvm_unreachable("Impossible option ID");}}getM88kTuneCPU() 函数实现之后
这是 clang -mtune= 选项的行为,它改变了指令调度模型,以使用 M88k 的给定 CPU 的数据
我们简单地针对我们当前所针对的 CPU 进行调整:StringRef m88k::getM88kTuneCPU(const ArgList &Args) { if (const Arg A = Args.getLastArg(options::OPT_mtune_EQ)) return normalizeCPU(A->getValue()); return StringRef();}我们还将实现 getM88kFloatABI() 方法,该方法获取浮点 ABI
最初,我们将 ABI 设置为 m88k::FloatABI::Invalid 作为默认值
接下来,我们必须检查是否向命令行传递了任何 -msoft-float 或 -mhard-float 选项
如果指定了 -msoft-float,则我们将 ABI 设置为 m88k::FloatABI::Soft
同样,当指定 -mhard-float 给 clang 时,我们将设置 m88k::FloatABI::Hard
最后,如果这些选项都没有指定,我们选择当前平台上的默认值,对于 M88k 将是硬浮点值:m88k::FloatABI m88k::getM88kFloatABI(const Driver &D, const ArgList &Args) { m88k::FloatABI ABI = m88k::FloatABI::Invalid; if (Arg A = Args.getLastArg(options::OPT_msoft_float, options::OPT_mhard_float)) { if (A->getOption().matches(options::OPT_msoft_float)) ABI = m88k::FloatABI::Soft; else if (A->getOption().matches(options::OPT_mhard_float)) ABI = m88k::FloatABI::Hard; } if (ABI == m88k::FloatABI::Invalid) ABI = m88k::FloatABI::Hard; return ABI;}接下来,我们将添加 getM88kTargetFeatures() 的实现
这个函数的重要部分是作为参数传递的 Features 向量
正如我们所看到的,唯一处理的目标特性是浮点 ABI
从传递给驱动程序的驱动程序和参数中,我们将获取适当的浮点 ABI,这是我们在前一步中实现的
注意,我们还为软浮点 ABI 向 Features 向量添加了 -hard-float 目标特性,这意味着当前 M88k 仅支持硬浮点:void m88k::getM88kTargetFeatures(const Driver &D, const llvm::Triple &Triple, const ArgList &Args, std::vector<StringRef> &Features) { m88k::FloatABI FloatABI = m88k::getM88kFloatABI(D, Args); if (FloatABI == m88k::FloatABI::Soft) Features.push_back("-hard-float");}我们将以关闭最初在文件开头启动的 clang 和 targets 命名空间来结束该文件:} // namespace targets} // namespace clang通过这些,我们已经完成了 M88k 目标的工具链支持实现,这意味着我们已经完成了 M88k 目标在 clang 中的集成
我们需要做的最后一步是使用 M88k 目标构建 clang
以下命令将构建 clang 和 LLVM 项目
对于 clang,请注意 M88k 目标
在这里,必须添加 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=M88k CMake 选项,就像前一节中一样:$ cmake -G Ninja ../llvm-project/llvm -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=M88k -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;llvm"$ ninja现在,我们应该拥有一个能够识别 M88k 目标的 clang 版本
我们可以通过检查 clang 支持的目标列表,使用 –print-targets 选项来确认:$ clang --print-targets | grep M88k m88k - M88k在本节中,我们深入探讨了将新后端目标集成到 clang 并让它被识别的技术细节
在下一节中,我们将探讨交叉编译的概念,我们将详细介绍针对与当前主机不同的 CPU 架构的目标的过程
针对不同的 CPU 架构 如今,许多小型计算机(如 Raspberry Pi)在使用,尽管它们的资源有限
在这样的计算机上运行编译器通常是不可能的,或者需要太多时间
因此,编译器的一个常见需求是为不同的 CPU 架构生成代码
整个让主机编译不同目标的可执行文件的过程称为交叉编译
在交叉编译中,涉及两个系统:主机系统和目标系统
编译器在主机系统上运行,并为目标系统生成代码
为了表示这些系统,使用所谓的三元组
这是一个配置字符串,通常由 CPU 架构、供应商和操作系统组成
此外,有关环境的额外信息通常添加到配置字符串中
例如,x86_64-pc-win32 三元组用于在 64 位 X86 CPU 上运行 Windows 系统的计算机
CPU 架构是 x86_64,pc 是一个通用供应商,win32 是操作系统,所有这些部分都由连字符连接
在 ARMv8 CPU 上运行 Linux 系统的计算机使用 aarch64-unknown-linux-gnu 作为三元组,其中 aarch64 是 CPU 架构
此外,操作系统是 linux,运行 gnu 环境
Linux 基础系统没有真正的供应商,所以这部分是未知的
此外,对于特定目的不知道或不重要的部分通常被省略:aarch64-linux-gnu 三元组描述了相同的 Linux 系统
假设您的开发计算机在 X86 64 位 CPU 上运行 Linux,您想交叉编译到运行 Linux 的 ARMv8 CPU 系统
主机三元组是 x86_64-linux-gnu,目标三元组是 aarch64-linux-gnu
不同的系统具有不同的特性
因此,您的应用程序必须以可移植的方式编写;否则,可能会出现问题
一些常见的陷阱如下:字节序:多字节值在内存中的存储顺序可能不同
指针大小:指针的大小随 CPU 架构的变化而变化(通常为 16、32 或 64 位)
C int 类型可能不足以容纳一个指针
类型差异:数据类型通常与硬件密切相关
长双精度类型可以使用 64 位(ARM)、80 位(X86)或 128 位(ARMv8)
PowerPC 系统可能使用双倍精度算术对长双精度,通过使用两个 64 位双精度值的组合来提供更多精度
如果您不注意这些点,即使您的应用程序在主机系统上运行完美,它也可能在目标平台上出现意外行为或崩溃
LLVM 库在不同的平台上进行了测试,并且还包含针对上述问题的便携式解决方案
对于交叉编译,需要以下工具:一个为目标生成代码的编译器 一个能够为目标生成二进制文件的链接器 目标的头文件和库 幸运的是,Ubuntu 和 Debian 发行版拥有支持交叉编译的软件包
我们在以下设置中利用了这一点
gcc 和 g++ 编译器、链接器 ld 以及库作为预编译的二进制文件可用,它们生成 ARMv8 代码和可执行文件
以下命令安装所有这些软件包:$ sudo apt –y install gcc-12-aarch64-linux-gnu \ g++-12-aarch64-linux-gnu binutils-aarch64-linux-gnu \ libstdc++-12-dev-arm64-cross新文件安装在 /usr/aarch64-linux-gnu 目录下
这个目录是目标系统(逻辑)的根目录
它包含通常的 bin、lib 和 include 目录
交叉编译器(aarch64-linux-gnu-gcc-8 和 aarch64-linux-gnu-g++-8)知道这个目录
在其他系统上交叉编译一些发行版,如 Fedora,仅提供对裸机目标(如 Linux 内核)的交叉编译支持,但不提供用户应用程序所需的头文件和库文件
在这种情况下,您可以从目标系统中简单地复制缺少的文件
如果您的发行版没有提供所需的工具链,则可以从源代码构建它
对于编译器,您可以使用 clang 或 gcc/g++
必须将 gcc 和 g++ 编译器配置为为目标系统生成代码,binutils 工具需要处理目标系统的文件
此外,需要使用此工具链编译 C 和 C++ 库
步骤因操作系统和主机以及目标架构而异
在 Web 上,如果您搜索 gcc 交叉编译 <架构>,您可以找到说明
有了这些准备,您几乎已准备好交叉编译示例应用程序(包括 LLVM 库),除了一个小细节
LLVM 在构建期间使用 TableGen 工具
在交叉编译期间,所有内容都为目标架构编译,包括此工具
您可以使用第 1 章中构建的 llvm-tblgen,或者您也可以只编译此工具
假设您在包含本书 GitHub 存储库克隆的目录中,输入以下内容:$ mkdir build-host$ cd build-host$ cmake -G Ninja \ -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_ENABLE_ASSERTIONS=ON -DCMAKE_BUILD_TYPE=Release ../llvm-project/llvmninjallvm−tblgen cd ..这些步骤现在应该很熟悉了
创建并进入一个构建目录
cmake 命令为 LLVM 创建构建文件,仅针对 X86 目标
为了节省空间和时间,进行了发布构建,但启用了断言以捕捉可能的错误
仅使用 ninja 编译了 llvm-tblgen 工具
有了 llvm-tblgen 工具,现在可以开始交叉编译过程了
CMake 命令行非常长,因此您可能希望将命令存储在脚本文件中
与以前的构建不同,必须提供更多信息:$ mkdir build-target$ cd build-target$ cmake -G Ninja \ -DCMAKE_CROSSCOMPILING=True \ -DLLVM_TABLEGEN=../build-host/bin/llvm-tblgen \ -DLLVM_DEFAULT_TARGET_TRIPLE=aarch64-linux-gnu \ -DLLVM_TARGET_ARCH=AArch64 \ -DLLVM_TARGETS_TO_BUILD=AArch64 \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_EXTERNAL_PROJECTS=tinylang \ -DLLVM_EXTERNAL_TINYLANG_SOURCE_DIR=../tinylang \ -DCMAKE_INSTALL_PREFIX=../target-tinylang \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc-12 \ -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++-12 \ ../llvm-project/llvm$ ninja再次,您创建一个构建目录并进入它,然后运行 CMake 命令
其中一些 CMake 参数以前没有使用过,需要一些解释:CMAKE_CROSSCOMPILING 设置为 ON 告诉 CMake 我们正在交叉编译
LLVM_TABLEGEN 指定要使用的 llvm-tblgen 工具的路径
这是上一次构建中的那个
LLVM_DEFAULT_TARGET_TRIPLE 是目标架构的三元组
LLVM_TARGET_ARCH 用于 JIT 代码生成
它默认为主机的架构
对于交叉编译,这必须设置为目标架构
LLVM_TARGETS_TO_BUILD 是 LLVM 应该包括代码生成器的目标列表
列表至少应该包括目标架构
CMAKE_C_COMPILER 和 CMAKE_CXX_COMPILER 分别指定用于构建的 C 和 C++ 编译器
交叉编译器的二进制文件以前缀为目标三元组,并且 CMake 不会自动找到它们
其他参数请求启用断言的发布构建,并将我们的 tinylang 应用程序作为 LLVM 的一部分构建
一旦编译过程完成,file 命令可以证明我们为 ARMv8 创建了一个二进制文件
具体来说,我们可以运行 $ file bin/tinylang 并检查输出是否说 ELF 64-bit object for the ARM aarch64 architecture
使用 clang 交叉编译由于 LLVM 为不同的架构生成代码,使用 clang 进行交叉编译似乎是显而易见的
这里的障碍是 LLVM 没有提供所有所需的部分 - 例如,缺少 C 库
因此,您必须使用 LLVM 和 GNU 工具的混合,结果,您需要告诉 CMake 更多关于您正在使用的环境的信息
作为最低要求,您需要为 clang 和 clang++ 指定以下选项:--target=<target-triple>(为目标启用代码生成),--sysroot=<path>(目标的根目录路径),I(头文件的搜索路径),以及 -L(库的搜索路径)
在 CMake 运行期间,会编译一个小应用程序,如果您的设置有问题,CMake 会抱怨
这一步足以检查您是否拥有一个工作的环境
常见问题包括选择错误的头文件或由于不同的库名称或错误的搜索路径而导致的链接失败
交叉编译出奇地复杂
有了本节的说明,您将能够为您选择的目标架构交叉编译您的应用程序
总结 在本章中,您学习了创建超出指令选择运行的通过,特别是探索了后端机器功能通过的创建
您还发现如何将新实验目标添加到 clang,以及所需的驱动程序、ABI 和工具链更改
最后,考虑到编译器构建的至高无上的纪律,您学习了如何为另一个目标架构交叉编译您的应用程序
现在,我们已经到达了 Learn LLVM 17 的结尾,您现在具备了在项目中以创造性方式使用 LLVM 的知识,并探索了许多有趣的主题
LLVM 生态系统非常活跃,并且新功能不断添加,因此请务必关注其发展
作为编译器开发人员,我们很高兴写关于 LLVM 的内容,并在此过程中发现一些新功能
玩得开心,LLVM
指令教程LLVM(目标函数机器指令寄存器)
(图片来源网络,侵删)

联系我们

在线咨询:点击这里给我发消息