1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
| ============
Region Store
============
The analyzer "Store" represents the contents of memory regions. It is an opaque
functional data structure stored in each ``ProgramState``; the only class that
can modify the store is its associated StoreManager.
Currently (Feb. 2013), the only StoreManager implementation being used is
``RegionStoreManager``. This store records bindings to memory regions using a
"base region + offset" key. (This allows ``*p`` and ``p[0]`` to map to the same
location, among other benefits.)
Regions are grouped into "clusters", which roughly correspond to "regions with
the same base region". This allows certain operations to be more efficient,
such as invalidation.
Regions that do not have a known offset use a special "symbolic" offset. These
keys store both the original region, and the "concrete offset region" -- the
last region whose offset is entirely concrete. (For example, in the expression
``foo.bar[1][i].baz``, the concrete offset region is the array ``foo.bar[1]``,
since that has a known offset from the start of the top-level ``foo`` struct.)
Binding Invalidation
--------------------
Supporting both concrete and symbolic offsets makes things a bit tricky. Here's
an example:
.. code-block:: cpp
foo[0] = 0;
foo[1] = 1;
foo[i] = i;
After the third assignment, nothing can be said about the value of ``foo[0]``,
because ``foo[i]`` may have overwritten it! Thus, *binding to a region with a
symbolic offset invalidates the entire concrete offset region.* We know
``foo[i]`` is somewhere within ``foo``, so we don't have to invalidate
anything else, but we do have to be conservative about all other bindings within
``foo``.
Continuing the example:
.. code-block:: cpp
foo[i] = i;
foo[0] = 0;
After this latest assignment, nothing can be said about the value of ``foo[i]``,
because ``foo[0]`` may have overwritten it! *Binding to a region R with a
concrete offset invalidates any symbolic offset bindings whose concrete offset
region is a super-region **or** sub-region of R.* All we know about ``foo[i]``
is that it is somewhere within ``foo``, so changing *anything* within ``foo``
might change ``foo[i]``, and changing *all* of ``foo`` (or its base region) will
*definitely* change ``foo[i]``.
This logic could be improved by using the current constraints on ``i``, at the
cost of speed. The latter case could also be improved by matching region kinds,
i.e. changing ``foo[0].a`` is unlikely to affect ``foo[i].b``, no matter what
``i`` is.
For more detail, read through ``RegionStoreManager::removeSubRegionBindings`` in
RegionStore.cpp.
ObjCIvarRegions
---------------
Objective-C instance variables require a bit of special handling. Like struct
fields, they are not base regions, and when their parent object region is
invalidated, all the instance variables must be invalidated as well. However,
they have no concrete compile-time offsets (in the modern, "non-fragile"
runtime), and so cannot easily be represented as an offset from the start of
the object in the analyzer. Moreover, this means that invalidating a single
instance variable should *not* invalidate the rest of the object, since unlike
struct fields or array elements there is no way to perform pointer arithmetic
to access another instance variable.
Consequently, although the base region of an ObjCIvarRegion is the entire
object, RegionStore offsets are computed from the start of the instance
variable. Thus it is not valid to assume that all bindings with non-symbolic
offsets start from the base region!
Region Invalidation
-------------------
Unlike binding invalidation, region invalidation occurs when the entire
contents of a region may have changed---say, because it has been passed to a
function the analyzer can model, like memcpy, or because its address has
escaped, usually as an argument to an opaque function call. In these cases we
need to throw away not just all bindings within the region itself, but within
its entire cluster, since neighboring regions may be accessed via pointer
arithmetic.
Region invalidation typically does even more than this, however. Because it
usually represents the complete escape of a region from the analyzer's model,
its *contents* must also be transitively invalidated. (For example, if a region
``p`` of type ``int **`` is invalidated, the contents of ``*p`` and ``**p`` may
have changed as well.) The algorithm that traverses this transitive closure of
accessible regions is known as ClusterAnalysis, and is also used for finding
all live bindings in the store (in order to throw away the dead ones). The name
"ClusterAnalysis" predates the cluster-based organization of bindings, but
refers to the same concept: during invalidation and liveness analysis, all
bindings within a cluster must be treated in the same way for a conservative
model of program behavior.
Default Bindings
----------------
Most bindings in RegionStore are simple scalar values -- integers and pointers.
These are known as "Direct" bindings. However, RegionStore supports a second
type of binding called a "Default" binding. These are used to provide values to
all the elements of an aggregate type (struct or array) without having to
explicitly specify a binding for each individual element.
When there is no Direct binding for a particular region, the store manager
looks at each super-region in turn to see if there is a Default binding. If so,
this value is used as the value of the original region. The search ends when
the base region is reached, at which point the RegionStore will pick an
appropriate default value for the region (usually a symbolic value, but
sometimes zero, for static data, or "uninitialized", for stack variables).
.. code-block:: cpp
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!).
NOTE: The fact that bindings are stored as a base region plus an offset limits
the Default Binding strategy, because in C aggregates can contain other
aggregates. In the current implementation of RegionStore, there is no way to
distinguish a Default binding for an entire aggregate from a Default binding
for the sub-aggregate at offset 0.
Lazy Bindings (LazyCompoundVal)
-------------------------------
RegionStore implements an optimization for copying aggregates (structs and
arrays) called "lazy bindings", implemented using a special SVal called
LazyCompoundVal. When the store is asked for the "binding" for an entire
aggregate (i.e. for an lvalue-to-rvalue conversion), it returns a
LazyCompoundVal instead. When this value is then stored into a variable, it is
bound as a Default value. This makes copying arrays and structs much cheaper
than if they had required memberwise access.
Under the hood, a LazyCompoundVal is implemented as a uniqued pair of (region,
store), representing "the value of the region during this 'snapshot' of the
store". This has important implications for any sort of liveness or
reachability analysis, which must take the bindings in the old store into
account.
Retrieving a value from a lazy binding happens in the same way as any other
Default binding: since there is no direct binding, the store manager falls back
to super-regions to look for an appropriate default binding. LazyCompoundVal
differs from a normal default binding, however, in that it contains several
different values, instead of one value that will appear several times. Because
of this, the store manager has to reconstruct the subregion chain on top of the
LazyCompoundVal region, and look up *that* region in the previous store.
Here's a concrete example:
.. code-block:: cpp
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.
|