MIAYC Static Analyzer是MIAYC项目的静态分析部分,决赛时我们针对初赛时发现的痛点与问题进行了点对点的细致优化与改进,主要体现在以下几个方面:
完善类型分析系统 : 基于LLVM IR提供的元数据,增强了对类型信息的提取和利用能力,能够更准确地分析变量的类型和生命周期,大大增加了精确性。 符号内存模型优化 : 重构原有符号内存系统,添加多级缓存机制,保障了符号执行分析中符号的唯一性,同时大大减少了内存消耗,大幅度提升了性能。 约束求解优化 : 添加链式约束机制,大幅提升了约束求解效率。 函数调用模型优化 : 添加函数调用栈机制,大幅度减少了因为函数调用产生的状态爆炸等开销。 执行策略优化 :引入基于MPC算法的启发式算法,在考虑路径减枝的基础上优先探索更多基本块,尽可能探索更多出错路径,提高了整体分析的覆盖率,极大加快了分析收敛速度。
在经过以上优化后,MIAYC静态分析器已达到先进水平,我们提供了详细的测试来证明其可用性与先进性。
决赛阶段的整体架构如下图所示:
我们在调研概要中曾分析过静态分析中基于源码,AST与LLVM IR三种语言层面的分析的优劣,最终选择基于LLVM IR进行类型分析。LLVM IR作为一种中间表示,虽然具有良好的语言无关性和抽象能力,但是也正是因为这一层抽象能力损失了大量源码(尤其是类型)信息,在LLVM 14后,IR统一使用不透明指针(Opaque Pointers),这一改变更加加剧了我们分析指针指向的内存区域大小的难度。在初赛时,我们选择使用保守估计来应对这一问题,即对不确定大小的内存块都标记位最小内存(1字节),虽然这样能保证召回率,但是牺牲了一定的准确率。对此,我们开始研究如何在类型信息有限的情况下经可能的还原出来原始信息,并且不影响召回率。
LLVM 14: Supports all necessary APIs for migrating to opaque pointers and deprecates/removes incompatible APIs. However, using opaque pointers in the optimization pipeline is not fully supported. This release can be used to make out-of-tree code compatible with opaque pointers, but opaque pointers should not be enabled in production.
LLVM 15: Opaque pointers are enabled by default. Typed pointers are still supported.
LLVM 16: Opaque pointers are enabled by default. Typed pointers are supported on a best-effort basis only and not tested.
LLVM 17: Only opaque pointers are supported. Typed pointers are not supported.
define i8* @test(i8* %p) {
%p2 = getelementptr i8, i8* %p, i64 1
ret i8* %p2
}
; Is automatically converted into the following if -opaque-pointers
; is enabled:
define ptr @test(ptr %p) {
%p2 = getelementptr i8, ptr %p, i64 1
ret ptr %p2
}通过仔细研究LLVM IR结构,我们发现:
-
如果编译时开启了调试选项(-g),我们可以通过llvm::DISubprogram来恢复出指针信息的上下文,从而可以依据此构建出一套类型体系,从调试信息中推断出指针指向内存的大小。
对于下图所示的函数,如果我们的需要在无调用者的上下文单独分析这个函数,我们无法推断出其参数ptr %0 的类型,也无法通过构建内存区域来模拟这个指针的具体行为(只能保守估计)
define dso_local void @_ml_cleanup_tree_buggy(ptr noundef %0) #0 !dbg !207
但是由于其有编译器附带的调试信息,我们可以通过llvm::DISubprogram来恢复出指针信息的上下文,从而可以依据此构建出一套类型体系,从调试信息中推断出指针指向内存的大小。
!24 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !25, size: 64) !25 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed) !206 = !DISubroutineType(types: !207) !207 = !{null, !24, !25}
-
对于无法获得调试信息的指针类型(例如被减枝路径中的指针结构),我们通过分析gep指令等,采用延迟绑定策略来动态绑定内存区域的大小。
对于下图所示的指针,虽然我们无法从
ptr %10中得知其具体类型,但是根据GEP指令的参数,我们能推断其为指向Packet结构体的指针,并且我们也能通过类型信息推断出其索引偏移和索引内存区域的大小。%10 = getelementptr inbounds nuw %struct.Packet, ptr %9, i32 0, i32 1, !dbg !241
对于方法1,我们通过以下步骤来实现:
- 在编译时开启调试选项(-g),以便生成调试信息。
- 使用llvm::DISubprogram来恢复指针信息的上下文。
- 根据恢复出的上下文信息,构建类型体系,并推断指针指向内存的大小。
// ExprEngine.cpp
...
llvm::DISubprogram* sp = func->getSubprogram();
llvm::DISubroutineType* func_type =
sp ? llvm::dyn_cast<llvm::DISubroutineType>(sp->getType()) : nullptr;
llvm::DITypeRefArray param_types =
func_type ? func_type->getTypeArray() : llvm::DITypeRefArray();
for (auto& arg : func->args()) {
// 为每个参数创建未知符号
SymExprPtr init_expr = nullptr;
auto* arg_type = arg.getType();
if (llvm::isa<llvm::PointerType>(arg_type)) {
uint64_t pointed_size = 1;
// 尝试通过调试信息获取指针指向类型的大小
if (param_types.size() > arg.getArgNo() + 1) {
llvm::DIType* arg_di_type = param_types[arg.getArgNo() + 1];
if (auto* derived_type =
llvm::dyn_cast<llvm::DIDerivedType>(arg_di_type)) {
auto* base_type = derived_type->getBaseType();
pointed_size = getBaseTypeSize(base_type);
} else {
pointed_size = getBaseTypeSize(arg_di_type);
}
}
LOG_INFO(
"Function {} parameter {} is a pointer, "
"using pointed type size = {}",
func->getName().str(), arg.getName().str(), pointed_size);
// 绑定一个未知指针和一个最小的区域
auto region =
createSymbolicRegion(nullptr, createConst(pointed_size));
init_expr = createLoc(region, LocSymExpr::State::UNKNOWN);
LOG_DEBUG("Initialize pointer parameter: {} as unknown pointer",
arg.getName().str());
} else {
// 其他类型参数 - 可以根据需要扩展
init_expr = createVar(func);
}
// 直接绑定符号值
state->envBind(&arg, init_expr);
LOG_DEBUG("Initialized parameter: {} of function {}",
arg.getName().str(), func->getName().str());
}
...以及
// utils.h
// 递归解析DIType获取基本具体类型的大小(字节)
inline uint64_t getBaseTypeSize(llvm::DIType* type) {
if (!type)
return 0;
while (auto* derived_type = llvm::dyn_cast<llvm::DIDerivedType>(type)) {
type = derived_type->getBaseType();
if (!type)
return 0;
}
// 现在type时DICompositeType或DIBasicType
if (auto* composite_type = llvm::dyn_cast<llvm::DICompositeType>(type)) {
// 复合类型,返回其大小
return composite_type->getSizeInBits() / 8; // 转换为字节
}
if (auto* basic_type = llvm::dyn_cast<llvm::DIBasicType>(type)) {
// 基本类型,直接返回大小
return basic_type->getSizeInBits() / 8; // 转换为字节
}
return 0;
}依靠这些分析,我们能正确识别无调用者的函数的参数类型,并且做出精确建模。
对于问题2,我们首先给指针添加了各种状态,然后通过识别可能的绑定时间并进行动态绑定,部分代码如下:
// TransferVisitor.cpp
void TransferVisitor::visitGetElementPtrInst(llvm::GetElementPtrInst& i) {
...
auto base_loc = getLocFromPtr(ptr_expr);
auto base_size = base_loc->size;
auto* base_type = gep_inst->getSourceElementType();
auto gep_base_size =
exprEngine_.DL.getTypeStoreSize(base_type); // 获取GEP基类型的大小
uint64_t element_size = exprEngine_.DL.getTypeStoreSize(
gep_inst->getResultElementType()); // 获取GEP返回类型的大小
if (ptr_exp->getState() == LocSymExpr::State::UNBIND) {
base_loc->size = createConst(gep_base_size);
base_size = base_loc->size;
}
...
}以及:
// TransferVisitor.cpp
SymExprPtr GetGEPOffsetExpr(llvm::GetElementPtrInst* GepInst,
llvm::DataLayout& DL, ProgramState& ps_) {
llvm::Type* base_type = GepInst->getSourceElementType();
llvm::Type* current_type = base_type;
SymExprPtr s = createConst(0);
if (GepInst->getNumIndices() == 1) {
// 单个索引情况(通常是数组)
auto* index = GepInst->getOperand(1);
auto index_expr = ps_.value2SymExprOrNew(index);
uint64_t type_size = DL.getTypeAllocSize(base_type);
auto type_expr = createConst(type_size);
auto offset_expr =
createBinary(index_expr, type_expr, BinarySymExpr::Op::Mul);
s = offset_expr;
} else {
for (unsigned i = 1; i < GepInst->getNumIndices(); i++) {
auto* index = GepInst->getOperand(i + 1);
auto index_expr = ps_.value2SymExprOrNew(index);
if (auto* st = llvm::dyn_cast<llvm::StructType>(current_type)) {
// 结构体类型:直接使用字段偏移量
auto* index_ci = llvm::dyn_cast<llvm::ConstantInt>(index);
if (!index_ci) {
LOG_ERROR("Struct type index is not a constant");
return nullptr;
}
unsigned field = index_ci->getZExtValue();
uint64_t field_offset =
DL.getStructLayout(st)->getElementOffset(field);
// 直接添加偏移量,不乘以索引
s = createBinary(s, createConst(field_offset),
BinarySymExpr::Op::Add);
} else {
// 数组或指针类型:计算 索引 * 元素大小
llvm::Type* elem_type = llvm::GetElementPtrInst::getTypeAtIndex(
current_type, index);
uint64_t elem_size = DL.getTypeAllocSize(elem_type);
auto offset = createBinary(index_expr, createConst(elem_size),
BinarySymExpr::Op::Mul);
s = createBinary(s, offset, BinarySymExpr::Op::Add);
}
// 更新当前类型
current_type =
llvm::GetElementPtrInst::getTypeAtIndex(current_type, index);
}
}
return s;
}在初赛的设计中,我们在创建符号时是通过类似以下的工厂函数来实现:
// 全局单例的工厂类,拥有缓存与化简功能
class SymExprFactory {
public:
static SymExprFactory& getInstance() {
static SymExprFactory instance;
return instance;
}
SymExprFactory(const SymExprFactory&) = delete;
SymExprFactory& operator=(const SymExprFactory&) = delete;
SymExprFactory(SymExprFactory&&) = delete;
SymExprFactory& operator=(SymExprFactory&&) = delete;
// 创建符号表达式 - 使用常量引用参数
SymExprPtr createConst(ConstType key) {
auto it = constCache_.find(key);
if (it != constCache_.end()) {
return it->second; // 如果缓存中存在,直接返回
}
SymExprPtr expr = std::make_shared<ConstSymExpr>(key);
constCache_[key] = expr; // 缓存新创建的表达式
return expr;
}
static SymExprPtr createVar(const std::string& name) {
SymExprPtr expr = std::make_shared<VarSymExpr>(name);
return expr; // 变量表达式不需要缓存
}
static SymExprPtr createBinary(const SymExprPtr& lhs, const SymExprPtr& rhs,
BinarySymExpr::Op op);
static SymExprPtr createCompare(const CompareSymExprPtr& cond,
bool isTrue) {
return std::make_shared<CompareSymExpr>(
CompareSymExpr::Op::Eq, cond,
SymExprFactory::getInstance().createConst(isTrue ? 1 : 0));
}
static SymExprPtr createCompare(const SymExprPtr& lhs,
const SymExprPtr& rhs,
CompareSymExpr::Op op) {
return std::make_shared<CompareSymExpr>(op, lhs, rhs);
}
static SymExprPtr createRange(const SymExprPtr& lhs,
const SymExprPtr& rhs) {
return std::make_shared<RangeSymExpr>(lhs, rhs);
}
static SymExprPtr createLoc(
const MemoryRegionPtr& region,
LocSymExpr::State state = LocSymExpr::State::CONFIRMED) {
return std::make_shared<LocSymExpr>(region, state);
}
private:
SymExprFactory() = default;
std::unordered_map<ConstType, SymExprPtr> constCache_;
};初赛时我们只实现了简单的常量缓存,而随着处理程的越来越大,以及对性能要求越来越重要,我们开始考虑对尽可能多的符号进行缓存的同时保证其上下文一致性。为此,我们对工厂类进行了扩展,增加了对变量、二元表达式等的缓存支持,并且添加了详细且足够完善的表达式化简机制来保证相同的表达式能够映射到同一片缓存中。
除了对符号变量的缓存,我们还注意到程序中在子区域索引时的性能表现很差,初赛时,我们通过在每个状态中内置一份子区域索引表来模拟动态子区域绑定的场景 std::unordered_map<IdType, std::vector<ElementRegionPtr>> subRegionMap_;,但经过性能分析后发现,通过表来索引子区域不仅效率低下,还会给符号求解器带来严重性能负担,于是我们决定探索更高效的方式来实现子区域的绑定。
对于问题1,我们大幅度重构了符号表达式的工厂体系,并且添加了足够完备的化简机制保证缓存的正确性:
namespace std {
template <>
struct hash<std::tuple<BinarySymExprOp, unsigned int, unsigned int>> {
size_t operator()(const std::tuple<BinarySymExprOp, unsigned int,
unsigned int>& key) const {
size_t h1 = std::hash<BinarySymExprOp>{}(std::get<0>(key));
size_t h2 = std::hash<unsigned int>{}(std::get<1>(key));
size_t h3 = std::hash<unsigned int>{}(std::get<2>(key));
// 简单组合哈希
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};
template <>
struct hash<std::tuple<CompareSymExprOp, unsigned int, unsigned int>> {
size_t operator()(const std::tuple<CompareSymExprOp, unsigned int,
unsigned int>& key) const {
size_t h1 = std::hash<CompareSymExprOp>{}(std::get<0>(key));
size_t h2 = std::hash<unsigned int>{}(std::get<1>(key));
size_t h3 = std::hash<unsigned int>{}(std::get<2>(key));
// 简单组合哈希
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};
} // namespace std
// 全局单例的工厂类,拥有缓存与化简功能
class SymExprManager {
public:
static SymExprManager& getInstance() {
static SymExprManager instance;
return instance;
}
SymExprManager(const SymExprManager&) = delete;
SymExprManager& operator=(const SymExprManager&) = delete;
SymExprManager(SymExprManager&&) = delete;
SymExprManager& operator=(SymExprManager&&) = delete;
// 创建常量表达式
SymExprPtr createConst(ConstType key) {
auto it = constCache_.find(key);
if (it != constCache_.end())
return it->second;
auto expr = std::make_shared<ConstSymExpr>(key);
constCache_[key] = expr;
return expr;
}
// 创建变量表达式
SymExprPtr createVar(const llvm::Value* value) {
auto expr = std::make_shared<VarSymExpr>(value);
return expr;
}
// 化简并缓存二元表达式
SymExprPtr createBinary(const SymExprPtr& lhs, const SymExprPtr& rhs,
BinarySymExpr::Op op);
// 创建比较表达式
SymExprPtr createCompare(const SymExprPtr& lhs, const SymExprPtr& rhs,
CompareSymExpr::Op op) {
CompareKey key(op, lhs->getId(), rhs->getId());
auto it = compareCache_.find(key);
if (it != compareCache_.end())
return it->second;
auto expr = std::make_shared<CompareSymExpr>(lhs, rhs, op);
compareCache_[key] = expr;
return expr;
}
// 创建比较表达式(用于条件判断)
SymExprPtr createCompare(const CompareSymExprPtr& cond, bool isTrue) {
return createCompare(cond, isTrue ? createConst(1) : createConst(0),
CompareSymExprOp::Eq);
}
// 创建指针表达式
LocSymExprPtr createLoc(
const MemoryRegionPtr& region,
LocSymExpr::State state = LocSymExpr::State::CONFIRMED) {
if (!region) {
// 返回空指针
return std::make_shared<LocSymExpr>(nullptr);
}
auto it = locCache_.find(region.get());
if (it != locCache_.end())
return it->second;
auto expr = std::make_shared<LocSymExpr>(region, state);
locCache_[region.get()] = expr;
return expr;
}
private:
using BinaryKey = std::tuple<BinarySymExpr::Op, IdType, IdType>;
using CompareKey = std::tuple<CompareSymExpr::Op, IdType, IdType>;
// 传入不能化简的lhs和rhs,缓存或拿取BinarySym
SymExprPtr getBinary(const SymExprPtr& lhs, const SymExprPtr& rhs,
BinarySymExpr::Op op) {
BinaryKey key(op, lhs->getId(), rhs->getId());
auto it = binaryCache_.find(key);
if (it != binaryCache_.end())
return it->second;
// 如果没有找到,创建一个新的BinarySymExpr
auto expr = std::make_shared<BinarySymExpr>(lhs, rhs, op);
binaryCache_[key] = expr;
return expr;
}
// 化简辅助函数
static ConstType BinTwoConst(ConstType lhs, ConstType rhs,
BinarySymExpr::Op op);
SymExprPtr BinOneConst(const SymExprPtr& lhs, ConstType rhs,
BinarySymExpr::Op op);
SymExprPtr BinNoConst(const SymExprPtr& lhs, const SymExprPtr& rhs,
BinarySymExpr::Op op);
SymExprManager() {}
std::unordered_map<ConstType, SymExprPtr> constCache_;
std::unordered_map<BinaryKey, SymExprPtr> binaryCache_;
std::unordered_map<CompareKey, SymExprPtr> compareCache_;
std::unordered_map<MemoryRegion*, LocSymExprPtr> locCache_;
};以及化简机制:
inline ConstType SymExprManager::BinTwoConst(ConstType lhs, ConstType rhs,
BinarySymExpr::Op op) {
switch (op) {
case BinarySymExpr::Op::Add:
return lhs + rhs;
case BinarySymExpr::Op::Sub:
return lhs - rhs;
case BinarySymExpr::Op::Mul:
return lhs * rhs;
case BinarySymExpr::Op::Div:
if (rhs == 0) {
// 除以零的情况,返回一个未知的表达式
return 0;
}
return lhs / rhs;
case BinarySymExpr::Op::And:
return lhs & rhs;
case BinarySymExpr::Op::Or:
return lhs | rhs;
case BinarySymExpr::Op::Xor:
return lhs ^ rhs;
case BinarySymExpr::Op::Shl:
return lhs << rhs;
case BinarySymExpr::Op::AShr:
case BinarySymExpr::Op::LShr:
return lhs >> rhs;
case BinarySymExpr::Op::Mod:
if (rhs == 0) {
// 除以零的情况,返回一个未知的表达式
return 0;
}
return lhs % rhs;
default:
return 0;
}
}
SymExprPtr SymExprManager::BinOneConst(const SymExprPtr& lhs, ConstType rhs,
BinarySymExpr::Op op) {
// 首先检查 lhs 是否为BinarySymExpr,以及是否能合并其右表达式
SymExprPtr new_lhs = lhs;
ConstType new_rhs = rhs;
if (lhs->getKind() == SymExpr::Kind::Binary) {
auto binary_expr = std::static_pointer_cast<BinarySymExpr>(lhs);
if (binary_expr->getRhs()->getKind() == SymExpr::Kind::Const) {
auto bin_op = binary_expr->getOp();
auto bin_rhs =
std::static_pointer_cast<ConstSymExpr>(binary_expr->getRhs())
->getValue();
// 目前只考虑相同操作符合并
if (bin_op == op) {
switch (op) {
case BinarySymExpr::Op::Add: // a + 1 + 2 = a + 3
case BinarySymExpr::Op::Sub: // a - 1 - 2 = a - 3
case BinarySymExpr::Op::Shl: // a << 1 << 2 = a << 3
case BinarySymExpr::Op::LShr: // a >> 1 >> 2 = a >> 3
case BinarySymExpr::Op::AShr:
new_rhs += bin_rhs;
break;
case BinarySymExpr::Op::Mul: // a * 2 * 3 = a * 6
case BinarySymExpr::Op::Div: // a / 2 / 3 = a / 6
new_rhs *= bin_rhs;
break;
case BinarySymExpr::Op::And: // a & 1 & 2 = a & (1 & 2)
new_rhs &= bin_rhs;
break;
case BinarySymExpr::Op::Or: // a | 1 | 2 = a | (1 | 2)
new_rhs |= bin_rhs; // 合并或操作
break;
case BinarySymExpr::Op::Xor: // a ^ 1 ^ 2 = a ^ (1 ^ 2)
new_rhs ^= bin_rhs; // 合并异或操作
break;
default:
goto skip; // 其他操作不处理
}
new_lhs = binary_expr->getLhs(); // 更新左表达式
}
}
}
skip:
// 检查吸收性
switch (op) {
case BinarySymExpr::Op::Add:
case BinarySymExpr::Op::Sub: {
if (rhs == 0) {
return new_lhs; // a - 0 = a
}
break;
}
case BinarySymExpr::Op::Mul: {
if (rhs == 1) {
return new_lhs; // a * 1 = a
}
if (rhs == 0) {
return createConst(0); // a * 0 = 0
}
break;
}
case BinarySymExpr::Op::Div: {
if (rhs == 1) {
return new_lhs; // a / 1 = a
}
if (rhs == 0) {
// 除以零的情况,返回一个未知的表达式
return nullptr;
}
break;
}
default:
break;
}
// 如果没有常量折叠或吸收,返回新的二元表达式
return getBinary(new_lhs, createConst(new_rhs), op);
}
SymExprPtr SymExprManager::BinNoConst(const SymExprPtr& lhs,
const SymExprPtr& rhs,
BinarySymExpr::Op op) {
// 直接缓存或创建新的二元表达式
return getBinary(lhs, rhs, op);
}
SymExprPtr SymExprManager::createBinary(const SymExprPtr& lhs,
const SymExprPtr& rhs,
BinarySymExpr::Op op) {
// 首先尝试化简
bool lhs_is_const = lhs->getKind() == SymExpr::Kind::Const;
bool rhs_is_const = rhs->getKind() == SymExpr::Kind::Const;
if (lhs_is_const && rhs_is_const) {
// 两个都是常量,直接计算结果
auto lhs_value =
std::static_pointer_cast<ConstSymExpr>(lhs)->getValue();
auto rhs_value =
std::static_pointer_cast<ConstSymExpr>(rhs)->getValue();
auto result_value = BinTwoConst(lhs_value, rhs_value, op);
return createConst(result_value);
}
if (lhs_is_const) {
// lhs 是常量,rhs 不是
if (op == BinarySymExpr::Op::Add || op == BinarySymExpr::Op::Mul) {
// 对于加法和乘法,交换顺序使常量在右边
return BinOneConst(
rhs, std::static_pointer_cast<ConstSymExpr>(lhs)->getValue(),
op);
}
// 对于减法和除法,保持 lhs 在左边
// 直接缓存或创建新的二元表达式
return getBinary(lhs, rhs, op);
}
if (rhs_is_const) {
// 不需要交换顺序,直接处理
return BinOneConst(
lhs, std::static_pointer_cast<ConstSymExpr>(rhs)->getValue(), op);
}
// 都不是常量,直接缓存或创建新的二元表达式
return BinNoConst(lhs, rhs, op);
}
对于问题2,我们在为符号表达式提供了上下文一致性后,我们就能为子区域提供一个类似的缓存机制,并且一切子区域的注册和访问都由子区域管理器提供,这个过程不需要访问符号求解器,大幅度优化了性能表现。
class MemRegionManager {
public:
static MemRegionManager& getInstance() {
static MemRegionManager instance;
return instance;
}
MemRegionManager(const MemRegionManager&) = delete;
MemRegionManager& operator=(const MemRegionManager&) = delete;
MemRegionManager(MemRegionManager&&) = delete;
MemRegionManager& operator=(MemRegionManager&&) = delete;
// 创建变量区域
VarRegionPtr createVarRegion(const llvm::Value* value,
const SymExprPtr& size = nullptr) {
auto region = std::make_shared<VarRegion>(value, size);
return region;
}
// 创建全局变量区域
GlobalRegionPtr createGlobalRegion(const llvm::GlobalVariable* global,
const SymExprPtr& size = nullptr) {
auto it = globalRegions_.find(global);
if (it != globalRegions_.end())
return it->second;
auto region = std::make_shared<GlobalRegion>(global, size);
globalRegions_[global] = region;
return region;
}
// 创建函数区域
FunctionRegionPtr createFunctionRegion(const llvm::Function* func,
const SymExprPtr& size = nullptr) {
auto it = functionRegions_.find(func);
if (it != functionRegions_.end())
return it->second;
auto region = std::make_shared<FunctionRegion>(func, size);
functionRegions_[func] = region;
return region;
}
// 创建符号化区域
SymbolicRegionPtr createSymbolicRegion(const llvm::Instruction* inst,
const SymExprPtr& size = nullptr) {
auto region = std::make_shared<SymbolicRegion>(inst, size);
return region;
}
// 创建元素区域
ElementRegionPtr createElementRegion(const MemoryRegionPtr& base,
const SymExprPtr& offset,
const SymExprPtr& size = nullptr) {
RegionOffset key(base, offset);
auto it = elementRegions_.find(key);
if (it != elementRegions_.end())
return it->second;
auto region = std::make_shared<ElementRegion>(base, offset, size);
elementRegions_[key] = region;
return region;
}
private:
MemRegionManager() = default;
// caches
std::unordered_map<const llvm::GlobalVariable*, GlobalRegionPtr>
globalRegions_;
std::unordered_map<const llvm::Function*, FunctionRegionPtr>
functionRegions_;
std::unordered_map<RegionOffset, ElementRegionPtr> elementRegions_;
};
初赛中,我们的约束求解器在处理复杂表达式时,往往会生成大量的中间约束,这些约束不仅数量庞大,而且相互之间存在着复杂的依赖关系。这导致求解器在进行约束求解时,面临着巨大的性能压力。同时我们的每一个状态都保存着一整份约束链,在进行程序状态的分裂和添加新约束时,都存在性能瓶颈。
为了解决这个问题,我们需要对约束求解的过程进行优化,主要从以下几个方面入手:
- 约束简化:通过分析约束之间的关系,尽可能地简化约束,减少不必要的约束生成。同时尽量减少程序状态携带的约束节点数量,通过约束链把约束组织成树的形式统一求解。
- 增量求解:对于动态变化的约束,采用增量求解的方式,避免重复求解已经解决的约束。
为了解决这个问题,我们首先让状态中只携带某个约束节点的头,从而可以把约束组织成一棵树,从根到状态节点的一条路径就是该状态的全部约束。这样,在进行程序状态的分裂和添加新约束时,只需要在分裂前的节点后添加一个新的约束节点,达成O(1)的约束添加。同时这样的约束组织也为后续实现增量约束打下基础。
struct ConstraintNode {
const SymExprPtr constraint;
const std::shared_ptr<ConstraintNode> parent;
int depth = 0;
ConstraintNode(const SymExprPtr& constraint,
const std::shared_ptr<ConstraintNode>& parent)
: constraint(constraint),
parent(parent),
depth(parent ? parent->depth + 1 : 1) {}
};
using ConstraintNodePtr = std::shared_ptr<ConstraintNode>;
在把约束节点组织成树后,为了避免每次状态切换时需要花费高额代价重新求解重复约束,我们构建了一套增量约束求解体系,其核心在于使用Z3 Solver的push和pop方法,通过在状态切换时保存和恢复求解上下文,实现对约束的增量求解。具体来说,当我们需要切换到一个新的状态时,可以先调用push方法保存当前的求解上下文,然后在新的状态中添加约束,最后再调用pop方法恢复到之前的上下文。这样,我们就可以在不重新求解的情况下,快速切换状态并应用新的约束。
而由于我们已经把约束组织成树,上述过程便可以抽象为在一棵动态变化的树上寻找最近公共祖先(LCA)的过程。由于约束深度通常不会很高,我们选择使用常量时间复杂度更低的双指针法来进行LCA问题的求解。(若面对约束深度非常高的程序,可以考虑使用具有更好的时间复杂度的LCT算法)。
void Solver::synTo(const std::shared_ptr<ConstraintNode>& targetHead) {
if (head_ == targetHead) {
return; // 已经同步到目标节点
}
if (head_ == nullptr) {
// 如果当前头节点为空,直接推到目标节点
std::vector<ConstraintNodePtr> target_path;
for (auto node = targetHead; node; node = node->parent) {
target_path.push_back(node);
}
std::ranges::reverse(target_path);
for (const auto& node : target_path) {
solver_.push();
LOG_INFO("[synTo] : Pushing node: {}",
node->constraint->toString());
solver_.add(symbolCache_.getZ3Expr(node->constraint));
}
head_ = targetHead;
return;
}
if (targetHead == nullptr) {
// pop
unsigned int pops_count = head_->depth;
LOG_INFO("[synTo] : Popping {} nodes to sync to nullptr", pops_count);
solver_.pop(pops_count);
head_ = nullptr;
return;
}
// 都不空
std::vector<ConstraintNodePtr> target_path;
int pop_count = 0;
ConstraintNodePtr current = head_;
ConstraintNodePtr target = targetHead;
int current_depth = head_->depth;
int target_depth = targetHead->depth;
// 先让当前头节点和目标头节点的深度一致
while (target_depth > current_depth) {
target_path.push_back(target);
target = target->parent;
target_depth--;
}
while (current_depth > target_depth) {
pop_count++;
current = current->parent;
current_depth--;
}
while (current != target) {
target_path.push_back(target);
pop_count++;
target = target->parent;
current = current->parent;
}
if (pop_count > 0) {
LOG_INFO("[synTo] : Popping {} nodes to sync to target", pop_count);
solver_.pop(pop_count);
}
// push到目标节点
std::ranges::reverse(target_path);
for (const auto& node : target_path) {
solver_.push();
LOG_INFO("[synTo] : Pushing node: {}", node->constraint->toString());
solver_.add(symbolCache_.getZ3Expr(node->constraint));
}
// 更新头节点
head_ = targetHead;
}
bool Solver::checkFeasible() {
return solver_.check() == z3::check_result::sat;
}
bool Solver::checkFeasible(const SymExprPtr& expr) {
solver_.push();
solver_.add(symbolCache_.getZ3Expr(expr));
z3::check_result result = solver_.check();
solver_.pop();
return result == z3::check_result::sat;
}在符号执行分析中,我们需要使用某种算法来指引符号引擎进行路径探索,常见的算法有BFS和DFS,但是BFS算法虽然能够找到最短路径,但在状态空间较大时,可能会导致内存消耗过大。而DFS算法虽然在内存使用上更为高效,但容易陷入某些深度较大的路径,重复探索相似路径从而错过更多可能的路径。为了应对路径爆炸,我们必须找到某种能在短时间内达到收敛的路径探索策略,并且这个策略既要高效,又要能适配之前符号引擎的减枝优化。
因此,我们选择参考MPC(Minimum Path Cover)算法,并开创新结合启发式路径探索策略,从而达到在短时间,短路径内探索更多的基本块,达成更快的收敛速度。
在原始MPC算法中,我们需要把包含循环回边和复杂函数调用原始CFG(iCFG)转换为不含有循环边的DAG,然后再在DAG上应用MPC算法,找到最小覆盖路径,然后在符号执行中跟随最小覆盖路径进行符号执行。
其中,转换CFG的步骤分为构建循环子图与函数子图两步。
- 转换函数子图,我们需要收集函数中的所有退出点,并且添加一个虚拟退出节点作为他们的后继,这个虚拟节点指向其原有后继。这样我们便构造出来了一份该函数专属的单入口单出口子图,我们可以在这份子图上独立进行启发式MPC路径搜索。
- 转换循环子图,转换循环子图在某种程度上类似于转换函数子图,我们把循环看成一个独立的子图,为循环添加虚拟占位符,收集所有循环退出节点并添加虚拟退出节点,移除循环回边。
于是我们便有了以下代码:
class MPCWorkList {
public:
using value_type = WorkItem;
// 优先级定义(数值越小优先级越高)
enum class Priority : int {
SUBGRAPH = 0, // 子图内未访问块(最高优先级)
CRITICAL = 1, // 全局未访问块
HIGH = 2, // 访问次数 < 3
MEDIUM = 3, // 访问次数 < 10
LOW = 4, // 其他
NUMTIERS = 5
};
explicit MPCWorkList(llvm::Function& F) {
// 在构造函数中分析函数,构建子图
analyzeFunction(F);
}
void push(const WorkItem& item) {
Priority tier = calculateTier(item);
tiers_[static_cast<int>(tier)].push_back(item);
markBasicBlock(item.getBB());
}
value_type pop() {
// 优先处理子图内未访问块
for (int i = 0; i < static_cast<int>(Priority::NUMTIERS); ++i) {
if (!tiers_[i].empty()) {
auto item = std::move(tiers_[i].back());
tiers_[i].pop_back();
// 更新访问统计
incrementVisitCount(item.getBB());
return item;
}
}
return {};
}
template <typename... Args>
void emplace(Args&&... args) {
push(WorkItem(std::forward<Args>(args)...));
}
bool empty() const {
return std::ranges::all_of(
tiers_.begin(), tiers_.end(),
[](const auto& tier) { return tier.empty(); });
}
void clear() {
for (auto& tier : tiers_) {
tier.clear();
}
visitCount_.clear();
visitedBlocks_.clear();
subgraphs_.clear();
}
// 覆盖率统计
size_t getCoveredBlocks() const { return visitedBlocks_.size(); }
bool is_all_covered() const {
return tiers_[0].empty() && tiers_[1].empty();
}
private:
std::array<std::deque<WorkItem>, static_cast<int>(Priority::NUMTIERS)>
tiers_;
std::unordered_map<llvm::BasicBlock*, int> visitCount_;
std::unordered_set<llvm::BasicBlock*> visitedBlocks_;
// 子图相关数据结构
std::vector<SubgraphDescriptor> subgraphs_;
std::unordered_map<llvm::BasicBlock*, SubgraphDescriptor*> subgraphMap_;
// 函数分析:识别循环和函数子图
void analyzeFunction(llvm::Function& F) {
// 1. 构建函数子图
SubgraphDescriptor func_sg;
func_sg.type = SubgraphDescriptor::Type::FUNCTION;
func_sg.entry = &F.getEntryBlock();
// 创建虚拟返回块
func_sg.virtualExit = llvm::BasicBlock::Create(
F.getContext(), F.getName() + ".virtual_return", &F);
llvm::BasicBlock::Create(F.getContext(), "ret", &F);
// 收集函数体基本块
for (llvm::BasicBlock& bb : F) {
if (&bb != func_sg.virtualExit) {
func_sg.blocks.insert(&bb);
}
}
subgraphs_.push_back(func_sg);
// 2. 分析循环
auto& li = getLoopInfo(&F);
for (llvm::Loop* l : li) {
buildLoopSubgraph(*l, F);
}
// 3. 建立基本块到子图的映射
for (auto& sg : subgraphs_) {
for (auto* bb : sg.blocks) {
subgraphMap_[bb] = &sg;
}
}
}
// 构建循环子图
void buildLoopSubgraph(llvm::Loop& L, llvm::Function& F) {
SubgraphDescriptor loop_sg;
loop_sg.type = SubgraphDescriptor::Type::LOOP;
loop_sg.entry = L.getHeader();
// 创建虚拟出口块
loop_sg.virtualExit = llvm::BasicBlock::Create(
F.getContext(), loop_sg.entry->getName() + ".loop_exit", &F);
llvm::BranchInst::Create(L.getExitBlock(), loop_sg.virtualExit);
// 收集循环体基本块
for (llvm::BasicBlock* bb : L.blocks()) {
loop_sg.blocks.insert(bb);
}
// 重定向退出边
llvm::SmallVector<llvm::BasicBlock*, 4> exits;
L.getExitingBlocks(exits);
for (llvm::BasicBlock* eb : exits) {
llvm::Instruction* term = eb->getTerminator();
for (unsigned i = 0; i < term->getNumSuccessors(); ++i) {
if (!L.contains(term->getSuccessor(i))) {
term->setSuccessor(i, loop_sg.virtualExit);
}
}
}
subgraphs_.push_back(loop_sg);
}
// 计算工作项优先级
Priority calculateTier(const WorkItem& item) const {
auto* bb = item.getBB();
// 检查是否在子图内且未访问
if (auto it = subgraphMap_.find(bb); it != subgraphMap_.end()) {
const SubgraphDescriptor* sg = it->second;
// 如果是子图入口,给予最高优先级
if (bb == sg->entry && !visitedBlocks_.contains(bb)) {
return Priority::SUBGRAPH;
}
// 检查子图内是否有未访问块
bool has_unvisited_in_subgraph = false;
for (auto* sg_bb : sg->blocks) {
if (!visitedBlocks_.contains(sg_bb)) {
has_unvisited_in_subgraph = true;
break;
}
}
// 子图内有未访问块且当前块未访问
if (has_unvisited_in_subgraph && !visitedBlocks_.contains(bb)) {
return Priority::SUBGRAPH;
}
}
// 全局未访问块
if (!visitedBlocks_.contains(bb)) {
return Priority::CRITICAL;
}
// 根据访问次数分层
auto it = visitCount_.find(bb);
int count = (it != visitCount_.end()) ? it->second : 0;
if (count < 3)
return Priority::HIGH;
if (count < 10)
return Priority::MEDIUM;
return Priority::LOW;
}
void markBasicBlock(llvm::BasicBlock* bb) {
if (visitedBlocks_.find(bb) == visitedBlocks_.end()) {
visitedBlocks_.insert(bb);
}
}
void incrementVisitCount(llvm::BasicBlock* bb) { visitCount_[bb]++; }
};
过程间分析作为符号执行中必不可少的一部分,学界一直在研究如何优化函数调用,由于我们需要高召回率,我们选择对所有用户定义的函数采用内联分析,对所有库函数和系统函数使用摘要分析,而如何优化内联分析便成了第一要务。我们在对初赛时静态分析器进行性能分析时发现,随着函数不断被内联,程序状态中的符号数目也在不断膨胀,其中包含了许多后续不会再使用到的符号。
针对这一问题,我们仿照现实中函数调用的过程,采用了*分层式状态(Layered State)或写时复制(Copy-on-Write)*技术。当进行函数调用时,我们不是完全复制整个状态,而是:
- 创建子状态:为被调用函数(callee)创建一个新的程序状态,这个新状态持有一个指向其父状态(caller's state)的指针 parent。
- 本地修改:在被调用函数内部的所有状态修改(如变量绑定、内存写入)都只发生在子状态的 Env 和 Store 中。
- 链式查找:当需要读取一个值时(envLookup, storeLookup),系统会先在当前子状态查找。如果找不到,则通过 parent指针递归地向父状态查找。
- 副作用传播:当函数返回时,必须将对调用者可见的修改(即副作用)从子状态合并回父状态。你通过*标记逃逸(Marking Escaping)*的内存区域来识别这些副作用。
void TransferVisitor::visitCallInst(llvm::CallInst& i) {
...
// 创建一个全新的空状态
auto callee_state = std::make_shared<ProgramState>(ps_);
// 2) 绑定参数
unsigned idx = 0;
for (auto& formal : callee->args()) {
auto* actual = call->getArgOperand(idx++);
// 把父状态的实际参数绑定为子状态的实际参数
auto actual_expr = ps_->value2SymExprOrNew(actual);
callee_state->envBind(&formal, actual_expr);
// 递归检测是否为指针
while (actual_expr->is<LocSymExpr>()) {
auto loc_expr = std::static_pointer_cast<LocSymExpr>(actual_expr);
auto region = loc_expr->getRegion();
if (region) {
callee_state->markEscaping(region);
actual_expr = ps_->storeLookup(region);
if (!actual_expr)
break;
} else
break;
}
}
...
}当分析器遇到 ret 指令时,执行以下操作:
- 弹出调用帧:从当前调用栈中弹出最顶层的 CallFrame,获取返回所需的所有上下文信息。
- 清理死符号:这是返回过程的核心步骤之一。在合并回父状态之前,丢弃所有对父状态不可见的、纯粹属于被调用函数的局部信息,以减小状态的复杂度。标记所有“活”的内存区域,规则如下:
a. 全局变量区域是活的。
b. 返回值(如果是个指针)指向的区域是活的。
c. 所有之前被标记为 escaped 的区域是活的。
d. 所有从“活”区域可以递归访问到的子区域也是活的。 3. 合并到父状态 :把"活"下来的状态和符号合并到父状态中,并作为一个新状态返回。 4. 绑定返回值:向新状态中绑定函数返回值
void TransferVisitor::visitReturnInst(llvm::ReturnInst& i) {
auto* ret = &i;
ProgramState::ProgramStatePtr ps = currentItem_->state;
if (currentItem_->callStack.empty()) {
return;
}
// 1) 弹出调用帧
auto frame = currentItem_->callStack.back();
// 2) 获取返回值
auto* ret_value = ret->getReturnValue();
ps->cleanDeadSymbols(ret_value);
if (!frame.callInst) {
// 从初始函数中返回
PtrChecker::checkMemoryLeakForMain(*ps, currentItem_->getCallStack());
return;
}
PtrChecker::checkMemoryLeak(*ps, currentItem_->getCallStack());
currentItem_->callStack.pop_back();
auto new_state = ps->mergeToParent();
if (ret_value) {
new_state->envBind(frame.callInst, ps->value2SymExpr(ret_value));
}
// 4) 恢复状态
exprEngine_.addWork(new_state, currentItem_->callStack, frame.callState);
}
在这个过程中,如何正确的处理清理死符号这一步便成了整个流程的核心,如果清理多了会导致符号执行分不准确,如果清理少了则会导致状态继续膨胀。我们选择使用逃逸传递算法来处理需要保留的符号,具体来说,我们会在每个状态中维护一个逃逸标记,当一个符号被标记为逃逸时,我们会将其对应的内存区域标记为活跃,并在后续的状态合并中保留这些符号。
void ProgramState::cleanDeadSymbols(llvm::Value* ret) {
// 1. 全局变量 对全局变量的store都是应该保留的,并且,如果把指针存到全局变量里了,指针指向的内存也逃逸
for (const auto& [dest_region, src_sym] : store_.getMap()) {
if (dest_region && dest_region->is<MemoryRegion::Kind::Global>()) {
// 如果store的是全局变量
markEscaping(dest_region);
if (src_sym->is<LocSymExpr>()) {
// 如果是指针,标记其区域为逃逸
auto loc_sym = std::static_pointer_cast<LocSymExpr>(src_sym);
if (loc_sym->getRegion()) {
markEscaping(loc_sym->getRegion());
}
}
}
}
// 2. 函数返回值
if (ret) {
auto ret_sym = envLookup(ret);
if (ret_sym && ret_sym->is<LocSymExpr>()) {
auto loc_sym = std::static_pointer_cast<LocSymExpr>(ret_sym);
if (loc_sym->getRegion()) {
markEscaping(loc_sym->getRegion());
}
}
}
// 3. 递归标记所有内存区域
std::unordered_map<MemoryRegionPtr, std::vector<MemoryRegionPtr>>
subregion_map;
// 构建子区域map来获得高校的子区域查询
for (const auto& [mem, sym] : store_.getMap()) {
if (sym && sym->is<LocSymExpr>()) {
auto loc_sym = std::static_pointer_cast<LocSymExpr>(sym);
if (loc_sym->getRegion()) {
subregion_map[loc_sym->getRegion()].push_back(mem);
}
}
}
std::unordered_set<MemoryRegionPtr> live_regions;
for (const auto& m : escapedMemRegions_) {
// 递归标记所有区域
markReachableRegions(m, subregion_map, live_regions);
}
auto& store_map = store_.getMap();
for (auto it = store_map.begin(); it != store_map.end();) {
if (live_regions.contains(it->first)) {
++it; // 保留活跃区域
} else {
it = store_map.erase(it); // 删除死区域
}
}
for (const auto& region : live_regions) {
markEscaping(region);
}
}
void ProgramState ::markReachableRegions(
const MemoryRegionPtr& region,
std::unordered_map<MemoryRegionPtr, std::vector<MemoryRegionPtr>>&
subregion_map,
std::unordered_set<MemoryRegionPtr>& live_regions) {
// 如果已经标记过,直接返回
if (!region || live_regions.contains(region))
return;
// 标记当前区域为活跃
live_regions.insert(region);
LOG_INFO("Marking region {} as reachable", region->toString());
// 递归标记所有子区域
auto it = subregion_map.find(region);
if (it != subregion_map.end()) {
for (const auto& sub_region : it->second) {
markReachableRegions(sub_region, subregion_map, live_regions);
}
}
auto sym = storeLookup(region);
if (!sym)
return;
// 如果是LocSymExpr,标记其区域为逃逸
if (sym->is<LocSymExpr>()) {
auto loc_sym = std::static_pointer_cast<LocSymExpr>(sym);
if (loc_sym->getRegion()) {
markReachableRegions(loc_sym->getRegion(), subregion_map,
live_regions);
}
}
}
在实现了分层调用与写时复制优化后,静态分析器面对大程序的性能突飞猛进,极大地降低了符号执行过程中累计的符号数目,减少内存和时间开销。相关证明将在系统测试环节详细说明。
我们通过构造prompt的方式,将符号执行分析器的结果与大模型进行交互,利用大模型的强大推理能力来辅助符号执行分析。具体来说,我们将符号执行分析器的状态、路径信息以及相关符号表达式等数据转换为自然语言描述,并将其作为输入传递给大模型。大模型可以根据这些信息生成更为复杂的符号表达式、路径探索策略或其他有助于符号执行的建议。
# 使用llm工具分析过滤静态检测器报告中的bug
import os
import json
from openai import OpenAI
import argparse
API_KEY = os.getenv("API_KEY")
if not API_KEY:
raise ValueError("API_KEY environment variable not set")
# 暂时使用deepseek v1
BASE_URL = "https://api.deepseek.com/v1"
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
def build_prompt_for_function(function_data, enable_stack=False):
"""为一个函数的所有issue构建一个组合的Prompt"""
# --- 新增:定义每个错误类型最多展示多少个调用栈示例 ---
issues_list_str = ""
MAX_CALL_STACKS_PER_ERROR = 3
# 遍历每个有问题的指令
for i, instruction_issue in enumerate(function_data.get("issues", [])):
error_details = (
{}
) # 格式: { "ErrorType": {("stack_frame1", "stack_frame2"), ...} }
for error_instance in instruction_issue.get("issues", []):
error_type = error_instance.get("error_type")
if not error_type:
continue
# 初始化该错误类型的集合(如果不存在)
if error_type not in error_details:
error_details[error_type] = set()
# 添加调用栈(如果存在)。必须转换为tuple才能添加到set中
if enable_stack:
call_stack = error_instance.get("call_stack")
if call_stack:
error_details[error_type].add(tuple(call_stack))
if not error_details:
continue
issue_details_str = ""
for error_type, call_stacks_set in sorted(error_details.items()):
issue_details_str += f"\n - Error Type: {error_type}"
if not call_stacks_set:
continue
# 限制调用栈示例的数量
limited_stacks = list(call_stacks_set)[:MAX_CALL_STACKS_PER_ERROR]
issue_details_str += (
f"\n - Call Stack Examples (up to {MAX_CALL_STACKS_PER_ERROR}):"
)
for stack_num, stack in enumerate(limited_stacks, 1):
# 将调用栈格式化为 "main -> func_a -> func_b" 的形式
stack_str = " -> ".join(
[frame.split("//")[0].strip() for frame in stack]
)
issue_details_str += f"\n {stack_num}. {stack_str}"
issues_list_str += f"""
- Issue #{i+1}:
- Problematic Source Code Line: "{instruction_issue.get('source_code', 'N/A')}"
- Corresponding LLVM IR: "{instruction_issue.get('instruction', 'N/A')}"
- Details:{issue_details_str}
"""
# --- FIX END ---
if not issues_list_str:
return None
prompt_template = f"""
**Role:** You are an expert C/C++ static analysis verification engine. Your primary function is to determine path feasibility for potential memory errors.
**Analysis Target:**
- **Function Name:** `{function_data['function_name']}`
**Full Function Source Code (for context):**
```c
{function_data['source_code']}
```
**Reported Issues to Analyze:**
{issues_list_str}
**Your Task:**
For **each issue** listed above, analyze the function's source code to determine if there exists **at least one feasible execution path** that leads to the identified memory error.
1. Trace the control flow, variable states, and pointer values from the function's entry point.
2. For each issue, conclude whether a path exists that causes the error. Consider variable constraints, loop conditions, and function call outcomes.
3. Provide your analysis for all issues in a single structured JSON object. The root object must have a key "analysis_results", which is an array of objects. Each object must correspond to one issue.
**Output JSON Format:**
{{
"analysis_results": [
{{
"issue_number": 1,
"path_exists": boolean,
"confidence": "High" | "Medium" | "Low",
"reasoning": "A concise explanation for issue #1. Explain why a feasible path does or does not exist. If it exists, briefly describe the conditions required to trigger it."
}},
{{
"issue_number": 2,
"path_exists": boolean,
"confidence": "High" | "Medium" | "Low",
"reasoning": "A concise explanation for issue #2."
}}
]
}}
"""
# print(prompt_template)
return prompt_template
def analyze_issues_with_llm(prompt):
"""调用 DeepSeek API 进行分析"""
try:
response = client.chat.completions.create(
model="deepseek-coder", # 或者使用 deepseek-chat
messages=[
{
"role": "system",
"content": "You are an expert C/C++ security code reviewer. Respond in the requested JSON format.",
},
{"role": "user", "content": prompt},
],
max_tokens=1500, # 增加token数量以容纳多个问题的分析
temperature=0.1, # 低温以获得更确定的、可复现的输出
response_format={"type": "json_object"}, # 请求JSON格式输出
)
# 解析返回的JSON字符串
analysis_result = json.loads(response.choices[0].message.content)
return analysis_result
except Exception as e:
print(f"Error calling LLM API: {e}")
return {"analysis_results": []}
def main():
# 读取json地址和需要分析的函数名称(如果没有则默认全部)
parser = argparse.ArgumentParser(description="Analyze C/C++ code issues using LLM.")
parser.add_argument("json_report_file", help="Path to the JSON report file")
parser.add_argument(
"--function_name", help="Name of the function to analyze (default: all)"
)
parser.add_argument(
"--stack",
action="store_true",
help="Whether to add call stack info in the prompt",
)
args = parser.parse_args()
enable_stack = False
# 处理命令行参数
if args.stack:
print("Adding call stack information to the prompt.")
enable_stack = True
# 读取JSON报告
with open(args.json_report_file, "r") as f:
json_report = json.load(f)
# 分析每个函数
for func_name, function_data in json_report.items():
# 如果通过命令行指定了函数名,则只分析该函数
if args.function_name and func_name != args.function_name:
continue
# 将函数名(字典的键)添加到函数数据中,以便 prompt 构建函数可以访问它
function_data["function_name"] = func_name
# 为整个函数构建一个Prompt
prompt = build_prompt_for_function(function_data, enable_stack)
print(f"\n--- Analyzing function: {func_name} ---")
if not prompt:
print("No issues to analyze for this function.")
continue
# 调用LLM进行分析
llm_analysis = analyze_issues_with_llm(prompt)
# 处理并打印结果
if not llm_analysis.get("analysis_results"):
print(" LLM analysis did not return valid results.")
continue
for result in llm_analysis.get("analysis_results", []):
issue_index = result.get("issue_number", 0) - 1
if 0 <= issue_index < len(function_data.get("issues", [])):
original_issue = function_data["issues"][issue_index]
print(
f"\n Issue #{result.get('issue_number')}: {original_issue['source_code']}"
)
else:
print(
f"\n Issue #{result.get('issue_number')}: (Could not retrieve original issue details)"
)
print(
f" Verdict: {'TRUE POSITIVE' if result.get('is_true_positive') else 'FALSE POSITIVE/UNSURE'}"
)
print(f" Confidence: {result.get('confidence')}")
print(f" Reasoning: {result.get('reasoning')}")
print("-" * 50)
if __name__ == "__main__":
main()