Tips & Tricks

努力获得更好的 C++ 代码,第一部分:数据流分析基础知识

Read this post in other languages:

CLion 提供了一个内置数据流分析器,它会在您编写代码时持续运行并帮助提高代码质量。 它可以显示各种代码问题,这些问题随后可能导致运行时问题、安全漏洞和其他漏洞。 举例来说,这些有用的检查包括常量条件、死代码、null 指针取消引用、内存泄漏和数组索引问题。 我们将发布一系列博文来说明 CLion 中某些检查的运作方式。

今天,我们将介绍数据流分析的基础知识,包括它的一般运作方式,同时介绍几个真实的例子,以帮助您编写出更好的代码。

控制流图

所有数据流检查都依赖于控制流图。 它是一个图表,其中顶点是程序中的语句,边是这些语句之间的控制流跳转(直接代码执行、条件跳转、循环、中断、转到等)。

例如,右侧的控制流图表示左侧的 foo 函数:

基本示例

CLion 会为每个函数构建相应的图。 每个图都有一个开始节点和一个退出节点,它们对应于函数的入口和出口。 通过访问从开始节点到退出节点的图节点,CLion 可以收集一些有价值的信息。

例如,CLion 会记住每条语句的每个变量中可以存储哪些值。 在上述示例中,CLion 知道在节点 0 和 1 处,形参 x 始终等于 1。 这是因为函数 foo 只有一个调用点,它会传递实参中的值 1。 因此,CLion 得出结论,节点 1 处的条件 x == 1 将始终为 true,因此控制流永远不会到达节点 3。 在节点 4 处,变量 y 只能保存值 2,因为控制流只能来自节点 2,永远不会来自节点 3。 因此,CLion 得出结论:

  1. 函数 foo 始终返回值 2
  2. 条件 x == 1 始终为 true
  3. 语句 y = 3 永远无法访问

现在,我们来看一个更复杂的示例:

带有控制图的更复杂示例

这里,我们有两个 if 块,第一个块的执行方式会影响第二个块的功能。 为了支持这种评估,CLion 将 if 语句的 exit 语句拆分为两个不同的上下文:

拆分上下文

控制流图的后续节点重复。 它们出现两次,第一次用于 if 语句的 Then 分支,第二次用于 Else 分支。 在第一个“克隆”变量中,x 保存值 1(因为它对应于 if 语句的正分支),y 保存值 2(它存储在节点 2 中)。 在第二个“克隆”变量中,x ! = 1y3

第二个条件 x == 1,对应于两个克隆的节点 45。 在节点 4 处,条件始终保存 true,因为 x == 1。 同时,在节点 5 处,它始终为 false。 因此,节点 810 永远无法访问,条件 y == 2 只有一个可访问的克隆节点,即节点 9。 在此节点处,y ! = 2,因此,此条件始终为 false。

数据流分析的实际运作情况

我们来看看这些技术如何帮助 CLion 在 C++ 程序中发现细微的 bug! 我们决定分析 Z3 定理证明器,以下是我们在 CLion 中的数据流分析结果。

这里,变量 u 被初始化为 null_lpvar,然后可能被重新指定至相同的值(因为 if 条件中的 j == null_lpvar)。 因此,条件 u == null_lpvar 始终为 true。 由于这条 if 子句的 true 分支中有返回,所有后续代码都被标记为无法访问(报告为 #6951):

问题 6951

下面是另一个示例。 这里,无符号变量 i 始终等于或大于零,并且在 else 分支中,它不为零。 因此,i > 0 条件始终为 true(报告为 #6952):

问题 6952

在这篇博文中,我们介绍了我们的一项数据流检查 – 常量条件。 CLion 还提供了许多其他数据流检查,我们将在接下来的博文中介绍其中一些。

立即试用

您认为代码分析在哪些情况下有用? 请在下面的评论中与我们分享您的例子!

本博文英文原作者:

image description

Discover more