Sprinter: добавить отладку C-исходников и интеграцию VS Code

This commit is contained in:
2026-09-15 17:58:41 +03:00
parent 50c6e56b7b
commit e4695b8281
62 changed files with 7147 additions and 27 deletions
+326
View File
@@ -0,0 +1,326 @@
# Отладка исходников: реализация и результаты
Дата: 2026-09-15. План: [mame-source-debug.md](mame-source-debug.md).
Реализованы сборка/карта, проверенный транспорт, базовая C-сессия,
постоянный session server, DAP MVP, VS Code launch и build task. MCP C-уровня,
безопасный restart и расширенная отладка ещё не готовы.
Этапы плана не считаются завершёнными целиком по одному успешному репро.
## Статус платформ
- **macOS:** текущий end-to-end цикл проверен, включая backend `sdbg` и
одновременную работу VS Code со штатным `osx` debugger MAME.
- **Linux:** ожидается работа через `sdbg`, `qt` или `imgui`, но полный
живой прогон ещё не выполнен.
- **Windows:** весь функционал пока работать не будет. Provider
`debugger: "windows"` открывает штатный debugger MAME, однако host-часть
использует `fcntl`, Unix domain sockets и Unix launcher. Native Windows
launch/attach из VS Code официально не поддерживается до реализации
переносимого транспорта, блокировки, запуска и end-to-end тестов.
## Доступно сейчас
- `bin/sprinter-cc --src-debug` и повторяемый `--src-debug-file FILE`.
- `app.mk`: `SRC_DEBUG=1` либо `SRC_DEBUG_FILES=...`; выбор интерпретатора
через `PYTHON` в make и `SPRINTER_PYTHON` у обёртки. В проекте используется
local pyenv Python 3.12 (`.python-version`); личный путь не зашит в код.
- Уникальные имена CDB-позиций и TU, включая повторные метки внутри одного
модуля. Публичные C/asm-символы и инструкции не переименовываются.
- Сборка в отдельном временном каталоге с проверкой пакета перед публикацией.
Ошибка compiler/linker/парсера сохраняет предыдущие EXE и пакет.
Конкурентные debug-сборки одного output сериализованы; одновременная
обычная и debug-сборка одного output пока не поддерживается.
- Manifest: build ID, хеш EXE/артефактов/исходников/включённых заголовков,
сохранённые исходники, параметры памяти, команды, версия SDCC и выбранные TU.
Зависимости собираются также для TU без карты в пер-модульном режиме.
- Проверяемый JSON-индекс `.sdbg.json`, offline CLI функций, переменных,
символов и соответствия C/asm/адресов. Банковые адреса учитывают секцию
и окно huge/big; банк данных не трактуется как резидентная память.
- Пересборка make при смене команды/содержимого зависимостей. Быстрая правка
заголовка не зависит от секундной точности mtime; повторный make без
изменений не вызывает compiler.
- Отдельный плагин `toolchain/mcp/sdbgbridge`, загружаемый непосредственно
из репозитория через pluginspath. Он не заменяет существующий mamebridge
и не требует копирования изменённых файлов в игнорируемое дерево MAME.
- Версия протокола, ID сессии, generation, атомарные файловые ответы,
ограниченный журнал событий, stopped snapshots, чтение logical memory
с отключёнными side effects, pause/continue/instruction step и точки
с проверкой владения. FileBridge запрещает повтор mutations после timeout.
- `DebugSession` проверяет EXE, Intel HEX и исполняемые байты реально
загруженного resident/current-bank образа перед интерпретацией PC. Он читает
`_bank_pages`, отвергает нули/дубликаты и учитывает, что state MAME хранит
старшие флаги над 8-битным значением page-port.
- Точки по строке и функции, удаление логической группы, `where` и чтение
поддержанных 1/2/4-байтных global/static. Банковская точка получает условие
по I/O page-port (`ib@e2==...` для W3), формируемое самим plugin; произвольная
debugger expression через этот вызов не принимается. Resident-объект или
bank-data не читается, если его физическая страница сейчас закрыта.
- Bootstrap живого теста не держит entry-breakpoint во время загрузки DSS.
Он активирует единственную service-точку перед вводом команды запуска,
сверяет сигнатуру/образ на `main`, а исходные точки ставит после attach.
Банковые пользовательские точки нельзя безопасно активировать до заполнения
`_bank_pages`, поэтому они намеренно создаются после runtime init.
- `sdbg_server.py` — единственный долгоживущий владелец backend. Локальный
Unix JSON-RPC допускает несколько клиентов, сериализует команды, хранит
логические группы и события. `set_source_breakpoints` и
`set_function_breakpoints` создают новый disabled-набор, при ошибке удаляют
его, затем заменяют прежний набор и активируют точки.
- DAP MVP и минимальное VS Code-расширение: attach к session server,
source/function breakpoints, один достоверный frame, registers,
поддержанные globals/statics, evaluate одного имени, continue/pause и
instruction step. F11 идёт до другой C-позиции, F10 использует MAME `over`,
Shift+F11 выходит через bank trampoline до C-позиции caller. Автомат имеет
предел 512 машинных операций и сохраняет приоритет пользовательской точки.
Длинный вызов с ожиданием внешнего ввода не блокирует DAP; Pause доступен.
Неподдержанные setVariable, disassemble, restart/terminate не рекламируются
либо возвращают явную ошибку.
- Собственное расширение является обязательной Sprinter-частью интеграции:
готовые C/C++ или clangd можно добавить для редактирования, но они не знают
source-debug package, DSS, банки и MAME DAP. Версия 0.2.0 даёт TaskProvider
для приложений с `app.mk`, команду Build Active Project и автоматическую
debug-сборку перед F5. Она использует `make SRC_DEBUG=1` и local Python 3.12;
ошибки SDCC с файлом/строкой попадают в Problems. Код сборки проверяется
самим расширением: при ошибке MAME не запускается. Run-команда, расширенные
linker/assembler diagnostics, выбор profile/EXTRA_DATA и VSIX остаются
следующими шагами.
- DAP `logMessage` без пересборки: разрешены литералы, `{{`/`}}` и только
подстановки `{variable}`. Session server читает типизированное значение,
отправляет `output` и продолжает CPU. Совпавшая обычная точка имеет
приоритет остановки, поэтому logpoint её не проглатывает. Произвольные
выражения, format specifier и conversion намеренно отвергаются.
- `<sdbg.h>`: `SDBG_LOG(tag, "total={total}")` и ограниченный
`SDBG_LOGIF(tag, flag, "...")` создают нулевые asm-якоря только в
выбранном debug-TU. Метаданные приходят из активного препроцессорного
потока; package содержит связанный адрес/причину unverified. Session server
автоматически ставит эти точки после attach, зеркалирует текст в MAME
debugger console и DAP Debug Console, затем продолжает CPU. Совпавшая
пользовательская точка по-прежнему останавливает исполнение. Локальные,
C-выражения и printf-форматы пока не доступны; значения читаются из
поддержанных global/static объектов, исходное приложение не печатает в DSS.
DAP event ring ограничен 1024 событиями; если клиент отстал, Debug Console
показывает число пропущенных событий. Частые logpoints останавливают CPU
при каждом попадании, их overhead пока не измерен. DAP хранит точный UTF-8
текст; MAME `printf` получает безопасную проекцию: кавычки заменяются
апострофом, управляющие символы — пробелами.
Практический контракт и таблица типов —
[руководство по SDBG_LOG](sdbg-log-macros.md). Decimal/hex formatter,
форматированный адрес и значение по ненулевому указателю — задачи финального
этапа, сейчас не поддерживаются. Для него `char *` задан как строка,
`int8_t *`/`uint8_t *` — как один 8-битный объект; текущий CDB не
различает `char *` и `uint8_t *`, поэтому нужна metadata declared type.
- `sdbg_launcher.py` реализует VS Code launch: создаёт временную дискету,
копию системного HDD и отдельные state-каталоги, показывает окно Sprinter,
ждёт DSS, ставит только service-точку `main`, вводит `a:\\NAME.EXE`,
проверяет сигнатуру entry и запускает session server. Закрытие DAP
завершает только созданные им server/MAME и удаляет временный каталог.
## Использование карты
В shell без инициализированных pyenv shims используйте `pyenv exec`:
```sh
pyenv exec make -C tests/hello SRC_DEBUG=1
pyenv exec python toolchain/sdbg.py --build tests/hello/.sprinter-cc-hello verify
pyenv exec python toolchain/sdbg.py --build tests/hello/.sprinter-cc-hello map
pyenv exec python toolchain/sdbg.py --build tests/hello/.sprinter-cc-hello vars
pyenv exec python toolchain/sdbg.py --build tests/hello/.sprinter-cc-hello line2addr hello.c 33
```
`addr2line` принимает адрес **линковщика** с префиксом `0x`, например
`0x1c00c`, а не логический PC без информации о банке. Ответ включает
logical_address, bank, window, функцию, инструкцию `.asm` и все найденные
исходные позиции. Неизвестный адрес возвращает unknown. Неоднозначности
не скрываются; изменённый исходник имеет stale-статус, сохранённый текст
собранной версии остаётся в пакете.
Для одной TU: `pyenv exec make -C tests/banked SRC_DEBUG_FILES=bank1.c`.
Режимы all/selected взаимоисключающие. `--debug` по-прежнему означает
runtime DEBUG_RT и не включает карту C.
Низкоуровневый live CLI работает с уже запущенным `sdbgbridge`; его IPC и
session ID должны совпадать с окружением процесса MAME:
```sh
pyenv exec python toolchain/sdbg_session.py \
--build tests/hello/.sprinter-cc-hello \
--ipc "$SDBG_IPC_DIR" --session "$SDBG_SESSION_ID" attach
```
Доступны `where`, `step`, `continue`, `break-line`, `break-function`,
`read-variable`, `activate-breakpoints` и `deactivate-breakpoints`.
Это диагностический однооперационный CLI: логические ID групп точек живут
только внутри процесса. Постоянное владение и DAP добавятся в общем session
server; до него для долгой ручной работы нужен один Python-процесс с
`DebugSession`.
Предпочтительный режим для нескольких клиентов — один server:
```sh
pyenv exec python toolchain/sdbg_server.py \
--build tests/hello/.sprinter-cc-hello \
--ipc "$SDBG_IPC_DIR" --session "$SDBG_SESSION_ID" \
--socket /tmp/sprinter-sdbg.sock
pyenv exec python toolchain/sdbg_client.py \
--socket /tmp/sprinter-sdbg.sock status
```
VS Code-расширение находится в `toolchain/vscode-sprinter-debug/`. В
Extension Development Host используется attach-конфигурация:
```json
{
"type": "sprinter-mame",
"request": "attach",
"name": "Sprinter MAME: Attach",
"socket": "/tmp/sprinter-sdbg.sock"
}
```
В VS Code расширение `0.2.0` запускает адаптер через абсолютный путь к local
pyenv shim `~/.pyenv/shims/python` и задаёт корень workspace как `cwd`.
Это устраняет зависимость от `PATH` процесса VS Code, открытого из Dock.
Выбранные пути видны в Output → `Sprinter MAME Debug`; при раннем сбое
launcher сообщает stderr и последние строки MAME log.
Интегрированный запуск:
```json
{
"type": "sprinter-mame",
"request": "launch",
"name": "Sprinter MAME: Launch",
"build": "${workspaceFolder}/tests/hello/.sprinter-cc-hello"
}
```
Launcher читает text-mode descriptors из VRAM и ждёт стабильный пустой prompt
`X:…>` 0,25 секунды. `dssTimeout` по умолчанию равен 30 эмулируемым секундам;
`launchAt` теперь только необязательная нижняя граница. Перед вводом launcher
сохраняет снимок DSS.
Пошаговый запуск development-расширения описан в
[vscode-sprinter-debug.md](vscode-sprinter-debug.md).
## Воспроизводимые проверки
```sh
pyenv exec python -m unittest discover -s tests/sdbg -v
pyenv exec python tests/sdbg/run_mame_probe.py
pyenv exec python tests/sdbg/run_mame_probe.py --bridge
pyenv exec python tests/sdbg/run_mame_probe.py --bridge --banked
pyenv exec python tests/sdbg/run_mame_probe.py --bridge --banked --server
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --native-debugger
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --source-step
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --step-out
pyenv exec python tests/sdbg/run_vscode_dap_probe.py
pyenv exec python tests/sdbg/run_vscode_dap_probe.py --waitkey --manual-key
```
Живой тест использует отдельную дискету, копию системного HDD и отдельные
каталоги состояния в `build/sdbg-live/`. Общие media и запущенный вручную
MAME не изменяются. Без `--bridge` проверяется шаг из Lua callback;
с `--bridge` — отдельный протокол и файловый клиент. `--native-debugger`
добавляет родное окно к тому же DAP-сценарию. Скрипт, запущенный системным Python <3.12,
перезапускает себя через local pyenv. Результаты — `result.jsonl`, `mame.log`.
Остановка процесса в конце bridge-теста относится только к MAME этого теста.
Зафиксировано на SDCC 4.5.0 #15242 и MAME v0.287,
commit `b0c4527c2edb1ee177fd09b3c412b65b385bf35b`:
| Проверка | Результат |
|---|---|
| Обычная debug-линковка двух TU с общим inline | Воспроизводится rc != 0, `Multiple definition of C$...` |
| Уникализация debug-символов | Успешная линковка; функции обоих экземпляров различаются |
| Повторные метки sprinter.h:111 | Сохранены начало и эпилог в каждом из трёх TU: 6 адресов |
| debug/non-debug probe, banked huge и big | Полные EXE идентичны |
| Одинаковые basename двух utils.c | Две функции и два независимых статика |
| bank-data в big | Переменная разрешена в банке 1, окне W1 |
| Ошибка линковки поверх опубликованной сборки | Предыдущие EXE и manifest не изменились |
| Испорченный артефакт / изменённый source | Ошибка проверки / явный stale; старый текст сохранён |
| Пер-модульная карта | Только выбранная TU в карте, все компиляционные зависимости учтены |
| Быстрая правка header / переключение debug | Пересборка; без изменений повторной компиляции нет |
| Архив с libc/string/strlwr.c | L-записи попадают в общий CDB, F/S из архивного adb не импортируются |
| Lua step на main | PC=0x8218 сразу после запроса; PC=0x821B на следующем callback |
| Порядок DSS → arm → launch | VRAM-detector нашёл стабильный prompt в строке 22 на 4,69 с; только затем включена entry-точка, ложных попаданий 0 |
| Новый мост, родной debugger | Проверены образ, регистры, `total`: 0 → 42, C-line breakpoint; instruction step около 32–124 мс в отдельных прогонах |
| Банковская точка huge | Условие через W3 page-port; остановка PC=0xC000 разрешена как linker 0x1C000, bank 1 |
| Session server + DAP | Owner lock удержан; DAP function breakpoint остановил `worker`; frame сохранил bank 1 |
| Production launcher | DSS prompt → `main` с `false_hits=0`; backend `sdbg` и опциональный `osx` прошли одинаковый DAP-сценарий, штатный terminate без нового crash report |
| DAP logpoint + breakpoint | В bank 1 напечатано `bank_value=0`; совпавшая function breakpoint сохранила остановку |
| Авторский `SDBG_LOG`, 2026-09-15 | Fixture `logmacro`: EXE с/без macro побайтово равны, сообщение отсутствует в EXE; активный wrapper и склейка литералов привязаны к linked-якорю, `#if 0` исключён. Живой DAP launch в `sdbg` и `osx`: `total=1` одновременно в MAME debugger console и VS Code Debug Console, `false_hits=0` |
| Source F11/F10 | `main`: `0x42bb``0x42c1`; F10 выполнил банковский `worker` и остановился в caller на `0x42c9` |
| Source Shift+F11 | Из `worker` bank 1 выполнен выход через trampoline в `main:6`, `0x42c9` |
| VS Code stdio DAP launch, 2026-09-15 | Local pyenv shim → DAP → изолированный MAME/DSS → `hello``main:17`, PC=`0x8224`; `false_hits=0`, штатный disconnect |
| F10 через `getchar()`, 2026-09-15 | Асинхронный `next` ответил за 21,49 мс; при ручном `x` в окне MAME `hello.c:62``:63`, возврат `DE.low=0x78`; обе клавиатуры включены в изолированном cfg |
| VS Code TaskProvider Build, 2026-09-15 | Одиннадцать Node-проверок: выбор Makefile рядом с пакетом/в `build/`, команда make с Python shim, SDCC matcher, успешный и неуспешный код задачи, pyenv вне workspace; реальный make в `tests/hello` прошёл, VS Code показал Build → MAME/DSS → `main` |
| Ошибочная debug-сборка, 2026-09-15 | Изолированный `fault.c` вернул код 2 от make, `file:1: error 20` сохранился, Python traceback удалён; MAME не участвует |
| Владение и generation | Второй FileBridge и чтение со stale generation отклоняются |
Полный host-набор: 29 тестов прошли, один Unix-socket тест пропущен только
из-за запрета `bind` в sandbox. Тот же socket path проверен живым DAP-запуском.
Это репро доступности механизма, а не полный benchmark или проверка всех
runtime/mapping/lifecycle. Отдельная публикация debug-библиотек требует
добавления метаданных извлечённых архивных модулей; одних флагов --debug
недостаточно. Режим библиотек пока не включён.
## Новое ограничение MAME
`src/osd/modules/debugger/none.cpp::wait_for_debugger()` вызывает `go()`.
Поэтому `-debugger none` автоматически продолжает CPU при остановке.
Автономные logpoint actions с ним возможны, ожидание интерактивных команд
в stopped-состоянии — нет. Поэтому добавлен provider `sdbg`: он возвращается
в stopped-loop после короткого sleep, а ядро вызывает Lua periodic перед каждой
итерацией. Во время остановки CPU обычная обработка событий окна не работает:
ранний `sdbg` только спал, отчего macOS помечала MAME «не отвечает» и окно
не открывалось через Cmd-Tab/Dock. Provider теперь опрашивает события
активного OSD примерно 100 раз в секунду (в текущем macOS SDL3 build:
`input_update(false)` и `process_events()`; в native macOS OSD:
`MacPollInputs()`). Новый бинарник проходит DAP launch до `main`; пользователь
подтвердил, что при остановке в `main` окно снова открывается через
Cmd-Tab/Dock. Регрессионный DAP-проход через `getchar()` с клавишей `x`
также завершился на следующей строке с кодом `0x78`.
Воспроизводимый patch и идемпотентный установщик находятся в
`toolchain/mame-patches/` и `toolchain/apply-mame-sdbg-patch.sh`;
`make mame-sdbg` собирает и устанавливает бинарник. Provider `osx` остаётся
доступной launch-опцией и проверен одновременно с DAP. Для непатченного MAME
можно использовать `auto`: штатные варианты — `osx` на macOS, `windows` в
native Windows build, `qt` или `imgui` в Linux. `qt` зависит от
`USE_QTDEBUG=1`, а `imgui` требует графическое окно. Windows-версия самого
host-моста пока не готова: FileBridge использует `fcntl`, server — Unix socket.
Также startup debugscript с `g` может продолжить первое реальное попадание;
живой bridge-тест не использует такой скрипт. Повторная загрузка autoboot
на reset защищена от дублирования callback. Старый mamebridge параллельно
с sdbgbridge не загружать: общая арбитрирующая сессия ещё не реализована.
## Размерный регресс и оставшаяся работа
`make size-check` запущен: сообщает рост у 12 программ относительно текущего
эталона, отсутствующие и новые сборки. В списке роста нет пересобранного
hello. Дополнительно cat и hello собраны исходной обёрткой из HEAD и новой
на тех же runtime/библиотеках: EXE попарно идентичны. Эталон не изменялся.
Этот общий check пока не считается прошедшим; расхождение существующих
сборок с baseline отделено от побайтовых регрессий новой debug-сборки.
Hard reset из RPC был удалён после воспроизводимого падения MAME 0.287:
старый Lua periodic callback обращался к `debugger_manager` во время нового
`running_machine::start()`. Reset notifier теперь только инвалидирует plugin,
а live-пробник прекращает работу callback до рестарта. До отдельного
безаварийного репро reset/restart capability не публикуется.
Crash reports в 19:22/19:23 относились к неподдержанному register symbol в
условии MAME; публичный протокол теперь принимает только window/page и plugin
строит проверенное условие через I/O port. После штатных прогонов в 20:43 и
позже новых `mame.arm-*.ips` не появилось.
Далее по плану: завершить инвалидацию на exit/state load, MCP-адаптер,
безопасный restart, расширенные выражения
и упаковку VS Code-расширения. Для IDE ещё нужны Run-команда, выбор профиля
сборки/данных и расширенная диагностика assembler/linker.
Базовый attach уже проверяет принадлежность resident/current-bank к build,
но не умеет читать неотображённую physical RAM. Нет автоматического чтения
неотображённого bank-data, локальных или backtrace.
Пути исходников в текущем пакете абсолютные. Снимки и уникальные TU уже
есть, переносимый source mapping — следующая доработка. Пока библиотечные
описания и отдельные форматы CDB не поддержаны, карта сообщает ограничения.