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
+7
View File
@@ -10,4 +10,11 @@
extern void (*_atexit_stack[ATEXIT_MAX])(void);
extern int _atexit_top;
/* Разматыватель цепочки (LIFO) — общее тело exit() и возврата из main. */
void _atexit_run(void);
/* Указатель на него для crt0; ставит atexit() при первой регистрации.
* Подробности и мотивация косвенности — в шапке _atexit_hook.c. */
extern void (*_atexit_hook)(void);
#endif
+18
View File
@@ -0,0 +1,18 @@
/*
* _atexit_hook — указатель на разматыватель цепочки atexit; его дёргает
* crt0 при ВОЗВРАТЕ ИЗ main.
*
* ЗАЧЕМ КОСВЕННОСТЬ. По стандарту C возврат из main эквивалентен
* exit(status), то есть цепочка atexit обязана размотаться. Но прямая
* ссылка crt0 на разматыватель притащила бы в КАЖДУЮ программу и его, и
* весь стек хендлеров — включая те, что atexit() не зовут вовсе. Поэтому
* crt0 ссылается на этот модуль (два байта _DATA, ни байта кода), а
* заполняет указатель сам atexit() при первой регистрации: нет
* регистраций — указатель нулевой, разматыватель в программу не приезжает,
* crt0 просто уходит в ESTEX EXIT как раньше. Тот же приём, что у
* _irq_cbl_hook в libc/irq.
*/
#include "_atexit.h"
void (*_atexit_hook)(void);
+22
View File
@@ -0,0 +1,22 @@
/*
* _atexit_run — размотать цепочку atexit-хендлеров (LIFO).
*
* Общее тело для двух входов: явного exit() и возврата из main (там его
* зовёт crt0 через _atexit_hook). Отдельным модулем — чтобы программа,
* не зарегистрировавшая ни одного хендлера, не получала этот код вовсе.
*
* Вершина сбрасывается ПЕРЕД вызовом каждого хендлера: если хендлер сам
* позовёт exit(), цепочка не пойдёт по второму кругу.
*/
#include "_atexit.h"
void _atexit_run(void)
{
while (_atexit_top > 0) {
void (*fn)(void) = _atexit_stack[--_atexit_top];
if (fn) {
fn();
}
}
}
+8 -3
View File
@@ -3,9 +3,11 @@
* завершении (exit()), максимум 8, порядок LIFO. Возвращает 0 или -1
* при переполнении стека хендлеров.
*
* Внимание: возврат из main() без явного exit() уходит в inline RST
* 10h #41 в crt0.s и цепочку atexit НЕ выполняет — программам с
* хендлерами нужно завершаться через exit().
* ВОЗВРАТ ИЗ main ТОЖЕ РАЗМАТЫВАЕТ ЦЕПОЧКУ (с 2026-09-02), как и требует
* стандарт C. Раньше crt0 уходил из main прямо в ESTEX EXIT, и хендлеры
* не выполнялись: программа теряла и сброс файловых буферов (_fclosall), и
* гашение звука, если не звала exit() явно. Теперь первая регистрация
* ставит _atexit_hook, а crt0 его дёргает — см. _atexit_hook.c.
*/
#include <stdlib.h>
@@ -17,5 +19,8 @@ int atexit(void (*fn)(void))
return -1;
}
_atexit_stack[_atexit_top++] = fn;
/* Хендлеры появились — значит crt0 обязан размотать цепочку и при
* простом `return` из main. Ставим каждый раз: проверка дороже. */
_atexit_hook = _atexit_run;
return 0;
}
+1 -6
View File
@@ -9,11 +9,6 @@
void exit(int code)
{
while (_atexit_top > 0) {
void (*fn)(void) = _atexit_stack[--_atexit_top];
if (fn) {
fn();
}
}
_atexit_run();
_exit(code);
}