Soot调用图与过程间控制流图实战:从构建到避坑

发布时间:2026/10/25 14:04:50
Soot调用图与过程间控制流图实战:从构建到避坑 简介SootTest是一份面向Java开发者的Soot框架实战示例资源适合对静态程序分析、字节码优化及调用关系可视化感兴趣的读者。项目演示了如何借助Soot从Java源码或字节码构建Scene场景并通过配置生成调用图Call Graph与过程间控制流程图ICFG为理解跨方法的数据流和控制流特征提供了可运行的范例。压缩包共23个文件主要包含Java源码、编译后的class文件、Soot依赖jar包、dot格式图文件、PNG输出图以及Eclipse工程配置文件其中jar包可直接用于运行示例dot与PNG结果便于对照和分析生成效果。资源包整体约12.27MB结构简洁上手门槛适中。目前已有1439人学习下载适合初学Soot框架、希望快速掌握调用图与ICFG生成流程的Java开发者参考实践。1. 从反编译黑匣子到程序分析地基为什么你需要 Soot 的调用图做程序分析的人大概率都有过这种经历拿到一个 APK 或 jar 包想搞清楚某个敏感函数到底被谁调用了、调用链有多长打开反编译工具一看满屏 switch 和 goto多态一多就彻底断了线索。调用图就是把这个过程从“人肉逆向”变成“形式化分析”的关键一跳而 Soot 是目前开源工具链里把调用图和过程间控制流图做得最顺手的一个。这套方案能帮你回答三个核心问题某个方法的所有调用点在哪、程序从入口到敏感操作经过了哪些路径、以及这些路径里的分支条件是否可控。适合做安卓隐私合规检测、漏洞挖掘前的基础建模以及静态分析框架的底层支撑。本文不绕理论直接从怎么用 Soot 生成调用图和过程间控制流图ICFG讲起参数、API、踩坑都放在后面。2. 从字节码到分析模型Jimple 中间表示与调用图前置概念2.1 为什么选 Soot 而不是直接解析字节码直接解析 JVM 字节码或 DEX 字节码的方式很多时候是从汇编层面去理解程序逻辑。Java 方法里的一个try-catch在字节码里被拆成异常表和跳转指令一个for循环展开后是几条条件跳转和goto的组合。把控制流从这些底层指令里还原出来不但费劲而且非常容易在边界情况上翻车。Soot 做的事情是先把字节码统一转换成一种名叫 Jimple 的三地址中间表示它保留了类型信息、局部变量、方法调用语句同时又比字节码抽象一层读起来像简化版的 Java 代码。这种中间表示最大的好处是你不用分别处理.class、.dex、.apk三套字节码语义Soot 已经帮你抹平了格式差异后面所有分析调用图、数据流、切片都在 Jimple 这一层做。另一个容易被忽略的理由是Soot 的框架不只是一个“翻译机”它自带一套完整的分析框架。程序分析里最难的部分不是遍历而是怎么在遍历中维护别名关系、异常边和跨方法的数据流状态。你用纯手写的方式解析字节码光处理invokevirtual的类型解析就要写一大坨逻辑换成 Soot一个Scene.v().getCallGraph()就能拿到全程序的调用图。分析框架的价值在于它为“过程间分析”提供了现成的数据结构而不是让你从零开始搭积木。我一般会建议团队里新做静态分析的人先花一周把 Soot 的 API 走一遍胜过自己造一个字节码解析轮子。2.2 调用图的两类构建算法CHA 与 Spark 的精确定位生成调用图在 Soot 里有两条主要路线CHAClass Hierarchy Analysis和 Spark。CHA 的思路很朴素——根据声明类型去类继承关系里找所有可能的重写方法把所有可能的目标全连上边。它的优点是快因为只查继承树不需要走进方法体分析数据流缺点是虚调用interface dispatch会爆出大量不可能的边。比如一个list.add(x)可能被 CHA 连向所有实现List接口的类的add方法哪怕那类压根没被实例化。实际项目里这种假边占比很高你要是基于 CHA 做污染路径分析很容易跑出几百条根本走不通的假路径。Spark 走的是另一条路它在构建过程中用指针分析和调用图互相迭代每次调用图新增一条边就把对应的实参指向关系传播到被调方法的形参再反过来通过 new 表达式的流向确定更精确的分派目标。它的分析粒度更细假边明显减少缺点是慢而且要处理递归调用时需要考虑 fixed-point 迭代。Soot 里同时提供几种分析变体像是paddle和spark框架默认的 Spark 配置适合大多数场景。实际使用中我的建议是如果你只是想知道方法之间的包含关系CHA 就够用如果你要基于调用图做污点传播或权限路径分析上 Spark。后文的命令都基于 Spark 配置展开参数细节在第三章给出。3. 把一个 JAR 变成可查询的调用图最小工程与第一个 Hello World3.1 工程配置的三条路径Maven、Python 绑定、命令行Soot 最常见的落地方式是 Java Maven 工程依赖坐标是ca.mcgill.sable:soot。这个坐标在 Maven Central 上长期维护目前主流版本对应 Java 8 及以上都兼容Gradle 项目也适用。如果你主要做快速预研可以直接用 Python 绑定库装一个soot包对应 Soot 的 Python 接口实现API 风格和 Java 版几乎一致适合在 Jupyter Notebook 里做探索式分析。工程团队我建议用 Maven因为后续要集成自研分析逻辑Java API 最完整。配置完成后的最小代码如下import soot # 配置分析目标与输出模式 soot.options.set_option(soot-class-path, target/classes) soot.options.set_option(output-format, jimple) soot.options.set_option(keep-line-number, True) # 这一步会把目录下的 .class 文件加载进 Scene soot.Main.main([com.example.target.Main]) # 直接访问全局场景中的调用图 call_graph soot.Scene.v().call_graph() print(调用边总数:, call_graph.size())这段代码做了三件事设置类路径、指定入口类、触发 Soot 的整包分析流程。soot-class-path是分析目标的类路径注意它应该指向编译产出目录而不是源码目录Soot 只认字节码。keep-line-number打开行号保留后面做过程间控制流图映射回源码位置时全靠它。soot.Main.main传入的是入口类列表Soot 会从这些类的 main 方法开始构建调用图。Python 绑定里call_graph对象提供size()、edges()等接口可以快速做一些基础统计但在复杂分析场景还是推荐回 Java 侧拿完整 API。3.2 深入 Scene入口、调用边与调用点的三类 API拿到调用图对象之后最常见的操作是查询某个方法的所有调用者和被调用者。Soot 的Scene是一个全局单例它持有所有加载的类、方法、字段和调用图。查询调用关系的核心接口是CallGraph类里面提供两类查询方式edgesOutOf(s)和edgesInto(s)其中s是SootMethod对象。在实际代码里调用图用UnitJimple 语句级别的边来组织这意味着你可以从一条 invoke 指令精确回溯到它的解析目标。// 获取入口方法 SootClass mainClass Scene.v().getSootClass(com.example.target.Main); SootMethod mainMethod mainClass.getMethodByName(main); // 遍历从 main 出发的所有边 for (IteratorEdge it Scene.v().getCallGraph().edgesOutOf(mainMethod); it.hasNext(); ) { Edge e it.next(); SootMethod tgt e.tgt(); Unit srcUnit e.srcUnit(); System.out.println(调用点: srcUnit.getJavaSourceLineNumber() - tgt.getSignature()); }这里的e.srcUnit()返回的是调用语句本身能拿到行号信息。做完这一步你就可以在 IDE 里跑出第一条真实调用链了。如果你的分析对象是 Android APKSoot 需要额外指定 Android 平台 JAR 的路径否则无法解析系统 API 的调用目标常见的做法是通过soot.options里的android-jars参数指向本地下载好的 android.jar 缓存目录。4. 从调用图到过程间控制流图带着异常边和数据流走完整个程序4.1 ICFG 的结构Unit 图、显式边与异常边调用图解决的是“谁调用了谁”但很多时候你还想知道“从方法入口到某个调用语句经过了哪些路径”。这就需要过程间控制流图也就是 ICFG。ICFG 在数据结构上就是把每个方法的 CFG控制流图通过调用边和返回边连成一张大图。Soot 里构建 ICFG 的入口是UnitGraph体系每个方法有一张UnitGraph节点是 Jimple 语句边模拟语句之间的执行顺序。方法之间有两种连接方式显式的调用边从调用语句连到被调方法入口返回边从被调方法的出口或 return 语句连回调用语句的下一条语句。这里有一个很关键的坑异常边。Java 里的try-catch、方法调用可能抛出的运行时异常都会产生额外的控制流路径。Soot 在建图时默认会为可能抛异常的方法调用构造异常边这些边不是你在源码里能直接看到的但对可达性分析影响很大。举个例子一个Integer.parseInt()调用在源码里看起来是顺序执行但 ICFG 里它可能同时有一条边流向NumberFormatException的 handler。如果你的分析逻辑遍历 ICFG 时没把异常边算进去会漏掉真实的崩溃路径算进去了又会在无异常的代码里多出不少分支。Soot 允许在构建UnitGraph时指定是否忽略异常边ExceptionalUnitGraph构造器的ignoreExceptionalEdges参数这个参数按需开关做可达性分析建议保留做精简路径生成建议关掉。4.2 写一个三行算法把 ICFG 的路径原样吐出来构建 ICFG 在 Soot 里不算复杂下面这段代码利用ExceptionalUnitGraph做过程间路径遍历思路是深度优先走当前方法的 CFG碰到 invoke 指令就进入被调方法的图里继续走同时记录路径上的方法签名import soot from soot.jimple import Stmt, InvokeExpr def build_icfg(entry_method): # 用异常敏感的 UnitGraph 做底图 graph soot.jimple.UnitGraph(entry_method, True) paths [] def dfs(stmt, path, depth): if depth 15: return path.append(stmt) # Jimple 里调用语句可以实现 InvokeExpr 接口 if isinstance(stmt, soot.jimple.InvokeStmt): for expr in stmt.invoke_exprs(): target expr.method() if target.is_concrete(): # 不进入抽象方法 dfs(target, path, depth 1) for succ in graph.get_succs_of(stmt): dfs(succ, path, depth) path.pop() dfs(graph.get_body().get_first_non_identity_stmt(), [], 0) return pathsdepth 15是深度限制防止递归调用把遍历栈打爆。is_concrete()会过滤掉抽象方法因为它们没有可执行的体。这个实现是一个极简的深度优先路径解析足够用于小规模 jar 包的路径枚举但对大型 APK跑全量路径会产生指数级路径爆炸常见做法是加约束条件只追踪从入口到某个特定 target 方法的路径中间用Unit的getJavaSourceLineNumber记录行号。Soot 在UnitGraph的getSuccsOf里已经包含了普通 CFG 后继和异常后继所以你的路径天然带有异常分支。4.3 定位敏感路径从入口方法到目标调用语句的可达性分析如果只是要回答“从 main 能不能走到sendTextMessage”不需要列全量路径只需要做一次反向的图搜索。Soot 的 ICFG 支持反向遍历从目标调用出发沿 ICFG 的入边往回找直到到达入口方法。下面这段代码用 BFS 检查可达性def reachable_in_icfg(entry_method, target_method): graph soot.jimple.UnitGraph(entry_method, True) queue [graph.get_body().get_first_non_identity_stmt()] visited set() while queue: stmt queue.pop(0) if stmt in visited: continue visited.add(stmt) if hasattr(stmt, invoke_exprs) and stmt.invoke_exprs(): for expr in stmt.invoke_exprs(): if expr.method() target_method: return True # 同样需要跨过程遍历 for succ in graph.get_succs_of(stmt): # 对被调方法的入口也入队 if isinstance(succ, soot.jimple.InvokeStmt): for expr in succ.invoke_exprs(): m expr.method() if m.is_concrete(): queue.append(m.get_body().get_first_non_identity_stmt()) queue.append(succ) return False这里有个细节容易踩坑UnitGraph.get_succs_of只返回当前方法内部的后继不会自动跨方法跳转。你在遍历时遇到调用语句要手动把被调方法体的第一条语句加入队列。上面代码里的InvokeStmt分支就是在补这一步。如果你用的是 Soot 里更高层的ExceptionalUnitGraph它在线性化时同样不会替你跨过程边跨过程这部分永远得自己处理。这也是为什么很多二次开发会直接基于 Soot 的Patching框架去改 IR而不是自己造遍历器。5. 常见避坑调用边界、递归与 Android 场景的三个黑匣子5.1 现象调用图里多出一堆Library类连的是System.exit用 CHA 生成调用图后会发现图里经常出现java.lang.System.exit、android.util.Log.d这类库方法的调用边。原因是 Soot 默认加载rt.jar或 Android JAR 中的系统类而这些类的某些方法可能在你的入口可达范围内被调用到。看起来无害但如果你把调用图渲染出来图上会塞满来自java.*包的噪声节点完全没法看。原因有两个第一Soot 把库类也加载进Scene没有区分 application 类与 library 类第二CHA 对虚方法的处理会把所有子类可能的实现都连上。解决方式是构建调用图前调用Scene.v().setApplicationClassPath()或者显式设置soot.ClassProvider告诉 Soot 只有target/classes下的类是分析对象。更进一步的方案使用soot.Scene.v().getSootClass().setApplicationClass()对单个类标记把库类排除在调度目标之外。项目里一般会在配置处加一行allow-phantom-refs来允许引用缺失的库类避免因为缺一个类导致整个分析中断。5.2 现象递归方法导致路径枚举卡死内存飙升跑路径遍历时遇到A - B - A这种递归调用如果深度限制写得不对程序会一直往深走最终堆内存溢出。这个坑在真实项目里出现频率极高特别是处理缓存框架、树形递归遍历这类代码时。原因很简单DFS 里只记录当前路径是否访问过没有记录“已访问过的方法调用组合”。解决方式有两个方向一个是在路径枚举时设定全局节点访问集合同一轮分析中一个(method, stmt)组合最多进入一次另一个是在调用图上做后向切片时每次递归检查调用栈中是否已存在同目标方法是则直接剪枝。上面代码里的depth 15只是粗暴的止损实际项目里限制条件一般改为“同一方法在递归调用栈内只能出现一次”避免因为大循环而无谓扩容。5.3 现象Android APK 分析时调用图缺入口也没法定位到 Activity如果你喂给 Soot 的是一个 APK而入口配置还是com.example.target.Main大概率会报找不到类的错误或者一个几乎为空的调用图。因为 Android 应用的入口不是一个简单的 main 方法而是AndroidManifest.xml里声明的 Activity、Service、BroadcastReceiver。Soot 提供专门的入口点计算机制当你通过soot.Main加载 APK 时它会自动解析 Manifest 并生成一组包括onCreate、onStart在内的 stub 方法。但如果你配置错了比如漏了-process-dir或-android-jarsSoot 会退化成只分析某一个 class 文件调用图自然不完整。通常的处理方式是分析 APK 时给soot.options传入process-dir指向 APK 解压后的目录或直接指向 APK 文件和android-jars指向本地缓存的安卓平台 jar然后使用Soot里内置的AndroidEntryPointCalculator来计算入口点。这个类会扫描 Manifest把application下的组件都作为入口。如果你用的是 Python 接口入口点计算预设在soot.Main内部但建议手动指定soot.options.set_option(src-prec, apk)把输入格式切为 APK 模式否则默认按 class 文件处理解析必然出错。5.4 现象ICFG 里出现不存在的边通过get_UseAndDefs发现异常处理被错误合并有些场景下Soot 的UnitGraph会生成错误边比如一个 try 块末尾的赋值语句竟然指向 catch 块。这通常源于字节码级别的异常表把覆盖范围扩大到了你预期之外的范围。JVM 字节码里异常表的最小粒度是字节码偏移区间如果一个 try 块里的某个指令内部发生了栈状态变化那么整个区间内的所有指令都有潜在异常边。Soot 在转换时由于没有源码级语义只能保守地保留异常边。从分析角度看这不是 bug但如果你拿着 ICFG 里的边去反推控制流是否真实容易得出错误结论。一个稳健的调试手法是用UnitGraph的getEdges()遍历所有边打印出每条边的源码行号把有异常边指向的语句单独标记出来。观察一段时间你会发现几乎所有的异常边都指向一个区域的“合并 catch 块”——这是 Java 编译器把多个 catch 合并成一个异常处理器的产物。遇到这种情况不要尝试从 Soot 配置层面去掉异常边因为那是字节码层面的结构问题而是在分析逻辑里用ThrowAnalysis接口给每条调用语句重新计算“可能抛出的异常类型”滤掉与当前场景无关的异常路径。Soot 默认的ThrowAnalysis是朴素实现对重载优化过的 Obfuscated 代码有偏差但作为初筛已经足够了。6. 进阶把你的调用图落成可视化 Web 服务和动态验证一条边6.1 用 TinyGraph 把 CALL_GRAPH 转成 DOT 格式做局部放大调用图生成之后第一件事是渲染出来。Soot 的DotGraph类可以导出 DOT 格式但图一大就会乱成一团。常用做法是提取某几个方法的局部子图再在 Graphviz 里展示。下面是一段基于 Java API 的导出逻辑private void exportSubGraph(SootMethod root, String path) { DotGraph graph DotGraph.DotGraph(subgraph); CallGraph cg Scene.v().getCallGraph(); for (IteratorEdge it cg.edgesOutOf(root); it.hasNext(); ) { Edge e it.next(); SootMethod src e.src(); SootMethod tgt e.tgt(); graph.drawEdge(src.getSignature(), tgt.getSignature()); } graph.plot(path .dot); }导出之后再跑一遍dot -Tpng subgraph.dot -o subgraph.png就能得到单一入口的调用关系图了。这个方案的优点是绕开了 Soot 内置可视化对超大图的渲染瓶颈缺点是只能静态地看一层。如果想让图可交互我一般会把调用图写进 Neo4j把SootMethod的getSignature()作为节点 IDEdge的类型invokevirtual、invokeinterface等作为关系类型这样就能在浏览器里点一个方法顺藤摸瓜找到它触发的一整条链路。数据量在十万节点以内时这种查询都是毫秒级返回的。6.2 动态验证调用边用 JUnit 加 AOP 打印真实运行栈静态调用图存在误报为了确认一条边真的会被触发可以用一个轻量级的运行时验证法。给目标项目加一个面向切面的探针在候选方法入口打印当前线程的调用栈然后跑一次测试或复现用例看实际运行路径里有没有出现静态分析出的那条边。这个手法成本很低但能把分析结果从“理论上可达”变成“实践中真实发生”。Aspect public class CallLogAspect { Before(execution(* com.example.target.*.*(..))) public void logCall(JoinPoint jp) { StackTraceElement[] stack Thread.currentThread().getStackTrace(); for (int i 0; i Math.min(stack.length, 15); i) { System.out.println( stack[i].getClassName() . stack[i].getMethodName()); } } }观察的重点不是打印出的完整调用栈而是对比静态分析出来的调用图上某一条边是否能在运行栈里找到。如果这条边在静态图里存在但运行时始终不出现一般有三种可能静态分析时的入口不够完备、Spark 指针分析把变量指向了过度近似的位置、或者运行时压根没走到那段逻辑。动态验证最大的价值是给静态结论标记可信度后续做代码审计报告时可以直接引用“运行栈实测确认”或“仅静态可达”的结论这比空口说“分析工具发现漏洞”有说服力得多。跑过一轮完整流程之后我对 Soot 的最大感受是它把最繁琐的字节码解析和类层级管理都包掉了但分析策略的质量还是握在使用者手里。越往后做越会发现边界条件、异常路径、递归深度这类细节才是调用图和 ICFG 能不能真正落地到生产环境的分水岭。我自己做 Android 隐私合规路径分析时前期大半时间不是花在写分析规则上而是花在调 Soot 的类路径和异常处理策略上。希望这些踩坑经验能让你少走一段弯路把精力放到真正有价值的分析逻辑上。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询