ocgraph 0.3.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.
Files changed (37) hide show
  1. ocgraph/__init__.py +0 -0
  2. ocgraph/__main__.py +95 -0
  3. ocgraph/configuration/__init__.py +4 -0
  4. ocgraph/configuration/architecture/__init__.py +8 -0
  5. ocgraph/configuration/architecture/architecture.py +66 -0
  6. ocgraph/configuration/architecture/arm.py +52 -0
  7. ocgraph/configuration/architecture/msp430.py +48 -0
  8. ocgraph/configuration/architecture/ppc.py +698 -0
  9. ocgraph/configuration/architecture/sparc.py +100 -0
  10. ocgraph/configuration/architecture/x86.py +34 -0
  11. ocgraph/configuration/configuration.py +122 -0
  12. ocgraph/configuration/disassembler/__init__.py +10 -0
  13. ocgraph/configuration/disassembler/disassembler.py +148 -0
  14. ocgraph/configuration/disassembler/gdb_default.py +11 -0
  15. ocgraph/configuration/disassembler/objdump_arm.py +11 -0
  16. ocgraph/configuration/disassembler/objdump_msp430.py +27 -0
  17. ocgraph/configuration/disassembler/objdump_ppc.py +11 -0
  18. ocgraph/configuration/disassembler/objdump_sparc.py +11 -0
  19. ocgraph/configuration/disassembler/objdump_x86.py +11 -0
  20. ocgraph/configuration/logger.py +50 -0
  21. ocgraph/data/__init__.py +9 -0
  22. ocgraph/data/address.py +58 -0
  23. ocgraph/data/basic_block.py +37 -0
  24. ocgraph/data/encoding.py +34 -0
  25. ocgraph/data/function_header.py +23 -0
  26. ocgraph/data/instruction.py +102 -0
  27. ocgraph/data/jump_table.py +60 -0
  28. ocgraph/interface/__init__.py +5 -0
  29. ocgraph/interface/analyzer.py +220 -0
  30. ocgraph/interface/coverage_reader.py +70 -0
  31. ocgraph/interface/drawer.py +181 -0
  32. ocgraph-0.3.0.dist-info/LICENSES/GTDGmbH.md +24 -0
  33. ocgraph-0.3.0.dist-info/LICENSES/MauriMustonen.md +24 -0
  34. ocgraph-0.3.0.dist-info/METADATA +55 -0
  35. ocgraph-0.3.0.dist-info/RECORD +37 -0
  36. ocgraph-0.3.0.dist-info/WHEEL +4 -0
  37. ocgraph-0.3.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,34 @@
1
+ """Encoding Class."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class Encoding:
7
+ """
8
+ Encoding represents a sequence of bytes for instruction encoding.
9
+
10
+ E.g. the '31 c0' in:
11
+
12
+ '16bd3: 31 c0 xor %eax,%eax'
13
+ """
14
+
15
+ def __init__(self, bites: list[int]) -> None:
16
+ self.bites = bites
17
+
18
+ def __len__(self) -> int:
19
+ """
20
+ Return size of the bytes.
21
+
22
+ >>> len(Encoding([0xa,0xb,0xc]))
23
+ 3
24
+ """
25
+ return len(self.bites)
26
+
27
+ def __repr__(self) -> str:
28
+ """
29
+ Convert an Encoding to a readable string.
30
+
31
+ >>> Encoding([0xa,0xb,0xc])
32
+ 0xa 0xb 0xc
33
+ """
34
+ return " ".join(f"{b:#x}" for b in self.bites)
@@ -0,0 +1,23 @@
1
+ """Represents a function with an address."""
2
+
3
+
4
+ class FunctionHeader:
5
+ """FunctionHeader Class."""
6
+
7
+ def __init__(
8
+ self,
9
+ name: str,
10
+ addr: int,
11
+ ) -> None:
12
+ """Create a new FunctionHeader."""
13
+ self.name = name
14
+ self.addr = addr
15
+
16
+ def __repr__(self) -> str:
17
+ """
18
+ Transform a FunctionHeader into a readable string.
19
+
20
+ >>> FunctionHeader("fn", 0x1234)
21
+ FunctionHeader(fn at 0x1234)
22
+ """
23
+ return f"FunctionHeader({self.name} at 0x{self.addr:x})"
@@ -0,0 +1,102 @@
1
+ """
2
+ Represents a single assembly instruction with its operands, location and optional branch
3
+ target.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from enum import Enum
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING: # pragma: no cover
12
+ from .address import Address
13
+
14
+
15
+ class Coverage(Enum):
16
+ """Enumeration for coverage records."""
17
+
18
+ MISS = "missed"
19
+ """Indicates instruction is not executed"""
20
+ LINE_TAKEN = "taken"
21
+ """Indicates instruction is executed"""
22
+ JUMP_PASS = "skipped"
23
+ """Indicates, if branch is just passed without jump"""
24
+ JUMP_TAKEN = "jumped"
25
+ """Indicates, if branch is just jumped without passed"""
26
+ JUMP_BOTH = "both taken"
27
+ """Indicates, if branch is just jumped without passed"""
28
+
29
+
30
+ class Instruction:
31
+ """Instruction Class."""
32
+
33
+ lineno: int | None = None
34
+ """Line number in the file"""
35
+
36
+ address: Address | None = None
37
+ """Computed address of the instruction"""
38
+
39
+ opcode: str | None = None
40
+ """Disassembled opcode"""
41
+
42
+ ops: list[str] = []
43
+ """Disassembled operands of the instruction"""
44
+
45
+ target: Address | None = None
46
+ """Optional target of the instruction (branch)"""
47
+
48
+ returns: set[int] = set()
49
+ """Stores addresses of sink instructions like return"""
50
+
51
+ coverage: Coverage = Coverage.MISS
52
+ """If line is executed on test"""
53
+
54
+ branch_taken: bool | None = None
55
+
56
+ def __init__(
57
+ self,
58
+ lineno: int,
59
+ address: Address,
60
+ opcode: str,
61
+ ops: list[str],
62
+ target: Address | None,
63
+ ) -> None:
64
+ self.lineno = lineno
65
+ self.address = address
66
+ self.opcode = opcode
67
+ self.ops = ops
68
+ self.target = target
69
+ self.returns = set()
70
+ self.coverage = Coverage.MISS
71
+
72
+ def update_coverage(self, addresses: set[int], is_branch: bool = False) -> None:
73
+ """
74
+ Update the coverage information of the instruction.
75
+
76
+ :param addresses: Set of addresses executed after this instruction,
77
+ used to determine whether a branch was taken or not.
78
+ :param is_branch: Flag whether the instruction should be
79
+ interpreted as a branch
80
+ """
81
+ if not is_branch:
82
+ # exception for ret (target is None) or call (target.abs is None)
83
+ self.coverage = Coverage.LINE_TAKEN
84
+ self.returns = addresses
85
+ elif len(addresses) == 2 and self.target and self.target.abs_addr in addresses:
86
+ self.coverage = Coverage.JUMP_BOTH
87
+ elif len(addresses) == 1 and self.target and self.target.abs_addr in addresses:
88
+ self.coverage = Coverage.JUMP_TAKEN
89
+ elif len(addresses) == 1:
90
+ self.coverage = Coverage.JUMP_PASS
91
+ else:
92
+ msg = f"Invalid Coverage Information at {self.address}: {addresses}"
93
+ raise AssertionError(msg)
94
+
95
+ def __repr__(self) -> str:
96
+ """Convert an Instruction into a representative string."""
97
+ result = f"{self.address}: {self.opcode}"
98
+ if self.ops:
99
+ result += f" {self.ops}"
100
+ if self.target is not None:
101
+ result += " -> " + str(self.target)
102
+ return f"Instruction({result})"
@@ -0,0 +1,60 @@
1
+ """Holds info about branch sources and destinations in asm function."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING: # pragma: no cover
8
+ from ocgraph.configuration.configuration import OcGraphConfiguration
9
+
10
+ from .address import Address
11
+ from .instruction import Instruction
12
+
13
+
14
+ class JumpTable:
15
+ """JumpTable Class."""
16
+
17
+ def __init__(
18
+ self,
19
+ instructions: list[Instruction],
20
+ configuration: OcGraphConfiguration,
21
+ ) -> None:
22
+ self.config: OcGraphConfiguration = configuration
23
+
24
+ # Address where the jump begins and value which address
25
+ # to jump to. This also includes calls.
26
+ self.abs_sources: dict[int, Address] = {}
27
+ self.rel_sources: dict[int, Address] = {}
28
+
29
+ # Addresses where jumps end inside the current function.
30
+ self.abs_destinations: set[int] = set()
31
+ self.rel_destinations: set[int] = set()
32
+
33
+ # Iterate over the instructions and collect jump targets and branching points.
34
+ for instr in instructions:
35
+ if instr is None or instr.target is None or instr.address is None:
36
+ continue
37
+
38
+ if instr.target.abs_addr is not None and instr.address.abs_addr is not None:
39
+ self.abs_sources[instr.address.abs_addr] = instr.target
40
+ self.abs_destinations.add(instr.target.abs_addr)
41
+
42
+ if instr.target.offset is not None and instr.address.offset is not None:
43
+ self.rel_sources[instr.address.offset] = instr.target
44
+ self.rel_destinations.add(instr.target.offset)
45
+
46
+ def is_jump_target(self, addr: Address) -> bool:
47
+ """Return if address is a destination."""
48
+ if addr.abs_addr is not None:
49
+ return addr.abs_addr in self.abs_destinations
50
+ if addr.offset is not None:
51
+ return addr.offset in self.rel_destinations
52
+ return False
53
+
54
+ def get_target(self, addr: Address) -> Address | None:
55
+ """Return the target of a branch."""
56
+ if addr.abs_addr is not None:
57
+ return self.abs_sources.get(addr.abs_addr)
58
+ if addr.offset is not None:
59
+ return self.rel_sources.get(addr.offset)
60
+ return None
@@ -0,0 +1,5 @@
1
+ """The main file of the interface module."""
2
+
3
+ from .analyzer import Analyzer as Analyzer
4
+ from .coverage_reader import CoverageReader as CoverageReader
5
+ from .drawer import Drawer as Drawer
@@ -0,0 +1,220 @@
1
+ """Class for read and analyze the input string."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ocgraph.data import Address, BasicBlock, Instruction, JumpTable
8
+
9
+ if TYPE_CHECKING: # pragma: no cover
10
+ from ocgraph.configuration import OcGraphConfiguration
11
+ from ocgraph.configuration.disassembler import Disassembler
12
+
13
+
14
+ class Analyzer:
15
+ """Analyzer Class."""
16
+
17
+ def __init__(self, config: OcGraphConfiguration) -> None:
18
+ self.configuration = config
19
+ self.logger = self.configuration.logger
20
+ self.parser: Disassembler = self.configuration.disassembler
21
+
22
+ self.lines: list[str] = []
23
+ self.function_name: str = ""
24
+ self.instructions: list[Instruction] = []
25
+ self.jump_table: JumpTable
26
+ self.basic_blocks: dict[int, BasicBlock] = {}
27
+
28
+ def parse_file(self, file_path: str) -> None:
29
+ """Parse a assembler file."""
30
+ with open(file_path, encoding="utf8") as asm_file:
31
+ lines = asm_file.readlines()
32
+ self.parse_lines(lines)
33
+
34
+ def parse_lines(self, lines: list[str]) -> None:
35
+ """Parse a list of assembly lines."""
36
+ self.lines = lines
37
+ self._parse_instructions()
38
+ # Compute already known jump targets
39
+ # This is only a first pass as indirect jumps can't be
40
+ # discovered yet
41
+ self._compute_jump_targets()
42
+
43
+ def analyze(self) -> None:
44
+ # Re-compute jump targets this time with indirect branches
45
+ self._compute_jump_targets()
46
+ self._infer_relative_addresses()
47
+ self._create_jump_table()
48
+ self._create_basic_blocks()
49
+
50
+ def _parse_instructions(self) -> None:
51
+ self.instructions = []
52
+
53
+ for info in self.parser.extract_information(self.lines):
54
+ if isinstance(info, Instruction):
55
+ self.instructions.append(info)
56
+ else:
57
+ if self.function_name:
58
+ msg = "we handle only one function for now"
59
+ raise RuntimeError(msg)
60
+ self.logger.info(f"New function {info.name}")
61
+ self.function_name = info.name
62
+
63
+ def _compute_jump_targets(self) -> None:
64
+ # Infer target address for jump instructions
65
+ for instr in self.instructions:
66
+ if (
67
+ instr.target is None or instr.target.abs_addr is None
68
+ ) and self.configuration.architecture.is_direct_branch(instr):
69
+ if instr.target is None:
70
+ instr.target = Address(None)
71
+ # parse the absolute target out of the operands
72
+ # (first hex address is assumed to be the target address)
73
+ try:
74
+ instr.target.abs_addr = self.parser.parse_jump_target(instr.ops)
75
+ except ValueError:
76
+ # Mark the address as not (yet) known
77
+ instr.target.abs_addr = None
78
+
79
+ def _infer_relative_addresses(self) -> None:
80
+ # Infer relative addresses (for objdump or stripped gdb)
81
+ if (
82
+ len(self.instructions) > 0
83
+ and self.instructions[0].address
84
+ and self.instructions[0].address.abs_addr is not None
85
+ ):
86
+ start_address = self.instructions[0].address.abs_addr
87
+ else:
88
+ start_address = 0
89
+ if (
90
+ len(self.instructions) > 0
91
+ and self.instructions[-1].address
92
+ and self.instructions[-1].address.abs_addr is not None
93
+ ):
94
+ end_address = self.instructions[-1].address.abs_addr
95
+ else:
96
+ end_address = 0xFFFFFFFF
97
+
98
+ for instr in self.instructions:
99
+ for addr in (instr.address, instr.target):
100
+ if (
101
+ addr is not None
102
+ and addr.abs_addr is not None
103
+ and addr.offset is None
104
+ and start_address <= addr.abs_addr <= end_address
105
+ ):
106
+ addr.offset = addr.abs_addr - start_address
107
+
108
+ self.logger.debug("Instructions:")
109
+ for instruction in self.instructions:
110
+ if instruction is not None:
111
+ self.logger.debug(f" {instruction}")
112
+
113
+ def _create_jump_table(self) -> None:
114
+ self.jump_table = JumpTable(self.instructions, self.configuration)
115
+
116
+ self.logger.debug("Absolute destinations:")
117
+ for dst in self.jump_table.abs_destinations:
118
+ if dst:
119
+ self.logger.debug(f" {dst:#x}")
120
+ else:
121
+ self.logger.debug(" None")
122
+ self.logger.debug("Relative destinations:")
123
+ for dst in self.jump_table.rel_destinations:
124
+ if dst:
125
+ self.logger.debug(f" {dst:#x}")
126
+ else:
127
+ self.logger.debug(" None")
128
+ self.logger.debug("Absolute branches:")
129
+ for key, addr in self.jump_table.abs_sources.items():
130
+ self.logger.debug(f" {key:#x} -> {addr}")
131
+ self.logger.debug("Relative branches:")
132
+ for key, addr in self.jump_table.rel_sources.items():
133
+ self.logger.debug(f" {key:#x} -> {addr}")
134
+
135
+ def _create_basic_blocks(self) -> None:
136
+ """
137
+ Now iterate over the assembly again and split it to basic blocks using the branching
138
+ information from earlier.
139
+ """
140
+ self.basic_blocks = {}
141
+
142
+ curr_basic_block: BasicBlock | None = None
143
+ # Store last block if ending with branch opcode
144
+ prev_branch_block: BasicBlock | None = None
145
+
146
+ # block completion flag (introduced for SPARC pipeline)
147
+ block_completion: int = 0
148
+
149
+ for instruction in self.instructions:
150
+ # if block completion is in progress
151
+ if block_completion > 0:
152
+ block_completion -= 1
153
+ if block_completion > 0 and curr_basic_block:
154
+ self.basic_blocks[curr_basic_block.key].add_instruction(instruction)
155
+ continue
156
+ curr_basic_block = None
157
+
158
+ # Current program counter
159
+ pc_addr = instruction.address
160
+ # Get optional jump target
161
+ jump_target = self.jump_table.get_target(pc_addr or Address(None))
162
+ is_unconditional = self.configuration.architecture.is_unconditional_branch(
163
+ instruction,
164
+ )
165
+
166
+ # Start new blocks if last ended
167
+ if curr_basic_block is None and pc_addr is not None and pc_addr.abs_addr is not None:
168
+ # Create new basic block
169
+ self.basic_blocks[pc_addr.abs_addr] = curr_basic_block = BasicBlock(
170
+ key=pc_addr.abs_addr,
171
+ )
172
+
173
+ # if previous basic block ended in branch instruction. Add the basic
174
+ # block what follows if the jump was not taken.
175
+ if prev_branch_block is not None:
176
+ prev_branch_block.add_no_jump_edge(curr_basic_block)
177
+ prev_branch_block = None
178
+ # or if current address is a jump target
179
+ elif (
180
+ curr_basic_block is not None
181
+ and pc_addr is not None
182
+ and pc_addr.abs_addr is not None
183
+ and self.jump_table.is_jump_target(pc_addr)
184
+ ):
185
+ closing_block = curr_basic_block
186
+ self.basic_blocks[pc_addr.abs_addr] = curr_basic_block = BasicBlock(
187
+ key=pc_addr.abs_addr,
188
+ )
189
+ closing_block.add_no_jump_edge(pc_addr.abs_addr)
190
+
191
+ if curr_basic_block is not None:
192
+ curr_basic_block.add_instruction(instruction)
193
+
194
+ # End current block if current opcode is a jump/branch/sink
195
+ if jump_target and curr_basic_block:
196
+ curr_basic_block.add_jump_edge(jump_target.abs_addr)
197
+ prev_branch_block = None if is_unconditional else curr_basic_block
198
+ block_completion = self.configuration.architecture.get_branch_delay(
199
+ instruction,
200
+ )
201
+ elif self.configuration.architecture.is_sink(instruction):
202
+ block_completion = self.configuration.architecture.get_branch_delay(
203
+ instruction,
204
+ )
205
+ prev_branch_block = None
206
+
207
+ if prev_branch_block:
208
+ # If last instruction of the function is jump/call, then add dummy
209
+ # block to designate end of the function.
210
+ end_instruction = Instruction(
211
+ 0,
212
+ Address(None),
213
+ "",
214
+ [],
215
+ Address(None),
216
+ )
217
+ end_block = BasicBlock("end_of_function")
218
+ end_block.add_instruction(end_instruction)
219
+ prev_branch_block.add_no_jump_edge(end_block.key)
220
+ self.basic_blocks[end_block.key] = end_block
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: GTDGmbH
2
+ """Class for read coverage input and update the instruction."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import ast
7
+ import csv
8
+ from typing import TYPE_CHECKING
9
+
10
+ from ocgraph.data import Address, Instruction
11
+
12
+ if TYPE_CHECKING: # pragma: no cover
13
+ from ocgraph.configuration import OcGraphConfiguration
14
+
15
+
16
+ class CoverageReader: # pylint: disable=too-few-public-methods
17
+ """CoverageReader Class."""
18
+
19
+ def __init__(
20
+ self,
21
+ instructions: list[Instruction],
22
+ config: OcGraphConfiguration,
23
+ ) -> None:
24
+ self.instructions = instructions
25
+ self.config = config
26
+
27
+ def update_by_csv(self, file_path: str) -> None:
28
+ """Read coverage csv file and update."""
29
+ # Store for coverage information
30
+ coverage_info: dict[int, set[int]] = {}
31
+
32
+ # read the csv file. expected values in address and branch_jumps
33
+ with open(file_path, newline="", encoding="utf-8") as csvfile:
34
+ reader = csv.DictReader(csvfile)
35
+ for row in reader:
36
+ _temp = ast.literal_eval(row["branch_jumps"])
37
+ coverage_info[int(row["address"], 0)] = {int(x, 0) for x in _temp}
38
+
39
+ # update instructions
40
+ for instr in self.instructions:
41
+ if (
42
+ instr.address
43
+ and instr.address.abs_addr
44
+ and (covered_branch_targets := coverage_info.get(instr.address.abs_addr, None))
45
+ is not None
46
+ ):
47
+ is_branch = self.config.architecture.is_branch(instr)
48
+
49
+ # Check if this is a branch which misses its branch target.
50
+ # This happens when the branch is indirect and
51
+ # objdump does not know the address of the branch target.
52
+ if is_branch:
53
+ if instr.target is None:
54
+ instr.target = Address(None)
55
+
56
+ if instr.target.abs_addr is None:
57
+ max_distance = 0
58
+ likely_branch_target = None
59
+
60
+ # Determine the most interesting branch target,
61
+ # which is the one furthest away from the branch location
62
+ for branch_target in covered_branch_targets:
63
+ distance = abs(instr.address.abs_addr - branch_target)
64
+ if distance > max_distance:
65
+ max_distance = distance
66
+ likely_branch_target = branch_target
67
+
68
+ instr.target.abs_addr = likely_branch_target
69
+
70
+ instr.update_coverage(covered_branch_targets, is_branch=is_branch)