资讯详情

Soot调用图与过程间控制流图ICFG构建实战

📅 2026/10/10 17:30:37 | 华诺云谱 👁 阅读
Soot调用图与过程间控制流图ICFG构建实战
简介面向Java开发者与静态分析初学者的一份Soot实战示例演示如何借助Soot框架生成调用图Call Graph与过程间控制流程图ICFG并解释两者在执行路径分析、性能优化与并发问题排查中的用途。内容围绕配置Soot选项、构建Scene场景、使用UnitGraph与InterproceduralControlFlowGraph等核心API展开适合希望从零上手程序分析、理解跨方法调用关系的读者。压缩包共23个文件约12.27MB以Java源码、class字节码、dot图文件、png可视化结果以及封装好的soot依赖jar为主同时包含Eclipse工程配置与完整目录结构解压后即可对照示例运行。资源已吸引1439人学习浏览对准备开展代码优化、依赖分析或缺陷检测的开发者尤其有价值。通过实际运行和阅读输出可直观掌握调用图与ICFG的生成流程并迁移到自己的Java项目中建立工程化的静态分析能力。1. 从静态分析说起为什么 Soot 的调用图和过程间 CFG 值得自己搭一遍接手一个没人维护的 Java 老项目时最想知道的往往不是某个方法里写了什么而是方法之间到底谁调了谁、调用链跨了几个类。Soot 在 Java 静态分析里几乎是绕不开的名字SootTest 就是基于它搭的一个最小工程用 Soot 的 CallGraph API 生成全程序调用图再以每个方法为节点、以调用边为桥梁拼出过程间控制流程图ICFG。IDE 的 Call Hierarchy 能看一条链但拿不到全量调用图也没法把控制流从方法内延伸到方法间。SootTest 适合三类人做污点分析、程序切片或死代码检测的静态分析开发者需要一张能被代码遍历的调用图而不是 PPT 里的示意图维护老系统的工程师想快速看清模块之间的依赖墙以及刚接触 Soot、需要一个能跑通的最小工程来理解 Jimple、方法体和图遍历的学生。它解决的不是单个算法问题而是把 Soot 从知道有这个工具变成能稳定产出图结构的过程后面所有分析都可以踩在这张图上继续做。2. 环境与工程骨架Soot 4.3 在 Java 11 下的最小工程2.1 版本怎么挑soot 3.x 与 4.x 的分水岭版本先把话说在前头。网上很多教程还停在 soot 2.5.0那是 Java 8 时代的东西跑在 JDK 11 上经常直接 NoSuchMethodError。我的经验是老项目用 Java 8 就锁 soot 3.3.0新项目用 Java 11 就用 soot 4.3.0这两个组合我实际跑过最稳。4.x 改了 Gradle 构建JDK 模块支持也重做了API 大体沿用 3.x所以旧的配置代码改改包名基本能跑。sootup 是官方新一代替代品但 API 设计和经典 Soot 差别很大文档也还没补全。如果你只是想快速拿到调用图和 ICFG经典 Soot 的生态和问答积累都更成熟我建议初学者别一上来就跳 sootup先把 SootTest 这套跑通再说。版本推荐 JDK调用图支持我的评价soot 3.3.0Java 8cha/rta/spark 齐全兼容老项目soot 4.3.0Java 11cha/rta/spark 齐全目前最稳的组合soot 4.4.0Java 11/17phase 接口有调整要上 JDK 17 时再用先看变更记录sootupJava 17新 API建议做过一轮 Soot 再试2.2 最小工程骨架Maven 依赖与入口类的写法SootTest 我建议直接用 Maven 管理相比手动下 jar 包最大的好处是 soot 自己带了一堆传递依赖guava、opencsv、antlr 这些手动管理很容易漏。加依赖properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target /properties dependencies dependency groupIdorg.soot-oss/groupId artifactIdsoot/artifactId version4.3.0/version /dependency /dependenciesorg.soot-oss是 4.x 的 groupId别写旧的ca.mcgill.sable那只会拉到 3.x 之前的版本。依赖配好后写入口类核心就一个 mainimport soot.*; import soot.options.Options; import java.util.Arrays; public class CallGraphGen { public static void main(String[] args) { // 1. classpath 里必须同时放待分析代码和依赖库 String cp target/classes: System.getProperty(java.class.path); Options.v().set_soot_classpath(cp); // 2. 指定分析哪些类这里是 Maven 编译产物的目录 Options.v().set_process_dir(Arrays.asList(target/classes)); // 3. 全程序模式下调用图才有意义 Options.v().set_whole_program(true); // 4. 不输出 Jimple 文件只跑分析 Options.v().set_output_format(Options.output_format_none); // 5. 加载类跑完所有启用的 phase Scene.v().loadNecessaryClasses(); PackManager.v().runPacks(); // 6. 调用图就在 Scene 里 CallGraph cg Scene.v().getCallGraph(); System.out.println(总边数: countEdges(cg)); } static int countEdges(CallGraph cg) { int n 0; for (var it cg.iterator(); it.hasNext(); ) { it.next(); n; } return n; } }每个参数都有讲究。set_soot_classpath是关键Soot 不会自动找你 Maven 的编译输出漏了 JDK 类库后面就会报一堆 ClassNotFoundException。set_process_dir决定哪些类进入应用集合没进这个集合的类只作为库类加载不会成为调用图的分析主体。set_output_format_none告诉 Soot 跑完 pack 不生成 Jimple 文件省掉磁盘 IO。runPacks()会执行点分析、标记等一堆 phase如果要精确控制可以只跑需要的 pack但第一版跑通最重要先全量跑。3. 生成调用图CHA、RTA 与 Spark 的取舍和 Scene API3.1 三种调用图算法的取舍调用图本质是哪个调用点可能调到哪个方法的二元关系集合。Java 里方法调用分五类invokestatic、invokespecial、invokevirtual、invokeinterface、invokedynamic。前两类编译器就能确定目标后三类在字节码层面写的是声明类型的方法真正运行时才知道实际类型是谁。调用图算法就是在算这个可能调用集。CHAClass Hierarchy Analysis最激进它把声明类型的所有子类都算进去。比如ListString l ...; l.size()声明类型是 ListCHA 会把所有实现 List 的类里的 size 都当作可能目标。优点是快缺点是虚边多。RTA 做了收紧只考虑程序里确实被实例化过的类型精度明显提升。Spark 是三类里最重的它跑一遍指向分析points-to analysis能推断出l实际可能指向哪些对象调用边数量最少代价是分析时间成倍上涨。算法精度速度Soot 开关适用CHA低虚边多快cg.cha先跑通流程RTA中中cg.rta中等规模项目Spark高慢cg.spark下游需要精确点我的建议是先 CHA 把管线打通确认输出的图结构符合预期再换 Spark 提精度。一上来就 Spark碰到问题你分不清是配置错还是算法慢。3.2 用 Scene API 取调用边并输出统计CHA 在整程序模式下默认启用不需要额外配置。要换 Spark在 runPacks 之前加一行Options.v().setPhaseOption(cg.spark, enabled:true,onflycg:true);enabled:true开启 Spark 算法onflycg:true表示在指向分析过程中同步构建调用图边会更精确内存占用也更高。换成 RTA 就是把这段的cg.spark换成cg.rta。配置完从 Scene 里取边import soot.jimple.toolkits.callgraph.Edge; import java.util.HashMap; import java.util.Map; public static void dumpCallStats(CallGraph cg) { MapSootMethod, Integer calleeCount new HashMap(); MapEdge.Kind, Integer kindCount new HashMap(); int total 0; for (var it cg.iterator(); it.hasNext(); ) { Edge e it.next(); SootMethod caller e.getSrc(); SootMethod callee e.getTgt(); calleeCount.merge(callee, 1, Integer::sum); kindCount.merge(e.kind(), 1, Integer::sum); total; } System.out.println(调用边总数: total); for (Map.EntryEdge.Kind, Integer kv : kindCount.entrySet()) { System.out.println(kv.getKey() : kv.getValue()); } }e.getSrc()是调用者方法e.getTgt()是被调方法e.kind()返回边的种类对应前面的调用类型。统计 kindCount 有个实际用处如果某个分析场景不该出现虚调用但 VIRTUAL 边占了 80%就能立刻意识到类层次信息没加载完整要回去补 classpath。提示Edge.getSrc()返回的是源方法调用点所在的语句要用e.getSrcUnit()拿后面拼 ICFG 时这个 Unit 就是调用边的锚点。4. 过程间控制流程图从 UnitGraph 到 supergraph 的拼接4.1 方法内 CFGExceptionalUnitGraph 怎么建过程间控制流程图的过程是方法流程是控制流。先讲方法内。Soot 里每个方法体Body是 Jimple 语句链的集合控制流图就是对语句链做后继关系分析。最基础的是 BriefUnitGraph它只处理正常控制流异常处理器会单独成块我一般直接用 ExceptionalUnitGraph它把 try-catch 和 finally 里的隐式边也展开更贴近真实执行。import soot.toolkits.graph.ExceptionalUnitGraph; import soot.toolkits.graph.UnitGraph; public static UnitGraph buildMethodCFG(SootMethod m) { if (!m.hasActiveBody()) { throw new IllegalStateException(方法没有激活的 Body: m.getSignature()); } UnitGraph cfg new ExceptionalUnitGraph(m.getActiveBody()); System.out.println(方法 m.getName() 有 cfg.size() 个 Unit); return cfg; }hasActiveBody()这步很关键。Soot 加载类时默认只保留 method 签名方法体会被延迟解析直接 getActiveBody 会触发解析但如果类没被完整加载就可能抛异常。拿到图之后可以逐节点打印后继for (Unit u : cfg) { System.out.println(u); System.out.println( 后继: cfg.getSuccsOf(u)); }Unit 在 Jimple 里就是一条条语句打印出来能看到r0 ...或if $z1 ! 0 goto label1这类形式。getSuccsOf返回直接后继列表getPredsOf返回前驱。这里有个容易忽略的点Unit 上绑定的 DefBoxes 和 UseBoxes 才是数据流分析的入口控制流只是骨架别只盯着图结构。4.2 过程间拼接调用边 返回边的 supergraph 构造单方法 CFG 只是过程内的要做切片或污点分析需要把调用关系接上去构成 supergraph。做法是对每个可达方法建 CFG然后在调用语句和被调方法的入口 Unit 之间加调用边在被调方法出口和调用语句后继之间加返回边。import soot.jimple.JInvokeStmt; import soot.jimple.InvokeExpr; import java.util.*; public class SupergraphBuilder { MapSootMethod, UnitGraph cfgs new HashMap(); MapUnit, SootMethod callSites new HashMap(); public SupergraphBuilder(SootMethod entry, CallGraph cg) { // 用工作队列做可达方法遍历 DequeSootMethod worklist new ArrayDeque(); worklist.push(entry); while (!worklist.isEmpty()) { SootMethod m worklist.pop(); if (cfgs.containsKey(m) || !m.hasActiveBody()) { continue; } cfgs.put(m, new ExceptionalUnitGraph(m.getActiveBody())); for (var it cg.getCalleesOf(m); it.hasNext(); ) { SootMethod callee it.next(); if (!cfgs.containsKey(callee)) { worklist.push(callee); } } } // 记录每个调用语句对应的被调方法 for (UnitGraph ug : cfgs.values()) { for (Unit u : ug) { if (u instanceof JInvokeStmt) { InvokeExpr ie ((JInvokeStmt) u).getInvokeExpr(); callSites.put(u, ie.getMethod()); } } } } }这个遍历有几个点值得说。worklist保证只展开从入口可达的方法不会把整条 classpath 上的库类全卷进来。getCalleesOf返回 Iterator在 while 里别在迭代过程中又 push 新方法到同一个 worklist这里用独立条件判断避免重复入队。callSites建成后从调用语句出发就能直接跳到被调方的头节点调用图、方法内 CFG、调用点三者在这个 Map 里对齐。调用边建好后下游分析最常用的操作是输入状态传给被调方入口再从出口拿结果。很多教材把 supergraph 讲得很玄其实就是三张表方法到 CFG 的映射、调用语句到被调方法的映射、调用语句在 CFG 里的后继列表。SootTest 把这三张表暴露成 Map后续写污点传播就是在这三张表上做 BFS。注意上面只处理了JInvokeStmt。真实代码里 InvokeExpr 还可能嵌在JAssignStmt的右值里完整的处理是对所有 Unit 检查getInvokeExprBox()否则会漏掉一部分调用边。5. 避坑与排查五个让调用图翻车的高频环境这部分是血泪经验全是我实际跑 SootTest 时踩过的坑按现象、原因、解决写。5.1 调用图只有 main 方法一个节点现象统计总边数只有个位数调用图里只有 main 和几个 JDK 方法自己写的业务类完全没进来代码里存在的方法调用一个都没有。原因set_process_dir指向的目录不对或者根本就没开set_whole_program(true)。Soot 在全程序模式下才会对应用类做整体分析不开 whole-program调用图只覆盖显式加载的那几个类目录不对则等于分析了个空集。解决先确认target/classes下面确实有 .class 文件再在 main 里打印Scene.v().getApplicationClasses().size()如果数量对不上就把 process_dir 改到编译产物根目录。这步是最容易翻车的每次改完路径先看应用类数量再往下走。5.2 ClassNotFoundException 与 classpath 拼装现象自己的类能加载跑到某个第三方依赖或 JDK 内部类时抛 ClassNotFoundException或者 NoClassDefFoundError报错堆栈指向的类名五花八门。原因set_soot_classpath只写了target/classes没把传递依赖加进去。Soot 的类加载器不会去读 pom.xml更不会自动解析依赖图它严格按给的 classpath 字符串到对应 jar 里找类。解决最省事的就是在入口代码里拼System.getProperty(java.class.path)用 IDE 或 exec 插件跑的时候这条属性已经包含全部依赖 jar手工 java 命令跑的话先执行mvn dependency:build-classpath把依赖路径拼出来。判断依据是报错里第一个找不到的类看它在哪个 jar把它补进 classpath。5.3 泛型擦除和桥接方法的边缺失现象泛型方法ListT foo()的调用边对不上泛型接口实现类里出现一堆同名但参数不同的方法调用图里的调用目标比源码里看到的多。原因Java 编译后泛型信息被擦除Soot 读的是字节码不是源码。ComparatorT编译后会生成compare(Object, Object)这样的桥接方法源码里不存在字节码里真实存在Soot 自然把它当作合法调用目标。解决别试图在 Soot 层还原泛型接受字节码视角。比对方法名时用getBytecodeName()和getBytecodeParms()而不是源码泛型签名。只想看应用逻辑时用e.isExplicit()过滤掉合成边但要注意 lambda 生成的边也可能被滤掉。5.4 Java 17 下 Soot 4.x 的启动故障现象用 JDK 17 跑 soot 4.3.0启动直接 IllegalAccessError 或 java.lang.reflect.InaccessibleObjectException堆栈指向 java.lang 包内部。原因JDK 17 默认强封装内部 APISoot 4.3 对 JDK 9 模块系统的适配还不完整反射访问被拦。解决两个方向选一个。换 JDK 11soot 4.3.0 在 Java 11 上很稳定或者升 soot 4.4.0并在启动参数里加--add-opens java.base/java.langALL-UNNAMED。SootTest 的 README 里我把 JDK 版本写死为 11就是为了省掉这一堆开关。5.5 Spark 内存溢出与剪枝现象小工程 CHA 秒出结果换 Spark 后几分钟没动静最后 OutOfMemoryError或者 GC 日志显示 90% 时间在回收。原因Spark 的指向分析对整个可执行程序做约束求解classpath 里的库类也参与库越大约束越多on-fly 模式下调用图边又喂回分析器双重压力。解决JVM 加-Xmx4g是基本操作更有效的是剪枝。用Options.v().set_exclude(Arrays.asList(java.*, javax.*, sun.*))把 JDK 排除出分析主体不需要精确库内调用时RTA 是更好的折中。我一般先 CHA 验证结构再对具体痛点方法单独开 Spark。6. 让结果可验证DOT 导出与 IDE 交叉核对6.1 把调用图导出成 Graphviz 可视化调用图生成只是第一步关键是怎么验证它是对的。我的做法是先导 DOT 用 Graphviz 渲染再拿 IDE 的 Call Hierarchy 交叉核对三条已知调用链。import java.io.*; import java.util.*; public static void exportDOT(CallGraph cg, String path) throws IOException { try (PrintWriter pw new PrintWriter(new FileWriter(path))) { pw.println(digraph callgraph {); SetSootMethod methods new TreeSet( Comparator.comparing(SootMethod::getSignature)); for (var it cg.iterator(); it.hasNext(); ) { Edge e it.next(); methods.add(e.getSrc()); methods.add(e.getTgt()); } for (SootMethod m : methods) { pw.println( \ m.getSignature() \ [label\ m.getName() \];); } for (var it cg.iterator(); it.hasNext(); ) { Edge e it.next(); pw.println( \ e.getSrc().getSignature() \ - \ e.getTgt().getSignature() \;); } pw.println(}); } }getSignature()输出的是完整签名含类名、方法名、参数类型用它做节点 id 永远不会重名。渲染时过滤掉 lambda 和桥接边否则图会糊成一团dot -Tsvg callgraph.dot -o callgraph.svg之后在浏览器里先找一棵你断言的调用树比在代码里 debug 快得多。核对的具体步骤在 SootTest 的测试代码里挑三个已知调用关系比如 Service - Repository - Dao先在 IDE 里确认这条链存在再在导出的 DOT 文件里 grep 三个方法名看边是否一条不少。如果代码写的是new Repository().query()Soot 里这条边一定是 SPECIAL 或 VIRTUAL而不是 STATIC这个语义也是核对点之一。从那以后我每次跑 SootTest 都强制走一遍三查先查 classpath 与应用类数量再查调用边总数是否在预期量级最后拿三条已知调用链去 DOT 里比对。这个习惯帮我挡掉了至少一半的算法看起来没问题、其实输入类根本没加载的翻车。希望你也能少踩这些坑一次跑出可信的图——希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑