0x01 写在前面

LLVM 在我的理解上是一种编译框架的作用,一种可以取代 GCC 框架的编译工具。设计出来的初衷,我不得而知,但是相比于 GCC 他拥有较多的好处。最直接一点是,他通过将不同的前端将高级语言转化成统一的 LLVM 中间语言(IR),然后可以使用统一的优化代码 (Pass) 来对进行代码优化,而程序分析着也可以在这个阶段对目标程序进行源代码静态分析,然后 LLVM 再将 IR 通过不同的后端编译成对应目标平台的可执行程序,因此具有很不错的跨平台性和跨语言性。

clangLLVM 针对 C 语言及其家族语言的前端。它的主要目标是提供一个 GNU 编译器套装(GCC)的替代品,支持 GNU 编译器大多数编译设置以及非官方语言拓展,项目包括 Clang 前端和 Clang 静态分析器。

以下所有的配置和分析均在 LLVM-13.0 的基础上进行的,不同版本略微语法有所不同。

LLVM编译过程

0x02 clang

环境配置

根据官方指导的教程即可,可以直接安装,也可以从源码编译。

编译程序

首先简单创建一个目标程序:

1
2
3
4
5
#include<stdio.h>
int main(){
printf("hello world!\n");
return 0;
}

直接编译

可以直接用类似于 gcc 的模式来进行编译:

1
clang helloworld.c -o hello 

分步编译

生成预处理文件

处理 define , include 等。

使用下面的命令会生成预处理文件 helloworld.i :

1
clang -E helloworld.c -o helloworld.i

生成汇编文件

先转化为中间代码 IR 这个过程中会有优化,然后再生成汇编代码。

使用下面的命令会生成汇编文件 helloworld.s :

1
clang -S helloworld.i

生成目标文件

使用下面的命令会生成目标文件 helloworld.o :

1
clang -c helloworld.s

生成可执行文件

使用下面的命令会生成可执行文件 helloworld :

1
clang -o helloworld helloworld.s

查看编译过程

使用下面的命令可以看到编译过程中的各阶段:

1
2
3
4
5
6
7
8
clang -ccc-print-phases helloworld.c

+- 0: input, "helloworld.c", c
+- 1: preprocessor, {0}, cpp-output
+- 2: compiler, {1}, ir
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
5: linker, {4}, image

生成 IR 文件

clang 中加入参数:

  • -emit-llvm 生成IR文件(ll)
  • -cc1 只调用前端
  • -ast-dump 输出 AST
  • -E only run preprocessor
  • -S only run preprocess and complication steps (.ll)
  • -c only run preprocess, compile, and assemble steps (.bc)

opt指令

看起来好像这个指令是一个优化指令,和三个参数有关,一个是需要处理的 ll 文件,一个是处理 ll 文件的 pass ,最后是输出 pass 优化处理后的 ll 文件。所以中间我们观测的值是通过 stderr 来进行输出的。

进行分析

分析之前我们都从简单的例子入手方便进行理解和分析,这时候前面的 helloworld.c 其实都太复杂了,因为 include 了头文件太多了,这里我们不追求能跑起来东西,所以简单的写个例子意思一下就好了:

1
2
3
4
// test.c
int add(int a, int b){
return a + b;
}

词法分析

词法分析(英語:lexical analysis)是计算机科学中将字符序列转换为记号(token)序列的过程。 进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner)。 词法分析器一般以函数的形式存在,供语法分析器调用。

词法分析阶段是编译过程的第一个阶段。这个阶段的任务是从左到右一个字符一个字符地读入源程序,即对构成源程序的字符流进行扫描然后根据构词规则识别单词(也称单词符号或符号)。词法分析程序实现这个任务。词法分析程序可以使用lex等工具自动生成。

词法分析要解决的问题是:如何将输入的字符序列转换成 token 序列,

Token = <种别码, 属性值>

使用命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
clang -fmodules -E -Xclang -dump-tokens test.c
int 'int' [StartOfLine] Loc=<test.c:1:1>
identifier 'add' [LeadingSpace] Loc=<test.c:1:5>
l_paren '(' Loc=<test.c:1:8>
int 'int' Loc=<test.c:1:9>
identifier 'a' [LeadingSpace] Loc=<test.c:1:13>
comma ',' Loc=<test.c:1:14>
int 'int' [LeadingSpace] Loc=<test.c:1:16>
identifier 'b' [LeadingSpace] Loc=<test.c:1:20>
r_paren ')' Loc=<test.c:1:21>
l_brace '{' Loc=<test.c:1:22>
return 'return' [StartOfLine] [LeadingSpace] Loc=<test.c:2:5>
identifier 'a' [LeadingSpace] Loc=<test.c:2:12>
plus '+' [LeadingSpace] Loc=<test.c:2:14>
identifier 'b' [LeadingSpace] Loc=<test.c:2:16>
semi ';' Loc=<test.c:2:17>
r_brace '}' [StartOfLine] Loc=<test.c:3:1>
eof '' Loc=<test.c:3:2>

语法分析

语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确.源程序的结构由上下文无关文法描述.

使用命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
clang -fmodules -fsyntax-only -Xclang -ast-dump test.c
TranslationUnitDecl 0x109b6b8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x109bf50 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x109bc50 '__int128'
|-TypedefDecl 0x109bfc0 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x109bc70 'unsigned __int128'
|-TypedefDecl 0x109c2d8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x109c0b0 'struct __NSConstantString_tag'
| `-Record 0x109c018 '__NSConstantString_tag'
|-TypedefDecl 0x109c370 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x109c330 'char *'
| `-BuiltinType 0x109b750 'char'
|-TypedefDecl 0x10df000 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x109c620 'struct __va_list_tag [1]' 1
| `-RecordType 0x109c460 'struct __va_list_tag'
| `-Record 0x109c3c8 '__va_list_tag'
`-FunctionDecl 0x10df200 <test.c:1:1, line:3:1> line:1:5 add 'int (int, int)'
|-ParmVarDecl 0x10df070 <col:9, col:13> col:13 used a 'int'
|-ParmVarDecl 0x10df0f0 <col:16, col:20> col:20 used b 'int'
`-CompoundStmt 0x10df3b0 <col:22, line:3:1>
`-ReturnStmt 0x10df3a0 <line:2:5, col:16>
`-BinaryOperator 0x10df380 <col:12, col:16> 'int' '+'
|-ImplicitCastExpr 0x10df350 <col:12> 'int' <LValueToRValue>
| `-DeclRefExpr 0x10df310 <col:12> 'int' lvalue ParmVar 0x10df070 'a' 'int'
`-ImplicitCastExpr 0x10df368 <col:16> 'int' <LValueToRValue>
`-DeclRefExpr 0x10df330 <col:16> 'int' lvalue ParmVar 0x10df0f0 'b' 'int'

这个语法分析得到的图是有彩色的:

语法分析图

语义分析

语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。比如语义分析的一个工作是进行类型审查,审查每个算符是否具有语言规范允许的运算对象,当不符合语言规范时,编译程序应报告错误。如有的编译程序要对实数用作数组下标的情况报告错误。又比如某些程序规定运算对象可被强制,那么当二目运算施于一整型和一实型对象时,编译程序应将整型转换为实型而不能认为是源程序的错误。

这里主要是生成 LLVM IR ,主要有三种存在的形式,但是我们主要是看 text 的格式就好。

执行下面的命令,会将中间语言 IR 存在文件 test.ll 中。

1
clang -S -emit-llvm test.c

文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @add(i32 %0, i32 %1) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 %0, i32* %3, align 4
store i32 %1, i32* %4, align 4
%5 = load i32, i32* %3, align 4
%6 = load i32, i32* %4, align 4
%7 = add nsw i32 %5, %6
ret i32 %7
}

attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "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"="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" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 10.0.0-4ubuntu1 "}

上述默认是优化之后的形式,我们还可以打出没有优化过的形式:

1
clang test.c -S -emit-llvm -o - -O3

会得到下面的代码,发现确实是有些不同的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

; Function Attrs: norecurse nounwind readnone uwtable
define dso_local i32 @add(i32 %0, i32 %1) local_unnamed_addr #0 {
%3 = add nsw i32 %1, %0
ret i32 %3
}

attributes #0 = { norecurse nounwind readnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="none" "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"="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" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 10.0.0-4ubuntu1 "}

几种关系图

CFG 控制流图 (basic blocks)

基本块 basic blocks

基本块是满足下面属性的最大的连续指令序列:

  • 控制流只能从基本块的第一行开始执行(不能有 jump 执行块中间的某行代码)
  • 除非是基本块的最后一条指令,否则不允许包含离开基本块的分支或者挂机指令

基本块的 leader :

  • 代码的第一行是基本块首领 (leader)
  • 任何条件或者非条件跳转指令的目标行是基本块首领
  • 任意条件或者非条件跳转指令的下一行是基本块首领

基本块的界定方法:

  • 基本块的首领是基本块的一部分
  • 基本块首领到下一个基本块的首领直接的代码,属于该基本块

画图

控制流图我们代码就写到一个函数中来看调用情况:

1
2
3
4
5
6
7
8
9
// cfg.c
int fact(int n) {
int ans = 1;
while (n > 1) {
ans *= n;
n--;
}
return ans;
}

输入的命令如下:

1
clang -S -emit-llvm cfg.c -o - | opt -analyze -dot-cfg

会对每个函数生成一个隐藏文件 .function_name.dot

然后我们再转化为对应的图像:

1
dot .fact.dot -Tpng -o cfg.fact.png

对应的图如下(其实可以染色的,但我还不会)

LLVM IR为我们提供的条件跳转指令是br,其接受三个参数,第一个参数是i1类型的值,用于作判断;第二和第三个参数分别是值为truefalse时需要跳转到的标签。

CFG

CallGraph 函数调用图

Clang对于函数调用的处理不够完整,对于特殊情况的函数调用处理不好,如:

  1. extern 函数调用,无法获知函数体,不会创建到 CallGraph 中
  2. 函数指针调用,无法获知函数体,不会创建到 CallGraph 中
  3. 模板函数调用,在编译期可以获知进行模板实例化,会添加到 CallGaph 中,且调用图上没有在别的c文件中定义的函数(.h中声明的),即没有正文的函数

为了比较好体现下面的函数调用图,代码不能太简单,至少逻辑上得出来这几种关系。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// callgraph.c

#include<stdio.h>
#include<stdlib.h>
int level()
{
int ret = rand() % 100;
return ret;
}
int level2(int input)
{
int ret = rand() % input;
return ret;
}
int main()
{
int a = level();
int b = level2(a);
printf("%d\n", b);
}

输入命令如下:

1
clang -S -emit-llvm callgraph.c -o - | opt -analyze -dot-callgraph

会将数据写到 callgraph.dot 中。

1
dot -Tpng -ocallgraph.png callgraph.dot

会将得到的数据做成图片: callgraph.png

callgraph.png

AST图

略,暂时用不上,就没搞。

0x03 LLVM Pass

简单尝试

先跑

这个地方尝试 LLVM pass 参考项目: https://github.com/abenkhadra/llvm-pass-tutorial

但是经过后面的探索,上述应该是来自 这里

我不想从源码编译,直接跳到了第二步,Building a trivial LLVM pass

在执行命令之前,需要先配置一下环境变量不然会报错。

下面设置的就是他 cmake 需要的环境变量 $LLVN_HOME/usr/localLLVM 的默认安装位置。

1
$ export LLVM_HOME=/usr/local

然后再按教程跑就可以成功编译了(但是不知道没有去编译 LLVM 能不能行):

1
2
3
$ # pwd (build)
$ cmake ..
$ make

然后我们简单创建一个测试的 c 文件,放置在项目根文件下:

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>

void f(){
printf("hello world!\n");
}

int main(){
f();
return 0;
}

然后在项目根目录运行下面的命令:

1
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.* test.c -o test

输出为:

1
2
I saw a function called f!
I saw a function called main!

这个项目的作用就是观测测试程序中有哪些函数的。(前一个学习的知识告诉我们 clang 都能画出函数调用图了,所以显然是完全能知道各个函数的名字的呀。

分析一下

其实这个和 官方教程 (翻译版) 差不多(简单分析了一下,代码应该是一模一样的意思),但是感觉官方的解释不是很友好,我并不想从头编译,也不知道 LLVM 源代码目录在哪里,所以。。。

在 pass 中可以写两行代码来确定使用 pass 的方式: clangopt 都行。

这个的过程也比较清晰: http://www.helloted.com/ios/2020/06/28/clang/

这篇 知乎 给了一个如何从头学习的路线,具有一定的参考价值,但此时我已经踩完坑了。

修改一下

简单修改了一下 CmakeList.txt 不然每次重开都要设置环境变量,让他自己去找系统默认 /usr/local 的就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cmake_minimum_required(VERSION 3.4)
project(llvm-pass-tutorial)

# we need LLVM_HOME in order to automatically set LLVM_DIR
if(NOT DEFINED ENV{LLVM_HOME})
message(WARNING "$LLVM_HOME is not defined, try to use '/usr/local/' [defaul]")
set(ENV{LLVM_DIR} /usr/local/lib/cmake/llvm)
else ()
set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib/cmake/llvm)
endif()

find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
if (${LLVM_VERSION_MAJOR} VERSION_GREATER_EQUAL 10)
set(CMAKE_CXX_STANDARD 14)
endif ()

add_subdirectory(skeleton) # Use your pass name here.
message("Success!")

学习 IR & pass

GitHub 上也有别人关于 LLVM-IR 的学习引导。

找到了某个大学(cornell.edu)介绍的 实验流程。还对应给了一个 GitHub 仓库 llvm-pass-skeleton 实现了对应的 pass 。

分析 Skeleton.cpp

  • That errs() thing is an LLVM-provided C++ output stream we can use to print to the console.
  • The function returns false to indicate that it didn’t modify F. Later, when we actually transform the program, we’ll need to return true.

LLVM IR

LLVM 中各个组件部分的关系:

components of LLVM

Module 包括了 FunctionFunction 包括了 BasicBlockBasicBlock 包括了 Instruction ,但是除了 Module 之外的所有东西都来自 Value

  • Module 是一堆粗糙的源代码或者说 translate unit ,所有的东西都包括在其中。
  • Module 包含 Function :命名的可执行代码块。(包括 C++ 中的 FunctionMethod
  • 除了声明其名称和参数之外,Function 主要是 BasicBlock 的容器。 BasicBlock 是编译器熟悉的概念,但就我们的目的而言,它只是一段连续的指令。
  • 指令是单个代码操作。 例如,指令可能是整数加法、浮点除法或内存存储。

LLVM 中的大多数东西——包括 FunctionBasicBlockInstruction ——都是从名为 Value 的杂食基类继承的 C++ 类。 Value 是可以在计算中使用的任何数据——例如,数字或某些代码的地址; 全局变量和常量。

IR 中的部分符号

1
2
3
4
5
%a -- 表示寄存器
@a -- 表示全局变量/函数名字
!0 -- 表示元变量 用于帮助后端进行优化啥的
#0 -- 表示属性组 用于修饰函数、变量啥的可见域、是否优化等
$a -- 应该是表示的函数的参数?

Instruction

例如下面这个指令

1
%5 = add i32 %4, 2

这个指令表示将寄存器 %4 中的值与数字 2 相加,然后将结果存储到寄存器 %5 中。

寄存器的数量可以有无限多

同样的指令在编译器内部表示为 C++ 类 Instruction 的一个实例。 该对象有一个操作码(opcode),表明它是一个加法、一个类型和一个操作数(operands)列表,这些操作数是指向其他 Value 对象的指针。

也就是说这个指令其实是一个C++类的实例,这个指令是一个操作码(add) 然后后面跟上一个类型 ( i32 ) 然后会跟上一串操作数的列表 ( %4 , 2) 这些操作数列表具体是指向其他 Value 这个类的某个实例对象。

在我们的例子中,它指向一个代表数字 2 的常量对象和另一个对应于寄存器 %4 的指令。 (由于 LLVM IR 是静态单一赋值形式,寄存器和指令实际上是相同的。寄存器编号仅仅为了方便进行文本表示。)

可以通过下面的命令查看自己编写的代码对应的 LLVM 的 IR 语言样儿(前面已经学过了

1
$ clang -emit-llvm -S -o - something.c

LoadInst

长相如下

1
%6 = load i32*, i32** %4, align 4

这个例子的意思是从 %4 这个地址读取一个 int32* 的数据,值存在寄存器 %6 里

这是一个一元指令,即只有一个操作数。在 llvm 中获取其操作数时命令为:

1
2
// LoadInst *LI;
Value *OP = LI->getOperand(0); // 等价于 LI->getPointerOperand()

此时你可能会好奇,这个值不是读取了存到了寄存器 %6 中间了吗?应该怎么获取这个值存储的位置 %6 呢。其实这个存储的值的位置就是这个指令本身。至于怎么构建连接,请看下文 StoreInst 中的例子。

StoreInst

长相如下

1
store i32 %0, i32* %6, align 4

这个例子的意思是将 %0 里面的 int32 值存储在 %6 这个寄存器制定的地址中。

这是一个二元指令,有两个操作数。在 llvm 中获取其操作数时命令为:

1
2
3
// StoreInst *SI;
Value *OP0 = SI->getOperand(0); // 等价于 SI->getValueOperand()
Value *OP1 = SI->getOperand(1); // 等价于 SI->getPointerOperand()

这时我们发现对于其中的 %0%6 ,我们都可以获取到。

但是如果是上文中一个 LoadInst 指令读取了一个值到 %6 中,此时这个 StoreInst 的位置不是就会被其影响吗?那应该怎么定位它呢?方法很简单,即 %6 这个 Value* 就是这个 LoadInst 本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// StoreInst *SI;

Value *OP0 = SI->getOperand(0); // 等价于 SI->getValueOperand()
Value *OP1 = SI->getOperand(1); // 等价于 SI->getPointerOperand()

if(LoadInst *LI = dyn_cast<LoadInst>(OP0)){
// 即此时这个 LI 所读取的数据就会在当前 StoreInst 准备存的值。
errs() << "This StoreInst '";
SI->print(errs());
errs() << "' is affected by this LoadInst '";
LI->print(errs());
errs() << "'!\n";
}
if(LoadInst *LI = dyn_cast<LoadInst>(OP1)){
// 即此时这个 LI 所读取的数据就会在当前 StoreInst 准备存往的地址。
errs() << "This StoreInst '";
SI->print(errs());
errs() << "' is affected by this LoadInst '";
LI->print(errs());
errs() << "'!\n";
}

GEPI

对这个指令的理解终于是透彻了。

1
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*

一句话解释从操作数指针位置通过一系列偏移计算得到一个指向其中某个域的指针。
其中就第一个有效地址是想操作的地址,通常是结构体或数组的指针,他是指向一个结构体的。(但是我不好说是指向结构体类型还是实例,应该是一个实例。
第二个参数通常是 0,表示从起始地址计算多少的偏移,这个起始就是按照数组的理解,一个数组 int a[10],其实 &a[0]a 本身是等价的,同时 a[1] 增加的偏移是 sizeof(TYPE) 。所以当第一个偏移参数是 i 时,它其实就是从传递的指针开始,计算数组偏移,i 个单位,一个单位是这个类型的结构所占据的空间。但是没有关系,下一个位置开始还是被理解成这样的结构体数据结构,只是实例数据位置变了,但还是这个结构体或者这个数组。后面的参数依次表示其中的第几项,是递归进去,层层的第几项。

所以要是分析结构体中某些域是否被访问,可以直接忽略第一个偏移的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
; %5 %8 %15 %23 %32 都是函数参数进来的
; int ga[10];
; struct structTy
; {
; int a;
; long long b;
; char c[10];
; int* ptr;
; }gs[10];

; int x = ga[a];
%6 = getelementptr inbounds [10 x i32], [10 x i32]* @ga, i64 0, i64 %5
%7 = load i32, i32* %6, align 4
; 可以看到 对于数组类型 getelementptr 后,就是我们要获取的值的地址,即数组名+偏移是一个地址

; int x = gs[a].a;
%9 = getelementptr inbounds [10 x %struct.structTy], [10 x %struct.structTy]* @gs, i64 0, i64 %8
; 此时也是一个数组 getelementptr 后是一个地址, 存的值是一个 结构体类型的地址
%10 = getelementptr inbounds %struct.structTy, %struct.structTy* %9, i32 0, i32 0
; 然后从这个结构体中获取时就是一个获取了结构体类型对应的偏移的东西的地址
%11 = load i32, i32* %10, align 8
; 然后就是读取了这个地址的值

; int y = gs[a].b;
%16 = getelementptr inbounds [10 x %struct.structTy], [10 x %struct.structTy]* @gs, i64 0, i64 %15
%17 = getelementptr inbounds %struct.structTy, %struct.structTy* %16, i32 0, i32 1
%18 = load i64, i64* %17, align 8
%19 = trunc i64 %18 to i32

; int z = gs[a].c[4];
%24 = getelementptr inbounds [10 x %struct.structTy], [10 x %struct.structTy]* @gs, i64 0, i64 %23
; 从数组中获取的是一个结构体变量的地址
%25 = getelementptr inbounds %struct.structTy, %struct.structTy* %24, i32 0, i32 2
; 根据结构体内部的偏移 获取到具体这个值的地址
%26 = getelementptr inbounds [10 x i8], [10 x i8]* %25, i64 0, i64 4
; 然后又是一个数组 再通过偏移拿到一个这个值的地址
%27 = load i8, i8* %26, align 4
; 然后从地址读取了这个值

; int* p = gs[a].ptr;
%33 = getelementptr inbounds [10 x %struct.structTy], [10 x %struct.structTy]* @gs, i64 0, i64 %32
; 从结构体数组中通过偏移拿到结构体变量的地址
%34 = getelementptr inbounds %struct.structTy, %struct.structTy* %33, i32 0, i32 3
; 结合结构体内部的偏移,从地址中取了个内部值的地址.
%35 = load i32*, i32** %34, align 8
; 然后从地址中读取了值

在 Pass 中检查 IR

我们可以通过 << 使用 C++ 的 ostream 将所有我们认为重要的 IR 对象打出来看看。

因为前面的 pass 是处理 Function 的,所以我们可以对 FunctionBasicBlock 进行迭代,然后遍历每个 BasicBlockInstruction

就是对上面的函数进行一个简单的修改就能进行一个遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
virtual bool runOnFunction(Function &F) {
errs() << "In a function called " << F.getName() << "!\n";

errs() << "Function body:\n";
//F.dump(); Out-of-Date: no more dump support in modern llvm unless you enable it at compile time.
F.print(llvm::errs());

for (auto &B : F) {
errs() << "Basic block:\n";
B.print(llvm::errs());

for (auto &I : B) {
errs() << "Instruction: \n";
I.print(llvm::errs(), true);
errs() << "\n";
}
}

然后对下面的 test.c 运行试试看:

1
2
3
4
5
6
7
8
9
10
11
// text.c
#include<stdio.h>

void f(){
printf("hello world!\n");
}

int main(){
f();
return 0;
}

然后运行的输出为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.* test.c 
In a function called f!
Function body:
; Function Attrs: noinline nounwind optnone uwtable
define dso_local void @f() #0 {
%1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0))
ret void
}
Basic block:

%1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0))
ret void
Instruction:
%1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0))
Instruction:
ret void
In a function called main!
Function body:
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
call void @f()
ret i32 0
}
Basic block:

%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
call void @f()
ret i32 0
Instruction:
%1 = alloca i32, align 4
Instruction:
store i32 0, i32* %1, align 4
Instruction:
call void @f()
Instruction:
ret i32 0

再来对 if 进行一个测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// iftest.c
#include <stdio.h>

int main(int argc, char const *argv[])
{
/* code */
if(argc == 0){
printf("argc = %d\n", argc);
}
else if(argc == 1){
printf("%s", argv[0]);
}
else{
printf("illegal!\n");
}
return 0;
}

这个输出就太多了,这就有更多的基本块了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
In a function called main!
Function body:
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main(i32 %0, i8** %1) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = load i32, i32* %4, align 4
%7 = icmp eq i32 %6, 0
br i1 %7, label %8, label %11

8: ; preds = %2
%9 = load i32, i32* %4, align 4
%10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str, i64 0, i64 0), i32 %9)
br label %22

11: ; preds = %2
%12 = load i32, i32* %4, align 4
%13 = icmp eq i32 %12, 1
br i1 %13, label %14, label %19

14: ; preds = %11
%15 = load i8**, i8*** %5, align 8
%16 = getelementptr inbounds i8*, i8** %15, i64 0
%17 = load i8*, i8** %16, align 8
%18 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* %17)
br label %21

19: ; preds = %11
%20 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.2, i64 0, i64 0))
br label %21

21: ; preds = %19, %14
br label %22

22: ; preds = %21, %8
ret i32 0
}
Basic block:

%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = load i32, i32* %4, align 4
%7 = icmp eq i32 %6, 0
br i1 %7, label %8, label %11
Instruction:
%3 = alloca i32, align 4
Instruction:
%4 = alloca i32, align 4
Instruction:
%5 = alloca i8**, align 8
Instruction:
store i32 0, i32* %3, align 4
Instruction:
store i32 %0, i32* %4, align 4
Instruction:
store i8** %1, i8*** %5, align 8
Instruction:
%6 = load i32, i32* %4, align 4
Instruction:
%7 = icmp eq i32 %6, 0
Instruction:
br i1 %7, label %8, label %11
Basic block:

8: ; preds = %2
%9 = load i32, i32* %4, align 4
%10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str, i64 0, i64 0), i32 %9)
br label %22
Instruction:
%9 = load i32, i32* %4, align 4
Instruction:
%10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str, i64 0, i64 0), i32 %9)
Instruction:
br label %22
Basic block:

11: ; preds = %2
%12 = load i32, i32* %4, align 4
%13 = icmp eq i32 %12, 1
br i1 %13, label %14, label %19
Instruction:
%12 = load i32, i32* %4, align 4
Instruction:
%13 = icmp eq i32 %12, 1
Instruction:
br i1 %13, label %14, label %19
Basic block:

14: ; preds = %11
%15 = load i8**, i8*** %5, align 8
%16 = getelementptr inbounds i8*, i8** %15, i64 0
%17 = load i8*, i8** %16, align 8
%18 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* %17)
br label %21
Instruction:
%15 = load i8**, i8*** %5, align 8
Instruction:
%16 = getelementptr inbounds i8*, i8** %15, i64 0
Instruction:
%17 = load i8*, i8** %16, align 8
Instruction:
%18 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* %17)
Instruction:
br label %21
Basic block:

19: ; preds = %11
%20 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.2, i64 0, i64 0))
br label %21
Instruction:
%20 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.2, i64 0, i64 0))
Instruction:
br label %21
Basic block:

21: ; preds = %19, %14
br label %22
Instruction:
br label %22
Basic block:

22: ; preds = %21, %8
ret i32 0
Instruction:
ret i32 0

在 pass 做一些有趣的事情

下面这个例子会将所有的二进制操作中的第一个 operator (+,-, etc.) 替换为 乘。

完整代码在 这里 核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for (auto& B : F) {
for (auto& I : B) {
if (auto* op = dyn_cast<BinaryOperator>(&I)) { // 它检查操作数是否属于指定类型,如果是,则返回指向它的指针(此运算符不适用于引用),否则返回 NULL。具体这里就是检查 I 是不是 BinaryOperator 类型的,如果不是就会 NULL,如果是就返回这个指令对应的指针。
// Insert at the point where the instruction `op` appears.
IRBuilder<> builder(op); // IRBuilder 用于构建代码,我们要的任何类型的指令都能构造
// 我猜此处的意思应该是 builder 是 IRBuilder 类的实例,然后是在 op 这个地址上创建一个指令

// Make a multiply with the same operands as `op`.
Value* lhs = op->getOperand(0); // 获取原来指令的两个操作数
Value* rhs = op->getOperand(1);
Value* mul = builder.CreateMul(lhs, rhs); // 使用 IRBuilder 来创建一个乘法

// Everywhere the old instruction was used as an operand, use our
// new multiply instruction instead.
// 任何旧指令被用来作为 operand 的都用我们的新指令进行替换
// 这里还不理解
for (auto& U : op->uses()) {
User* user = U.getUser(); // A User is anything with operands.
user->setOperand(U.getOperandNo(), mul);
}

// We modified the code.
return true;
}
}
}

然后对下面的 a_add_b.c 进行测试:

1
2
3
4
5
6
7
#include <stdio.h>
int main(int argc, const char** argv) {
int num;
scanf("%i", &num);
printf("%i\n", num + 2);
return 0;
}

然后插桩编译和不插桩编译会有两种情况的结果:

1
2
3
4
5
6
7
8
9
$ clang a_add_b.c
$ ./a.out
4
6

$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.* a_add_b.c
$ ./a.out
4
8

dyn_cast 和 isa

dyn_cast<CallInst>(&I)isa<CallInst>(&I) 有什么区别?

dyn_cast<CallInst>(&I)isa<CallInst>(&I) 都是 LLVM 中用于检查对象类型的函数,但它们的行为略有不同。

isa<CallInst>(&I) 函数是一个模板函数,用于检查指定的对象是否是特定类的实例。如果指定的对象是该类的实例,则该函数返回 true,否则返回 false。例如,isa<CallInst>(&I) 用于检查指令 I 是否是 CallInst 类的实例。

dyn_cast<CallInst>(&I) 函数也是一个模板函数,用于将指定的对象转换为特定类的实例。如果指定的对象可以转换为该类的实例,则该函数返回该实例的指针,否则返回空指针。例如,dyn_cast<CallInst>(&I) 用于将指令 I 转换为 CallInst 类的实例,如果指令 I 不是 CallInst 类的实例,则返回空指针。

因此,isa<CallInst>(&I) 函数只是用于检查对象类型,而 dyn_cast<CallInst>(&I) 函数则可以将对象转换为特定类型的实例。在使用这些函数时,应当根据需要选择使用哪个函数。如果只需要检查对象是否是某个特定类的实例,则使用 isa<>() 函数;如果需要将对象转换为某个特定类的实例,则使用 dyn_cast<>() 函数。

CallGraphSCCPass

CallGraphSCCPassLLVM 中的一个 Pass,用于对函数调用图(`Call Graph)进行处理。在 CallGraphSCCPass 中,可以使用 CallGraphSCC 类来表示函数调用图中的一个强连通分量(Strongly Connected Component),其中包含了多个函数。

您可以使用 CallGraphSCCPassrunOnSCC() 函数来遍历每个强连通分量,并对其中的每个函数进行处理。在 runOnSCC() 函数中,可以使用 auto 关键字来遍历 CallGraphSCC 中的每个函数。以下是一个示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "llvm/Pass.h"
#include "llvm/Analysis/CallGraphSCCPass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
struct MyCallGraphSCCPass : public CallGraphSCCPass {
static char ID;
MyCallGraphSCCPass() : CallGraphSCCPass(ID) {}

bool runOnSCC(CallGraphSCC &SCC) override {
// Traverse functions in SCC
for (auto &I : SCC) {
Function *F = I->getFunction();
if (F) {
errs() << "Processing function: " << F->getName() << "\n";
// Process the function here
}
}

return false;
}
};
}

char MyCallGraphSCCPass::ID = 0;
static RegisterPass<MyCallGraphSCCPass> X("mycallgraphsccpass", "My CallGraphSCC Pass", false, false);

Linking With a Runtime Library

当您需要检测代码来做一些重要的事情时,使用 IRBuilder 生成 LLVM 指令来完成它可能会很痛苦。那就想或许可以写一个 C 的运行时库,在运行时给链接上去,然后记录日志啥的。下面就教你如何编写一个运行时库 log 每个指令操作的结果。

通过这样的方式,我们不用每次都去改 Pass 然后重新编译程序进行插桩/替换啥的,而是直接告诉他我要固定调用某个运行时库中的函数,这样我们可以每次只修改外部的函数的具体实现,而不用再重新编译原来的程序了。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/InstrTypes.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
#include "llvm/IR/Module.h"
using namespace llvm;

namespace {
struct SkeletonPass : public FunctionPass {
static char ID;
SkeletonPass() : FunctionPass(ID) {}

virtual bool runOnFunction(Function &F) {
// Get the function to call from our runtime library.
LLVMContext &Ctx = F.getContext();
std::vector<Type*> paramTypes = {Type::getInt32Ty(Ctx)};
Type *retType = Type::getVoidTy(Ctx);
FunctionType *logFuncType = FunctionType::get(retType, paramTypes, false);
// Module::getOrInsertFunction 为你的运行时函数 logop 添加了一个声明,类似于 C 源代码中有函数体的声明 “void logop(int i); ”
// 该指令代码与定义该 logop 函数的运行时库(存储库中的 rtlib.c)配对
// rtlib.c
// #include <stdio.h>
// void logop(int i) {
// printf("computed: %i\n", i);
// }


FunctionCallee logFunc =
F.getParent()->getOrInsertFunction("logop", logFuncType);

for (auto &B : F) {
for (auto &I : B) {
if (auto *op = dyn_cast<BinaryOperator>(&I)) {
// Insert *after* `op`.
IRBuilder<> builder(op);
builder.SetInsertPoint(&B, ++builder.GetInsertPoint());

// Insert a call to our function.
Value* args[] = {op};
builder.CreateCall(logFunc, args);

return true;
}
}
}

return false;
}
};
}

char SkeletonPass::ID = 0;

// Automatically enable the pass.
// http://adriansampson.net/blog/clangpass.html
static void registerSkeletonPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new SkeletonPass());
}
static RegisterStandardPasses
RegisterMyPass(PassManagerBuilder::EP_EarlyAsPossible,
registerSkeletonPass);

外部的动态链接库 rtlib.c 代码为:

1
2
3
4
5
// rtlib.c
#include <stdio.h>
void logop(int i) {
printf("computed: %i\n", i);
}

然后编译该库:

1
2
3
4
5
6
7
$ cc -c rtlib.c
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so -c a_add_b.c
$ cc a_add_b.o rtlib.o
$ ./a.out
15
computed: 17
17

这时候我们简单修改一下 rtlib.c 试试:

1
2
3
4
5
6
// rtlib.c
#include <stdio.h>
void logop(int i) {
printf("computed: %i\n", i);
printf("new rtlib.c\n");
}

再重新编译、链接一下:

1
2
3
4
5
6
7
$ cc -c rtlib.c
$ cc a_add_b.o rtlib.o
$ ./a.out
3
computed: 5
new rtlib.c
5

Annotations 注释信息

大多数项目最终都需要与程序员进行交互。 您最终会希望某种将额外信息从程序传递到您的 LLVM pass 的方法。 有几种方法可以建立注释系统:

  • 实用的方法是使用魔法函数。 在头文件中使用特殊的、可能唯一的名称声明一些空函数。 将该文件包含在您的源代码中并调用那些不执行任何操作的函数。 然后,在 pass 中,查找调用您的函数的 CallInst 指令并使用它们来触发您的魔法函数。 例如,您可以使用 __enable_instrumentation() 和 __disable_instrumentation() 之类的调用来让程序将您的代码修改限制在特定区域。

  • 如果您需要让程序员将标记添加到函数或变量声明中,Clang 的 __attribute__((annotate(“foo”))) 语法将发出带有您可以在传递中处理的任意字符串的 metadata 。 Brandon Holt again has some background on this technique。 如果您需要标记表达式而不是声明,则未没有文档的且是有限的 __builtin_annotation(e, “foo”) 内在函数可能会起作用。

插桩条件分支语句

在学习了上述的部分基础之后再看之前看雪中的 编译时插桩 代码好像就容易懂一些其位置了。但是此时我才明白这个中提到的插桩是插入 if-else 逻辑的代码到源代码中,而不是对源代码中的 if-else 逻辑进行插桩。

于是简单看了看官方文档中关于 Instruction 的函数都有啥,于是就发现了以下几个有趣的函数:

1
2
3
4
llvm::Value::getValueID()
getOpcode() unsigned llvm::Instruction::getOpcode ( ) const
getOpcodeName() const char * Instruction::getOpcodeName ( unsigned OpCode )
getOpcodeName() const char* llvm::Instruction::getOpcodeName ( ) const

于是我们简单写一个下面的 pass 进行测试,把上面的函数都用一次看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
virtual bool runOnFunction(Function &F) {
errs() << "In a function called " << F.getName() << "!\n";

for (auto &B : F) {

for (auto &I : B) {
errs() << "Instruction: \n";
I.print(llvm::errs(), true);

errs() << ", ";
errs() << "getValueID() = ";
errs() << I.getValueID();

errs() << ", ";
errs() << "getOpcode() = ";
errs() << I.getOpcode();

errs() << ", ";
errs() << "getOpcodeName() = ";
errs() << I.getOpcodeName();

errs() << ", ";
errs() << "getOpcodeName(unsigned OpCode) = ";
errs() << I.getOpcodeName(I.getOpcode());

errs() << "\n";
}
}

return false;
}

然后我们简单对 iftest.c 进行一个测试, 得到的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.* iftest.c
In a function called main!
Instruction:
%3 = alloca i32, align 4, getValueID() = 55, getOpcode() = 31, getOpcodeName() = alloca, getOpcodeName(unsigned OpCode) = alloca
Instruction:
%4 = alloca i32, align 4, getValueID() = 55, getOpcode() = 31, getOpcodeName() = alloca, getOpcodeName(unsigned OpCode) = alloca
Instruction:
%5 = alloca i8**, align 8, getValueID() = 55, getOpcode() = 31, getOpcodeName() = alloca, getOpcodeName(unsigned OpCode) = alloca
Instruction:
store i32 0, i32* %3, align 4, getValueID() = 57, getOpcode() = 33, getOpcodeName() = store, getOpcodeName(unsigned OpCode) = store
Instruction:
store i32 %0, i32* %4, align 4, getValueID() = 57, getOpcode() = 33, getOpcodeName() = store, getOpcodeName(unsigned OpCode) = store
Instruction:
store i8** %1, i8*** %5, align 8, getValueID() = 57, getOpcode() = 33, getOpcodeName() = store, getOpcodeName(unsigned OpCode) = store
Instruction:
%6 = load i32, i32* %4, align 4, getValueID() = 56, getOpcode() = 32, getOpcodeName() = load, getOpcodeName(unsigned OpCode) = load
Instruction:
%7 = icmp eq i32 %6, 0, getValueID() = 77, getOpcode() = 53, getOpcodeName() = icmp, getOpcodeName(unsigned OpCode) = icmp
Instruction:
br i1 %7, label %8, label %11, getValueID() = 26, getOpcode() = 2, getOpcodeName() = br, getOpcodeName(unsigned OpCode) = br
Instruction:
%9 = load i32, i32* %4, align 4, getValueID() = 56, getOpcode() = 32, getOpcodeName() = load, getOpcodeName(unsigned OpCode) = load
Instruction:
%10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str, i64 0, i64 0), i32 %9), getValueID() = 80, getOpcode() = 56, getOpcodeName() = call, getOpcodeName(unsigned OpCode) = call
Instruction:
br label %22, getValueID() = 26, getOpcode() = 2, getOpcodeName() = br, getOpcodeName(unsigned OpCode) = br
Instruction:
%12 = load i32, i32* %4, align 4, getValueID() = 56, getOpcode() = 32, getOpcodeName() = load, getOpcodeName(unsigned OpCode) = load
Instruction:
%13 = icmp eq i32 %12, 1, getValueID() = 77, getOpcode() = 53, getOpcodeName() = icmp, getOpcodeName(unsigned OpCode) = icmp
Instruction:
br i1 %13, label %14, label %19, getValueID() = 26, getOpcode() = 2, getOpcodeName() = br, getOpcodeName(unsigned OpCode) = br
Instruction:
%15 = load i8**, i8*** %5, align 8, getValueID() = 56, getOpcode() = 32, getOpcodeName() = load, getOpcodeName(unsigned OpCode) = load
Instruction:
%16 = getelementptr inbounds i8*, i8** %15, i64 0, getValueID() = 58, getOpcode() = 34, getOpcodeName() = getelementptr, getOpcodeName(unsigned OpCode) = getelementptr
Instruction:
%17 = load i8*, i8** %16, align 8, getValueID() = 56, getOpcode() = 32, getOpcodeName() = load, getOpcodeName(unsigned OpCode) = load
Instruction:
%18 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* %17), getValueID() = 80, getOpcode() = 56, getOpcodeName() = call, getOpcodeName(unsigned OpCode) = call
Instruction:
br label %21, getValueID() = 26, getOpcode() = 2, getOpcodeName() = br, getOpcodeName(unsigned OpCode) = br
Instruction:
%20 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.2, i64 0, i64 0)), getValueID() = 80, getOpcode() = 56, getOpcodeName() = call, getOpcodeName(unsigned OpCode) = call
Instruction:
br label %21, getValueID() = 26, getOpcode() = 2, getOpcodeName() = br, getOpcodeName(unsigned OpCode) = br
Instruction:
br label %22, getValueID() = 26, getOpcode() = 2, getOpcodeName() = br, getOpcodeName(unsigned OpCode) = br
Instruction:
ret i32 0, getValueID() = 25, getOpcode() = 1, getOpcodeName() = ret, getOpcodeName(unsigned OpCode) = ret

可以看到其中确实出现了我们想要的,如果我们对指令名字进行一个判断,就可以精确确定这些存在条件分支的地方的指令了(IR 中条件比较都是用的 icmpfcmp)(对于 switchphi 指令应该另外考虑),然后再进一步去监控相关的变量应该就可以了?

现在我们简单修改,只打印和 icmp 相关的指令部分,修改 pass 如下:

1
2
if (I.getOpcode() != 53)
continue;

得到的运行输出为:

1
2
3
4
5
6
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.* iftest.c
In a function called main!
Instruction:
%7 = icmp eq i32 %6, 0, getValueID() = 77, getOpcode() = 53, getOpcodeName() = icmp, getOpcodeName(unsigned OpCode) = icmp
Instruction:
%13 = icmp eq i32 %12, 1, getValueID() = 77, getOpcode() = 53, getOpcodeName() = icmp, getOpcodeName(unsigned OpCode) = icmp

接下来应该考虑怎么去获取

  1. 某个指令的操作数 —> 直接查阅官方文档即可
  2. 如果是寄存器应该怎么把值拖出来—>这个我还不会

0x04 参考链接