如何使用AST树或其他工具进行静态代码逻辑分析?

时间:2019-07-17 01:58:27

标签: c compiler-construction abstract-syntax-tree cppcheck

void f1(char *s)
{
 s[20] = 0;
}
void f2()
{
 char a[10];
 if (x + y == 2) {
 f1(a);
 }
}

Cppcheck将报告以下消息: 数组'a [10]'越界索引20

Cppcheck如何获得f2中的“ a”和f1中的“ s”之间的联系?

我已经建立了AST树,但是它仅提供每个符号的信息,而很少给我有关符号逻辑关系的信息。 电脑如何知道f2中的“ a”和f1中的“ s”是同一回事? 据我所知,我们必须考虑很多情况,例如:

void f1(char *s)
{
 char str_arry[30];
 s= str_arry;
 s[20] = 0;
}

在这种情况下,“ s”和“ a”不是相同的东西。

3 个答案:

答案 0 :(得分:2)

我不知道Cppcheck的工作原理如何,但我将告诉您一般如何解决此问题。相互关联的功能分析有两种主要方法。

在第一种情况下,当分析器遇到函数调用时,它将开始考虑通过函数传递的事实参数的值来分析其主体。仅当知道将哪些值传输到函数时,这种情况自然发生。这是指:一个精确值,一个范围,一组值,空/非空指针等。所传输信息的复杂性取决于分析仪的复杂程度。例如,它可以在知道两个传输的指针引用同一数组的情况下开始分析函数体。

这是一种极好的精确方法。但是有一个严重的问题。基于此概念的分析仪非常慢。他们必须一遍又一遍地分析具有不同输入数据集的功能主体。这些函数依次调用其他函数,依此类推。在某些时候,必须停止“内部”分析,这实际上使这种方法不像理论上看起来那样准确和出色。

还有第二种方法。它基于自动功能注释。问题是,当分析函数时,有关如何使用其参数以及它们不能采用的值的信息会被凝视。让我们考虑一下我在文章“ Technologies used in the PVS-Studio code analyzer for finding bugs and potential vulnerabilities”中给出的简单示例。

int Div(int X)
{
  return 10 / X;
}
void Foo()
{
  for (int i = 0; i < 5; ++i)
    Div(i);
}

分析器识别出X变量中的Div变量用作分隔符。基于此,将自动创建一个特殊的Div函数注释。然后考虑到[0..4]值的范围作为X参数传递给函数的事实。分析仪得出结论认为应该出现被零除的现象。

这种方法较为粗糙,不如第一种方法准确。但这非常快,并且可以在大量功能之间建立强大的关联,而不会降低生产力。

在实践中可能要复杂得多。例如,PVS-Studio分析仪将第二种方法用作主要方法,但并非总是如此。有时在处理模板函数时,我们会再次对其进行分析(第一种方法)。换句话说,我们使用组合的方法来维持分析的深度和速度之间的平衡。

答案 1 :(得分:1)

  

Cppcheck如何获得f2中的“ a”和f1中的“ s”之间的联系?

它们绝对不一样。可能发生以下情况之一:


您将a传递给函数,即使您使用形式参数a访问s,CPPcheck仍会记住s= str_arry; 的大小。

您必须牢记,静态分析工具和编译器的工作方式有所不同,目的不同。准确地创建了静态分析工具是为了捕获问题中出现的内容。


在第二个示例中,您具有:

s

删除a和{{1}}之间的连接。

答案 2 :(得分:1)

为了分析某些值的可能来源,一个好主意是通过在每次更改原始符号时引入一个新符号并将所有后续事件都使用该新符号来将所有变量变成不可变的(原符号不会在原始代码中重新分配它之后使用。

考虑以下代码:

// control flow block 1
int i = 1;
if (some_condition()) {
    // control flow block 2
    i = 2;
}
// control flow block 3
int j = i;

带有控制流程图

[1]
 | \     <- if (some_condition())
 |  [2]
 | /     <- join of control flow after the if block ends
[3]

您可以在控制流程图的块的入口和出口点处写出一个所有活动符号的列表(该值在控制流程图的任何位置稍后使用):

[1] entry: nothing; exit: i
[2] entry: nothing; exit: i
[3] entry: i; exit: i, j (I assume i, j are re-used after the end of this example)

请注意,[2] entry为空,因为从未读取i并将其始终写入块[2]中。这种表示方式的问题在于,i在所有块的退出列表中,但是每个块都有不同的可能值。

因此,让我们在伪代码中引入不可变符号:

// control flow block 1
i = 1;
if (some_condition()) {
    // control flow block 2
    i_1 = 2;
}
// control flow block 3
// join-logic of predecessor [1] and [2]
i_2 = one_of(i, i_1);
j = i_2;

现在,每个变量都精确耦合到其第一个(也是唯一一个)赋值。意思是,可以通过分析分配中涉及的符号来构造依赖图

i   -> i_2
i_1 -> i_2
i_2 -> j

现在,如果对j的允许值有任何约束,则静态检查器可能会要求j所有前辈(也就是i_2(依次源自ii_1)满足此要求。

对于函数调用,依赖关系图将包含从每个调用参数到函数定义中相应参数的边。

如果我们仅关注数组变量而忽略对数组内容的更改,则将此示例直接应用到示例中(我不太确定静态检查器将跟踪单个数组项的内容的程度,以便在路上发现危险):

示例1:

void f1(char *s)
{
    s[20] = 0;
}

void f2()
{
    char a[10];
    if (x + y == 2) {
        f1(a);
    }
}

转换为

f1(s)
{
    s[20] = 0;
}

f2()
{
    a = char[10];
    if (x + y == 2) {
        call f1(a);
    }
}

具有依赖图,包括通过函数调用传递的参数

a -> s

因此,很明显,对于静态分析a的安全性必须考虑使用s[20]

示例2:

void f1(char *s)
{
    char str_arry[30];
    s= str_arry;
    s[20] = 0;
}

转换为

f1(s)
{
    // control flow block 1
    str_arry = char[30];
    s_1 = str_arry;
    s_1[20] = 0;
}

具有依赖图

str_arry -> s_1

因此,很明显,对s_1[20]的安全性进行静态分析的唯一考虑因素是str_arry