LLVM是如何编译指令的

2023-11-17

本文将会通过一条指令在LLVM中的不同阶段,从源程序语言中的语义结构到成为机器二进制码来研究LLVM的工作原理。

本文不会介绍LLVM是如何工作的,这需要理解LLVM的设计以及code以及各种细节。

输入代码

我们从一段C代码开始探险,如下:

int foo(int aa, int bb, int cc) {
  int sum = aa + bb;
  return sum / cc;
}

本文将会重点关注除法操作。

Clang

Clang是作为LLVM的前端使用的,负责将C,C++,以及ObjC源程序转化为LLVM IR。

Clang主要的复杂在于它需要正确的parse以及语义分析C++程序;解析C程序还是比较简单的。

Clang的parser会建立一个抽象语法树Abstract Syntax Tree(AST). Clang主要通过AST进行处理。对于我们的除法操作来说,Clang会在AST中创建一个BinaryOperator节点,其带有BO_div操作属性。Clang的代码生成器然后会从该节点产生sdiv LLVM IR指令,因为这是一个有符号整型的除法操作。

LLVM IR

上述程序的LLVM IR如下:

define i32 @foo(i32 %aa, i32 %bb, i32 %cc) nounwind {
entry:
  %add = add nsw i32 %aa, %bb
  %div = sdiv i32 %add, %cc
  ret i32 %div
}

在LLVM IR中,sdiv是一个Binary Operator,是SDiv指令的subclass。像其他的任何指令一样,它可以被LLVM分析并转化。

代码生成器 code generator是LLVM中最复杂的一个部分,它的任务是将相对high-level,不依赖目标机器的LLVM IR转化为 low-level的,依赖目标的机器指令(MachineInstr)。在生成Machine Instr之前,LLVM IR的指令会经过“Selection DAG node”转化。

SelectionDAG Node

Selection DAG node是由SelectionDAGBuilder在SelectionDAGSel阶段创建的,这是instruction selection的主要部分。
SelectionDAGIsel会走遍IR指令,在指令上调用SelectionDAGBuilder::visit dispatcher。处理SDiv指令的是SelectionDAGBuilder::visitDiv. 它需要在DAG中创建一个新的SDNode节点,其操作符为ISD::SDIV.

最初的DAG只是部分依赖目标机器的。在LLVM的命名中,这被叫做“illegal”,因为它的类型可能无法被目标机器支持。同样,其中包含的操作可能也无法支持。

有几种方式来可视化DAG;一种是将 -debug flag传递到LLC,这将会在Selection Phase的过程中创建DAG的文本dump。另一种方式就是使用-view选项,可以dump并display graph的真实图像。如下就是在DAG创建之后的图像:

在这里插入图片描述
在SelectionDAG从DAG节点真正的输出机器指令之前,这些节点也会经历一些其他的变化。其中最重要的就是类型和操作合法化,通过使用target-specific hook来将所有的操作和类型转为那些机器真正支持的操作和类型。

将SDiv合规化到sdivrem on X86

X86中的idvi指令,同时计算商和余数,并且将结果存到两个不同的寄存器中,因为LLVM的指令选择会将这类指令(叫做ISD::SDIVREM)和只计算商的操作(ISD::SDIV)区分开,因此我们的DAG 节点会在DAG合规化阶段被“legalized”,如果目标机器是X86的话。

代码生成器使用的一个重要的接口:TargetLowering,来将传递target-specific的信息传输到target-indepent算法中。 目标会实现这个接口来描述LLVM IR指令应该怎样被lowered到合规的SelectionDAG操作。 x86的对应接口叫做X86TargetLowering。在它的构造函数中,它标记了那些操作应当被合规化,ISD::SDIV就是其中之一。如下是该段代码的注释:

// Scalar integer divide and remainder are lowered to use operations that
// produce two results, to match the available instructions. This exposes
// the two-result form to trivial CSE, which is able to combine x/y and x%y
// into a single instruction.

当SelectionDAGLegalize::LegalizeOp看到SDIV节点有Expand flag时,它会将其替换为ISD::SDIVREM。这个例子展示了在Selection DAG格式时,一个操作可能经历的变化。

Instruction selection - from SDNode to MachineSDNode

指令生成中的下一步即为instruction selection。 LLVM提供了一个通用的table-based instruction selection 机制,该文件通过TableGen工具自动生成。

然而很多目标后端,都选择自己写SelectionDAGIsel::Select的实现代码来手动处理一些指令。其他的指令会送到叫做SelectCode的auto-generated selector。

X86后端手动的处理ISD::SDIVREM来解决一些特殊的情况和优化。在这个阶段创建的DAG节点叫做MachineSDNode,是SDNode的一个subclass,会存有生成实际的机器指令的信息,但是仍然是以DAG node格式的。此时,真正的的X86指令op code会被选择,在这个例子中为X86::IDIV32r。

调度和发射MachineInstr

此时我们的代码还是DAG格式的,但是CPU不会执行DAG,他们执行的是线性的指令队列。调度的目标是通过给操作节点一个顺序来线性化DAG,最简单的方式就是按照拓扑的方式排序DAG,但是LLVM的代码生成器使用了更为聪明的方式,比如register pressure reduction,来尝试产生更快的代码。

一般每个目标都有自己的hook,来实现指令的调度。

最终,调度器会通过使用InstrEmitter::EmitMachineNode函数将SDNode转化,发射一系列的指令到MachineBasicBlock。这些指令使用MachineInstr 的格式(MI 格式),DAG可以被销毁了。

我们通过调用llc -print-machineinstr 来看看产生的machine instruction。看看在instruction selecttion之后的第一次输出:

# After Instruction Selection:
# Machine code for function foo: SSA
Function Live Ins: %EDI in %vreg0, %ESI in %vreg1, %EDX in %vreg2
Function Live Outs: %EAX

BB#0: derived from LLVM BB %entry
    Live Ins: %EDI %ESI %EDX
        %vreg2<def> = COPY %EDX; GR32:%vreg2
        %vreg1<def> = COPY %ESI; GR32:%vreg1
        %vreg0<def> = COPY %EDI; GR32:%vreg0
        %vreg3<def,tied1> = ADD32rr %vreg0<tied0>, %vreg1, %EFLAGS<imp-def,dead>; GR32:%vreg3,%vreg0,%vreg1
        %EAX<def> = COPY %vreg3; GR32:%vreg3
        CDQ %EAX<imp-def>, %EDX<imp-def>, %EAX<imp-use>
        IDIV32r %vreg2, %EAX<imp-def>, %EDX<imp-def,dead>, %EFLAGS<imp-def,dead>, %EAX<imp-use>, %EDX<imp-use>; GR32:%vreg2
        %vreg4<def> = COPY %EAX; GR32:%vreg4
        %EAX<def> = COPY %vreg4; GR32:%vreg4
        RET

# End machine code for function foo.

注意输出是按照SSA格式的,其中的一些寄存器使用的是虚拟寄存器(比如%vreg1)。

寄存器分配 —从SSA到non-SSA机器指令

除了一些定义好的异常,指令选择器产生的代码是SSA(静态单赋值)格式的。尤其是,它假想此时我们有无穷的虚拟寄存器。当然,这是假的。因此,指令产生器的下一步就是调用寄存分配器,该分配器的任务就是使用物理寄存器替换掉虚拟寄存器。

上文所说的异常也是比较重要并且有趣的,因此我们再多讨论一点。

一些架构中的一些指令只能使用特定的寄存器。一个例子就是x86中的除法操作,要求输入在EDX和EAX寄存器中。指令选择器知道这些限制,因此我们在上面的代码中可以看到,IDIV32r的输入是物理寄存器,而不是虚拟寄存器。这个是通过X86DAGToDAGISel::Select处理的。

寄存器分配器会处理所有的非固定寄存器,此外,SSA格式的机器指令还会进行一些优化。

输出代码

现在我们原始的C代码已经被翻译为MI 格式,一个使用instruction objects(MachineInstr)组成的MachineFunction。此时,代码生成器完成了它的工作,我们可以输出代码。在LLVM中,有两种方式实现它,一种是使用JIT来产生可执行的,ready-to-run code到内存中。另一种就是MC,是一种复杂的object-file-and-assembly生成器。MC现在被用于汇编和目标文件生成。MC也允许使用MCJIT,是基于MC layer的JIT-ting 框架。

LLVMTargetMachine::addPassesToEmitMachineCode定义了JIT产生代码的pass序列。它调用了addPassesToGenerateCode,该函数调用了所有需要的passes,将IR转为MI格式。下一步,叫做addCodeEmitter,是一个目标特定target-specific的pass用来将MI转化为实际的machine code。因为MI已经十分low-level了,因此可以相对简单的将它们转化为可运行的machine code。X86代码对应的文件为lib/Target/X86/X86CodeEmitter.cpp。我们的除法操作此处不需要特殊的处理,因为MachineInstr已经包含了opcode和操作数了。它和其他的指令一般在emitInstruction中处理。

MCInst

LLVM如果是被用作静态编译器,那么MI被发送到MC layer中,来处理object-file emission,它也可以产生汇编文件。

LLVMTargetMachine::addPassesToEmitFile 负责定义需要产生目标文件的一系列操作。实际上MI-to-MCInst转化在AsmPrinter接口的EmitInstruction函数中完成。在X86中,使用X86AsmPrinter::EmitInstruction函数实现,该函数会分派给X86McInstLower来处理。与JIT相似,除法指令和其他指令相同,不需要特殊的处理。
通过传递-show-mc-inst到LLC,我们可以看到在MC-level创建的指令:

foo:                                    # @foo
# BB#0:                                 # %entry
        movl    %edx, %ecx              # <MCInst #1483 MOV32rr
                                        #  <MCOperand Reg:46>
                                        #  <MCOperand Reg:48>>
        leal    (%rdi,%rsi), %eax       # <MCInst #1096 LEA64_32r
                                        #  <MCOperand Reg:43>
                                        #  <MCOperand Reg:110>
                                        #  <MCOperand Imm:1>
                                        #  <MCOperand Reg:114>
                                        #  <MCOperand Imm:0>
                                        #  <MCOperand Reg:0>>
        cltd                            # <MCInst #352 CDQ>
        idivl   %ecx                    # <MCInst #841 IDIV32r
                                        #  <MCOperand Reg:46>>
        ret                             # <MCInst #2227 RET>
.Ltmp0:
        .size   foo, .Ltmp0-foo

目标文件(或者汇编代码)的发射是通过MCStreamer 接口实现的。目标文件通过MCObjectStreamer产生,该类会因为实际上的目标文件进一步扩展。比如,ELF 产生时在MCELFStreamer产生的。MCInst会先经历MCObjectStreamer::EmitInstruction,然后是针对特定格式的EmitInstToData。最终产生的二进制格式的指令,是目标特定的,这是通过MCCodeEmitter接口(比如X86MCCodeEmitter)。此时的LLVM的代码,一部分是完全通用的,一部分是依赖特定输出目标文件格式的,一部分则是针对特定目标机器的。

Assemblers and disassemblers

MCInst是一个比较简单的格式。它尽可能的去除语义信息,只保存指令的操作码和操作数。像LLVM IR一样,这也是一个内部的表示,可以有不同的编码格式,最常使用的是汇编和二进制文件。

llvm-mc是一个使用MC框架来实现汇编器和反汇编器的工具。在内部,MCInst被用于在二进制和文本格式间进行翻译。此时,工具并不关心是什么编译器产生的汇编或者目标文件。

个人总结:

  1. Clang将输入源程序转为LLVM IR
  2. SelectionDAGBuilder遍历IR指令产生Selection DAG,此时DAG基本上还是非目标依赖的
  3. SelectionDAGLegalize使用TargetLowering对SelectionDAG,针对operation和type进行针对目标依赖的合规化
  4. SelectCode进行instruction selection,产生MachineSDNode(仍为DAG格式),包含对应的opcode
  5. InstrEmitter::EmitMachineNode产生线性序列的SSA格式的MachineInstr(MI)指令, DAG可以销毁
  6. 物理寄存器分配
  7. code emitter产生最终的目标文件

欢迎关注我的公众号《处理器与AI芯片》

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LLVM是如何编译指令的 的相关文章

  • flex&bison编写语法分析器

    使用flex和bison 对c语言代码块进行词法分析 识别词法错误 按照c 语法规则进行文法分析 并形成c语言代码块的语法树 syntax tree 并将语法树按照特定的格式打印出来 如何编译 两种方法 1 使用make命令 先将要执行的所
  • Flex&Bison 简单入门

    Flex Bison 简单入门 Ref flex与bison 中文版 1 Flex Bison安装 安装flex sudo apt install flex 安装bison sudo apt install bison 安装gcc 若缺少
  • 【编译原理】FIRST集合和FOLLOW集合

    FIRST集合 定义 可从 推导得到的串的首符号的集合 其中 是任意的文法符号串 规则 计算文法符号 X 的 FIRST X 不断运用以下规则直到没有新终结符号或 可以被加入为止 1 如果 X 是一个终结符号 那么 FIRST X X 2
  • 深入浅出编译原理-5-一个简单语法分析器的C语言实现

    引言 前面已经介绍了编译器的预处理 词法分析 词法分析器的实现 也在其中说到了语法分析的任务和过程 语法分析的输入是词法单元序列 然后根据语言的文法表示 展开式 利用有限状态机理论 生成抽象语法树 然后遍历得到中间代码 即 三地址码 本节就
  • 电子科技大学编译原理复习笔记(四):程序语言的设计

    目录 前言 重点一览 语言的定义 比较 生成观点与识别观点 语义又该怎么描述 符号串 符号串集合 文法 超重点 定义 组成 表示 分类 重点 文法产生的语言 短语 直接短语和句柄 求它们目的是语法分析 语法树 推导树 语言的设计 本章习题
  • 【编译原理】flex实现词法分析器

    flex自动实现词法分析器 FLEX 与 BISON 的使用 FLEX介绍 Flex是一个生成词法分析器的工具 它可以利用正则表达式来生成匹配相应字符串的C语言代码 其语法格式基本同Lex相同 单词的描述称为模式 Lexical Patte
  • Code Block & Basic Block

    Code Block In a programming language a code block typically starts with certain syntactical constructs such as loops con
  • 吉首大学_编译原理实验题_基于预测方法的语法分析程序的设计【通过代码】

    一 实验要求 实验二 基于预测方法的语法分析程序的设计 一 实验目的 了解预测分析器的基本构成及用自顶向下的预测法对表达式进行语法分析的方法 掌握预测语法分析程序的手工构造方法 二 实验内容 1 了解编译程序的基于预测方法的语法分析过程 2
  • (二):C++求解文法的First集和Follow集

    功能及代码结构 为实现编译器前端 需要对文法进行分析 该部分实现从文件中读入文法 方便修改 用合适的数据结构表示并求解各个非终结符号的First集和Follow集 仓库 https github com xs1317 Complier 文件
  • 解析目标文件

    最近在看 程序员的自我修养 颇有体会 故化繁为简 整理书中部分内容 作为学习笔记 PC平台上流行的可执行文件格式主要是windows下的PE Portable Executable 和Linux下的ELF Executable Linkab
  • PL0语言出错编号表

    Notes 编译原理第 3 版的书貌似没有这个表 做实验和写课设的时候很不方便 把别人拍的第 2 版书上的这个表在这备份一份 Error Code Table 出错编号 出错原因 1 常数说明中的 写成 2 常数说明中的 后应是数字 3 常
  • LLVM SSA 介绍

    最近做研究碰到了一个难题 需要对程序变量按生命期进行重命名 考虑到 SSA 中一个变量在不同的程序分支中赋值时会进行重命名 因此打算以此作为参考 看看能否采取同样的方法达到目的 由于之前看到的文档中都说 LLVM IR 是 SSA 形式的
  • 词法分析器(分析C语言)

    问题描述 用C或C 语言编写一个简单的词法分析程序 扫描C语言小子集的源程序 根据给定的词法规则 识别单词 填写相应的表 如果产生词法错误 则显示错误信息 位置 并试图从错误中恢复 简单的恢复方法是忽略该字符 或单词 重新开始扫描 相关词法
  • 编译原理实验二:Bison

    编译原理实验二 Bison 实验要求 1 了解Bision基础知识 如何将文法产生式转换为Bison语句 2 阅读 src common SyntaxTree c 对应头文件 include SyntaxTree h 理解分析树生成的过程
  • 编译原理实验:使用C/C++语言编写C-语言的词法分析器

    文章目录 实验目的 实验任务 实验内容 实验步骤 分析c 的词法规则 算法基本思想 Step1 find token Step2 DFA状态图构建 Step3 使用while switch双循环将DFA代码化 主程序流程 各程序模块之间层次
  • LLVM是如何编译指令的

    本文将会通过一条指令在LLVM中的不同阶段 从源程序语言中的语义结构到成为机器二进制码来研究LLVM的工作原理 本文不会介绍LLVM是如何工作的 这需要理解LLVM的设计以及code以及各种细节 输入代码 我们从一段C代码开始探险 如下 i
  • LL(1)文法的预测分析表以及对某输入串的分析过程

    举例说明LL 1 文法的预测分析 以及对 a a 的分析过程 文法G S S gt a S gt S gt T T gt SN N gt SN N gt 是否 gt First集 Follow集 S 否 a T 否 a N 是 Select
  • 编译原理_计算器_flex、bison实现(详细辅助理解)

    编译原理 计算器 flex bison实现 详细辅助理解 个人博客 https www yuque com ngp blog tuanh6 https www yuque com ngp blog tuanh6 P S 这篇文章只能助你理解
  • Compiler- 自增运算

    我们来看一下C语言中的前自增 i 和后自增 i 这个经典案例 大家在学习C的时候肯定学过前自增是先自增 然后将结果用于计算 后自增是先参与计算 再增加 好 看一下这段代码的结果 include
  • 编译原理13:SLR(1)分析表、LR(1)分析表

    更强的LR分析 可以根据当前单词 来选择是移进还是归约 只要所有移进项目中的点后面的那些终结符 与归约项目生成的非终结符的Follow集合的元素没有重叠 若当前单词属于上述Follow集合里则规约 SLR 1 冲突解决办法 SLR 1 分析

随机推荐

  • 数学计算模拟类问题:加法,除法和幂,注意越界问题。题 剑指Offer,Pow(x, n) ,Divide Two Integers

    数学计算的模拟类题目 往往是要求实现某种计算 比如两数相除 实现的过程中会有所限定 比如不允许乘法等等 这类题目首先要注意计算过程中本身的特殊情况 比如求相除 则必须首先反映过来除数不能为0 其次要记得考虑负数的情况 如果计算范围不单单是整
  • 简单的matlab分布式计算

    matlab的分布式计算可以理解为一台机器作为client 主控机 其他的机器分别作为计算的结点 要由client进行控制和操作 如果把单机上的 m文件直接放到client运行 是不会产生分布式计算的效果的 只相当于在主控机进行了计算 而其
  • 【JavaScript】defer和async的区别

    转载自 https segmentfault com q 1010000000640869 先来试个一句话解释仨 当浏览器碰到 script 脚本的时候 没有 defer 或 async 浏览器会立即加载并执行指定的脚本 立即 指的是在渲染
  • 华为性格测试通关指南

    一 华为性格测试关键要点 前后一致 积极乐观 吃苦耐劳 二 华为喜欢的人才性格画像 服从领导 能够按部就班按时完成工作 能够死命干活 没有太多性格 比如有野心 好胜 想当领导 坚持己见 坚持自己做事方式 别人有错当面硬刚这些类似的性格 喜欢
  • java实现航班信息查询管理系统

    一 任务概述 二 目录结构 三 详细代码 JDBC工具类模块 package com kaikeba task task010404 utils import com alibaba druid pool DruidDataSource i
  • python打包编译成pyd或者,Python .py生成.pyd文件并打包.exe 的注意事项说明

    最近用python写了一个小程序 想发布出去让人试用又不想暴露源码 搜索了一下发现将py文件编译成pyd文件就能达到目的 转换过程很简单 但是在调用pyd文件并且打包为单个exe文件的时候遇到一个坑 搞了一天才解决 在这里分享一下 首先安装
  • 使用post请求建立长连接实现sse,接收后端主动发来的消息,实现chat-gpt的弹字效果,EventSource的应用

    每日鸡汤 每个你想要学习的瞬间都是未来的你向自己求救 最近在做一个chat相关的功能 然后由于接口返回特别特别慢 所以需要搞一个慢慢等待的效果 就是接口一个单词一个单词的返回 然后前端收到一个展示一个 提升用户体验 说实话我是第一次做这类需
  • 消费者不用手机凭一张脸就能完成支付和转账

    以前出门要看钱包交易完成的节点 而商业活动发生于诸多场景中 商家若想为消费者提供更好的服务 就必须更深入地了解消费人群 赢得消费者的青睐 蜻蜓二代推出的AI刷脸会员功能 帮助商家完成顾客的会员一键开卡 不涉及填表 确认 签字等繁琐的流程 只
  • ETL为什么经常变成ELT甚至LET?

    ETL是将数据从来源端经过清洗 extract 转换 transform 加载 load 至目的端的过程 正常的 ETL 过程应当是 E T L 这三个步骤逐步进行 也就是先清洗转换之后再加载进目标端 通常是数据库 最后在数据库中的只是合理
  • Hive(7) Hive的DML语句-Hive的数据库和表的修改和删除

    Hive 3 DML语句 DML 数据操作语句 导入数据 直接从文件向表中导入数据 load data load data local inpath lt 文件路径 gt overwrite into table lt 表名 gt part
  • 内部类详解

    目录 一 什么是内部类 二 内部类的划分 2 1 实例内部类 2 2 静态内部类 2 3 局部内部类 2 4 匿名内部类 一 什么是内部类 定义 当一个事物的内部 还有一个完整的结构进行描述 而这个内部的完整的结构又只为外部事物提供服务 那
  • 递归-回溯算法

    一 递归 回溯算法 1 递归的思想 递归就是方法自己调用自己 每次调用的时候传入不同的变量 2 递归的原理 1 每执行一个方法 就在 栈内存 中分配一块空间 该空间是独立的 2 如果是 基本数据类型 则每块空间中的变量都是局部变量 是相互
  • 简单理解c语言——‘\0’ ,‘0’, “0” ,0之间的区别

    看来基础还是很重要的 基础不扎实就难以学好c语言 就别说写出高质量的c语言代码了 今天 我就被这个问题折磨的不行了 哈哈 不过现在终于明白了 0 0 0 之间的区别了 首先比较一下 0 和 0 的区别 有一个共同点就是它们都是字符 在c语言
  • 喜报

    8月16日 2023年度 IDC中国FinTech 50 榜单正式揭晓 擎创科技继2022年入选该榜单后 再次以创新者姿态成功入选 并以技术赋能业务创新 成为中国金融科技领域创新与活力的重要贡献者 IDC中国FinTech 50 旨在评选出
  • 网络安全岗位介绍——售前工程师

    一 工作内容 1 独立完成并配合销售人员引导客户完成方案设计 产品选型 配置报价和能为客户提供安全咨询与方案优化等服务 2 作为售前工程师 跟踪整个项目的进展 和销售进行配合 协调公司各种资源完成项目中标 3 编写投标文件的技术方案文档及投
  • Elasticsearch增删改查 之 —— Update更新

    Elasticsearch增删改查 之 Update更新 更新操作 一般用这个的 应该不会很多吧 ES本身还是一个倾向于查询检索的框架 对于这种更新的操作 太过频繁总归是不好的 不过阅读本篇后 你可以使用Script对所有的文档执行更新操作
  • 执行程序报错,could notcreate temporary directory ‘/tmp/poifiles‘

    could notcreate temporary directory tmp poifiles chmod R 777 tmp poifiles 重启jar包 运行命令就可以了
  • vba字典的key属性、item属性和keys方法、items方法、add方法

    1 key属性 修改字典中某一键值对的key值 2 item属性 修改字典中某一键值对的item值 3 keys方法 获取字典的所有键 4 items方法 获取字典的所有值 5 item属性 如果 key已存在 则修改其item值 如果不存
  • Jenkins之Maven的配置

    Jenkins之Maven配置与项目集成 1 Maven集成 1 1 环境准备 1 2 Jenkins的web界面配置 1 3 安装maven插件 1 Maven集成 在Jenkins上发布Java项目时需要使用Maven来进行构建打包 G
  • LLVM是如何编译指令的

    本文将会通过一条指令在LLVM中的不同阶段 从源程序语言中的语义结构到成为机器二进制码来研究LLVM的工作原理 本文不会介绍LLVM是如何工作的 这需要理解LLVM的设计以及code以及各种细节 输入代码 我们从一段C代码开始探险 如下 i