type
status
date
slug
summary
tags
category
icon
password
如果代码高亮看着难受,看这个,我用obsidian写的
LLVM IR 介绍
官方文档对此部分有超级详细的介绍,这篇文章只限于简单入门
首先 LLVM IR 是一门低级编程语言,语法类似于汇编,所以任何高级编程语言(如 C++等等)都可以用 LLVM IR 表示,好处是基于 LLVM IR 可以很方便地进行代码优化
LLVM IR 的两种表示方法
- 第一种是人类可以阅读的文本形式,文件后缀为 . ll
- 第二种是易于机器处理的二进制格式,文件后缀为 . bc
它们两个可以通过编译完的 llvm 目录的命令行工具, dis, as 相互转换
LLVM IR 结构
大致分为三块
- 模块 Module
- 函数 Function
- 基本块 BasicBlock
模块 Module
- 一个源代码文件对应 LLVM IR 中的一个模块。
- 头部信息包含程序的目标平台,如 X86、ARM 等等,和一些其他信息。
- 全局符号包含全局变量、函数的定义与声明都在这里。
函数 Function
- LLVM IR 中的函数表示源代码中的某个函数。
- 参数,顾名思义为函数的参数。
- 一个函数由若干基本块组成,其中函数最先执行的基本块为入口块。
基本块 BasicBlock
- 一个基本块由若干个指令和标签组成。
- 正常情况下,基本块的最后一条指令为跳转指令 (br 或 switch),或返回指令 (retn),也叫作终结指令 (Terminator Instruction)。
- PHI 指令是一种特殊的指令。(下面会详细说明)
类比 IDA 流程图
IDA 每个函数里面执行流程是以一块一块的代码组成的
LLVM IR 混淆基础
- 以函数为基本单位的混淆:控制流平坦化。
- 以基本块基本单位的混淆:虚假控制流。
- 以指令为基本单位的混淆:指令替代。
LLVM IR 指令
指令类型
- 终结指令 Terminator Instructions
- 比较指令 Compare Operations
- 二元运算 Binary Operations
- 按位二元运算 Bitwise Binary Operations
- 内存访问和寻址操作 Memory Access and Addressing Operations
- 类型转换操作 Conversion Operations
- 其他操作指令 Other Operations
终结指令
ret 指令
函数的返回指令,对应 C/C++ 中的 return
指令源码
示例
返回一个结构体了解即可,其他重点掌握
br 指令
br 是”分支”的英文 branch 的缩写,分为非条件分支和条件分支,对应 C/C++的 if 语句。
无条件分支类似于 x86 汇编中的 jmp 指令,条件分支类似于 x86 汇编中的 jnz, je 等等条件跳转指令。
指令源码
示例
switch 指令
分支指令,可看做是 br 指令的升级版,支持的分支更多。对应 C/C++ 中的 switch
指令源码
示例
比较指令
前置知识
在 x86 汇编中,条件跳转指令 (jnz, je 等) 通常与比较指令 cmp, test 等一起出
现。(cmp 和 test 的差别就是 cmp 做减法,test 做与运算)
在 LLVM IR 中也有这样的指令,他们通常与条件分支指令 br 一起出现。也就是下面的这两个指令 icmp 和 fcmp
icmp 指令
整数或指针的比较指令
条件 cond 可以是
- eq (相等)
- ne (不相等)
- ugt (无符号大于)
- ult (无符号小于)
- ule (无符号小于等于)
- sgt (符号大于)
- sge (符号大于等于)
- 等等
指令源码
示例
fcmp 指令
浮点数的比较指令
条件 cond 可以是
- oeq(ordered and equal)相等
- ueq(unordered orequal)不等
- false(必定不成立)
- 等等
ordered 的意思是,两个操作数都不能为 NAN
指令源码
示例
二元运算指令
add 指令
整数加法指令,对应 C/C++ 中的“+”操作符,类似 x86 汇编中的 add 指令
指令源码
示例
sub 指令
整数减法指令,对应 C/C++ 中的“-”操作符,类似x86汇编中的 sub 指令
指令源码
示例
mul 指令
整数乘法指令,对应 C/C++ 中的“*”操作符,类似 x86 汇编中的 mul 指令
指令源码
示例
udiv 指令
无符号整数除法指令,对应 C/C++ 中的“/”操作符。如果存在 exact 关键字,且 op1不是op2的倍数,就会出现错误
指令源码
示例
sdiv 指令
有符号整数除法指令,对应 C/C++ 中的“/”操作符
指令源码
示例
urem 指令
无符号整数取余指令,对应 C/C++ 中的“%”操作符
指令源码
示例
srem 指令
有符号整数取余指令,对应 C/C++ 中的“%”操作符
指令源码
示例
按位二元运算指令
shl 指令
整数左移指令,对应 C/C++ 中的“<<”操作符,类似x86汇编中的 shl 指令
指令源码
示例
lshr 指令
整数逻辑右移指令,对应 C/C++ 中的“>>”操作符,右移指定位数后在左侧补0
指令源码
示例
ashr 指令
整数算数右移指令,右移指定位数后在左侧补符号位(负数的符号位为 1,正数的符号 位为 0)
指令源码
示例
and 指令
整数按位与运算指令,对应 C/C++ 中的“&”操作符
指令源码
示例
or 指令
整数按位或运算指令,对应 C/C++ 中的“|”操作符
指令源码
示例
xor 指令
整数按位异或运算指令,对应 C/C++ 中的“^”操作符
指令源码
示例
内存访问和寻址操作指令
前置知识
在编译器设计中,静态单赋值(Static Single Assignment, SSA),是 IR 的一种属性,简单来说,SSA 的特点是:在程序中一个变量仅能有一条赋值语句
例如
要改成 ssa 特点其实很简单,只需要单独下一个下标,但这样的解决方案存在一个问题
再看一个
假设 C++ 也是基于静态单赋值原则的(即一个变量只能被赋值一次),要怎样修改
这个 for 循环,使其符合 SSA 原则?
这个代码,每一次循环 i 都会被赋值一次,那我们如何修改呢,用上面的方法好像不太适用
这时候,就需要对内存进行操作了,办法就是用指针直接操作 i 变量的内存
alloca 指令
内存分配指令,在栈中分配一块空间并获得指向该空间的指针,类似于 C/C++ 中的 malloc 函数
指令源码
示例
store 指令
内存存储指令,向指针指向的内存中存储数据,类似于 C/C++ 中的指针解引用后的 赋值操作
指令源码
示例
load 指令
内存读取指令,从指针指向的内存中读取数据,类似于 C/C++ 中的指针解引用操 作
指令源码
示例
类型转换操作指令
trunc .. to 指令
截断指令,将一种类型的变量截断为另一种类型的变量。对应 C/C++ 中大类型向 小类型的强制转换(比如 long 强转 int)
指令源码
示例
zext .. to 指令
零拓展(Zero Extend)指令,将一种类型的变量拓展为另一种类型的变量,高位补 0。对应 C/C++ 中小类型向大类型的强制转换(比如 int 强转 long)
指令源码
示例
sext .. to 指令
符号位拓展(Sign Extend)指令,通过复制符号位(最高位)将一种类型的变量拓展为 另一种类型的变量
指令源码
示例
其他操作指令
phi 指令
- phi 指令可以看做是为了解决 SSA 一个变量只能被赋值一次而引起的问题衍生出的
指令
- phi 指令的计算结果由 phi 指令所在的基本块的前驱块确定
指令源码
示例
select 指令
select 指令类似于 C/C++ 中的三元运算符”... ? ... : ...”
指令源码
示例
call 指令
call 指令用来调用某个函数,对应 C/C++ 中的函数调用,与 x86 汇编中的 call 指 令类似
指令源码
示例
LLVM IR 指令实战
简单的读一下这个代码
总结
LLVM IR 这一部分,官方文档用了大片章节对此进行赘述,这是 llvm 的灵魂,也是最值得花费时间的地方,花了两天,终于打字打完了