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是如何编译指令的 的相关文章

  • 编译原理第七章笔记 -- 中间代码生成

    本文中内容整理西安交通大学软件学院吴晓军老师的ppt中 仅供学习使用 请勿转载或他用 参考教材 程序设计语言 编译原理 第3版 陈火旺等 国防工业出版社 这一章分数在35左右 两个大题 数组的引用四元式生成 控制语句当中布尔表达式的翻译 考
  • C++实现的利用LR(1)分析表对赋值表达式进行语法制导翻译生成四元式及汇编代码

    赋值语句的语法制导翻译 后续已完善算术运算文法 赋值文法 布尔运算文法 if while do while和复合语句文法 编译器项目已上传GitHub https github com sleep jyx compiler 一 需要的语义过
  • 电子科技大学编译原理复习笔记(五):词法分析

    目录 前言 重点一览 词法分析概述 词法分析的功能 词法分析器的输出形式 词法分析器的结构 状态转换图 状态转换图的构造 词法分析器的设计 基本结构 内容 符号表 目的 组成 在词法分析中的作用 符号表的一般形式 常用的符号表结构 总结与补
  • 电子科技大学编译原理复习笔记(四):程序语言的设计

    目录 前言 重点一览 语言的定义 比较 生成观点与识别观点 语义又该怎么描述 符号串 符号串集合 文法 超重点 定义 组成 表示 分类 重点 文法产生的语言 短语 直接短语和句柄 求它们目的是语法分析 语法树 推导树 语言的设计 本章习题
  • FLEX & BISON 联合使用

    flex是词法分析器 bison是语法分析器 基本原理就是flex解析出token后 让bison来使用 实际上 一般是先编写bison脚本 里面的token就是一个定义 没有实现 里面的yylex也是没有实现 只有定义 为什么先做biso
  • 编译原理------语法分析器C/C++代码实现

    一 实验目的 编制一个递归下降分析程序 实现对词法分析程序所提供的单词序列的语法检查和结构分析 二 实验内容 利用C语言编制递归下降分析程序 并对简单语言进行语法分析 2 1 待分析的简单语言的语法 用扩充的BNF表示如下 lt 程序 gt
  • 用 Rust 编写一个简单的词法分析器

    词法分析是编译器和解释器的第一个环节 相对而言也比较简单 词法分析器 有时也称为单元生成器 tokenizer 或者扫描器 scanner 将源代码转换为词法单元 这个过程称为词法分析 本文代码设计复刻自 用Go语言自制解释器 词法分析器一
  • 编译课设 (词法分析+LR1语法分析+语法制导翻译(四元式生成))

    代码已上传至 Github 完整的 VS2019 项目已上传至百度云 提取码 lql1 目录 源语言 语义动作 中间代码定义 整体框架 声明 语句 i f if if 语句
  • 语义分析- C-- 语言

    C V1 0 E gt n true false E E E E 类型合法的程序 3 4 false true 类型不合法的程序 3 true true false 对这个语言 语义分析的任务是 对给定的一个表达式e 写一个函数type c
  • (二):C++求解文法的First集和Follow集

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

    源代码仓库 CompilePrincipleLearning experiment 4 yusixian CompilePrincipleLearning github com 源代码在demo文件夹中 一 实验目的 掌握LR 1 分析法的
  • GDB 程序调试常用命令

    调试之前 若要在GDB中调试程序在编译时需要加上调试信息 在GCC中添加的方法 GCC g a c o a exe 或下面提供更符合GDB的调试信息 GCC ggdb a c o a exe 运行流程 命令 作用 start 开始执行程序
  • PL0语言出错编号表

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

    实验一 词法分析程序 一 实验目的 通过设计编制调试一个具体的词法分析程序 加深对词法分析原理的理解 并掌握在对程序设计语言源程序进行扫描过程中将其分解为各类单词的词法分析方法 编制一个读单词过程 从输入的源程序中 识别出各个具有独立意义的
  • 语法分析—自上而下分析

    1 美图 2 位置 语法分析器的功能 语法分析的任务是分析一个文法的句子结构 语法分析器的功能 按照文法的产生式 语言的语法规则 识别输入符号串是否为一个句子 合式程序 语法分析的方法 不行 看不懂 我太难了 不看了
  • 编译原理实验:使用C/C++语言编写C-语言的词法分析器

    文章目录 实验目的 实验任务 实验内容 实验步骤 分析c 的词法规则 算法基本思想 Step1 find token Step2 DFA状态图构建 Step3 使用while switch双循环将DFA代码化 主程序流程 各程序模块之间层次
  • 编译原理LL(1)文法之提取左公因子,消除左递归

    在判断LL 1 文法是否符合的时候 需要判断LL 1 文法是否存在左公因子 和左递归的情况 以下给出相应的判断方法以及通过提取左公因子和消除左递归使非LL 1 文法转换为LL 1 法的方法 第一种情况 存在左公因子 解决方法 提取左公因子
  • 编译原理(4)LR(0)语法分析程序设计(Python实现)

    1 实验要求 1 已知文法G S 0 S E 1 E aA 2 E bB 3 A cA 4 A d 5 B cB 6 B d 手工建立文法G S 的LR 0 的项目集规范族DFA和LR 0 分析表 2 根据清华大学版 编译原理 第3版 教材
  • 【编译原理】 CS143 斯坦福大学公开课 第一周:简介

    youtube 1 1 Introduction to Compilers and interpreters 1 1 Introduction to Compilers and interpreters 编译器解释器介绍 两种主要的实现编程
  • 编译原理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