207 lines
19 KiB
Markdown
207 lines
19 KiB
Markdown
# Логи 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-окно. Подключённый
|
||
[MCP-адаптер C-уровня](sdbg-mcp.md) читает те же записи `output` через
|
||
`recent_events(after=...)`; он не ставит второй набор макросных точек.
|
||
|
||
## Быстрый пример
|
||
|
||
```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`.
|