资讯动态

Soot实战:从零生成调用图与过程间控制流图ICFG

发布时间:2026/10/17 10:53:05 来源:尧图企业网站定制
简介这是一份基于Soot框架的Java静态分析实战项目面向需要掌握代码结构分析与程序优化技术的Java开发者。项目演示了如何通过Soot生成调用图和过程间控制流图ICFG完整覆盖从加载输入、配置Soot、构建场景到输出图形结构的操作流程适合有一定Java基础并希望深入了解静态分析工具链的开发者学习实践。资源包共23个文件大小12.27MB包含Java源码、编译后的class文件、jar依赖包以及dot格式图形文件等其中dot文件可直接展现生成的调用图与控制流图结果便于对照学习验证。已有1440人学习使用。通过该项目读者可以掌握Soot核心API的调用方法理解Jimple中间表示与调用图的构建原理并实操生成可视化分析结果为后续开展代码质量检测、性能优化和并发问题排查打下扎实基础。1. 为什么你要自己生成调用图从黑匣子到可视化的第一步接手一个六七年没动过的老项目第一件事不是看注释而是跑一遍测试、然后盯着 IDE 的调用层次面板干瞪眼它只能一层一层点看不到整体结构。那会儿我花了一下午也没搞清某个入口到底触碰到了哪些模块。后来某位同事甩给我一个脚本跑完直接吐出一张调用关系和每个方法内的控制流图效率完全是两个量级。那个脚本就是用 Soot 做的今天我把这套用 Soot 生成调用图和过程间控制流程图的完整流程拆开写清楚。不管你是做安全审计、代码理解还是准备做重构影响面评估这套东西都能把你的黑匣子变成一张看得见的路网。适合的人群很简单已经在用 Java 写工程、但觉得静态分析很遥远的开发者。2. Soot核心参数与调用图生成从Options.v()到CG变换2.1 理解Soot的三种图UnitGraph、CallGraph 和 ICFGSoot 里有三张图很多人一开始分不清我当初也绕了好一阵。最底层的是UnitGraph它描述单个方法内部的语句执行顺序节点是Unit也就是一行 Jimple 中间代码边是语句间的跳转。第二层是CallGraph它描述方法之间的调用关系节点是方法边是调用关系粗粒度但全局。第三层才是我们要的 ICFG——过程间控制流图它把每个方法的 UnitGraph 和 CallGraph 拼在一起形成一张完整的、覆盖所有方法内部语句 跨方法调用的大图。ICFG 是静态分析里相当常用的结构。比如你要算一个漏洞从入口到Sink的路径单纯看 CallGraph 不行你得知道Sink方法内部第几行真正执行了危险操作必须连语句粒度一起分析。Soot 的Transformer机制允许你在每个 phase 里把这些图取出来直接操作不需要自己从零构造。平时最常听到的 Soot 流程是Java 字节码 → Jimple 中间表示 → 各种分析 → 输出结果。Jimple 比字节码可读性好太多所以拿它做控制流分析比直接怼 bytecode 舒服得多。后面所有图都是在 Jimple 层级上构建的这一点先刻在脑子里。2.2 用CHA和SPARK生成调用图的完整代码生成调用图有两种主流算法CHAClass Hierarchy Analysis和 SPARK。CHA 通过类继承关系做保守决议速度快但精度低SPARK 基于指向分析精度高但费时。我在比较小的项目里直接上 SPARK几秒钟就跑完遇到大 jar 包就先用 CHA 出一版再对热点方法局部做定点分析。下面是基于 Soot 4.x 的标准写法public class CallGraphGenerator { public static void main(String[] args) { String classPath System.getProperty(java.class.path); String targetDir target/classes; soot.G.reset(); Options.v().set_soot_classpath(classPath); Options.v().set_output_format(Options.output_format_jimple); Options.v().set_whole_program(true); Options.v().set_process_dir(java.util.Collections.singletonList(targetDir)); Options.v().setPhaseOption(cg.spark, on); // 启用SPARK Scene.v().loadNecessaryClasses(); // 触发调用图构建 PackManager.v().runPacks(); CallGraph cg Scene.v().getCallGraph(); // 遍历调用图 IteratorMethodOrMethodContext it cg.iterator(); while (it.hasNext()) { MethodOrMethodContext mom it.next(); SootMethod method mom.method(); System.out.println(Method: method.getSignature()); } } }这段代码做的事情先是G.reset()清空全局状态避免多次运行互相污染。然后设置 classpathset_whole_program(true)表示做全程序分析必须对整个项目构建 Sceneset_process_dir指向你要分析的 classes 目录。setPhaseOption(cg.spark, on)是关键不启用的话默认走 CHA调用图会粗很多。PackManager.runPacks()执行所有 phase其中包括调用图构建。构建完成后调用Scene.v().getCallGraph()拿到图通过过滤器遍历方法即可。如果不只想遍历还想拿到具体调用关系可以用cg.edgesInto(method)和cg.edgesOutOf(method)得到每个方法的具体调用边。这里补充一个血泪经验必须调用loadNecessaryClasses()否则你在处理某些类型时会出现大量ClassNotFoundException因为 Soot 没加载rt.jar里的基础类。2.3 参数说明与选型resolution、phases和入口指定参数里最容易踩坑的是-process-dir和-cp的配合。process-dir指定哪些目录下的类要被分析而 classpath 是分析过程中解析依赖用的。如果 classpath 不全很多方法会被标记为phantom调用图里就看不到它们的内部结构。我通常把项目编译输出目录、依赖 jar 全部拼进 classpath再单独把目标 classes 目录交给process-dir。另一个需要调的是PhaseOptions。比如只做调用图不想要后续 Jimple 输出可以把jb和jj等 phase 关掉减少计算量。常用的是Options.v().setPhaseOption(jb, off); Options.v().setPhaseOption(jj, off);想要指定入口方法可以用Options.v().set_main_class(com.example.Main)Soot 会从 main 方法开始构建调用图。对于 Android 项目就麻烦一些因为没有 main 方法需要手动加一个合成入口或者用EntryPoints类的接口做自定义入口。这块不做展开但提醒一句入口不对调用图会漏掉一大部分边。3. 过程间控制流图的拼装从UnitGraph到ICFG的实现细节3.1 ICFG的语义把call边插进控制流里ICFG 不是一张独立生成的图它是在UnitGraph基础上叠加调用边得到的。你可以把每个方法的UnitGraph想象成一个独立的子图调用边就是连接子图之间的桥梁。每条调用边从call site调用语句指向被调方法的入口 Unit同时有一条返回边从被调方法的 exit Unit 指回 call site 的下一个 Unit。这个模型的好坏直接决定分析精度。Soot 提供了现成的类UnitGraph和它的子类ExceptionalUnitGraph后者会把 JVM 可能抛出的异常路径也建模成边。默认的CompleteUnitGraph不含异常边这会漏掉很多真实的控制流——比如一个try-catch块catch 路径不会被画出来。所以构造 ICFG 时我一般都用ExceptionalUnitGraph。3.2 用Soot API构造ICFG的代码Soot 4.x 里没有直接叫ICFG的类你需要自己把两者合并。常见做法是遍历程序的所有方法为每个方法生成ExceptionalUnitGraph然后用CallGraph做边连接。下面这段代码演示了怎么完成合并并输出为简单的边列表import java.util.*; import soot.toolkits.graph.*; import soot.jimple.toolkits.callgraph.*; public class ICFGBuilder { public static void main(String[] args) { // 沿用第2章中的Options设置此处省略 PackManager.v().runPacks(); CallGraph cg Scene.v().getCallGraph(); // 遍历所有方法生成各自的UnitGraph for (SootMethod method : Scene.v().getApplicationClasses()) { if (!method.hasActiveBody()) continue; ExceptionalUnitGraph ug new ExceptionalUnitGraph(method.getActiveBody()); // 输出方法内部控制流 for (Unit u : ug.getHead().getSuccs()) { System.out.println(method.getName() : u); } } // 遍历调用的边打印跨方法边 IteratorEdge edgeIt cg.iterator(); while (edgeIt.hasNext()) { Edge e edgeIt.next(); Unit src e.src(); Unit tgt e.tgt(); if (src ! null tgt ! null) { System.out.println(ICF edge: src - tgt); } } } }这里的关键是ExceptionalUnitGraph的构造传进去SootMethod.getActiveBody()Soot 会自动把该方法的 body 解析为 Unit 序列并为UnitGraph的每个节点建立 predecessor/successor 链。能拿到getHead()是因为 UnitGraph 已经做了 topo 排序。输出时我只打印了 head 的后继实际项目里往往会遍历整张图。调用边的处理稍微绕一点CG 里的边是MethodOrMethodContext级别的但Edge.src()能拿到具体调用语句UnitEdge.tgt()是被调方法的第一个 Unit。这里有一个坑从cg.iterator()拿到的Edge其src()可能为 null因为某些调用是线程启动之类的不定源需要手动过滤。3.3 处理异常边缘与JVM的隐式调用很多人在这一步发现 ICFG 缺边原因多半是忽略了异常边缘和隐式调用。比如new表达式可能触发OutOfMemoryError数组访问可能触发NullPointerException这些都不出现在普通控制流里。ExceptionalUnitGraph已经把这些隐式异常边加进去了但代价是图会变得很大分析性能下降。折中方案是只在关键方法的图里启用异常边其余方法用普通CompleteUnitGraph。隐式调用包括Thread.run()、finalize()等SPARK 默认不会替你把new Thread().start()跟run()连起来。我一般会自己写一个EdgePredicate把常见的隐式调用手动补进 CallGraph否则分析结果会漏掉一类跟并发相关的路径。相关代码判断调用者是否在某个SootMethod里有invoke指令即可这里不再赘述。4. 避坑与常见问题classpath不全、版本差异和反射调用4.1 现象启动后狂抛 ClassNotFoundException程序直接挂第一次跑 Soot 时我用默认 classpath 直接处理 Java 8 项目的 classes结果报了一连串java.lang.ClassNotFoundException: java.lang.Object。原因曝光Soot 需要知道 JDK 的类在哪里如果你不指定rt.jar或者完整的 classpathSoot 会把java.lang.Object当成一个 phantom class然后各种分析就崩了。解决方法是显式加上Options.v().set_phantom_jar_list()或者直接拼上rt.jar。更稳的姿势是用 Maven 里现成的sotest工具类自动探测java.home下的 lib 目录并填进 classpath。4.2 现象调用图小得可怜只有几十个方法明明整个项目有好几百个类生成的调用图却只包含入口类和直接被调用的少数方法。原因大概率是set_whole_program(true)没设置Soot 默认只分析非静态的、从main可达的方法达不到整程序效果。也可能是process_dir设错了比如指向了空目录。解决办法是在main开头加一行Options.v().set_whole_program(true)并且检查Scene.v().getApplicationClasses().size()如果数量明显小于项目真实类数就去检查 classpath 和目录路径。4.3 现象生成的 ICFG 没有异常路径如果我拿CompleteUnitGraph去生成流程try-catch的内容完全不在图里导致分析结果比预期乐观很多。原因很直接普通的CompleteUnitGraph只保留显式跳转不建模异常边。解决方式是换用ExceptionalUnitGraph并把Options.v().setThrowAnalysis(pedantic)打开这样 Soot 会采用更保守的异常分析把所有可能抛异常的点都连上 catch。代价是图会变大特别是大方法但这是精度必须付的账。4.4 现象反射调用Class.forName、invoke在调用图里看不到SPARK 本质上做的是 Java 字节码层的指向分析它在设计上就不处理反射。也就是说你用method.invoke(target)的代码在调用图里只会看到Method.invoke这个 JDK 方法没法连到你真正想调的target.method。如果项目里反射比较多两个选择一是自己写一个SootMethodHandle去解析常量字符串并加边二是借助 Soot 的Jasmin层把那部分代码用生成的桥接方法替代。常见的折中方案是写一个简单的预处理脚本把代码里能静态枚举的invoke目标扫描出来然后以phantom的方式强制加进 CG。4.5 现象Soot 升级到 4.4 之后原来编译好的分析脚本报 API 错误Soot 在 4.x 系列里对Options内部结构做过不少调整比如setAware之类的方法名变过部分访问路径也改了。遇到这种版本迁移问题最靠谱的是直接锁死版本用 4.3.0 或者 4.4.0 的 Maven dependency 就行。如果非要用新版本先对着soot-options的 javadoc 找出废弃方法的替代品一般用setPhaseOption都能绕过。我的个人习惯是项目里把 Soot 作为独立进程跑用 Gradle 一个专门的模块维护这样升级不会波及主工程。5. 把调用图和ICFG可视化从XML导出到Graphviz渲染5.1 导出图数据的两种方式拿到图之后你总归要把它画出来给人看。第一种方式是直接遍历图节点和边输出 Graphviz 的 dot 语言。第二种方式是利用 Soot 自带的CallGraphToString工具但它输出的是文本格式不够直观。我一般是自定义遍历把方法签名、语句文本都写进 dot 文件。这样可以在节点上附加额外信息比如标注该方法是 public 还是 private是否被并发调用等。简单的 dot 导出代码public static void exportToDot(CallGraph cg, String path) { try (PrintWriter pw new PrintWriter(new FileWriter(path))) { pw.println(digraph callgraph {); for (IteratorEdge it cg.iterator(); it.hasNext(); ) { Edge e it.next(); SootMethod src e.srcMethod(); SootMethod tgt e.tgtMethod(); if (src ! null tgt ! null) { pw.printf( \%s\ - \%s\;%n, src.getSignature(), tgt.getSignature()); } } pw.println(}); } catch (IOException e) { e.printStackTrace(); } }注意这里Edge的srcMethod()和tgtMethod()是 4.x 里较方便的方法直接返回方法对象无需再加工。dot 文件渲染成 PNG 用一行命令即可dot -Tpng callgraph.dot -o callgraph.png。如果节点太多我一般会在导出的过滤条件里排除java.*的 JDK 类避免整张图被库函数淹没。5.2 用Graphviz渲染并查找关键路径Graphviz 有个酷炫的功能——按路径高亮。你要从某个疑似漏洞入口查到一个危险 sink可以在 dot 文件里把路径上经过的边着成红色这样看到的结果就跟地铁线路图一样清楚。实际做法是先调用 Soot 的Analysis类做正向可达性分析找出入口能访问到的所有 Unit然后再把包含这些 Unit 的边标记成红色。如果找不到红色路径说明中间断了这时候大概率是反射或者隐式调用没补上回到第 4 章的思路继续补边。5.3 进阶结合Jimple定位特定方法的调用链有一个技巧很实用在生成 ICFG 的同时给每个方法输出一份 Jimple 文件第 2 章已经把output_format设为 jimple。这样你在 dot 图里看到Method_1指向Method_2后可以立刻打开Method_1.jimple查看具体是哪个 invoke 触发的甚至能定位到某一行代码。我一般会在 jimple 输出里加上行号属性方便跟源码对应。Soot 的Jimple文件本身结构清晰阅读成本不高配合调用图做审查非常顺手。从那以后我每次接手陌生 Java 项目都会强制跑一遍调用图生成 → 异常边检查 → 关键路径高亮这个流程。跑完之后心里就有底了不会一头扎进代码里瞎翻。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑