#!/usr/bin/env python3 """ Find GSP firmware functions using the actual calling convention: - Prologue: addi sp, sp, -N; sd s1-s11, offsets(sp); addi s0, sp, N - ra saved to callee-saved reg: mv sX, ra (or c.mv sX, ra) - Epilogue: mv ra, sX (or c.mv ra, sX); ret """ import struct import json import os FWIMAGE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gsp-570.144.fwimage.bin") OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_ADDR = 0x04000000 # Executable sections EXEC_SECTIONS = [ (0x04005000, 0x04006413), (0x04007000, 0x04008413), (0x04009000, 0x04009073), (0x0400a000, 0x0400cfff), (0x0400e000, 0x04010fff), (0x04018000, 0x0401cfff), (0x04c58000, 0x05b21fff), ] def read_binary(path): with open(path, 'rb') as f: return f.read() def is_in_exec(va): for start, end in EXEC_SECTIONS: if start <= va <= end: return True return False def next_inst(data, idx, max_bytes=1000): """Yield (offset, instruction, is_32bit) for instructions starting at idx.""" end = min(idx + max_bytes, len(data)) i = idx while i < end: if i + 2 > len(data): break first16 = struct.unpack('> 7) & 0x7 rs2 = (inst >> 10) & 0x7 return ('c.mv', {'rd': rd + 8, 'rs2': rs2 + 8}) if (inst & 0xE003) == 0x6001: # c.addi rd = (inst >> 7) & 0x7 imm = ((inst >> 2) & 0x3F) | ((inst >> 12) & 0x1) << 6 if imm & 0x40: imm -= 0x80 return ('c.addi', {'rd': rd + 8, 'imm': imm}) return None else: opcode = inst & 0x7F funct3 = (inst >> 12) & 0x7 rd = (inst >> 7) & 0x1F rs1 = (inst >> 15) & 0x1F if opcode == 0x13 and funct3 == 0: # addi imm = (inst >> 20) & 0xFFF if imm >= 0x800: imm -= 0x1000 return ('addi', {'rd': rd, 'rs1': rs1, 'imm': imm}) if opcode == 0x23 and funct3 == 3: # sd rs2 = (inst >> 20) & 0x1F offset = ((inst >> 7) & 0x1F) | ((inst >> 25) & 0x7F) << 5 return ('sd', {'rs1': rs1, 'rs2': rs2, 'offset': offset}) if opcode == 0x03 and funct3 == 3: # ld offset = (inst >> 20) & 0xFFF if offset >= 0x800: offset -= 0x1000 return ('ld', {'rd': rd, 'rs1': rs1, 'offset': offset}) if opcode == 0x6F: # jal imm = ((inst >> 31) & 0x1) << 20 | ((inst >> 12) & 0xFF) << 12 | ((inst >> 20) & 0x1) << 11 | ((inst >> 21) & 0x3FF) << 1 if imm & 0x100000: imm -= 0x200000 return ('jal', {'rd': rd, 'imm': imm}) if opcode == 0x67 and funct3 == 0: # jalr imm = (inst >> 20) & 0xFFF if imm >= 0x800: imm -= 0x1000 return ('jalr', {'rd': rd, 'rs1': rs1, 'imm': imm}) if inst == 0x00008067: # ret return ('ret', {}) return None def main(): print(f"Reading firmware...") data = read_binary(FWIMAGE_PATH) print(f"Binary size: {len(data)} bytes") functions = [] i = 0 while i < len(data) - 4: if not is_in_exec(BASE_ADDR + i): i += 2 continue # Look for function prologue: addi sp, sp, -N (N >= 32) if i + 4 <= len(data): inst32 = struct.unpack('> 7) & 0x1F rs1 = (inst32 >> 15) & 0x1F if rd == 2 and rs1 == 2: imm = (inst32 >> 20) & 0xFFF if imm >= 0x800: stack_size = -(imm - 0x1000) if stack_size >= 32: # Found potential prologue start va = BASE_ADDR + i # Scan forward to verify function structure saves_s = 0 has_s0_setup = False ra_save_reg = None ra_restore_reg = None has_ret = False calls = 0 for offset, inst, is_32 in next_inst(data, i + 4, max_bytes=500): dec = decode_inst(inst, is_32) if not dec: continue mnemonic, ops = dec # Count sd sX, offset(sp) - callee-saved saves if mnemonic == 'sd' and ops['rs1'] == 2 and 9 <= ops['rs2'] <= 18: saves_s += 1 # addi s0, sp, N (frame pointer setup) if mnemonic == 'addi' and ops['rd'] == 8 and ops['rs1'] == 2: if ops['imm'] == stack_size: has_s0_setup = True # mv sX, ra (save ra to callee-saved reg) if mnemonic == 'addi' and ops['rd'] >= 9 and ops['rd'] <= 18 and ops['rs1'] == 1 and ops['imm'] == 0: if ra_save_reg is None: ra_save_reg = ops['rd'] # c.mv sX, ra if mnemonic == 'c.mv' and ops['rs2'] == 1 and 8 <= ops['rd'] <= 15: if ra_save_reg is None: ra_save_reg = ops['rd'] # mv ra, sX (restore ra from callee-saved reg) if mnemonic == 'addi' and ops['rd'] == 1 and ops['imm'] == 0 and 9 <= ops['rs1'] <= 18: ra_restore_reg = ops['rs1'] # c.mv ra, sX if mnemonic == 'c.mv' and ops['rd'] == 1 and 8 <= ops['rs2'] <= 15: ra_restore_reg = ops['rs2'] # Function calls if mnemonic in ('jal', 'jalr'): calls += 1 # Return if mnemonic == 'ret': has_ret = True break # Validate: must have prologue elements and either: # - saves ra to callee-saved reg (if it makes calls) # - or is a leaf function (no calls) if saves_s >= 2 and has_s0_setup and has_ret: if calls == 0 or (ra_save_reg is not None and ra_restore_reg is not None): functions.append({ "address": f"0x{va:08x}", "stack_size": stack_size, "saves": saves_s, "ra_save_reg": ra_save_reg, "ra_restore_reg": ra_restore_reg, "calls": calls, "type": "non_leaf" if calls > 0 else "leaf" }) i += 4 continue i += 2 print(f"Found {len(functions)} functions") # Deduplicate seen = set() unique = [] for f in functions: if f["address"] not in seen: seen.add(f["address"]) unique.append(f) unique.sort(key=lambda x: int(x["address"], 16)) print(f"Unique functions: {len(unique)}") # Save with open(f"{OUTPUT_DIR}/functions_final.json", "w") as f: json.dump(unique, f, indent=2) with open(f"{OUTPUT_DIR}/function_addresses_final.txt", "w") as f: for func in unique: f.write(f"{func['address']} stack={func['stack_size']} saves={func['saves']} calls={func['calls']} ra_save={func.get('ra_save_reg') or 'none'} ra_restore={func.get('ra_restore_reg') or 'none'} type={func['type']}\n") # Stats leaf = sum(1 for f in unique if f['type'] == 'leaf') non_leaf = sum(1 for f in unique if f['type'] == 'non_leaf') print(f"Leaf functions: {leaf}") print(f"Non-leaf functions: {non_leaf}") # Show first 50 print("\nFirst 50 functions:") for f in unique[:50]: print(f" {f['address']}: stack={f['stack_size']} saves={f['saves']} calls={f['calls']} ra_save={f.get('ra_save_reg') or 'none'} type={f['type']}") if __name__ == "__main__": main()