在前面的部分中介绍的跨过程分析方法被称为上下文无关的分析,因为它不区分对同一函数的不同调用。举个例子,考虑将符号分析应用于以下程序:
f(z) {
return z*42;
}
main() {
var x,y;
x = f(0); // call 1
y = f(87); // call 2
return x + y;
}
由于对f的第一次调用,参数z可能为0;对于第二次调用,它可能为正数,因此在f的入口处的抽象状态中,z的抽象值为 $\top$。这个值在f的主体中传播回给调用者,所以x和y也变成了$\top$。这是一个数据流在过程间分析中存在无效路径的示例:根据分析的约束,来自一个调用节点的数据流通过函数体传播,不仅在匹配的after-call节点返回,而且在所有的after-call节点返回。虽然分析仍然是sound的,但由此导致的精度损失可能是不可接受的。
解决这个问题的一个朴素方法是使用函数克隆。在这个具体的例子中,我们可以克隆f,并让这两个调用点调用名字不同但函数体相同的函数。通过将函数主体内联到每个调用中,也可以获得类似的效果。然而,更一般地说,这可能会显著增加程序的大小,并且在(相互)递归函数的情况下,会导致程序变得无限大。接下来我们将看到,我们可以通过使用表达能力更强的格来编码相关信息,以区分不同的调用上下文,就像第7章中的路径敏感方法一样。
如前边章节所讨论,一个基础的上下文无关数据流分析可以用形如$States^n$的格来表示;其中$States$ 是一个格,用于描述抽象状态,$n = |Nodes|$表示CFG中节点的个数(或者等价的,格可以表示为$Nodes \\rightarrow States$)。上下文敏感的分析,可以将格扩展为如下形式
$$ (Contexts \rightarrow lift(States))^n $$
或者等价的表示为:$Contexts\rightarrow(lift(States))^n$、$Nodes\rightarrow Contexts\rightarrow(lift(States))^n$、$Contexts \times Nodes \rightarrow(lift(States))$。其中Contexts就是函数调用的集合。使用lift(States)(如第4.3节中定义的)的原因是Contexts可能很大,所以我们只想为可能是可行的调用上下文推断抽象状态。lift(States)的底部元素,表示为unreachable,用于不可达程序入口的调用上下文。(当然,在分析中States已经提供了类似的信息,因此我们并不需要lifted的版本。)
在接下来的几节中,我们将介绍选择调用上下文的集合的不同方式。一个简单的选择是让Contexts成为一个单例集,这相当于上下文无关分析。我们将研究的另一个极端是选择Contexts = States,这允许完全的上下文敏感性。这些想法源自Sharir和Pnueli的工作[SP81]。
不涉及函数调用和返回的CFG节点的数据流被像往常一样建模,只是现在需要一个抽象状态(或额外的格元素unreachable)来描述函数调用的上下文。这意味着约束变量现在范围是$Contexts \\rightarrow lift(States)$ ,而不仅仅是States。例如,来自第5.1节的函数内符号分析中的赋值语句X=E的约束规则如下:
在引入上下文分析之后,需要扩展为如下:
其中:
请注意,不同的调用上下文的信息是分开保存的,并且可达性信息是一起传播的。如何模拟调用节点、调用后节点、函数入口节点和函数退出节点的数据流取决于上下文敏感性策略,我们将在下面的各节介绍。