libc/crt0: возврат из main завершает программу по-настоящему

Два бага одного пути завершения, оба видны только на железе.

1. ЗВУК ПРОДОЛЖАЛСЯ ПОСЛЕ ВЫХОДА.  cbl_close() закрывает СЕССИЮ, но не
гасит железо: bit7 порта 0x004E держит gfx_wait_vsync ради бита луча,
поэтому порт оставался включённым ("немой" режим), и CBL крутил свои 256
слотов уже под шеллом — тихо ровно до первой чужой записи в порт данных,
а дальше она зацикливалась.  Новый cbl_shutdown() гасит bit7 независимо
от держателей и центрует ЦАП обычного COVOX; pop_shutdown зовёт его
последним действием, а _cbl_open_raw регистрирует в atexit его, а не
cbl_close.

2. ЦЕПОЧКА atexit НЕ ВЫПОЛНЯЛАСЬ ПРИ ВОЗВРАТЕ ИЗ main.  crt0 уходил прямо
в ESTEX EXIT, то есть нарушал контракт C (возврат из main = exit(status)).
Молча терялись не только гашение звука и снятие vsync-ссылки, но и
_fclosall: буферизованная запись в файлы пропадала, если программа не
звала exit() явно.  Теперь crt0 после main дёргает _atexit_hook.

Косвенность обязательна: прямая ссылка crt0 на разматыватель притащила бы
его и стек хендлеров в КАЖДУЮ программу.  Указатель живёт в отдельном
data-модуле (два байта _DATA, ни байта кода), ставит его сам atexit() при
первой регистрации — нет регистраций, нет и кода.  Тот же приём, что у
_irq_cbl_hook.

Цена замерена: +14 Б всем программам (блок в crt0) и +64 Б тем
одиннадцати, что реально регистрируют хендлеры (CBL, файловые через
_fclosall, irqtest, gfx_dbuf, solidt) — у них раньше эти хендлеры были
мёртвым кодом.  Эталоны обновлены (кроме atlas: его +434 Б не отсюда,
замерен тот же и без этих правок).

Проверено в MAME: старт и звук как были, выход по F10 возвращает в шелл
чисто (текстовый режим восстановлен, зависания нет), повторный запуск
работает.  Пункт 1 проверяется только на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
This commit is contained in:
2026-09-02 14:15:48 +03:00
parent 31d0075090
commit 8490288d79
12 changed files with 203 additions and 58 deletions
+38
View File
@@ -0,0 +1,38 @@
/*
* cbl_shutdown — ЖЁСТКО погасить звук перед возвратом в шелл.
*
* ЧЕМ ОТЛИЧАЕТСЯ ОТ cbl_close(). Тот закрывает СЕССИЮ: снимает насос и
* пересчитывает порт по текущим держателям. А держатель обычно есть —
* gfx_wait_vsync() занимает bit7 навсегда ради бита луча (см. _cbl_port.c),
* — и порт остаётся включённым в "немом" режиме. Для работающей программы
* это правильно, но при выходе означает, что железо продолжает крутить свои
* 256 слотов уже под шеллом: тихо ровно до первой чужой записи в порт
* данных, а дальше эта запись зацикливается навсегда.
*
* ПОЧЕМУ НЕЛЬЗЯ ПОЛОЖИТЬСЯ НА atexit. Цепочку atexit разматывает только
* exit() (libc/sys/exit.c), а crt0 при ВОЗВРАТЕ ИЗ main уходит прямо в
* ESTEX EXIT (runtime/crt0_banked.s) — ни один хендлер не выполняется.
* Значит и снятие vsync-ссылки, и cbl_close из atexit при обычном
* `return 0;` из main не происходят. Поэтому выключение должно быть
* ЯВНЫМ действием программы, а не следствием чужой уборки.
*
* Что делает: закрывает сессию (если была), обнуляет счётчик держателей —
* программа уходит, держать порт больше некому, — гасит bit7 и центрует
* ЦАП обычного COVOX (при выключенном CBL записи в порт данных идут прямо
* в него, см. _cbl_prime.c), чтобы вместо щелчка осталась ровная тишина.
*
* Идемпотентно; звать последним действием завершения.
*/
#include "_cbl.h"
#include "../irq/_irq.h" /* IRQ_DISABLE/IRQ_ENABLE */
void cbl_shutdown(void)
{
cbl_close(); /* снять насос и отпустить IM2-таблицу */
IRQ_DISABLE();
_cbl_port_refs = 0; /* держателей больше нет: программа уходит */
_cbl_ctrl(0); /* bit7 = 0 — CBL выключен целиком */
_cbl_prime(0x80); /* ...и ЦАП COVOX в центре, без щелчка */
IRQ_ENABLE();
}