# LLVM 安装与环境配置
# 安装
由于 xcode 自带的 LLVM 是经过阉割的版本,很多 llvm 的命令都没有,所以需要单独下载 llvm,推荐使用 Homebrew
来安装。
# 搜索所有可用的 llvm 版本 | |
brew search llvm | |
# 下载指定版本 | |
brew install llvm@11 |
由于 llvm 不同版本之间的 API 有些微不同,所以最好是根据需求安装指定版本的 LLVM,防止出现 undefined symbol
的问题。
无法安装/安装出错
如果安装时出现报错: Error: llvm@11 has been disabled because it is a versioned formula!
,可以通过如下方式解决:
brew tap --force homebrew/core
brew edit llvm@11
- 把
disable! date: "2024-02-22", because: :versioned_formula
中的过期时间改为当前或以后,然后保存 HOMEBREW_NO_INSTALL_FROM_API=1 brew install llvm@11
安装完成后需要将如下配置写入环境变量中,由于笔者使用的 zsh
,所以就是把如下内容写入到 ~/.zshrc
中:
export LDFLAGS="-L/usr/local/opt/llvm@11/lib -Wl,-rpath,/usr/local/opt/llvm@11/lib" | |
export PATH="/usr/local/opt/llvm@11/bin:$PATH" | |
export LDFLAGS="-L/usr/local/opt/llvm@11/lib" | |
export CPPFLAGS="-I/usr/local/opt/llvm@11/include" |
这是在当前版本下 Homebrew
的默认 LLVM 安装路径,如果你的路径不是这个,请改成自己的路径。
# 测试
# 重新加载一下环境变量 | |
source ~/.zshrc | |
# 测试 llvm 是否成功安装 | |
llvm-config --version | |
╰▸ 11.1.0 |
# 环境配置
笔者尝试了多种不同的 IDE,包括 VSCode、Xcode 和 CLion,最终成功在 CLion 上成功搭建了可调试的 LLVM 开发环境,所以推荐使用 CLion 作为 IDE。
随便创建一个
C++ Executable
项目,语言标准任选。点击 CLion => settings => Build, Execution, Deployment => CMake options,写入:
-DLLVM_DIR:PATH=/usr/local/Cellar/llvm@11/11.1.0_4/lib/cmake/llvm
CLion 应该会自动为当前项目创建一个
CMakeLists.txt
。如果没有,则在 CLion 左边的项目根目录中右键 => new => CMakeLists.txt。将如下内容写入
CMakeLists.txt
中(假设你的项目名为 llvm_test):cmake_minimum_required(VERSION 3.27)
project(llvm_test) # 需改为你的项目名
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")
# Set your project compile flags.
# E.g. if using the C++ header files
# you will need to enable C++11 support
# for your compiler.
include_directories(${LLVM_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})
set(CMAKE_CXX_STANDARD 17)
add_executable(llvm_test main.cpp) # 需改为你的项目名 需要编译的 cpp 文件名
llvm_map_components_to_libnames(llvm_libs irreader) #根据需要填入 LLVM 组件
# Link against LLVM libraries
target_link_libraries(llvm_test ${llvm_libs})
保存后即可使用 CLion 愉快的开发 / Debug LLVM C++ API 代码了。
⚠️常见问题
- 如果在第 4 步出现错误:找不到
LLVMConfig.cmake
文件,代表你的Homebrew
和笔者不同,请全局搜索LLVMConfig.cmake
文件并在-DLLVM_DIR:PATH=
后写入正确的位置) - 上述代码中
llvm_map_components_to_libnames(llvm_libs irreader)
是一种按需使用 LLVM 组件的方式,irreader
就是一个组件,对应着头文件"llvm/IR/IRBuilder.h"
,如果你在引用头文件是出现"xxx.h" not found
的错误,请检查是否在CMakeLists.txt
中引入了对应的组件。 - 可以通过
llvm-config --components all
列出 LLVM 的所有组件。
# LLVM 基本知识
# 基本概念
# 引入
随着计算机技术的不断发展以及各种领域需求的增多,近几年来,许多编程语言如雨后春笋般出现,他们大多为了解决某一些特定领域的需求,比如说为 JavaScript 增加静态类型检查的 TypeScript,为解决服务器端高并发的 Golang,为解决内存安全和线程安全的 Rust。
让我们设身处地地想象一下,如果我们想开发一门新的编译型的编程语言,有什么需要解决的问题呢?
怎样让我的编程语言能在尽可能多的平台上运行
我想让我的编程语言能够在 Windows、macOS 和 Linux 上都可以运行;我想让我的编程语言能够在 Intel 芯片、ARM 芯片,乃至龙芯上都可以运行。
怎样让我的编程语言可以使用前人先进的技术
在编程语言发展的过程中,有许许多多成熟的技术。从汇编指令层面来看,简单地想给一个变量值置 0,AMD64 架构下可以用
xor %eax, %eax
异或自身,AArch64 架构下可以用mov w0, wzr
使用零寄存器;从算法层面来看,可以使用尾调用优化、CFI 等。如果我不想重复造轮子,该如何复用这些技术呢?怎样让我的编程语言在汇编层面实现「定制」
高级语言中的函数名,在汇编层面,我想让他换个名字;我想让 C 语言库能用它的调用约定来调用我的高级语言中的函数。
我们可以选择使用 C 语言来作为 “中间层”,当我们开发新编译语言时,提供该语言到 C 语言的转换,这样做是因为:
- 绝大部分的操作系统都是由 C 和汇编语言写成,因此平台大多会提供一个 C 编译器可以使用,这样就解决了第一个问题
- C 语言历史久远,有非常多的优化器,程序翻译成 C 语言后就可以直接使用那些为 C 语言定制的优化器,从而方便的使用前人的成熟技术,这就解决了第二个问题。
- C 语言本身并没有笨重的运行时,代码很贴近底层,可以使用一定程度的定制,一定程度上解决了第三个问题。
然而,C 语言毕竟是 “老古董” 了,相对于现在的各种编程语言来说还是太过僵硬死板。如果有这么一种语言,他可以在各个平台运行,且自带极高的代码优化能力,同时提供灵活操作系统底层的能力,那么当我们想开发一个新的编程语言时,岂不是只需要提供一个从新语言到这种语言的转换器就可以了。没错,这就是 LLVM!
简单来说,当我们想开发一种新的编程语言时,只需提供将自己语言的源代码编译成 LLVM 的中间代码(LLVM IR)的能力,然后就可以交由 LLVM 对中间代码进行优化,并编译成所需运行平台的二进制程序。LLVM 的优点也正对应我们之前讲的三个问题:
LLVM 后端支持的平台很多,我们不需要担心 CPU、操作系统的问题
有一点需要指出的是,操作系统 API 的调用还是得由编程语言开发者自己实现,LLVM 不负责这个。例如,编程语言的标准库中,打开一个文件,其底层实现在 Linux 中是否是
open
系统调用,在 Windows 中是否是CreateFile
函数,这都得编程语言开发者自己实现。LLVM 后端的优化水平较高,我们只需要将代码编译成 LLVM IR,就可以由 LLVM 后端作相应的优化
但我们需要知道一点,LLVM IR 毕竟是更为底层的语言,在高级语言中的许多细节,都会在 LLVM IR 中丢失。因此,高级语言中能做的优化,还是得趁早做,把一大堆未经优化的 LLVM IR 丢给 LLVM 后端去优化,会严重拖慢编译时间,降低生成的二进制程序的性能。
LLVM IR 本身比较贴近汇编语言,同时也提供了许多 ABI 层面的定制化功能
# 架构
LLVM 整体架构如上图所示。在整个编译过程中,LLVM 的前端是为各种编程语言定制的,负责将对应编程语言的源代码转换成 LLVM 中间表示(IR,Intermediate Representation);LLVM 的后端为各种不同的平台做了适配,负责将 LLVM IR 转换成在特定平台上可执行的二进制文件。如果在编译过程中涉及到优化的话,则由 LLVM 优化器来对 IR 文件进行优化,并将优化后的 IR 文件输出给 LLVM 后端去编译,因此 IR 的设计是 LLVM 的灵魂所在。
# LLVM IR
LLVM IR 的设计体现了权衡的计算思维。低级的 IR(即更接近目标代码的 IR)允许编译器更容易地生成针对特定硬件的优化代码,但不利于支持多目标代码的生成。高级的 IR 允许优化器更容易地提取源代码的意图,但不利于编译器根据不同的硬件特性进行代码优化。
LLVM IR 的设计采用 common IR
和 specific IR
相结合的方式。 common IR
旨在不同的后端共享对源程序的相同理解,以将其转换为不同的目标代码。除此之外,也为多个后端之间共享一组与目标无关的优化提供了可能性。 specific IR
允许不同的后端在不同的较低级别优化目标代码。这样做,既可以支持多目标代码的生成,也兼顾了目标代码的执行效率。
LLVM IR 有如下 3 种等价形式:
- 内存表示
- 类
llvm::Function
、llvm::Instruction
等用于表示common IR
。 - 类
llvm::MachineFunction
、llvm::MachineInstr
等用于表示specific IR
。
- 类
.bc
格式(Bitcode Files,存储在磁盘中).ll
格式(存储在磁盘中,便于人类阅读)
LLVM IR 的特点如下:
- 采用静态单一赋值(Static Single Assignment,SSA),即每个值只有一个定义它的赋值操作
- 代码被组织为三地址指令(Three-address Instructions)
- 有无限多个寄存器
# 基本命令
# C to IR/Bitcode
由 test.c
生成 IR
文件
clang -emit-llvm -S test.c -o test.ll | |
clang -emit-llvm -c test.c -o test.bc |
如果需要其他编译参数可以直接附加,只要保证有 -emit-llvm -S
参数即可生成 IR/Bicode
文件,例如:
clang -emit-llvm -S -g -O3 -funroll-loops test.c -o test.ll | |
clang -emit-llvm -c -g -O3 -funroll-loops test.c -o test.bc |
# CPP to IR/Bitcode
由 test.cpp
生成 IR
文件
clang++ -emit-llvm -S test.cpp -o test.ll | |
clang++ -emit-llvm -c test.cpp -o test.bc |
# C/CPP to execute
使用方法与 gcc
类似
clang test.c -o test | |
clang++ test.cpp -o test |
# IR to Bitcode
对于 LLVM 来说, .ll
格式是给 “人” 看的格式, .bc
是给 “机器” 看的格式,将 “人” 看的代码转成 “机器码”,就类似于汇编的过程,所以用 llvm-as
命令。
llvm-as test.ll -o test.bc |
# Bitcode to IR
同理,将 .bc
格式转换为 .ll
格式的过程就类似于反汇编的过程,所以用 llvm-dis
命令。
llvm-dis test.bc -o test.bc.ll |
# IR/Bicode to asm
生成目标平台的汇编代码,输入文件可以是 .bc
类型的,也可以是 .ll
类型的。
llc test.bc -o test.bc.s | |
llc test.ll -o test.ll.s |
# IR/Bicode to execute
IR 和 Bitcode 格式都能直接运行
lli test.ll | |
lli test.bc |
# 基本 IR 语法
# IR 结构
LLVM IR的结构(可先往下看,自己总结)
- 模块(Module):LLVM IR 的最顶层结构是模块,表示整个程序或库。一个模块由一组全局变量和函数组成。
- 全局变量(Global Variables):LLVM IR 允许声明全局变量,它们是在整个程序中可见的。全局变量由其类型、名称和可选的初始值组成。全局变量的声明以
@
符号开头,后面跟着变量名称和类型信息。例如,@myGlobal = global i32 0
表示一个名为myGlobal
的 32 位整数全局变量,初始值为 0。 - 函数(Functions):LLVM IR 中的函数由函数类型、名称、参数列表和函数体组成。函数类型包括返回类型和参数类型。函数的声明以
define
关键字开头,后面是返回类型、函数名称、参数列表和函数体。函数体由基本块(Basic Block)组成,每个基本块由一组指令组成。函数体的最后一条指令通常是返回指令。 - 基本块(Basic Blocks):基本块是 LLVM IR 中的基本执行单元。它由一系列按顺序执行的指令组成,没有分支或跳转指令。基本块以标签(Label)开头,后面是一组指令。基本块可以包含控制流指令,如条件分支和无条件跳转,以便实现程序的控制流程。
- 指令(Instructions):LLVM IR 包括各种指令来表示程序的操作。指令由操作码和操作数组成,用于执行特定的操作。LLVM IR 支持常见的指令类型,如赋值指令、算术运算指令、逻辑运算指令、条件分支指令、内存访问指令等。指令可以使用变量、常量和表达式作为操作数。
首先来看看 IR 长什么样子
#define _GNU_SOURCE | |
#include <stdio.h> | |
int main() { | |
printf("Hello World!\n"); | |
return 0; | |
} |
使用 clang -emit-llvm -S main.c -o main.ll
将上述代码编译成 IR 的 .ll
格式:
; ModuleID = 'main.c' | |
source_filename = "main.c" | |
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" | |
target triple = "x86_64-apple-macosx14.0.0" | |
@.str = private unnamed_addr constant [14 x i8] c"Hello World!\0A\00", align 1 | |
; Function Attrs: noinline nounwind optnone ssp uwtable | |
define dso_local i32 @main() #0 { | |
%1 = alloca i32, align 4 | |
store i32 0, i32* %1, align 4 | |
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0)) | |
ret i32 0 | |
} | |
declare i32 @printf(i8*, ...) #1 | |
attributes #0 = { noinline nounwind optnone ssp uwtable "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" } | |
attributes #1 = { "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" } | |
!llvm.module.flags = !{!0, !1} | |
!llvm.ident = !{!2} | |
!0 = !{i32 1, !"wchar_size", i32 4} | |
!1 = !{i32 7, !"PIC Level", i32 2} | |
!2 = !{!"Homebrew clang version 12.0.1"} |
虽然只是一个简单的 Hello world
程序,但是也基本包含了 IR 文件的各个部分。“麻雀虽小,五脏俱全”,让我们从上往下依次来看:
第一部分(1~4 行)
source_filename
:描述了源文件的名称及其所在路径。target datalayout
:描述了目标机器中数据的内存布局方式,包括:字节序、类型大小以及对齐方式「具体说明」。target triple
:描述了目标机器是什么,从而指示后端生成相应的目标代码。
第二部分(6~19 行)
标识符
LLVM IR 中的标识符分为:全局标识符和局部标识符。全局标识符以
@
开头,比如:全局函数、全局变量。局部标识符以%
开头,类似于汇编语言中的寄存器。标识符有如下 3 种形式:
- 有名称的值(Named Value),表示为带有前缀(
@
或%
)的字符串。比如:% val、@name。 - 无名称的值(Unnamed Value),表示为带前缀(
@
或%
)的无符号数值。比如:%0、%1、@2。 - 常量。
- 有名称的值(Named Value),表示为带有前缀(
define
:用于定义一个函数define dso_local i32 @main() #0 { ... }
,表示定义一个函数。其函数名称为main
,返回值的数据类型为i32
(占用 4 字节的整型),没有参数。#0
,用于修饰函数时表示一组函数属性「具体说明」。这些属性定义在文件末尾。如下:attributes #0 = { noinline nounwind optnone ssp uwtable "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
LLVM IR 中,函数体是由基本块(Basic Blocks)构成的。基本块是由一系列顺序执行的语句构成的,并(可选地)以标签作为起始。不同的标签代表不同的基本块。
基本块的特点如下:
- 仅有一个入口,即基本块中的第一条指令。
- 仅有一个出口,即基本块中的最后一条指令(被称为
terminator instruction
)。该指令要么跳转到其他基本块(不包括入口基本块),要么从函数返回。 - 函数体中第一个出现的基本块,称为入口基本块(Entry Basic Block)。它是一个特殊的基本块,在进入函数时立即执行该基本块,并且不允许作为其他基本块的跳转目标(即不允许该基本块有前继节点)。
第三部分(21~26 行)
llvm.module.flags
:命名元数据(Named Metadata)之一,用于描述模块级别的信息。llvm.module.flags
是一个包含一系列元数据节点的集合。集合中的每个元数据节点都是一个形如{<behavior>, <key>, <value>}
的三元组。其中,<behavior>
用于指定来自不同模块的<key>
和<value>
都相同的元数据合并时如何处理,<key>
表示元数据在本模块中的唯一标识符(仅在本模块内唯一,来自不同模块的元数据可能有相同的标识符),<value>
表示元数据所携带信息的值。常见的
<behavior>
值为「1」,表示「如果两个值不一致,则发出错误,否则生成的值就是操作数的值」。!llvm.module.flags = !{!0, !1} ; 指定了两个元数据!0 和!1
...
!0 = !{i32 1, !"wchar_size", i32 4} ; 表示标识符为 "wchar_size" 的元数据所携带信息的值在所有模块中都应该是 4,否则会报错
!1 = !{i32 7, !"PIC Level", i32 2} ; 同理
llvm.ident
:用于表示 Clang 的版本信息。
# IR 指令
在 LLVM 的 IR 中,所有的数据类型都是基于 LLVM 类型系统定义的,这些数据类型包括整数、浮点数、指针、数组、结构体等等,每个数据类型都具有自己的属性,例如位宽、对齐方式等等。在 IR 中,每个值都有一个类型,这个类型可以被显式地指定,或者通过指令的操作数推导出来。
LLVM 的 IR 指令非常丰富,包括算术、逻辑、比较、转换、控制流等等,它们可以被用来表达复杂的程序结构,同时 IR 指令还可以通过 LLVM 的优化器进行优化,以生成高效的目标代码。
IR 指令类型比较多,以下是一些常见的指令类型:
- 加减乘除指令:add, sub, mul, sdiv, udiv, fadd, fsub, fmul, fdiv 等
- 位运算指令:and, or, xor, shl, lshr, ashr 等
- 转换指令:trunc, zext, sext, fptrunc, fpext, fptoui, fptosi, uitofp, sitofp, ptrtoint, inttoptr, bitcast 等
- 内存指令:alloca, load, store, getelementptr 等
- 控制流指令:br, switch, ret, indirectbr, invoke, resume, unreachable 等
- 其他指令:phi, select, call, va_arg, landingpad 等
指令相关内容资料丰富,不再具体赘述了,可以参考:
- LLVM 的 IR 指令详解
- LLVM 官方手册
# LLVM C++ API 相关操作
# 读 / 写 IR 文件
读写 IR 文件是分析的基础,通过读入 IR 文件并进行解析,可以向源程序中插入我们自定义的操作,然后将更改后的 IR 写回文件,将结果保存下来。假设需要读入一个 main.ll
的 IR 文件并对其进行操作,操作完成后将 IR 保存成 .bc
的格式。
#include <iostream> | |
#include <llvm/IR/LLVMContext.h> | |
#include <llvm/IR/Module.h> | |
#include <llvm/IR/IRBuilder.h> | |
#include <llvm/IR/Instructions.h> | |
#include <llvm/Support/SourceMgr.h> | |
#include <llvm/IRReader/IRReader.h> | |
#include <llvm/IR/BasicBlock.h> | |
#include <llvm/Transforms/Utils/BasicBlockUtils.h> | |
#include "llvm/Support/raw_ostream.h" | |
#include "llvm/Bitcode/BitcodeWriter.h" | |
using namespace llvm; | |
int main() { | |
LLVMContext ctx; | |
SMDiagnostic err; | |
// 读入指定 IR 文件并解析,获取 LLVMContext | |
std::unique_ptr<Module> mod_ptr = parseIRFile("main.ll", err, ctx); | |
if (!mod_ptr) { | |
std::cerr << "Failed to read IR File\n"; | |
return -1; | |
} | |
Module *mod = mod_ptr.get(); | |
// 分析操作 | |
// 保存成文件 | |
std::error_code EC; | |
llvm::raw_fd_ostream OS("main.bc", EC); | |
if (EC) { | |
std::cerr << "failed to open output file\n"; | |
return -1; | |
} | |
llvm::WriteBitcodeToFile(*mod_ptr, OS); | |
return 0; | |
} |
保存成
.ll
格式:std::error_code EC;
llvm::raw_fd_ostream OS(source_ll, EC);
if (EC) {
std::cerr << "failed to open output file\n";
return -1;
}
mod->print(OS, nullptr);
在终端直接输出
.ll
格式mod->print(outs(), nullptr);
# 遍历 IR 文件
这里给出遍历所有指令的代码
#include <iostream> | |
#include <llvm/IR/LLVMContext.h> | |
#include <llvm/IR/Module.h> | |
#include <llvm/IR/IRBuilder.h> | |
#include <llvm/IR/Instructions.h> | |
#include <llvm/Support/SourceMgr.h> | |
#include <llvm/IRReader/IRReader.h> | |
#include <llvm/IR/BasicBlock.h> | |
#include <llvm/Transforms/Utils/BasicBlockUtils.h> | |
#include "llvm/Support/raw_ostream.h" | |
#include "llvm/Bitcode/BitcodeWriter.h" | |
using namespace llvm; | |
int main() { | |
LLVMContext ctx; | |
SMDiagnostic err; | |
std::unique_ptr<Module> mod_ptr = parseIRFile("main.ll", err, ctx); | |
if (!mod_ptr) { | |
std::cerr << "Failed to read IR File\n"; | |
return -1; | |
} | |
Module *mod = mod_ptr.get(); | |
// 依次遍历 * 函数 * | |
for (Function &F : *mod) { | |
// 依次遍历 * 基本块 * | |
for (BasicBlock &BB : F) { | |
// 依次遍历 * 指令 * | |
for (Instruction &I : BB) { | |
// 对指令进行操作 | |
} | |
} | |
} | |
mod->print(outs(), nullptr); | |
return 0; | |
} |
范围for「C++ 11引入」
C++ 11 中引入了迭代器对象,可通过使用成员和全局函数(如 begin()
和 end()
)以及运算符(如 ++
和 --
)向前或向后移动,来显式使用迭代器。也可以通过范围 for 循环来隐式使用迭代器。
/* 显示使用迭代器 */ | |
// 定义一个迭代器 | |
vector<int> vec{ 0,1,2,3,4 }; | |
for (auto it = begin(vec); it != end(vec); it++) | |
{ | |
// Access element using dereference operator | |
cout << *it << " "; | |
} |
上述代码等价于
// 推荐使用 auto 来自动获取类型 | |
for (auto num : vec) | |
{ | |
// no dereference operator | |
cout << num << " "; | |
} |
# 创建函数 / 基本块 / 指令
在创建函数 / 基本块 / 指令时需要先获取当前的上下文,并设置好 “插入点”,创建的基本块 / 指令会插入到 “插入点” 指定的位置。
#include <iostream> | |
#include <llvm/IR/LLVMContext.h> | |
#include <llvm/IR/Module.h> | |
#include <llvm/IR/IRBuilder.h> | |
#include <llvm/IR/Instructions.h> | |
#include <llvm/Support/SourceMgr.h> | |
#include <llvm/IRReader/IRReader.h> | |
#include <llvm/IR/BasicBlock.h> | |
#include <llvm/Transforms/Utils/BasicBlockUtils.h> | |
#include "llvm/Support/raw_ostream.h" | |
#include "llvm/Bitcode/BitcodeWriter.h" | |
using namespace llvm; | |
int main() { | |
LLVMContext ctx; | |
SMDiagnostic err; | |
std::unique_ptr<Module> mod_ptr = parseIRFile("main.ll", err, ctx); | |
if (!mod_ptr) { | |
std::cerr << "Failed to read IR File\n"; | |
return -1; | |
} | |
Module *mod = mod_ptr.get(); | |
IRBuilder<> builder(ctx); | |
// 先声明函数结构:这里声明了一个返回值为 double,参数为一个 double 类型的函数;false 表明该函数参数个数不可变 | |
FunctionType *funcType = FunctionType::get(Type::getDoubleTy(ctx), {Type::getDoubleTy(ctx)}, false); | |
// 创建一个函数,将函数命名为 check | |
Function *checkFunc = Function::Create(funcType, Function::ExternalLinkage, "check", mod); | |
// 存储参数(获取参数的引用) | |
Function::arg_iterator argsIT = checkFunc->arg_begin();//Function 中的一个方法,获取参数的迭代器 | |
Value *arg_n = argsIT++; // 获取第一个参数 | |
// 在 "check" 函数中创建一个基本块 | |
BasicBlock *entryBlock = BasicBlock::Create(ctx, "", checkFunc); | |
builder.SetInsertPoint(entryBlock); // 设置插入点为上一行创建的基本块 | |
Value *fRemResult = builder.CreateFRem(arg_n, ConstantFP::get(ctx, APFloat(100.0))); // 创建 "FRem" 指令 | |
builder.CreateRet(fRemResult); // 创建 "ret" 指令, 返回值为 "FRem" 的结果 | |
for (Function &F : *mod) { | |
for (BasicBlock &BB : F) { | |
for (Instruction &I : BB) { | |
... | |
} | |
} | |
} | |
mod->print(outs(), nullptr); | |
return 0; | |
} |
# 创建跳转
通过函数调用进行跳转
// BO 是 FAdd 指令指针,同时也是该指令的返回值
auto *BO = dyn_cast<BinaryOperator>(&I) {
if (BO->getOpcode() == Instruction::FAdd) {
// 设置插入点为 BO 指令的下一条
builder.SetInsertPoint(BO->getNextNode());
// 创建 call 指令,call checkFunc 函数,参数为 BO
Value *checkResult = builder.CreateCall(checkFunc, {BO});
}
}
通过函数返回进行跳转
// 函数原型:ReturnInst *CreateRet (Value *RetVal, Instruction *InsertBefore = nullptr);
// 接受一个返回值 RetVal,将其作为函数的返回值返回。可选的 InsertBefore 参数允许指定插入该指令的位置,默认为 nullptr,表示在当前插入点处插入。
builder.CreateRet(fRemResult);
无条件跳转
// 在当前基本块的末尾创建一个无条件跳转指令,使控制流转移到 DestBB 指定的基本块
builder.CreateBr(DestBB);
条件跳转
// 创建比较指令,比较 sum 和 100.0 的大小
Value *cmp = builder.CreateFCmpUGT(sum, ConstantFP::get(ctx, APFloat(100.0)));
// 创建条件跳转,完整定义如下:
// BranchInst *CreateCondBr(Value *Cond, BasicBlock *TrueBB, BasicBlock *FalseBB, Instruction *InsertBefore = nullptr);
builder.CreateCondBr(cmp, TrueBB, FalseBB);
# 获取 / 更改参数
加入我想通过 LLVM C++ API 获取一个 IR 指令的参数以便后续分析、或将其修改为指定值
// 创建一个加法指令 | |
Instruction *AddInst = BinaryOperator::CreateAdd(Operand1, Operand2, "sum"); | |
Value *NewValue = ...; // 定义一个新的操作数值 NewValue | |
// 函数原型:void Instruction::setOperand (unsigned index, Value *val); | |
//index 表示要设置的操作数的索引,从 0 开始,表示指令的第一个操作数; | |
//val 表示要设置的新的操作数值,类型为 Value,可以是常量、变量或其他指令的结果 | |
AddInst->setOperand(0, NewValue); // 将 Add 指令的第一个参数修改为 NewValue | |
// 获取第一个操作数 Operand1 | |
// 注意:AddInst 同时也是一个 Value * 类型,表示 add 的计算结果 | |
AddInst->getOperand(0) |
# 参考
- https://evian-zhang.github.io/llvm-ir-tutorial/index.html
- https://llvm.org/docs/LangRef.html
- https://llvm.org/docs/CodingStandards.html