pcode-graph 1.0.0__tar.gz → 1.1.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.
Files changed (43) hide show
  1. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/.github/workflows/release.yml +1 -1
  2. pcode_graph-1.1.0/.github/workflows/test.yml +17 -0
  3. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/PKG-INFO +2 -2
  4. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/analysis.py +3 -2
  5. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/charac.py +22 -5
  6. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/checker.py +4 -4
  7. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/cli.py +20 -2
  8. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/emulator.py +3 -2
  9. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/graph.py +52 -39
  10. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/lief_importer.py +1 -1
  11. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/maker.py +32 -15
  12. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/simplifier.py +83 -72
  13. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pyproject.toml +1 -1
  14. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/maker_test.py +11 -11
  15. pcode_graph-1.1.0/todo.md +3 -0
  16. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/.gitignore +0 -0
  17. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/LICENSE +0 -0
  18. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/README.md +0 -0
  19. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/__init__.py +0 -0
  20. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/__main__.py +0 -0
  21. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/arch.py +0 -0
  22. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/asm.py +0 -0
  23. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/gnn_exporter.py +0 -0
  24. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/log.py +0 -0
  25. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/nx_exporter.py +0 -0
  26. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/pcode.py +0 -0
  27. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/registers.py +0 -0
  28. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/torch_utils.py +0 -0
  29. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/translator.py +0 -0
  30. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/utils.py +0 -0
  31. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/view.py +0 -0
  32. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/pcode_graph/visitor.py +0 -0
  33. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/requirements.txt +0 -0
  34. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/__init__.py +0 -0
  35. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/analysis_test.py +0 -0
  36. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/charac_test.py +0 -0
  37. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/conftest.py +0 -0
  38. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/emulator_test.py +0 -0
  39. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/exporter_test.py +0 -0
  40. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/fixtures.py +0 -0
  41. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/helpers.py +0 -0
  42. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/translator_test.py +0 -0
  43. {pcode_graph-1.0.0 → pcode_graph-1.1.0}/tests/utils_test.py +0 -0
@@ -29,4 +29,4 @@ jobs:
29
29
  with:
30
30
  name: dist
31
31
  path: dist/
32
- - uses: pypa/gh-action-pypi-publish@release/v1
32
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ name: Tests
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches: [main]
6
+ workflow_call:
7
+
8
+ jobs:
9
+ pytest:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install .[test,torch]
17
+ - run: python -m pytest tests
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pcode_graph
3
- Version: 1.0.0
3
+ Version: 1.1.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
@@ -3,7 +3,7 @@ from enum import Enum
3
3
  from typing import Iterator, override
4
4
  from pypcode import OpCode, PcodeOp, Varnode
5
5
  from pcode_graph.arch import Arch
6
- from pcode_graph.pcode import OpCodes, dump_operation, from_native_opcode
6
+ from pcode_graph.pcode import dump_operation, from_native_opcode
7
7
  from pcode_graph.visitor import PcodeVisitor
8
8
 
9
9
  OpIndex = int
@@ -56,7 +56,8 @@ class Var:
56
56
  def wrap(var: Varnode) -> Var:
57
57
  match var.space.name:
58
58
  case "register":
59
- return Var(VarKinds.Register, var.size, var.offset, var.getRegisterName())
59
+ name = var.getRegisterName() or "UNKNOWN"
60
+ return Var(VarKinds.Register, var.size, var.offset, name)
60
61
  case "unique":
61
62
  return Var(VarKinds.Unique, var.size, var.offset, f"${var.offset}")
62
63
  case "const":
@@ -11,13 +11,27 @@ from pcode_graph.pcode import OpCodes
11
11
  from collections import deque
12
12
  from itertools import chain
13
13
 
14
- class Diameters(NamedTuple):
14
+
15
+ class Stats(NamedTuple):
15
16
  data_only: int
16
17
  control_only: int
17
18
  both: int
18
19
 
19
20
 
20
- def get_graph_diameters(cdg: CDG) -> Diameters:
21
+ def count_edges(cdg: CDG) -> Stats:
22
+
23
+ data_only: int = 0
24
+
25
+ for edge in cdg.edges:
26
+ if edge.kind == EdgeKinds.Data:
27
+ data_only += 1
28
+
29
+ return Stats(data_only, len(cdg.edges) - data_only, len(cdg.edges))
30
+
31
+
32
+ def get_graph_diameters(cdg: CDG) -> Stats:
33
+ """Computes the diameter of the given graph."""
34
+
21
35
  n = len(cdg.nodes)
22
36
 
23
37
  def build_adjacency(edges):
@@ -47,17 +61,19 @@ def get_graph_diameters(cdg: CDG) -> Diameters:
47
61
  if dist[neigbor] == -1:
48
62
  dist[neigbor] = dist[current] + 1
49
63
  front.append(neigbor)
50
- d = max(d, max(dist))
64
+ d = max(d, max(dist))
51
65
  return d
52
66
 
53
- return Diameters(
67
+ return Stats(
54
68
  diameter(build_adjacency(data_edges)),
55
69
  diameter(build_adjacency(ctrl_edges)),
56
70
  diameter(build_adjacency(chain(data_edges, ctrl_edges))),
57
71
  )
58
72
 
73
+
59
74
  class FunctionCharacteristics(IntFlag):
60
- """Helps evaluating the level of difficulty of this function."""
75
+ """Helps evaluating the level of difficulty of a function,
76
+ regarding miscalleneous ML tasks."""
61
77
 
62
78
  data = auto()
63
79
  condition = auto()
@@ -68,6 +84,7 @@ class FunctionCharacteristics(IntFlag):
68
84
 
69
85
 
70
86
  def get_function_characteristics(graph: CDG) -> FunctionCharacteristics:
87
+ """Characterizes a function regarding control flow features."""
71
88
 
72
89
  # Every function contains data-flow easiest level
73
90
  function_characteristics = FunctionCharacteristics.data
@@ -34,8 +34,8 @@ def check_graph(graph: CDG):
34
34
  )
35
35
 
36
36
  proxy = proxy_dict[index]
37
- control_succs = proxy.get_control_successors()
38
- data_preds = proxy.get_data_predecessors()
37
+ control_succs = list(proxy.get_control_successors())
38
+ data_preds = list(proxy.get_data_predecessors())
39
39
 
40
40
  if node.kind != NodeKinds.Operation:
41
41
  assert node.opcode is None
@@ -46,11 +46,11 @@ def check_graph(graph: CDG):
46
46
 
47
47
  data_numbers: list[int] = []
48
48
  control_numbers: list[int] = []
49
- for edge in proxy.in_edges:
49
+ for edge in proxy.get_in_edges():
50
50
  if edge.kind == EdgeKinds.Data:
51
51
  assert edge.operand_number is not None
52
52
  data_numbers.append(edge.operand_number)
53
- for edge in proxy.out_edges:
53
+ for edge in proxy.get_out_edges():
54
54
  if edge.kind == EdgeKinds.Control:
55
55
  assert edge.operand_number is not None
56
56
  control_numbers.append(edge.operand_number)
@@ -3,7 +3,7 @@
3
3
  Dumps Control and Data Graph (CDG) and various data from a binary or assembly file.
4
4
 
5
5
  Usage:
6
- cdg ( asm | functions | html | md | pcode | table ) <input_path> [options]
6
+ cdg ( asm | functions | html | md | pcode | table | stats ) <input_path> [options]
7
7
 
8
8
  Dump commands:
9
9
  asm Assembly dump
@@ -12,6 +12,7 @@ Dump commands:
12
12
  md CDG in markdown/mermaid format
13
13
  pcode P-Code operations
14
14
  table Markdown table of the P-Code enriched by static analysis results
15
+ stats Prints graph statistics
15
16
 
16
17
  Options:
17
18
  -f, --function <name> Take only the function with given symbol name.
@@ -21,10 +22,11 @@ Options:
21
22
  -D, --dataflow-only Only extract dataflow graph (and no control flow).
22
23
  -o, --output <path> Where to write the output.
23
24
  -h, --help This help.
24
- -d, --debug Debug mode: do no catch exceptions.
25
+ -d, --debug Debug mode: do no catch exceptions.
25
26
  """
26
27
 
27
28
  from pathlib import Path
29
+ import re
28
30
  import sys
29
31
  from docopt import docopt
30
32
  from lief import Binary
@@ -33,6 +35,7 @@ from loguru import logger
33
35
  import pypcode
34
36
  from pcode_graph.arch import Arch
35
37
  from pcode_graph.asm import Assembler
38
+ from pcode_graph.charac import count_edges, get_graph_diameters
36
39
  from pcode_graph.lief_importer import (
37
40
  get_function,
38
41
  iter_code_sections,
@@ -104,6 +107,10 @@ def main():
104
107
  code = function.content
105
108
  else:
106
109
  # Lift piece of code
110
+ if not re.search("[a-fA-FxX]", chunk_begin_end):
111
+ logger.warning(
112
+ f"This does not look like hexadecimal addresses: {chunk_begin_end}"
113
+ )
107
114
  start_address, end_address = [
108
115
  int(a, 16) for a in chunk_begin_end.split(":")
109
116
  ]
@@ -164,6 +171,17 @@ def main():
164
171
  print(str(cdg), file=output)
165
172
  sys.exit(0)
166
173
 
174
+ if args["stats"]:
175
+ stats = dict(
176
+ diameter=get_graph_diameters(cdg)._asdict(),
177
+ edges=count_edges(cdg)._asdict(),
178
+ nodes=len(cdg.nodes),
179
+ )
180
+
181
+ print(repr(stats), file=output)
182
+ sys.exit(0)
183
+
184
+ assert args["html"], "Unknown command"
167
185
  assert output_path, "HTML output file is mandatory"
168
186
  output.close()
169
187
  draw_graph(cdg, Path(output_path))
@@ -69,7 +69,7 @@ class Emulator(PcodeVisitor):
69
69
  OperandNumber | None, tuple[NodeIndex, Value]
70
70
  ] = {}
71
71
 
72
- for in_edge in p.in_edges:
72
+ for in_edge in p.get_in_edges():
73
73
  if in_edge.kind != EdgeKinds.Data:
74
74
  continue
75
75
 
@@ -88,7 +88,8 @@ class Emulator(PcodeVisitor):
88
88
  propagated = True
89
89
 
90
90
  case NodeKinds.OutputRegister:
91
- (in_edge,) = p.get_inputs_edges()
91
+ (in_edge_index,) = p.get_input_edges()
92
+ in_edge = self.graph.edges[in_edge_index]
92
93
  value = self.values.get(in_edge.source_node)
93
94
  if value is not None:
94
95
  self.values[index] = value
@@ -1,9 +1,11 @@
1
1
  from dataclasses import dataclass, replace
2
2
  from enum import Enum, auto
3
+ from typing import Iterator
3
4
  from pcode_graph.pcode import OpCodes
4
5
  from pcode_graph.utils import unsigned_to_signed
5
6
 
6
7
  NodeIndex = int
8
+ EdgeIndex = int
7
9
  OperandNumber = int
8
10
  RegisterId = int
9
11
  Value = int
@@ -31,7 +33,7 @@ class EdgeKinds(Enum):
31
33
  Control = 1
32
34
 
33
35
 
34
- @dataclass
36
+ @dataclass(slots=True)
35
37
  class Edge:
36
38
  source_node: NodeIndex
37
39
 
@@ -44,7 +46,7 @@ class Edge:
44
46
  operand_number: OperandNumber | None
45
47
 
46
48
 
47
- @dataclass
49
+ @dataclass(slots=True)
48
50
  class Node:
49
51
  # Kind of node
50
52
  kind: NodeKinds
@@ -99,43 +101,54 @@ class Node:
99
101
  raise ValueError(self.kind)
100
102
 
101
103
 
102
- @dataclass
104
+ @dataclass(slots=True)
103
105
  class EdgeProxy:
104
106
  """Easy access to edges for one node."""
105
107
 
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]:
108
+ graph: CDG
109
+ in_edge_indexes: list[EdgeIndex]
110
+ out_edge_indexes: list[EdgeIndex]
111
+
112
+ def get_in_edges(self) -> Iterator[Edge]:
113
+ for index in self.in_edge_indexes:
114
+ yield self.graph.edges[index]
115
+
116
+ def get_out_edges(self) -> Iterator[Edge]:
117
+ for index in self.out_edge_indexes:
118
+ yield self.graph.edges[index]
119
+
120
+ def get_data_predecessors(self) -> Iterator[NodeIndex]:
121
+ for e in self.in_edge_indexes:
122
+ edge = self.graph.edges[e]
123
+ if edge.kind == EdgeKinds.Data:
124
+ yield edge.source_node
125
+
126
+ def get_control_predecessors(self) -> Iterator[NodeIndex]:
127
+ for e in self.in_edge_indexes:
128
+ edge = self.graph.edges[e]
129
+ if edge.kind == EdgeKinds.Control:
130
+ yield edge.source_node
131
+
132
+ def get_data_successors(self) -> Iterator[NodeIndex]:
133
+ for e in self.out_edge_indexes:
134
+ edge = self.graph.edges[e]
135
+ if edge.kind == EdgeKinds.Data:
136
+ yield edge.destination_node
137
+
138
+ def get_control_successors(self) -> Iterator[NodeIndex]:
139
+ for e in self.out_edge_indexes:
140
+ edge = self.graph.edges[e]
141
+ if edge.kind == EdgeKinds.Control:
142
+ yield edge.destination_node
143
+
144
+ def get_input_edges(self) -> list[EdgeIndex]:
134
145
  """Returns data input operands, by number."""
135
- operands: list[tuple[OperandNumber | None, Edge]] = []
146
+
147
+ operands: list[tuple[OperandNumber | None, EdgeIndex]] = []
136
148
  hasNone = False
137
149
  hasNumber = False
138
- for in_edge in self.in_edges:
150
+ for in_edge_index in self.in_edge_indexes:
151
+ in_edge = self.graph.edges[in_edge_index]
139
152
  if in_edge.kind == EdgeKinds.Control:
140
153
  continue
141
154
  n = in_edge.operand_number
@@ -143,14 +156,14 @@ class EdgeProxy:
143
156
  hasNone = True
144
157
  else:
145
158
  hasNumber = True
146
- operands.append((n, in_edge))
159
+ operands.append((n, in_edge_index))
147
160
  assert hasNone ^ hasNumber
148
161
  if hasNumber:
149
162
  operands.sort()
150
163
  return [o[1] for o in operands]
151
164
 
152
165
 
153
- @dataclass
166
+ @dataclass(slots=True)
154
167
  class CDG:
155
168
  """
156
169
  Graph of P-Code operations with Control and Data flow edges.
@@ -167,10 +180,10 @@ class CDG:
167
180
  return CDG([replace(n) for n in self.nodes], [replace(e) for e in self.edges])
168
181
 
169
182
  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)
183
+ cache = {index: EdgeProxy(self, [], []) for index in range(len(self.nodes))}
184
+ for e, edge in enumerate(self.edges):
185
+ cache[edge.source_node].out_edge_indexes.append(e)
186
+ cache[edge.destination_node].in_edge_indexes.append(e)
174
187
  return cache
175
188
 
176
189
  def add_node(self, node: Node) -> NodeIndex:
@@ -29,7 +29,7 @@ class FunctionOps(ChunkOps):
29
29
 
30
30
 
31
31
  def parse_binary(binary_path: Path) -> lief.Binary:
32
- assert binary_path.exists(), f"{binary_path} not found"
32
+ assert binary_path.is_file(), f"{binary_path} is not a file"
33
33
  logger.debug(f"Parse {binary_path}")
34
34
  b = lief.parse(binary_path.as_posix())
35
35
  assert isinstance(b, lief.Binary)
@@ -26,10 +26,18 @@ class MakerFlags:
26
26
  build_cfg: bool = True
27
27
  """Add control flow links in addition to the data flow. Defaults to True."""
28
28
 
29
- ignore_reg_outputs: set[str] | None = None
29
+ reg_outputs_black_list: set[str] | None = None
30
30
  """Do not consider the given registers as graph outputs to simplify the graphs.
31
31
  Arch-dependent register names.
32
- If None, the result of pcode.get_special_register_names() is used.
32
+ If white-list is not None, the value of black-list is ignored.
33
+ If both black-list and white-list are None, the content of SPECIAL_REGISTERS is used
34
+ for black-list.
35
+ """
36
+
37
+ reg_outputs_white_list: set[str] | None = None
38
+ """Consider only the given registers as graph outputs to simplify the graphs.
39
+ Arch-dependent register names.
40
+ If white-list is not None, the value of black-list is ignored.
33
41
  """
34
42
 
35
43
 
@@ -45,10 +53,11 @@ def make_graph(prg: RichPcodeList, flags: MakerFlags | None = None) -> CDG:
45
53
  """
46
54
 
47
55
  if flags is None:
48
- flags = MakerFlags()
49
- ignore_reg_outputs = flags.ignore_reg_outputs
50
- if ignore_reg_outputs is None:
51
- ignore_reg_outputs = SPECIAL_REGISTERS[prg.arch]
56
+ flags = MakerFlags()
57
+
58
+ black_list, white_list = flags.reg_outputs_black_list, flags.reg_outputs_white_list
59
+ if black_list is None and white_list is None:
60
+ black_list = SPECIAL_REGISTERS[prg.arch]
52
61
 
53
62
  # Already created input nodes, by related input variable
54
63
  input_node_indexes: dict[Var, NodeIndex] = {}
@@ -189,13 +198,22 @@ def make_graph(prg: RichPcodeList, flags: MakerFlags | None = None) -> CDG:
189
198
  for var, def_op_indexes in prg.get_exit_defs().items():
190
199
  assert def_op_indexes
191
200
  match var.kind:
192
- case VarKinds.Register:
193
- if var.pretty_name.lower().startswith("tmp"):
194
- # Ignore writes to temporaries
195
- continue
196
- if var.pretty_name in ignore_reg_outputs:
197
- # Ignore writes to special registers
198
- continue
201
+ case VarKinds.Register:
202
+ if white_list is not None:
203
+ if var.pretty_name not in white_list:
204
+ continue
205
+ else:
206
+ assert black_list is not None
207
+ if var.pretty_name.lower().startswith("tmp"):
208
+ # Ignore writes to temporaries
209
+ continue
210
+ if var.pretty_name in black_list:
211
+ # Ignore writes to special registers
212
+ continue
213
+ if var.pretty_name == "UNKNOWN":
214
+ # Register name not provided in P-Code
215
+ continue
216
+ assert var.pretty_name
199
217
  output_node = Node(
200
218
  NodeKinds.OutputRegister,
201
219
  var.size,
@@ -309,8 +327,7 @@ def make_graph(prg: RichPcodeList, flags: MakerFlags | None = None) -> CDG:
309
327
 
310
328
  graph = CDG(nodes, edges)
311
329
 
312
- # Uncomment to locate a graph structure bug
313
- # Comment to speed up
330
+ # Uncomment to locate a bug: graph structure vs simplifier
314
331
  # check_graph(graph)
315
332
 
316
333
  GraphSimplifier(graph).process()
@@ -1,5 +1,13 @@
1
+
1
2
  from pcode_graph.checker import check_graph
2
- from pcode_graph.graph import CDG, Edge, EdgeKinds, NodeIndex, OpCodes, NodeKinds
3
+ from pcode_graph.graph import (
4
+ CDG,
5
+ EdgeIndex,
6
+ EdgeKinds,
7
+ NodeIndex,
8
+ OpCodes,
9
+ NodeKinds,
10
+ )
3
11
 
4
12
  from loguru import logger
5
13
  from pathlib import Path
@@ -13,81 +21,86 @@ class GraphSimplifier:
13
21
  ) -> None:
14
22
  self.graph = graph
15
23
 
16
- def _update_proxy(self):
17
24
  self._proxy = self.graph.compute_edge_proxy()
18
25
 
26
+ def _finalize_removals(self):
27
+
28
+ remap: dict[int, int] = {}
29
+ shift = 0
30
+ kept_nodes = []
31
+ for index, node in enumerate(self.graph.nodes):
32
+ if node is None:
33
+ shift += 1
34
+ else:
35
+ remap[index] = index - shift
36
+ kept_nodes.append(node)
37
+
38
+ self.graph.nodes = kept_nodes
39
+
40
+ kept_edges = []
41
+ for edge in self.graph.edges:
42
+ if edge is None:
43
+ continue
44
+ edge.destination_node = remap[edge.destination_node]
45
+ edge.source_node = remap[edge.source_node]
46
+ kept_edges.append(edge)
47
+
48
+ self.graph.edges = kept_edges
49
+
19
50
  def _remove_node(self, index: NodeIndex):
20
- def remap_index(n: NodeIndex):
21
- assert n != index
22
- return n - int(n > index)
23
51
 
24
52
  # logger.debug(f"Remove {self.graph.nodes[index]} at {index}")
25
53
 
26
- # Remap edges
27
54
  proxy = self._proxy[index]
28
55
 
29
- control_succs = proxy.get_control_successors()
56
+ control_succs = list(proxy.get_control_successors())
30
57
  assert (
31
58
  len(control_succs) <= 1
32
59
  ), "More than one control successor to the node to remove"
33
60
 
34
- data_preds = proxy.get_data_predecessors()
61
+ data_preds = list(proxy.get_data_predecessors())
35
62
 
36
63
  # Convert to set to handle particular case of Phi node
37
64
  assert len(data_preds) <= 1 or (
38
65
  self.graph.nodes[index].kind == NodeKinds.Phi and len(set(data_preds)) == 1
39
66
  ), "More than one data predecessor to the node to remove"
40
67
 
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:
68
+ for i, e in enumerate(self.graph.edges):
69
+ if e is None:
70
+ continue
71
+ remove = False
58
72
  if e.source_node == index:
59
73
  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:
74
+ self._proxy[e.source_node].out_edge_indexes.remove(i)
75
+ e.source_node = data_preds[0]
76
+ self._proxy[e.source_node].out_edge_indexes.append(i)
77
+ else:
78
+ remove = True
79
+
80
+ if e.destination_node == index:
67
81
  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)
82
+ self._proxy[e.destination_node].in_edge_indexes.remove(i)
83
+ e.destination_node = control_succs[0]
84
+ self._proxy[e.destination_node].in_edge_indexes.append(i)
85
+ else:
86
+ remove = True
76
87
 
77
- # Patch the graph
78
- self.graph.edges = kept_edges
79
- self.graph.nodes.pop(index)
88
+ if remove:
89
+ self._remove_edge(i)
80
90
 
91
+ # Patch the graph
92
+ self.graph.nodes[index] = None # type: ignore
81
93
  self._graph_changed = True
82
- self._update_proxy()
83
94
 
84
95
  # self.debug_helper()
85
96
 
86
- def _remove_edge(self, edge: Edge):
97
+ def _remove_edge(self, edge_index: int):
87
98
 
88
- self.graph.remove_edge(edge)
99
+ edge = self.graph.edges[edge_index]
100
+ self._proxy[edge.source_node].out_edge_indexes.remove(edge_index)
101
+ self._proxy[edge.destination_node].in_edge_indexes.remove(edge_index)
102
+ self.graph.edges[edge_index] = None # type: ignore
89
103
  self._graph_changed = True
90
- self._update_proxy()
91
104
 
92
105
  def debug_helper(self):
93
106
  try:
@@ -102,7 +115,6 @@ class GraphSimplifier:
102
115
  def process(self):
103
116
 
104
117
  self._graph_changed = True
105
- self._update_proxy()
106
118
 
107
119
  while self._graph_changed:
108
120
  self._graph_changed = False
@@ -110,6 +122,9 @@ class GraphSimplifier:
110
122
  # Remove useless phi nodes
111
123
  for index, node in enumerate(self.graph.nodes):
112
124
 
125
+ if node is None:
126
+ continue
127
+
113
128
  if node.kind == NodeKinds.Phi:
114
129
 
115
130
  phi_data_preds = set(self._proxy[index].get_data_predecessors())
@@ -117,13 +132,12 @@ class GraphSimplifier:
117
132
  # Keep this Phi node
118
133
  continue
119
134
 
120
- self._remove_node(index)
121
- break
135
+ self._remove_node(index)
122
136
 
123
137
  elif node.opcode == OpCodes.COPY:
124
138
 
125
139
  ctrl_preds = set(self._proxy[index].get_control_predecessors())
126
- ctrl_succs = self._proxy[index].get_control_successors()
140
+ ctrl_succs = set(self._proxy[index].get_control_successors())
127
141
 
128
142
  remove = True
129
143
  if ctrl_preds and ctrl_succs:
@@ -131,55 +145,52 @@ class GraphSimplifier:
131
145
 
132
146
  remove = False
133
147
  # Remove copies that dominates their successor...
134
- if self._proxy[ctrl_succ].get_control_predecessors() == [index]:
148
+ if list(self._proxy[ctrl_succ].get_control_predecessors()) == [
149
+ index
150
+ ]:
135
151
  remove = True
136
152
  elif len(ctrl_preds) == 1:
137
153
  # ...or are dominated by their unique predecessor
138
154
  (ctrl_pred,) = ctrl_preds
139
- if self._proxy[ctrl_pred].get_control_successors() == [
140
- index
141
- ]:
155
+ if list(
156
+ self._proxy[ctrl_pred].get_control_successors()
157
+ ) == [index]:
142
158
  remove = True
143
159
 
144
160
  if remove:
145
161
  self._remove_node(index)
146
- break
147
162
 
148
- elif node.kind == NodeKinds.Constant:
149
- if not self._proxy[index].get_data_successors():
163
+ elif node.kind == NodeKinds.Constant:
164
+ if not list(self._proxy[index].get_data_successors()):
150
165
  # Remove constants let orphan by other simplifications
151
166
  self._remove_node(index)
152
- break
153
167
 
154
168
  elif node.opcode in {
155
169
  OpCodes.INT_ZEXT,
156
170
  OpCodes.INT_SEXT,
157
171
  }:
158
172
  self._remove_node(index)
159
- break
160
173
 
161
174
  elif node.opcode == OpCodes.SUBPIECE:
162
175
 
163
- operands: list[Edge] = self._proxy[index].get_inputs_edges()
164
-
176
+ operands: list[EdgeIndex] = self._proxy[index].get_input_edges()
165
177
  assert len(operands) == 2
166
- num_trunc_bytes = self.graph.nodes[operands[1].source_node]
178
+ source_node_index = self.graph.edges[operands[1]].source_node
179
+ num_trunc_bytes = self.graph.nodes[source_node_index]
167
180
  assert num_trunc_bytes.kind == NodeKinds.Constant
168
181
  if num_trunc_bytes.value == 0:
169
182
  self._remove_edge(operands[1])
170
- self._remove_node(index)
171
- break
183
+ self._remove_node(index)
172
184
 
173
185
  elif node.opcode == OpCodes.INT_MULT:
174
186
  # Remove mulitiplication by one
175
- operands: list[Edge] = self._proxy[index].get_inputs_edges()
176
- for op_edge in operands:
187
+ for op_edge_index in self._proxy[index].get_input_edges():
188
+ op_edge = self.graph.edges[op_edge_index]
177
189
  o = op_edge.source_node
178
190
  op = self.graph.nodes[o]
179
191
  if op.kind == NodeKinds.Constant and op.value == 1:
180
- self._remove_edge(op_edge)
181
- self._remove_node(index)
192
+ self._remove_edge(op_edge_index)
193
+ self._remove_node(index)
182
194
  break
183
- else:
184
- continue
185
- break
195
+
196
+ self._finalize_removals()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pcode_graph"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  authors = [
9
9
  { name="Samuel Hangouët", email="samuel.hangouet@quarkslab.com" },
10
10
  ]
@@ -98,7 +98,7 @@ ret
98
98
  """,
99
99
  x86_64,
100
100
  outdir,
101
- flags=MakerFlags(ignore_reg_outputs=special_regs),
101
+ flags=MakerFlags(reg_outputs_black_list=special_regs),
102
102
  )
103
103
 
104
104
  check_inputs(g, "#0x8", "RSP", "MEMORY")
@@ -131,7 +131,7 @@ ldp x29, x30, [sp], #0x10
131
131
  """,
132
132
  arm64,
133
133
  outdir,
134
- flags=MakerFlags(ignore_reg_outputs=special_regs),
134
+ flags=MakerFlags(reg_outputs_black_list=special_regs),
135
135
  )
136
136
 
137
137
  load1, load2 = [n for n, node in enumerate(g.nodes) if node.opcode == OpCodes.LOAD]
@@ -166,10 +166,10 @@ ldr x2, [sp, 0x10]
166
166
 
167
167
  (load_index,) = [n for n, node in enumerate(g.nodes) if node.opcode == OpCodes.LOAD]
168
168
  proxy = g.compute_edge_proxy()
169
- phi_index, add_index = proxy[load_index].get_data_predecessors()
169
+ phi_index, add_index = list(proxy[load_index].get_data_predecessors())
170
170
  assert g.nodes[phi_index].kind == NodeKinds.Phi
171
171
  assert g.nodes[add_index].opcode == OpCodes.INT_ADD
172
- assert len(proxy[phi_index].get_data_predecessors()) == 3
172
+ assert len(list(proxy[phi_index].get_data_predecessors())) == 3
173
173
 
174
174
 
175
175
  def test_call_ind_arm(outdir: Path, arm64: ArchContext, log):
@@ -199,7 +199,7 @@ mul x1, x0, x0
199
199
  else:
200
200
  assert False, "Call not found"
201
201
 
202
- assert len(g.compute_edge_proxy()[call].get_control_successors()) == 2
202
+ assert len(list(g.compute_edge_proxy()[call].get_control_successors())) == 2
203
203
 
204
204
 
205
205
  def test_div(outdir: Path, x86_64: ArchContext, log):
@@ -240,7 +240,7 @@ mov x0, x6
240
240
 
241
241
  proxy = g.compute_edge_proxy()
242
242
 
243
- assert proxy[x0_output].get_data_predecessors() == [x1_input]
243
+ assert list(proxy[x0_output].get_data_predecessors()) == [x1_input]
244
244
 
245
245
 
246
246
  def test_various_opcodes(outdir: Path, arm64: ArchContext, log):
@@ -409,14 +409,14 @@ ret
409
409
 
410
410
  proxy = g.compute_edge_proxy()
411
411
  (load,) = [i for i, n in enumerate(g.nodes) if n.opcode == OpCodes.LOAD]
412
- assert len(proxy[load].get_control_predecessors()) == 2
412
+ assert len(list(proxy[load].get_control_predecessors())) == 2
413
413
 
414
414
  (rax,) = [
415
415
  i
416
416
  for i, n in enumerate(g.nodes)
417
417
  if n.kind == NodeKinds.OutputRegister and n.register_name == "RAX"
418
418
  ]
419
- (rax_pred,) = proxy[rax].get_data_predecessors()
419
+ (rax_pred,) = list(proxy[rax].get_data_predecessors())
420
420
  assert g.nodes[rax_pred].kind == NodeKinds.Phi
421
421
 
422
422
 
@@ -450,7 +450,7 @@ jmp rax
450
450
 
451
451
  (end,) = [i for i, n in enumerate(g.nodes) if n.kind == NodeKinds.External]
452
452
  (branchind,) = [i for i, n in enumerate(g.nodes) if n.opcode == OpCodes.BRANCHIND]
453
- assert g.compute_edge_proxy()[end].get_control_predecessors() == [branchind]
453
+ assert list(g.compute_edge_proxy()[end].get_control_predecessors()) == [branchind]
454
454
 
455
455
 
456
456
  def test_imul(outdir: Path, x86_64: ArchContext):
@@ -569,7 +569,7 @@ call 0x546fb
569
569
  asm,
570
570
  x86_64,
571
571
  outdir / "full",
572
- flags=MakerFlags(ignore_reg_outputs=ignored_regs),
572
+ flags=MakerFlags(reg_outputs_black_list=ignored_regs),
573
573
  )
574
574
  check_inputs(g2, "RSP", "#0x8", "#0x5") # 0x5 is return address
575
575
  check_outputs(g2, "RSP", "MEMORY")
@@ -1131,7 +1131,7 @@ RET
1131
1131
 
1132
1132
  for n, node in enumerate(g.nodes):
1133
1133
  if node.kind == NodeKinds.Constant:
1134
- assert proxy[n].get_data_successors(), f"Orphan constant {node}"
1134
+ assert list(proxy[n].get_data_successors()), f"Orphan constant {node}"
1135
1135
 
1136
1136
 
1137
1137
  def test_lea_simplification(outdir: Path, x86_64: ArchContext, log):
@@ -0,0 +1,3 @@
1
+ - Add Basic Blocks in Analysis stage to speed up Reaching Defs computation
2
+ - Specify CDGv2 with BB nodes and check it helps to reduce graph diameter while keeping CFG invariant by instruction reordering.
3
+
File without changes
File without changes
File without changes