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.
@@ -0,0 +1,208 @@
1
+ from torch import Tensor
2
+ import torch
3
+ from torch_geometric.data import Data
4
+ from pcode_graph.arch import Arch
5
+ from pcode_graph.pcode import OpCodes
6
+ from pcode_graph.registers import ALIAS, CALLING_CONVENTIONS, PARENTS
7
+ from pcode_graph.torch_utils import BinaryConverter
8
+ from pcode_graph.graph import EdgeKinds, NodeKinds, Node, CDG
9
+
10
+
11
+ def map_registers(registers: set[str], arch: Arch) -> dict[str, Tensor]:
12
+ """Maps the given register names to a one-hot encoded mask,
13
+ handling registers aliases."""
14
+
15
+ kept_regs: set[str] = set()
16
+
17
+ for r in registers:
18
+ kept_regs.add(PARENTS[arch].get(r, r))
19
+
20
+ masks = {}
21
+
22
+ zeros = torch.zeros((len(kept_regs),), dtype=torch.float32)
23
+ for i, r in enumerate(kept_regs):
24
+ mask = zeros.clone()
25
+ mask[i] = 1
26
+
27
+ return masks
28
+
29
+
30
+ def map_calling_convention_registers(arch: Arch) -> dict[str, Tensor]:
31
+ """Maps the register involved in parameter passing and value-return to a bit mask."""
32
+
33
+ lengths: dict[str, int] = {}
34
+
35
+ for arch_cc in CALLING_CONVENTIONS.values():
36
+ for k, regs in arch_cc.items():
37
+ lengths[k] = max(len(regs), lengths.get(k, 0))
38
+
39
+ arch_cc = CALLING_CONVENTIONS[arch]
40
+ registers = sum(arch_cc.values(), start=[])
41
+ zeros = torch.zeros((sum(lengths.values()),), dtype=torch.float)
42
+ masks: dict[str, Tensor] = {r: zeros.clone() for r in registers}
43
+
44
+ i = 0
45
+ for key, l in lengths.items():
46
+ for j, r in enumerate(arch_cc.get(key, [])):
47
+ masks[r][i + j] = 1
48
+ i += l
49
+
50
+ for r in registers:
51
+ for a in ALIAS[arch].get(r, set()):
52
+ masks[a] = masks[r]
53
+
54
+ return masks
55
+
56
+
57
+ def node_to_tensor(
58
+ node: Node,
59
+ include_size: bool,
60
+ value_converter: BinaryConverter,
61
+ registers_emb: dict[str, Tensor] | None,
62
+ opcodes_mapper: dict[OpCodes, int],
63
+ node_kinds_mapper: dict[NodeKinds, int],
64
+ encode_registers_in_all_nodes: bool,
65
+ ) -> Tensor:
66
+ """Computes the initial feature for the given node."""
67
+
68
+ features = []
69
+
70
+ node_kind = torch.zeros((len(node_kinds_mapper),), dtype=torch.float32)
71
+ if node.kind in node_kinds_mapper:
72
+ node_kind[node_kinds_mapper[node.kind]] = 1
73
+ features.append(node_kind)
74
+
75
+ max_bytes = value_converter.num_bits // 8
76
+ if include_size:
77
+ # Use hot-encoding for possible value sizes,
78
+ # which are necessarily powers of two
79
+ size_indexes = {
80
+ 1: 0,
81
+ 2: 1,
82
+ 4: 2,
83
+ 8: 3,
84
+ 16: 4,
85
+ }
86
+ node_size = torch.zeros((size_indexes[max_bytes] + 1,), dtype=torch.float32)
87
+ if node.size:
88
+ node_size[size_indexes[node.size]] = 1
89
+ features.append(node_size)
90
+
91
+ node_opcode = torch.zeros((len(opcodes_mapper),), dtype=torch.float32)
92
+ if node.opcode is not None:
93
+ node_opcode[opcodes_mapper[node.opcode]] = 1
94
+ features.append(node_opcode)
95
+
96
+ if registers_emb is not None:
97
+ mask = unknown_reg = torch.zeros_like(next(iter(registers_emb.values())))
98
+ if (
99
+ node.kind in {NodeKinds.InputRegister, NodeKinds.OutputRegister}
100
+ or encode_registers_in_all_nodes
101
+ ):
102
+ mask = registers_emb.get(node.register_name, unknown_reg)
103
+ features.append(mask)
104
+
105
+ node_val = value_converter.value_to_bits(
106
+ node.signed_value() if node.kind == NodeKinds.Constant else 0
107
+ )
108
+
109
+ features.append(node_val)
110
+
111
+ return torch.cat(features)
112
+
113
+
114
+ # Used to encode the order of operation inputs,
115
+ # (except for OutputRegister, Phi and CALLOTHER Operation,
116
+ # which can have more inputs) and the special successor
117
+ # for CBRANCH, CALL and CALLIND.
118
+ MAX_OPERAND_NUMBER = 2
119
+
120
+
121
+ def graph_to_data(
122
+ graph: CDG,
123
+ registers_emb: dict[str, Tensor] | None = None,
124
+ opcodes_mapper: dict[OpCodes, int] | None = None,
125
+ node_kinds_mapper: dict[NodeKinds, int] | None = None,
126
+ max_value_bytes: int = 4,
127
+ include_size: bool = False,
128
+ include_output_registers_for_all_node: bool = False,
129
+ ) -> Data:
130
+ """Converts this graph into pytorch geometric Data format.
131
+
132
+ Args:
133
+ graph (CDG): the graph to export.
134
+ registers_emb (dict[str, Tensor]): Dictionary giving the embedding to use for a set of registers
135
+ to encode in node features. Registers not found in the dict will be encoded as full-zero.
136
+ You can pass None to avoid any register encoding in node features.
137
+ opcodes_mapper (dict[OpCodes, int]): A dict mapping Operation OpCodes into zero-based indexes.
138
+ OpCodes are one-hot encoded. OpCodes not found in the dict are converted to zero.
139
+ Pass None to use the whole list of supported opcodes.
140
+ node_kinds_mapper (dict[NodeKinds, int]): A dict mapping node kinds into zero-based indexes.
141
+ Node kinds are one-hot encoded. Node kinds not found in the dict are converted to zero.
142
+ Pass None to use the whole list of node kinds.
143
+ max_value_bytes (int): Maximum bytes to encode when putting constant values in the graph.
144
+ include_size (bool): Whether to add sizes in node features.
145
+ include_output_registers_for_all_node (bool): Whether to include output register for all node
146
+ else, only InputRegister and OutputRegister node include them.
147
+
148
+ Returns:
149
+ Data: the graph in pytorch_geometric format.
150
+ """
151
+
152
+ value_converter = BinaryConverter(max_value_bytes * 8)
153
+
154
+ features = []
155
+ edge_src = []
156
+ edge_dst = []
157
+ edge_attr = []
158
+
159
+ if opcodes_mapper is None:
160
+ opcodes_mapper = {o: o.value for o in OpCodes}
161
+
162
+ if node_kinds_mapper is None:
163
+ node_kinds_mapper = {k: k.value for k in NodeKinds}
164
+
165
+ for index, node in enumerate(graph.nodes):
166
+ node_features = node_to_tensor(
167
+ node,
168
+ include_size,
169
+ value_converter,
170
+ registers_emb,
171
+ opcodes_mapper,
172
+ node_kinds_mapper,
173
+ include_output_registers_for_all_node
174
+ or node.kind in {NodeKinds.InputRegister, NodeKinds.OutputRegister},
175
+ )
176
+ features.append(node_features)
177
+
178
+ # Gather edges by node
179
+ assert len(EdgeKinds) == 2
180
+ for edge in graph.edges:
181
+ attr = torch.zeros((2 + MAX_OPERAND_NUMBER,))
182
+ if edge.kind == EdgeKinds.Data:
183
+ attr[0] = 1
184
+ else:
185
+ attr[1] = 1
186
+ if edge.operand_number is not None and edge.operand_number < MAX_OPERAND_NUMBER:
187
+ attr[edge.operand_number + 2] = 1
188
+
189
+ edge_src.append(edge.source_node)
190
+ edge_dst.append(edge.destination_node)
191
+ edge_attr.append(attr)
192
+
193
+ if len(edge_attr) == 0:
194
+ # Support for a graph without any edge
195
+ edges = torch.zeros((2, 2 + MAX_OPERAND_NUMBER, 0))
196
+ else:
197
+ edges = torch.stack(edge_attr)
198
+
199
+ return Data(
200
+ x=torch.stack(features),
201
+ edge_index=torch.stack(
202
+ (
203
+ torch.tensor(edge_src, dtype=torch.int64),
204
+ torch.tensor(edge_dst, dtype=torch.int64),
205
+ )
206
+ ),
207
+ edge_attr=edges,
208
+ )
pcode_graph/graph.py ADDED
@@ -0,0 +1,234 @@
1
+ from dataclasses import dataclass, replace
2
+ from enum import Enum, auto
3
+ from pcode_graph.pcode import OpCodes
4
+ from pcode_graph.utils import unsigned_to_signed
5
+
6
+ NodeIndex = int
7
+ OperandNumber = int
8
+ RegisterId = int
9
+ Value = int
10
+
11
+
12
+ class NodeKinds(Enum):
13
+
14
+ InputRegister = 0
15
+ OutputRegister = 1
16
+ Constant = 2
17
+
18
+ Operation = 3
19
+ Phi = 4
20
+
21
+ ReadMemory = 5
22
+ WrittenMemory = 6
23
+
24
+ Begin = 7
25
+ External = 8
26
+ End = 9
27
+
28
+
29
+ class EdgeKinds(Enum):
30
+ Data = 0
31
+ Control = 1
32
+
33
+
34
+ @dataclass
35
+ class Edge:
36
+ source_node: NodeIndex
37
+
38
+ destination_node: NodeIndex
39
+
40
+ # Kind of edge
41
+ kind: EdgeKinds
42
+
43
+ # Operand rank if has meaning
44
+ operand_number: OperandNumber | None
45
+
46
+
47
+ @dataclass
48
+ class Node:
49
+ # Kind of node
50
+ kind: NodeKinds
51
+
52
+ # Number of bytes of the input or operation represented by this node
53
+ size: int = 0
54
+
55
+ # Value for a constant node
56
+ value: Value = 0
57
+
58
+ # Id of the register whatever its size version (ex: w0 and x0 will have same id in arm64)
59
+ # Warning: this value is unique in the current pcode context only.
60
+ register_id: RegisterId = -1
61
+
62
+ # Name of the register, for display purpose
63
+ register_name: str = ""
64
+
65
+ # Operation code
66
+ opcode: OpCodes | None = None
67
+
68
+ def __post_init__(self):
69
+ assert (self.kind == NodeKinds.Operation) ^ (self.opcode is None)
70
+
71
+ def signed_value(self) -> int:
72
+ assert self.kind == NodeKinds.Constant
73
+ return unsigned_to_signed(self.value, self.size)
74
+
75
+ @property
76
+ def size_bits(self) -> int:
77
+ return self.size * 8
78
+
79
+ def __str__(self):
80
+ match self.kind:
81
+ case NodeKinds.ReadMemory | NodeKinds.WrittenMemory:
82
+ return "MEMORY"
83
+ case NodeKinds.InputRegister | NodeKinds.OutputRegister:
84
+ return self.register_name
85
+ case NodeKinds.Constant:
86
+ return f"#{hex(self.signed_value())}"
87
+ case NodeKinds.Operation:
88
+ assert self.opcode is not None
89
+ return self.opcode.name
90
+ case NodeKinds.Phi:
91
+ return "ϕ"
92
+ case NodeKinds.Begin:
93
+ return "BEGIN"
94
+ case NodeKinds.External:
95
+ return "EXTERNAL"
96
+ case NodeKinds.End:
97
+ return "END"
98
+ case _:
99
+ raise ValueError(self.kind)
100
+
101
+
102
+ @dataclass
103
+ class EdgeProxy:
104
+ """Easy access to edges for one node."""
105
+
106
+ in_edges: list[Edge]
107
+ out_edges: list[Edge]
108
+
109
+ def get_data_predecessors(self) -> list[NodeIndex]:
110
+ return [
111
+ edge.source_node for edge in self.in_edges if edge.kind == EdgeKinds.Data
112
+ ]
113
+
114
+ def get_control_predecessors(self) -> list[NodeIndex]:
115
+ return [
116
+ edge.source_node for edge in self.in_edges if edge.kind == EdgeKinds.Control
117
+ ]
118
+
119
+ def get_data_successors(self) -> list[NodeIndex]:
120
+ return [
121
+ edge.destination_node
122
+ for edge in self.out_edges
123
+ if edge.kind == EdgeKinds.Data
124
+ ]
125
+
126
+ def get_control_successors(self) -> list[NodeIndex]:
127
+ return [
128
+ edge.destination_node
129
+ for edge in self.out_edges
130
+ if edge.kind == EdgeKinds.Control
131
+ ]
132
+
133
+ def get_inputs_edges(self) -> list[Edge]:
134
+ """Returns data input operands, by number."""
135
+ operands: list[tuple[OperandNumber | None, Edge]] = []
136
+ hasNone = False
137
+ hasNumber = False
138
+ for in_edge in self.in_edges:
139
+ if in_edge.kind == EdgeKinds.Control:
140
+ continue
141
+ n = in_edge.operand_number
142
+ if n is None:
143
+ hasNone = True
144
+ else:
145
+ hasNumber = True
146
+ operands.append((n, in_edge))
147
+ assert hasNone ^ hasNumber
148
+ if hasNumber:
149
+ operands.sort()
150
+ return [o[1] for o in operands]
151
+
152
+
153
+ @dataclass
154
+ class CDG:
155
+ """
156
+ Graph of P-Code operations with Control and Data flow edges.
157
+ """
158
+
159
+ # Graph nodes
160
+ # Order in this list gives the NodeIndex used in Edge class
161
+ nodes: list[Node]
162
+
163
+ # Edges, order does not matter
164
+ edges: list[Edge]
165
+
166
+ def clone(self) -> "CDG":
167
+ return CDG([replace(n) for n in self.nodes], [replace(e) for e in self.edges])
168
+
169
+ def compute_edge_proxy(self) -> dict[NodeIndex, EdgeProxy]:
170
+ cache = {index: EdgeProxy([], []) for index in range(len(self.nodes))}
171
+ for edge in self.edges:
172
+ cache[edge.source_node].out_edges.append(edge)
173
+ cache[edge.destination_node].in_edges.append(edge)
174
+ return cache
175
+
176
+ def add_node(self, node: Node) -> NodeIndex:
177
+ """Adds a node into the graph, without any edge. Returns the index of the added node."""
178
+ index = len(self.nodes)
179
+ self.nodes.append(node)
180
+ return index
181
+
182
+ def remove_node(self, index: NodeIndex):
183
+ """Removes a node from the graph and its immediate edges, former linked nodes can remain unconnected."""
184
+
185
+ def remap(i: NodeIndex) -> NodeIndex:
186
+ return i - int(i > index)
187
+
188
+ self.nodes.pop(index)
189
+ self.edges = [
190
+ replace(
191
+ e,
192
+ source_node=remap(e.source_node),
193
+ destination_node=remap(e.destination_node),
194
+ )
195
+ for e in self.edges
196
+ if e.source_node != index and e.destination_node != index
197
+ ]
198
+
199
+ def add_edge(self, edge: Edge):
200
+ self.edges.append(edge)
201
+
202
+ def remove_edge(self, edge: Edge):
203
+ self.edges.remove(edge)
204
+
205
+ def dump(self) -> str:
206
+ """Dumps the graph in mermaid format."""
207
+ s = ""
208
+ for index, node in enumerate(self.nodes):
209
+ match node.kind:
210
+ case NodeKinds.Begin | NodeKinds.End | NodeKinds.External:
211
+ s += f"n{index}[[{node}]]\n"
212
+ case NodeKinds.Operation:
213
+ s += f"n{index}[{node}]\n"
214
+ case NodeKinds.Phi:
215
+ s += f"n{index}" + "{{" + f"{node}" + "}}\n"
216
+ case _:
217
+ s += f"n{index}(({node}))\n"
218
+
219
+ for edge in self.edges:
220
+ link = "-->" if edge.kind == EdgeKinds.Data else "-.->"
221
+ rank = f"|{edge.operand_number}|" if edge.operand_number is not None else ""
222
+ s += f"n{edge.source_node} {link}{rank} n{edge.destination_node}\n"
223
+
224
+ return s
225
+
226
+ def dump_edge(self, edge: Edge) -> str:
227
+ link = "-->" if edge.kind == EdgeKinds.Data else "-.->"
228
+ rank = f"|{edge.operand_number}|" if edge.operand_number is not None else ""
229
+ return f"{self.nodes[edge.source_node]} {link}{rank} {self.nodes[edge.destination_node]}"
230
+
231
+ def __str__(self) -> str:
232
+ """Dumps the graph in markdown + mermaid format."""
233
+
234
+ return "```mermaid\nflowchart\n" + self.dump() + "```\n"