codevanta 0.1.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 (65) hide show
  1. codevanta/__init__.py +1 -0
  2. codevanta/analysis/__init__.py +1 -0
  3. codevanta/analysis/binary_diff.py +1168 -0
  4. codevanta/analysis/callgraph.py +542 -0
  5. codevanta/analysis/cfg.py +1201 -0
  6. codevanta/analysis/dataflow.py +762 -0
  7. codevanta/analysis/debugger.py +289 -0
  8. codevanta/analysis/debuginfo.py +304 -0
  9. codevanta/analysis/decompile.py +1094 -0
  10. codevanta/analysis/decompilers/__init__.py +250 -0
  11. codevanta/analysis/decompilers/angr_backend.py +251 -0
  12. codevanta/analysis/decompilers/base.py +278 -0
  13. codevanta/analysis/decompilers/ghidra_backend.py +316 -0
  14. codevanta/analysis/dynamic.py +756 -0
  15. codevanta/analysis/emulate.py +1349 -0
  16. codevanta/analysis/evidence_bundle.py +736 -0
  17. codevanta/analysis/function_hash.py +562 -0
  18. codevanta/analysis/graph_export.py +441 -0
  19. codevanta/analysis/html_view.py +695 -0
  20. codevanta/analysis/jump_tables.py +546 -0
  21. codevanta/analysis/solver.py +715 -0
  22. codevanta/analysis/symbolic.py +1809 -0
  23. codevanta/analysis/trace.py +646 -0
  24. codevanta/analysis/type_propagation.py +1055 -0
  25. codevanta/analysis/type_recovery.py +1214 -0
  26. codevanta/analysis/vtables.py +572 -0
  27. codevanta/analysis/xrefs.py +434 -0
  28. codevanta/api/__init__.py +1 -0
  29. codevanta/cli/__init__.py +1 -0
  30. codevanta/cli/main.py +3038 -0
  31. codevanta/core/__init__.py +31 -0
  32. codevanta/core/disassembler.py +121 -0
  33. codevanta/core/entropy.py +237 -0
  34. codevanta/core/functions.py +176 -0
  35. codevanta/core/loader.py +1351 -0
  36. codevanta/core/report.py +306 -0
  37. codevanta/core/strings.py +109 -0
  38. codevanta/evidence/__init__.py +7 -0
  39. codevanta/evidence/confidence.py +36 -0
  40. codevanta/evidence/models.py +43 -0
  41. codevanta/evidence/provenance.py +37 -0
  42. codevanta/gui/__init__.py +46 -0
  43. codevanta/gui/layout.py +1384 -0
  44. codevanta/gui/server.py +1305 -0
  45. codevanta/ir/__init__.py +6 -0
  46. codevanta/ir/adapters/__init__.py +31 -0
  47. codevanta/ir/base.py +31 -0
  48. codevanta/ir/instruction.py +42 -0
  49. codevanta/mcp_server/__init__.py +5 -0
  50. codevanta/mcp_server/server.py +710 -0
  51. codevanta/mcp_server/tools.py +1364 -0
  52. codevanta/plugins/__init__.py +748 -0
  53. codevanta/setup/__init__.py +28 -0
  54. codevanta/setup/apply.py +22 -0
  55. codevanta/setup/clients.py +285 -0
  56. codevanta/setup/plan.py +79 -0
  57. codevanta/setup/verify.py +43 -0
  58. codevanta/storage/__init__.py +1 -0
  59. codevanta/storage/session.py +514 -0
  60. codevanta-0.1.0.dist-info/METADATA +688 -0
  61. codevanta-0.1.0.dist-info/RECORD +65 -0
  62. codevanta-0.1.0.dist-info/WHEEL +5 -0
  63. codevanta-0.1.0.dist-info/entry_points.txt +2 -0
  64. codevanta-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. codevanta-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1094 @@
1
+ """Heuristic pseudo-C decompilation of one function (phase 3).
2
+
3
+ :func:`decompile_function` renders a single function as readable pseudo-C. It
4
+ never re-implements decoding: the recursive CFG
5
+ (:func:`codevanta.analysis.cfg.build_recursive_cfg`) supplies the instructions
6
+ and the block structure, function discovery supplies symbol names, and this
7
+ module only *shapes* that structure into text.
8
+
9
+ 1. **Signature.** The argument count is inferred from the ABI argument registers
10
+ read before the first ``call`` (``rdi``/``rsi``/``rdx``/``rcx``/``r8``/``r9``
11
+ plus their 32/16/8-bit aliases on x86_64, ``ecx``/``edx`` on x86), capped at
12
+ six. A register counts as *read* unless it is only the destination of the
13
+ instruction, so ``mov rax, rdi`` reads ``rdi`` while ``mov rdi, rax`` does
14
+ not. The name comes from the discovered function candidate, else
15
+ ``sub_<address>``. Every parameter and the return value then carry an inferred
16
+ C type from :mod:`codevanta.analysis.type_propagation` -- the count rule is
17
+ shared with it, so the signature and the rendered ``a0``..``a5`` operands
18
+ always agree -- and the header repeats one short line per argument. An
19
+ argument nothing can be proven about keeps the ``unknown<bitness>``
20
+ placeholder with a low confidence rather than a guess.
21
+ 2. **Operands.** Argument registers become ``a0``..``a5``; every other register
22
+ keeps its architectural name. ``[rbp - 8]``-style operands become frame slots
23
+ (``local_0x8`` below the frame pointer, ``arg_0x8`` above it) and other
24
+ memory operands become ``*(u64*)(a0 + 0x10)`` dereferences with the pointer
25
+ width of the binary (``u32`` for 32-bit files).
26
+ 3. **Statements.** Only semantics that the mnemonic alone justifies are
27
+ rendered: ``mov``/``lea``/compound assignments and unary updates. Every other
28
+ instruction keeps its comment but produces no statement, so nothing is
29
+ invented. Every emitted source line carries the raw instruction text as a
30
+ trailing ``/* mov rax, rdi */`` comment.
31
+ 4. **Control flow.** A forward conditional branch with two decoded successors
32
+ opens ``if (<condition>) {`` with the *inverted* condition (the fallthrough is
33
+ the then-branch) and closes with ``}``; when the then-branch jumps over an
34
+ exclusive else region the two halves are joined as ``} else {``. A block that
35
+ is the target of a back edge opens ``while (1) {``; the latch renders
36
+ ``continue;`` or ``if (<exit condition>) { break; }``. Anything left over --
37
+ irreducible edges, forward branches that cross an enclosing region, targets
38
+ outside the decoded window -- is rendered as ``goto LABEL_<addr>;`` with a
39
+ matching ``LABEL_<addr>:``.
40
+ 5. **Conditions.** ``cmp``/``test`` operands feed the following branch:
41
+ ``je``/``jne`` give ``==``/``!=``, ``jl``..``jg`` give signed ``< <= > >=``
42
+ and ``jb``..``ja`` give the ``(unsigned)`` forms. When no comparison can be
43
+ justified the condition is ``flag_from_<mnemonic>`` (or
44
+ ``!flag_from_<mnemonic>`` for an inverted branch).
45
+ 6. **Calls.** ``call_<target>();`` for a direct target, ``<import_name>()`` when
46
+ the target matches an entry of ``BinaryInfo.imports`` and ``(*ptr)();`` for an
47
+ indirect one (recorded as :class:`CallSite` with ``target=None``).
48
+
49
+ Every result carries :class:`Evidence`; the confidence is :attr:`Confidence.LOW`
50
+ because the whole rendering is a mnemonic-level heuristic. The header comment
51
+ block repeats the address, the confidence and the evidence note, and the target
52
+ binary is only ever read, never executed. ``evidence.extra`` carries the
53
+ inferred signature as data (``argument_types``, ``return_type``) so JSON
54
+ consumers do not have to parse the pseudo-C.
55
+
56
+ Work is bounded by ``max_instructions`` and ``max_blocks`` (defaults
57
+ :data:`DEFAULT_MAX_INSTRUCTIONS` / :data:`DEFAULT_MAX_BLOCKS`); a cut-short
58
+ rendering sets :attr:`PseudoFunction.truncated` and adds a ``warning:`` line to
59
+ the header. An address that is not executable code, a budget out of range and a
60
+ region that decodes to nothing all raise :class:`AnalysisError`.
61
+ """
62
+
63
+ from __future__ import annotations
64
+
65
+ import re
66
+ from dataclasses import dataclass
67
+ from pathlib import Path
68
+ from typing import Any, Literal
69
+
70
+ from codevanta.analysis.cfg import BasicBlock, FunctionCFG, build_recursive_cfg
71
+ from codevanta.analysis.type_propagation import (
72
+ ArgumentType,
73
+ ReturnType,
74
+ infer_argument_types,
75
+ infer_return_type,
76
+ )
77
+ from codevanta.core.functions import discover_functions
78
+ from codevanta.core.loader import BinaryInfo
79
+ from codevanta.core.report import MAX_DISASSEMBLY_COUNT, AnalysisError
80
+ from codevanta.evidence.confidence import Confidence
81
+ from codevanta.evidence.models import Evidence
82
+
83
+ __all__ = [
84
+ "CallSite",
85
+ "DEFAULT_MAX_BLOCKS",
86
+ "DEFAULT_MAX_INSTRUCTIONS",
87
+ "MAX_MAX_BLOCKS",
88
+ "MAX_MAX_INSTRUCTIONS",
89
+ "PseudoFunction",
90
+ "decompile_function",
91
+ ]
92
+
93
+ #: Default instruction budget for one rendering.
94
+ DEFAULT_MAX_INSTRUCTIONS = 2_000
95
+
96
+ #: Largest accepted ``max_instructions``; larger requests are rejected.
97
+ MAX_MAX_INSTRUCTIONS = MAX_DISASSEMBLY_COUNT
98
+
99
+ #: Default block budget for one rendering.
100
+ DEFAULT_MAX_BLOCKS = 500
101
+
102
+ #: Largest accepted ``max_blocks``; larger requests are rejected.
103
+ MAX_MAX_BLOCKS = MAX_DISASSEMBLY_COUNT
104
+
105
+ #: One indentation step, in spaces.
106
+ _INDENT = " "
107
+
108
+ #: Evidence source label for a rendered function.
109
+ _SOURCE = "decompile.pseudo_c"
110
+
111
+ #: Evidence method label for a rendered function.
112
+ _METHOD = "mnemonic_heuristic"
113
+
114
+ #: Mnemonics that end a block with a plain return.
115
+ _RET_MNEMONICS = frozenset({"ret", "retn", "retf", "retq", "iret", "iretd", "iretq", "eret"})
116
+
117
+ #: Mnemonics that are an unconditional jump.
118
+ _UNCONDITIONAL_JUMPS = frozenset({"jmp", "ljmp", "b", "br", "bx"})
119
+
120
+ #: Mnemonics that transfer control to a subroutine.
121
+ _CALL_MNEMONICS = frozenset({"call", "callq", "bl", "blx", "blr"})
122
+
123
+ #: ``mov``-style mnemonics: the first operand is only ever written.
124
+ _MOV_MNEMONICS = frozenset({"mov", "movabs", "movbe", "movsx", "movsxd", "movzx", "movq"})
125
+
126
+ #: Read-modify-write mnemonics rendered as ``dst <op>= src``.
127
+ _COMPOUND_ASSIGN: dict[str, str] = {
128
+ "add": "+=",
129
+ "sub": "-=",
130
+ "and": "&=",
131
+ "or": "|=",
132
+ "xor": "^=",
133
+ "imul": "*=",
134
+ "shl": "<<=",
135
+ "sal": "<<=",
136
+ "shr": ">>=",
137
+ "sar": ">>=",
138
+ }
139
+
140
+ #: Unary mnemonics rendered as ``dst = <prefix>dst``.
141
+ _UNARY_ASSIGN: dict[str, str] = {"neg": "-", "not": "~"}
142
+
143
+ #: Unary increment/decrement mnemonics.
144
+ _STEP_ASSIGN: dict[str, str] = {"inc": "++", "dec": "--"}
145
+
146
+ #: SysV argument registers, in argument order, for x86_64.
147
+ _ARG_REGISTERS_64 = ("rdi", "rsi", "rdx", "rcx", "r8", "r9")
148
+
149
+ #: Argument registers used for 32-bit x86 (``__thiscall``/``__fastcall`` order).
150
+ _ARG_REGISTERS_32 = ("ecx", "edx")
151
+
152
+ #: Every name a canonical argument register can appear under in Capstone text.
153
+ _ARG_ALIASES: dict[str, tuple[str, ...]] = {
154
+ "rdi": ("rdi", "edi", "di", "dil"),
155
+ "rsi": ("rsi", "esi", "si", "sil"),
156
+ "rdx": ("rdx", "edx", "dx", "dl"),
157
+ "rcx": ("rcx", "ecx", "cx", "cl"),
158
+ "r8": ("r8", "r8d", "r8w", "r8b"),
159
+ "r9": ("r9", "r9d", "r9w", "r9b"),
160
+ "ecx": ("ecx", "cx", "cl"),
161
+ "edx": ("edx", "dx", "dl"),
162
+ }
163
+
164
+ #: Registers whose relative operands denote stack slots.
165
+ _STACK_REGISTERS = frozenset({"rbp", "ebp", "rsp", "esp"})
166
+
167
+ #: Instruction-pointer register names (RIP-relative operands hold a global).
168
+ _PC_REGISTERS = frozenset({"rip", "eip"})
169
+
170
+ #: Capstone size keywords that may prefix a memory operand.
171
+ _SIZE_KEYWORDS = "byte|word|dword|qword|tbyte|xmmword|oword|fword"
172
+
173
+ #: A single address operand, e.g. ``0x401010`` (the only direct form accepted).
174
+ _DIRECT_OPERAND = re.compile(r"^0x[0-9a-fA-F]+$")
175
+
176
+ #: A memory operand: an optional size keyword plus a bracketed expression.
177
+ _MEMORY_OPERAND = re.compile(
178
+ rf"^(?:(?:{_SIZE_KEYWORDS})\s+ptr\s+)?\[\s*(?P<body>[^\]]*?)\s*\]$",
179
+ re.IGNORECASE,
180
+ )
181
+
182
+ #: One term of a bracketed address expression.
183
+ _ADDRESS_TERM = re.compile(
184
+ r"(?P<sign>[+-])?\s*(?P<value>0x[0-9a-fA-F]+|\d+|[a-zA-Z][a-zA-Z0-9]*)"
185
+ r"(?:\s*\*\s*(?P<scale>\d+))?"
186
+ )
187
+
188
+ #: An operand that is exactly one register or one immediate.
189
+ _SIMPLE_OPERAND = re.compile(r"^(?:[a-z][a-z0-9]*|-?(?:0x[0-9a-f]+|\d+))$")
190
+
191
+ #: Maximum number of arguments rendered in a signature.
192
+ _MAX_ARGUMENTS = 6
193
+
194
+ #: Branch mnemonic -> (operator, unsigned) for a condition built from ``cmp``.
195
+ _COMPARISONS: dict[str, tuple[str, bool]] = {
196
+ "je": ("==", False),
197
+ "jz": ("==", False),
198
+ "jne": ("!=", False),
199
+ "jnz": ("!=", False),
200
+ "jl": ("<", False),
201
+ "jle": ("<=", False),
202
+ "jg": (">", False),
203
+ "jge": (">=", False),
204
+ "jb": ("<", True),
205
+ "jbe": ("<=", True),
206
+ "ja": (">", True),
207
+ "jae": (">=", True),
208
+ }
209
+
210
+ #: Branch mnemonic -> the mnemonic with the opposite polarity.
211
+ _INVERSES: dict[str, str] = {
212
+ "je": "jne",
213
+ "jne": "je",
214
+ "jz": "jnz",
215
+ "jnz": "jz",
216
+ "jl": "jge",
217
+ "jge": "jl",
218
+ "jle": "jg",
219
+ "jg": "jle",
220
+ "jb": "jae",
221
+ "jae": "jb",
222
+ "jbe": "ja",
223
+ "ja": "jbe",
224
+ "js": "jns",
225
+ "jns": "js",
226
+ "jo": "jno",
227
+ "jno": "jo",
228
+ "jp": "jnp",
229
+ "jnp": "jp",
230
+ }
231
+
232
+ #: Mnemonics whose only side effect on a condition is a comparison/test.
233
+ _COMPARE_MNEMONICS = frozenset({"cmp", "test", "comiss", "comisd", "ucomiss", "ucomisd"})
234
+
235
+
236
+ @dataclass(frozen=True)
237
+ class CallSite:
238
+ """One control transfer out of, or into, the rendered function.
239
+
240
+ Attributes:
241
+ address: Virtual address of the transferring instruction.
242
+ target: Absolute destination for a direct transfer, ``None`` when the
243
+ destination is only known at run time (a register or memory
244
+ operand).
245
+ name: Import or function-symbol name known for ``target``, if any.
246
+ kind: ``"call"`` for a subroutine call, ``"jump"`` for a direct jump out
247
+ of the function (a tail call), ``"unknown"`` for an indirect jump
248
+ whose destination could not be resolved.
249
+ """
250
+
251
+ address: int
252
+ target: int | None
253
+ name: str | None
254
+ kind: Literal["call", "jump", "unknown"]
255
+
256
+ def to_dict(self) -> dict[str, Any]:
257
+ """Serialize the call site for the JSON report."""
258
+ return {
259
+ "address": f"0x{self.address:x}",
260
+ "target": None if self.target is None else f"0x{self.target:x}",
261
+ "name": self.name,
262
+ "kind": self.kind,
263
+ }
264
+
265
+
266
+ @dataclass(frozen=True)
267
+ class PseudoFunction:
268
+ """Readable pseudo-C rendering of one function.
269
+
270
+ Attributes:
271
+ address: Virtual address the rendering starts at.
272
+ name: Function name from discovery, or ``None`` when unnamed.
273
+ lines: Rendered body lines in source order; each carries the raw
274
+ instruction text as a trailing comment.
275
+ body: Complete text: header comment block, signature, braces and body.
276
+ call_sites: Control transfers in ascending address order.
277
+ block_count: Blocks reported by the underlying CFG.
278
+ instruction_count: Unique instructions reported by the CFG.
279
+ truncated: ``True`` when a limit cut the rendering short.
280
+ evidence: How the rendering was produced.
281
+ """
282
+
283
+ address: int
284
+ name: str | None
285
+ lines: tuple[str, ...]
286
+ body: str
287
+ call_sites: tuple[CallSite, ...]
288
+ block_count: int
289
+ instruction_count: int
290
+ truncated: bool
291
+ evidence: Evidence
292
+
293
+ def to_dict(self) -> dict[str, Any]:
294
+ """Serialize the rendering for the JSON report."""
295
+ return {
296
+ "address": f"0x{self.address:x}",
297
+ "name": self.name,
298
+ "block_count": self.block_count,
299
+ "instruction_count": self.instruction_count,
300
+ "truncated": self.truncated,
301
+ "lines": list(self.lines),
302
+ "body": self.body,
303
+ "call_sites": [site.to_dict() for site in self.call_sites],
304
+ "evidence": self.evidence.to_dict(),
305
+ }
306
+
307
+
308
+ @dataclass(frozen=True)
309
+ class _Memory:
310
+ """A parsed bracketed address expression.
311
+
312
+ Attributes:
313
+ base: Base register (already lowercased), or ``None`` for an absolute.
314
+ index: Index register, or ``None``.
315
+ scale: Index scale, ``1`` when absent.
316
+ displacement: Signed displacement added to the base.
317
+ absolute: Literal address for absolute operands, else ``None``.
318
+ """
319
+
320
+ base: str | None
321
+ index: str | None
322
+ scale: int
323
+ displacement: int
324
+ absolute: int | None
325
+
326
+
327
+ @dataclass
328
+ class _Frame:
329
+ """One open structured construct while rendering.
330
+
331
+ Attributes:
332
+ kind: ``"if"``, ``"else"`` or ``"while"``.
333
+ stop: Address at which the construct closes (``None`` means "at the end
334
+ of the function", used by a loop whose exit is not known).
335
+ block: Start address of the block that opened the construct.
336
+ merge: For an ``"if"`` frame, the address its else region merges at.
337
+ """
338
+
339
+ kind: Literal["if", "else", "while"]
340
+ stop: int | None
341
+ block: int
342
+ merge: int | None = None
343
+
344
+
345
+ def _split_operands(text: str) -> list[str]:
346
+ """Split Capstone operand text on top-level commas."""
347
+ text = (text or "").strip()
348
+ if not text:
349
+ return []
350
+ parts: list[str] = []
351
+ current: list[str] = []
352
+ depth = 0
353
+ for character in text:
354
+ if character == "[":
355
+ depth += 1
356
+ elif character == "]":
357
+ depth = max(0, depth - 1)
358
+ if character == "," and depth == 0:
359
+ parts.append("".join(current).strip())
360
+ current = []
361
+ continue
362
+ current.append(character)
363
+ parts.append("".join(current).strip())
364
+ return [part for part in parts if part]
365
+
366
+
367
+ def _to_int(token: str) -> int | None:
368
+ """Parse a hexadecimal or decimal token, returning ``None`` on junk."""
369
+ try:
370
+ if token[:2].lower() == "0x":
371
+ return int(token, 16)
372
+ return int(token, 10)
373
+ except ValueError: # pragma: no cover - callers only pass matched digits
374
+ return None
375
+
376
+
377
+ def _parse_memory(operand: str) -> _Memory | None:
378
+ """Parse a bracketed address expression, or return ``None`` if unhandled.
379
+
380
+ Only ``[base]``, ``[base +/- disp]``, ``[base + index]`` and
381
+ ``[base + index*scale +/- disp]`` shapes are understood. Absolute operands
382
+ keep their literal address in :attr:`_Memory.absolute`; segment prefixes and
383
+ expressions with more than a base and an index are left to the caller.
384
+
385
+ Args:
386
+ operand: Capstone operand text, e.g. ``"qword ptr [rdi + 0x10]"``.
387
+ """
388
+ match = _MEMORY_OPERAND.match(operand.strip())
389
+ if match is None:
390
+ return None
391
+ body = match.group("body")
392
+ if ":" in body: # a segment override is never guessed at
393
+ return None
394
+ base: str | None = None
395
+ index: str | None = None
396
+ scale = 1
397
+ displacement = 0
398
+ for term in _ADDRESS_TERM.finditer(body):
399
+ value = term.group("value")
400
+ sign = -1 if term.group("sign") == "-" else 1
401
+ if value[0].isdigit():
402
+ number = _to_int(value)
403
+ if number is None: # pragma: no cover - the regex only matches digits
404
+ return None
405
+ displacement += sign * number
406
+ continue
407
+ register = value.lower()
408
+ term_scale = int(term.group("scale")) if term.group("scale") else 1
409
+ if base is None and term_scale == 1:
410
+ base = register
411
+ elif index is None:
412
+ index = register
413
+ scale = term_scale
414
+ else: # more than a base and an index: not modelled
415
+ return None
416
+ if base is None and index is None:
417
+ return _Memory(None, None, 1, 0, displacement)
418
+ return _Memory(base, index, scale, displacement, None)
419
+
420
+
421
+ def _instruction_text(instruction: dict[str, Any]) -> str:
422
+ """Return the raw ``"mnemonic operands"`` text of a decoded instruction."""
423
+ text = str(instruction.get("text") or "").strip()
424
+ if text:
425
+ return text
426
+ mnemonic = str(instruction.get("mnemonic") or "").strip()
427
+ operands = str(instruction.get("operands") or "").strip()
428
+ return f"{mnemonic} {operands}".strip()
429
+
430
+
431
+ def _mnemonic(instruction: dict[str, Any]) -> str:
432
+ """Return the lowercased mnemonic of a decoded instruction."""
433
+ return str(instruction.get("mnemonic") or "").strip().lower()
434
+
435
+
436
+ def _operands(instruction: dict[str, Any]) -> str:
437
+ """Return the operand text of a decoded instruction."""
438
+ return str(instruction.get("operands") or "")
439
+
440
+
441
+ def _address(instruction: dict[str, Any]) -> int:
442
+ """Return the virtual address of a decoded instruction.
443
+
444
+ ``BasicBlock.instructions`` holds ``IRInstruction.to_dict()`` payloads, where
445
+ the address is a hexadecimal string, so both shapes are accepted.
446
+ """
447
+ value = instruction.get("address")
448
+ if isinstance(value, int):
449
+ return value
450
+ if isinstance(value, str):
451
+ return _to_int(value.strip()) or 0
452
+ return 0 # pragma: no cover - defensive
453
+
454
+
455
+ def _size(instruction: dict[str, Any]) -> int:
456
+ """Return the encoded length of a decoded instruction."""
457
+ return int(instruction.get("size") or 0)
458
+
459
+
460
+ def _direct_target(operands: str) -> int | None:
461
+ """Return the absolute target encoded in ``operands``, if any.
462
+
463
+ Only a straight hexadecimal immediate is accepted (``jmp 0x401006``);
464
+ register and memory operands are indirect and yield ``None``.
465
+ """
466
+ text = (operands or "").strip()
467
+ if not _DIRECT_OPERAND.match(text):
468
+ return None
469
+ return _to_int(text)
470
+
471
+
472
+ def _argument_names(bitness: int) -> dict[str, str]:
473
+ """Map every alias of an ABI argument register to ``a0``..``a5``."""
474
+ registers = _ARG_REGISTERS_64 if bitness >= 64 else _ARG_REGISTERS_32
475
+ names: dict[str, str] = {}
476
+ for position, register in enumerate(registers[:_MAX_ARGUMENTS]):
477
+ for alias in _ARG_ALIASES.get(register, (register,)):
478
+ names.setdefault(alias, f"a{position}")
479
+ names.setdefault(register, f"a{position}")
480
+ return names
481
+
482
+
483
+ def _identifier(name: str | None, fallback: str) -> str:
484
+ """Return an ASCII C-ish identifier for a symbol name."""
485
+ if not name:
486
+ return fallback
487
+ clean = re.sub(r"[^0-9A-Za-z_]", "_", name.strip())
488
+ if not clean:
489
+ return fallback
490
+ return f"_{clean}" if clean[0].isdigit() else clean
491
+
492
+
493
+ def _slot(displacement: int) -> str:
494
+ """Name a frame-relative stack slot."""
495
+ if displacement < 0:
496
+ return f"local_0x{-displacement:x}"
497
+ if displacement == 0:
498
+ return "local_0x0"
499
+ return f"arg_0x{displacement:x}"
500
+
501
+
502
+ def _loop_exits(
503
+ blocks: list[BasicBlock], by_address: dict[int, BasicBlock]
504
+ ) -> dict[int, int | None]:
505
+ """Map every loop header to the lowest exit address its latches reveal."""
506
+ candidates: dict[int, list[int]] = {}
507
+ for block in blocks:
508
+ for successor in block.successors:
509
+ if successor > block.start or successor not in by_address:
510
+ continue
511
+ forwards = [
512
+ value for value in block.successors if value > block.start and value in by_address
513
+ ]
514
+ candidates.setdefault(successor, []).append(min(forwards) if forwards else -1)
515
+ exits: dict[int, int | None] = {}
516
+ for header, values in candidates.items():
517
+ known = [value for value in values if value >= 0]
518
+ exits[header] = min(known) if known else None
519
+ return exits
520
+
521
+
522
+ def _require_int(name: str, value: int, maximum: int) -> int:
523
+ """Validate one budget argument against its hard limits."""
524
+ if value < 1:
525
+ raise AnalysisError(f"{name} must be >= 1")
526
+ if value > maximum:
527
+ raise AnalysisError(f"{name} too large (limit {maximum})")
528
+ return value
529
+
530
+
531
+ class _Renderer:
532
+ """Shapes one function's blocks into pseudo-C lines.
533
+
534
+ The renderer walks the blocks of a
535
+ :class:`~codevanta.analysis.cfg.FunctionCFG` in ascending address order and
536
+ keeps a stack of open constructs. Blocks are never re-ordered, so every line
537
+ stays in instruction order; a construct closes at the address the branch
538
+ structure predicts, and every edge that cannot be nested inside the current
539
+ region becomes a ``goto``/``LABEL_<addr>`` pair.
540
+ """
541
+
542
+ def __init__(
543
+ self,
544
+ address: int,
545
+ blocks: list[BasicBlock],
546
+ argument_names: dict[str, str],
547
+ pointer_width: int,
548
+ names: dict[int, str],
549
+ imports: dict[int, str],
550
+ ) -> None:
551
+ self.address = address
552
+ self.blocks = blocks
553
+ self.by_address = {block.start: block for block in blocks}
554
+ self.argument_names = argument_names
555
+ self.pointer_width = pointer_width
556
+ self.names = names
557
+ self.imports = imports
558
+ self.preds: dict[int, set[int]] = {block.start: set() for block in blocks}
559
+ #: Edges whose control transfer is already expressed by the structure.
560
+ self.consumed: dict[int, set[int]] = {block.start: set() for block in blocks}
561
+ for block in blocks:
562
+ for successor in block.successors:
563
+ if successor not in self.by_address:
564
+ continue
565
+ self.preds[successor].add(block.start)
566
+ if successor == block.end:
567
+ # Falling through to the next block needs no label.
568
+ self.consumed[successor].add(block.start)
569
+ self.loops = _loop_exits(blocks, self.by_address)
570
+ self.lines: list[str] = []
571
+ self.calls: list[CallSite] = []
572
+ self.stack: list[_Frame] = []
573
+ self.indent = 0
574
+ self.opened_loops: set[int] = set()
575
+ self.consumed_jumps: set[int] = set()
576
+
577
+ # -- public ---------------------------------------------------------
578
+
579
+ def render(self) -> tuple[tuple[str, ...], tuple[CallSite, ...]]:
580
+ """Render every block and return the body lines plus the call sites."""
581
+ for block in self.blocks:
582
+ self._close_frames(block.start)
583
+ opened = self._open_loop(block.start)
584
+ if not opened and self._needs_label(block.start):
585
+ self._emit(f"LABEL_{block.start:x}:")
586
+ self._render_block(block)
587
+ self._close_all()
588
+ return tuple(self.lines), tuple(self.calls)
589
+
590
+ # -- structure bookkeeping ------------------------------------------
591
+
592
+ def _emit(self, text: str) -> None:
593
+ """Append one already-composed line with the current indentation."""
594
+ self.lines.append(f"{_INDENT * self.indent}{text}")
595
+
596
+ def _line(self, statement: str | None, instruction: dict[str, Any]) -> None:
597
+ """Append one source line with the raw instruction as a trailing comment."""
598
+ text = _instruction_text(instruction)
599
+ if statement is None:
600
+ self._emit(f"/* {text} */")
601
+ else:
602
+ self._emit(f"{statement} /* {text} */")
603
+
604
+ def _consume(self, target: int, source: int) -> None:
605
+ """Record that the edge ``source -> target`` is expressed by structure."""
606
+ self.consumed.setdefault(target, set()).add(source)
607
+
608
+ def _needs_label(self, start: int) -> bool:
609
+ """Whether a block needs ``LABEL_<addr>:`` because something jumps to it."""
610
+ if start == self.address or start in self.opened_loops:
611
+ return False
612
+ return bool(self.preds.get(start, set()) - self.consumed.get(start, set()))
613
+
614
+ def _close_frames(self, address: int) -> None:
615
+ """Close every construct that ends at or before ``address``."""
616
+ while self.stack:
617
+ frame = self.stack[-1]
618
+ if frame.stop is None or frame.stop > address:
619
+ return
620
+ self.stack.pop()
621
+ self.indent -= 1
622
+ if frame.kind == "if" and frame.merge is not None and frame.merge > address:
623
+ self._emit("} else {")
624
+ self.indent += 1
625
+ self.stack.append(_Frame("else", frame.merge, frame.block))
626
+ else:
627
+ self._emit("}")
628
+
629
+ def _close_all(self) -> None:
630
+ """Close every construct still open at the end of the function."""
631
+ while self.stack:
632
+ self.stack.pop()
633
+ self.indent -= 1
634
+ self._emit("}")
635
+
636
+ def _open_loop(self, start: int) -> bool:
637
+ """Open ``while (1) {`` when ``start`` is the target of a back edge."""
638
+ if start not in self.loops or start in self.opened_loops:
639
+ return False
640
+ self.opened_loops.add(start)
641
+ self._emit("while (1) {")
642
+ self.indent += 1
643
+ self.stack.append(_Frame("while", self.loops[start], start))
644
+ for block in self.blocks:
645
+ if start in block.successors and start <= block.start:
646
+ self._consume(start, block.start)
647
+ return True
648
+
649
+ def _limit(self) -> int | None:
650
+ """Return the address of the innermost enclosing region's end, if any."""
651
+ for frame in reversed(self.stack):
652
+ if frame.stop is not None:
653
+ return frame.stop
654
+ return None
655
+
656
+ def _loop_stop(self) -> int | None:
657
+ """Return the exit address of the innermost open loop, if any."""
658
+ for frame in reversed(self.stack):
659
+ if frame.kind == "while":
660
+ return frame.stop
661
+ return None
662
+
663
+ # -- blocks ---------------------------------------------------------
664
+
665
+ def _render_block(self, block: BasicBlock) -> None:
666
+ """Render one block's body and its terminator."""
667
+ instructions = block.instructions
668
+ if not instructions:
669
+ return
670
+ body = instructions[:-1] if block.terminator else instructions
671
+ compare: dict[str, Any] | None = None
672
+ for instruction in body:
673
+ if (
674
+ _mnemonic(instruction) in _COMPARE_MNEMONICS
675
+ and len(_split_operands(_operands(instruction))) == 2
676
+ ):
677
+ compare = instruction
678
+ self._line(self._statement(instruction), instruction)
679
+ if not block.terminator:
680
+ return
681
+ last = instructions[-1]
682
+ mnemonic = _mnemonic(last)
683
+ if mnemonic in _RET_MNEMONICS:
684
+ self._line("return;", last)
685
+ elif mnemonic in _UNCONDITIONAL_JUMPS:
686
+ self._jump(block, last)
687
+ else:
688
+ self._branch(block, last, compare)
689
+
690
+ def _statement(self, instruction: dict[str, Any]) -> str | None:
691
+ """Return the C statement for an instruction, or ``None`` when it has none."""
692
+ mnemonic = _mnemonic(instruction)
693
+ operands = _split_operands(_operands(instruction))
694
+ if mnemonic in _CALL_MNEMONICS:
695
+ return self._call(instruction)
696
+ if mnemonic in _MOV_MNEMONICS and len(operands) == 2:
697
+ target = self._operand(operands[0], instruction)
698
+ source = self._operand(operands[1], instruction)
699
+ return f"{target} = {source};"
700
+ if mnemonic == "lea" and len(operands) == 2:
701
+ target = self._operand(operands[0], instruction)
702
+ return f"{target} = {self._address_of(operands[1], instruction)};"
703
+ if mnemonic == "imul" and len(operands) == 3:
704
+ target = self._operand(operands[0], instruction)
705
+ left = self._operand(operands[1], instruction)
706
+ right = self._operand(operands[2], instruction)
707
+ return f"{target} = {left} * {right};"
708
+ if mnemonic in _COMPOUND_ASSIGN and len(operands) == 2:
709
+ target = self._operand(operands[0], instruction)
710
+ source = self._operand(operands[1], instruction)
711
+ return f"{target} {_COMPOUND_ASSIGN[mnemonic]} {source};"
712
+ if mnemonic in _UNARY_ASSIGN and len(operands) == 1:
713
+ target = self._operand(operands[0], instruction)
714
+ return f"{target} = {_UNARY_ASSIGN[mnemonic]}{target};"
715
+ if mnemonic in _STEP_ASSIGN and len(operands) == 1:
716
+ target = self._operand(operands[0], instruction)
717
+ return f"{target}{_STEP_ASSIGN[mnemonic]};"
718
+ return None
719
+
720
+ def _call(self, instruction: dict[str, Any]) -> str:
721
+ """Render a call and record it as a :class:`CallSite`."""
722
+ address = _address(instruction)
723
+ target = _direct_target(_operands(instruction))
724
+ if target is None:
725
+ self.calls.append(CallSite(address, None, None, "call"))
726
+ return "(*ptr)();"
727
+ imported = self.imports.get(target)
728
+ if imported is not None:
729
+ self.calls.append(CallSite(address, target, imported, "call"))
730
+ return f"{imported}();"
731
+ self.calls.append(CallSite(address, target, self.names.get(target), "call"))
732
+ return f"call_{target:x}();"
733
+
734
+ def _jump(self, block: BasicBlock, instruction: dict[str, Any]) -> None:
735
+ """Render an unconditional jump as ``continue``, ``goto`` or a tail call."""
736
+ address = _address(instruction)
737
+ target = _direct_target(_operands(instruction))
738
+ if target is None:
739
+ self.calls.append(CallSite(address, None, None, "unknown"))
740
+ self._line(None, instruction)
741
+ return
742
+ if block.start in self.consumed_jumps:
743
+ # The enclosing if/else structure already expresses this transfer.
744
+ self._line(None, instruction)
745
+ return
746
+ if target <= block.start and target in self.opened_loops:
747
+ self._consume(target, block.start)
748
+ self._line("continue;", instruction)
749
+ return
750
+ if target in self.by_address:
751
+ self._line(f"goto LABEL_{target:x};", instruction)
752
+ return
753
+ self.calls.append(CallSite(address, target, self.imports.get(target), "jump"))
754
+ self._line(f"goto 0x{target:x};", instruction)
755
+
756
+ def _branch(
757
+ self,
758
+ block: BasicBlock,
759
+ instruction: dict[str, Any],
760
+ compare: dict[str, Any] | None,
761
+ ) -> None:
762
+ """Render a conditional branch as a loop exit, an ``if`` or a ``goto``."""
763
+ mnemonic = _mnemonic(instruction)
764
+ target = _direct_target(_operands(instruction))
765
+ if target is None:
766
+ self._line(None, instruction)
767
+ return
768
+ loop_stop = self._loop_stop()
769
+ if loop_stop is not None and target == loop_stop:
770
+ self._consume(target, block.start)
771
+ self._break(self._condition(mnemonic, compare, invert=False), instruction)
772
+ return
773
+ if target <= block.start and target in self.opened_loops:
774
+ self._consume(target, block.start)
775
+ self._break(self._condition(mnemonic, compare, invert=True), instruction)
776
+ return
777
+ if self._can_structure(block, target):
778
+ self._structure_if(block, instruction, compare, target)
779
+ return
780
+ condition = self._condition(mnemonic, compare, invert=False)
781
+ self._line(f"if ({condition}) {{", instruction)
782
+ self.indent += 1
783
+ if target in self.by_address:
784
+ self._emit(f"goto LABEL_{target:x};")
785
+ else:
786
+ self._emit(f"goto 0x{target:x};")
787
+ self.indent -= 1
788
+ self._emit("}")
789
+
790
+ def _break(self, condition: str, instruction: dict[str, Any]) -> None:
791
+ """Emit one loop-exit test followed by ``break;``."""
792
+ self._line(f"if ({condition}) {{", instruction)
793
+ self.indent += 1
794
+ self._emit("break;")
795
+ self.indent -= 1
796
+ self._emit("}")
797
+
798
+ def _can_structure(self, block: BasicBlock, target: int) -> bool:
799
+ """Whether a forward branch fits inside the current region as an ``if``."""
800
+ if target <= block.start:
801
+ return False
802
+ if target not in self.by_address or block.end not in self.by_address:
803
+ return False
804
+ limit = self._limit()
805
+ return limit is None or target <= limit
806
+
807
+ def _structure_if(
808
+ self,
809
+ block: BasicBlock,
810
+ instruction: dict[str, Any],
811
+ compare: dict[str, Any] | None,
812
+ target: int,
813
+ ) -> None:
814
+ """Open ``if (<inverted condition>) {`` for a forward branch.
815
+
816
+ The condition is inverted because the *fallthrough* path is rendered as
817
+ the then-branch: instruction order then equals source order and the
818
+ branch target stays the else/merge address.
819
+ """
820
+ condition = self._condition(_mnemonic(instruction), compare, invert=True)
821
+ fallthrough = self.by_address[block.end]
822
+ merge = self._merge_address(block, fallthrough, target)
823
+ self._line(f"if ({condition}) {{", instruction)
824
+ self.indent += 1
825
+ self.stack.append(_Frame("if", target, block.start, merge))
826
+ self._consume(target, block.start)
827
+ if merge is not None:
828
+ self._consume(merge, fallthrough.start)
829
+ self.consumed_jumps.add(fallthrough.start)
830
+
831
+ def _merge_address(self, block: BasicBlock, fallthrough: BasicBlock, target: int) -> int | None:
832
+ """Return the address an if/else pair merges at, or ``None`` if it cannot.
833
+
834
+ ``} else {`` is only honest when the then-branch is a single block that
835
+ jumps over a region only this branch reaches, so the two halves are
836
+ mutually exclusive.
837
+ """
838
+ if self.preds.get(target, set()) != {block.start}:
839
+ return None
840
+ if not fallthrough.instructions:
841
+ return None
842
+ last = fallthrough.instructions[-1]
843
+ if _mnemonic(last) not in _UNCONDITIONAL_JUMPS:
844
+ return None
845
+ merge = _direct_target(_operands(last))
846
+ if merge is None or merge <= target or merge not in self.by_address:
847
+ return None
848
+ return merge
849
+
850
+ # -- operands -------------------------------------------------------
851
+
852
+ def _register(self, name: str) -> str:
853
+ """Name a register from the ABI, falling back to its architectural name."""
854
+ lowered = name.lower()
855
+ return self.argument_names.get(lowered, lowered)
856
+
857
+ def _operand(self, operand: str, instruction: dict[str, Any]) -> str:
858
+ """Render one operand as a pseudo-C expression."""
859
+ text = operand.strip()
860
+ memory = _parse_memory(text)
861
+ if memory is not None:
862
+ expression = self._address_of(text, instruction)
863
+ if memory.base in _STACK_REGISTERS and memory.index is None:
864
+ # A frame slot already *is* the value; only real pointers dereference.
865
+ return expression
866
+ width = self.pointer_width * 8
867
+ return f"*(u{width}*)({expression})"
868
+ lowered = text.lower()
869
+ if not lowered:
870
+ return ""
871
+ if not _SIMPLE_OPERAND.match(lowered):
872
+ # An operand shape the renderer does not model (a segment override, an
873
+ # unsupported expression): keep the raw text instead of guessing.
874
+ return text
875
+ return self._register(lowered)
876
+
877
+ def _address_of(self, operand: str, instruction: dict[str, Any]) -> str:
878
+ """Render a bracketed operand as a plain address expression (no deref)."""
879
+ memory = _parse_memory(operand)
880
+ if memory is None:
881
+ return self._operand(operand, instruction)
882
+ if memory.base in _PC_REGISTERS:
883
+ return f"0x{_address(instruction) + _size(instruction) + memory.displacement:x}"
884
+ if memory.absolute is not None:
885
+ return f"0x{memory.absolute:x}"
886
+ if memory.base is None and memory.index is None: # pragma: no cover - defensive
887
+ return f"0x{memory.displacement:x}"
888
+ if memory.base in _STACK_REGISTERS and memory.index is None:
889
+ return _slot(memory.displacement)
890
+ parts: list[str] = []
891
+ if memory.base is not None:
892
+ parts.append(self._register(memory.base))
893
+ if memory.index is not None:
894
+ index = self._register(memory.index)
895
+ parts.append(index if memory.scale == 1 else f"{index} * {memory.scale}")
896
+ expression = " + ".join(parts)
897
+ if memory.displacement > 0:
898
+ expression = f"{expression} + 0x{memory.displacement:x}"
899
+ elif memory.displacement < 0:
900
+ expression = f"{expression} - 0x{-memory.displacement:x}"
901
+ return expression
902
+
903
+ def _condition(
904
+ self,
905
+ mnemonic: str,
906
+ compare: dict[str, Any] | None,
907
+ *,
908
+ invert: bool,
909
+ ) -> str:
910
+ """Build a condition from a branch mnemonic and the comparison feeding it."""
911
+ name = mnemonic.lower()
912
+ if invert:
913
+ inverse = _INVERSES.get(name)
914
+ if inverse is None:
915
+ return f"!flag_from_{name}"
916
+ name = inverse
917
+ operators = _COMPARISONS.get(name)
918
+ if operators is not None and compare is not None:
919
+ text = self._comparison(operators, compare)
920
+ if text is not None:
921
+ return text
922
+ return f"flag_from_{name}"
923
+
924
+ def _comparison(self, operators: tuple[str, bool], compare: dict[str, Any]) -> str | None:
925
+ """Render ``cmp``/``test`` operands as a comparison, or ``None``."""
926
+ operands = _split_operands(_operands(compare))
927
+ if len(operands) != 2:
928
+ return None
929
+ operator, unsigned = operators
930
+ left = self._operand(operands[0], compare)
931
+ right = self._operand(operands[1], compare)
932
+ if _mnemonic(compare) == "test":
933
+ if operator not in ("==", "!="):
934
+ return None
935
+ return f"({left} & {right}) {operator} 0"
936
+ cast = "(unsigned) " if unsigned else ""
937
+ return f"{cast}{left} {operator} {right}"
938
+
939
+
940
+ def _header(
941
+ evidence: Evidence,
942
+ display: str,
943
+ cfg: FunctionCFG,
944
+ truncated: bool,
945
+ argument_types: tuple[ArgumentType, ...] = (),
946
+ return_type: ReturnType | None = None,
947
+ ) -> list[str]:
948
+ """Build the header comment block (ASCII only)."""
949
+ warnings = list(cfg.warnings)
950
+ if truncated and not warnings:
951
+ warnings.append(
952
+ "rendering stopped at the instruction/block budget; the output is incomplete"
953
+ )
954
+ lines = [
955
+ "/*",
956
+ " * CODEVANTA pseudo-C decompilation (heuristic, phase 3)",
957
+ f" * address: 0x{cfg.function_address:x}",
958
+ f" * symbol: {display}",
959
+ f" * confidence: {evidence.confidence.value}",
960
+ f" * evidence: {evidence.source} / {evidence.method} - {evidence.detail}",
961
+ f" * blocks: {len(cfg.blocks)} instructions: {cfg.instruction_count}"
962
+ f" truncated: {str(truncated).lower()}",
963
+ " * caveat: control flow, argument count and types are inferred from mnemonics;"
964
+ " nothing is executed",
965
+ ]
966
+ for argument in argument_types:
967
+ lines.append(
968
+ f" * argument {argument.name} ({argument.register}): {argument.c_type} "
969
+ f"[{argument.confidence.value}] - {argument.evidence_note}"
970
+ )
971
+ if return_type is not None:
972
+ lines.append(
973
+ f" * return: {return_type.c_type} [{return_type.confidence.value}] - "
974
+ f"{return_type.evidence_note}"
975
+ )
976
+ for warning in warnings[:6]:
977
+ lines.append(f" * warning: {warning}")
978
+ if len(warnings) > 6:
979
+ lines.append(f" * warning: (+{len(warnings) - 6} more)")
980
+ lines.append(" */")
981
+ return lines
982
+
983
+
984
+ def decompile_function(
985
+ path: str | Path,
986
+ info: BinaryInfo,
987
+ address: int,
988
+ max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
989
+ max_blocks: int = DEFAULT_MAX_BLOCKS,
990
+ ) -> PseudoFunction:
991
+ """Render one function as pseudo-C.
992
+
993
+ Args:
994
+ path: File the bytes are read from (read-only; never executed).
995
+ info: Loaded binary metadata used for address mapping, decoding, symbol
996
+ names and imports.
997
+ address: Virtual address of the function; must be inside an executable
998
+ section.
999
+ max_instructions: Instruction budget for the recursive traversal
1000
+ (``1`` .. :data:`MAX_MAX_INSTRUCTIONS`).
1001
+ max_blocks: Block budget for the recursive traversal
1002
+ (``1`` .. :data:`MAX_MAX_BLOCKS`).
1003
+
1004
+ Returns:
1005
+ The rendered :class:`PseudoFunction`.
1006
+
1007
+ Raises:
1008
+ AnalysisError: When ``address`` is not executable code, when a budget is
1009
+ out of range, or when the region decodes to no instruction at all.
1010
+ """
1011
+ instruction_budget = _require_int("max_instructions", max_instructions, MAX_MAX_INSTRUCTIONS)
1012
+ block_budget = _require_int("max_blocks", max_blocks, MAX_MAX_BLOCKS)
1013
+ cfg = build_recursive_cfg(
1014
+ path,
1015
+ info,
1016
+ address,
1017
+ max_instructions=instruction_budget,
1018
+ max_blocks=block_budget,
1019
+ )
1020
+ if cfg.instruction_count == 0:
1021
+ raise AnalysisError(f"no decodable instruction at 0x{address:x}")
1022
+
1023
+ pointer_width = 8 if info.bitness >= 64 else 4
1024
+ argument_names = _argument_names(info.bitness)
1025
+ names: dict[int, str] = {}
1026
+ for candidate in discover_functions(info):
1027
+ if candidate.name:
1028
+ names[candidate.address] = candidate.name
1029
+ imports: dict[int, str] = {}
1030
+ for item in info.imports:
1031
+ if not item.address:
1032
+ continue
1033
+ printable = _identifier(item.name, "")
1034
+ if printable:
1035
+ imports[int(item.address)] = printable
1036
+
1037
+ blocks = list(cfg.blocks)
1038
+ renderer = _Renderer(
1039
+ address=address,
1040
+ blocks=blocks,
1041
+ argument_names=argument_names,
1042
+ pointer_width=pointer_width,
1043
+ names=names,
1044
+ imports=imports,
1045
+ )
1046
+ lines, call_sites = renderer.render()
1047
+
1048
+ display = _identifier(cfg.function_name, f"sub_{address:x}")
1049
+ instructions = [
1050
+ instruction for block in blocks for instruction in block.instructions
1051
+ ]
1052
+ argument_types = infer_argument_types(instructions, pointer_width)
1053
+ return_type = infer_return_type(instructions, pointer_width)
1054
+ parameters = ", ".join(
1055
+ f"{argument.c_type} {argument.name}" for argument in argument_types
1056
+ )
1057
+ if not parameters:
1058
+ parameters = "void"
1059
+ truncated = cfg.truncated
1060
+ evidence = Evidence(
1061
+ source=_SOURCE,
1062
+ method=_METHOD,
1063
+ confidence=Confidence.LOW,
1064
+ address=address,
1065
+ detail=(
1066
+ f"pseudo-C rendered from {cfg.instruction_count} instruction(s) in "
1067
+ f"{len(blocks)} block(s) of a recursive traversal; argument count and "
1068
+ "argument/return types from register use, control flow from branch "
1069
+ "targets, widths from the pointer size; mnemonics only, nothing is "
1070
+ "executed"
1071
+ ),
1072
+ extra={
1073
+ "argument_types": [argument.to_dict() for argument in argument_types],
1074
+ "return_type": return_type.to_dict(),
1075
+ },
1076
+ )
1077
+
1078
+ body_lines = _header(evidence, display, cfg, truncated, argument_types, return_type)
1079
+ body_lines.append(f"{return_type.c_type} {display}({parameters})")
1080
+ body_lines.append("{")
1081
+ for line in lines:
1082
+ body_lines.append(f"{_INDENT}{line}" if line else "")
1083
+ body_lines.append("}")
1084
+ return PseudoFunction(
1085
+ address=address,
1086
+ name=cfg.function_name,
1087
+ lines=lines,
1088
+ body="\n".join(body_lines),
1089
+ call_sites=call_sites,
1090
+ block_count=len(blocks),
1091
+ instruction_count=cfg.instruction_count,
1092
+ truncated=truncated,
1093
+ evidence=evidence,
1094
+ )