USENIX Association 25th USENIX Security Symposium 585
assemblers deviate from the traditional CFG; typically
by omitting indirect edges, and sometimes by defining
a global CFG rather than per-function CFGs. Therefore,
we define the Interprocedural CFG (ICFG): the union of
all function-level CFGs, connected through interprocedu-
ral call and jump edges. This allows us to abstract from
the disassemblers’ varying CFG definitions, by focusing
our measurement on the coverage of basic blocks in the
ICFG. We pay special attention to hard-to-resolve basic
blocks, such as the heads of address-taken functions and
switch/case blocks reached via jump tables.
(5) Callgraph accuracy: The correctness of the digraph
G =(V
cs
∪ V
f
, E
call
)
linking the set
V
cs
of call sites to
the function starts
V
f
through call edges
E
call
⊆ V
cs
×V
f
.
Similarly to the CFG, disassemblers deviate from the
traditional callgraph definition by including only direct
call edges. In our experiments, we therefore measure the
completeness of this direct callgraph, considering indirect
calls and tailcalls separately in our complex case analysis.
2.3 Complex Constructs
We also study the prevalence in real-world binaries of
complex corner cases which are often cited as particularly
harmful to disassembly [5, 23, 34].
(1) Overlapping/shared basic blocks: Basic blocks may
be shared between different functions, hindering disas-
semblers from properly separating these functions.
(2) Overlapping instructions: Since x86/x64 uses
variable-length instructions without any enforced memory
alignment, jumps can target any offset within a multi-byte
instruction. This allows the same code bytes to be in-
terpreted as multiple overlapping instructions, some of
which may be missed by disassemblers.
(3) Inline data and jump tables: Data bytes may be
mixed in with instructions in a code section. Examples of
potential inline data include jump tables or local constants.
Such data can cause false positive instructions, and can
desynchronize the instruction stream if the last few data
bytes are mistakenly interpreted as the start of a multi-
byte instruction. Disassembly then continues parsing this
instruction into the actual code bytes, losing track of the
instruction stream alignment.
(4) Switches/case blocks: Switches are a challenge for
basic block discovery, because the switch case blocks are
typically indirect jump targets (encoded in jump tables).
(5) Alignment bytes: Some code (i.e.,
nop
) or data
bytes may have no semantic meaning, serving only to
align other code for optimization of memory accesses.
Alignment bytes may cause desynchronization if they do
not encode valid instructions.
(6) Multi-entry functions: Functions may have multiple
basic blocks used as entry points, which can complicate
function start recognition.
<BB
0
>
cmp ecx, edx
jl <BB
2
>
jmp <BB
1
>
<BB
1
>
mov eax,[fptr+ecx]
call eax
<BB
2
>
mov eax,[fptr+edx]
call eax
<f
1
>
<f
2
>
<f
0
>
<inline data>
<BB
0
>
cmp ecx, edx
jl <BB
2
>
jmp <BB
1
>
<BB
1
>
mov eax,[fptr+ecx]
call eax
<BB
2
>
mov eax,[fptr+edx]
call eax
<f
1
>
<f
2
>
<f
0
>
<inline data>
Figure 1: Disassembly methods. Arrows show disassem-
bly flow. Gray blocks show missed or corrupted code.
(7) Tail calls: In this common optimization, a function
ends not with a return, but with a jump to another function.
This makes it more difficult for disassemblers to detect
where the optimized function ends.
2.4 Disassembly & Testing Environment
We conducted all disassembly experiments on an Intel
Core i5 4300U machine with 8GB of RAM, running
Ubuntu 15.04 with kernel 3.19.0-47. We compiled our
gcc
and
clang
test cases on this same machine. The
Visual Studio binaries were compiled on an Intel Core i7
3770 machine with 8GB of RAM, running Windows 10.
We tested nine popular industry and research dis-
assemblers: IDA Pro v6.7, Hopper v3.11.5, Dyninst
v9.1.0 [
5
], BAP v0.9.9 [
7
], ByteWeight v0.9.9 [
4
], Jakstab
v0.8.4 [
17
], angr v4.6.1.4 [
36
], PSI v1.1 [
47
] (the suc-
cessor of BinCFI [
48
]), and objdump v2.22. ByteWeight
yields only function starts, while Dyninst and PSI sup-
port only ELF binaries (for Dyninst, this is due to our
Linux testing environment). Jakstab supports only x86
PE binaries. We omit angr results for x86, as angr is opti-
mized for x64. PSI is based on objdump, with added error
correction. Section 3 shows that PSI (and all linear dis-
assemblers) perform equivalently to objdump; therefore,
we group these under the name linear disassembly.
All others are recursive descent disassemblers, illus-
trated in Figure 1. These follow control flow to avoid
desynchronization by inline data, and to discover com-
plex cases like overlapping instructions. In contrast, linear
disassemblers like objdump simply decode all code bytes
consecutively, and may be confused by inline data, possi-
bly causing garbled code like
BB
1
in the figure. Recursive
disassemblers avoid this problem, but may miss indirect
control flow targets, such as f
1
and f
2
in the figure.