Files
2026-09-17 10:23:18 +03:00

19 KiB
Raw Permalink Blame History

Логи 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-уровня читает те же записи output через recent_events(after=...); он не ставит второй набор макросных точек. Маршрут DAP проверен и с новым MAME.HT/sprinter 0.289 в режимах sdbg и osx; выбор бинарника описан в руководстве VS Code.

Быстрый пример

#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 продолжается. Ненулевое значение включает запись. Например:

SDBG_LOGIF(after_load, ready, "ready={ready}");

ready != 0, !ready, вызов функции, локальная переменная и регистр CPU как condition сейчас не поддержаны. Условие не исполняется кодом C: отладчик читает значение при остановке. Если переменная недоступна, запись пропускается и отладчик один раз сообщает причину. Значения с побочными эффектами (counter++, вызовы функций) нельзя использовать и в шаблоне: аргументы макроса не вычисляются на Sprinter.

Якорь привязан к текущему адресу ассемблера без инструкции. Он обозначает машинную границу рядом с вызовом макроса, а не обещает точный порядок всех выражений C после оптимизации. Ставьте вызов отдельным statement после интересующего действия и проверяйте фактический адрес/значение на нужной сборке. Если адрес не совпал с началом доказанной инструкции, точка получает статус unverified и не активируется. Макрос в неактивном #if не создаёт точку; wrappers, многострочные вызовы и склейка литералов обрабатываются активным препроцессорным проходом.

Синтаксис сообщения сейчас

После обработки C-escape-последовательностей шаблон состоит из литералов и подстановок {name}. name — имя одной доступной переменной; значение выводится десятичным числом. Например:

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 плана. Сейчас {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.