3.5. 区域存储

分析器的“存储”表示内存区域的内容。它是在每个 ProgramState 中存储的不透明函数式数据结构;唯一可以修改存储的类是其关联的 StoreManager。

目前(2013 年 2 月),唯一使用的 StoreManager 实现是 RegionStoreManager。此存储使用“基本区域 + 偏移量”键记录到内存区域的绑定。(这允许 *pp[0] 映射到同一个位置,以及其他优点。)

区域被分组到“集群”中,这些集群大致对应于“具有相同基本区域的区域”。这使得某些操作(如失效)效率更高。

没有已知偏移量的区域使用特殊的“符号”偏移量。这些键存储原始区域和“具体偏移量区域”——最后一个完全具体的偏移量区域。(例如,在表达式 foo.bar[1][i].baz 中,具体偏移量区域是数组 foo.bar[1],因为它与顶级 foo 结构的开头具有已知的偏移量。)

3.5.1. 绑定失效

同时支持具体和符号偏移量会使事情变得有点棘手。以下是一个示例

foo[0] = 0;
foo[1] = 1;
foo[i] = i;

在第三次赋值之后,无法确定 foo[0] 的值,因为 foo[i] 可能已覆盖它!因此,绑定到具有符号偏移量的区域会使整个具体偏移量区域失效。 我们知道 foo[i] 位于 foo 之内,因此我们不必使其他任何内容失效,但我们必须对 foo 中的所有其他绑定保持保守。

继续示例

foo[i] = i;
foo[0] = 0;

经过这次最新的赋值之后,无法确定 foo[i] 的值,因为 foo[0] 可能已覆盖它!绑定到具有具体偏移量的区域 R 会使任何具有具体偏移量区域为 R 的**超区域**或**子区域**的符号偏移量绑定失效。 我们只知道 foo[i] 位于 foo 之内,因此更改 foo 内部的任何内容都可能会更改 foo[i],而更改 foo(或其基本区域)的所有内容肯定会更改 foo[i]

此逻辑可以通过使用 i 的当前约束来改进,但这会以速度为代价。后一种情况也可以通过匹配区域类型来改进,即更改 foo[0].a 不会影响 foo[i].b,无论 i 是什么。

有关更多详细信息,请阅读 RegionStore.cpp 中的 RegionStoreManager::removeSubRegionBindings

3.5.2. ObjCIvarRegions

Objective-C 实例变量需要进行一些特殊处理。与结构体字段类似,它们不是基本区域,当其父对象区域失效时,所有实例变量也必须失效。但是,它们没有具体的编译时偏移量(在现代“非脆弱”运行时中),因此无法轻松地表示为分析器中对象开头的偏移量。此外,这意味着使单个实例变量失效不应使对象的其他部分失效,因为与结构体字段或数组元素不同,无法执行指针运算来访问另一个实例变量。

因此,尽管 ObjCIvarRegion 的基本区域是整个对象,但 RegionStore 偏移量是从实例变量的开头计算的。因此,不能假设所有具有非符号偏移量的绑定都从基本区域开始!

3.5.3. 区域失效

与绑定失效不同,区域失效发生在整个区域的内容可能已更改时——例如,因为该区域已传递给分析器可以建模的函数(如 memcpy),或者因为其地址已转义,通常作为不透明函数调用的参数。在这些情况下,我们需要丢弃的不只是区域本身的所有绑定,还要丢弃其整个集群中的所有绑定,因为可以使用指针运算访问相邻区域。

然而,区域失效通常比这还要多。因为它通常代表一个区域从分析器模型中的完全转义,所以其内容也必须进行传递失效。(例如,如果类型为 int ** 的区域 p 失效,则 *p**p 的内容也可能已更改。)遍历此可访问区域传递闭包的算法称为 ClusterAnalysis,也用于查找存储中的所有活动绑定(以便丢弃死绑定)。“ClusterAnalysis”这个名字早于基于集群的绑定组织,但指的是同一个概念:在失效和活动性分析期间,必须以相同的方式处理集群中的所有绑定,以对程序行为进行保守的建模。

3.5.4. 默认绑定

RegionStore 中的大多数绑定都是简单的标量值——整数和指针。这些被称为“直接”绑定。但是,RegionStore 支持第二种类型的绑定,称为“默认”绑定。这些用于为聚合类型(结构体或数组)的所有元素提供值,而不必为每个单独的元素显式指定绑定。

当某个特定区域没有直接绑定时,存储管理器会依次查看每个超区域,以查看是否存在默认绑定。如果存在,此值将用作原始区域的值。搜索在到达基本区域时结束,此时 RegionStore 将为该区域选择一个适当的默认值(通常是符号值,但有时是零,用于静态数据,或者“未初始化”,用于堆栈变量)。

int manyInts[10];
manyInts[1] = 42;   // Creates a Direct binding for manyInts[1].
print(manyInts[1]); // Retrieves the Direct binding for manyInts[1];
print(manyInts[0]); // There is no Direct binding for manyInts[0].
                    // Is there a Default binding for the entire array?
                    // There is not, but it is a stack variable, so we use
                    // "uninitialized" as the default value (and emit a
                    // diagnostic!).

注意:绑定存储为基本区域加偏移量这一事实限制了默认绑定策略,因为在 C 中,聚合体可以包含其他聚合体。在 RegionStore 的当前实现中,无法区分整个聚合体的默认绑定与偏移量为 0 的子聚合体的默认绑定。

3.5.5. 延迟绑定(LazyCompoundVal)

RegionStore 实现了一种用于复制聚合体(结构体和数组)的优化,称为“延迟绑定”,它使用一个名为 LazyCompoundVal 的特殊 SVal 来实现。当存储被要求提供整个聚合体的“绑定”(即用于左值到右值的转换)时,它会返回一个 LazyCompoundVal。当此值随后被存储到变量中时,它将作为默认值绑定。这使得复制数组和结构体的成本远低于需要成员访问的情况。

在幕后,LazyCompoundVal 实现为一个唯一的(区域、存储)对,表示“在存储的此‘快照’期间的区域值”。这对任何类型的活动性或可达性分析都有重要影响,这些分析必须考虑旧存储中的绑定。

从延迟绑定中检索值与从任何其他默认绑定中检索值的方式相同:由于没有直接绑定,存储管理器会回退到超区域以查找适当的默认绑定。但是,LazyCompoundVal 与普通默认绑定不同,因为它包含多个不同的值,而不是一个将在多个地方出现的值。因此,存储管理器必须在 LazyCompoundVal 区域之上重建子区域链,并在之前的存储中查找区域。

以下是一个具体示例

CGPoint p;
p.x = 42;       // A Direct binding is made to the FieldRegion 'p.x'.
CGPoint p2 = p; // A LazyCompoundVal is created for 'p', along with a
                // snapshot of the current store state. This value is then
                // used as a Default binding for the VarRegion 'p2'.
return p2.x;    // The binding for FieldRegion 'p2.x' is requested.
                // There is no Direct binding, so we look for a Default
                // binding to 'p2' and find the LCV.
                // Because it's a LCV, we look at our requested region
                // and see that it's the '.x' field. We ask for the value
                // of 'p.x' within the snapshot, and get back 42.