pcode-graph 1.1.2__py3-none-any.whl → 2.0.2__py3-none-any.whl
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/analysis.py +302 -170
- pcode_graph/basic_blocks.py +196 -0
- pcode_graph/{utils.py → bit_utils.py} +0 -2
- pcode_graph/{charac.py → characterization.py} +5 -5
- pcode_graph/checker.py +177 -62
- pcode_graph/cli.py +7 -3
- pcode_graph/emulator.py +1 -1
- pcode_graph/gnn_exporter.py +19 -17
- pcode_graph/graph.py +20 -10
- pcode_graph/lief_importer.py +3 -5
- pcode_graph/maker.py +30 -13
- pcode_graph/nx_exporter.py +7 -6
- pcode_graph/pcode.py +1 -2
- pcode_graph/registers.py +0 -1
- pcode_graph/simplifier.py +1 -2
- pcode_graph/torch_utils.py +1 -0
- pcode_graph/translator.py +8 -3
- pcode_graph/view.py +21 -11
- pcode_graph/visitor.py +1 -1
- {pcode_graph-1.1.2.dist-info → pcode_graph-2.0.2.dist-info}/METADATA +2 -2
- pcode_graph-2.0.2.dist-info/RECORD +29 -0
- {pcode_graph-1.1.2.dist-info → pcode_graph-2.0.2.dist-info}/WHEEL +1 -1
- pcode_graph-1.1.2.dist-info/RECORD +0 -28
- {pcode_graph-1.1.2.dist-info → pcode_graph-2.0.2.dist-info}/entry_points.txt +0 -0
- {pcode_graph-1.1.2.dist-info → pcode_graph-2.0.2.dist-info}/licenses/LICENSE +0 -0
pcode_graph/analysis.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from enum import Enum
|
|
3
|
+
from functools import cache
|
|
4
|
+
import heapq
|
|
3
5
|
from typing import Iterator, override
|
|
4
6
|
from pypcode import OpCode, PcodeOp, Varnode
|
|
5
7
|
from pcode_graph.arch import Arch
|
|
6
8
|
from pcode_graph.pcode import dump_operation, from_native_opcode
|
|
9
|
+
from pcode_graph.bit_utils import unsigned_to_signed
|
|
7
10
|
from pcode_graph.visitor import PcodeVisitor
|
|
8
11
|
|
|
9
12
|
OpIndex = int
|
|
@@ -13,6 +16,8 @@ ExitIndex = -3
|
|
|
13
16
|
|
|
14
17
|
Address = int
|
|
15
18
|
|
|
19
|
+
BlockIndex = int
|
|
20
|
+
|
|
16
21
|
|
|
17
22
|
def dump_index(i: OpIndex) -> str:
|
|
18
23
|
match i:
|
|
@@ -45,7 +50,7 @@ class Var:
|
|
|
45
50
|
"""Hashable and comparable wrapper for temporaries, registers, constants and memory."""
|
|
46
51
|
|
|
47
52
|
kind: VarKinds
|
|
48
|
-
size: int
|
|
53
|
+
size: int
|
|
49
54
|
value: int
|
|
50
55
|
pretty_name: str = field(compare=False)
|
|
51
56
|
|
|
@@ -71,6 +76,71 @@ def wrap(var: Varnode) -> Var:
|
|
|
71
76
|
# Special variable singleton
|
|
72
77
|
MEMORY = Var(VarKinds.IndirectAccess, 0, 0, "MEMORY")
|
|
73
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
|
+
|
|
74
144
|
|
|
75
145
|
@dataclass
|
|
76
146
|
class RichPcodeList:
|
|
@@ -82,21 +152,18 @@ class RichPcodeList:
|
|
|
82
152
|
# List of P-Code operations
|
|
83
153
|
operations: list[PcodeOp]
|
|
84
154
|
|
|
85
|
-
# CFG edges: src/dst indexes on the previous list.
|
|
86
|
-
# Order of successors matters for cbranch, call, etc.
|
|
87
|
-
predecessors: dict[OpIndex, set[OpIndex]] = field(default_factory=dict)
|
|
88
|
-
successors: dict[OpIndex, list[OpIndex]] = field(default_factory=dict)
|
|
89
|
-
|
|
90
155
|
# First P-Code operation for each instruction address
|
|
91
156
|
addresses: dict[Address, OpIndex] = field(default_factory=dict)
|
|
92
157
|
|
|
93
|
-
#
|
|
94
|
-
|
|
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)
|
|
95
161
|
|
|
96
162
|
# Reaching definitions. For each operation, by variable, list of:
|
|
97
163
|
# - definitions (= writing operation index) that reach this operation,
|
|
98
164
|
# - or EntryIndex (for initial definitions of inputs).
|
|
99
|
-
|
|
165
|
+
# Sets are shared between operations: do not modify them.
|
|
166
|
+
reaching_defs: list[Definitions] = field(default_factory=list)
|
|
100
167
|
|
|
101
168
|
def __post_init__(self):
|
|
102
169
|
InstructionAddressCollector(self)()
|
|
@@ -107,6 +174,34 @@ class RichPcodeList:
|
|
|
107
174
|
def __str__(self) -> str:
|
|
108
175
|
return self.dump()
|
|
109
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
|
+
|
|
110
205
|
def dump(self) -> str:
|
|
111
206
|
"""Dumps the program and analysis results in a markdown table."""
|
|
112
207
|
|
|
@@ -116,8 +211,8 @@ class RichPcodeList:
|
|
|
116
211
|
s += "|--|--|--|--|--|--|--|\n"
|
|
117
212
|
for i, op in enumerate(self.operations):
|
|
118
213
|
s += f"|{i}|{dump_operation(op)}|"
|
|
119
|
-
s += ", ".join(dump_index(p) for p in self.
|
|
120
|
-
s += ", ".join(dump_index(p) for p in self.
|
|
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)) + "|"
|
|
121
216
|
var_defs = []
|
|
122
217
|
for var, defs in self.iter_input_defs(i):
|
|
123
218
|
if defs:
|
|
@@ -126,7 +221,7 @@ class RichPcodeList:
|
|
|
126
221
|
)
|
|
127
222
|
|
|
128
223
|
s += ", ".join(var_defs) + "|"
|
|
129
|
-
if self.reachable
|
|
224
|
+
if self.reachable(i):
|
|
130
225
|
s += "x"
|
|
131
226
|
s += "|"
|
|
132
227
|
exit_def_here = []
|
|
@@ -143,42 +238,40 @@ class RichPcodeList:
|
|
|
143
238
|
def get_defs(self, index: OpIndex, var: Var) -> set[OpIndex]:
|
|
144
239
|
"""Returns all the operations defining the value at given operation of the given variable."""
|
|
145
240
|
|
|
146
|
-
|
|
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
|
|
147
246
|
|
|
148
|
-
def get_exit_defs(self) ->
|
|
247
|
+
def get_exit_defs(self) -> Definitions:
|
|
149
248
|
"""Computes side effects of the program."""
|
|
150
249
|
|
|
151
|
-
exit_defs:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
continue
|
|
156
|
-
|
|
157
|
-
if not {UnknownCodeIndex, ExitIndex}.intersection(
|
|
158
|
-
self.successors.get(index, [])
|
|
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
|
|
159
254
|
):
|
|
160
|
-
# Not an exit
|
|
161
255
|
continue
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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:
|
|
165
262
|
# Ignore P-Code temporary
|
|
166
263
|
continue
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
var = wrap(op.output)
|
|
173
|
-
if var.kind != VarKinds.Unique:
|
|
174
|
-
exit_defs[var] = {index}
|
|
175
|
-
elif op.opcode == OpCode.STORE:
|
|
176
|
-
# Do not kill other memory defs
|
|
177
|
-
exit_defs.setdefault(MEMORY, set()).add(index)
|
|
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)
|
|
178
269
|
return exit_defs
|
|
179
270
|
|
|
180
271
|
def iter_inputs(self, index: OpIndex) -> Iterator[Var]:
|
|
181
|
-
"""Iterates operation inputs, skipping **special** ones
|
|
272
|
+
"""Iterates operation inputs, skipping **special** ones
|
|
273
|
+
that have no semantic interest to put in the graph (see P-Code documentation).
|
|
274
|
+
"""
|
|
182
275
|
|
|
183
276
|
op = self.operations[index]
|
|
184
277
|
if op.opcode == OpCode.LOAD:
|
|
@@ -192,6 +285,7 @@ class RichPcodeList:
|
|
|
192
285
|
OpCode.CALL,
|
|
193
286
|
OpCode.LOAD,
|
|
194
287
|
OpCode.STORE,
|
|
288
|
+
OpCode.CALLOTHER,
|
|
195
289
|
}:
|
|
196
290
|
yield wrap(var)
|
|
197
291
|
|
|
@@ -219,178 +313,216 @@ class InstructionAddressCollector(PassBase):
|
|
|
219
313
|
|
|
220
314
|
|
|
221
315
|
class CFGBuilder(PassBase):
|
|
222
|
-
"""List the CFG edges"""
|
|
223
316
|
|
|
224
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)
|
|
225
346
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
for o, op in enumerate(self.program.operations):
|
|
230
|
-
opcode = from_native_opcode(op.opcode)
|
|
231
|
-
self.visit(opcode, o, op)
|
|
232
|
-
|
|
233
|
-
for src, dst in self._edges:
|
|
234
|
-
assert src >= 0 and src < len(self.program.operations)
|
|
235
|
-
self.program.successors.setdefault(src, []).append(dst)
|
|
236
|
-
if dst not in {UnknownCodeIndex, ExitIndex}:
|
|
237
|
-
self.program.predecessors.setdefault(dst, set()).add(src)
|
|
347
|
+
if self.program.basic_blocks:
|
|
348
|
+
self.program.basic_blocks[0].pred_blocks.add(EntryIndex)
|
|
238
349
|
|
|
239
|
-
|
|
350
|
+
def _next_index(self, index: OpIndex) -> OpIndex:
|
|
351
|
+
return index + 1 if index + 1 < len(self.program.operations) else ExitIndex
|
|
240
352
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
next_index = index + 1
|
|
245
|
-
if next_index == len(self.program.operations):
|
|
246
|
-
next_index = ExitIndex
|
|
247
|
-
self._edges.append((index, next_index))
|
|
248
|
-
|
|
249
|
-
def _get_jump_edge(self, index: int, op: PcodeOp):
|
|
250
|
-
# Edge to branched operation
|
|
251
|
-
address_op = op.inputs[0]
|
|
252
|
-
match address_op.space.name:
|
|
353
|
+
def _get_jump_target(self, index: OpIndex, op: PcodeOp) -> OpIndex:
|
|
354
|
+
address = op.inputs[0]
|
|
355
|
+
match address.space.name:
|
|
253
356
|
case "ram":
|
|
254
|
-
|
|
255
|
-
address_op.offset, UnknownCodeIndex
|
|
256
|
-
)
|
|
257
|
-
self._edges.append((index, target_index))
|
|
357
|
+
return self.program.addresses.get(address.offset, UnknownCodeIndex)
|
|
258
358
|
case "const":
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
offset = address_op.offset - (1 << bits)
|
|
264
|
-
else:
|
|
265
|
-
offset = address_op.offset
|
|
266
|
-
self._edges.append((index, index + offset))
|
|
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
|
|
267
363
|
case "register" | "unique":
|
|
268
|
-
|
|
364
|
+
return UnknownCodeIndex
|
|
269
365
|
case _ as name:
|
|
270
366
|
raise ValueError(f"Unexpected address space for jump address: {name}")
|
|
271
367
|
|
|
272
368
|
@override
|
|
273
|
-
def
|
|
274
|
-
|
|
275
|
-
self._get_jump_edge(index, op)
|
|
369
|
+
def visit_default(self, index: OpIndex, op: PcodeOp) -> None:
|
|
370
|
+
pass
|
|
276
371
|
|
|
277
372
|
@override
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
self._get_jump_edge(index, op)
|
|
373
|
+
def visit_branch(self, index: OpIndex, op: PcodeOp) -> None:
|
|
374
|
+
self._jumps[index] = [self._get_jump_target(index, op)]
|
|
281
375
|
|
|
282
376
|
@override
|
|
283
|
-
def
|
|
284
|
-
|
|
285
|
-
self._get_jump_edge(index, op)
|
|
377
|
+
def visit_branchind(self, index: OpIndex, op: PcodeOp) -> None:
|
|
378
|
+
self._jumps[index] = [self._get_jump_target(index, op)]
|
|
286
379
|
|
|
287
380
|
@override
|
|
288
|
-
def
|
|
289
|
-
self.
|
|
290
|
-
if self._edges[-1][-1] is UnknownCodeIndex:
|
|
291
|
-
# Calling an unseen function: we suppose it returns here after
|
|
292
|
-
self.visit_default(index, op)
|
|
381
|
+
def visit_return(self, index: OpIndex, op: PcodeOp) -> None:
|
|
382
|
+
self._jumps[index] = [self._get_jump_target(index, op)]
|
|
293
383
|
|
|
294
384
|
@override
|
|
295
|
-
def
|
|
296
|
-
self.
|
|
297
|
-
if
|
|
385
|
+
def visit_call(self, index: OpIndex, op: PcodeOp) -> None:
|
|
386
|
+
target = self._get_jump_target(index, op)
|
|
387
|
+
if target == UnknownCodeIndex:
|
|
298
388
|
# Calling an unseen function: we suppose it returns here after
|
|
299
|
-
self.
|
|
389
|
+
self._jumps[index] = [target, self._next_index(index)]
|
|
390
|
+
else:
|
|
391
|
+
self._jumps[index] = [target]
|
|
300
392
|
|
|
301
393
|
@override
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
self._get_jump_edge(index, op)
|
|
305
|
-
self.visit_default(index, op)
|
|
394
|
+
def visit_callind(self, index: OpIndex, op: PcodeOp) -> None:
|
|
395
|
+
self.visit_call(index, op)
|
|
306
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)]
|
|
307
400
|
|
|
308
|
-
|
|
309
|
-
def
|
|
310
|
-
self.
|
|
311
|
-
active: set[int] = {0}
|
|
312
|
-
while active:
|
|
313
|
-
index = active.pop()
|
|
314
|
-
self.program.reachable[index] = True
|
|
315
|
-
for s in self.program.successors.get(index, []):
|
|
316
|
-
if s >= 0 and not self.program.reachable[s]:
|
|
317
|
-
active.add(s)
|
|
401
|
+
@override
|
|
402
|
+
def visit_callother(self, index: OpIndex, op: PcodeOp) -> None:
|
|
403
|
+
self._jumps[index] = [UnknownCodeIndex, self._next_index(index)]
|
|
318
404
|
|
|
319
405
|
|
|
320
|
-
class
|
|
406
|
+
class UnreachableCodeFinder(PassBase):
|
|
407
|
+
|
|
321
408
|
def __call__(self):
|
|
322
409
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
)
|
|
326
419
|
|
|
327
|
-
# To detect the end of propagation
|
|
328
|
-
changed: bool = True
|
|
329
420
|
|
|
330
|
-
|
|
331
|
-
nonlocal changed
|
|
332
|
-
assert to_op >= 0
|
|
421
|
+
class ReachingDefBuilder(PassBase):
|
|
333
422
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
changed = True
|
|
423
|
+
def __call__(self):
|
|
424
|
+
self.program.reaching_defs = [{} for _ in self.program.operations]
|
|
425
|
+
if not self.program.basic_blocks:
|
|
426
|
+
return
|
|
339
427
|
|
|
340
|
-
|
|
341
|
-
for index, op in enumerate(self.program.operations):
|
|
428
|
+
block_inputs: list[Definitions] = [{} for _ in self.program.basic_blocks]
|
|
342
429
|
|
|
343
|
-
|
|
430
|
+
# Initialize
|
|
431
|
+
for index in range(len(self.program.operations)):
|
|
432
|
+
if not self.program.reachable(index):
|
|
344
433
|
continue
|
|
345
|
-
|
|
346
|
-
if op.output:
|
|
347
|
-
var = wrap(op.output)
|
|
348
|
-
for succ in self.program.successors.get(index, []):
|
|
349
|
-
if succ >= 0:
|
|
350
|
-
add_def(var, succ, index)
|
|
351
|
-
|
|
352
|
-
elif op.opcode == OpCode.STORE:
|
|
353
|
-
for succ in self.program.successors.get(index, []):
|
|
354
|
-
if succ >= 0:
|
|
355
|
-
add_def(MEMORY, succ, index)
|
|
356
|
-
|
|
357
434
|
for var in self.program.iter_inputs(index):
|
|
358
435
|
if var.kind in {
|
|
359
436
|
VarKinds.IndirectAccess,
|
|
360
437
|
VarKinds.Ram,
|
|
361
438
|
VarKinds.Register,
|
|
362
439
|
}:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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)
|
|
377
466
|
|
|
378
|
-
|
|
467
|
+
for successor in self.program.basic_blocks[block_index].succ_blocks:
|
|
468
|
+
if successor < 0:
|
|
379
469
|
continue
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|