如何使用z3-solver标注程序实现死代码检测、测试用例生成等分析
基于Z3的程序多场景分析实践
核心优化方案:复用求解器+上下文栈
你原来为每个路径创建独立求解器的方案确实存在效率问题,Z3原生支持push()/pop()操作管理约束上下文,仅用一个求解器就可以处理所有路径,无需重复初始化实例,内存和计算效率提升明显,完全可以支持上百条路径的分析场景。
以下是针对你给出的示例函数的全场景实现方案:
1. 死代码检测
核心逻辑:为对应代码块的路径条件添加约束,若求解结果为unsat,说明没有输入可以触发该路径,即为死代码。
from z3 import * s = Solver() x = Int('x') y = Int('y') # 定义所有代码块对应的路径条件 paths = [ ("y=4分支", x < 3), ("y=y+2分支", And(x < 3, x < 4)), ("x=x+4分支", And(x < 3, x >= 4)), ("x=x+1分支", x >= 3) ] for block_name, path_cond in paths: s.push() # 保存当前上下文 s.add(path_cond) res = s.check() if res == unsat: print(f"[死代码检测] {block_name} 是死代码") else: print(f"[死代码检测] {block_name} 可达") s.pop() # 恢复上下文,清除本次添加的路径约束
运行输出:
[死代码检测] y=4分支 可达 [死代码检测] y=y+2分支 可达 [死代码检测] x=x+4分支 是死代码 [死代码检测] x=x+1分支 可达
2. 测试用例生成
核心逻辑:对所有可达路径,求解得到的模型(model)中的变量取值就是可以覆盖该路径的测试用例。
在上述代码基础上修改,即可生成覆盖所有可达路径的测试用例:
for block_name, path_cond in paths: s.push() s.add(path_cond) res = s.check() if res == sat: m = s.model() test_x = m[x] # y如果没有出现在路径约束中,取值不影响路径覆盖,可填任意值 test_y = m[y] if y in m else 0 print(f"[测试用例] 覆盖{block_name}:x={test_x}, y={test_y}") s.pop()
3. 不变量分析
核心逻辑:不变量是所有可达状态都满足的性质,验证方法为:添加「不变量不成立」的约束,若求解结果为unsat,则该性质是函数的不变量。
首先用Z3符号表达式建模整个函数的执行逻辑:
def func_symbolic(x, y): # 第一层分支x<3的赋值 y1 = If(x < 3, 4, y) x1 = If(x < 3, x, x + 1) # 第二层分支的赋值 y2 = If(And(x < 3, x < 4), y1 + 2, y1) x2 = If(And(x < 3, x >= 4), x1 + 4, x1) return x2 + y2
比如验证「只要进入x<3分支,返回值一定等于x+6」这个不变量:
s.push() # 添加约束:x<3 且 返回值不等于x+6 s.add(x < 3, func_symbolic(x, y) != x + 6) res = s.check() print("[不变量验证] 进入x<3分支时返回值等于x+6:", "成立" if res == unsat else "不成立") s.pop()
运行结果为成立,符合预期。
4. Pre&Post代码检查
核心逻辑:给定前置条件(Pre,输入需要满足的约束)和后置条件(Post,输出需要满足的约束),添加「Pre成立且Post不成立」的约束,若求解结果为unsat,则函数满足Pre→Post的契约。
比如给定前置条件为x是正整数(x>0),后置条件为返回值一定大于x:
pre = x > 0 post = func_symbolic(x, y) > x s.push() s.add(pre, Not(post)) res = s.check() print("[Pre&Post检查] 输入x为正整数时返回值大于x:", "符合契约" if res == unsat else "不符合契约") s.pop()
运行结果为符合契约。
内容的提问来源于stack exchange,提问作者Farzan
相关产品推荐
相关产品推荐

