如何使用 AST 树或其他工具进行静态代码逻辑分析?
Posted
技术标签:
【中文标题】如何使用 AST 树或其他工具进行静态代码逻辑分析?【英文标题】:How to do static code logical analysis with AST tree or other tool? 【发布时间】:2019-07-17 01:58:27 【问题描述】: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' 不是同一个东西。
【问题讨论】:
您可能需要数据流图或类似的东西才能得出结论s
指向a
用于代码中的某些函数调用。看看***.com/a/15755160/5265292
【参考方案1】:
我不知道 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 分析仪使用第二种方法作为主要方法,但并非总是如此。有时在处理模板函数时,我们会再次分析它们(第一种方法)。换句话说,我们使用组合方法来保持分析深度和速度之间的平衡。
【讨论】:
【参考方案2】:为了分析某些值的可能来源,最好将所有变量转换为不可变变量,方法是在更改原始符号时引入一个新符号,并对所有后续出现使用新符号(原始符号不会在原始代码中重新分配的位置之后使用)。
考虑以下代码:
// 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
,依次源自i
和i_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
。
【讨论】:
【参考方案3】:Cppcheck 如何获得 f2 中的“a”和 f1 中的“s”之间的联系?
它们绝对不一样。可能会发生以下情况之一:
您将a
传递给函数,CPPcheck 会继续记住a
的大小,即使您使用形式参数s
访问它也是如此。
您必须记住,静态分析工具和编译器的工作方式不同,目的也不同。静态分析工具的创建完全是为了捕捉您在问题中提出的内容。
在您的第二个示例中,您有:
s= str_arry;
这会删除s
和a
之间的连接。
【讨论】:
众所周知,s= str_arry; 很容易理解will 删除人类的 s 和 a 之间的连接,但是计算机怎么知道呢?你能给我一些提示吗?s
指向的地址与a
指向的地址不同。以上是关于如何使用 AST 树或其他工具进行静态代码逻辑分析?的主要内容,如果未能解决你的问题,请参考以下文章