mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-20 09:00:28 +02:00
211 lines
6.4 KiB
Python
211 lines
6.4 KiB
Python
"""Classify nonexact functions by same-translation-unit caller context.
|
|
|
|
The tool is deliberately conservative. It never grants exactness and it does
|
|
not claim that adding callers will repair a helper. It answers the narrower
|
|
scheduling question: whether source-shape work is justified now, or whether
|
|
absent same-object callers should be reconstructed and measured first.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
from .coff_compare import (
|
|
CoffError,
|
|
IMAGE_REL_I386_DIR32,
|
|
IMAGE_REL_I386_REL32,
|
|
load,
|
|
relocation_infos_equal,
|
|
section_info,
|
|
)
|
|
|
|
|
|
VERDICTS = {
|
|
"ready-to-shape",
|
|
"blocked-on-callers",
|
|
"park-candidate",
|
|
"structural",
|
|
}
|
|
|
|
|
|
def classify_candidate(
|
|
base_present,
|
|
objdiff_percent,
|
|
relocation_identities_equal,
|
|
absent_callers,
|
|
size_equal,
|
|
relocation_count_equal,
|
|
):
|
|
"""Return a fail-closed scheduling verdict for one nonexact function."""
|
|
if not base_present:
|
|
return "structural"
|
|
if absent_callers and objdiff_percent >= 70.0:
|
|
return "blocked-on-callers"
|
|
if relocation_identities_equal and size_equal and not absent_callers:
|
|
return "park-candidate"
|
|
if not size_equal or not relocation_count_equal or objdiff_percent < 70.0:
|
|
return "structural"
|
|
return "ready-to-shape"
|
|
|
|
|
|
def _relocation_identities_equal(target_info, base_info):
|
|
return relocation_infos_equal(
|
|
target_info["relocations"], base_info["relocations"])
|
|
|
|
|
|
def analyze(project_root, report_path, config_path):
|
|
project_root = Path(project_root)
|
|
report = json.loads(Path(report_path).read_text(encoding="utf-8"))
|
|
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
config_units = {unit["name"]: unit for unit in config.get("units", [])}
|
|
results = []
|
|
errors = []
|
|
|
|
for report_unit in report.get("units", []):
|
|
unit_name = report_unit["name"]
|
|
config_unit = config_units.get(unit_name)
|
|
if not config_unit or "base_path" not in config_unit:
|
|
continue
|
|
functions = {
|
|
function["name"]: function
|
|
for function in report_unit.get("functions", [])
|
|
if not function["name"].startswith("$")
|
|
}
|
|
if not functions:
|
|
continue
|
|
try:
|
|
target = load(project_root / config_unit["target_path"])
|
|
base = load(project_root / config_unit["base_path"])
|
|
except (CoffError, OSError) as error:
|
|
errors.append({"unit": unit_name, "error": str(error)})
|
|
continue
|
|
|
|
target_info = {}
|
|
base_info = {}
|
|
for function_name in functions:
|
|
try:
|
|
target_info[function_name] = section_info(target, function_name)
|
|
except CoffError:
|
|
continue
|
|
try:
|
|
base_info[function_name] = section_info(base, function_name)
|
|
except CoffError:
|
|
base_info[function_name] = None
|
|
|
|
callers = defaultdict(set)
|
|
address_taken = set()
|
|
for caller_name, info in target_info.items():
|
|
for relocation in info["relocations"]:
|
|
target_identity = relocation["target"]
|
|
if len(target_identity) < 2 or target_identity[0] != "symbol":
|
|
continue
|
|
callee_name = target_identity[1]
|
|
if callee_name not in functions:
|
|
continue
|
|
if relocation["type"] == IMAGE_REL_I386_REL32:
|
|
callers[callee_name].add(caller_name)
|
|
elif relocation["type"] == IMAGE_REL_I386_DIR32:
|
|
address_taken.add(callee_name)
|
|
|
|
for function_name, target_measurement in target_info.items():
|
|
base_measurement = base_info[function_name]
|
|
if base_measurement is not None and target_measurement == base_measurement:
|
|
continue
|
|
function_callers = sorted(callers[function_name])
|
|
absent_callers = sorted(
|
|
caller for caller in function_callers if base_info.get(caller) is None
|
|
)
|
|
percent = float(functions[function_name].get("fuzzy_match_percent", 0.0))
|
|
if base_measurement is None:
|
|
size_equal = False
|
|
relocation_count_equal = False
|
|
relocation_identities_equal = False
|
|
base_snapshot = None
|
|
else:
|
|
size_equal = target_measurement["size"] == base_measurement["size"]
|
|
relocation_count_equal = (
|
|
target_measurement["relocation_count"] ==
|
|
base_measurement["relocation_count"]
|
|
)
|
|
relocation_identities_equal = _relocation_identities_equal(
|
|
target_measurement, base_measurement
|
|
)
|
|
base_snapshot = {
|
|
"size": base_measurement["size"],
|
|
"relocation_count": base_measurement["relocation_count"],
|
|
"normalized_sha256": base_measurement["normalized_sha256"],
|
|
}
|
|
|
|
verdict = classify_candidate(
|
|
base_measurement is not None,
|
|
percent,
|
|
relocation_identities_equal,
|
|
absent_callers,
|
|
size_equal,
|
|
relocation_count_equal,
|
|
)
|
|
results.append({
|
|
"unit": unit_name,
|
|
"function": function_name,
|
|
"callers": function_callers,
|
|
"callers_absent": absent_callers,
|
|
"address_taken": function_name in address_taken,
|
|
"objdiff_percent": percent,
|
|
"target": {
|
|
"size": target_measurement["size"],
|
|
"relocation_count": target_measurement["relocation_count"],
|
|
"normalized_sha256": target_measurement["normalized_sha256"],
|
|
},
|
|
"base": base_snapshot,
|
|
"relocation_identities_equal": relocation_identities_equal,
|
|
"verdict": verdict,
|
|
})
|
|
|
|
return {
|
|
"summary": {
|
|
"functions": len(results),
|
|
"errors": len(errors),
|
|
"verdicts": {
|
|
verdict: sum(item["verdict"] == verdict for item in results)
|
|
for verdict in sorted(VERDICTS)
|
|
},
|
|
},
|
|
"functions": sorted(results, key=lambda item: (item["unit"], item["function"])),
|
|
"errors": errors,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--project", type=Path, default=Path.cwd())
|
|
parser.add_argument("--report", type=Path, default=Path("build/report.json"))
|
|
parser.add_argument("--config", type=Path, default=Path("objdiff.json"))
|
|
parser.add_argument("--output", type=Path)
|
|
parser.add_argument("--unit", action="append", help="limit output to an object name")
|
|
args = parser.parse_args()
|
|
root = args.project.resolve()
|
|
result = analyze(root, root / args.report, root / args.config)
|
|
if args.unit:
|
|
allowed = set(args.unit)
|
|
result["functions"] = [
|
|
item for item in result["functions"] if item["unit"] in allowed
|
|
]
|
|
result["summary"]["functions"] = len(result["functions"])
|
|
result["summary"]["verdicts"] = {
|
|
verdict: sum(item["verdict"] == verdict for item in result["functions"])
|
|
for verdict in sorted(VERDICTS)
|
|
}
|
|
serialized = json.dumps(result, indent=2) + "\n"
|
|
if args.output:
|
|
output = root / args.output
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(serialized, encoding="utf-8")
|
|
print(serialized, end="")
|
|
if result["errors"]:
|
|
raise SystemExit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|