Using Data Structure Analysis for Alias and IP Mod/Ref Analysis
4.2 Implementing Alias and Mod/Ref Analysis with DSA: ds-aa
The Data Structure Analysis algorithm described in Chapter 3 constructs several sets of graphs which capture a general-purpose abstraction of the program memory image. These graphs are
designed to represent important information about the memory usage of the program without tying the representation to a specific client. In this section, we describe ds-aa, an alias analysis implementation that uses the results of DSA to answer alias analysis queries.
4.2.1 Computing Alias Analysis Responses
DSA consists of three primary passes, each of which compute a set of graphs: the Local pass (Sec-tion 3.2.2), the Bottom-Up pass (Sec(Sec-tion 3.2.3), and the Top-Down pass (Sec(Sec-tion 3.2.4). Because DSA keeps track of what information is “complete” (see Section 3.1.1) at each stage of construction, we could use any of these three graphs to implement alias analysis.
In practice, we use the TD graphs for alias analysis, as they have the most complete information available in them: the graph for a function includes the effects of all callers and all callees, so the only incomplete information remaining is due to information that leaks in from outside of the analysis scope (e.g. memory which is passed to or returned from an external function). To answer an “alias(P1, S1, P2, S2)” query, ds-aa performs the following steps:
1. Look up the TD DSGraph G, which contains P1 and P2 in its EV mapping.
2. Let the node/field pairsn1, f1 = EV(P1) and n2, f2 = EV(P2), using G’s EV mapping.
3. If C /∈ flags(n1) and C /∈ flags(n2), return MayAlias (if both nodes contain incomplete information, no judgement can be made).
4. If n1 = n2, return NoAlias (pointers point to two distinct nodes).
5. If not overlap(offsetof(f1), offsetof(f1)+S1, offsetof(f2), offsetof(f2)+S2) return NoAlias (if the fields cannot overlap, pointers point to distinct fields).
6. Return MayAlias.
Steps #1 and #2 perform simple map lookups to find the relevant information. Step #3 ensures that ds-aa is safe for incomplete programs: if both pointers point to non-complete nodes, no conclusion about them can be made. Note that if n1 is complete and n2 is not (or visa-versa), we know that the nodes are distinct and that n1 can never be merged with n2no matter what code
is outside of analysis scope. If n1 could ever be merged with n2, it could not be marked complete, as described in Section 3.1.1).
Step #4 draws the conclusion that if the pointers point to distinct nodes (and if at least one is marked complete, due to step #3), the pointers can never alias. Step #5 uses field sensitivity to refine the alias analysis in the case when the pointers point to the same node. In this case, if the two fields do not overlap, ds-aa can conclude NoAlias. If neither Step #4 or #5 are able to determine non-aliasing, ds-aa must return MayAlias.
Notice that DSA is incapable of returning must alias information. In particular, even if n1 = n2 and f1 = f2, DSA cannot prove that both pointers point to the same dynamic memory object, only that they are in the same class (for example, it cannot determine that the pointers point to the exact same linked list node). If a node only contains Global information (no heap, stack or unknown memory), we could conceptually provide must alias information in cases where our aggregate model does not make this unsound (e.g. we collapse an entire array to one element). We have not investigated this possibility.
4.2.2 Computing Mod/Ref Responses
The steps required to compute a safe answer to the “modref” queries described in Section 4.1.2 depend on the the different instructions passed in as arguments. As mentioned above, there are 4 operations that (directly or indirectly) can access memory: load, store, call, & invoke. The LLVM framework handles the simple mod/ref queries automatically (e.g., an add operation mod/refs nothing), and dispatches the remaining queries to the “alias” query above (e.g. to determine if a store mods a location being loaded), to a call/call dependence tester, or to the second second modref query above which checks a call against a memory range (e.g. to test a load against a call).
DSA has all of the information it needs to compute context-sensitive mod/ref information for function calls. In particular, the Bottom-Up graphs capture the direct and indirect mod/ref effects of calling the function, in any context, at a per DSNode granularity. While this information is general enough to even allow testing for call/call dependence, none of our clients currently use this information. As such, we only describe call/location mod/ref analysis here.
To respond to a “modref(I, P , S)” query, where I is a call or invoke, ds-aa performs the
following steps:
1. Look up the Top-Down DSGraph G, that includes the function containing I and P in its EV mapping.
2. Let the node/field pair n, f = EV(P ), using G’s EV mapping.
3. If C /∈ flags(n), return ModRef (memory is incomplete, cannot draw a conclusion if F accesses it).
4. Let AC be the set of actual callees for I. If the actual callees are unknown, return ModRef.
5. Remove any external functions from AC.
6. If AC is empty, return NoModRef (AC must have been empty1 or contained only external functions. Since the memory does not escape the program, external functions cannot mod/ref it).
7. Union together all of the Bottom-Up graphs for the callees, merging the corresponding formals for each function:
CG = Empty DS Graph
∀F ∈ AC
cloneGraphInto(BUDSG(F ), CG) mergeArguments(F , CG)
8. Compute the mapping M from nodes in G to nodes in CG, as defined by the actual argu-ment/formal argument bindings defined by I, and mutual global variables defined in G and CG.
9. Check nodes from the BU Graphs for mod/ref flags:
ModRefResult R ={}
∀nCG∈ M(n)
if (M ∈ flags(nCG)) R = R∪ {Mod}
1If the set of actual callees for a function is empty, the call site must be dynamically unreachable.
if (R ∈ flags(nCG)) R = R∪ {Ref}
Return R
Steps #1 and #2 perform simple lookups to get the information we need. Step #3 checks for incomplete information: if P points to incomplete memory, we do not draw any conclusion about it. Step #4 computes the actual callees for a calle site (note that our implementation currently only implements direct calls, but we could easily add support for indirect calls). Step #6 implements a trivial form of mod/ref analysis that any conservatively-correct whole-program analysis can provide:
calls to external functions are known to not access memory that does not escape from the program2. Given a direct call to a function in the program, Step #8 computes the relevant mapping from nodes in TD Graph G to the nodes in CG graph. Because the bottom-up graphs for the functions in AC were inlined into the caller graph, we know that the caller graph is at least as constrainted as the callee graphs (and may be more so). As such, we compute (and cache) the mapping from nodes in G to nodes in CG defined by the call site I. Finally, Step #9 iterates over all of the nodes that n maps to in the CG graph, and unions together the mod/ref information from these nodes to form a result. Note that if n is never accessed by F , it will not map to any nodes, thus we will compute a NoModRef result.
Note that DSNodes in CG only track mod/ref information on a per-node basis. DSA could be trivially extended to support more precise mod/ref information by tracking mod/ref information on a per-field basis. To do this, we expand the M and R bits to be bit-vectors that tracks one bit for every field in a node. This would have slightly higher overhead than tracking one bit per node, but would improve mod/ref precision for programs that use structures heavily.
ds-aa Mod/Ref precision could also be improved for nodes that escape the program. In partic-ular, even if a node is not marked complete (which is handled above by Step #3), a call does not mod/ref the node if all of the DS Nodes mod/ref’d by the call are complete. The check for Step #3 above could be enhanced to take this into consideration.
2Note that this judgement relies on the assumption that the externally called function cannot make a direct call back into the program. This assumption is guaranteed by standard “whole program” optimziation flags offered by many aggressive compilers, and is always safe for the programs in our testsuite.