Files
2026-09-16 10:01:43 +03:00

210 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Проверенная оффлайновая карта SDCC; неизвестные диапазоны не угадываются."""
from __future__ import annotations
from dataclasses import asdict, dataclass
import json
from pathlib import Path
import re
from .build import digest
@dataclass(frozen=True)
class Location:
link_address: int
logical_address: int
section: str
bank: int | None
window: int | None
class DebugMap:
def __init__(self, directory):
self.directory = Path(directory).resolve()
self.manifest = json.loads((self.directory/'manifest.json').read_text())
if self.manifest['schema_version'] != 1:
raise ValueError('Неподдержанная версия пакета')
for name, expected in self.manifest['artifacts'].items():
path = (self.directory/name).resolve()
if not path.is_relative_to(self.directory) or digest(path) != expected:
raise ValueError('Повреждённый артефакт: ' + name)
self.stale_sources = [name for name, value in self.manifest['sources'].items()
if not Path(name).is_file() or digest(Path(name)) != value['sha256']]
stem = Path(self.manifest['executable']).stem
self.symbols = {}
for row in (self.directory/(stem+'.noi')).read_text().splitlines():
match = re.fullmatch(r'DEF (\S+) (0x[0-9A-Fa-f]+)', row)
if match:
self.symbols[match[1]] = int(match[2], 16)
self.sections = []
for row in (self.directory/(stem+'.map')).read_text().splitlines():
match = re.match(r'^(\S+)\s+([0-9A-F]{8})\s+([0-9A-F]{8})\s+=', row)
if match and int(match[3],16):
item = (match[1], int(match[2],16), int(match[3],16))
if item not in self.sections: self.sections.append(item)
self.units = {u['module']: u for u in self.manifest['units']}
self.instructions = {}
self.markers = []
self.functions = []
self.variables = []
self.logpoints = []
self.diagnostics = []
self._load((self.directory/(stem+'.cdb')).read_text().splitlines())
def verify_executable(self):
path = Path(self.manifest['executable_path'])
if not path.is_file() or digest(path) != self.manifest.get('executable_sha256', self.manifest['build_id']):
raise ValueError('EXE не соответствует пакету')
def location(self, address):
sections = [s for s in self.sections if s[1] <= address < s[1]+s[2]]
if len(sections) != 1:
raise ValueError(f'Неоднозначная/неизвестная секция адреса {address:#x}')
section = sections[0][0]
match = re.fullmatch(r'_?BANK(\d+)', section)
bank = int(match[1]) if match else None
if address > 0xffff and (bank is None or address >> 16 != bank):
raise ValueError(f'Неподдержанное размещение {section}: {address:#x}')
logical = address & 0xffff
return asdict(Location(address, logical, section, bank, logical >> 14))
def variable_location(self, address, symbol):
"""Разрешить доказанный linker-символ `__at` вне обычных секций.
Код/точки по-прежнему обязаны принадлежать ровно одной секции. Для
global/static SDCC может намеренно поместить объект по абсолютному
адресу (например, scratch в W1); его адрес подтверждают и CDB, и NOI.
"""
try:
return self.location(address)
except ValueError:
if not (0 <= address <= 0xffff and self.symbols.get(symbol) == address):
raise
return asdict(Location(address, address, '_ABS', None, address >> 14))
def _load(self, records):
addresses, declarations = {}, []
module = None
for record in records:
if record.startswith('M:'): module = record[2:]
elif record.startswith('L:'):
name, value = record[2:].rsplit(':', 1)
address = int(value, 16)
if name in addresses and addresses[name] != address:
raise ValueError('Конфликт отладочного символа: ' + name)
addresses[name] = address
elif record.startswith(('F:', 'S:')):
declarations.append((module, record))
elif record and not record.startswith('T:'):
self.diagnostics.append('Неизвестная запись: '+record)
# A$ у ассемблера использует basename файла, а не .module.
for unit in self.units.values():
asm = (self.directory/unit['asm']).read_text().splitlines()
sizes = {}
listing = (self.directory/Path(unit['asm']).with_suffix('.lst')).read_text()
for row in listing.splitlines():
match = re.match(r'^\s+[0-9A-F]{6,8}\s+(.+?)\s+\[\s*\d+\]\s+(\d+)\s', row)
if match:
sizes[int(match[2])] = len(re.findall(r'[0-9A-F]{2}', match[1]))
prefix = 'A$'+Path(unit['asm']).stem+'$'
for symbol, address in addresses.items():
if symbol.startswith(prefix):
line = int(symbol[len(prefix):])
if line in sizes and sizes[line] > 0:
self.instructions[address] = {
**self.location(address), 'size': sizes[line],
'asm': unit['asm'], 'asm_line': line, 'text': asm[line-1].strip(),
'module': unit['module'],
}
for symbol, marker in unit['markers'].items():
if symbol not in addresses:
raise ValueError('Отсутствует linked CDB-маркер: '+symbol)
paths = unit['sources'].get(Path(marker['file']).name, [])
if len(paths) > 1:
raise ValueError('Неоднозначный путь debug-записи: ' + marker['file'])
self.markers.append({**self.location(addresses[symbol]),
'line': marker['line'], 'sources': paths,
'module': unit['module']})
for module, record in declarations:
match = re.match(r'([FS]):([^($]+\$[^($]+\$[^($]+\$[^($]+)\(\{(\d+)\}(.+)\),([A-Z]),', record)
if not match: continue
kind, key, size, ctype, space = match.groups()
parts = key.split('$')
if kind == 'F':
startkey = '$'.join(parts[:2])+'$0$0'
start, last = addresses.get(startkey), addresses.get('X'+startkey)
if start is None or last not in self.instructions: continue
end = last + self.instructions[last]['size']
if end <= start: continue
function = {'name': parts[1], 'module': module, 'start': start,
'end': end, **self.location(start)}
if function not in self.functions: self.functions.append(function)
elif space == 'E' and parts[0].startswith(('G','F')) and not ctype.startswith('DF,'):
address = addresses.get(key)
if address is None and parts[0] == 'G': address = self.symbols.get('_'+parts[1])
if address is None: continue
supported = bool(re.fullmatch(r'S[ICL]:[SU]', ctype) or ctype.startswith('DG,'))
linker_symbol = key if key in self.symbols else '_'+parts[1]
variable = {'name': parts[1], 'module': module if parts[0] != 'G' else None,
'size': int(size), 'type': ctype,
'signed': ctype.endswith(':S') and not ctype.startswith('D'),
'supported': supported,
**self.variable_location(address, linker_symbol)}
if variable not in self.variables: self.variables.append(variable)
self.functions.sort(key=lambda f: f['start'])
self.markers.sort(key=lambda m: m['link_address'])
for unit in self.units.values():
for macro in unit.get('log_macros', []):
address = self.symbols.get(macro['symbol'])
if address is None:
raise ValueError('Связанный SDBG_LOG-якорь отсутствует: ' + macro['tag'])
try:
location = self.location(address)
verified = address in self.instructions and self.function_at(address) is not None
reason = None if verified else 'Якорь не совпал с началом исполняемой инструкции'
except ValueError as error:
location, verified, reason = {}, False, str(error)
self.logpoints.append({**macro, **location, 'module': unit['module'],
'verified': verified, 'reason': reason})
def function_at(self, address):
found = [f for f in self.functions if f['start'] <= address < f['end']]
return found[0] if len(found) == 1 else None
def addr2line(self, address):
function = self.function_at(address)
instruction = next((v for k,v in self.instructions.items()
if k <= address < k+v['size']), None)
if not function or not instruction:
return {'address': address, 'status': 'unknown', 'function': function}
markers = [m for m in self.markers if m['module'] == function['module']
and function['start'] <= m['link_address'] <= instruction['link_address']]
nearest = max((m['link_address'] for m in markers), default=None)
sources = []
for marker in markers:
if marker['link_address'] == nearest:
for source in marker['sources']:
item = {'file': source, 'line': marker['line']}
if item not in sources: sources.append(item)
return {'status': 'mapped' if len(sources) == 1 else 'ambiguous' if sources else 'unknown',
'instruction': instruction, 'function': function, 'sources': sources, 'stale_source': any(s['file'] in self.stale_sources for s in sources)}
def line_locations(self, filename, line):
exact = str(Path(filename).resolve())
known = self.manifest['sources']
candidates = [exact] if exact in known else [p for p in known if Path(p).name == filename]
if len(candidates) > 1: raise ValueError('Неоднозначный source; укажите полный путь')
result = []
for marker in self.markers:
address = marker['link_address']
if marker['line'] == line and any(p in candidates for p in marker['sources']):
if address in self.instructions and self.function_at(address):
item = {**marker, 'function': self.function_at(address)['name']}
if item not in result: result.append(item)
stale = any(p in self.stale_sources for p in candidates)
return {'status': 'stale' if stale else 'verified' if result else 'unverified',
'locations': result, 'stale_source': stale}
def source_text(self, filename, line):
source = self.manifest['sources'][filename]
text = (self.directory/source['snapshot']).read_text(errors='replace').splitlines()
return text[line-1] if 0 < line <= len(text) else None