108 lines
5.5 KiB
Python
108 lines
5.5 KiB
Python
"""TI C28x tool adapter. Decode instructions using TI's own assembler tools."""
|
||
from pathlib import Path
|
||
import subprocess
|
||
import tempfile
|
||
import os
|
||
import re
|
||
|
||
|
||
def align_disassembly(*texts):
|
||
"""Align TI listings by word offset even when instruction sizes differ."""
|
||
listings = []
|
||
for text in texts:
|
||
if text.startswith('Ошибка:'):
|
||
return texts
|
||
listing = {}
|
||
previous = None
|
||
for line in text.splitlines():
|
||
match = re.match(r'^([0-9A-Fa-f]{8})\s+', line)
|
||
if match:
|
||
previous = int(match.group(1), 16)
|
||
listing[previous] = line
|
||
elif previous is not None and line.strip():
|
||
listing[previous] += ' ' + line.strip()
|
||
if not listing:
|
||
return texts
|
||
listings.append(listing)
|
||
addresses = sorted({address for listing in listings for address in listing})
|
||
return tuple('\n'.join(listing.get(address, f'{address:08X} —') for address in addresses)
|
||
for listing in listings)
|
||
|
||
|
||
def find_ghidra(root):
|
||
configured = os.environ.get('GHIDRA_INSTALL_DIR', '')
|
||
if configured and (Path(configured) / 'support/analyzeHeadless.bat').is_file():
|
||
return configured
|
||
return next((str(path) for path in sorted(Path(root).glob('ghidra_*'), reverse=True)
|
||
if (path / 'support/analyzeHeadless.bat').is_file()), '')
|
||
|
||
|
||
def decompile_c28x(data: bytes, tools: str, base: int = 0) -> str:
|
||
"""Run the configured Ghidra C28x processor and decompiler locally."""
|
||
root = Path(tools)
|
||
headless = root / 'support/analyzeHeadless.bat'
|
||
if not tools or not headless.is_file():
|
||
raise ValueError('Укажите папку Ghidra с установленным процессором TMS320C28x.')
|
||
if not data or len(data) % 2 or len(data) > 262144:
|
||
raise ValueError('Нужен непустой чётный диапазон до 256 КиБ.')
|
||
if not 0 <= base <= 0x3fffff:
|
||
raise ValueError('Адрес C28x должен быть в диапазоне 0…0x3FFFFF (в словах).')
|
||
scripts = Path(__file__).parent / 'disassembly_scripts'
|
||
env = dict(os.environ)
|
||
jdks = sorted(path for path in root.parent.glob('jdk*')
|
||
if path.is_dir() and (path / 'bin/java.exe').is_file())
|
||
if jdks:
|
||
env['JAVA_HOME'] = str(jdks[-1])
|
||
with tempfile.TemporaryDirectory(prefix='setgui-decompile-') as folder:
|
||
work = Path(folder)
|
||
binary, output = work / 'range.bin', work / 'result.c'
|
||
binary.write_bytes(data)
|
||
arguments = [str(headless), str(work), 'analysis', '-import', str(binary),
|
||
'-processor', 'TMS320C28x:LE:32:default', '-loader', 'BinaryLoader',
|
||
'-loader-baseAddr', f'{base:x}', '-noanalysis', '-scriptPath', str(scripts),
|
||
'-postScript', 'SetguiExportC.java', str(output), '-deleteProject']
|
||
result = subprocess.run(arguments, env=env, capture_output=True, text=True,
|
||
errors='replace', timeout=180, stdin=subprocess.DEVNULL,
|
||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||
if result.returncode or not output.is_file():
|
||
raise RuntimeError((result.stderr + '\n' + result.stdout)[-6000:])
|
||
text = output.read_text(encoding='utf-8')
|
||
if not text.rstrip().endswith('/* SETGUI_DECOMPILE_SUCCESS */'):
|
||
raise RuntimeError('Декомпиляция не завершена. ' + (result.stderr + '\n' + result.stdout)[-6000:])
|
||
return text.removesuffix('/* SETGUI_DECOMPILE_SUCCESS */\n')
|
||
|
||
|
||
def find_c28x_tools():
|
||
candidates = sorted(Path('C:/ti').glob('ti-cgt-c2000_*/bin'), reverse=True)
|
||
candidates += sorted(Path('C:/ti').glob('ccs*/ccs/tools/compiler/ti-cgt-c2000_*/bin'), reverse=True)
|
||
return next((str(path) for path in candidates
|
||
if (path / 'cl2000.exe').is_file() and (path / 'dis2000.exe').is_file()), '')
|
||
|
||
|
||
def disassemble_c28x(data: bytes, tools: str) -> str:
|
||
"""Wrap raw little-endian words in a temporary text section for dis2000."""
|
||
if not data or len(data) % 2:
|
||
raise ValueError('Для C28x нужен непустой диапазон с чётным числом байтов.')
|
||
if len(data) > 262144:
|
||
raise ValueError('Выберите диапазон не более 256 КиБ.')
|
||
directory = Path(tools)
|
||
for executable in ('cl2000.exe', 'dis2000.exe'):
|
||
if not (directory / executable).is_file():
|
||
raise ValueError(f'Не найден {executable} в {directory}')
|
||
with tempfile.TemporaryDirectory(prefix='setgui-disasm-') as folder:
|
||
root = Path(folder)
|
||
source = root / 'dump.asm'
|
||
source.write_text(' .text\n' + '\n'.join(
|
||
f' .word 0x{int.from_bytes(data[i:i + 2], "little"):04X}'
|
||
for i in range(0, len(data), 2)) + '\n', encoding='ascii')
|
||
def run(arguments):
|
||
result = subprocess.run(arguments, cwd=root, capture_output=True,
|
||
text=True, errors='replace', timeout=30,
|
||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||
if result.returncode:
|
||
raise RuntimeError(result.stderr or result.stdout or 'Ошибка TI tools')
|
||
return result.stdout
|
||
run([str(directory / 'cl2000.exe'), '--silicon_version=28', '--compile_only',
|
||
'--output_file=dump.obj', str(source)])
|
||
return run([str(directory / 'dis2000.exe'), '--data_as_text', 'dump.obj'])
|