Sprinter: добавить отладку C-исходников и интеграцию VS Code
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""Проверенная оффлайновая карта 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 _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,'))
|
||||
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.location(address)}
|
||||
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
|
||||
Reference in New Issue
Block a user