LLVM笔记(2) - IR

本文深入解析LLVM中间表示(IR),涵盖IR的概念、生成方法、特点及语法基础,探讨SSA形式、类型系统与指令集,适合编译原理初学者与进阶者。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 什么是IR
  IR(intermediate representation)是LLVM独创的中间表达式. 经典的compiler架构由前端frontend(读入源代码, 通过词法, 语法与语义分析建立AST), 中端optimizer(优化模块)与backend(通过指令选择, 寄存器分配等阶段最终输出为目标架构的汇编). 其中frontend随语言类型变化而变化, backend随目标架构变化而变化, 而优化部分的实现是不区分语言或架构的. 传统的编译器如gcc将中端与后端耦合在一起, 导致每个架构不光要实现后端功能也要实现中端优化, 类似的对不同语言后端也不能复用. 而在LLVM中通过将前端输出的AST转换为LLVM IR实现了前端与中端的分离, 在中端优化完后再将其转换为汇编, 大大节约了编译器开发的工作量. 更多相关介绍详见http://www.aosabook.org/en/llvm.html
  LLVM IR有三种表现形式: 在编译器内部的IR, 在磁盘中存储的bitcode(用于JIT编译器)以及最常见的易于阅读的LLVM IR汇编. 三种格式的IR是等价的(可互相转化), 因此LLVM IR提供了高效的编译器优化手段的同时又保证了方便调试与定位问题.
  使用IR的优点. 1. 通用, 任意语言都能转换为IR, 同一IR能转换为任意架构汇编. 2. 可移植性好, 容易定位问题, 只要保证IR正确性就能确定问题范围(前端还是后端还是某个优化pass). 3. 支持LTO(link time optimization).

2. 如何生成IR
  在编译时添加选项-emit-llvm即可生成IR, 此时的IR为bitcode格式(默认文件名后缀为bc), 若要生成汇编格式还需添加-S选项(默认文件名后缀为ll). 以test.c为例:

 1 [23:13:26] hansy@hansy:~/llvm/llvm (master)$ cat ~/test.c
 2 int test(int a, int b)
 3 {
 4   int c = 0;
 5   if (a) {
 6     c = b;
 7     a = c;
 8   }
 9   return c;
10 }
11 [23:13:29] hansy@hansy:~/llvm/llvm (master)$ ../llvm_build/bin/clang ~/test.c -O0 -emit-llvm -S -o ~/test.ll
12 [23:13:37] hansy@hansy:~/llvm/llvm (master)$ cat ~/test.ll
13 ; ModuleID = '/home/hansy/test.c'
14 source_filename = "/home/hansy/test.c"
15 target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
16 target triple = "x86_64-unknown-linux-gnu"
17 
18 ; Function Attrs: noinline nounwind optnone uwtable
19 define dso_local i32 @test(i32 %a, i32 %b) #0 {
20 entry:
21   %a.addr = alloca i32, align 4
22   %b.addr = alloca i32, align 4
23   %c = alloca i32, align 4
24   store i32 %a, i32* %a.addr, align 4
25   store i32 %b, i32* %b.addr, align 4
26   store i32 0, i32* %c, align 4
27   %0 = load i32, i32* %a.addr, align 4
28   %tobool = icmp ne i32 %0, 0
29   br i1 %tobool, label %if.then, label %if.end
30 
31 if.then:                                          ; preds = %entry
32   %1 = load i32, i32* %b.addr, align 4
33   store i32 %1, i32* %c, align 4
34   %2 = load i32, i32* %c, align 4
35   store i32 %2, i32* %a.addr, align 4
36   br label %if.end
37 
38 if.end:                                           ; preds = %if.then, %entry
39   %3 = load i32, i32* %c, align 4
40   ret i32 %3
41 }
42 
43 attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
44 
45 !llvm.module.flags = !{!0}
46 !llvm.ident = !{!1}
47 
48 !0 = !{i32 1, !"wchar_size", i32 4}
49 !1 = !{!"clang version 9.0.0 (https://github.com/llvm-mirror/clang.git 8a55120a7d72bed6c93749e0a6dbd0a2fcd873dd) (https://github.com/llvm-mirror/llvm.git ff5f64e4c8e72159f06487684037dcd3eca2cd8e)"}


  ll文件与bc文件同样能作为源文件被被编译, 编译ll/bc文件时无需特殊选项. 由于这个特性我们可以方便对比不同语言/不同架构的ll来确认问题.

3. IR的特点
  引用官方文档对IR的描述: LLVM is a Static Single Assignment (SSA) based representation that provides type safety, low-level operations, flexibility, and the capability of representing ‘all’ high-level languages cleanly. 作为一门语言IR有如下特点: 基于SSA, 类型安全, 提供低层级操作与支持所有高级语言.

  3.1. 什么是SSA. SSA(static single assignment), 静态单赋值, 是指变量必须在使用前被定义(static)且指赋值一次(single assignment). 举个简单的例子:

x = 0;
x = 1;
y = 2 * x;

  编译器如何确定y值的来源? 通过引入冗余变量使每个赋值都有对应变量名.

x1 = 0;
x2 = 1;
y = 2 * x2;

  在SSA模式下可以方便的定义def-use chain. 然而对于y值来源不确定的情况则需要特殊处理. 以上文程序为例, 局部变量c可能为0(a = 0)或b, 那么在程序返回的节点c的来源可能有两个, 类似的a可能为0(a = 0或a = b)或b.

  a1 = a;
  c1 = 0;
  if (a1) {
    c2 = b;
    a2 = c2;
  }
  a = phi(a, a2)
  c = phi(c1, c2)

  为了解决这个问题SSA引入了phi函数, phi表示该值的来源于多个前驱节点. 关于phi节点的表述在后文中分析.

  3.2. 类型系统. LLVM IR是强类型语言, 由于类型系统的支持, LLVM可以无需额外分析即可实现部分转化. LLVM支持的类型如下:
  void type - 无返回值, 无大小
  integer type - 基础类型(simple type), 表示为iN, 其中N为指定位宽, 大小为指定长度位的整型, 注意是无符号整型
  floating point type - 浮点类型, 对应为IEEE754标准浮点数类型
  pointer type - 指针类型, 指向某个内存对象, 注意没有void *, 而使用i8 *代替
  array type - 数组类型, 包含元素类型与个数
  label type - 标识代码块, 类似于汇编的label
  token type - 与指令相关的特殊值类型, 比如phi / select等
  struct type -
  aggregate type -
  metadata type -
  x86_mmx type -
  function type -
  first class type -
  single value type -

4. IR语法简介
  关于IR的reference manual请参考docs/LangRef中完整介绍, 本节的目标是介绍最基础的IR语法.

  4.1. 标识符
  LLVM中包含两类标识符: 全局类型与局部类型. 全局类型(函数, 全局变量)以@为起始字符, 局部类型(寄存器名, 类型)以%为标识. 一共有三类标识符, 分别为: 有名变量, 以%或@为起始字符, 后接字符串; 匿名变量以%或@为起始字符, 后接数字; 常量, 后文介绍. 其中有名变量通常源于代码中的变量, 匿名变量来自于LLVM中的临时变量, 寄存器等. 注意匿名变量是LLVM在输出ll时自动顺序命名的(编译过程中并不存在同名的变量, 后文会讲述), 因此必须顺序连续排列(比如将以上代码中%2修改为%3会导致编译不通过).
  4.2. 常量
  与类型系统对应, LLVM提供各类型(bool, int, float, pointer, token)以及复杂类型的常量. 注意两个特殊的常量: zeroinitializer(用于指定内存对象的零初始化)与metadata node(无类型常量).
  4.3. metadata
  metadata是LLVM提供的一种附加信息到某个指令的手段. 有两类metadata原语: metadata node与metadata string. 所有的metadata均以!起始. metadata string是以双引号包含的字符串, metadata node类以于结构体常量的表现方式(用逗号分割的元素列表).
  4.4. Instruction
  //TODO
  4.5. Intrinsic
  //TODO

转载于:https://www.cnblogs.com/Five100Miles/p/10686743.html

<think>我们正在处理一个关于LLVMIR编译到RISC-V机器码的问题。根据用户的问题和提供的引用,我们可以构建一个清晰的回答。引用[1]提到有一个交互式的LLVMIR学习和实验平台,我们可以利用它来学习LLVMIR。引用[3]详细介绍了LLVM后端移植的笔记,特别是关于RISC-V的示例,这直接与用户的问题相关。步骤:1.首先,我们需要将LLVMIR转换为特定于RISC-V架构的机器码。LLVM后端负责这一过程。2.根据引用[3],LLVM后端包含多个部分和环节,其中代码生成(CodeEmission)环节负责生成机器码。具体步骤:-使用LLVM的工具链,特别是`llc`(LLVM静态编译器)将LLVMIR文件编译为目标架构的汇编代码或机器码。-对于RISC-V,我们需要指定目标三元组(targettriple)为riscv32或riscv64。示例命令:将LLVMIR文件(例如input.ll)编译为RISC-V汇编代码:llc-march=riscv32-ooutput.sinput.ll或者直接生成目标文件(机器码):llc-march=riscv32-filetype=obj-ooutput.oinput.ll注意:LLVM需要支持RISC-V后端。可以通过`llc--version`查看支持的后端,确保riscv在其中。另外,引用[3]还提到了后端移植的七大步骤,但用户的问题是如何使用现有的RISC-V后端生成机器码,因此我们不需要深入到后端移植,而是使用现有的后端。因此,回答将包括:1.使用LLVM自带的工具(llc)进行转换。2.指定目标架构为riscv。3.生成汇编代码或机器码(目标文件)的方法。同时,根据引用[2]中关于GraalVM将Java编译成本地机器码的示例,我们可以类比:LLVMIR类似于Java字节码,而通过LLVM后端(这里针对RISC-V)将其编译为机器码。最后,生成相关问题。</think>### 如何将LLVM IR编译为RISC-V机器码 将LLVM中间表示(IR)编译为RISC-V架构的机器码需通过LLVM后端工具链实现,主要流程如下: #### 1. **准备LLVM工具链** - 确保LLVM版本≥10(完整支持RISC-V),使用命令验证: ```bash llc --version | grep riscv ``` - 若未启用RISC-V后端,需从源码编译LLVM并添加参数: ```bash cmake -DLLVM_TARGETS_TO_BUILD="X86;RISCV" .. ``` #### 2. **IR到目标文件转换** - 使用`llc`工具将IR编译为目标文件: ```bash llc -march=riscv32 -filetype=obj input.ll -o output.o # 32位 llc -march=riscv64 -filetype=obj input.ll -o output.o # 64位 ``` - 关键参数: - `-march=riscvXX`:指定RISC-V架构位数 - `-filetype=obj`:直接生成机器码(ELF格式目标文件) #### 3. **后端处理流程** 编译过程触发LLVM后端的七大环节[^3]: 1. **指令选择**:将IR操作映射到RISC-V ISA 2. **寄存器分配**:使用RISC-V的32个通用寄存器 3. **代码发射**:生成`.text`/`.data`等机器码段 ```mermaid graph LR A[LLVM IR] --> B(指令选择) B --> C(寄存器分配) C --> D[RISC-V机器码] ``` #### 4. **链接与执行** - 使用RISC-V工具链链接目标文件: ```bash riscv64-unknown-elf-ld output.o -o executable ``` - 在QEMU模拟器运行: ```bash qemu-riscv64 executable ``` #### 注意事项 1. **ABI兼容性**:需匹配`-mabi=ilp32`(32)或`-mabi=lp64`(64位) 2. **优化级别**:通过`-O1`~`-O3`控制代码生成策略 3. **扩展指令集**:使用`-mattr=+c`等参数启用压缩指令扩展 > 提示:可通过[LLVM IR在线实验平台](https://llvm.org/docs/CompilerWriter.html)实时验证IR转换逻辑[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值