aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/phase3_decompile.py
blob: 62de6cf2e5413d7413eed1c935af7aca60d5b551 (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
"""Phase 3: decompile every named function in every build and normalise the text
so that only SEMANTICS remain.

Normalisation rules (and the reasoning):
  * symbolic autonames (sub_/loc_/off_/dword_/byte_/word_/unk_/jpt_ + hex) carry a
    build-specific address -> replaced by a generic token. A *named* callee keeps
    its name, because that is semantic.
  * bare hex that falls INSIDE the image's own VA range is an address -> ADDR.
    Anything outside that range is a real constant and is KEPT VERBATIM. This is
    the crux: a bounds value like 0x80, a mask like 0x7FF or a size like 0x1400
    must survive, because fixes of exactly that shape are what the previous
    mnemonic-only diff was blind to.
  * Hex-Rays locals (v3, a1, ...) are renumbered by order of first appearance, so
    that adding one local does not shift every name and fake a diff.
  * comments are stripped (they include our own port-tier markers).

Determinism is asserted: decompiling the same build twice must be byte-identical.
"""
import glob, os, re, sys, json, struct, hashlib
import idapro, idautils, idc, ida_hexrays

SYMRE = re.compile(r'\b(?:sub|loc|off|dword|byte|word|qword|unk|jpt|algn|asc|flt|stru)_[0-9A-Fa-f]+\b')
HEXRE = re.compile(r'\b0x([0-9A-Fa-f]+)\b')
VARRE = re.compile(r'\b([av])(\d+)\b')

def elf_range(p):
    raw=open(p,'rb').read(); ph=struct.unpack('<I',raw[28:32])[0]
    o,v,pa,f=struct.unpack('<IIII',raw[ph+4:ph+20])
    return v, v+f

def normalise(txt, lo, hi):
    txt = re.sub(r'//.*', '', txt)                 # strip comments
    txt = SYMRE.sub('SYM', txt)                    # build-specific autonames
    def hx(m):
        val=int(m.group(1),16)
        return 'ADDR' if lo <= val < hi else m.group(0)   # literals preserved
    txt = HEXRE.sub(hx, txt)
    order={}
    def vr(m):
        k=m.group(0)
        if k not in order: order[k]='%s%d'%(m.group(1), len(order))
        return order[k]
    txt = VARRE.sub(vr, txt)
    return re.sub(r'\s+', ' ', txt).strip()

def run(elf):
    lo,hi = elf_range(elf)
    rc=idapro.open_database(elf, run_auto_analysis=False)
    if rc!=0: return None
    ida_hexrays.init_hexrays_plugin()
    out={}
    for f in idautils.Functions():
        n=idc.get_func_name(f)
        if n.startswith('sub_') or n.startswith('nullsub'): continue   # unnamed -> not comparable
        try: t=str(ida_hexrays.decompile(f))
        except Exception: continue
        out[n]=normalise(t, lo, hi)
    idapro.close_database(save=False)
    return out

if __name__=='__main__':
    fams=sys.argv[1:] or ['hdcp','sos','sys_drv','asd']
    os.makedirs('phase3', exist_ok=True)
    for fam in fams:
        for elf in sorted(glob.glob('handover/elf/versions/%s/*.elf'%fam)):
            tag=os.path.basename(elf).replace('.elf','')
            dst='phase3/%s.json'%tag
            d=run(elf)
            if d is None: print("  FAILED %s"%tag, flush=True); continue
            json.dump(d, open(dst,'w'))
            print("  %-42s %5d functions decompiled"%(tag, len(d)), flush=True)