80 lines
4.2 KiB
TeX
80 lines
4.2 KiB
TeX
\section{A baseline: dynamic dependencies detection with \valgrind{}}
|
|
|
|
As we already saw, a dynamic analyzer, such as \gus{}, has direct access to
|
|
the actual data dependencies occurring throughout an execution. While such
|
|
analyzers are often too slow to use in practice, they can be used as a baseline
|
|
to evaluate static alternatives.
|
|
|
|
Due to its complexity, \gus{} is, however, ill-suited for the implementation of
|
|
a simple experiment if one is not already familiar with its codebase. For this
|
|
reason, we implement \depsim{}, a dynamic analysis tool based on top of
|
|
\valgrind{}, whose goal is to report dependencies encountered at runtime.
|
|
|
|
\subsection{Valgrind}
|
|
|
|
Most low-level developers and computer scientists know
|
|
\valgrind{}~\cite{tool:valgrind} as a memory
|
|
analysis tool, reporting bad memory accesses, memory leaks and the like.
|
|
However, this is only the \texttt{memcheck} tool built into \valgrind{}, while the
|
|
whole program is actually a binary instrumentation framework.
|
|
|
|
\valgrind{} supports a wide variety of platforms, including x86-64 and
|
|
ARM. However, at the time of the writing, it supports AVX2, but does not yet
|
|
support AVX-512 on x86-64~\cite{tool:valgrind_arch_support} ---~there is,
|
|
however, ongoing work in that direction~\cite{valgrind_avx512}. While its
|
|
documentation is seemingly sparse and not frequently maintained, its code
|
|
is well-commented and readable, with interfaces cleanly exposed; thus, once one
|
|
gets a good enough comprehension of how the project's code is structured, this
|
|
lack of documentation is not problematic. \valgrind{} is also a free and
|
|
open-source software, distributed under the GNU GPLv2 license.
|
|
|
|
\medskip{}
|
|
|
|
In order to instrument a binary file, \valgrind{} first lifts the original
|
|
assembly code to an intermediate representation. The instrumentation tool is
|
|
then able to modify this intermediate representation before passing it back to
|
|
Valgrind, which will re-compile it to a native binary before running it.
|
|
|
|
While this intermediate representation, called \vex{}, is convenient to
|
|
instrument a binary, it may further be used as a way to obtain \emph{semantics}
|
|
for some assembly code, independently of the Valgrind framework.
|
|
|
|
\subsection{Depsim}\label{ssec:depsim}
|
|
|
|
The tool we write to extract runtime-gathered dependencies, \depsim{}, is
|
|
able to extract dependencies through both registers, memory and temporary
|
|
variables ---~in its intermediate representation, Valgrind keeps some values
|
|
assigned to temporary variables in static single-assignment (SSA) form.
|
|
It however supports a flag to detect only memory-carried dependencies, as this
|
|
will be useful to evaluate our static algorithm later.
|
|
|
|
As a dynamic tool, the distinction between straight-line code and loop-carried
|
|
dependencies is irrelevant, as the analysis follows the actual program flow.
|
|
|
|
\medskip{}
|
|
|
|
In order to track dependencies, each basic block of the program is
|
|
instrumented. Dependencies are stored as a hash table and represented as a
|
|
pair of source and destination program counter; they are mapped to a number of
|
|
encountered occurrences.
|
|
|
|
Dependencies through temporaries are, by construction, resident to a single
|
|
basic block ---~they are thus statically detected at instrumentation time. At
|
|
runtime, the occurrence count of those dependencies is updated whenever the
|
|
basic block is executed.
|
|
|
|
For both register- and memory-carried dependencies, each write is instrumented
|
|
by adding a runtime write to a \emph{shadow} register file or memory, noting
|
|
that the written register or memory address was last written at the current
|
|
program counter. Each read, in turn, is instrumented by adding a fetch to this
|
|
shadow register file or memory, retrieving the last program counter at which
|
|
this location was written to; the dependency count between this program counter
|
|
and the current program counter is then incremented.
|
|
|
|
In practice, the shadow register file is simply implemented as an array
|
|
holding, for each register id, the last program counter that wrote at this
|
|
location. The shadow memory is instead implemented as a hash table.
|
|
|
|
At the end of the run, all the dependencies retrieved are reported. Care is
|
|
taken to translate back the runtime program counters to addresses in the
|
|
original ELF files, using the running process' memory map.
|