两位老师在介绍Tai-e的时候,都会提到Tai-e的IR设计,会比Soot的更直观,这里具体看看Tai-e的IR设计及实现。
还是先给个老师演示Tai-e的IR例子。
0x01 Tai-e IR
1.1 Soot VS Tai-e
1.2 重点结构
- JClass(在pascal.taie.language.classes)表示程序中的类。每个实例包含类的各种信息,如类名、修饰符、声明的方法和字段等。
- JMethod 和 JField:(在pascal.taie.language.classes):表示程序中的类成员,即方法和字段。每个 JMethod/JField 实例都包含方法/字段的各种信息,如声明类、名称等。
- ClassHierarchy(在pascal.taie.language.classes):管理程序中的所有类。它提供了查询类层次信息的API,如方法分派、子类检查等。
- Type(在pascal.taie.language.type):表示程序中的类型。它有几个子类,如 PrimitiveType、ClassTyp 和 ArrayType,代表不同类型的 Java 类型。
- TypeSystem(在pascal.taie.language.type):提供用于检索特定类型和子类检查的API。
- World(在pascal.taie):管理整个程序的信息。通过使用它的获取器,你可以访问这些信息,例如ClassHierarchy 和TypeSystem。World 本质上是一个单例类,可以通过调用 World.get() 获得实例。
这几个都很重要,是阅读Tai-e代码,了解Tai-e实现的基础。
1.3 Tai-e IR
Tai-e 的IR 基于类型、3地址码、Java方法的语句(statement)和表达式(expression)。
重要的三个核心类:
- IR 是 Tai-e 中间表示的核心数据结构,每个 IR 实例都可以看作是特定方法主体信息(如变量、参数、语句等)的容器。您可以通过 JMethod.getIR()(只要方法不是抽象的)轻松获取方法的 IR 实例。
- Stmt 表示程序中的所有语句。该接口有十几个子类,分别对应不同的语句。Stmt 保存在IR 中,可以通过 IR.getStmts() 获取。
- Exp 表示程序中的所有表达式。该接口有几十个子类,分别对应各种表达式。Exp 与Stmts 关联,可以通过Stmt 的特定API 获取。
详细的Expr和Stmt子类就不贴在这里了,可以直接看参考【1】中的原文
1.4 Demo
看个例子,InvokeDemo.java
1 | import java.lang.reflect.Method; |
指定-a ir-dumper
1 | java -jar tai-e-all.jar -cp ./InvokeDemoPath -pp --main-class InvokeDemo -ap -a ir-dumper |
Tai-e IR 如下:
1 | public class InvokeDemo extends java.lang.Object { |
0x02 IR的实现
虽然Tai-e 拿Soot 做对比,但是实际上Tai-e 也是在Soot 基础之上,通过Transfromer接口实现Soot IR 到Tai-e IR 转化。
2.1 World
World 是个很重要的Tai-e 实现类,它存着整个分析程序的IR信息,它是一个final class,里面有很多结构,这里一一简单介绍下:
- World: static,通过getWorld() 获取
- options: 配置
- typeSystem: 类型关系
- classHierarchy: 类层次结构
- irBuilder: 生成IR,重点
- nativeModel:
- mainMethod: 指定的main方法
- implicitEntries: 隐式入口,比如Thread.run(这个在CodeQL是没有的)
2.2 buildWorld
问题:World 是如何生成的呢?
先看下调用栈,如下:
1 | build:197, SootWorldBuilder (pascal.taie.frontend.soot) |
2.2.1 SootWorldBuilder#build1
具体逻辑在SootWorldBuilder中:
1 | public void build(Options options, List<AnalysisConfig> analyses) { |
这里首先可以看看runSoot的实现,其实就是调用Soot进行分析
1 | private static void runSoot(String[] args) { |
整个build逻辑可以分为三部分
- 初始化soot参数,initSoot
- 获取待分析的类,并组合成soot.Main 的参数
- 执行soot分析
2.2.2 initSoot
那么具体的逻辑就是Soot的执行了,返回去看看Soot是怎么配置的,具体在initSoot方法中:
1 | private static void initSoot(Options options, List<AnalysisConfig> analyses, |
重点逻辑是Soot 的Transform 接口。
在soot.Main.v().run(args) 过程中,会执行PackManager.v().runPacks(),其会调用所有的transfrom接口。
Transform中的builder 还是SootWorldBuilder,通过transfrom 接口执行build,不过这个build方法不是上面的那个了,参数不同。
2.2.3 SootWorldBuilder#build2
注意和上面的那个build不同。
1 | private void build(Options options, Scene scene) { |
可以看到,这里对World进行初始化。
赋值呢?
1. World.ClassHierarchy
1 | protected static void buildClasses(ClassHierarchy hierarchy, Scene scene) { |
2. World.irBuilder
在最后,如果prebuildIR为true,通过irBuilder = new IRBuilder(converter);
& irBuilder.buildAll(hierarchy)
; 进行了tai-e IR构建。
如果prebuildIR false呢,只是不在这里构建IR,而是在后续以需要的时候构建,在pta指针分析中,其调用栈如下:
1 | build:209, MethodIRBuilder (pascal.taie.frontend.soot) |
一直要在当需要获取当前节点的IR时,进行build。
2.2.4 Converter逻辑
Tai-e的IR和Soot的IR是不一样的,但是一开始Tai-e又用到soot去获取待分析jar的类、IR等信息,具体在哪里作的转换呢?
pascal.taie.frontend.soot目录
1 | ├── Converter.java |
在上面的World.irBuilder 例子中,就是将SootMethod转化为Tai-e IR。
MethodIRBuilder#build
MethodIRBuilder
1 | IR build() { |
其他的Soot IR也类似,不赘述。
0x03 拓展
3.1 World应用
World 在哪些地方用到呢?
3.2 Main 限制
Y4er师傅的文章中也提到了Tai-e 在PTA 分析中的Main 函数限制怎么突破,其实我们从以上的逻辑中,不难发现Main函数的限制是怎么产生的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public void onStart() {
// process program main method
JMethod mainMethod = World.get().getMainMethod();
if (mainMethod != null) {
solver.addEntryPoint(new EntryPoint(mainMethod,
new MainEntryPointParamProvider(mainMethod, solver)));
}
// process implicit entries
if (solver.getOptions().getBoolean("implicit-entries")) {
for (JMethod entry : World.get().getImplicitEntries()) {
solver.addEntryPoint(new EntryPoint(entry, EmptyParamProvider.get()));
}
}
}
疑问?soot只有一个MainMethod吗?多个怎么办?
如何突破Main限制,可以看Y4er师傅提到的参考【3】
TODO: 如何解决Y4er师傅提到的Main Entry问题?
3.3 implicit Entries 隐式入口
AbstractWorldBuilder
1 | public abstract class AbstractWorldBuilder implements WorldBuilder { |
实际jdk17下
分析完毕,输出统计信息