mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-18 03:00:42 +02:00
608 lines
21 KiB
Python
608 lines
21 KiB
Python
"""Classify strict COFF codegen differences without granting match credit.
|
|
|
|
The classifier is deliberately fail-closed. It maps a small set of measured
|
|
MSVC 7.0 code-generation signatures to the production recipes documented by
|
|
the XDK 3911 codegen atlas. Anything that does not satisfy a complete rule is
|
|
``UNKNOWN``. A result is diagnostic evidence only: this tool never changes a
|
|
matching ledger or decides that a function is exact.
|
|
"""
|
|
|
|
import argparse
|
|
import itertools
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from .coff_compare import (
|
|
CoffError,
|
|
load,
|
|
relocation_infos_equal,
|
|
section_info,
|
|
section_infos_equal,
|
|
)
|
|
from . import coff_compare as _coff_compare
|
|
except ImportError:
|
|
# Script invocation: python tools/codegen_blocker_classifier.py
|
|
import os
|
|
import sys
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from coff_compare import (
|
|
CoffError,
|
|
load,
|
|
relocation_infos_equal,
|
|
section_info,
|
|
section_infos_equal,
|
|
)
|
|
import coff_compare as _coff_compare
|
|
|
|
|
|
VERSION = 1
|
|
GENERAL_REGISTERS = ("eax", "ebx", "ecx", "edx", "esi", "edi")
|
|
REGISTER_ALIASES = {
|
|
"eax": ("eax", "ax", "al", "ah"),
|
|
"ebx": ("ebx", "bx", "bl", "bh"),
|
|
"ecx": ("ecx", "cx", "cl", "ch"),
|
|
"edx": ("edx", "dx", "dl", "dh"),
|
|
"esi": ("esi", "si"),
|
|
"edi": ("edi", "di"),
|
|
"ebp": ("ebp", "bp"),
|
|
"esp": ("esp", "sp"),
|
|
}
|
|
ALIAS_TO_REGISTER = {
|
|
alias: register
|
|
for register, aliases in REGISTER_ALIASES.items()
|
|
for alias in aliases
|
|
}
|
|
INSTRUCTION_RE = re.compile(
|
|
r"^\s*([0-9a-fA-F]+):\s+((?:[0-9a-fA-F]{2}\s+)+)"
|
|
r"([A-Za-z][A-Za-z0-9_.]*)\s*(.*?)\s*$")
|
|
REGISTER_RE = re.compile(
|
|
r"%(eax|ax|al|ah|ebx|bx|bl|bh|ecx|cx|cl|ch|edx|dx|dl|dh|"
|
|
r"esi|si|edi|di|ebp|bp|esp|sp)\b")
|
|
|
|
|
|
class ClassifierError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def parse_disassembly(text):
|
|
"""Parse llvm-objdump AT&T instruction lines into stable evidence."""
|
|
instructions = []
|
|
for line in text.splitlines():
|
|
match = INSTRUCTION_RE.match(line)
|
|
if not match:
|
|
continue
|
|
instructions.append({
|
|
"address": int(match.group(1), 16),
|
|
"bytes": "".join(match.group(2).split()).lower(),
|
|
"mnemonic": match.group(3).lower(),
|
|
"operands": _normalize_operands(match.group(4)),
|
|
})
|
|
return instructions
|
|
|
|
|
|
def _normalize_operands(operands):
|
|
return re.sub(r"\s+", " ", operands.strip().lower())
|
|
|
|
|
|
def _instruction_key(instruction):
|
|
return instruction["mnemonic"], _normalize_operands(instruction["operands"])
|
|
|
|
|
|
def _split_operands(operands):
|
|
"""Split AT&T operands without treating SIB commas as separators."""
|
|
parts = []
|
|
start = 0
|
|
depth = 0
|
|
for index, character in enumerate(operands):
|
|
if character == "(":
|
|
depth += 1
|
|
elif character == ")":
|
|
depth = max(0, depth - 1)
|
|
elif character == "," and depth == 0:
|
|
parts.append(operands[start:index].strip())
|
|
start = index + 1
|
|
parts.append(operands[start:].strip())
|
|
return parts
|
|
|
|
|
|
def _relocation_identities_equal(left, right):
|
|
"""Compare ordered relocation semantics while allowing address drift."""
|
|
if len(left) != len(right):
|
|
return False
|
|
left_without_addresses = [dict(relocation, address=0) for relocation in left]
|
|
right_without_addresses = [dict(relocation, address=0) for relocation in right]
|
|
return relocation_infos_equal(left_without_addresses, right_without_addresses)
|
|
|
|
|
|
def _metadata(target, candidate):
|
|
target_relocations = target.get("relocations", [])
|
|
candidate_relocations = candidate.get("relocations", [])
|
|
return {
|
|
"size_equal": target.get("size") == candidate.get("size"),
|
|
"relocation_count_equal": (
|
|
target.get("relocation_count") == candidate.get("relocation_count")),
|
|
"relocation_identities_equal": _relocation_identities_equal(
|
|
target_relocations, candidate_relocations),
|
|
"relocations_exact": relocation_infos_equal(
|
|
target_relocations, candidate_relocations),
|
|
}
|
|
|
|
|
|
def _result(kind, blocker_class, recipe, confidence, evidence, metadata,
|
|
stop_rule, preconditions=None):
|
|
return {
|
|
"version": VERSION,
|
|
"kind": kind,
|
|
"class": blocker_class,
|
|
"recipe": recipe,
|
|
"confidence": confidence,
|
|
"evidence": evidence,
|
|
"strict_metadata": metadata,
|
|
"preconditions": preconditions or [],
|
|
"stop_rule": stop_rule,
|
|
"grants_match_credit": False,
|
|
}
|
|
|
|
|
|
def classify(target, candidate):
|
|
"""Return one conservative codegen classification for two evidence dicts."""
|
|
metadata = _metadata(target, candidate)
|
|
target_instructions = target.get("instructions", [])
|
|
candidate_instructions = candidate.get("instructions", [])
|
|
if not target_instructions or not candidate_instructions:
|
|
return _unknown(metadata, "both disassemblies are required")
|
|
|
|
if target.get("normalized_sha256") == candidate.get("normalized_sha256") \
|
|
and metadata["size_equal"] and metadata["relocations_exact"]:
|
|
return _result(
|
|
"REFUSE", "no-codegen-blocker", None, "PROVEN",
|
|
["strict measurements are already equal"], metadata,
|
|
"Use the ordinary strict comparator; this classifier does not grant credit.")
|
|
|
|
classification = _classify_return_shape(
|
|
target_instructions, candidate_instructions, metadata)
|
|
if classification:
|
|
return classification
|
|
classification = _classify_private_abi(
|
|
target_instructions, candidate_instructions, metadata)
|
|
if classification:
|
|
return classification
|
|
classification = _classify_alias_shape(
|
|
target_instructions, candidate_instructions, metadata)
|
|
if classification:
|
|
return classification
|
|
classification = _classify_x87_liveness(
|
|
target_instructions, candidate_instructions, metadata)
|
|
if classification:
|
|
return classification
|
|
classification = _classify_register_permutation(
|
|
target_instructions, candidate_instructions, metadata)
|
|
if classification:
|
|
return classification
|
|
classification = _classify_scheduling_tie(
|
|
target_instructions, candidate_instructions, metadata)
|
|
if classification:
|
|
return classification
|
|
return _unknown(metadata, "no proven recipe or strict STOP signature matched")
|
|
|
|
|
|
def _unknown(metadata, reason):
|
|
return _result(
|
|
"UNKNOWN", "unclassified", None, "NONE", [reason], metadata,
|
|
"Gather stronger disassembly/source evidence; do not infer a recipe or park class.")
|
|
|
|
|
|
def _ret_count(instructions):
|
|
return sum(instruction["mnemonic"].startswith("ret") for instruction in instructions)
|
|
|
|
|
|
def _classify_return_shape(target, candidate, metadata):
|
|
target_count = _ret_count(target)
|
|
candidate_count = _ret_count(candidate)
|
|
if target_count == candidate_count or not metadata["relocation_identities_equal"]:
|
|
return None
|
|
return _result(
|
|
"RECIPE", "control-flow-return-shape", "R5", "PROVEN", [
|
|
f"target ret count={target_count}; candidate ret count={candidate_count}",
|
|
"ordered relocation identities agree",
|
|
], metadata,
|
|
"Stop after the return count matches; any residual needs a fresh classification.", [
|
|
"Preserve the target's early-return versus single-exit structure.",
|
|
])
|
|
|
|
|
|
def _has_frame(instructions):
|
|
keys = [_instruction_key(instruction) for instruction in instructions[:8]]
|
|
push_ebp = any(mnemonic.startswith("push") and operands == "%ebp"
|
|
for mnemonic, operands in keys)
|
|
mov_frame = any(mnemonic.startswith("mov") and operands == "%esp, %ebp"
|
|
for mnemonic, operands in keys)
|
|
sub_stack = any(mnemonic.startswith("sub") and operands.endswith(", %esp")
|
|
for mnemonic, operands in keys)
|
|
return push_ebp and mov_frame and sub_stack
|
|
|
|
|
|
def _classify_private_abi(target, candidate, metadata):
|
|
if _has_frame(target) or not _has_frame(candidate):
|
|
return None
|
|
target_head = " ".join(instruction["operands"] for instruction in target[:12])
|
|
candidate_head = " ".join(instruction["operands"] for instruction in candidate[:12])
|
|
if not ("%ecx" in target_head or "%edx" in target_head):
|
|
return None
|
|
if not re.search(r"0x(?:8|c)\(%ebp\)", candidate_head):
|
|
return None
|
|
if not metadata["relocation_identities_equal"]:
|
|
return None
|
|
return _result(
|
|
"RECIPE", "private-abi-frame-convention", "R1", "STRONG", [
|
|
"target is frameless and consumes ECX/EDX near entry",
|
|
"candidate builds an EBP frame and reads stack arguments",
|
|
"ordered relocation identities agree",
|
|
], metadata,
|
|
"Stop if the helper still requires a stack frame after shortening live ranges.", [
|
|
"Confirm the source helper is static.",
|
|
"Confirm its address is never taken and it has a same-TU caller.",
|
|
])
|
|
|
|
|
|
def _has_sib_scale_eight(instructions):
|
|
for instruction in instructions:
|
|
if not instruction["mnemonic"].startswith("mov"):
|
|
continue
|
|
operands = _split_operands(instruction["operands"])
|
|
if operands and re.search(r"\([^)]*,[^)]*,8\)", operands[-1]):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _has_scaled_temporary(instructions):
|
|
return any(
|
|
(instruction["mnemonic"].startswith("lea")
|
|
and re.search(r"\([^)]*,[^)]*,8\)", instruction["operands"]))
|
|
or (instruction["mnemonic"].startswith("shl")
|
|
and re.search(r"\$(?:0x)?3,", instruction["operands"]))
|
|
for instruction in instructions)
|
|
|
|
|
|
def _classify_alias_shape(target, candidate, metadata):
|
|
if not _has_sib_scale_eight(target) or not _has_scaled_temporary(candidate):
|
|
return None
|
|
return _result(
|
|
"RECIPE", "alias-global-ownership", "R3", "PROVEN", [
|
|
"target contains direct scale-8 SIB stores",
|
|
"candidate materializes a scale-8 address with LEA/SHL",
|
|
], metadata,
|
|
"Stop and classify as ownership-blocked if target relocations use one aggregate anchor.", [
|
|
"Audit target COFF ownership before splitting an aggregate global.",
|
|
"After any source change, require strict relocation semantics as well as code bytes.",
|
|
])
|
|
|
|
|
|
def _fst_kind(mnemonic):
|
|
if mnemonic.startswith("fstp"):
|
|
return "fstp", mnemonic[4:]
|
|
if mnemonic.startswith("fst"):
|
|
return "fst", mnemonic[3:]
|
|
return None
|
|
|
|
|
|
def _classify_x87_liveness(target, candidate, metadata):
|
|
if len(target) != len(candidate) or not metadata["relocation_identities_equal"]:
|
|
return None
|
|
differences = []
|
|
for index, (target_instruction, candidate_instruction) in enumerate(zip(target, candidate)):
|
|
if _instruction_key(target_instruction) == _instruction_key(candidate_instruction):
|
|
continue
|
|
target_kind = _fst_kind(target_instruction["mnemonic"])
|
|
candidate_kind = _fst_kind(candidate_instruction["mnemonic"])
|
|
if (not target_kind or not candidate_kind
|
|
or target_kind[0] == candidate_kind[0]
|
|
or target_kind[1] != candidate_kind[1]
|
|
or target_instruction["operands"] != candidate_instruction["operands"]):
|
|
return None
|
|
differences.append((index, target_kind[0], candidate_kind[0]))
|
|
if not differences:
|
|
return None
|
|
return _result(
|
|
"RECIPE", "x87-value-liveness", "R4", "PROVEN", [
|
|
"all instruction differences are FST/FSTP opcode pairs",
|
|
"ordered relocation identities agree",
|
|
*[f"instruction {index}: target {left}, candidate {right}"
|
|
for index, left, right in differences],
|
|
], metadata,
|
|
"Stop after x87 stack depth and subsequent instruction addresses realign.", [
|
|
"Target FST requires keeping the computed value live after the store.",
|
|
"Target FSTP requires letting the computed value die at the store.",
|
|
])
|
|
|
|
|
|
def _replace_registers(operands, mapping):
|
|
def replace(match):
|
|
alias = match.group(1)
|
|
register = ALIAS_TO_REGISTER[alias]
|
|
mapped = mapping.get(register, register)
|
|
aliases = REGISTER_ALIASES[mapped]
|
|
original_aliases = REGISTER_ALIASES[register]
|
|
try:
|
|
index = original_aliases.index(alias)
|
|
except ValueError:
|
|
return match.group(0)
|
|
if index >= len(aliases):
|
|
return match.group(0)
|
|
return "%" + aliases[index]
|
|
return REGISTER_RE.sub(replace, operands)
|
|
|
|
|
|
def _classify_register_permutation(target, candidate, metadata):
|
|
if (not metadata["size_equal"] or not metadata["relocations_exact"]
|
|
or len(target) != len(candidate)):
|
|
return None
|
|
if [item["mnemonic"] for item in target] != [item["mnemonic"] for item in candidate]:
|
|
return None
|
|
target_keys = [_instruction_key(instruction) for instruction in target]
|
|
candidate_keys = [_instruction_key(instruction) for instruction in candidate]
|
|
differing = [
|
|
index for index, pair in enumerate(zip(target_keys, candidate_keys))
|
|
if pair[0] != pair[1]
|
|
]
|
|
if not differing:
|
|
return None
|
|
# A register live range can begin and end inside a function. Permit one
|
|
# contiguous local window, but refuse disconnected differences because a
|
|
# single causal permutation is then unproven.
|
|
if differing != list(range(differing[0], differing[-1] + 1)):
|
|
return None
|
|
window = range(differing[0], differing[-1] + 1)
|
|
for permutation in itertools.permutations(GENERAL_REGISTERS):
|
|
mapping = dict(zip(GENERAL_REGISTERS, permutation))
|
|
if all(mapping[register] == register for register in GENERAL_REGISTERS):
|
|
continue
|
|
if all(
|
|
_replace_registers(candidate_instruction["operands"], mapping)
|
|
== target_instruction["operands"]
|
|
for target_instruction, candidate_instruction in
|
|
((target[index], candidate[index]) for index in window)
|
|
):
|
|
changed = [
|
|
f"{source}->{destination}"
|
|
for source, destination in mapping.items()
|
|
if source != destination
|
|
]
|
|
return _result(
|
|
"STOP", "pure-register-permutation", None, "PROVEN", [
|
|
"equal padded size and exact relocation semantics",
|
|
f"one contiguous instruction window {differing[0]}..{differing[-1]} "
|
|
"becomes identical under one bijective GPR mapping",
|
|
"mapping: " + ", ".join(changed),
|
|
], metadata,
|
|
"No measured legal-C control moved a pure register permutation; keep parked.")
|
|
return None
|
|
|
|
|
|
def _registers(instruction):
|
|
return {
|
|
ALIAS_TO_REGISTER[match.group(1)]
|
|
for match in REGISTER_RE.finditer(instruction["operands"])
|
|
}
|
|
|
|
|
|
def _memory_role(instruction):
|
|
operands = instruction["operands"]
|
|
if "(" not in operands:
|
|
return "none"
|
|
mnemonic = instruction["mnemonic"]
|
|
if mnemonic.startswith("lea"):
|
|
return "none"
|
|
parts = _split_operands(operands)
|
|
if mnemonic.startswith("mov") and parts and "(" in parts[-1]:
|
|
return "write"
|
|
if mnemonic.startswith("fst"):
|
|
return "write"
|
|
return "read"
|
|
|
|
|
|
def _flag_role(mnemonic):
|
|
if mnemonic.startswith(("j", "set", "cmov", "adc", "sbb")):
|
|
return "read"
|
|
if mnemonic.startswith((
|
|
"add", "sub", "cmp", "test", "and", "or", "xor", "inc", "dec",
|
|
"shl", "shr", "sar", "sal", "neg", "imul")):
|
|
return "write"
|
|
return "none"
|
|
|
|
|
|
def _independent(left, right):
|
|
if left["mnemonic"].startswith(("j", "call", "ret", "loop", "int", "push", "pop")):
|
|
return False
|
|
if right["mnemonic"].startswith(("j", "call", "ret", "loop", "int", "push", "pop")):
|
|
return False
|
|
if left["mnemonic"].startswith("f") and right["mnemonic"].startswith("f"):
|
|
return False
|
|
if _registers(left) & _registers(right):
|
|
return False
|
|
left_memory = _memory_role(left)
|
|
right_memory = _memory_role(right)
|
|
if "write" in (left_memory, right_memory) and "none" not in (left_memory, right_memory):
|
|
return False
|
|
left_flags = _flag_role(left["mnemonic"])
|
|
right_flags = _flag_role(right["mnemonic"])
|
|
if left_flags == "read" or right_flags == "read":
|
|
return False
|
|
if left_flags == right_flags == "write":
|
|
return False
|
|
return True
|
|
|
|
|
|
def _classify_scheduling_tie(target, candidate, metadata):
|
|
if (not metadata["size_equal"] or not metadata["relocations_exact"]
|
|
or len(target) != len(candidate)):
|
|
return None
|
|
target_keys = [_instruction_key(instruction) for instruction in target]
|
|
candidate_keys = [_instruction_key(instruction) for instruction in candidate]
|
|
differing = [
|
|
index for index, pair in enumerate(zip(target_keys, candidate_keys))
|
|
if pair[0] != pair[1]
|
|
]
|
|
if len(differing) != 2 or differing[1] != differing[0] + 1:
|
|
return None
|
|
first, second = differing
|
|
if not (target_keys[first] == candidate_keys[second]
|
|
and target_keys[second] == candidate_keys[first]):
|
|
return None
|
|
if not _independent(target[first], target[second]):
|
|
return None
|
|
return _result(
|
|
"STOP", "independent-instruction-scheduling", None, "PROVEN", [
|
|
"equal padded size and exact relocation semantics",
|
|
f"instructions {first} and {second} are one adjacent transposition",
|
|
"the pair has disjoint registers, no control transfer, and no memory/flag dependency",
|
|
], metadata,
|
|
"No measured legal-C control moved an independent scheduling tie; keep parked.")
|
|
|
|
|
|
_DECIMAL_IMMEDIATE_RE = re.compile(r"\$(-?)(\d+)\b")
|
|
_DECIMAL_DISPLACEMENT_RE = re.compile(r"(^|[\s,])(-?)(\d+)\(")
|
|
_PAREN_SPACE_RE = re.compile(r"\(([^)]*)\)")
|
|
|
|
|
|
def _llvm_style_operands(operands):
|
|
"""Rewrite capstone AT&T operands into llvm-objdump conventions.
|
|
|
|
The measured rule signatures were written against llvm-objdump output:
|
|
hex immediates and displacements (``$0x10``, ``0x8(%ebp)``) and no spaces
|
|
inside SIB expressions (``(%eax,%ebx,8)``). capstone prints decimal and
|
|
spaced forms. Only the spelling is changed; SIB scales stay decimal in
|
|
both producers.
|
|
"""
|
|
operands = _PAREN_SPACE_RE.sub(
|
|
lambda match: "(" + match.group(1).replace(", ", ",") + ")", operands)
|
|
operands = _DECIMAL_IMMEDIATE_RE.sub(
|
|
lambda match: "$" + match.group(1) + hex(int(match.group(2))), operands)
|
|
operands = _DECIMAL_DISPLACEMENT_RE.sub(
|
|
lambda match: match.group(1) + match.group(2)
|
|
+ hex(int(match.group(3))) + "(", operands)
|
|
return operands
|
|
|
|
|
|
def _capstone_instructions(obj, function_name):
|
|
"""Disassemble one function COMDAT with capstone, relocation slots masked.
|
|
|
|
Masking the four addend bytes at every relocation address before decoding
|
|
keeps csplit targets (which store linked addresses) and rebuilt candidates
|
|
(which store link-time addends) from producing spurious operand
|
|
differences. Both sides are masked identically, so equality remains
|
|
meaningful; the relocation identities themselves are still compared by the
|
|
strict metadata.
|
|
"""
|
|
try:
|
|
import capstone
|
|
except ImportError as error:
|
|
raise ClassifierError(
|
|
"neither llvm-objdump nor capstone is available") from error
|
|
fn = None
|
|
for item in obj["symbols"]:
|
|
if item["name"] == function_name and item["section"] > 0:
|
|
fn = item
|
|
break
|
|
if fn is None:
|
|
raise ClassifierError(
|
|
f"section-relative symbol {function_name!r} not found")
|
|
section = obj["sections"][fn["section"] - 1]
|
|
raw = bytearray(_coff_compare._section_bytes(obj, section))[:section["size"]]
|
|
for index in range(section["reloc_count"]):
|
|
offset = section["reloc"] + index * 10
|
|
address = int.from_bytes(obj["data"][offset:offset + 4], "little")
|
|
if address + 4 <= len(raw):
|
|
raw[address:address + 4] = b"\0\0\0\0"
|
|
engine = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
|
|
engine.syntax = capstone.CS_OPT_SYNTAX_ATT
|
|
engine.skipdata = True
|
|
instructions = []
|
|
for decoded in engine.disasm(bytes(raw), 0):
|
|
instructions.append({
|
|
"address": decoded.address,
|
|
"bytes": decoded.bytes.hex(),
|
|
"mnemonic": decoded.mnemonic.lower(),
|
|
"operands": _normalize_operands(
|
|
_llvm_style_operands(decoded.op_str)),
|
|
})
|
|
if not instructions:
|
|
raise ClassifierError(f"no instructions found for {function_name!r}")
|
|
return instructions
|
|
|
|
|
|
def _run_objdump(objdump, object_path, function_name):
|
|
command = [
|
|
str(objdump), "-d", "-r", "--symbolize-operands",
|
|
f"--disassemble-symbols={function_name}", str(object_path),
|
|
]
|
|
completed = subprocess.run(
|
|
command, capture_output=True, text=True, check=False)
|
|
if completed.returncode:
|
|
raise ClassifierError(completed.stderr.strip() or "llvm-objdump failed")
|
|
instructions = parse_disassembly(completed.stdout)
|
|
if not instructions:
|
|
raise ClassifierError(f"no instructions found for {function_name!r}")
|
|
return instructions
|
|
|
|
|
|
def evidence_from_object(object_path, function_name, objdump=None, backend="auto"):
|
|
"""Measure one COFF function and attach disassembly evidence.
|
|
|
|
``backend`` selects the disassembler: ``"objdump"`` requires llvm-objdump,
|
|
``"capstone"`` uses the in-process capstone port, and ``"auto"`` prefers
|
|
llvm-objdump when present and falls back to capstone. Both producers of a
|
|
comparison must use the same backend; mixing them is not meaningful.
|
|
"""
|
|
object_path = Path(object_path)
|
|
try:
|
|
obj = load(object_path)
|
|
measurement = section_info(obj, function_name)
|
|
except (CoffError, OSError) as error:
|
|
raise ClassifierError(str(error)) from error
|
|
if backend != "capstone":
|
|
objdump = objdump or shutil.which("llvm-objdump") or shutil.which("llvm-objdump.exe")
|
|
if backend == "objdump" and not objdump:
|
|
raise ClassifierError("llvm-objdump was not found; pass --objdump")
|
|
if backend != "capstone" and objdump:
|
|
measurement["instructions"] = _run_objdump(objdump, object_path, function_name)
|
|
else:
|
|
measurement["instructions"] = _capstone_instructions(obj, function_name)
|
|
return measurement
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("target")
|
|
parser.add_argument("candidate")
|
|
parser.add_argument("function")
|
|
parser.add_argument("--objdump")
|
|
parser.add_argument(
|
|
"--backend", choices=("auto", "objdump", "capstone"), default="auto")
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
target = evidence_from_object(
|
|
args.target, args.function, objdump=args.objdump,
|
|
backend=args.backend)
|
|
candidate = evidence_from_object(
|
|
args.candidate, args.function, objdump=args.objdump,
|
|
backend=args.backend)
|
|
result = classify(target, candidate)
|
|
except ClassifierError as error:
|
|
parser.error(str(error))
|
|
serialized = json.dumps(result, indent=2) + "\n"
|
|
if args.output:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(serialized, encoding="utf-8")
|
|
print(serialized, end="")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|