pcode-graph 1.1.2__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pcode_graph-2.0.0/CHANGELOG +6 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/PKG-INFO +2 -2
- pcode_graph-2.0.0/pcode_graph/analysis.py +528 -0
- pcode_graph-2.0.0/pcode_graph/basic_blocks.py +196 -0
- pcode_graph-1.1.2/pcode_graph/utils.py → pcode_graph-2.0.0/pcode_graph/bit_utils.py +0 -2
- pcode_graph-1.1.2/pcode_graph/charac.py → pcode_graph-2.0.0/pcode_graph/characterization.py +5 -5
- pcode_graph-2.0.0/pcode_graph/checker.py +235 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/cli.py +7 -3
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/emulator.py +1 -1
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/gnn_exporter.py +19 -17
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/graph.py +20 -10
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/lief_importer.py +3 -5
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/maker.py +30 -13
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/nx_exporter.py +7 -6
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/pcode.py +1 -2
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/registers.py +0 -1
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/simplifier.py +1 -2
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/torch_utils.py +1 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/translator.py +8 -3
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/view.py +21 -11
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/visitor.py +1 -1
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pyproject.toml +2 -2
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/analysis_test.py +48 -12
- pcode_graph-2.0.0/tests/basic_block_test.py +281 -0
- pcode_graph-2.0.0/tests/callother_test.py +193 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/charac_test.py +4 -1
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/exporter_test.py +2 -2
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/fixtures.py +32 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/maker_test.py +13 -34
- pcode_graph-2.0.0/todo.md +2 -0
- pcode_graph-1.1.2/pcode_graph/analysis.py +0 -396
- pcode_graph-1.1.2/pcode_graph/checker.py +0 -120
- pcode_graph-1.1.2/todo.md +0 -3
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/.github/workflows/release.yml +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/.github/workflows/test.yml +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/.gitignore +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/LICENSE +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/README.md +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/__init__.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/__main__.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/arch.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/asm.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/log.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/requirements.txt +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/__init__.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/conftest.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/emulator_test.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/helpers.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/translator_test.py +0 -0
- {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/utils_test.py +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# 2.0.0
|
|
2
|
+
|
|
3
|
+
- Add basic blocks node in the graph, to make the graph invariant by instruction reordering, while reducing its diameter. The previous format is still available by setting maker flags. Because of new "membership" edge kind, the number of egde features changed.
|
|
4
|
+
- Change translation of pcode `CALLOTHER` operation as a call to express its possible side effects.
|
|
5
|
+
- Change default number of encoded operands in edge features.
|
|
6
|
+
- Improve graph creation speed, by switching to a basic-blocks based def-use analysis
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pcode_graph
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Build, display and vectorize semantic graphs from binary code
|
|
5
5
|
Project-URL: Homepage, https://github.com/quarkslab/pcode_graph
|
|
6
6
|
Project-URL: Issues, https://github.com/quarkslab/pcode_graph/issues
|
|
@@ -15,7 +15,7 @@ Requires-Dist: docopt
|
|
|
15
15
|
Requires-Dist: keystone-engine~=0.9.2
|
|
16
16
|
Requires-Dist: lief~=1.0.0
|
|
17
17
|
Requires-Dist: loguru
|
|
18
|
-
Requires-Dist: pypcode>=4.0.
|
|
18
|
+
Requires-Dist: pypcode>=4.0.1
|
|
19
19
|
Requires-Dist: pyvis
|
|
20
20
|
Provides-Extra: test
|
|
21
21
|
Requires-Dist: pytest; extra == 'test'
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from functools import cache
|
|
4
|
+
import heapq
|
|
5
|
+
from typing import Iterator, override
|
|
6
|
+
from pypcode import OpCode, PcodeOp, Varnode
|
|
7
|
+
from pcode_graph.arch import Arch
|
|
8
|
+
from pcode_graph.pcode import dump_operation, from_native_opcode
|
|
9
|
+
from pcode_graph.bit_utils import unsigned_to_signed
|
|
10
|
+
from pcode_graph.visitor import PcodeVisitor
|
|
11
|
+
|
|
12
|
+
OpIndex = int
|
|
13
|
+
EntryIndex = -1
|
|
14
|
+
UnknownCodeIndex = -2
|
|
15
|
+
ExitIndex = -3
|
|
16
|
+
|
|
17
|
+
Address = int
|
|
18
|
+
|
|
19
|
+
BlockIndex = int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def dump_index(i: OpIndex) -> str:
|
|
23
|
+
match i:
|
|
24
|
+
case -1:
|
|
25
|
+
return "entry"
|
|
26
|
+
case -2:
|
|
27
|
+
return "elsewhere"
|
|
28
|
+
case -3:
|
|
29
|
+
return "exit"
|
|
30
|
+
case _:
|
|
31
|
+
assert i >= 0
|
|
32
|
+
return str(i)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VarKinds(Enum):
|
|
36
|
+
# Register or named P-Code temporary
|
|
37
|
+
Register = 0
|
|
38
|
+
# P-Code temporary
|
|
39
|
+
Unique = 1
|
|
40
|
+
# Numerical constant
|
|
41
|
+
Constant = 2
|
|
42
|
+
# PC-relative address value
|
|
43
|
+
Ram = 3
|
|
44
|
+
# Dummy access to the memory
|
|
45
|
+
IndirectAccess = 4
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, order=True)
|
|
49
|
+
class Var:
|
|
50
|
+
"""Hashable and comparable wrapper for temporaries, registers, constants and memory."""
|
|
51
|
+
|
|
52
|
+
kind: VarKinds
|
|
53
|
+
size: int
|
|
54
|
+
value: int
|
|
55
|
+
pretty_name: str = field(compare=False)
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return self.pretty_name
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def wrap(var: Varnode) -> Var:
|
|
62
|
+
match var.space.name:
|
|
63
|
+
case "register":
|
|
64
|
+
name = var.getRegisterName() or "UNKNOWN"
|
|
65
|
+
return Var(VarKinds.Register, var.size, var.offset, name)
|
|
66
|
+
case "unique":
|
|
67
|
+
return Var(VarKinds.Unique, var.size, var.offset, f"${var.offset}")
|
|
68
|
+
case "const":
|
|
69
|
+
return Var(VarKinds.Constant, var.size, var.offset, f"${var.offset}")
|
|
70
|
+
case "ram":
|
|
71
|
+
return Var(VarKinds.Ram, var.size, var.offset, f"[0x{var.offset:x}]")
|
|
72
|
+
case _ as name:
|
|
73
|
+
raise ValueError(f"Unexpected space for input variable: {name}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Special variable singleton
|
|
77
|
+
MEMORY = Var(VarKinds.IndirectAccess, 0, 0, "MEMORY")
|
|
78
|
+
|
|
79
|
+
# Writing operations by variable byte (see split), or for MEMORY
|
|
80
|
+
Definitions = dict[Var, set[OpIndex]]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def split(var: Var) -> tuple[Var, ...]:
|
|
84
|
+
"""Splits a register into its bytes to track definitions.
|
|
85
|
+
Overlapping registers share bytes (RAX, AX, AH) so that a partial write only kills the definitions of the bytes it writes.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
if var.kind in {VarKinds.IndirectAccess, VarKinds.Constant}:
|
|
89
|
+
return (var,)
|
|
90
|
+
return _split_bytes(var.kind, var.value, var.size)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@cache
|
|
94
|
+
def _split_bytes(kind: VarKinds, value: int, size: int) -> tuple[Var, ...]:
|
|
95
|
+
return tuple(
|
|
96
|
+
Var(kind, 1, value + offset, f"{kind.name}:{value + offset:#x}")
|
|
97
|
+
for offset in range(size)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(slots=True)
|
|
102
|
+
class BasicBlock:
|
|
103
|
+
"""Maximal sequence of operations with a single entry and a single exit."""
|
|
104
|
+
|
|
105
|
+
# Operation indexes range: start included, end excluded
|
|
106
|
+
start: OpIndex
|
|
107
|
+
end: OpIndex
|
|
108
|
+
|
|
109
|
+
# Block-level CFG edges, special indexes (EntryIndex, ExitIndex...) are kept as is.
|
|
110
|
+
# Order of successors matters, as for operations.
|
|
111
|
+
pred_blocks: set[BlockIndex] = field(default_factory=set)
|
|
112
|
+
succ_blocks: list[BlockIndex] = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
reachable: bool = False
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def last_op_index(self) -> OpIndex:
|
|
118
|
+
return self.end - 1
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def op_indexes(self) -> range:
|
|
122
|
+
return range(self.start, self.end)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def apply_operation(
|
|
126
|
+
definitions: Definitions, index: OpIndex, op: PcodeOp
|
|
127
|
+
) -> Definitions:
|
|
128
|
+
"""Returns the new definitions after the given operation is applied."""
|
|
129
|
+
|
|
130
|
+
if op.output:
|
|
131
|
+
result = dict(definitions)
|
|
132
|
+
written = {index}
|
|
133
|
+
for byte in split(wrap(op.output)):
|
|
134
|
+
result[byte] = written
|
|
135
|
+
return result
|
|
136
|
+
if op.opcode == OpCode.STORE:
|
|
137
|
+
# As we do not analyze which part of memory is written,
|
|
138
|
+
# a write to memory won't kill previous definitions of MEMORY.
|
|
139
|
+
result = dict(definitions)
|
|
140
|
+
result[MEMORY] = definitions.get(MEMORY, set()) | {index}
|
|
141
|
+
return result
|
|
142
|
+
return definitions
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class RichPcodeList:
|
|
147
|
+
"""A class to run static analysis on a list of P-Code operations."""
|
|
148
|
+
|
|
149
|
+
# Source architecture
|
|
150
|
+
arch: Arch
|
|
151
|
+
|
|
152
|
+
# List of P-Code operations
|
|
153
|
+
operations: list[PcodeOp]
|
|
154
|
+
|
|
155
|
+
# First P-Code operation for each instruction address
|
|
156
|
+
addresses: dict[Address, OpIndex] = field(default_factory=dict)
|
|
157
|
+
|
|
158
|
+
# Basic blocks sorted by operation index, and block of each operation
|
|
159
|
+
basic_blocks: list[BasicBlock] = field(default_factory=list)
|
|
160
|
+
block_indexes: list[BlockIndex] = field(default_factory=list)
|
|
161
|
+
|
|
162
|
+
# Reaching definitions. For each operation, by variable, list of:
|
|
163
|
+
# - definitions (= writing operation index) that reach this operation,
|
|
164
|
+
# - or EntryIndex (for initial definitions of inputs).
|
|
165
|
+
# Sets are shared between operations: do not modify them.
|
|
166
|
+
reaching_defs: list[Definitions] = field(default_factory=list)
|
|
167
|
+
|
|
168
|
+
def __post_init__(self):
|
|
169
|
+
InstructionAddressCollector(self)()
|
|
170
|
+
CFGBuilder(self)()
|
|
171
|
+
UnreachableCodeFinder(self)()
|
|
172
|
+
ReachingDefBuilder(self)()
|
|
173
|
+
|
|
174
|
+
def __str__(self) -> str:
|
|
175
|
+
return self.dump()
|
|
176
|
+
|
|
177
|
+
def block_of(self, index: OpIndex) -> BasicBlock:
|
|
178
|
+
return self.basic_blocks[self.block_indexes[index]]
|
|
179
|
+
|
|
180
|
+
def reachable(self, index: OpIndex) -> bool:
|
|
181
|
+
return self.block_of(index).reachable
|
|
182
|
+
|
|
183
|
+
def pred_ops(self, index: OpIndex) -> set[OpIndex]:
|
|
184
|
+
"""Operation-level CFG predecessors, derived from the block-level CFG."""
|
|
185
|
+
|
|
186
|
+
block = self.block_of(index)
|
|
187
|
+
if index != block.start:
|
|
188
|
+
return {index - 1}
|
|
189
|
+
return {
|
|
190
|
+
self.basic_blocks[pred].last_op_index if pred >= 0 else pred
|
|
191
|
+
for pred in block.pred_blocks
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def succ_ops(self, index: OpIndex) -> list[OpIndex]:
|
|
195
|
+
"""Operation-level CFG successors, derived from the block-level CFG."""
|
|
196
|
+
|
|
197
|
+
block = self.block_of(index)
|
|
198
|
+
if index != block.last_op_index:
|
|
199
|
+
return [index + 1]
|
|
200
|
+
return [
|
|
201
|
+
self.basic_blocks[successor].start if successor >= 0 else successor
|
|
202
|
+
for successor in block.succ_blocks
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
def dump(self) -> str:
|
|
206
|
+
"""Dumps the program and analysis results in a markdown table."""
|
|
207
|
+
|
|
208
|
+
s = "|index|op|preds|succs|input defs|reachable|exit def|\n"
|
|
209
|
+
exit_defs = self.get_exit_defs()
|
|
210
|
+
|
|
211
|
+
s += "|--|--|--|--|--|--|--|\n"
|
|
212
|
+
for i, op in enumerate(self.operations):
|
|
213
|
+
s += f"|{i}|{dump_operation(op)}|"
|
|
214
|
+
s += ", ".join(dump_index(p) for p in self.pred_ops(i)) + "|"
|
|
215
|
+
s += ", ".join(dump_index(p) for p in self.succ_ops(i)) + "|"
|
|
216
|
+
var_defs = []
|
|
217
|
+
for var, defs in self.iter_input_defs(i):
|
|
218
|
+
if defs:
|
|
219
|
+
var_defs.append(
|
|
220
|
+
str(var) + " from " + " or ".join(dump_index(d) for d in defs)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
s += ", ".join(var_defs) + "|"
|
|
224
|
+
if self.reachable(i):
|
|
225
|
+
s += "x"
|
|
226
|
+
s += "|"
|
|
227
|
+
exit_def_here = []
|
|
228
|
+
for var, defs in exit_defs.items():
|
|
229
|
+
for j in defs:
|
|
230
|
+
if i == j:
|
|
231
|
+
exit_def_here.append(str(var))
|
|
232
|
+
if exit_def_here:
|
|
233
|
+
s += ", ".join(exit_def_here)
|
|
234
|
+
s += "|\n"
|
|
235
|
+
|
|
236
|
+
return s
|
|
237
|
+
|
|
238
|
+
def get_defs(self, index: OpIndex, var: Var) -> set[OpIndex]:
|
|
239
|
+
"""Returns all the operations defining the value at given operation of the given variable."""
|
|
240
|
+
|
|
241
|
+
definitions = self.reaching_defs[index]
|
|
242
|
+
defs: set[OpIndex] = set()
|
|
243
|
+
for byte in split(var):
|
|
244
|
+
defs |= definitions.get(byte, set())
|
|
245
|
+
return defs
|
|
246
|
+
|
|
247
|
+
def get_exit_defs(self) -> Definitions:
|
|
248
|
+
"""Computes side effects of the program."""
|
|
249
|
+
|
|
250
|
+
exit_defs: Definitions = {}
|
|
251
|
+
for block in self.basic_blocks:
|
|
252
|
+
if not block.reachable or not {UnknownCodeIndex, ExitIndex}.intersection(
|
|
253
|
+
block.succ_blocks
|
|
254
|
+
):
|
|
255
|
+
continue
|
|
256
|
+
index = block.last_op_index
|
|
257
|
+
outputs = apply_operation(
|
|
258
|
+
self.reaching_defs[index], index, self.operations[index]
|
|
259
|
+
)
|
|
260
|
+
for byte, defs in outputs.items():
|
|
261
|
+
if byte.kind == VarKinds.Unique:
|
|
262
|
+
# Ignore P-Code temporary
|
|
263
|
+
continue
|
|
264
|
+
for writer in defs - {EntryIndex}:
|
|
265
|
+
# Group bytes back by written variable
|
|
266
|
+
output = self.operations[writer].output
|
|
267
|
+
var = wrap(output) if output else MEMORY
|
|
268
|
+
exit_defs.setdefault(var, set()).add(writer)
|
|
269
|
+
return exit_defs
|
|
270
|
+
|
|
271
|
+
def iter_inputs(self, index: OpIndex) -> Iterator[Var]:
|
|
272
|
+
"""Iterates operation inputs, skipping **special** ones
|
|
273
|
+
that have no semantic interest to put in the graph (see P-Code documentation).
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
op = self.operations[index]
|
|
277
|
+
if op.opcode == OpCode.LOAD:
|
|
278
|
+
yield MEMORY
|
|
279
|
+
|
|
280
|
+
for i, var in enumerate(op.inputs):
|
|
281
|
+
if i != 0 or op.opcode not in {
|
|
282
|
+
OpCode.IMARK,
|
|
283
|
+
OpCode.CBRANCH,
|
|
284
|
+
OpCode.BRANCH,
|
|
285
|
+
OpCode.CALL,
|
|
286
|
+
OpCode.LOAD,
|
|
287
|
+
OpCode.STORE,
|
|
288
|
+
OpCode.CALLOTHER,
|
|
289
|
+
}:
|
|
290
|
+
yield wrap(var)
|
|
291
|
+
|
|
292
|
+
def iter_input_defs(self, index: OpIndex) -> Iterator[tuple[Var, set[OpIndex]]]:
|
|
293
|
+
"""For each operand, returns the written variable and the operation(s) defining it."""
|
|
294
|
+
|
|
295
|
+
for var in self.iter_inputs(index):
|
|
296
|
+
yield var, self.get_defs(index, var)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class PassBase(PcodeVisitor):
|
|
300
|
+
|
|
301
|
+
def __init__(self, program: RichPcodeList) -> None:
|
|
302
|
+
super().__init__()
|
|
303
|
+
self.program = program
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class InstructionAddressCollector(PassBase):
|
|
307
|
+
"""Collects instruction addresses removing IMARK operations"""
|
|
308
|
+
|
|
309
|
+
def __call__(self):
|
|
310
|
+
for i, op in enumerate(self.program.operations):
|
|
311
|
+
if op.opcode == OpCode.IMARK:
|
|
312
|
+
self.program.addresses[op.inputs[0].offset] = i
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class CFGBuilder(PassBase):
|
|
316
|
+
|
|
317
|
+
def __call__(self):
|
|
318
|
+
# Successors of jumping operations
|
|
319
|
+
self._jumps: dict[OpIndex, list[OpIndex]] = {}
|
|
320
|
+
for index, op in enumerate(self.program.operations):
|
|
321
|
+
self.visit(from_native_opcode(op.opcode), index, op)
|
|
322
|
+
|
|
323
|
+
op_count = len(self.program.operations)
|
|
324
|
+
leaders = {0} | {index + 1 for index in self._jumps}
|
|
325
|
+
for targets in self._jumps.values():
|
|
326
|
+
leaders.update(target for target in targets if target >= 0)
|
|
327
|
+
sorted_leaders = sorted(leaders - {op_count}) if op_count else []
|
|
328
|
+
|
|
329
|
+
self.program.basic_blocks = []
|
|
330
|
+
self.program.block_indexes = []
|
|
331
|
+
for start, end in zip(sorted_leaders, sorted_leaders[1:] + [op_count]):
|
|
332
|
+
self.program.block_indexes.extend(
|
|
333
|
+
[len(self.program.basic_blocks)] * (end - start)
|
|
334
|
+
)
|
|
335
|
+
self.program.basic_blocks.append(BasicBlock(start, end))
|
|
336
|
+
|
|
337
|
+
for block_index, block in enumerate(self.program.basic_blocks):
|
|
338
|
+
last_index = block.last_op_index
|
|
339
|
+
for successor in self._jumps.get(
|
|
340
|
+
last_index, [self._next_index(last_index)]
|
|
341
|
+
):
|
|
342
|
+
if successor >= 0:
|
|
343
|
+
successor = self.program.block_indexes[successor]
|
|
344
|
+
self.program.basic_blocks[successor].pred_blocks.add(block_index)
|
|
345
|
+
block.succ_blocks.append(successor)
|
|
346
|
+
|
|
347
|
+
if self.program.basic_blocks:
|
|
348
|
+
self.program.basic_blocks[0].pred_blocks.add(EntryIndex)
|
|
349
|
+
|
|
350
|
+
def _next_index(self, index: OpIndex) -> OpIndex:
|
|
351
|
+
return index + 1 if index + 1 < len(self.program.operations) else ExitIndex
|
|
352
|
+
|
|
353
|
+
def _get_jump_target(self, index: OpIndex, op: PcodeOp) -> OpIndex:
|
|
354
|
+
address = op.inputs[0]
|
|
355
|
+
match address.space.name:
|
|
356
|
+
case "ram":
|
|
357
|
+
return self.program.addresses.get(address.offset, UnknownCodeIndex)
|
|
358
|
+
case "const":
|
|
359
|
+
# Relative jump inside the current instruction
|
|
360
|
+
target = index + unsigned_to_signed(address.offset, address.size)
|
|
361
|
+
assert 0 <= target < len(self.program.operations)
|
|
362
|
+
return target
|
|
363
|
+
case "register" | "unique":
|
|
364
|
+
return UnknownCodeIndex
|
|
365
|
+
case _ as name:
|
|
366
|
+
raise ValueError(f"Unexpected address space for jump address: {name}")
|
|
367
|
+
|
|
368
|
+
@override
|
|
369
|
+
def visit_default(self, index: OpIndex, op: PcodeOp) -> None:
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
@override
|
|
373
|
+
def visit_branch(self, index: OpIndex, op: PcodeOp) -> None:
|
|
374
|
+
self._jumps[index] = [self._get_jump_target(index, op)]
|
|
375
|
+
|
|
376
|
+
@override
|
|
377
|
+
def visit_branchind(self, index: OpIndex, op: PcodeOp) -> None:
|
|
378
|
+
self._jumps[index] = [self._get_jump_target(index, op)]
|
|
379
|
+
|
|
380
|
+
@override
|
|
381
|
+
def visit_return(self, index: OpIndex, op: PcodeOp) -> None:
|
|
382
|
+
self._jumps[index] = [self._get_jump_target(index, op)]
|
|
383
|
+
|
|
384
|
+
@override
|
|
385
|
+
def visit_call(self, index: OpIndex, op: PcodeOp) -> None:
|
|
386
|
+
target = self._get_jump_target(index, op)
|
|
387
|
+
if target == UnknownCodeIndex:
|
|
388
|
+
# Calling an unseen function: we suppose it returns here after
|
|
389
|
+
self._jumps[index] = [target, self._next_index(index)]
|
|
390
|
+
else:
|
|
391
|
+
self._jumps[index] = [target]
|
|
392
|
+
|
|
393
|
+
@override
|
|
394
|
+
def visit_callind(self, index: OpIndex, op: PcodeOp) -> None:
|
|
395
|
+
self.visit_call(index, op)
|
|
396
|
+
|
|
397
|
+
@override
|
|
398
|
+
def visit_cbranch(self, index: OpIndex, op: PcodeOp) -> None:
|
|
399
|
+
self._jumps[index] = [self._get_jump_target(index, op), self._next_index(index)]
|
|
400
|
+
|
|
401
|
+
@override
|
|
402
|
+
def visit_callother(self, index: OpIndex, op: PcodeOp) -> None:
|
|
403
|
+
self._jumps[index] = [UnknownCodeIndex, self._next_index(index)]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class UnreachableCodeFinder(PassBase):
|
|
407
|
+
|
|
408
|
+
def __call__(self):
|
|
409
|
+
|
|
410
|
+
pending: list[BlockIndex] = [0] if self.program.basic_blocks else []
|
|
411
|
+
while pending:
|
|
412
|
+
block = self.program.basic_blocks[pending.pop()]
|
|
413
|
+
if block.reachable:
|
|
414
|
+
continue
|
|
415
|
+
block.reachable = True
|
|
416
|
+
pending.extend(
|
|
417
|
+
successor for successor in block.succ_blocks if successor >= 0
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class ReachingDefBuilder(PassBase):
|
|
422
|
+
|
|
423
|
+
def __call__(self):
|
|
424
|
+
self.program.reaching_defs = [{} for _ in self.program.operations]
|
|
425
|
+
if not self.program.basic_blocks:
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
block_inputs: list[Definitions] = [{} for _ in self.program.basic_blocks]
|
|
429
|
+
|
|
430
|
+
# Initialize
|
|
431
|
+
for index in range(len(self.program.operations)):
|
|
432
|
+
if not self.program.reachable(index):
|
|
433
|
+
continue
|
|
434
|
+
for var in self.program.iter_inputs(index):
|
|
435
|
+
if var.kind in {
|
|
436
|
+
VarKinds.IndirectAccess,
|
|
437
|
+
VarKinds.Ram,
|
|
438
|
+
VarKinds.Register,
|
|
439
|
+
}:
|
|
440
|
+
for byte in split(var):
|
|
441
|
+
block_inputs[0][byte] = {EntryIndex}
|
|
442
|
+
|
|
443
|
+
# Block effects are computed once, on demand, as (generated defs, killed variables)
|
|
444
|
+
effects: dict[BlockIndex, tuple[Definitions, set[Var]]] = {}
|
|
445
|
+
|
|
446
|
+
# Each reachable block is processed at least once, lowest index first:
|
|
447
|
+
# follows the code layout, close to a reverse post-order.
|
|
448
|
+
# The list is sorted, so it is already a valid heap.
|
|
449
|
+
pending = [
|
|
450
|
+
index
|
|
451
|
+
for index, block in enumerate(self.program.basic_blocks)
|
|
452
|
+
if block.reachable
|
|
453
|
+
]
|
|
454
|
+
queued = set(pending)
|
|
455
|
+
|
|
456
|
+
while pending:
|
|
457
|
+
block_index = heapq.heappop(pending)
|
|
458
|
+
queued.remove(block_index)
|
|
459
|
+
|
|
460
|
+
if block_index not in effects:
|
|
461
|
+
effects[block_index] = self._compute_effect(
|
|
462
|
+
self.program.basic_blocks[block_index]
|
|
463
|
+
)
|
|
464
|
+
generated, killed = effects[block_index]
|
|
465
|
+
outputs = self._apply_effect(block_inputs[block_index], generated, killed)
|
|
466
|
+
|
|
467
|
+
for successor in self.program.basic_blocks[block_index].succ_blocks:
|
|
468
|
+
if successor < 0:
|
|
469
|
+
continue
|
|
470
|
+
changed = self._merge_definitions(block_inputs[successor], outputs)
|
|
471
|
+
if changed and successor not in queued:
|
|
472
|
+
heapq.heappush(pending, successor)
|
|
473
|
+
queued.add(successor)
|
|
474
|
+
|
|
475
|
+
for block, inputs in zip(self.program.basic_blocks, block_inputs):
|
|
476
|
+
if block.reachable:
|
|
477
|
+
self._expand_block(block, inputs)
|
|
478
|
+
|
|
479
|
+
def _compute_effect(self, block: BasicBlock) -> tuple[Definitions, set[Var]]:
|
|
480
|
+
generated: Definitions = {}
|
|
481
|
+
killed: set[Var] = set()
|
|
482
|
+
for index in block.op_indexes:
|
|
483
|
+
op = self.program.operations[index]
|
|
484
|
+
if op.output:
|
|
485
|
+
written = {index}
|
|
486
|
+
for byte in split(wrap(op.output)):
|
|
487
|
+
generated[byte] = written
|
|
488
|
+
killed.add(byte)
|
|
489
|
+
elif op.opcode == OpCode.STORE:
|
|
490
|
+
# As we do not analyze which part of memory is written,
|
|
491
|
+
# a write to memory won't kill previous definitions of MEMORY.
|
|
492
|
+
generated.setdefault(MEMORY, set()).add(index)
|
|
493
|
+
return generated, killed
|
|
494
|
+
|
|
495
|
+
@staticmethod
|
|
496
|
+
def _apply_effect(
|
|
497
|
+
inputs: Definitions, generated: Definitions, killed: set[Var]
|
|
498
|
+
) -> Definitions:
|
|
499
|
+
# Sets are never modified in place, so they can be shared
|
|
500
|
+
outputs = dict(generated)
|
|
501
|
+
for var, defs in inputs.items():
|
|
502
|
+
if var in killed:
|
|
503
|
+
continue
|
|
504
|
+
outputs[var] = outputs[var] | defs if var in outputs else defs
|
|
505
|
+
return outputs
|
|
506
|
+
|
|
507
|
+
def _expand_block(self, block: BasicBlock, inputs: Definitions):
|
|
508
|
+
"""Computes the definitions reaching each operation of the block."""
|
|
509
|
+
|
|
510
|
+
current = inputs
|
|
511
|
+
for index in block.op_indexes:
|
|
512
|
+
self.program.reaching_defs[index] = current
|
|
513
|
+
current = apply_operation(current, index, self.program.operations[index])
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _merge_definitions(target: Definitions, source: Definitions) -> bool:
|
|
517
|
+
"""Adds source definitions to target ones, returns True if target changed."""
|
|
518
|
+
|
|
519
|
+
changed = False
|
|
520
|
+
for var, defs in source.items():
|
|
521
|
+
target_defs = target.get(var)
|
|
522
|
+
if target_defs is None:
|
|
523
|
+
target[var] = set(defs)
|
|
524
|
+
changed = True
|
|
525
|
+
elif not defs <= target_defs:
|
|
526
|
+
target_defs |= defs
|
|
527
|
+
changed = True
|
|
528
|
+
return changed
|