pcode-graph 1.0.0__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/__init__.py +0 -0
- pcode_graph/__main__.py +3 -0
- pcode_graph/analysis.py +395 -0
- pcode_graph/arch.py +10 -0
- pcode_graph/asm.py +60 -0
- pcode_graph/charac.py +97 -0
- pcode_graph/checker.py +120 -0
- pcode_graph/cli.py +175 -0
- pcode_graph/emulator.py +419 -0
- pcode_graph/gnn_exporter.py +208 -0
- pcode_graph/graph.py +234 -0
- pcode_graph/lief_importer.py +299 -0
- pcode_graph/log.py +36 -0
- pcode_graph/maker.py +369 -0
- pcode_graph/nx_exporter.py +20 -0
- pcode_graph/pcode.py +187 -0
- pcode_graph/registers.py +2929 -0
- pcode_graph/simplifier.py +185 -0
- pcode_graph/torch_utils.py +30 -0
- pcode_graph/translator.py +235 -0
- pcode_graph/utils.py +15 -0
- pcode_graph/view.py +65 -0
- pcode_graph/visitor.py +103 -0
- pcode_graph-1.0.0.dist-info/METADATA +205 -0
- pcode_graph-1.0.0.dist-info/RECORD +28 -0
- pcode_graph-1.0.0.dist-info/WHEEL +4 -0
- pcode_graph-1.0.0.dist-info/entry_points.txt +2 -0
- pcode_graph-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from pcode_graph.checker import check_graph
|
|
2
|
+
from pcode_graph.graph import CDG, Edge, EdgeKinds, NodeIndex, OpCodes, NodeKinds
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GraphSimplifier:
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
graph: CDG,
|
|
13
|
+
) -> None:
|
|
14
|
+
self.graph = graph
|
|
15
|
+
|
|
16
|
+
def _update_proxy(self):
|
|
17
|
+
self._proxy = self.graph.compute_edge_proxy()
|
|
18
|
+
|
|
19
|
+
def _remove_node(self, index: NodeIndex):
|
|
20
|
+
def remap_index(n: NodeIndex):
|
|
21
|
+
assert n != index
|
|
22
|
+
return n - int(n > index)
|
|
23
|
+
|
|
24
|
+
# logger.debug(f"Remove {self.graph.nodes[index]} at {index}")
|
|
25
|
+
|
|
26
|
+
# Remap edges
|
|
27
|
+
proxy = self._proxy[index]
|
|
28
|
+
|
|
29
|
+
control_succs = proxy.get_control_successors()
|
|
30
|
+
assert (
|
|
31
|
+
len(control_succs) <= 1
|
|
32
|
+
), "More than one control successor to the node to remove"
|
|
33
|
+
|
|
34
|
+
data_preds = proxy.get_data_predecessors()
|
|
35
|
+
|
|
36
|
+
# Convert to set to handle particular case of Phi node
|
|
37
|
+
assert len(data_preds) <= 1 or (
|
|
38
|
+
self.graph.nodes[index].kind == NodeKinds.Phi and len(set(data_preds)) == 1
|
|
39
|
+
), "More than one data predecessor to the node to remove"
|
|
40
|
+
|
|
41
|
+
kept_edges: list[Edge] = []
|
|
42
|
+
|
|
43
|
+
def remap_edge(
|
|
44
|
+
kind: EdgeKinds, source: NodeIndex, dest: NodeIndex, operand: int | None
|
|
45
|
+
):
|
|
46
|
+
# logger.debug(
|
|
47
|
+
# f"Remap {self.graph.dump_edge(Edge(kind=kind, source_node=source, destination_node=dest, operand_number=operand))}"
|
|
48
|
+
# )
|
|
49
|
+
edge = Edge(
|
|
50
|
+
source_node=remap_index(source),
|
|
51
|
+
destination_node=remap_index(dest),
|
|
52
|
+
kind=kind,
|
|
53
|
+
operand_number=operand,
|
|
54
|
+
)
|
|
55
|
+
kept_edges.append(edge)
|
|
56
|
+
|
|
57
|
+
for e in self.graph.edges:
|
|
58
|
+
if e.source_node == index:
|
|
59
|
+
if e.kind == EdgeKinds.Data:
|
|
60
|
+
remap_edge(
|
|
61
|
+
e.kind,
|
|
62
|
+
data_preds[0],
|
|
63
|
+
e.destination_node,
|
|
64
|
+
e.operand_number,
|
|
65
|
+
)
|
|
66
|
+
elif e.destination_node == index:
|
|
67
|
+
if e.kind == EdgeKinds.Control:
|
|
68
|
+
remap_edge(
|
|
69
|
+
e.kind,
|
|
70
|
+
e.source_node,
|
|
71
|
+
control_succs[0],
|
|
72
|
+
e.operand_number,
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
remap_edge(e.kind, e.source_node, e.destination_node, e.operand_number)
|
|
76
|
+
|
|
77
|
+
# Patch the graph
|
|
78
|
+
self.graph.edges = kept_edges
|
|
79
|
+
self.graph.nodes.pop(index)
|
|
80
|
+
|
|
81
|
+
self._graph_changed = True
|
|
82
|
+
self._update_proxy()
|
|
83
|
+
|
|
84
|
+
# self.debug_helper()
|
|
85
|
+
|
|
86
|
+
def _remove_edge(self, edge: Edge):
|
|
87
|
+
|
|
88
|
+
self.graph.remove_edge(edge)
|
|
89
|
+
self._graph_changed = True
|
|
90
|
+
self._update_proxy()
|
|
91
|
+
|
|
92
|
+
def debug_helper(self):
|
|
93
|
+
try:
|
|
94
|
+
self.rounds += 1
|
|
95
|
+
except AttributeError:
|
|
96
|
+
self.rounds = 0
|
|
97
|
+
path = Path(f"round{self.rounds}.md")
|
|
98
|
+
path.write_text(str(self.graph))
|
|
99
|
+
logger.debug(f"Written {path}")
|
|
100
|
+
check_graph(self.graph)
|
|
101
|
+
|
|
102
|
+
def process(self):
|
|
103
|
+
|
|
104
|
+
self._graph_changed = True
|
|
105
|
+
self._update_proxy()
|
|
106
|
+
|
|
107
|
+
while self._graph_changed:
|
|
108
|
+
self._graph_changed = False
|
|
109
|
+
|
|
110
|
+
# Remove useless phi nodes
|
|
111
|
+
for index, node in enumerate(self.graph.nodes):
|
|
112
|
+
|
|
113
|
+
if node.kind == NodeKinds.Phi:
|
|
114
|
+
|
|
115
|
+
phi_data_preds = set(self._proxy[index].get_data_predecessors())
|
|
116
|
+
if len(phi_data_preds) > 1:
|
|
117
|
+
# Keep this Phi node
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
self._remove_node(index)
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
elif node.opcode == OpCodes.COPY:
|
|
124
|
+
|
|
125
|
+
ctrl_preds = set(self._proxy[index].get_control_predecessors())
|
|
126
|
+
ctrl_succs = self._proxy[index].get_control_successors()
|
|
127
|
+
|
|
128
|
+
remove = True
|
|
129
|
+
if ctrl_preds and ctrl_succs:
|
|
130
|
+
(ctrl_succ,) = ctrl_succs
|
|
131
|
+
|
|
132
|
+
remove = False
|
|
133
|
+
# Remove copies that dominates their successor...
|
|
134
|
+
if self._proxy[ctrl_succ].get_control_predecessors() == [index]:
|
|
135
|
+
remove = True
|
|
136
|
+
elif len(ctrl_preds) == 1:
|
|
137
|
+
# ...or are dominated by their unique predecessor
|
|
138
|
+
(ctrl_pred,) = ctrl_preds
|
|
139
|
+
if self._proxy[ctrl_pred].get_control_successors() == [
|
|
140
|
+
index
|
|
141
|
+
]:
|
|
142
|
+
remove = True
|
|
143
|
+
|
|
144
|
+
if remove:
|
|
145
|
+
self._remove_node(index)
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
elif node.kind == NodeKinds.Constant:
|
|
149
|
+
if not self._proxy[index].get_data_successors():
|
|
150
|
+
# Remove constants let orphan by other simplifications
|
|
151
|
+
self._remove_node(index)
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
elif node.opcode in {
|
|
155
|
+
OpCodes.INT_ZEXT,
|
|
156
|
+
OpCodes.INT_SEXT,
|
|
157
|
+
}:
|
|
158
|
+
self._remove_node(index)
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
elif node.opcode == OpCodes.SUBPIECE:
|
|
162
|
+
|
|
163
|
+
operands: list[Edge] = self._proxy[index].get_inputs_edges()
|
|
164
|
+
|
|
165
|
+
assert len(operands) == 2
|
|
166
|
+
num_trunc_bytes = self.graph.nodes[operands[1].source_node]
|
|
167
|
+
assert num_trunc_bytes.kind == NodeKinds.Constant
|
|
168
|
+
if num_trunc_bytes.value == 0:
|
|
169
|
+
self._remove_edge(operands[1])
|
|
170
|
+
self._remove_node(index)
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
elif node.opcode == OpCodes.INT_MULT:
|
|
174
|
+
# Remove mulitiplication by one
|
|
175
|
+
operands: list[Edge] = self._proxy[index].get_inputs_edges()
|
|
176
|
+
for op_edge in operands:
|
|
177
|
+
o = op_edge.source_node
|
|
178
|
+
op = self.graph.nodes[o]
|
|
179
|
+
if op.kind == NodeKinds.Constant and op.value == 1:
|
|
180
|
+
self._remove_edge(op_edge)
|
|
181
|
+
self._remove_node(index)
|
|
182
|
+
break
|
|
183
|
+
else:
|
|
184
|
+
continue
|
|
185
|
+
break
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
class BinaryConverter:
|
|
4
|
+
"""Helper to convert between scalar tensor and tensor of bits"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, num_bits: int) -> None:
|
|
7
|
+
self.num_bits = num_bits
|
|
8
|
+
self._min_value = -(2 ** (num_bits - 1))
|
|
9
|
+
self._max_value = 2 ** (num_bits - 1) - 1
|
|
10
|
+
self._mask = 2 ** torch.arange(num_bits - 1, -1, -1, dtype=torch.long)
|
|
11
|
+
|
|
12
|
+
def value_to_bits(self, value: int) -> torch.Tensor:
|
|
13
|
+
"""Converts the given signed value to bits"""
|
|
14
|
+
|
|
15
|
+
if value < self._min_value:
|
|
16
|
+
value = self._min_value
|
|
17
|
+
elif value > self._max_value:
|
|
18
|
+
value = self._max_value
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
torch.tensor(value, dtype=torch.long)
|
|
22
|
+
.unsqueeze(-1)
|
|
23
|
+
.bitwise_and(self._mask)
|
|
24
|
+
.ne(0)
|
|
25
|
+
.float()
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def bits_to_value(self, logits: torch.Tensor) -> torch.Tensor:
|
|
29
|
+
bits = (logits > 0).to(torch.long)
|
|
30
|
+
return torch.sum(self._mask * bits, -1)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Iterator, NamedTuple
|
|
3
|
+
from loguru import logger
|
|
4
|
+
import pypcode
|
|
5
|
+
from pcode_graph.arch import Arch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class ChunkOps:
|
|
10
|
+
"""Result of lifting a piece of binary."""
|
|
11
|
+
|
|
12
|
+
num_instructions: int
|
|
13
|
+
address: int
|
|
14
|
+
operations: list[pypcode.PcodeOp]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Instruction:
|
|
19
|
+
"""Result of disassembling."""
|
|
20
|
+
|
|
21
|
+
address: int
|
|
22
|
+
length: int
|
|
23
|
+
mnem: str
|
|
24
|
+
body: str
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return self.dump(True)
|
|
28
|
+
|
|
29
|
+
def dump(self, print_address: bool):
|
|
30
|
+
s = ""
|
|
31
|
+
if print_address:
|
|
32
|
+
s += f"{self.address:05x} "
|
|
33
|
+
s += self.mnem
|
|
34
|
+
if self.body:
|
|
35
|
+
s += " " + self.body
|
|
36
|
+
return s
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class LiftStats:
|
|
41
|
+
total_bytes: int = 0
|
|
42
|
+
lifted_bytes: int = 0
|
|
43
|
+
skipped_bytes: int = 0
|
|
44
|
+
truncated_bytes: int = 0
|
|
45
|
+
num_instructions: int = 0
|
|
46
|
+
resyncs: int = 0
|
|
47
|
+
translate_calls: int = 0
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def coverage(self) -> float:
|
|
51
|
+
return self.lifted_bytes / self.total_bytes if self.total_bytes else 1.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ArchParams(NamedTuple):
|
|
55
|
+
#: Resynchronisation step, in bytes, after an undecodable instruction.
|
|
56
|
+
#: Must divide every valid instruction address, otherwise recovery lands
|
|
57
|
+
#: mid-instruction.
|
|
58
|
+
alignment: int
|
|
59
|
+
#: Longest single instruction encoding, in bytes.
|
|
60
|
+
max_instruction_size: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
ARCH_PARAMETERS: dict[Arch, ArchParams] = {
|
|
64
|
+
Arch.x86_32: ArchParams(1, 15),
|
|
65
|
+
Arch.x86_64: ArchParams(1, 15),
|
|
66
|
+
Arch.arm_32: ArchParams(4, 4),
|
|
67
|
+
Arch.arm_64: ArchParams(4, 4),
|
|
68
|
+
Arch.mips_32: ArchParams(4, 4),
|
|
69
|
+
Arch.mips_64: ArchParams(4, 4),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_pcode_context(arch: Arch) -> pypcode.Context:
|
|
74
|
+
match arch:
|
|
75
|
+
case Arch.arm_32:
|
|
76
|
+
return pypcode.Context("ARM:LE:32:v8")
|
|
77
|
+
case Arch.arm_64:
|
|
78
|
+
return pypcode.Context("AARCH64:LE:64:v8A")
|
|
79
|
+
# return pypcode.Context("AARCH64:LE:64:AppleSilicon")
|
|
80
|
+
case Arch.x86_32:
|
|
81
|
+
return pypcode.Context("x86:LE:32:default")
|
|
82
|
+
case Arch.x86_64:
|
|
83
|
+
return pypcode.Context("x86:LE:64:default")
|
|
84
|
+
case Arch.mips_32:
|
|
85
|
+
return pypcode.Context("MIPS:BE:32:default")
|
|
86
|
+
case Arch.mips_64:
|
|
87
|
+
return pypcode.Context("MIPS:BE:64:default")
|
|
88
|
+
case _:
|
|
89
|
+
raise ValueError(f"Unknown architecture {arch}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_pcode_context_arch(context: pypcode.Context) -> Arch:
|
|
93
|
+
size = int(context.language.size)
|
|
94
|
+
processor = context.language.processor
|
|
95
|
+
match processor:
|
|
96
|
+
case "ARM":
|
|
97
|
+
if size == 32:
|
|
98
|
+
return Arch.arm_32
|
|
99
|
+
case "AARCH64":
|
|
100
|
+
if size == 64:
|
|
101
|
+
return Arch.arm_64
|
|
102
|
+
case "MIPS":
|
|
103
|
+
match size:
|
|
104
|
+
case 32:
|
|
105
|
+
return Arch.mips_32
|
|
106
|
+
case 64:
|
|
107
|
+
return Arch.mips_64
|
|
108
|
+
case "x86":
|
|
109
|
+
match size:
|
|
110
|
+
case 32:
|
|
111
|
+
return Arch.x86_32
|
|
112
|
+
case 64:
|
|
113
|
+
return Arch.x86_64
|
|
114
|
+
|
|
115
|
+
raise ValueError(f"Unsupported context with {processor=} and {size=}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Translator:
|
|
119
|
+
|
|
120
|
+
def __init__(self, arch_context: pypcode.Context | Arch):
|
|
121
|
+
if isinstance(arch_context, Arch):
|
|
122
|
+
self.arch = arch_context
|
|
123
|
+
self.context = create_pcode_context(self.arch)
|
|
124
|
+
else:
|
|
125
|
+
self.context = arch_context
|
|
126
|
+
self.arch = get_pcode_context_arch(arch_context)
|
|
127
|
+
|
|
128
|
+
self.alignment, self.max_instr_size = ARCH_PARAMETERS[self.arch]
|
|
129
|
+
|
|
130
|
+
def translate(self, data: bytes, base_address: int) -> list[pypcode.PcodeOp]:
|
|
131
|
+
"""Translates a piece of code into P-Code operations."""
|
|
132
|
+
|
|
133
|
+
ops = []
|
|
134
|
+
for chunk in self.iter_operations(data, base_address):
|
|
135
|
+
ops += chunk.operations
|
|
136
|
+
return ops
|
|
137
|
+
|
|
138
|
+
def iter_operations(
|
|
139
|
+
self,
|
|
140
|
+
data: bytes,
|
|
141
|
+
base_address: int,
|
|
142
|
+
window: int = 4096,
|
|
143
|
+
max_instructions: int = 1024,
|
|
144
|
+
stats: LiftStats | None = None,
|
|
145
|
+
) -> Iterator[ChunkOps]:
|
|
146
|
+
"""Translates a piece of code into P-Code, handling errors and avoiding
|
|
147
|
+
out-of-memory for big sections.
|
|
148
|
+
|
|
149
|
+
Each ChunkOps covers a contiguous range: undecodable bytes are skipped,
|
|
150
|
+
``instruction_alignment`` at a time. ``window`` and ``max_instructions``
|
|
151
|
+
both bound the memory of a single call.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
assert window > 1 and max_instructions > 1
|
|
155
|
+
if stats is None:
|
|
156
|
+
stats = LiftStats()
|
|
157
|
+
stats.total_bytes += len(data)
|
|
158
|
+
|
|
159
|
+
offset = 0
|
|
160
|
+
padded_data = data
|
|
161
|
+
if self.arch in {Arch.mips_32, Arch.mips_64}:
|
|
162
|
+
# Avoid delay slot exception
|
|
163
|
+
padded_data = data + b"\x00\x00\x00\x00"
|
|
164
|
+
|
|
165
|
+
while offset < len(data):
|
|
166
|
+
window_base = base_address + offset
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
stats.translate_calls += 1
|
|
170
|
+
ops = self.context.translate(
|
|
171
|
+
padded_data,
|
|
172
|
+
window_base,
|
|
173
|
+
max_instructions=max_instructions,
|
|
174
|
+
offset=offset,
|
|
175
|
+
max_bytes=window,
|
|
176
|
+
).ops
|
|
177
|
+
except (IndexError, pypcode.BadDataError, pypcode.UnimplError, pypcode.LowlevelError):
|
|
178
|
+
ops = []
|
|
179
|
+
|
|
180
|
+
chunk: list[pypcode.PcodeOp] = []
|
|
181
|
+
num_instructions = 0
|
|
182
|
+
consumed_bytes = 0
|
|
183
|
+
truncated = False
|
|
184
|
+
|
|
185
|
+
for op in ops:
|
|
186
|
+
if op.opcode == pypcode.OpCode.IMARK:
|
|
187
|
+
# A single IMARK may cover several instructions (grouped delay slots)
|
|
188
|
+
# Do not assume the varnodes are sorted.
|
|
189
|
+
end = max(i.offset + i.size for i in op.inputs) - base_address
|
|
190
|
+
|
|
191
|
+
# pypcode silently zero-pads past the end of the buffer
|
|
192
|
+
# and reports the instruction's full length, so the
|
|
193
|
+
# last instruction of a section can be entirely
|
|
194
|
+
# fabricated. Any instruction consuming a padding byte
|
|
195
|
+
# necessarily ends past `size`, which makes this test
|
|
196
|
+
# both sufficient and exact.
|
|
197
|
+
if end > len(data):
|
|
198
|
+
truncated = True
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
num_instructions += len(op.inputs)
|
|
202
|
+
consumed_bytes = max(consumed_bytes, end - offset)
|
|
203
|
+
|
|
204
|
+
chunk.append(op)
|
|
205
|
+
|
|
206
|
+
if consumed_bytes > 0:
|
|
207
|
+
stats.lifted_bytes += consumed_bytes
|
|
208
|
+
stats.num_instructions += num_instructions
|
|
209
|
+
yield ChunkOps(num_instructions, window_base, chunk)
|
|
210
|
+
offset += consumed_bytes
|
|
211
|
+
|
|
212
|
+
if truncated:
|
|
213
|
+
stats.truncated_bytes += len(data) - offset
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
if consumed_bytes == 0:
|
|
217
|
+
# Nothing decodable at `offset`: resynchronise.
|
|
218
|
+
stats.skipped_bytes += min(self.alignment, len(data) - offset)
|
|
219
|
+
stats.resyncs += 1
|
|
220
|
+
offset += self.alignment
|
|
221
|
+
|
|
222
|
+
def disassemble(self, code: bytes, base_address: int) -> Iterator[Instruction]:
|
|
223
|
+
data = self.context.disassemble(code, base_address=base_address)
|
|
224
|
+
for i in data.instructions:
|
|
225
|
+
yield Instruction(i.addr.offset, i.length, i.mnem, i.body)
|
|
226
|
+
|
|
227
|
+
def dump_asm(
|
|
228
|
+
self,
|
|
229
|
+
code: bytes,
|
|
230
|
+
base_address: int,
|
|
231
|
+
print_address: bool = True,
|
|
232
|
+
) -> str:
|
|
233
|
+
return "\n".join(
|
|
234
|
+
i.dump(print_address) for i in self.disassemble(code, base_address)
|
|
235
|
+
)
|
pcode_graph/utils.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
def unsigned_to_signed(value: int, size_bytes: int) -> int:
|
|
2
|
+
"""Converts the given unsigned value into a signed one."""
|
|
3
|
+
|
|
4
|
+
assert size_bytes > 0
|
|
5
|
+
|
|
6
|
+
mask = 1 << (size_bytes * 8 - 1)
|
|
7
|
+
return (value ^ mask) - mask
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def signed_to_unsigned(value: int, size_bytes: int) -> int:
|
|
11
|
+
"""Converts the given signed value into an unsigned one."""
|
|
12
|
+
|
|
13
|
+
return value + (value < 0) * (2 ** (size_bytes * 8))
|
|
14
|
+
|
|
15
|
+
|
pcode_graph/view.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from loguru import logger
|
|
3
|
+
from pyvis.network import Network
|
|
4
|
+
|
|
5
|
+
from pcode_graph.graph import CDG, EdgeKinds, NodeKinds
|
|
6
|
+
from pcode_graph.pcode import JUMP_OPCODES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def draw_graph(
|
|
10
|
+
graph: CDG,
|
|
11
|
+
out_file: Path,
|
|
12
|
+
open_browser: bool = False,
|
|
13
|
+
print_ids: bool = False,
|
|
14
|
+
):
|
|
15
|
+
"""Creates a HTML representation of the given graph."""
|
|
16
|
+
|
|
17
|
+
parent_dir = out_file.parent
|
|
18
|
+
parent_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
network = Network(width="1200", height="1200", directed=True)
|
|
21
|
+
|
|
22
|
+
# Create nodes
|
|
23
|
+
for index, node in enumerate(graph.nodes):
|
|
24
|
+
style: dict
|
|
25
|
+
|
|
26
|
+
match node.kind:
|
|
27
|
+
case NodeKinds.InputRegister | NodeKinds.ReadMemory:
|
|
28
|
+
style = dict(color="#c0d0ff") # Light Blue
|
|
29
|
+
case NodeKinds.OutputRegister | NodeKinds.WrittenMemory:
|
|
30
|
+
style = dict(color="#7070da") # Blue
|
|
31
|
+
case NodeKinds.Constant:
|
|
32
|
+
style = dict(color="#ff8080") # Red
|
|
33
|
+
case NodeKinds.Begin | NodeKinds.End | NodeKinds.External:
|
|
34
|
+
style = dict(color="#70d660", shape="box")
|
|
35
|
+
case NodeKinds.Operation | NodeKinds.Phi:
|
|
36
|
+
if node.opcode in JUMP_OPCODES:
|
|
37
|
+
# Jumps in light Green
|
|
38
|
+
style = dict(color="#ab2a", shape="box")
|
|
39
|
+
else:
|
|
40
|
+
# Other operations in Grey
|
|
41
|
+
style = dict(color="#d1c5c5", shape="box")
|
|
42
|
+
case _:
|
|
43
|
+
raise ValueError(node.kind)
|
|
44
|
+
|
|
45
|
+
label = str(node)
|
|
46
|
+
if print_ids:
|
|
47
|
+
label += " " + str(index)
|
|
48
|
+
style["label"] = label
|
|
49
|
+
network.add_node(index, **style)
|
|
50
|
+
|
|
51
|
+
# Create edges
|
|
52
|
+
for edge in graph.edges:
|
|
53
|
+
network.add_edge(
|
|
54
|
+
edge.source_node,
|
|
55
|
+
edge.destination_node,
|
|
56
|
+
style={"dashes": edge.kind == EdgeKinds.Control},
|
|
57
|
+
label=str(edge.operand_number) if edge.operand_number is not None else "",
|
|
58
|
+
color="#a1a5c5" if edge.kind == EdgeKinds.Data else "#70d660",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Layout
|
|
62
|
+
network.force_atlas_2based(spring_length=75)
|
|
63
|
+
|
|
64
|
+
network.write_html(out_file.as_posix(), open_browser=open_browser)
|
|
65
|
+
logger.info(f"Written {out_file}")
|
pcode_graph/visitor.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from pcode_graph.pcode import OpCodes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PcodeVisitor:
|
|
7
|
+
"""A hierarchical visitor to P-Code operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._pcode_callbacks: dict[OpCodes | None, Callable] = {
|
|
11
|
+
opcode: getattr(self, "visit_" + opcode.name.lower()) for opcode in OpCodes
|
|
12
|
+
}
|
|
13
|
+
self._pcode_callbacks[None] = self.visit_nop
|
|
14
|
+
|
|
15
|
+
def visit(self, opcode: OpCodes | None, *args, **kwargs):
|
|
16
|
+
"Visitor main entry point."
|
|
17
|
+
return self._pcode_callbacks[opcode](*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
def visit_nop(self, *args, **kwargs):
|
|
20
|
+
return self.visit_default(*args, **kwargs)
|
|
21
|
+
|
|
22
|
+
def visit_binary(self, *args, **kwargs):
|
|
23
|
+
"A binary logical or arithmetical operation."
|
|
24
|
+
return self.visit_default(*args, **kwargs)
|
|
25
|
+
|
|
26
|
+
def visit_unary(self, *args, **kwargs):
|
|
27
|
+
"An unary logical or arithmetical operation."
|
|
28
|
+
return self.visit_default(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
def visit_jump(self, *args, **kwargs):
|
|
31
|
+
"Any operation impacting the usual instruction flow."
|
|
32
|
+
return self.visit_default(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def visit_default(self, *args, **kwargs):
|
|
35
|
+
"Catch-all callback"
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
# fmt: off
|
|
39
|
+
def visit_invalid(self, *args, **kwargs): return self.visit_default(*args, **kwargs)
|
|
40
|
+
def visit_bool_and(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
41
|
+
def visit_bool_negate(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
42
|
+
def visit_bool_or(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
43
|
+
def visit_bool_xor(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
44
|
+
def visit_branch(self, *args, **kwargs): return self.visit_jump(*args, **kwargs)
|
|
45
|
+
def visit_branchind(self, *args, **kwargs): return self.visit_jump(*args, **kwargs)
|
|
46
|
+
def visit_call(self, *args, **kwargs): return self.visit_branch(*args, **kwargs)
|
|
47
|
+
def visit_callind(self, *args, **kwargs): return self.visit_branchind(*args, **kwargs)
|
|
48
|
+
def visit_callother(self, *args, **kwargs): return self.visit_default(*args, **kwargs) # Note: callother is not a implemented as a branching instruction but is followed by one
|
|
49
|
+
def visit_cbranch(self, *args, **kwargs): return self.visit_jump(*args, **kwargs)
|
|
50
|
+
def visit_copy(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
51
|
+
def visit_float_abs(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
52
|
+
def visit_float_add(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
53
|
+
def visit_float_ceil(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
54
|
+
def visit_float_div(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
55
|
+
def visit_float_equal(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
56
|
+
def visit_float_float2float(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
57
|
+
def visit_float_floor(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
58
|
+
def visit_float_int2float(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
59
|
+
def visit_float_less(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
60
|
+
def visit_float_lessequal(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
61
|
+
def visit_float_mult(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
62
|
+
def visit_float_nan(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
63
|
+
def visit_float_neg(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
64
|
+
def visit_float_notequal(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
65
|
+
def visit_float_round(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
66
|
+
def visit_float_sqrt(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
67
|
+
def visit_float_sub(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
68
|
+
def visit_float_trunc(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
69
|
+
def visit_int_2comp(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
70
|
+
def visit_int_add(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
71
|
+
def visit_int_and(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
72
|
+
def visit_int_carry(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
73
|
+
def visit_int_div(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
74
|
+
def visit_int_equal(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
75
|
+
def visit_int_left(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
76
|
+
def visit_int_less(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
77
|
+
def visit_int_lessequal(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
78
|
+
def visit_int_mult(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
79
|
+
def visit_int_negate(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
80
|
+
def visit_int_notequal(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
81
|
+
def visit_int_or(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
82
|
+
def visit_int_rem(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
83
|
+
def visit_int_right(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
84
|
+
def visit_int_sborrow(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
85
|
+
def visit_int_scarry(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
86
|
+
def visit_int_sdiv(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
87
|
+
def visit_int_sext(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
88
|
+
def visit_int_sless(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
89
|
+
def visit_int_slessequal(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
90
|
+
def visit_int_srem(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
91
|
+
def visit_int_sright(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
92
|
+
def visit_int_sub(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
93
|
+
def visit_int_xor(self, *args, **kwargs): return self.visit_binary(*args, **kwargs)
|
|
94
|
+
def visit_int_zext(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
95
|
+
def visit_load(self, *args, **kwargs): return self.visit_default(*args, **kwargs)
|
|
96
|
+
def visit_lzcount(self, *args, **kwargs): return self.visit_unary(*args, **kwargs)
|
|
97
|
+
def visit_piece(self, *args, **kwargs): return self.visit_default(*args, **kwargs)
|
|
98
|
+
def visit_popcount(self, *args, **kwargs): return self.visit_default(*args, **kwargs)
|
|
99
|
+
def visit_return(self, *args, **kwargs): return self.visit_branchind(*args, **kwargs)
|
|
100
|
+
def visit_segmentop(self, *args, **kwargs): return self.visit_default(*args, **kwargs)
|
|
101
|
+
def visit_store(self, *args, **kwargs): return self.visit_default(*args, **kwargs)
|
|
102
|
+
def visit_subpiece(self, *args, **kwargs): return self.visit_default(*args, **kwargs)
|
|
103
|
+
# fmt: on
|