SootTest实战:Java静态分析从字节码到调用图与ICFG

简介:SootTest 是一份面向 Java 静态分析与编译优化学习者的实战示例资源,基于开源分析框架 Soot,演示如何生成调用图与过程间控制流程图(ICFG)。它适合已掌握 Java 基础、希望深入字节码分析与程序结构理解的开发者,用于理解方法调用关系、跨方法数据流与控制流特性,为性能优化和缺陷检测打基础。压缩包共 23 个文件,约 12.27MB,包含 10 个 class 编译产物、3 个 java 源码、2 个 dot 图形输出、1 个依赖 jar 及 Eclipse 工程配置等,覆盖从源码到可视化结果的完整链路。项目以 SootTest-master 为目录主体,内含 testers 示例模块与 sootOutput 输出目录,可直观看到调用图与 ICFG 的生成效果。目前已有 1440 人学习下载,适合作为 Soot 入门的练手项目,帮助读者快速掌握场景构建、控制流图与调用图 API 的使用方式。

1. 从一行命令到一张图:SootTest 到底在解决什么问题

如果你写过 Java 静态分析,大概率经历过这样的场景:接手一个十几万行的老项目,想搞清楚某个接口到底调了哪些方法、异常从哪一层抛出来、哪些分支永远走不到。靠 IDE 的 Find Usages 一层层点,点到第三层就迷路了。SootTest 这个项目就是冲着这个痛点来的——它用 Soot 这个字节码分析框架,把编译后的 class 文件直接转成调用图(Call Graph)和过程间控制流图(ICFG),让你从「人肉追代码」变成「看图说话」。

它适合两类人:一类是做代码审计、漏洞挖掘、死代码检测的,需要程序依赖关系做底图;另一类是在学编译原理或程序分析课程,想找一个能跑起来、能改参数的实战 Demo。项目本身不复杂,但麻雀虽小,Soot 的 Scene、Spark、Jimple 这些核心概念一个不少。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆开讲一遍。

2. 环境搭建与首次运行:把 class 文件喂给 Soot

2.1 依赖与目录结构

SootTest 是一个标准的 Maven 工程,核心依赖就两个:soot 主包和 slf4j 日志门面。我一般会在pom.xml里锁定 Soot 的版本,因为不同版本 API 差异不小,尤其是Scene和Options的调用方式。

<dependencies> <!-- Soot 核心,负责字节码解析与图构建 --> <dependency> <groupId>org.soot-oss</groupId> <artifactId>soot</artifactId> <version>4.4.1</version> </dependency> <!-- 日志门面,Soot 内部会打不少调试信息 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.36</version> </dependency> </dependencies>

目录结构很直白:src/main/java下放分析入口和工具类,src/main/resources放 Soot 的配置文件,target/classes是编译产物。如果你要分析的第三方 jar 不在 Maven 仓库里,常见做法是在项目根目录建一个libs文件夹,运行时通过-cp参数把 jar 路径传进去。

2.2 配置 Soot 的入口参数

Soot 的入口是soot.Main,但更灵活的方式是在代码里直接操作Options和Scene。下面这段是 SootTest 里最核心的初始化逻辑,我加了逐行注释:

// 设置 Soot 的 classpath,告诉它去哪里找待分析的类 Options.v().set_soot_classpath( "target/classes:" + // 本项目编译产物 "/path/to/your/app.jar" // 待分析的目标 jar ); // 指定要分析的类,支持通配符,比如 com.example.* Options.v().set_process_dir( Collections.singletonList("/path/to/your/app.jar") ); // 开启全程序分析模式,这是生成调用图的前提 Options.v().set_whole_program(true); // 设置输出格式为 Jimple,方便后续遍历中间表示 Options.v().set_output_format(Options.output_format_jimple); // 允许 Phantom 引用,避免因为缺少依赖类而直接崩溃 Options.v().set_allow_phantom_refs(true); // 加载基础类,比如 java.lang.*,否则连 String 都识别不了 Scene.v().loadNecessaryClasses();

这里有几个参数值得展开说。set_whole_program(true)是必须的,Soot 默认只做单类分析,不开全程序模式就拿不到跨方法的调用边。set_allow_phantom_refs(true)是个后悔药——当你的目标 jar 依赖了某个没传进来的库,Soot 会为缺失的类生成一个占位符而不是抛异常,代价是这部分调用关系会丢失,所以生产环境还是要把依赖补全。loadNecessaryClasses()这一步会触发类加载和 Jimple 体生成,耗时跟项目规模成正比,十万行级别的项目大概要等十几秒。

2.3 生成调用图并导出

初始化完成后,生成调用图只需要一行:

// 使用 Spark 算法构建调用图,比 CHA 精度高 CallGraph callGraph = Scene.v().getCallGraph(); // 遍历所有调用边,打印调用者和被调用者 for (Edge<Unit, SootMethod> edge : callGraph) { SootMethod src = edge.src().getMethod(); // 调用方方法 SootMethod tgt = edge.tgt(); // 被调用方方法 System.out.println(src.getSignature() + " --> " + tgt.getSignature()); }

Scene.v().getCallGraph()默认走的是 CHA(Class Hierarchy Analysis),精度较低但速度快。如果你在配置里显式设置了Options.v().setPhaseOption("cg.spark", "enabled:true"),Soot 会切换到 Spark 算法,能处理虚方法调用和接口分派,精度提升明显,代价是内存占用翻倍。我实测过一个 5 万方法左右的项目,CHA 跑完 8 秒,Spark 要 40 秒左右,内存从 1G 涨到 3G。所以如果你的机器内存紧张,或者只是做粗粒度筛选,CHA 够用了。

导出格式上,SootTest 默认打印到控制台,但更实用的做法是输出成 DOT 格式,用 Graphviz 渲染成图片:

// 将调用图导出为 DOT 文件 soot.util.dot.DotGraph dot = new soot.util.dot.DotGraph("callgraph"); for (Edge<Unit, SootMethod> edge : callGraph) { String src = edge.src().getMethod().getSignature(); String tgt = edge.tgt().getSignature(); dot.drawEdge(src, tgt); // 添加一条有向边 } dot.plot("output/callgraph.dot"); // 写入文件

生成的.dot文件用dot -Tpng callgraph.dot -o callgraph.png就能转成图片。不过要注意,大项目的调用图节点动辄上万,直接渲染会糊成一团,常见做法是按包名或类名做过滤,只画你关心的子图。

3. 核心模块拆解:从 Jimple 到 ICFG 的完整链路

3.1 Jimple 中间表示:Soot 的通用语言

Soot 不直接操作 Java 字节码,而是先把它转成 Jimple——一种三地址码形式的中间表示。每个方法体在 Jimple 里就是一条条Stmt组成的链,每条语句最多涉及一个操作。理解 Jimple 是改 SootTest 的前提,因为后面所有图分析都建立在它上面。

// 获取某个方法的 Jimple 体 SootMethod method = Scene.v().getMethod("<com.example.Demo: void run()>"); Body body = method.retrieveActiveBody(); // 遍历方法体中的每一条语句 for (Unit unit : body.getUnits()) { Stmt stmt = (Stmt) unit; // 判断语句类型:赋值、调用、返回、分支等 if (stmt.containsInvokeExpr()) { // 提取方法调用表达式 InvokeExpr invoke = stmt.getInvokeExpr(); System.out.println("调用方法: " + invoke.getMethod().getSignature()); } }

retrieveActiveBody()会触发 Jimple 体的构建,如果方法没有方法体(比如 native 方法),这里会返回 null,需要做判空。body.getUnits()返回的是一个链式结构,顺序就是字节码的执行顺序。containsInvokeExpr()用来筛选出方法调用语句,这是构建调用图的基础。实际改造时,你可能会想过滤掉 JDK 内部调用,只保留业务代码,那就在循环里加一个包名前缀判断。

3.2 过程间控制流图:把调用边和控制边拼起来

ICFG 的本质是把每个方法的 CFG 通过调用边连接起来。Soot 本身不直接提供 ICFG 的现成类,SootTest 里的做法是手动组合:先拿到每个方法的 CFG,再根据调用图把 caller 的调用点和 callee 的入口连起来。

// 构建 ICFG 的核心逻辑 UnitGraph cfg = new BriefUnitGraph(body); // 单个方法的控制流图 // 遍历 CFG 中的每条边 for (Unit unit : cfg) { // 如果当前节点是方法调用 if (((Stmt) unit).containsInvokeExpr()) { SootMethod callee = ((Stmt) unit).getInvokeExpr().getMethod(); // 找到 callee 的入口节点,建立跨过程边 if (callee.hasActiveBody()) { Unit entry = callee.getActiveBody().getUnits().getFirst(); // 这里记录一条从 unit 到 entry 的边 icfgEdges.add(new Edge<>(unit, entry)); } } }

BriefUnitGraph是最常用的 CFG 实现,它只保留必要的分支和跳转边,不包含异常边。如果你需要分析异常传播路径,得换成ExceptionalUnitGraph,它会额外生成 throw 到 catch 的边,图规模会大不少。callee.hasActiveBody()这个判断很关键——如果被调用方法没有方法体(比如接口方法或 native 方法),就没法连边,直接跳过。实际项目中,接口调用占很大比例,Spark 算法能根据类型推断把接口调用解析到具体实现类,这也是为什么做 ICFG 时建议开 Spark。

3.3 调用图算法选型:CHA、Spark 与 VTA 的取舍

Soot 内置了多种调用图构建算法,SootTest 默认用 CHA,但你可以通过参数切换。下面这张表是我在实际项目中总结的对比:

算法精度速度内存适用场景
CHA低快小快速筛选、粗粒度分析
Spark高慢大漏洞挖掘、精确依赖分析
VTA中中中平衡精度与性能

切换方式是在Scene.v().loadNecessaryClasses()之前加一行:

// 启用 Spark,关闭默认的 CHA Options.v().setPhaseOption("cg.spark", "enabled:true"); // 开启点对点分析,精度更高但更慢 Options.v().setPhaseOption("cg.spark", "on-fly-cg:true");

on-fly-cg:true表示在构建调用图的同时做指向分析,能处理更复杂的虚方法分派,但耗时也会增加。如果你的目标项目里接口和继承层次很深,建议开这个选项;如果只是分析工具类之间的调用,CHA 足够了。

4. 避坑与排查:Soot 分析中常见的五类翻车

4.1 类找不到导致分析中断

现象:运行时报Exception in thread "main" java.lang.RuntimeException: This operation requires resolving level SIGNATURES,或者大量Phantom class警告。

原因:Soot 的 classpath 没配全,目标 jar 依赖的第三方库没传进去,导致类解析失败。

解决:把目标项目的所有依赖 jar 都加到set_soot_classpath里。Maven 项目可以用mvn dependency:copy-dependencies把依赖拉到target/dependency,然后把这个目录拼进 classpath。如果实在凑不齐,至少开set_allow_phantom_refs(true)让分析能跑完,但要清楚丢失的那部分调用关系会影响结果。

4.2 调用图边数爆炸

现象:一个几千行的项目,调用图边数几十万,DOT 文件几百兆,Graphviz 直接卡死。

原因:CHA 算法对虚方法调用做了过度近似,把所有可能的实现类都连了边,导致图规模膨胀。

解决:三个方向。一是换 Spark 算法提高精度,减少虚假边;二是在遍历边的时候加过滤,只保留业务包名下的调用;三是用Scene.v().getReachableMethods()先做可达性裁剪,把不可达的方法排除掉再建图。

4.3 内存溢出

现象:分析大项目时抛OutOfMemoryError: Java heap space。

原因:Soot 会把所有类的 Jimple 体加载到内存,加上 Spark 的指向分析数据结构,内存消耗远超预期。

解决:启动时加-Xmx4g甚至-Xmx8g。如果还是不够,可以开Options.v().set_keep_line_number(false)和Options.v().set_keep_offset(false)减少元数据存储。另外,分析完一个模块后及时调Scene.v().releaseActiveBodies()释放方法体。

4.4 接口调用解析不到实现类

现象:调用图里只看到interface.method()的边,没有具体实现类的边。

原因:CHA 不做指向分析,无法确定接口变量实际指向哪个对象。

解决:启用 Spark 并开启on-fly-cg:true。如果还是解析不到,检查目标类是否在 classpath 里,以及接口的实现类是否被set_process_dir包含。

4.5 输出格式乱码或缺失

现象:导出的 DOT 文件里方法签名包含特殊字符,Graphviz 渲染报错。

原因:Java 方法签名里的<、>、$等字符在 DOT 格式里有特殊含义。

解决:在写入 DOT 之前对节点名做转义,把<替换成_,>替换成_,或者用双引号把整个签名包起来。SootTest 里我一般会写一个escapeDot()工具方法统一处理。

5. 二次开发改造:从调用图到自定义分析规则

5.1 扩展点一:按包名过滤子图

原始项目输出全量调用图,实际用的时候往往只关心某几个包。改造思路是在遍历边的时候加一层过滤:

// 只保留 com.example.service 包下的调用关系 String targetPackage = "com.example.service"; for (Edge<Unit, SootMethod> edge : callGraph) { String srcClass = edge.src().getMethod().getDeclaringClass().getName(); String tgtClass = edge.tgt().getDeclaringClass().getName(); if (srcClass.startsWith(targetPackage) || tgtClass.startsWith(targetPackage)) { dot.drawEdge(escapeDot(srcClass), escapeDot(tgtClass)); } }

这样导出的图节点数能降一个数量级,渲染出来也看得清。如果你要做分层分析,可以按包名把图拆成多个子图分别导出。

5.2 扩展点二:标记敏感方法调用

做安全审计时,经常需要定位哪些方法调用了Runtime.exec、ProcessBuilder这类敏感 API。可以在遍历调用边时加一个敏感方法名单:

// 敏感方法签名列表 Set<String> sensitiveMethods = new HashSet<>(Arrays.asList( "<java.lang.Runtime: java.lang.Process exec(java.lang.String)>", "<java.lang.ProcessBuilder: java.lang.Process start()>" )); for (Edge<Unit, SootMethod> edge : callGraph) { SootMethod tgt = edge.tgt(); if (sensitiveMethods.contains(tgt.getSignature())) { // 标记这条边为高危,用红色输出 dot.drawEdge(escapeDot(edge.src().getMethod().getSignature()), escapeDot(tgt.getSignature()), "color=red"); } }

DotGraph.drawEdge支持传属性字符串,color=red能让 Graphviz 把这条边画成红色。实际项目中,敏感方法名单可以从配置文件读,方便随时增删。

5.3 扩展点三:结合 ICFG 做数据流追踪

调用图只告诉你「谁调了谁」,ICFG 才能告诉你「数据怎么流」。一个实用的改造是在 ICFG 上做简单的污点传播:从 source 方法出发,沿着控制流边和调用边往下走,看能不能到达 sink 方法。

// 简化的污点传播:从 source 出发做 BFS Queue<Unit> worklist = new LinkedList<>(); Set<Unit> visited = new HashSet<>(); worklist.add(sourceUnit); while (!worklist.isEmpty()) { Unit current = worklist.poll(); if (visited.contains(current)) continue; visited.add(current); // 如果到达 sink 方法,记录一条污点路径 if (isSink(current)) { System.out.println("发现污点路径: " + current); } // 沿着 ICFG 边继续传播 for (Unit succ : icfg.getSuccsOf(current)) { worklist.add(succ); } }

icfg.getSuccsOf(current)返回当前节点的所有后继,包括方法内的控制流后继和跨过程的调用后继。这个 BFS 框架很粗糙,没有做别名分析和字段敏感处理,但作为二次开发的起点足够了。你可以在此基础上加路径记录、加 sanitizer 判断、加字段访问处理,逐步逼近一个可用的污点分析器。

5.4 验证改造效果的方法

改完代码后怎么确认没跑偏?我的习惯是准备一个小型测试用例,包含三个类:A 调用 B,B 调用 C,C 里有一个敏感方法。跑完分析后检查三件事:调用图里 A→B→C 的边是否完整;ICFG 里跨过程边是否正确连接;敏感方法是否被正确标记。如果这三项都对,再拿真实项目跑一遍,对比改造前后的节点数和边数变化,确认过滤逻辑生效。

从那以后我每次改 Soot 分析逻辑,都强制先跑这个小用例,确认基础链路没断再上大项目。希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

AutoRemesher:3 步完成自动四边形网格重划分,高模秒变低模

AutoRemesher:3 步完成自动四边形网格重划分,高模秒变低模

AutoRemesher&#xff1a;3 步完成自动四边形网格重划分&#xff0c;高模秒变低模 【免费下载链接】autoremesher Automatic quad remeshing tool 项目地址: https://gitcode.com/GitHub_Trending/au/autoremesher AutoRemesher 是一款跨平台自动四边形网格重划分工具&a…

2026/10/20 1:59:13 阅读更多 →
GEFCOM2014-EPFL负荷数据集:电力时序建模的工业级实战指南

GEFCOM2014-EPFL负荷数据集:电力时序建模的工业级实战指南

简介&#xff1a;本资源是面向数据科学学习者与电力行业建模人员的GEFCOM2014能源负荷预测实战包&#xff0c;聚焦R语言环境下的时间序列建模与预测全流程。资源提供EPFL参与构建的多地区小时级负荷数据集及配套R分析框架&#xff0c;覆盖数据加载、可视化&#xff08;ggplot2/…

2026/10/20 1:59:13 阅读更多 →
智能瘦身系统怎么做?SpringBoot+小程序从零到一全流程拆解

智能瘦身系统怎么做?SpringBoot+小程序从零到一全流程拆解

在校招季和毕业季&#xff0c;我经常被问到同一个问题&#xff1a;“智能瘦身系统”这个题目到底该怎么做&#xff1f;后台挂Java和SpringBoot&#xff0c;前端接小程序&#xff0c;名字还挂着“智能”和“健康”&#xff0c;听起来像是个健康类APP&#xff0c;可拿到题目之后很…

2026/10/20 1:59:13 阅读更多 →

最新新闻

程序员转大模型应用开发:6个月从RAG到Agent的实战路线图

程序员转大模型应用开发:6个月从RAG到Agent的实战路线图

前两年我还在纠结“大模型这么火&#xff0c;跟我一个写业务接口的程序员有什么关系”&#xff0c;后来真跳进去做了几个落地项目才发现&#xff1a;大模型领域现在最缺的&#xff0c;恰恰不是能推导Attention公式的研究员&#xff0c;而是能把模型接进业务、能把检索链路调通、…

2026/10/20 2:38:36 阅读更多 →
MAX9286+96705四路AHD车载摄像头方案实战指南

MAX9286+96705四路AHD车载摄像头方案实战指南

简介&#xff1a;本资源是一套面向嵌入式Linux视频驱动开发者的AHD多路视频采集方案&#xff0c;聚焦于将四路模拟高清&#xff08;AHD&#xff09;摄像头信号经MAX9286解码器与MAX96705串行器协同处理&#xff0c;统一转换为MIPI CSI-2数字接口输出&#xff0c;适用于车载DVR、…

2026/10/20 2:38:36 阅读更多 →
SPEI干旱指数工程化计算工具包:支持多PET算法与时空尺度聚合

SPEI干旱指数工程化计算工具包:支持多PET算法与时空尺度聚合

简介&#xff1a;本资源是一套面向气象水文、生态遥感及干旱研究领域的Python计算工具包&#xff0c;专为解决现有开源代码仅支持月尺度SPEI而缺乏日/周尺度计算能力的痛点设计。代码完整实现多时间尺度&#xff08;日、周、月&#xff09;、多分辨率&#xff08;支持逐日输入&…

2026/10/20 2:38:36 阅读更多 →
AiPy实战:用Python本地搭建AI自动化流程,低成本替代昂贵AI助手

AiPy实战:用Python本地搭建AI自动化流程,低成本替代昂贵AI助手

1. 先说清楚&#xff1a;这玩意儿到底是个啥如果你这段时间在关注AI圈&#xff0c;多少应该听过几个名字——某个老外做的自动化AI助手&#xff0c;能自己拆任务、调工具、跑流程&#xff0c;动辄几千块一个月的订阅费&#xff0c;还得折腾各种付费接口。很多开发者、中小团队被…

2026/10/20 2:38:36 阅读更多 →
换两行代码就换引擎:laya-mlx 接口兼容迁移的设计野心

换两行代码就换引擎:laya-mlx 接口兼容迁移的设计野心

换两行代码就换引擎&#xff1a;laya-mlx 接口兼容迁移的设计野心 【免费下载链接】laya-mlx Native MLX runtime for Laya typed decision models — 7–14 ms short decisions on M3 Max. No text generation, PyTorch, or cloud API. 项目地址: https://gitcode.com/gh_mi…

2026/10/20 2:38:36 阅读更多 →
博途1500PLC字符串包含判断:FOR+MID滑动窗口实现详解

博途1500PLC字符串包含判断:FOR+MID滑动窗口实现详解

用博途1500PLC判断源字符串中是否含有子字符串&#xff0c;这个需求听起来简单&#xff0c;我第一次在现场做的时候也以为一条指令就完事了。那是条包装线的扫码工位&#xff0c;读码器通过Profinet把条码送进PLC&#xff0c;我需要在ABC240315001XYZ这样的字符串里判断有没有2…

2026/10/20 2:37:36 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →