aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_diff.py
blob: b3e06d89fecdcb52f68e867712ca8ce61c076169 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python3
"""Per-function diff between two TA builds.

The emitted artifacts are already aligned for this -- addresses stripped from banners and
functions sorted by name -- but a raw `diff` of two builds is still 6-17% of lines changed,
and almost none of it is a firmware change. Measured on three adjacent GCP pairs, the noise
is:

  * `[tier=MNEM size=22]` vs `[tier=EXACT size=22]` -- how the name was derived, not what the
    code does. 42 of 302 changed lines on one pair.
  * `off_40480C` -> `off_40481C`, `loc_4057E8` -> `loc_4057F8` -- unnamed globals and branch
    targets whose addresses shifted when code was inserted upstream. Normalising these alone
    cuts one pair from 405 changed lines to 175.
  * raw hex in `JUMPOUT(0x4055E4)` and similar.
  * IDA resolving the same call site to a name in one build and `loc_*` in another, plus
    local-variable renumbering (`v65`..`v76`) when the decompiler's register allocation shifts.

What survives normalisation is worth reading. On 22.0.0.73 -> 22.0.0.77 it is two new switch
arms, `case 0x26` and `case 0x27` -- new command IDs in a dispatcher, which is exactly the kind
of finding this corpus exists to surface.

Residual false positives, measured after the naming convention was aligned:

  * A function can carry a richer name in one build than another -- `gcp_cmdFE0C` against
    `gcp_cmdFE0C_CreateCryptoSession` -- because the semantic half comes from a pass that only
    reaches some builds. That reports as one added and one removed function. It took the
    22.0.4.49 -> 22.0.4.50 pair from +8/-8 to +4/-4; the rest is this.
  * `start` and `*_ta_main_loop` show as changed on IDA instability alone.

So treat added/removed as "look at this", not as ground truth, and changed-function counts as
sound. Adjacent patch versions across the corpus average 0.5 (ras) to 26 (xgmi) changed
functions, and the high numbers track modules whose builds are genuinely far apart.

Usage:
  ta_diff.py <buildA> <buildB>           per-function summary
  ta_diff.py <buildA> <buildB> --body    unified diff per changed function
  ta_diff.py --module gcp                every adjacent pair in a module
"""
import argparse
import difflib
import re
import sys
from pathlib import Path

DECOMP = Path(__file__).resolve().parent / "decomp"

# Tokens IDA derives from an address. These shift wholesale when code is inserted upstream,
# so they say nothing about whether the function changed.
ADDR_TOKEN = re.compile(r"\b(off|dword|word|byte|loc|unk|sub|flt|dbl|jpt|a)_[0-9A-Fa-f]{4,8}\b")
HEX_LITERAL = re.compile(r"\b0x[0-9A-Fa-f]{5,8}\b")
TIER = re.compile(r"\[tier=\w+ size=\d+\]")
LOCAL = re.compile(r"\bv\d+\b")
BANNER = re.compile(r"/\* ==== (\S+)[^\n]*? ==== \*/")


def normalise(line: str, locals_too: bool = False) -> str:
    line = TIER.sub("[..]", line)
    line = ADDR_TOKEN.sub(lambda m: m.group(0).split("_")[0] + "_X", line)
    line = HEX_LITERAL.sub("0xX", line)
    if locals_too:
        line = LOCAL.sub("vX", line)
    return line


def split_functions(path: Path) -> dict:
    """banner name -> normalised body lines."""
    out, cur = {}, None
    for line in path.read_text().splitlines():
        m = BANNER.match(line.strip())
        if m:
            cur = m.group(1)
            out[cur] = []
            continue
        if cur:
            out[cur].append(line)
    return out


def compare(a: Path, b: Path, locals_too: bool):
    fa, fb = split_functions(a), split_functions(b)
    added = sorted(set(fb) - set(fa))
    removed = sorted(set(fa) - set(fb))
    changed = []
    for name in sorted(set(fa) & set(fb)):
        na = [normalise(x, locals_too) for x in fa[name]]
        nb = [normalise(x, locals_too) for x in fb[name]]
        if na != nb:
            changed.append((name, na, nb))
    return added, removed, changed


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("builds", nargs="*")
    ap.add_argument("--module")
    ap.add_argument("--body", action="store_true", help="unified diff per changed function")
    ap.add_argument("--keep-locals", action="store_true",
                    help="do not normalise vN names (noisier, but shows real new locals)")
    args = ap.parse_args()

    pairs = []
    if args.module:
        cs = sorted(p for p in DECOMP.glob(f"{args.module}_*.c")
                    if not p.name.endswith(".anon.c") and "reference" not in p.name)
        pairs = list(zip(cs, cs[1:]))
    elif len(args.builds) == 2:
        pairs = [(DECOMP / (args.builds[0] + ".c"), DECOMP / (args.builds[1] + ".c"))]
    else:
        ap.error("give two build names or --module")

    for a, b in pairs:
        if not a.exists() or not b.exists():
            print("missing: %s or %s" % (a.name, b.name))
            continue
        added, removed, changed = compare(a, b, not args.keep_locals)
        print("=" * 78)
        print("%s  ->  %s" % (a.stem, b.stem))
        print("  functions: +%d  -%d  changed %d  (of %d)"
              % (len(added), len(removed), len(changed), len(split_functions(a))))
        for n in added:
            print("    + %s" % n)
        for n in removed:
            print("    - %s" % n)
        for name, na, nb in changed:
            d = [l for l in difflib.unified_diff(na, nb, lineterm="", n=0)
                 if l[:1] in "+-" and not l.startswith(("+++", "---"))]
            print("    ~ %-52s %d lines" % (name[:52], len(d)))
            if args.body:
                for l in d:
                    print("        %s" % l[:140])
    return 0


if __name__ == "__main__":
    raise SystemExit(main())