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
+15
View File
@@ -471,6 +471,21 @@ text_cursor load_cursor/get_cursor(mouse_cursor_t*) set_sensitivity
get_sensitivity_x/y video_mode_changed`. Sensitivity = делитель
(меньше = быстрее). Solid-C алиасы ms_* включены.
## <sdbg.h> — логи source debugger без кода вывода Sprinter
`SDBG_LOG(tag, "text={global}")` и
`SDBG_LOGIF(tag, global_flag, "text={global}")` создают авторские
точки журналирования в выбранном `--src-debug` TU. `tag` — уникальный
C-идентификатор внутри TU; условие `LOGIF` пока только имя поддержанной
global/static переменной, ненулевое значение означает вывод. Подстановки
`{name}` читаются отладчиком при попадании; локальные и C-выражения пока
не поддержаны. Обычная сборка превращает макросы в пустое выражение.
Полный контракт и планы форматирования —
[руководство по SDBG_LOG](sdbg-log-macros.md).
Аргументы не вычисляются кодом приложения, поэтому побочные эффекты в них
недопустимы. Сообщение видно в MAME debugger console и VS Code Debug Console
при подключённой source-debug сессии; без неё макрос сам ничего не печатает.
## <sprinter.h> — платформа
Константы портов (PORT_PAGE_W0..W3, PORT_RGADR, PORT_RGMOD), номера
+20 -1
View File
@@ -239,7 +239,6 @@ end
Lua-REPL — удобно нащупывать поля/тайминги вживую перед скриптованием.
- **Второй видеорежим/варианты BIOS** — при необходимости менять
`COMMON_ARGS`.
```
## 10. Тайминги MCP-моста (run_bridge.sh) — НЕ ждать дольше
@@ -258,3 +257,23 @@ end
Пересобрал HDD-образ (`make hdd`) → MAME **обязан** полный рестарт
(`chdman -f` даёт новый inode; см. memory `mame_hdd_rebuild_restart`):
остановка через `exit` в дебаггере, затем `run_bridge.sh` заново.
## 11. Карта C/asm для отладки
`SRC_DEBUG=1` в app.mk создаёт проверенный пакет исходников и адресов без
изменения EXE в проверенных конфигурациях. Пер-модульный вариант:
`SRC_DEBUG_FILES=helper.c`. Команды карты и результаты живых экспериментов:
[mame-source-debug-status.md](mame-source-debug-status.md); полный план:
[mame-source-debug.md](mame-source-debug.md). Базовый C-attach/where/точки и
чтение простых переменных доступны через `toolchain/sdbg_session.py`.
`sdbg_server.py`, DAP MVP, VS Code launch и development-расширение уже есть;
безопасный restart и MCP C-уровня пока не готовы. `-debugger none`
автоматически продолжает CPU и не подходит для ожидания команд пошаговой
сессии.
Source-debug launcher ждёт до 10-й секунды эмулируемого времени, сохраняет
снимок готового DSS, только затем вооружает entry breakpoint и вводит команду
EXE. Пользовательские точки создаются после проверенного `main`, когда
`_bank_pages` уже заполнена. В обычной конфигурации это даёт 0 попаданий на
адрес entry во время загрузки DSS. Автоматического распознавания приглашения
`C:\\>` пока нет; `launchAt` в VS Code можно увеличить.
+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 не поддержаны, карта сообщает ограничения.
+817
View File
@@ -0,0 +1,817 @@
# Отладка C-приложений Sprinter в MAME и VS Code
Редакция: **2026-09-14**, после технического ревью плана от 2026-09-13.
Статус: **ЧАСТИЧНАЯ РЕАЛИЗАЦИЯ**. Сборка/оффлайновая карта, транспорт,
live C-сессия, постоянный server, DAP и VS Code launch MVP реализованы; результаты —
[mame-source-debug-status.md](mame-source-debug-status.md).
Полный lifecycle/restart и расширенная отладка ещё не реализованы.
Основной маршрут включает CLI/MCP и VS Code через DAP;
GDB — самостоятельное расширение по отдельной потребности.
### Статус платформ
- **macOS** — полный текущий цикл build → DSS → MAME → DAP → VS Code
проверен живыми запусками. Доступны backend `sdbg` и совместный режим
VS Code + штатное окно debugger через `debugger: "osx"`.
- **Linux** — архитектура и используемые host-механизмы совместимы; для
штатного окна MAME выбирается `qt` либо `imgui`. Полный живой прогон на
Linux ещё требуется, поэтому статус остаётся экспериментальным.
- **Windows** — полный функционал пока **не поддерживается**. Значение
`debugger: "windows"` выбирает только штатное окно debugger MAME, но
текущие owner lock (`fcntl`), Unix domain socket, shell-команды запуска и
тесты рассчитаны на macOS/Linux. Нужны Windows-реализации блокировки и
локального RPC, переносимый launcher и отдельные end-to-end тесты. До
этого launch/attach из VS Code на native Windows не считаются рабочими.
Прежние фазы 0–5A/5B заменены этапами §10. IDE учитывается в архитектуре
с самого начала; прежняя «опциональная фаза 4» теперь распределена между
этапами IDE и расширенной диагностики. Это изменение состава плана,
а не разрешение выполнять ранее отложенную реализацию.
## 1. Цель и критерий доверия
Целевой цикл: редактирование C → сборка sprinter-cc → упаковка приложения
и данных → запуск MAME/DSS → остановка на main → отладка из VS Code,
CLI или MCP.
Планируемые возможности:
- Точки по функции/строке C, условия, счётчики попаданий, логпоинты без
пересборки, watchpoint на поддержанные объекты.
- Текущий исходник, сгенерированный `.asm`, реальные инструкции, регистры.
- Чтение/изменение глобалов и статиков с учётом типов, банков и окон.
- Шаг по инструкции и исходнику; затем next/stepOut, проверенные локальные
переменные и восстановление стека в поддержанных случаях.
- Структурированные логи для автотестов/IDE, комментарии в родном
дизассемблере; позднее покрытие и профилирование.
- Общая семантика адресов и одна сессия для всех интерфейсов.
**Критерий доверия:** неизвестная строка, неподтверждённая точка или
недоступная переменная лучше правдоподобного неверного результата.
Карта оптимизированного кода не обещает отдельную инструкцию для каждого
оператора C и наличие всех переменных в любой момент исполнения.
Связанные файлы: [mame-autotest.md](mame-autotest.md),
[sprinter-cc](../bin/sprinter-cc), [app.mk](../app.mk),
[mame_interactive.py](../toolchain/mame_interactive.py),
[bank.s](../runtime/bank.s), [crt0_banked.s](../runtime/crt0_banked.s).
Локальная интеграция: `mame/sources/MAME/plugins/mamebridge/init.lua`,
`mame/sources/MAME/src/mame_mcp.py`.
## 2. Доказательства и пределы выводов
### 2.1 Результаты первоначальной разведки
Ниже сохранены результаты редакции 2026-09-13. При обновлении плана
сборочные эксперименты не повторялись. Перед реализацией их требуется
воспроизвести с сохранением репро, команд и версий.
| Эксперимент | Зафиксированный результат | Предел вывода |
|---|---|---|
| `sdcc --debug`, probe | `.exe` идентичен; `_CODE` 3354 → 3354 | Один пример, не гарантия для всех программ |
| `tests/banked`, huge, 3 TU, 2 банка | `.ihx` идентичен сборке без debug | rc=1 из-за C$; корректность карты не доказана |
| `.globl _dbg_x` и `_dbg_x:` в inline asm | Ошибка локальных меток `NNNNN$` в проверенных циклах/ветвлениях | Такую форму якоря не используем |
| `.globl _dbg_x` и `_dbg_x = .` | Релоцируемый символ; в probe байты идентичны, якорь у `ret` | Отдельно проверить влияние inline asm на оптимизацию |
| `;;SDBG x` | Код идентичен, адреса в `.map` нет | Для адреса нужен листинг/дополнительная привязка |
| SDCC 4.5.0 z80, `--out-fmt-elf` | Неизвестная опция | Готового ELF/DWARF этого target ожидать нельзя |
SDCC сам использует `sym = .`. Это подтверждает пригодность ассемблерной
формы, но не доказывает отсутствие влияния пользовательского inline asm
на оптимизатор C и peephole. Раздельно проверяем идентичность бинарника,
точность карты и скорость программы под debugger.
Примеры `.cdb` из первоначальной разведки:
```text
L:C$probe.c$18$3_0$48:8236 C-строка → адрес линковщика
L:A$probe$111:8236 строка .asm → адрес
L:G$main$0$0:8222 начало функции
L:Fprobe$counter$0_0$0:8E40 статик модуля
L:G$total$0_0$0:8E42 глобал
S:G$total$0_0$0({2}SI:S),E,0,0 описание типа
S:Lprobe.add$s$1_0$44({2}SI:S),R,0,0,[e,d] описание регистровой локальной
```
Парсер разбирает семейства `L:`, `S:`, `F:` и диагностирует неизвестные
записи. Построчная форма не делает семантику типов, scope, инлайнинга
и границ функций тривиальной. Семантику конечных адресов установить репро,
внутренние диапазоны нормализовать к `[start, end)`.
### 2.2 Проверено чтением текущих локальных исходников
- MAME поддерживает breakpoint с condition/action, printf/logerror/tracelog,
source/debugscript, comadd и debugger без окна (`-debugger none`).
`none` автоматически вызывает go() при остановке: он пригоден для
автономных logpoint actions, но не для ожидания интерактивных команд.
Подтверждено исходниками и живым репро; прототип использует osx debugger.
- `device_debug::compute_opcode_crc32` в `src/emu/debug/debugcpu.cpp`
считает CRC **одной инструкции**. Одинаковый `ret` по одному адресу
в двух банках имеет одинаковый ключ комментария. CRC не определяет банк.
- `execute_trace` в `src/emu/debug/debugcmd.cpp` включает трассировку CPU,
а не просто открывает файл для сообщений.
- Мост содержит `clog` и обслуживает `register_periodic` при stopped.
Комментарий у `do_step` прямо указывает: инструкция выполняется после
возврата callback, поэтому чтение PC сразу после step даст старый PC.
- `emu.symbol_table` в `luaengine_debug.cpp` создаёт отдельную таблицу;
готового symadd для консоли нет. C-имена разрешаем на своей стороне.
- В штатных debug views нет окна C-исходника. Для родного UI используем
комментарии, для полноценного исходника — IDE.
- sprinter-cc размещает банки по `(bank << 16) | 0xC000` в huge и
`(bank << 16) | 0x4000` в big. Есть `--bank-data`: банки содержат и данные.
- `_bank_pages[1..N]` заполняется crt0 при загрузке банков, а не до entry.
Драйвер экспортирует PG0..PG3 и другие состояния отображения.
- `bootstrap_r/w` в sprinter.cpp при обычном исполнении перенаправляют
логический адрес в `0x10000 | addr`; загрузочный режим отличается.
Адрес CPU и адрес пространства MAME нельзя смешивать.
### 2.3 Пакет доказательств этапа 0
Сохранить небольшие исходники-репро, команды, полные диагностики,
версии SDCC/ассемблера/линкера, commit MAME и патчи, хеши бинарников,
выбранные фрагменты `.asm/.map/.cdb`, протокол живой проверки MAME.
Большие артефакты допускается хранить вне Git с командой воспроизведения.
Ссылки на меняющиеся номера строк MAME заменять именами функций и
закреплённой ревизией в отчёте эксперимента.
## 3. Реестр решений: что заменено и почему
| ID | Риск / прежнее предложение | Решение и обоснование |
|---|---|---|
| R01 | Игнорировать rc=1 при C$, если есть `.ihx` | Устранить конфликт debug-имён; исправность кода не доказывает карту, старый `.ihx` может пережить ошибку |
| R02 | «Полная карта, ноль влияния» | Раздельные регрессии бинарника, карты и overhead; гарантия ограничена проверенными конфигурациями |
| R03 | Адрес — одно число, банки только у кода | Типизированные адреса и snapshot отображения, включая bank-data |
| R04 | Первый адрес строки / следующая строка вместо якоря | Все доказанные позиции, фактическое разрешение и unverified; другой путь исполнения не заменяет удалённую точку |
| R05 | Арминг по совпадению PC с entry | Проверка образа, готовности runtime/банков и выхода; DSS использует те же адреса |
| R06 | Цикл step внутри Lua | Асинхронный автомат с лимитами/отменой: шаг исполняется после callback |
| R07 | Разные ID файлов достаточно для нескольких клиентов | Общая сессия и арбитраж; ID не устраняют гонки run/stop |
| R08 | Безусловный `g` в логпоинте, clear-all | Реестр владельцев и диспетчер попаданий; логи не отменяют остановку |
| R09 | `trace` по умолчанию для логов | Отдельный журнал; instruction trace имеет другое назначение и стоимость |
| R10 | Формат C printf можно передать MAME | Ограниченная грамматика, типы, знак, длина строк и CP866; форматы различаются |
| R11 | CRC скрывает чужой банк | Сначала резидентные комментарии; затем обновление при remap или расширение ключа |
| R12 | Каталог сборки достаточен для attach | Manifest, хеши и фиксированный пакет сессии; исключить смешение сборок |
| R13 | Все DAP-запросы сразу, стек эвристикой | Честные capabilities и один достоверный frame в MVP; сложные функции отдельно |
| R14 | GDB не имеет логов/банков | Учесть dprintf/overlays; DAP выбран за модель Sprinter и общую сессию |
| R15 | Сокет автоматически ускорит шаг | Сначала измерить RTT и execution latency; callback сокетом не исправляется |
| R16 | Ручные правки в игнорируемом mame/ | Версионируемые исходники/патчи и установщик с проверкой расхождений |
| R17 | Библиотеки/локальные автоматически следуют из CDB | Эксперимент архивной линковки и доказанные location ranges |
| R18 | Один launch.json завершает интеграцию | Сборка, диагностики, язык, упаковка данных, запуск DSS и повторный F5 |
| R19 | debugger none держит stopped для IDE | В none wait_for_debugger вызывает go(); использовать родной debugger, для режима без окон реализовать отдельный backend ожидания |
| R20 | `debugger: "windows"` означает поддержку всей цепочки на Windows | Разделить MAME provider и host-инструменты: Windows provider существует, но Python/DAP launch/attach остаются неподдержанными до замены `fcntl`/Unix sockets, переноса launcher и живых тестов |
| R21 | Короткий sleep в `sdbg` достаточно для окна MAME | При stopped CPU обычный frame loop не обрабатывает события GUI, и macOS помечает приложение «не отвечает». В `wait_for_debugger` периодически вызывать event pump выбранного OSD (SDL3: `input_update` + `process_events`, native macOS: штатный poll), ограничив частоту; проверить Cmd-Tab/Dock при длительной остановке и сохранение DAP/клавиатурного ввода |
| R22 | CDB pointee type достаточен для выбора строки или скаляра | SDCC 4.5 кодирует проверенные `char *` и `uint8_t *` одинаково (`DG,SC:U`). В финальном debug-пакете сохранять исходный declared type/typedef chain, сверять его с CDB и размером; при неопределённости явно `unavailable` либо запросить типовую аннотацию. Отдельный fixture должен доказать, что `char *` читается как строка, а `int8_t *`/`uint8_t *` — как один 8-битный объект |
## 4. Архитектура, сборка и пакет
```text
sprinter-cc → .exe + отладочный пакет + manifest
sdbg: карта и типы
CLI ────────┐ │
MCP ────────┼──→ общая сессия отладки ──→ мост MAME ──→ CPU/debugger
VS Code/DAP ┘ состояние, точки,
события, загрузка
```
### 4.1 Ответственность компонентов
- `toolchain/sdbg.py` — публичный CLI; реализацию разделить на модули
пакета/парсера, адресов, типов, точек, сессии и транспорта по мере роста.
- Карта — чистые преобразования артефактов без команд MAME.
- Общая сессия — процесс для подключения CLI/MCP/DAP, хранит build ID,
generation, состояние, владельца управления и реестр точек.
- Мост — низкоуровневые действия, согласованные снимки, асинхронные
операции и события; второго парсера CDB в Lua нет.
- DAP/MCP — тонкие адаптеры; существующие input/screenshots сохраняются,
команды изменения исполнения проходят арбитраж.
- `.dbgs` — ограниченный автономный экспорт, не второй менеджер сессии.
Неподдержанную семантику экспорт отклоняет с объяснением.
### 4.2 Флаги и manifest
`--src-debug` включает debug для всех пользовательских TU;
повторяемый `--src-debug-file FILE` — для выбранных. Режимы взаимоисключающие,
неизвестный FILE — ошибка. Необязательный аргумент прежнего флага убран,
поскольку он неоднозначен рядом с позиционными `.c`.
`--debug` остаётся DEBUG_RT. В app.mk: `SRC_DEBUG := 1` либо
`SRC_DEBUG_FILES := a.c b.c`. Обычные сборки прежние; профиль IDE явно
включает карту, а не меняет defaults всего проекта.
Пакет в `.sprinter-cc-<name>/`: `.cdb/.map/.noi/.ihx/.asm`, нужные листинги,
manifest и индекс `.sdbg.json`. Конфигурация и содержимое входов входят
в ключ пересборки: смена debug/fast/safe/memory не оставляет stale artifacts.
Параллельные сборки одного output сериализуются или используют разные
каталоги. Пакет публикуется только после успешных проверок, атомарно.
Manifest: schema version, build ID, хеш `.exe` и артефактов, версии
инструментов, команды/флаги, библиотеки, TU и исходники/заголовки с хешами,
соответствующие `.asm`, entry, секции, окна, банки и полнота debug-покрытия.
Адрес `>=0x10000` не объявляется банком без проверки секции.
Неподдержанное размещение диагностируется, не угадывается по имени режима.
Пути относительно корня сборки, уникальный TU ID, поддержка одинаковых
basename и source path mapping при переносе проекта. IDE проверяет хеши;
при расхождении показывает stale source или сохранённый снимок через DAP
source. Сессия фиксирует пакет: новая сборка не меняет карту старого образа.
### 4.3 Ошибки CDB
Основное решение R01: проверить уникализацию отладочных имён по TU до
линковки в `.asm` с согласованным преобразованием всех связанных CDB-записей.
Если формат не позволяет надёжный внешний проход, подготовить патч SDCC.
Не изменять публичные C/asm-символы и инструкции. Выбор способа — результат
репро этапа 0, а не утверждение, что простое переименование уже достаточно.
До исправления допустим пер-модульный режим только при успешной линковке;
manifest перечисляет отсутствующие TU. Неполный выбранный набор отличается
от повреждённой карты. Повреждённая карта не публикуется.
Автоматического превращения rc=1 в успех по наличию `.ihx` не будет.
Неуспешные артефакты можно сохранять для исследования отдельно, но обычный
attach их не принимает. Полные диагностики сохраняются, включая pipefail.
### 4.4 Библиотеки
Проверить извлечение `S/F/L` и путей из архивов libc/libbgi. После успеха
добавить явный режим debug-артефактов библиотек с отдельным ключом кэша,
сохранив fast/safe и «одна функция — один модуль». Проверить DCE, типы,
строки, отсутствие коллизий и идентичность кода. До этого библиотечный
код доступен как asm/символы без выдуманных C-строк.
## 5. Адреса, исходники, переменные и выражения
### 5.1 Адресная модель
`CodeLocation/DataLocation`: build ID, секция, адрес линковщика, logical
CPU address, bank ID приложения при наличии, окно и offset.
Физическая страница — свойство сессии, не константа пакета.
`MappingSnapshot`: generation остановки, PC/SP, регистры, PG0..PG3,
остальные нужные биты отображения, готовность `_bank_pages`.
```text
line_locations(source_id, line, function_id=None) → список CodeLocation
resolve_pc(pc, mapping_snapshot) → SourceLocation | Unknown
asm_at(code_location) → номер/текст asm и происхождение
function_at(code_location) → функция | Unknown
resolve_symbol(name, module_id=None) → Symbol | Ambiguous | Unknown
read_variable(symbol, snapshot) → TypedValue | Unavailable
```
Backend различает logical CPU memory, пространство MAME и physical RAM.
Преобразование `0x10000 | addr` инкапсулировано в драйверном backend.
Bootstrap/configuration имеет отдельную семантику и не допускает обычный
attach приложения.
Банковая точка проверяет страницу окна против `_bank_pages[N]` после
подтверждения таблицы и резидентного контекста приложения. huge — W3,
big — W1. Проверить достаточность PGn с учётом CNF и других битов драйвера;
простое равенство — базовый случай, не доказанный полный контракт.
Manual-размещения допускаются по фактической карте и поддержке backend.
### 5.2 Разрешение строк
Хранить все позиции строки и диапазоны инструкции/функции/секции.
Breakpoint строки по умолчанию покрывает все доказанные позиции исполнения;
дополнительно можно выбрать функцию/экземпляр. Resolver возвращает реальные
адреса, фактическую строку и причины неоднозначности/переноса.
Для пустой или удалённой строки можно предложить ближайшую позицию в том
же контексте, но не подтверждать её молча как точное совпадение.
Без допустимого соответствия — unverified с объяснением.
addr2line не распространяет предыдущую метку через конец функции, дыру,
секцию или банк. Inline-экземпляры и неоднозначности сохраняются, вместо
выбора первого TU. Привязка точного макроса рассматривается отдельно (§7.2).
### 5.3 Типы, память и watchpoint
MVP: доказанные целочисленные типы, указатели, глобалы/статики. Одинаковые
имена требуют квалификации модулем. Затем массивы, структуры, битовые поля
и другие типы с fixtures. Размеры, знак, byte order и ABI задаёт target
SDCC, не хост. Неподдержанный тип отображается raw bytes с диагностикой.
Регистры/память/mapping читаются в одной остановке, пакетные запросы
привязаны к generation. После resume старые handles и snapshots недействительны.
Запись требует stopped, актуальную generation, проверку диапазона/типа
и известного отображения. Порты показывать по экспортированному состоянию;
отладочные чтения отключают side effects там, где backend это поддерживает.
Неотображённый банк читается через проверенный доступ к physical RAM без
переключения страниц приложения; до реализации — Unavailable. Один
16-битный указатель не содержит достаточного bank ID: его нельзя выдумывать.
Чтение/запись через границу окна обрабатывается явно.
Для watchpoint экспериментом установить пространство/alias реального пути
CPU и момент остановки до/после записи. PC-источник, старое и новое значение
показывать лишь при достоверном получении: текущий PC может отличаться от
адреса записавшей инструкции. Банковый watchpoint учитывает mapping;
до доказательства не объявлять его поддержанным.
### 5.4 Выражения
Один парсер для condition/logMessage/evaluate/watches: имена, integer
literals, `$`-регистры, ограниченные арифметические/битовые/сравнительные
операции. Документировать грамматику, приоритеты, знаковость и ошибки.
Поля/индексы/разыменование добавлять по поддержке типов. По умолчанию
evaluate без присваиваний, вызовов C и других побочных эффектов.
Raw MAME expressions/commands — отдельный явно обозначенный режим;
это не C. Изменяющие команды также требуют управления и синхронизации.
Команды генерировать из проверенного AST с экранированием строк/путей,
не конкатенацией произвольного текста в action.
## 6. Общая сессия и управление исполнением
### 6.1 Жизненный цикл
```text
disconnected → waiting_load → verifying_image → runtime_initializing
→ stopped ↔ running
→ exited
reset / state load / потеря связи → invalidated → повторная проверка
```
Совпадение PC с entry — только кандидат загрузки. Подтверждение сочетает
build ID выбранного файла, протокол запуска и сравнение неизменяемых
участков RAM по карте. Хеш `.exe` на хосте сам по себе RAM не проверяет.
Изменяемые crt0 данные/таблицы исключаются; сигнатуры и точки проверки
определяются для конкретного runtime.
На entry активируются только доказанные резидентные точки. Банковые —
после загрузки и проверки `_bank_pages`. На main runtime должен быть готов.
Отладка crt0 — отдельный режим с asm и постепенным появлением областей.
Частичная ошибка загрузки не переводит сессию в ready.
Выход через runtime/ESTEX и возврат в DSS деактивируют точки приложения.
Проверить обычный/аварийный выход и обход штатного exit. Если контекст
невозможно уверенно распознать — invalidated, а не продолжение со старой картой.
Reset/state load/restart сбрасывают mapping, temporary points, handles,
generation. Attach к работающей программе сначала останавливает CPU,
проверяет образ и runtime; неизвестную сборку не принимает молча.
### 6.2 Управление и реестр точек
Один клиент владеет run/step/write/input; остальные наблюдают согласованные
данные. Передача управления явная. Потеря клиента снимает lease и отменяет
его незавершённые операции согласно политике сессии. Screenshots доступны
наблюдателям; ввод влияет на приложение и арбитрируется. Изменения run/stop
из родного UI MAME отражаются событиями.
Реестр: logical breakpoint ID, MAME IDs, owner, build ID/generation,
вид (user/log/temporary/service), адреса/условия. Clear-all ограничен owner.
Повторная загрузка файла точек заменяет его набор, не дублирует его.
Совпавшие точки обслуживает диспетчер: вычислить условия, записать логи,
собрать причины остановки, продолжить только если их нет. User breakpoint,
pause и ошибка шага имеют приоритет над автоматическим `g`.
Изменения точек в родном UI требуют сверки; если backend не может
гарантировать совместное управление, сообщить конфликт.
### 6.3 Протокол и транспорт
Protocol version/capabilities, session ID, request ID, generation,
ответы и упорядоченные события stop/run/output/reset/exit/error.
Ответ на установку шага значит «принят», не «CPU уже шагнул».
Нужны timeout/cancel, snapshot/varbatch, EOF/disconnect и защита от повторного
исполнения: write/continue после timeout не повторяется вслепую.
Сначала измерить файловый IPC: RTT, pause→snapshot, step→stopped,
periodic при running/stopped. Файловый backend: отдельный каталог сессии,
один писатель backend, атомарная публикация запросов/ответов, очистка stale
и журнал событий с курсором. Разделение диапазонов ID независимых MCP
заменяется одним владельцем backend: оно не решало гонки состояния.
Сокет реализует тот же протокол при подтверждённом выигрыше. MAME имеет
пример `emu.file` socket в plugins/gdbstub, но неблокирующий ввод/вывод
при stop проверяется отдельно. Framing, частичные сообщения, лимит очереди,
reconnect, loopback по умолчанию обязательны. Callback не ждёт клиента
блокирующим чтением. Сокет не исправляет задержку исполнения инструкции.
### 6.4 Шаги
Сначала надёжный instruction step. Source-step — асинхронный автомат:
задать действие, вернуть управление MAME, дождаться фактической остановки,
получить snapshot, решить следующий шаг. Для скорости применить temporary
breakpoints на доказанных границах либо C++ hook, если Lua periodic
недостаточен по измерениям.
StepIn идёт к следующей доступной позиции исходника с заходом в вызов.
Next обходит вызов только при распознанной семантике; stepOut требует
достоверного контекста возврата. Сравнивать source/TU/function/bank и
исполняемую позицию, не один номер строки. Повтор строки в цикле не должен
вечно ждать смены номера.
Составной шаг имеет предел инструкций/wall time, отмену и итоговую причину.
Код без исходников, HALT, ISR, рекурсия, tail call, bank trampolines,
BIOS rst 08 и ESTEX rst 10 имеют явную политику. Для неизвестного случая
допустим отказ или переход к asm с объяснением; stepIn не выдаётся за next.
Системный код пропускается только при безопасном способе дождаться возврата.
## 7. Логи, макросы и расширенная диагностика
### 7.1 Внешние точки и журнал
Основной путь — версионируемый внешний файл: ID, source/function/location,
condition, hit condition, сообщение и выражения. Тот же resolver и реестр,
что у VS Code; пересборка не требуется. Адреса пересчитываются для новой
сборки из сохранённой исходной привязки. DAP logMessage переводится в эту
модель, не образует отдельный движок.
Журнал: sequence, build/session ID, host time, доступное emulated time,
PC, bank/page, location, tag и типизированные значения. JSONL — автотестам,
текст — CLI, DAP output — IDE, курсор — MCP. Ограничить объём/частоту,
предусмотреть ротацию или bounded buffer, счётчик потерь, flush на stop/exit.
clog остаётся для сообщений MAME, но хвост консоли не считается полным
надёжным журналом приложения.
Форматирование — задача финального этапа §10, а не свойство текущего
`{name}`. Целевой ограниченный набор включает decimal/hex, ширину,
символ и строку; MAME `%d/%x/%X/%c/%s/%%` может быть форматом совместимого
экспорта, но не передаётся напрямую как C-макрос. Принято это разделение,
потому что formatter обязан проверять тип/знак, размер, pointer против
массива, границы и кодировку, а CDB не отличает `char *` от `uint8_t *`
(R22). Для строк нужны bounded read до NUL и проверка CP866→UTF-8;
raw bytes имеют отдельное представление. Значения собираются до resume.
Горячие логи могут исполняться в мосте по скомпилированному описанию;
стоимость измеряется отдельно.
Console/logerror допустимы для совместимого экспорта. Trace/tracelog —
явный режим instruction tracing, не default для логов. Экспорт `.dbgs`
указывает ограничения и не ставит безусловный resume в общей сессии.
### 7.2 SDBG_LOG и SDBG_LOGIF
Практический синтаксис, типы, ограничения и примеры описаны в отдельном
[руководстве по SDBG_LOG](sdbg-log-macros.md). Этот документ — источник
истины для пользовательского контракта макросов: при изменении грамматики,
типов, регистров, чтения указателей или вывода в MAME/DAP обновлять его
вместе с кодом и проверками, затем синхронизировать краткие описания здесь.
Первый поддержанный вариант реализован в `<sdbg.h>`: `SDBG_LOG(tag,
"total={total}")` и `SDBG_LOGIF(tag, flag, "total={total}")`.
Обычная сборка получает `((void)0)`; выбранный `--src-debug` TU —
символ `sym = .` без инструкции. `tag` обязан быть уникальным в TU,
условие пока только имя поддержанной global/static переменной, а
`{name}` соответствует ограниченной грамматике DAP logMessage.
Сборщик извлекает только активные вызовы из отдельного препроцессорного
metadata-прохода, проверяет единственный asm-якорь и linked address.
Автоматически установленный logpoint пишет в журнал debugger MAME и DAP
`output`, затем продолжает CPU; совпавшая обычная точка сохраняет остановку.
Событийный буфер DAP ограничен 1024 записями и сообщает разрыв курсора с
числом пропусков; overhead горячих macro-logpoints ещё требуется измерить.
Десятичный/hex formatter и чтение типизированного значения по указателю
отложены до финального этапа §10: сначала нужны доказанные тип, контекст
банка, границы и безопасное чтение памяти, иначе лог может показать неверный
объект. Текущий `{name}` выводит десятичное число и никогда не разыменовывает
указатель.
Если якорь не совпал с началом доказанной инструкции, он помечен unverified
и не активируется. В проверенном fixture EXE совпал побайтово с обычной
сборкой; это доказательство конкретного случая, не гарантия для всех
оптимизаций SDCC и inline asm.
Макросы остаются расширением для авторских устойчивых точек. Без
anchors — `((void)0)`, с anchors — символ `sym = .` без инструкции.
Аргументы логирования не исполняются приложением и не имеют C-побочных
эффектов; это явно документируется, чтобы counter++ не считался кодом C.
Точная точка требует найденного и проверенного якоря. Без него — missing/
unverified; приблизительная привязка выбирается отдельно с показом
фактической позиции. Якорь задаёт машинную границу перед инструкцией,
но не гарантирует материализацию локальной или порядок всех вычислений C.
ID включает TU и tag. Повторные/inline экземпляры имеют отдельные ID либо
отклоняются до линковки; уникальный tag в тексте программы не предотвращает
повторное разворачивание inline.
Реализованный препроцессорный проход учитывает активные #if, include,
wrappers, многострочные вызовы, комментарии и склейку литералов; связывает
SDBG-описания с единственным реально собранным якорем. Текущая грамматика
намеренно отклоняет вычисляемый tag/condition, нестроковое сообщение и
повторные tag; поддержку сложных C-выражений и локальных добавлять только
после доказанного location range и ограниченного парсера.
Приёмка сравнивает полные бинарники с/без anchors и без макроса на циклах,
ветках, inline и multi-TU. Если оптимизация меняется, внешние логпоинты
остаются путём без пересборки, anchors обозначаются инструментированным
режимом с измеренной дельтой. Без доказательства «ноль влияния» не обещать.
При добавлении sdbg.h обновить публичный справочник API.
### 7.3 Комментарии, стек, локальные, покрытие
Резидентные comadd ставятся после проверки образа и обновляются при смене
сборки. Банковые включаются после поддержки remap-обновления с удалением
старых либо патча ключа `(address, page/context, opcode CRC)`.
Оффлайновый cmt допустим для проверенного резидентного образа; CRC из ihx
не решает банковые коллизии. Учитывать владение пользовательскими комментариями.
Backtrace сначала даёт текущий достоверный frame. Затем поддержать
распознанные прологи/эпилоги, SDCC __sdcccall(1), callee-pops, IX,
trampolines и ISR. Сканирование стека на похожие адреса — отдельная
маркированная эвристика, не основание для stepOut/локальных. Возможная
альтернатива — история call/return, но attach посреди исполнения не знает
прошлого, а нестандартные переходы требуют инвалидирования истории.
Локальные доступны лишь при доказанном location range (регистр/стек/память).
Лексический scope не равен live-range. Неизвестные/оптимизированные
значения — unavailable/optimized out. CFI/location lists из asm — отдельное
исследование, а не автоматически доступная возможность CDB.
Покрытие различает посещение адреса, число исполнений и время. Trackpc
сам по себе не даёт времени и требует проверки банковых коллизий.
Ключ покрытия включает build ID/bank/location, знаменатель — доказанные
исполняемые позиции. Профиль использует измеренный источник cycles/time
или sampling с указанной погрешностью и overhead.
## 8. MCP, DAP и полный цикл VS Code
### 8.1 CLI/MCP
Интерфейсы: attach/status/detach, where, disassemble_src, break_at,
clear_owned_breakpoints, read_var/write_var, watch_var, registers/memory,
step_instruction/step_in/next/step_out/pause/continue, logs с курсором,
console_log и загрузка набора точек. Возвращать build ID/generation,
фактическое разрешение и ограничения там, где они нужны для интерпретации.
Неподдержанное — явная ошибка. Старые низкоуровневые команды интегрируются
в арбитраж, а не обходят его.
### 8.2 DAP MVP и развитие
MVP: initialize, attach, configurationDone, disconnect, setBreakpoints,
setFunctionBreakpoints, threads, stackTrace, scopes, variables,
ограниченный evaluate, continue/pause, проверенный instruction step,
disassemble и logMessage/output. Один Z80 — один thread; stackTrace
сначала содержит один текущий frame. Source-step, если ещё не готов,
отвечает отказом с объяснением; instruction granularity включается
только при реализации.
Соблюдать initialize→initialized→configurationDone; stopped содержит
причину/ID точек, continued сообщает внешний resume, breakpoint — новое
разрешение. Terminated означает конец сессии; exited выдаётся только при
установленном завершении приложения и достоверном коде выхода.
SetBreakpoints заменяет набор данного source, включая очистку пустым
списком, а не добавляет точки бесконечно.
Capabilities отражают реальную поддержку. Расширения: breakpointLocations,
instruction/conditional/hit breakpoints, readMemory/writeMemory, setVariable,
dataBreakpointInfo вместе с setDataBreakpoints, source-step/next/stepOut,
cancel и сложные типы. Frame/variables references привязаны к остановке,
memory references различают банк/пространство. Предусмотреть пагинацию.
Stdout адаптера содержит только DAP, диагностики — stderr/log.
### 8.3 Разработка и запуск из редактора
**Архитектурное решение:** Sprinter-специфичный цикл реализуется собственным
расширением `toolchain/vscode-sprinter-debug`. Готовые C/C++ или clangd можно
использовать для подсветки, completion и навигации, а VS Code Tasks и Debug UI
— как стандартные интерфейсы. Они не знают ABI SDCC/Z80, пакет
`.sprinter-cc-*`, DSS, банковую адресацию и протокол MAME, поэтому не могут
заменить project extension и не считаются источником истины для диагностики
компилятора или отладки.
Мини-расширение уже содержит `contributes.debuggers`, точки для C, схему и
шаблоны конфигурации. Следующий уровень переносит в него build/run/debug
оркестрацию: `DebugConfigurationProvider` проверяет конфигурацию и при
необходимости запускает выбранную build task; команды расширения выбирают
target/profile/EXTRA_DATA; TaskProvider и problem matcher переводят ошибки
SDCC/линкера в Problems. Сборку всё равно выполняют `make`/`sprinter-cc`, а
запуск — общий Python launcher: расширение не дублирует их логику.
Установка через VSIX, разработка через Extension Development Host. Наличие
VS Code, pyenv Python и необязательного языкового расширения проверяется при
настройке, а не считается постоянным свойством конкретной машины.
Включить в поставку:
- TaskProvider и/или tasks.json: make/sprinter-cc и problem matcher
SDCC/ассемблера/линкера; debug не стартует после неуспешной сборки.
- Языковые настройки: include/defines/gfx/safe/memory из той же сборочной
конфигурации. SDCC-расширения вроде __naked требуют совместимых редакторских
определений; generic clang/GCC-анализ не равен полному анализу SDCC.
- launch.json: приложение/пакет, MAME/ROM/media, EXTRA_DATA, timeout,
stopOnEntry/stopOnMain, source mappings; без личных абсолютных путей.
- Launch: успешная сборка → упаковка → MAME → загрузка DSS → проверка образа
→ готовность runtime → main. Переиспользовать текущую упаковку/launcher,
не дублировать сценарии ввода клавиатуры.
- Attach к подготовленной сессии без второго MAME; restart с новым build ID
и пересчётом точек после новой сборки/загрузки.
- Disconnect не закрывает чужой MAME; terminate запущенного адаптером
процесса имеет явную политику. Выход приложения отличается от выхода
MAME. Ошибки сборки/ROM/media показываются в редакторе с причиной.
## 9. Альтернативный маршрут GDB
DAP выбран за прямую модель Sprinter, reuse сессии MCP и отсутствие
обязательного DWARF-писателя/target GDB. GDB полезен для его скриптов и
фронтендов; маршрут сохранён как самостоятельное расширение.
Первоначальная разведка: OSD gdbstub знает z84c015, но объявляет mame.z80
и другой порядок регистров; исследованный GDB ожидает org.gnu.gdb.z80.cpu
и набор с объединённым IR. Повторить проверку выбранной пары версий.
Lua gdbstub с i386-картой не является готовым backend Z80. Оценка патча
«20 строк» заменяется проверкой полного контракта.
1. Собрать/закрепить target GDB; проверить handshake. Патч feature/регистров
и IR проходит round-trip всех регистров, включая альтернативные,
byte order и семантику R.
2. Проверить RSP step/continue/interrupt, logical memory read/write,
break/watch и отсутствие патчинга инструкций. Разведка сообщала, что
Z0/Z2..Z4 используют точки MAME; включить это в регрессию.
3. sdbg_elf.py на общей карте: ELF32 EM_Z80, секции по реальному размещению,
symtab, debug_line, затем CU/subprogram/variable и базовые типы DWARF.
Дыры/банки не склеивать в ложный text. Сначала binutils/GDB offline,
затем live. ELF с symtab остаётся полезным самостоятельным экспортом.
4. debug_frame/CFI и locations — только по доказанным данным; строки DWARF
сами по себе не исправляют unwinder SDCC.
5. VS Code cppdbg/target GDB/app.elf — отдельная MI-конфигурация. Банки
сначала вручную/ограниченно; затем исследовать overlays и синхронизацию
с runtime, а не обещать автоматическую поддержку.
GDB имеет dprintf без пересборки и поддержку overlays. Редакторский
logMessage зависит от frontend; overlays требуют интеграции Sprinter.
Поэтому прежние «логпоинтов нет» и «банки невозможны» заменены конкретными
ограничениями. Области Registers/Ports также возможны поверх GDB,
но в DAP непосредственно используют нашу target-модель.
RSP и DAP не управляют исполнением независимо одновременно. Режим GDB
получает отдельного владельца; наблюдающие функции MCP проверяются отдельно.
Общую карту можно переиспользовать без совместного run/stop.
## 10. Этапы реализации и условия перехода
Календарные оценки прежнего плана считаются оценками демонстрационного
прототипа: надёжность lifecycle/банков/DAP ими не покрыта. После этапа 0
оценить каждый этап по репро и измерениям, раздельно прототип, поддержанный
выпуск и исследовательские функции. Проход этапа определяется приёмкой.
### Этап 0 — проверка предположений
Сохранить §2.3; воспроизвести общий inline в двух вызываемых TU и проверить
адреса обоих экземпляров/решение R01. Проверить anchors, huge/big/bank-data,
библиотечную CDB, memory aliases/watchpoint PC, lifecycle crt0, async step,
задержки моста и CRC банков. Закрепить версии и baseline производительности.
**Выход:** доказанный способ R01 либо явный ограниченный пер-модульный
режим; таблица проверенных/непроверенных возможностей. Общую карту не
объявлять готовой при скрытом конфликте.
### Этап 1 — сборка, manifest и карта
Флаги/app.mk, публикация пакета, исправление CDB, типизированные адреса,
resolver и CLI map/addr2line/line2addr/vars/asm. Debug-библиотеки добавляются
после успешного эксперимента. Обновить документацию сборки/mame-autotest.
**Выход:** fixtures, одинаковые basename, inline multi-TU, huge/big и
bank-data разрешаются достоверно; unknown явен. Полный cmp debug/non-debug;
make size-check без необъяснённой дельты. Baseline не обновляется ради
скрытия регрессии.
### Этап 2 — сессия и мост
Lifecycle/image verification, snapshot/generation, арбитраж, реестр точек,
событийный протокол. Сначала файловый backend/измерения, затем socket
по необходимости. Pause/continue/instruction step и ограниченный async
stepIn. Версионировать мост/патчи и установщик (§12). Новый самостоятельный плагин
можно загружать прямо из toolchain/mcp через pluginspath без копирования
в vendor. Для интерактивного режима без окон требуется решение R19.
**Выход:** нет ложных точек DSS, банки активируются после готовности,
exit/reset/reload инвалидируют состояние; потеря клиента не блокирует MAME,
конкурентные операции не читают смешанный snapshot.
### Этап 3 — полезная CLI/MCP-отладка и логи
Внешние точки/условия/логи, диспетчер совпадений, базовые типы, чтение/запись
резидентных объектов, доказанные watchpoint, where/disassemble_src/clog.
Интегрировать пакет/журнал в автотестовый launcher; проверить headless debugger.
**Выход:** значения совпадают с эталонными байтами, логи воспроизводимы
и ограничены, breakpoint не проглатывается логпоинтом, повторная загрузка
набора не дублирует точки.
### Этап 4 — VS Code MVP
DAP §8.2 и мини-расширение, сначала attach. Source/asm, break/logMessage,
globals/registers, pause/continue/instruction step. Source-step включить
после его приёмки, другие запросы не рекламировать заранее.
**Выход:** редакторская точка показывает фактическое разрешение,
остановка — верные строку/банк/значения; lifecycle DAP, удаление точек,
disconnect и stale references проверены протокольными тестами.
### Этап 5 — полный цикл разработки и расширенная отладка
Собственное расширение получает команды Build/Run/Debug, TaskProvider,
problem matcher, выбор target/profile/EXTRA_DATA и проверку инструментов.
Языковой сервис C/C++ либо clangd остаётся необязательной внешней
зависимостью с генерируемыми include/defines; его диагностика не подменяет
SDCC. Добавить launch/restart/stopOnMain и упаковку VSIX. Довести next,
доступные stepOut, memory/setVariable, data breakpoints, physical bank-data,
массивы/структуры и резидентные комментарии.
**Выход:** новый рабочий каталог проходит документированную настройку/F5
на обычной и банковой программе; restart не использует старую карту.
У каждой расширенной функции свой тест/capability; недоступная функция
не мешает использовать поддержанные.
### Этап 6 — якоря и исследовательские функции
Первый `SDBG_LOG` и ограниченный `SDBG_LOGIF` уже реализованы; продолжить
§7.2 проверкой сложных inline/оптимизаций и условных выражений только после
появления соответствующего evaluator. Независимые подэтапы: банковые комментарии,
доказанные stack frames/локальные, coverage/profiling. GDB §9 — отдельный
подэтап по потребности.
Финальные задачи для макросов, **без реализации в текущем этапе**:
- Добавить явные десятичные и hex-форматы для 8/16/32-битных signed/unsigned
объектов и адресов, с проверкой типа, знака, ширины и ограниченной общей
грамматикой для SDBG_LOG, внешних logpoints и DAP. Проверить значения на
реальных SDCC-сборках; не передавать Python format specifier в MAME `printf`.
- Для указателя выводить его собственное значение (логический/банковый адрес)
в decimal/hex и отдельно значение по адресу только если указатель не `NULL`.
`char *` трактовать как строку с bounded read до NUL и проверенной кодировкой;
`int8_t *`/`uint8_t *` — как один 8-битный signed/unsigned объект (для
16/32-битных typed pointers — соответствующий размер). Не выбирать режим
по одному CDB: `char *` и `uint8_t *` там неразличимы (R22); сохранить
объявленный тип/typedef chain из исходника и сверить с CDB. Чтение только
без side effects, после проверки размера, доступного банка, границ и image
identity; `NULL`, закрытая страница, отсутствие NUL в лимите и неизвестный
pointee дают явный `unavailable`/truncated, а не неверное значение.
**Выход:** доказательства и ограничения каждой функции опубликованы.
Эвристика не выдаётся за стек, anchors — за гарантированную идентичность,
посещения адресов — за время CPU.
## 11. Матрица приёмки и регрессий
| Сценарий | Проверяемое свойство | Этап / риски |
|---|---|---|
| hello/probe debug и обычный | Полный cmp exe, размеры, карта main | 01, R02 |
| Два TU с общим вызываемым inline | Нет коллизий, адреса/строки обоих экземпляров | 0–1, R01/R04 |
| Ошибка линковки при старом ihx | Ошибка сохранена, новый пакет не опубликован | 1, R01/R12 |
| Два utils.c и перенесённый проект | TU identity и source mapping | 1, R12 |
| Цикл/ветка/удалённая строка/много адресов | Все позиции или unverified, без ложного переноса | 1–4, R04 |
| Два банка с одинаковыми PC и ret | Верная строка/точка, нет чужого комментария | 0–6, R03/R11 |
| huge W3, big W1, поддержанные manual | Адреса/guards из фактической карты | 1–5, R03 |
| bank-data, невключённый банк, граница окна | Physical чтение или Unavailable | 15, R03 |
| Watchpoint через CPU/alias | Адрес, момент и PC-источник корректны | 0–5, R03 |
| Entry до банков, ошибка loader | Нет ранней активации/ложного ready | 2, R05 |
| DSS/exit/restart/reset/state load | Нет чужих попаданий и stale handles | 25, R05/R12 |
| Изменённый source или чужой exe | Явное расхождение карты | 1–5, R12 |
| Log + user + temporary по одному PC | Лог есть, остановка сохранена | 2–4, R08 |
| Два клиента, ручной resume, reconnect | Арбитраж/события, нет повторных mutations | 24, R07/R15 |
| Одна строка в цикле, HALT/ISR/рекурсия/trampoline | Ограниченный отменяемый шаг | 2–5, R06/R13 |
| Signed 8/16, pointer/array, CP866, нет NUL | Знак/тип/кодировка, bounded read | 35, R10 |
| Горячий лог, переполнение, выход | Overhead, счётчик потерь, flush | 3, R09 |
| Debug libc/libbgi fast/safe, DCE | Строки/типы, нет роста кода | 0–1, R17 |
| Anchors с #if/include/wrappers/inline | Метаданные совпадают, unsupported отклонён | 6, R02/R04 |
| DAP replace/empty/events/references | Протокол и честные capabilities | 4, R13 |
| F5 с данными, ошибка ROM/build, restart | Полный цикл и диагностика | 5, R18 |
| MAME update с ручными изменениями | Установщик не затирает расхождение | 2, R16 |
Тесты карты/протокола — небольшие fixtures без MAME; CPU/банки/lifecycle —
живые интеграционные сценарии. Golden-карта сверяется независимо с
листингом, байтами и фактическими остановками, не только выводом парсера.
Tests/hello и tests/banked — стартовые кандидаты, не вся приёмка.
Замеры: без debugger, debugger без точек, resident/bank breakpoints,
горячий лог, instruction trace, transport RTT, end-to-end step.
Записывать хост/версию, emulation speed, объём журнала, latency p50/p95.
Пороги зафиксировать после baseline этапа 0 до выбора backend;
«21 МГц терпимо» не критерий приёмки.
## 12. Размещение и воспроизводимость
Исходники моста/MCP — `toolchain/mcp/`, патчи MAME —
`toolchain/mame-patches/`; патч SDCC при необходимости отдельно с версией.
`toolchain/install-mame-bridge.sh` проверяет upstream revision/хеши,
показывает diff при расхождении и не затирает неизвестные ручные изменения.
Повторная установка идемпотентна; protocol version проверяется handshake.
Самостоятельный sdbgbridge загружается прямо из toolchain/mcp через
pluginspath: это устраняет необходимость копировать его в vendor и риск
потери изменений. Установщик остаётся нужен для патчей существующего
MAME/моста, если они потребуются. Игнорируемое mame/ не источник истины. Документировать сборку/применение
патчей, проверку установленного бинарника и откат. Не хранить личные пути,
ROM и большие образы в исходниках расширения.
Defaults вместо прежних открытых вопросов: source debug явный/профиль IDE;
CDB errors не подавляются; отдельный журнал; общий ограниченный язык;
DAP — основной IDE-путь; одна сессия владеет backend; socket по измерениям;
debug-библиотеки после эксперимента. Технически открыты: способ R01,
наблюдение загрузки/выхода, полная mapping-формула, physical RAM и стоимость
hooks. У каждого — репро этапа 0 и условие допуска, а не молчаливое
предположение следующих этапов.
## 13. Внешние спецификации
Локальные версии исходников и репро первичны для конкретной сборки.
Online-документация задаёт общую семантику; при реализации фиксировать
использованную версию.
- [DAP specification](https://microsoft.github.io/debug-adapter-protocol/specification): запросы, события, capabilities и references.
- [DAP specification source](https://github.com/microsoft/debug-adapter-protocol/blob/main/specification.md): полный контракт.
- [VS Code debugger extension](https://code.visualstudio.com/api/extension-guides/debugger-extension): регистрация/упаковка адаптера.
- [MAME general commands](https://docs.mamedev.org/debugger/general.html): printf/tracelog/source/trackpc.
- [MAME execution commands](https://docs.mamedev.org/debugger/execution.html): шаги и трассировка.
- [MAME Lua debugger classes](https://docs.mamedev.org/luascript/ref-debugger.html): низкоуровневый API.
- [GDB Dynamic Printf](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Dynamic-Printf.html): логирование без пересборки.
- [GDB Overlays](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Overlays.html): перекрывающиеся размещения и target-интеграция.
+204
View File
@@ -0,0 +1,204 @@
# Логи C-приложения через `SDBG_LOG`
Этот документ описывает **работающий контракт** макросов из `<sdbg.h>`.
Они ставят авторские logpoints в MAME debugger без вызовов DSS/BIOS и без
кода печати в приложении. Сообщение попадает в Debug Console VS Code и в
журнал debugger MAME. Отдельное окно MAME debugger видно при launch с
`"debugger": "osx"`; режим `sdbg` оставляет окно Sprinter и журнал MAME,
но не открывает штатное debugger-окно.
## Быстрый пример
```c
#include <stdint.h>
#include <sdbg.h>
volatile uint16_t frame_no;
volatile uint8_t ready;
void draw_frame(void)
{
++frame_no;
SDBG_LOG(frame_counter, "frame={frame_no}");
SDBG_LOGIF(frame_ready, ready, "ready={ready}, frame={frame_no}");
}
```
Соберите приложение с `make SRC_DEBUG=1` либо запустите F5 в VS Code:
расширение выполняет debug-сборку перед запуском. Для прямого вызова
обёртки подходит `bin/sprinter-cc --src-debug ...`; в режиме
`--src-debug-file FILE` якоря создаются только в выбранных единицах
трансляции. После загрузки DSS launcher останавливается в `main`, проверяет
образ, и session server активирует макросные точки. При попадании он читает
поддержанные значения, выводит текст и продолжает CPU. Если на том же
адресе есть обычный breakpoint, сообщение печатается, а CPU остаётся
остановленным.
В обычной сборке оба макроса раскрываются в `((void)0)`: без source-debug
сессии они сами ничего не печатают. Строка сообщения не хранится в EXE;
в проверенных обычном и банковом fixture бинарники с макросом и без него
побайтово совпали. Inline asm может влиять на оптимизацию в других случаях,
поэтому одинаковый размер/код каждой программы следует проверять отдельно.
## Параметры и место вызова
`SDBG_LOG(tag, message)` принимает два аргумента.
| Параметр | Что передать | Ограничение |
|---|---|---|
| `tag` | Имя точки, например `frame_counter` | C-идентификатор `[A-Za-z_][A-Za-z_0-9]*`, **без кавычек**, уникальный внутри `.c`/TU |
| `message` | Строковый литерал C, например `"frame={frame_no}"` | От 1 до 1024 символов после декодирования, поддержана склейка соседних литералов |
Одинаковый `tag` в разных `.c` допустим: сборщик добавляет к символу
уникальный ID TU. Повторный `tag` в одном TU, в том числе из повторно
развёрнутого inline-макроса, отклоняется при сборке. Вычисляемый tag,
строка вместо tag и автоматический `__LINE__` пока не поддержаны.
Используйте название, которое остаётся понятным после правки строк файла.
`SDBG_LOGIF(tag, condition, message)` принимает третий смысловой компонент:
между tag и сообщением указывается **одно имя** поддержанной global/static
переменной. Если её значение при попадании равно нулю, запись пропускается,
CPU продолжается. Ненулевое значение включает запись. Например:
```c
SDBG_LOGIF(after_load, ready, "ready={ready}");
```
`ready != 0`, `!ready`, вызов функции, локальная переменная и регистр CPU
как `condition` сейчас не поддержаны. Условие не исполняется кодом C:
отладчик читает значение при остановке. Если переменная недоступна, запись
пропускается и отладчик один раз сообщает причину. Значения с побочными
эффектами (`counter++`, вызовы функций) нельзя использовать и в шаблоне:
аргументы макроса не вычисляются на Sprinter.
Якорь привязан к текущему адресу ассемблера без инструкции. Он обозначает
машинную границу рядом с вызовом макроса, а не обещает точный порядок всех
выражений C после оптимизации. Ставьте вызов отдельным statement после
интересующего действия и проверяйте фактический адрес/значение на нужной
сборке. Если адрес не совпал с началом доказанной инструкции, точка
получает статус `unverified` и не активируется. Макрос в неактивном `#if`
не создаёт точку; wrappers, многострочные вызовы и склейка литералов
обрабатываются активным препроцессорным проходом.
## Синтаксис сообщения сейчас
После обработки C-escape-последовательностей шаблон состоит из литералов
и подстановок `{name}`. `name` — имя **одной** доступной переменной;
значение выводится десятичным числом. Например:
```c
SDBG_LOG(progress, "step={step}, total={total}");
SDBG_LOG(braces, "literal {{value}}; actual={total}");
SDBG_LOG(multiline, "step={step}, " "total={total}");
```
`{{` и `}}` дают буквальные `{` и `}`. `%` сейчас обычный символ: `%d`,
`%x` и `%s` **не являются** форматами макроса. Синтаксис `{name:04X}`,
`{name!r}`, `{name+1}`, индексы массивов и разыменование указателя
отклоняются во время debug-сборки, до запуска MAME. Не добавляйте префикс
`0x` перед `{name}`: значение пока десятичное, и результат будет неверно
выглядеть как шестнадцатеричный.
Подстановки разрешаются в контексте TU, где расположен макрос. Можно
прочитать единственную поддержанную global или file-static переменную этого
TU. Если имя не найдено/неоднозначно, тип неподдержан или физический банк
сейчас не отображён, вместо значения выводится `<unavailable: причина>`.
Локальные/параметры функции не имеют доказанных location ranges и пока
не читаются. Resident-страница, закрытая банком, тоже недоступна.
Текст DAP сохраняет символы UTF-8. Перед отправкой в MAME debugger `printf`
кавычки заменяются апострофами, а управляющие символы — пробелами;
проценты и обратные слэши экранируются. Очень длинный сформированный текст
может не пройти ограничение MAME console 2048 байт, но DAP output остаётся.
При частом попадании CPU останавливается каждый раз, поэтому эмуляция может
замедлиться. DAP хранит до 1024 событий и показывает число пропусков,
если клиент отстал; скорость горячих logpoints ещё не измерена.
## Какие значения можно подставить
Источник истины — тип и размер глобального/file-static объекта в debug-карте
SDCC. Доступны целые скаляры размером 1, 2 или 4 байта, а также проверенный
обычный 16-битный указатель. Значение читается из памяти без side effects.
Ниже приведены результаты реальной debug-сборки SDCC 4.5 для этих типов.
| C-тип объекта | Сейчас в `{name}` | Что остаётся недоступным |
|---|---|---|
| `uint8_t` / `unsigned char` | Десятичное 0…255 | Hex/битовая маска |
| `int8_t` / `signed char` | Десятичное −128…127 | Принудительный unsigned/hex |
| `uint16_t` / `unsigned int` | Десятичное 0…65535 | Hex с 4 цифрами |
| `int16_t` / `int` | Десятичное со знаком | Иной числовой формат |
| `uint32_t` / `unsigned long` | Десятичное 0…4294967295 | Hex с 8 цифрами |
| `int32_t` / `long` | Десятичное со знаком | Иной числовой формат |
| `char` | **Числовой код** байта согласно signedness SDCC; в проверенной сборке `char` был unsigned | Вывод как символ, CP866→UTF-8 |
| `char *` | **Числовой 16-битный адрес** указателя | По принятому для финального API правилу это строка, а не один `char`: bounded read до NUL, границы, кодировка |
| `int8_t *` / `uint8_t *` | **Числовой 16-битный адрес** указателя | По финальному API — один знаковый/беззнаковый 8-битный объект по ненулевому адресу, decimal/hex |
| `char[]`, другие массивы, struct/union, float/double | Не поддержаны | Элементы/поля/значение объекта |
| Локальные и параметры функции | Не поддержаны | Нужны доказанные регистр/стек и live-range |
| Регистры CPU (`PC`, `HL`, `DE`, `AF`, `PG0`…) | Не являются подстановками макроса | Они видны в scope Registers VS Code и MAME debugger, но `{PC}` сейчас не читает регистр |
Для `char *` вывод адреса **не доказывает**, что память по нему доступна.
Сам массив `char[]` сейчас не выводится даже если его размер 1/2/4 байта.
Числовые коды `char` не декодируются как символы Sprinter. Поддержка
конкретного объекта зависит и от того, есть ли он в карте: оптимизированное
или неразрешённое объявление может быть недоступно.
Тип указателя нельзя выбирать только по текущему CDB: SDCC 4.5 записал
проверенные `char *` и `uint8_t *` одинаково как `DG,SC:U` (2 байта),
а `int8_t *` как `DG,SC:S`. Для финального вывода строки или скаляра нужны
метаданные **исходного объявленного типа**, сохранённые в debug-пакете и
сверенные с CDB/размером. Неоднозначные typedef/объявления должны давать
`unavailable` либо требовать явную аннотацию формата, а не угадывать по CDB.
## Форматирование, которое рассматривается
Эта таблица описывает **предложение для следующей версии**, а не действующий
синтаксис. В текущей версии все записи справа вызовут ошибку сборки.
| Предлагаемый шаблон | Назначение | Что нужно реализовать и проверить |
|---|---|---|
| `{u8:02X}`, `{u16:04X}`, `{u32:08X}` | Hex с шириной по типу | Ограниченный formatter, signed/unsigned и ширина 8/16/32 бит |
| `{value:d}`, `{value:x}`, `{value:X}` | Одну переменную можно вывести в десятичном и hex виде в одном сообщении | Проверка типа/знака, 8/16/32-битной ширины и недопущение произвольных Python format specifier |
| `{letter:c}` | Символ из `char` | CP866→UTF-8, различие числового кода и отображения символа |
| `{ptr:p}` | Значение самого указателя: логический/банковый адрес | Типизированный адрес, явный `NULL`, различие числового decimal/hex отображения |
| `{p8:*d}`, `{p8:*x}` **условно** | Один 8-битный scalar по `int8_t *`/`uint8_t *` при ненулевом адресе; decimal/hex, окончательный синтаксис ещё не утверждён | Исходный тип и signedness, side-effect-free чтение одного байта, границы/банк; `NULL`/закрытая страница → `unavailable` |
| `{text:s}` | Строка по `char *` при ненулевом адресе, а не один `char`; прямой `char[]` — отдельный случай | Сохранить declared type, bounded read до NUL, доступный банк, отсутствие NUL/кодировка/лимит вывода |
| `{reg:PC}` | Значение регистра CPU | Отдельное пространство имён, чтобы не спутать регистр с C-global `PC` |
Форматирование должно использовать один и тот же ограниченный движок для
C-макросов, внешних logpoints и DAP output. До реализации предпочтительнее
выводить десятичное значение и читать hex/регистры в штатных views debugger,
чем имитировать `printf` в тексте сообщения.
Обе задачи — decimal/hex вывод и чтение значения по указателю — записаны
в финальный [этап 6 плана](mame-source-debug.md).
Сейчас `{ptr}` показывает только десятичное значение адреса; разыменование
не выполняется даже при ненулевом указателе. В финальном API `char *`
обозначает строку, а указатель на один 8-битный объект записывается как
`int8_t *` или `uint8_t *`. Адрес любого указателя должен выводиться
отдельно от значения по адресу. Для строки нужна отдельная проверка NUL,
лимита и кодировки (CP866→UTF-8 либо явное представление raw bytes).
Целевой результат можно представить как `text=<адрес>, value=<строка>` для
`char *` и `p8=<адрес>, value=<одно 8-битное число>` для `uint8_t *`;
показанные выше `{text:s}`/`{p8:*d}` ещё нельзя вставлять в рабочий C-код.
## Если лог не появился
| Симптом | Причина и действие |
|---|---|
| Нет лога после обычной сборки | Макрос пуст; запустите `SRC_DEBUG=1`/F5 и проверьте, что TU выбран для карты |
| Сборка сообщает о повторном `tag` | Дайте каждой точке отдельный идентификатор; повторные inline-развёртки в одном TU требуют отдельного решения |
| Сборка сообщает «только подстановки» | Уберите `:04X`, `%d` не подставляет значение; используйте `{name}` |
| `unverified`/«не активирован» в Debug Console | Якорь не совпал с исполняемой инструкцией; переместите макрос к доказанной границе и пересоберите |
| `<unavailable: …>` | Проверьте global/static тип и отображение страницы банка; локальные пока не поддерживаются |
| Нет сообщения в окне MAME при `sdbg` | Штатное debugger-окно в этом режиме скрыто; Debug Console VS Code работает, для окна выберите `osx` |
| При частом логе программа заметно медленнее | Каждое попадание останавливает CPU; уменьшите частоту или используйте условный макрос |
## Актуализация контракта
При изменении `<sdbg.h>`, парсера/форматтера шаблона, набора читаемых типов,
источников значений, условий, поведения breakpoint или маршрутов MAME/DAP
сначала обновляйте этот документ и проверяемые примеры. Затем синхронизируйте
краткое описание в `docs/mame-source-debug.md`, `docs/vscode-sprinter-debug.md`,
`docs/mame-source-debug-status.md` и `docs/libc-reference.md`. Реальные
проверки: `tests/sdbg/test_sdbg.py`, `test_server.py`, `test_dap.py` и живой
`tests/sdbg/run_macro_log_probe.py` для `sdbg`/`osx`.
+272
View File
@@ -0,0 +1,272 @@
# Source-debug Sprinter в VS Code
Статус: MVP для разработки самого отладчика. Поддержаны автоматический launch,
ручной attach, точки по строкам и функциям, logpoints, один C-frame,
регистры, простые global/static, continue/pause, instruction step и шаги
по исходнику F10/F11/Shift+F11.
> [!WARNING]
> **Windows пока не поддерживает полный цикл отладки.** Значение
> `debugger: "windows"` включает только штатное окно debugger MAME.
> Текущие launcher, owner lock и DAP/session transport используют Unix
> shell, `fcntl` и Unix domain sockets. Поэтому native Windows launch/attach
> из VS Code пока не считается рабочим. Поддержка потребует отдельного
> Windows transport и end-to-end проверки.
## Какие расширения нужны
Для build/run/debug используется собственное расширение этого проекта —
`toolchain/vscode-sprinter-debug`. Только оно знает формат source-debug
пакета, загрузку приложения через DSS, банки Sprinter и DAP-сессию MAME.
Microsoft C/C++ или clangd можно поставить дополнительно ради completion,
переходов по исходникам и подсветки. Оба анализируют код как близкий к
обычному C и не являются точной моделью SDCC: параметры `__naked`, ABI и
часть target-заголовков потребуют отдельных defines/configuration. Ошибки
реальной сборки всегда определяет `sprinter-cc`.
## Подготовка программы
Соберите приложение с полной картой:
```sh
pyenv exec make -C tests/hello SRC_DEBUG=1
```
Рядом с EXE появится `.sprinter-cc-hello/manifest.json`. При изменении C,
заголовка или опций `make` пересоберёт пакет по хэшу содержимого.
## Загрузка development-расширения
Из корня репозитория откройте VS Code с распакованным расширением:
```sh
code --extensionDevelopmentPath="$PWD/toolchain/vscode-sprinter-debug" "$PWD"
```
Если команда `code` не добавлена в `PATH`, на macOS этого проекта доступен
полный путь:
```sh
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" \
--new-window \
--extensionDevelopmentPath="$PWD/toolchain/vscode-sprinter-debug" \
"$PWD"
```
Расширение в режиме `auto` использует `~/.pyenv/shims/python` при наличии
local `.python-version`; для внешнего workspace ищет установленный
`~/.pyenv/versions/3.12*/bin/python`, затем `.venv/bin/python` и известные
абсолютные пути Python 3.12. Local-версия передаётся задаче сборки через
`PYENV_VERSION`, даже если приложение лежит вне workspace. Это не зависит от `PATH` процесса VS Code,
который мог быть открыт из Dock. Выбор можно переопределить абсолютным путём
в `sprinterDebugger.pythonCommand`; дополнительные аргументы задаются через
`sprinterDebugger.pythonArguments`.
## Автоматический launch
Добавьте в локальный `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "sprinter-mame",
"request": "launch",
"name": "Sprinter MAME: hello",
"build": "${workspaceFolder}/tests/hello/.sprinter-cc-hello"
}
]
}
```
Launcher создаёт временные floppy/HDD/state-каталоги и видимое окно MAME.
В отдельный `cfg/sprinter.cfg` он записывает только включение обеих клавиатур
Sprinter: `:` и `:kbd:ms_naturl`. MAME по умолчанию активирует лишь первую,
поэтому без этой настройки физические клавиши не доходят до DSS `getchar()`.
Действующая пользовательская конфигурация MAME при этом не копируется.
Он читает символы текстового экрана из VRAM, находит пустой prompt `X:…>` и
требует, чтобы тот оставался готовым 0,25 секунды. Только затем ставится
service-точка `main` и вводится `a:\\HELLO.EXE`. После совпадения PC и
сигнатуры кода запускается session server, DAP принимает редакторские точки,
а VS Code показывает остановку на entry. Завершение debug session останавливает
только созданные ей MAME/server.
`dssTimeout` задаёт предельное время ожидания prompt (30 эмулируемых секунд).
`launchAt` можно задать как необязательную нижнюю границу времени запуска;
наличие prompt всё равно обязательно. Перед вводом сохраняется диагностический
снимок DSS.
Дополнительные файлы на floppy задаются массивом `data`, путь к другому MAME —
полем `mame`.
## Ручная проверка VS Code
В репозитории локально подготовлены два профиля `.vscode/launch.json`:
`Sprinter: hello (VS Code)` и `Sprinter: hello (VS Code + MAME debugger)`.
Файл `.vscode` намеренно игнорируется Git и не содержит личных путей.
1. Соберите `hello` командой из раздела «Подготовка программы» и запустите
Extension Development Host одной из команд выше.
2. Откройте `tests/hello/hello.c` и поставьте breakpoint на строке 31,
вызове `puts`.
3. В Run and Debug выберите `Sprinter: hello (VS Code)` и нажмите F5.
Расширение сначала выполнит `make SRC_DEBUG=1` в `tests/hello` как задачу
Sprinter Build. После успешной сборки должно появиться окно Sprinter;
launcher дождётся prompt DSS, введёт
`A:\\HELLO.EXE` и VS Code остановится в `main`.
4. Проверьте Call Stack, scope Registers и Debug Console. После Continue
должна сработать подтверждённая точка строки 31 по адресу `0x824b`.
5. На остановке проверьте F11 и F10. Курсор должен переходить только после
фактической остановки CPU, а не сразу после отправки команды. На строке 62
(`getchar`) нажмите F10, щёлкните окно Sprinter MAME и нажмите латинскую `x`:
выполнение должно перейти на строку 63. Пока программа ждёт клавишу,
кнопка Pause в VS Code должна останавливать CPU.
6. Завершите сессию кнопкой Stop. Затем повторите профиль с `osx`: вместе с
тем же VS Code-сеансом должно открыться штатное Cocoa-окно debugger MAME.
Команда палитры `Sprinter: Build Active Project` собирает приложение по
Makefile открытого C-файла. Задачи `Sprinter: Build ...` доступны и в
`Tasks: Run Task`. Для launch автоматическая сборка включена по умолчанию,
если из `build` можно найти Makefile с `app.mk`; нестандартный каталог задаётся
полем `project`, а уже собранный пакет запускается с `"autoBuild": false`.
Ошибка SDCC с файлом и строкой видна в Problems; ненулевой код задачи
останавливает F5 до запуска MAME. Явный `preLaunchTask` использует стандартное
поведение VS Code и отключает автоматическую сборку расширения.
Если F5 сообщает, что тип `sprinter-mame` неизвестен, окно запущено без
`--extensionDevelopmentPath`. Сообщение `Couldn't find a debug adapter
descriptor` означает, что extension manifest загружен, но расширение не
активировалось; после изменения `package.json` выполните `Developer: Reload
Window`. Канал Output → `Sprinter MAME Debug` показывает выбранные Python и
DAP. При раннем завершении MAME ответ launch включает последние строки
MAME log. Если сборочный пакет устарел, адаптер должен отказать до запуска
программы и показать причину, а не использовать старую карту.
Логи из исходника без вывода в DSS задаются в C через `<sdbg.h>`;
полный синтаксис и таблица поддержанных типов — в
[руководстве по SDBG_LOG](sdbg-log-macros.md):
```c
#include <sdbg.h>
volatile int total;
/* tag уникален в этом .c; total читает debugger, не приложение. */
SDBG_LOG(after_increment, "total={total}");
```
При F5 debug-сборка привяжет нулевой asm-якорь к адресу и session server
поставит logpoint после загрузки DSS. Попадание напишет `total=...` в
Debug Console VS Code и в debugger console MAME, затем продолжит выполнение.
В обычной сборке макрос пуст. Поддерживаются только global/static переменные
доказанного типа, литералы и `{name}`; локальные, выражения и `%d` пока нет.
`SDBG_LOGIF(tag, flag, "...")` выводит сообщение, если поддержанная
global/static `flag` ненулевая. Аргументы не исполняются на Sprinter, поэтому
выражения с побочными эффектами не имеют здесь смысла. В режиме `sdbg`
журнал MAME существует, но его штатное окно не открывается; для видимого
окна выберите `osx` ниже. Горячий logpoint может заметно замедлить эмуляцию:
CPU останавливается на каждом попадании. Если DAP не успевает забрать
события из ограниченного журнала, Debug Console сообщает число пропусков.
Текст в VS Code сохраняется точно; MAME console заменяет двойные кавычки
апострофами и управляющие символы пробелами из-за синтаксиса `printf`.
## Родной debugger MAME вместе с VS Code
По умолчанию используется project backend `sdbg`: видимо окно Sprinter,
а отдельное Cocoa-окно debugger не создаётся. Для регистров, дизассемблера,
памяти и console штатного MAME добавьте в launch-конфигурацию:
```json
"debugger": "osx"
```
Bridge и DAP продолжают работать параллельно. Ручные `go`, изменение точек и
reset в native console обходят модель состояния VS Code; reset пока нельзя
использовать из-за lifecycle-crash MAME 0.287.
Для MAME без project patch выберите штатный provider:
| Платформа/сборка | `debugger` |
|---|---|
| macOS | `osx` |
| Windows native | `windows` |
| Linux с Qt debugger | `qt` |
| Linux/SDL без Qt | `imgui` |
| Автоматический выбор доступного provider | `auto` |
`imgui` использует основное графическое окно MAME; launcher уже запускает его
с `-video soft -window`. `none` немедленно продолжает остановленный CPU, а
`gdbstub` ждёт протокол GDB, поэтому они не подходят для sdbgbridge.
Текущая host-часть sdbg использует `fcntl` и Unix domain sockets. Она работает
на macOS/Linux. Для native Windows нужны отдельные реализации owner lock,
локального RPC и launcher; выбор `windows` решает только сторону MAME и не
обеспечивает работу VS Code-интеграции.
Backend `sdbg` входит как воспроизводимый patch к MAME 0.287. Для локального
checkout достаточно:
```sh
make mame-sdbg
```
Команда идемпотентно применяет
`toolchain/mame-patches/0001-sdbg-debugger-backend.patch`, инкрементально
собирает MAME и устанавливает `mame/v306/mame.arm`.
## Logpoints
Обычная команда VS Code **Add Logpoint…** работает без пересборки. В сообщении
разрешены литералы и простые подстановки:
```text
frame={frame_counter} state={state}
```
Подстановка принимает только имя поддержанной переменной. Выражения,
format specifier и conversion отклоняются. Если logpoint и обычная точка
разрешились в один адрес, сообщение печатается, а CPU остаётся остановленным.
## Шаги по исходнику
F11 выполняет Z80-инструкции до следующей отличающейся C-позиции. F10 на
каждом машинном шаге использует MAME `over`, поэтому банковский вызов проходит
целиком. Shift+F11 сначала делает MAME `out`, затем проходит служебный bank
trampoline до первой C-позиции вызывающей функции. Instruction granularity
остаётся доступна для точного одиночного шага.
Автомат ограничен 512 машинными операциями. Команда шага отвечает VS Code
сразу; пока `getchar()` или другой вызов ждёт внешнего ввода, MAME продолжает
работать, а пользователь может нажать клавишу в его окне либо выполнить Pause
в VS Code. Время ожидания ввода не ограничивается таймаутом исходного шага.
При достижении предела CPU остаётся остановленным, а Debug Console получает
сообщение.
Пользовательская точка внутри шага имеет приоритет; logpoint печатается и шаг
продолжается с прежней семантикой.
## Ручной attach
Для общего MAME между CLI и VS Code сначала запустите `sdbg_server.py`, затем:
```json
{
"type": "sprinter-mame",
"request": "attach",
"name": "Sprinter MAME: Attach",
"socket": "/tmp/sprinter-sdbg.sock"
}
```
Команды server и диагностического CLI приведены в
[mame-source-debug-status.md](mame-source-debug-status.md).
## Ограничения MVP
- Reset/restart отключён после найденного crash MAME 0.287 на старом Lua
callback. Session завершается штатным terminate.
- Локальные, backtrace, setVariable, watchpoints и чтение неотображённого
bank-data ещё не реализованы.
- Если одна инструкция имеет несколько C-маркеров, frame помечается
`[ambiguous]`; адаптер не выбирает строку молча.