Download scripts/analyze_native_dispatch.py from nmarshall/urt-rs: direct link, hf CLI and curl.
- Browser
- Download file 4.69 kB
-
https://huggingface.co/nmarshall/urt-rs/resolve/f47beb128571f948ffaaae9ea0aee143f6632dca/scripts/analyze_native_dispatch.py
- Command line
-
hf download hf://nmarshall/urt-rs@f47beb128571f948ffaaae9ea0aee143f6632dca/scripts/analyze_native_dispatch.py
-
curl -L -o analyze_native_dispatch.py https://huggingface.co/nmarshall/urt-rs/resolve/f47beb128571f948ffaaae9ea0aee143f6632dca/scripts/analyze_native_dispatch.py
4.69 kB
| #!/usr/bin/env python3 | |
| """Recover absolute x86_64 ELF syscall switch tables, retaining case evidence.""" | |
| import gzip | |
| import hashlib | |
| import json | |
| from pathlib import Path | |
| import re | |
| import struct | |
| from analyze_native import parse, take | |
| from analyze_qvm import write_json | |
| def recover(data, disassembly, symbol): | |
| layout, _ = parse(data) | |
| if layout['format'] != 'ELF' or layout['bits'] != 64: | |
| raise ValueError('this switch-table recognizer supports ELF64 only') | |
| function = re.search(r'(?m)^([0-9a-f]+) <'+re.escape(symbol)+r'>:\n(.*?)(?=\n[0-9a-f]+ <|\Z)', disassembly, re.S) | |
| if not function: | |
| raise ValueError(f'missing disassembly symbol {symbol}') | |
| prologue = '\n'.join(function[2].splitlines()[:24]) | |
| maximum = re.search(r'cmpq\s+\$0x([0-9a-f]+), %(\w+)', prologue) | |
| table = re.search(r'jmpq\s+\*0x([0-9a-f]+)\(,%(\w+),8\)', prologue) | |
| if not maximum or not table or maximum[2] != table[2]: | |
| raise ValueError(f'unrecognized bounds/table pattern in {symbol}') | |
| maximum, table_address = int(maximum[1], 16), int(table[1], 16) | |
| if maximum > 4096: | |
| raise ValueError('unreasonable syscall switch size') | |
| table_section = next((s for s in layout['sections'] if not s['zero_fill'] and s['address'] <= table_address and table_address+(maximum+1)*8 <= s['address']+s['bytes']), None) | |
| if table_section is None: | |
| raise ValueError('switch table outside a file-backed section') | |
| offset = table_section['file_offset']+table_address-table_section['address'] | |
| targets = struct.unpack('<'+'Q'*(maximum+1), take(data, offset, (maximum+1)*8)) | |
| lines = [] | |
| for line in disassembly.splitlines(): | |
| match = re.match(r'\s*([0-9a-f]+):\s', line) | |
| if match: | |
| lines.append((int(match[1], 16), line.strip())) | |
| indexes = {address: n for n, (address, _) in enumerate(lines)} | |
| groups = {} | |
| for number, target in enumerate(targets): | |
| if not any(s['executable'] and s['address'] <= target < s['address']+s['bytes'] for s in layout['sections']) or target not in indexes: | |
| raise ValueError('switch target is not a decoded executable instruction') | |
| groups.setdefault(target, []).append(number) | |
| cases = [] | |
| for target, numbers in sorted(groups.items()): | |
| snippet = [] | |
| for _, line in lines[indexes[target]:indexes[target]+24]: | |
| snippet.append(line) | |
| if re.search(r'\t(?:retq?|jmpq?)\s', line): | |
| break | |
| names = sorted(set(re.findall(r'\b(?:callq?|jmpq?)\s+[^\n<]*<([^>+]+)(?:\+[^>]*)?>', '\n'.join(snippet)))) | |
| cases.append({'target_address': target, 'numbers_sharing_target': numbers, | |
| 'first_instructions': snippet, 'direct_call_or_jump_labels': names}) | |
| return {'symbol': symbol, 'entry_address': int(function[1], 16), 'maximum_case_number': maximum, | |
| 'table_address': table_address, 'table_file_offset': offset, | |
| 'case_target_count': len(groups), 'cases': cases, | |
| 'limitations': 'Recognized bounds-check and absolute pointer-table pattern. Shared targets can be default/aliases; 24-instruction snippets are evidence, not complete case semantics or syscall signatures.'} | |
| def main(): | |
| output = [] | |
| for label in ('linux--Quake3-UrT-Ded.x86_64', 'linux--Quake3-UrT.x86_64'): | |
| report = json.loads(Path(f'docs/research/native/{label}.json').read_text()) | |
| path = Path('.cache/UrbanTerror43')/report['source']['path'] | |
| data = path.read_bytes() | |
| if hashlib.sha256(data).hexdigest() != report['source']['sha256']: | |
| raise ValueError('native snapshot mismatch') | |
| artifact = report['artifacts']['disassembly.txt'] | |
| disassembly = gzip.open(artifact['path'], 'rb').read() | |
| if hashlib.sha256(disassembly).hexdigest() != artifact['uncompressed_sha256']: | |
| raise ValueError('disassembly artifact changed') | |
| symbols = ['SV_GameSystemCalls'] if 'Ded' in label else ['SV_GameSystemCalls', 'CL_CgameSystemCalls', 'CL_UISystemCalls'] | |
| rows = [recover(data, disassembly.decode(), symbol) for symbol in symbols] | |
| result = {'source': report['source'], 'dispatches': rows} | |
| write_json(Path(f'docs/research/native/{label}.dispatch.json'), result) | |
| output.extend({'image': label, 'symbol': r['symbol'], 'maximum': r['maximum_case_number'], | |
| 'table_address': r['table_address'], 'distinct_targets': r['case_target_count']} for r in rows) | |
| write_json(Path('docs/research/native/dispatch-summary.json'), output) | |
| print('Recovered four native syscall dispatch tables with address-keyed case snippets') | |
| if __name__ == '__main__': | |
| main() | |