Skip to content

语义存储和检索

语义存储和检索(src/lian/util/loader)是数据管理的核心基础设施模块。在大规模静态分析过程中,本模块高效地管理、存储和检索分析产生的海量中间数据;同时,面对有限的内存资源,通过缓存策略、分块存储机制和统一的数据访问接口,为Lian执行复杂程序分析提供数据管理基础。

语义存储和检索模块内部包含多种Loader,管理Lian中不同类型的分析结果。Loader框架采用了如下两类组织方式:

  • 模板Loader类:定义数据访问的通用模式;
  • 具体Loader实现:绑定到某一类分析结果。

模板类解决“这一类数据怎么被访问”的问题,而具体Loader解决“这是什么数据”的问题。

2 Loader模板体系

Loader框架通过一套继承体系实现了从通用到专用的分层抽象。底层的数据序列化由DataModel处理,Loader只关注数据的组织逻辑和访问模式优化。

2-1 通用数据管理模板GeneralLoader

GeneralLoader是Loader基类,实现了核心的缓存和分块存储机制。

缓存机制

它采用了两级缓存系统:

访问请求 
  ↓
条目缓存 (Item Cache) ← LRU,存储反序列化后的对象
  ↓ (未命中)
分块缓存 (Bundle Cache) ← LRU,存储原始数据块
  ↓ (未命中)
硬盘文件 (Bundle Files)

数据存储机制

对于数据存储,采用分块存储策略,即数据不是逐条写入硬盘,而是先在内存中累积到"活跃分块"(Active Bundle):

save() → Active Bundle (内存)
  ↓ (累积到 MAX_ROWS)
export() → Bundle 文件 (硬盘)

当活跃分块达到配置的行数上限(config.MAX_ROWS),触发批量写入,生成形如cfg.bundle0的文件。这种批量I/O策略将数百次小写入合并为一次大写入,降低硬盘开销。

索引机制

GeneralLoader采用独立的索引文件记录数据项ID → Bundle文件ID的映射,例如:

method_id: 1001 → bundle0
method_id: 1002 → bundle0
method_id: 2001 → bundle1

查询时先通过索引定位Bundle,再从Bundle中提取具体数据。

抽象接口

GeneralLoader定义了三个抽象方法,子类必须实现以适配不同的数据结构:

  • query_flattened_item_when_loading():从Bundle中查询特定数据项的记录;
  • unflatten_item_dataframe_when_loading():将扁平化的数据帧重建为复杂对象;
  • flatten_item_when_saving():将复杂对象转换为扁平化的字典列表。

这种设计区分了数据存储职责(DataModel)与数据组织职责(Loader)。

2-2 UnitLevelLoader:源文件粒度数据管理

UnitLevelLoader专门处理以源文件(unit_id)为粒度的数据,如作用域层次结构、导出符号等。

2-3 MethodLevelAnalysisResultLoader:方法粒度数据管理

MethodLevelAnalysisResultLoader处理以方法(method_id)或更细粒度(如调用点CallSite)为单位的分析结果。

2-4 OneToManyMapLoader/ManyToOneMapLoader:映射关系管理

这类Loader专门处理ID之间的映射关系,如"一个类包含多个方法"、"一个方法属于一个类"。这些Loader提供了快速的双向查询能力,为调用关系、类型关系和符号归属关系提供索引支持。

3 具体Loader实现

各种具体Loader是Loader模板类的实例化实现。每个具体Loade管理特定的分析数据。它们继承相应的Loader模板类,并实现特定的序列化逻辑。重要的是,这些Loader不关心数据如何物理存储(由DataModel处理),而只关心数据的逻辑结构和访问模式。

3-1 结构性数据Loader

ModuleSymbolsLoader: - 管理所有源文件和目录的元信息 - 建立文件路径与unit_id的双向映射 - 是分析流程中单元与路径映射的起始位置

UnitGIRLoader: - 存储单元的通用中间表示(GIR) - 提供细粒度的语句级访问

3-2 图结构Loader

CFGLoader: - 存储控制流图的边:(src_stmt_id, dst_stmt_id, control_flow_type) - 加载时重建NetworkX有向图

SymbolGraphLoader: - 处理混合节点图:节点既包括语句ID,也包括SymbolDefNode - 根据边的方向区分"定义"边和"使用"边 - 通过条件字段(defined_symbol_*vsused_symbol_*)实现稀疏存储

StateFlowGraphLoader: - 存储状态流图,节点为SFGNode,边为SFGEdge - 通过to_tuple()/from_tuple()方法序列化复杂对象

ImportGraphLoader: - 管理三个相关数据结构: - 符号级导入图(节点为符号ID) - 节点元信息(符号详细信息) - 单元级依赖图(节点为unit_id) - 分离存储、统一查询

TypeGraphLoader: - 存储类型继承关系图 - 支持查询父类和子类

3-3 数据流分析 Loader

BitVectorManagerLoader: - 存储语义节点(SymbolDefNode/StateDefNode)到位位置的映射 - 采用单向存储、双向重建策略

StmtStatusLoader: - 存储语句的定义-使用信息 - 字段包括:defined_symbolused_symbolsin_symbol_bitsout_symbol_bits

SymbolStateSpaceLoader: - 存储符号和状态的完整信息 - 区分SymbolState两种类型 - State包含访问路径(AccessPoint列表)

3-4 方法摘要Loader

MethodDefUseSummaryLoader: - 存储方法的基本定义-使用摘要 - 字段:参数符号、局部符号、外部符号、返回符号

MethodSummaryLoader: - 处理两种对象: - MethodSummaryTemplate(键为method_id) - MethodSummaryInstance(键为CallSite) - 通过检查caller_id字段区分类型

CalleeParameterMappingLoader: - 存储调用点的参数映射关系 - ParameterMapping包含参数索引、状态ID、访问路径等

3-5 调用关系Loader

CallGraphLoader: - 存储方法间的调用关系图 - 边包含调用语句ID

CallPathLoader: - 存储完整的调用路径(CallPath) - 每个路径是CallSite的序列 - 提供复合查询接口: - get_callers_by_method_id():获取调用者 - get_call_path_between_two_methods():获取两方法间的路径 - get_lowest_common_ancestor():查找最近公共祖先

4 统一接口

语义存储和检索模块聚合了数十个具体Loader,提供统一的、语义化的访问接口。

4-1 统一的get/save接口

Loader类为每个专用Loader提供了语义化的访问方法,形成统一的命名规范:

基本模式:

# 保存数据
save_xxx(id, data)

# 读取数据
get_xxx(id)  data

# 转换映射
convert_xxx_to_yyy(xxx_id)  yyy_id

# 判断类型
is_xxx(id)  bool

示例:

# 模块符号
save_module_symbols(module_symbol_results)
get_module_symbol_table()  DataModel

# 单元 GIR
save_unit_gir(unit_id, unit_gir)
get_unit_gir(unit_id)  DataModel

# 方法 CFG
save_method_cfg(method_id, cfg)
get_method_cfg(method_id)  nx.DiGraph

# ID 转换
convert_method_id_to_unit_id(method_id)  unit_id
convert_stmt_id_to_method_id(stmt_id)  method_id

# 类型判断
is_method_decl(stmt_id)  bool
is_parameter_decl_of_method(stmt_id, method_id)  bool

这种统一接口屏蔽了底层 Loader 的复杂性,使得上层分析代码无需关心数据存储在哪里、如何序列化。

4-2 智能查询封装

Loader 提供了大量高级查询方法,封装了复杂的逻辑:

调用关系查询:

get_callers(method, phase="p3", match_by="name", entry_point_id=-1)
get_callees(method, phase="p3", match_by="name", entry_point_id=-1)

通过参数选择对应阶段的实现: - phase="p2":使用CallGraphLoader - phase="p3":使用CallPathLoader - match_by="name":返回方法名集合 - match_by="id":返回方法ID集合

源代码查询:

get_stmt_source_code_with_comment(stmt_id)  List[str]
get_method_decl_source_code(method_id)  str
get_unit_source_code_by_stmt_id(stmt_id)  List[str]

这些方法读取文件,基于既定规则处理注释和边界情况: - 注释的识别和包含 - 方法声明与方法体的区分 - 行号映射和边界处理

方法名称查询:

get_method_name_by_method_id(method_id)  str
# 返回"ClassName.method_name"或"method_name"

自动拼接类名,提供更友好的显示。

5 多阶段分析支持(P1/P2/P3)

Lian 的分析流程分为三个阶段,每个阶段的数据规模和精度递增: - P1:基础结构分析 - P2:从下而上语义分析 - P3:从上而下语义分析

Loader通过文件系统路径分离实现阶段间的数据隔离:

workspace/
  ├── frontend/          # 前端解析结果(GIR、模块符号等)
  ├── semantic_p1/       # P1 基础结构性分析
  ├── semantic_p2/       # P2 自下而上过程间分析
  └── semantic_p3/       # P3 自上而下全局分析

5-1 P1:基础结构性分析

目标:进行单元内的基础分析,建立程序的结构性信息。

核心数据:

  • 控制流与作用域:
  • CFGLoader:方法级控制流图
  • ScopeHierarchyLoader:作用域层次结构
  • UnitSymbolDeclSummaryLoader:符号声明摘要

  • 符号与状态基础:

  • SymbolBitVectorManagerLoader (P1):符号位向量映射
  • StmtStatusLoader (P1):语句定义-使用信息
  • SymbolStateSpaceLoader (P1):符号状态空间

  • 方法与类信息:

  • MethodDefUseSummaryLoader:方法定义-使用摘要
  • MethodInternalCalleesLoader:方法内部调用点
  • ClassIDToMethodsLoader:类到方法映射

  • 调用图初步:

  • CallGraphLoader:初步调用关系
  • GroupedMethodsLoader:方法分组

5-2 P2:自下而上过程间分析

目标:从被调用者向调用者传播信息,进行过程间的数据流分析。

核心数据:

  • 符号与状态传播:
  • SymbolBitVectorManagerLoader (P2):扩展的符号位向量
  • StateBitVectorManagerLoader (P2):状态位向量
  • StmtStatusLoader (P2):更新的语句状态
  • SymbolStateSpaceLoader (P2):扩展的符号状态空间
  • SymbolStateSpaceSummaryLoader (P2):摘要信息

  • 图结构细化:

  • SymbolGraphLoader (P2):符号依赖图
  • StateFlowGraphLoader (P2):状态流图

  • 方法摘要:

  • MethodSummaryLoader (Template):方法摘要模板
  • CalleeParameterMappingLoader (P2):参数映射

  • 调用图细化:

  • CallGraphLoader (P2):更精确的静态调用图

5-3 P3:自上而下全局分析

目标:从入口点出发,进行路径敏感的全局分析。

核心数据:

  • 全局符号与状态:
  • SymbolBitVectorManagerLoader (P3):全局符号位向量
  • StateBitVectorManagerLoader (P3):全局状态位向量
  • StmtStatusLoader (P3):上下文敏感的语句状态
  • SymbolStateSpaceLoader (P3):全局符号状态空间
  • SymbolStateSpaceSummaryLoader (P3):全局摘要

  • 调用路径:

  • CallPathLoader:核心组件,存储完整调用路径

  • 上下文敏感:

  • MethodSummaryLoader (Instance):特定上下文的方法摘要
  • CalleeParameterMappingLoader (P3):上下文敏感的参数映射
  • SymbolGraphLoader (P3):上下文敏感的符号图
  • StateFlowGraphLoader (P3):全局状态流图

6 扩展能力

语义存储和检索模提供数据读写和检索能力,方便上层分析直接使用。

6-1 源代码与GIR关联

问题:分析结果是基于GIR的(语句ID),但用户需要看源代码。

解决方案:

get_stmt_source_code_with_comment(stmt_id)  List[str]
  • 自动定位语句所在的源文件
  • 根据GIR中的行号信息提取对应代码
  • 智能处理注释(通过util.determine_comment_line()识别方法前的注释)
  • 对方法声明,返回整个方法体;对普通语句,只返回该语句

6-2 规则匹配与污点分析

问题:污点分析需要根据规则(如"哪些函数是源/汇")快速定位代码位置。

解决方案:

get_source_sink_stmt_ids_by_rules()  (source_stmt_ids, sink_stmt_ids)

工作流程:

  1. RuleManager获取所有源/汇规则
  2. 按文件路径过滤规则(避免无效匹配)
  3. 遍历每个单元的GIR,对每条语句:
  4. 转换为可读的GIR字符串(readable_gir.get_gir_str()
  5. 使用正则表达式匹配符号名
  6. 如果规则指定了行号,额外检查行号是否匹配
  7. 返回匹配的语句ID集合

6-3 调用关系统一查询

问题:不同阶段的调用关系存储方式不同(P2用图,P3用路径),查询接口也不同。

解决方案:

get_callers(method, phase="p3", match_by="name", entry_point_id=-1)
get_callees(method, phase="p3", match_by="name", entry_point_id=-1)

参数说明: - method:方法名或方法ID - phase"p2"/"p3"/"all"(自动合并) - match_by"name"(返回方法名集合)/"id"(返回方法ID集合) - entry_point_id:指定入口点(仅对P3有效)

6-4 类型层次查询

能力:

get_parent_class_by_class_name(class_name)  List[TypeNode]
get_son_class_by_class_name(class_name)  List[TypeNode]

用途:分析继承关系、虚函数调用、类型转换等。

实现:遍历TypeGraphLoader中的类型图,根据边的parent_name字段查询。

6-5 模块导入关系查询

能力:

get_import_node_with_name(unit_id, import_name)  SymbolNodeInImportGraph
get_edges_and_nodes_with_edge_attrs_in_import_graph(node_id, attr_dict)

用途:分析模块依赖、解析符号引用。

设计: - ImportGraphLoader维护符号级导入图和单元级依赖图 - 支持按导入名称、边类型(内部/外部)、语句ID等属性查询 - 返回边和节点的组合(edge, node)

7 总结:

语义存储和检索框架实现了高效、灵活、可维护的数据管理,在可控的内存和I/O开销下,为高精度全局分析提供数据管理基础。