114 lines
6.3 KiB
Python
114 lines
6.3 KiB
Python
"""Сборочные операции sdbg: уникальные debug-символы без изменения инструкций."""
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import json
|
||
import re
|
||
import subprocess
|
||
from pathlib import Path
|
||
from .macros import anchor_symbols, extract
|
||
|
||
|
||
def digest(path: Path) -> str:
|
||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||
|
||
|
||
def write_json(path: Path, data) -> None:
|
||
temporary = path.with_suffix(path.suffix + '.tmp')
|
||
temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n')
|
||
temporary.replace(path)
|
||
|
||
|
||
def normalize(assembly: str, adb: str, module: str):
|
||
"""Меняет только имена debug-записей; сохраняет число строк .asm."""
|
||
match = re.search(r'^\s*\.module\s+(\S+)', assembly, re.M)
|
||
if not match:
|
||
raise ValueError('В asm отсутствует .module')
|
||
old = match.group(1)
|
||
# F/XF/L связывают статики, функции и локальные с единицей трансляции.
|
||
def rename(text):
|
||
text = re.sub(r'\b(X?F)' + re.escape(old) + r'(?=\$)',
|
||
lambda m: m[1] + module, text)
|
||
return re.sub(r'\bL' + re.escape(old) + r'(?=[.$])', 'L' + module, text)
|
||
# Не трогаем .ascii и инструкции: строковый литерал может содержать
|
||
# текст, похожий на debug-символ. Переименовываем только определения
|
||
# и объявления символов, сгенерированные SDCC.
|
||
symbol_line = re.compile(r'^\s*(?:\.globl\s+|(?:X?F|L)' + re.escape(old) + r'[$.])')
|
||
assembly = ''.join(rename(line) if symbol_line.match(line) else line
|
||
for line in assembly.splitlines(keepends=True))
|
||
adb = rename(adb)
|
||
assembly = re.sub(r'(?m)^(\s*\.module\s+)\S+', lambda m: m[1] + module, assembly)
|
||
adb = re.sub(r'(?m)^M:.*$', 'M:' + module, adb)
|
||
markers, active, output = {}, {}, []
|
||
for number, line in enumerate(assembly.splitlines(keepends=True), 1):
|
||
definition = re.match(r'\s*(C\$[^\s=]+)\s*=\s*\.', line)
|
||
if definition:
|
||
original = definition[1]
|
||
parts = original.split('$')
|
||
if len(parts) != 5:
|
||
raise ValueError('Неизвестная форма CDB: ' + original)
|
||
unique = f'C${module}_{len(markers)}${parts[2]}${parts[3]}${parts[4]}'
|
||
active[original] = unique
|
||
markers[unique] = {'original': original, 'file': parts[1],
|
||
'line': int(parts[2]), 'asm_line': number}
|
||
line = line.replace(original, unique)
|
||
elif re.match(r'\s*\.globl\s+C\$', line):
|
||
original = line.split()[1]
|
||
if original not in active:
|
||
raise ValueError('CDB .globl без определения: ' + original)
|
||
line = line.replace(original, active[original])
|
||
output.append(line)
|
||
return ''.join(output), adb, markers
|
||
|
||
|
||
def compile_unit(sdcc: Path, assembler: Path, source: Path, output: Path, flags, debug=True):
|
||
"""-S и те же опции ассемблера, которые SDCC 4.5 использует с --debug."""
|
||
source = source.resolve()
|
||
asm, adb = output.with_suffix('.asm'), output.with_suffix('.adb')
|
||
compile_flags = [*flags, '-DSPRINTER_SDBG_ANCHORS=1'] if debug else flags
|
||
# SDCC -E кодирует внутренний разделитель строк inline asm байтом 0x87;
|
||
# для списка #line нужны только ASCII-пути. Само описание логов извлекаем
|
||
# отдельным metadata-проходом без asm-развёртки.
|
||
preprocessed = subprocess.check_output([str(sdcc), *compile_flags, '-E', str(source)],
|
||
text=True, errors='replace')
|
||
dependencies = {str(source)}
|
||
for filename in re.findall(r'^#(?:line)?\s*\d+\s+"([^"]+)"', preprocessed, re.M):
|
||
path = Path(filename).resolve()
|
||
if path.is_file(): dependencies.add(str(path))
|
||
hashes = {name: digest(Path(name)) for name in dependencies}
|
||
if not debug:
|
||
subprocess.run([str(sdcc), *flags, '-c', '-o', str(output), str(source)], check=True)
|
||
if any(digest(Path(name)) != sha for name, sha in hashes.items()):
|
||
raise ValueError('Исходники изменились во время компиляции')
|
||
write_json(output.with_suffix('.sdbg-input.json'), {'dependency_hashes': hashes})
|
||
return
|
||
metadata = subprocess.check_output([str(sdcc), *compile_flags,
|
||
'-DSPRINTER_SDBG_METADATA=1', '-E', str(source)], text=True)
|
||
macros = extract(metadata, dependencies)
|
||
subprocess.run([str(sdcc), *compile_flags, '--debug', '-S', '-o', str(asm), str(source)], check=True)
|
||
if any(digest(Path(name)) != sha for name, sha in hashes.items()):
|
||
raise ValueError('Исходники изменились во время компиляции')
|
||
original = asm.read_text()
|
||
# Включаем output stem: один исходник может собираться в разные банки.
|
||
module = 'sdbg_' + hashlib.sha256((str(source) + ':' + output.stem).encode()).hexdigest()[:16]
|
||
assembly, descriptions, markers = normalize(original, adb.read_text(), module)
|
||
assembly, log_macros = anchor_symbols(assembly, module, macros)
|
||
asm.write_text(assembly)
|
||
adb.write_text(descriptions)
|
||
subprocess.run([str(assembler), '-plosgffwy', str(output), str(asm)], check=True)
|
||
# Комментарии SDCC сохраняют фактические пути строк, включая заголовки.
|
||
sources = {}
|
||
for name in dependencies:
|
||
sources.setdefault(Path(name).name, set()).add(name)
|
||
for filename in re.findall(r'^;(.+?):\d+:', original, re.M):
|
||
path = Path(filename).resolve()
|
||
if path.is_file():
|
||
sources.setdefault(path.name, set()).add(str(path))
|
||
write_json(output.with_suffix('.sdbg-unit.json'), {
|
||
'module': module, 'source': str(source), 'source_sha256': digest(source),
|
||
'asm': asm.name, 'adb': adb.name, 'object': output.name,
|
||
'flags': compile_flags, 'markers': markers, 'log_macros': log_macros,
|
||
'dependency_hashes': hashes,
|
||
'sources': {k: sorted(v) for k, v in sources.items()},
|
||
})
|