⚠️ 笔者本人也是 LLVM 新手,所以本篇内容只是笔者在自己的理解上写出的笔记性质的 “教程”,如有错误,还请大佬们指正。写这篇文章的目的在于,笔者自己在入门 LLVM IR 及 API 时基本都是英文教程,国内的教程非常杂乱,LLVM IR 相关的内容还比较丰富,但对于 LLVM API 相关的资料就比较少了,笔者在学习过程中的很多问题都是在 Stack Overflow 上得到解答的,所以这里将各种踩坑记录下来,帮助其他同学。

# LLVM 安装与环境配置

笔者的环境为:

  • MacBook Pro 2019 Intel 芯片 macOS 14.2.1 Sonoma
  • Homebrew 4.2.11
  • CLion 233.14475.31, built on February 13, 2024

# 安装

由于 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! ,可以通过如下方式解决:

  1. brew tap --force homebrew/core
  2. brew edit llvm@11
  3. disable! date: "2024-02-22", because: :versioned_formula 中的过期时间改为当前或以后,然后保存
  4. 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。

  1. 随便创建一个 C++ Executable 项目,语言标准任选。

  2. 点击 CLion => settings => Build, Execution, Deployment => CMake options,写入:
    -DLLVM_DIR:PATH=/usr/local/Cellar/llvm@11/11.1.0_4/lib/cmake/llvm

  3. CLion 应该会自动为当前项目创建一个 CMakeLists.txt 。如果没有,则在 CLion 左边的项目根目录中右键 => new => CMakeLists.txt

  4. 将如下内容写入 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})
  5. 保存后即可使用 CLion 愉快的开发 / Debug LLVM C++ API 代码了。

⚠️常见问题
  1. 如果在第 4 步出现错误:找不到 LLVMConfig.cmake 文件,代表你的 Homebrew 和笔者不同,请全局搜索 LLVMConfig.cmake 文件并在 -DLLVM_DIR:PATH= 后写入正确的位置)
  2. 上述代码中 llvm_map_components_to_libnames(llvm_libs irreader) 是一种按需使用 LLVM 组件的方式, irreader 就是一个组件,对应着头文件 "llvm/IR/IRBuilder.h" ,如果你在引用头文件是出现 "xxx.h" not found 的错误,请检查是否在 CMakeLists.txt 中引入了对应的组件。
  3. 可以通过 llvm-config --components all 列出 LLVM 的所有组件。

# LLVM 基本知识

# 基本概念

# 引入

随着计算机技术的不断发展以及各种领域需求的增多,近几年来,许多编程语言如雨后春笋般出现,他们大多为了解决某一些特定领域的需求,比如说为 JavaScript 增加静态类型检查的 TypeScript,为解决服务器端高并发的 Golang,为解决内存安全和线程安全的 Rust。

让我们设身处地地想象一下,如果我们想开发一门新的编译型的编程语言,有什么需要解决的问题呢?

  1. 怎样让我的编程语言能在尽可能多的平台上运行

    ​ 我想让我的编程语言能够在 Windows、macOS 和 Linux 上都可以运行;我想让我的编程语言能够在 Intel 芯片、ARM 芯片,乃至龙芯上都可以运行。

  2. 怎样让我的编程语言可以使用前人先进的技术

    ​ 在编程语言发展的过程中,有许许多多成熟的技术。从汇编指令层面来看,简单地想给一个变量值置 0,AMD64 架构下可以用 xor %eax, %eax 异或自身,AArch64 架构下可以用 mov w0, wzr 使用零寄存器;从算法层面来看,可以使用尾调用优化、CFI 等。如果我不想重复造轮子,该如何复用这些技术呢?

  3. 怎样让我的编程语言在汇编层面实现「定制」

    ​ 高级语言中的函数名,在汇编层面,我想让他换个名字;我想让 C 语言库能用它的调用约定来调用我的高级语言中的函数。

我们可以选择使用 C 语言来作为 “中间层”,当我们开发新编译语言时,提供该语言到 C 语言的转换,这样做是因为:

  1. 绝大部分的操作系统都是由 C 和汇编语言写成,因此平台大多会提供一个 C 编译器可以使用,这样就解决了第一个问题
  2. C 语言历史久远,有非常多的优化器,程序翻译成 C 语言后就可以直接使用那些为 C 语言定制的优化器,从而方便的使用前人的成熟技术,这就解决了第二个问题。
  3. 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 的前端是为各种编程语言定制的,负责将对应编程语言的源代码转换成 LLVM 中间表示(IR,Intermediate Representation);LLVM 的后端为各种不同的平台做了适配,负责将 LLVM IR 转换成在特定平台上可执行的二进制文件。如果在编译过程中涉及到优化的话,则由 LLVM 优化器来对 IR 文件进行优化,并将优化后的 IR 文件输出给 LLVM 后端去编译,因此 IR 的设计是 LLVM 的灵魂所在。

# LLVM IR

🔔 有关 IR 的更多知识,将在「基本 IR 指令」章节中讲解

LLVM IR 的设计体现了权衡的计算思维。低级的 IR(即更接近目标代码的 IR)允许编译器更容易地生成针对特定硬件的优化代码,但不利于支持多目标代码的生成。高级的 IR 允许优化器更容易地提取源代码的意图,但不利于编译器根据不同的硬件特性进行代码优化。

LLVM IR 的设计采用 common IRspecific IR 相结合的方式。 common IR 旨在不同的后端共享对源程序的相同理解,以将其转换为不同的目标代码。除此之外,也为多个后端之间共享一组与目标无关的优化提供了可能性。 specific IR 允许不同的后端在不同的较低级别优化目标代码。这样做,既可以支持多目标代码的生成,也兼顾了目标代码的执行效率。

LLVM IR 有如下 3 种等价形式:

  • 内存表示
    • llvm::Functionllvm::Instruction 等用于表示 common IR
    • llvm::MachineFunctionllvm::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的结构(可先往下看,自己总结)
  1. 模块(Module):LLVM IR 的最顶层结构是模块,表示整个程序或库。一个模块由一组全局变量和函数组成。
  2. 全局变量(Global Variables):LLVM IR 允许声明全局变量,它们是在整个程序中可见的。全局变量由其类型、名称和可选的初始值组成。全局变量的声明以 @ 符号开头,后面跟着变量名称和类型信息。例如, @myGlobal = global i32 0 表示一个名为 myGlobal 的 32 位整数全局变量,初始值为 0。
  3. 函数(Functions):LLVM IR 中的函数由函数类型、名称、参数列表和函数体组成。函数类型包括返回类型和参数类型。函数的声明以 define 关键字开头,后面是返回类型、函数名称、参数列表和函数体。函数体由基本块(Basic Block)组成,每个基本块由一组指令组成。函数体的最后一条指令通常是返回指令。
  4. 基本块(Basic Blocks):基本块是 LLVM IR 中的基本执行单元。它由一系列按顺序执行的指令组成,没有分支或跳转指令。基本块以标签(Label)开头,后面是一组指令。基本块可以包含控制流指令,如条件分支和无条件跳转,以便实现程序的控制流程。
  5. 指令(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。
    • 常量。
  • 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 指令类型比较多,以下是一些常见的指令类型:

  1. 加减乘除指令:add, sub, mul, sdiv, udiv, fadd, fsub, fmul, fdiv 等
  2. 位运算指令:and, or, xor, shl, lshr, ashr 等
  3. 转换指令:trunc, zext, sext, fptrunc, fpext, fptoui, fptosi, uitofp, sitofp, ptrtoint, inttoptr, bitcast 等
  4. 内存指令:alloca, load, store, getelementptr 等
  5. 控制流指令:br, switch, ret, indirectbr, invoke, resume, unreachable 等
  6. 其他指令:phi, select, call, va_arg, landingpad 等

指令相关内容资料丰富,不再具体赘述了,可以参考:

  • LLVM 的 IR 指令详解
  • LLVM 官方手册

# LLVM C++ API 相关操作

  • 本章使用的 C++ API 对应 LLVM 17.0.6_1 ,同大版本下 API 基本不会发生太大变化,而如果是不同大版本,部分 API 的定义可能不同,需要自行查看 API 手册。

  • 查看 LLVM 版本: llvm-config --version

  • API 手册:https://llvm.org/docs/LangRef.html

  • ⚠️ 为了方便书写,给出的代码大多是片段的形式。在默认情况下,本章节代码具有连续性,即若代码片段中某个变量未定义但直接使用,可以往前找找看。

# 读 / 写 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;
}

# 创建跳转

  1. 通过函数调用进行跳转

    // 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});
    	}
    }
  2. 通过函数返回进行跳转

    // 函数原型:ReturnInst *CreateRet (Value *RetVal, Instruction *InsertBefore = nullptr);
    // 接受一个返回值 RetVal,将其作为函数的返回值返回。可选的 InsertBefore 参数允许指定插入该指令的位置,默认为 nullptr,表示在当前插入点处插入。
    builder.CreateRet(fRemResult);
  3. 无条件跳转

    // 在当前基本块的末尾创建一个无条件跳转指令,使控制流转移到 DestBB 指定的基本块
    builder.CreateBr(DestBB);
  4. 条件跳转

    // 创建比较指令,比较 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

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Gality 微信支付

微信支付

Gality 支付宝

支付宝