版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
前言
LLVM 是一个编译器框架 ,用来把 C/C++ 源码编译成目标文件,似乎和日常的 GCC、Clang 没什么两样。事实上,LLVM 的能力远不止于此。
相关文章:LLVM 全面解析:NDK 为什么离不开它?如何亲手编译调试 clang
凭借它灵活的 中间表示(LLVM IR) 和强大的 Pass 插件机制 ,我们不仅能优化代码,还能在编译阶段对程序进行“改造”——比如 函数名加密、混淆、代码保护 ,让逆向工程变得更加困难。
本文带你从零开始,体验一次完整的 LLVM 定制之旅:
-
学习如何生成和操作 LLVM IR
-
使用 opt 和 CLion 调试 LLVM Pass
-
编写自定义 Pass,实现 函数名加密保护
-
最终定制 clang,让它自动加载我们的加密插件
1. LLVM IR 介绍
LLVM IR(Intermediate Representation,中间表示)是 LLVM 编译框架的核心。它是一种 介于高级语言(如 C/C++)与底层机器码之间的中间语言 ,既能表达高级语言的语义,又足够接近机器指令,便于优化和生成目标代码。
LLVM IR 的两种存储形式:
-
文本格式(.ll):人类可读,方便分析和调试。
-
二进制格式(.bc):Bitcode 文件,适合存储和传输。
LLVM IR 与具体 CPU 架构解耦,可以跨平台复用优化逻辑。
一个简单示例 (C 代码 → LLVM IR):
int add(int a, int b) {
return a + b;
}
对应的 LLVM IR(.ll 文本):
define i32 @add(i32 %a, i32 %b) {
entry:
%0 = add i32 %a, %b
ret i32 %0
}
LLVM IR 语言参考手册:https://llvm.org/docs/LangRef.html
在这个例子中:
-
define i32 @add 定义了一个返回 i32 的函数 add;
-
%a 和 %b 是函数参数;
-
add i32 %a, %b 表示整数相加;
-
ret i32 %0 返回结果。
通过 LLVM IR,编译器就能在架构无关的层面做优化,然后再交由 LLVM 后端翻译成目标平台的机器码。
1.1 将 C 文件转换为 LLVM IR
生成文本形式的 LLVM IR(.ll 文件)
clang -S -emit-llvm hello.c -o hello.ll
以 hello.ll 为例,生成的内容大概如下
; Function Attrs: noinline nounwind optnone uwtable
; 这行注释了函数的属性
; - noinline: 该函数不会被内联(inline)。
; - nounwind: 该函数不会引发异常(不会 unwind stack)。
; - optnone: 编译器不会对此函数进行任何优化。
; - uwtable: 该函数有一个可用的异常处理表(unwind table)。
define dso_local i32 @main() #0 {
; define: 定义一个函数。
; dso_local: 该函数在本地动态库中可见(仅限于当前编译单元)。
; i32: 返回类型为 32 位整数。
; @main: 函数名为 'main'。
; #0: 函数使用属性组 0 中定义的属性(即上面注释的那一行)。
entry:
; entry: 函数的入口基本块(Basic Block)的标签。
%retval = alloca i32, align 4
; alloca: 在栈上分配内存。这里分配了一个 32 位整数(i32)。
; align 4: 分配的内存对齐到 4 字节边界。
; %retval: 变量的名称(SSA 变量),用于存储 main 函数的返回值。
store i32 0, ptr %retval, align 4
; store: 将一个值存储到内存地址中。
; i32 0: 要存储的值是 32 位整数 0(即 main 函数的返回值)。
; ptr %retval: 存储的位置是之前分配的栈变量 %retval。
; align 4: 存储操作按 4 字节对齐。
%call = call i32 (ptr, ...) @printf(ptr noundef @"??_C@_0P@MHJMLPNF@Hello?0?5World?$CB?6?$AA@")
; call: 调用一个函数,这里调用了 C 标准库的 printf 函数。
; i32 (ptr, ...): printf 函数的原型。它接受一个指针(通常是 C 字符串格式化字符串)和可变参数列表(...)。
; ptr noundef @"??_C@_0P@MHJMLPNF@Hello?0?5World?$CB?6?$AA@": 这是一个字符串常量的地址(指针)。
; noundef: 表示传递给函数的参数不会是未定义的(undefined)。
; %call: 保存 printf 的返回值(返回打印的字符数)。
ret i32 0
; ret: 返回指令,用于结束函数的执行。
; i32 0: main 函数返回 0(表示正常退出)。
}
1.2 生成二进制 LLVM IR(.bc 文件)
生成二进制 LLVM IR(.bc 文件)
clang -emit-llvm -c hello.c -o hello.bc
可以使用 llvm-dis 转换回文本形式
llvm-dis hello.bc -o hello.ll
1.3 使用 clang 编译 IR 文件
运行以下命令将 .ll 文件编译为可执行程序
clang hello.ll -o hello.exe
2. opt 介绍
opt 是 LLVM 提供的一个命令行工具,主要用于 对 LLVM IR 进行分析和优化 。它不会直接生成目标机器码,而是专注于 IR 层面的处理 ,通常在编译流程中作为“优化器”环节出现。
opt 工具通过应用各种优化 Pass,可以实现 **“优化/分析” ** 的目的。
LLVM IR (.ll / .bc)
↓
opt 工具
↓ (加载并执行多个 Pass)
┌───────────────┐
│ Pass A (分析) │
│ Pass B (优化) │
│ Pass C (混淆) │
└───────────────┘
↓
优化/变换后的 LLVM IR
显示所有可用的 Pass
opt --help
优化 LLVM IR 文件
opt -O3 hello.ll -o hello_opt.bc
参数说明:
-
-O3:表示应用最高级别的优化(与 clang 的 -O3 类似)。
-
-o hello_opt.bc:输出优化后的文件
生成可读的 LLVM IR 文件
opt -O3 hello.bc -S -o hello_opt.ll
参数说明:
- -S:表示输出可读的文本格式(.ll 文件)。
应用特定的 Pass,并查看优化效果。例如
opt -passes=mem2reg hello.ll -S -o hello_mem2reg.ll
参数说明:
-
-passes:应用特定的优化 pass
-
mem2reg:把内存变量提升为寄存器变量(SSA 形式)。
-
-S:输出可读的 .ll 文件。
函数内联优化,将函数调用替换为函数体,从而减少函数调用的开销。
opt -passes=inline hello.ll -S -o hello_inline.ll
可以同时应用多个 Pass(前一个 Pass 的输出就是后一个 Pass 的输入)
opt -passes="mem2reg,inline,constprop" input.ll -o output.ll
生成函数的控制流图(Control Flow Graph),输出为 .dot 文件,可以用 Graphviz 进行可视化。
opt -passes=dot-cfg hello.ll
3. 使用 Clion 调试 opt
使用 CLion 打开

最低0.47元/天 解锁文章
1072

被折叠的 条评论
为什么被折叠?



