LLVM 不止能编译!自定义 Pass + 定制 clang 实现函数名加密

版权归作者所有,如有转发,请注明文章出处: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 打开

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值