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:
@@ -25,6 +25,7 @@
|
||||
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
||||
#include "pop_shadow.h" /* запечённый набор Тени */
|
||||
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
||||
#include <cbl.h> /* cbl_shutdown — погасить железо звука на выходе */
|
||||
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
|
||||
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
||||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||||
@@ -1490,4 +1491,14 @@ void pop_shutdown(void) __banked
|
||||
pop_guard_free();
|
||||
pop_kid_data_free();
|
||||
pop_level_free();
|
||||
/* ЖЕЛЕЗО ЗВУКА ГАСИМ ПОСЛЕДНИМ ДЕЙСТВИЕМ, и pop_sfx_close выше для этого
|
||||
* НЕ ДОСТАТОЧНО: он закрывает сессию, а bit7 порта CBL остаётся включён,
|
||||
* пока его держит gfx_wait_vsync ради бита луча. Дальше main просто
|
||||
* возвращается, а crt0 при возврате уходит прямо в ESTEX EXIT, минуя
|
||||
* цепочку atexit (runtime/crt0_banked.s) — то есть vsync-ссылку снять
|
||||
* уже некому. В результате мы уходили в шелл с включённым CBL, и он
|
||||
* крутил свои 256 слотов: тихо до первой чужой записи в порт данных, а
|
||||
* дальше она зацикливалась (поймано пользователем на железе 2026-09-02;
|
||||
* в MAME не воспроизводится). */
|
||||
cbl_shutdown();
|
||||
}
|
||||
|
||||
+45
-45
@@ -1,67 +1,67 @@
|
||||
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
|
||||
accfill 3783
|
||||
accfill 3797
|
||||
accop 7067
|
||||
argv 3431
|
||||
assrtest 3847
|
||||
argv 3445
|
||||
assrtest 3861
|
||||
atlas 9193
|
||||
attrprob 4087
|
||||
banked 1056
|
||||
bankedbg 1067
|
||||
banklocl 4683
|
||||
banktest 3767
|
||||
bgi_img 8677
|
||||
bgitest 3748
|
||||
bios_text 4461
|
||||
attrprob 4101
|
||||
banked 1070
|
||||
bankedbg 1081
|
||||
banklocl 4697
|
||||
banktest 3781
|
||||
bgi_img 8691
|
||||
bgitest 3762
|
||||
bios_text 4475
|
||||
blitperf 5860
|
||||
blitw 4939
|
||||
cat 913
|
||||
cblstream 5862
|
||||
cbltest 6008
|
||||
cblwav 6101
|
||||
conio 4605
|
||||
conio2 3929
|
||||
cat 927
|
||||
cblstream 5944
|
||||
cbltest 6090
|
||||
cblwav 6183
|
||||
conio 4619
|
||||
conio2 3943
|
||||
convbench 3508
|
||||
dec_test 860
|
||||
errno 5966
|
||||
fbench 8085
|
||||
fdmax 6023
|
||||
filetest 10547
|
||||
dec_test 874
|
||||
errno 5980
|
||||
fbench 8149
|
||||
fdmax 6087
|
||||
filetest 10611
|
||||
fpsdiv 4870
|
||||
gets 509
|
||||
gfx_dbuf 5142
|
||||
gfx_demo 4114
|
||||
gets 523
|
||||
gfx_dbuf 5207
|
||||
gfx_demo 4128
|
||||
gfxbanks 6044
|
||||
hello 4167
|
||||
hello2 4244
|
||||
irqtest 6690
|
||||
hello 4181
|
||||
hello2 4258
|
||||
irqtest 6754
|
||||
kbdpoll 1282
|
||||
kbdraw 4911
|
||||
ls 4835
|
||||
malloc 4461
|
||||
mem_test 4555
|
||||
mouse 4382
|
||||
openenv 6124
|
||||
ls 4849
|
||||
malloc 4475
|
||||
mem_test 4569
|
||||
mouse 4396
|
||||
openenv 6138
|
||||
pageflip 6334
|
||||
palfile 5406
|
||||
ptime 5744
|
||||
rt_test 5197
|
||||
ptime 5758
|
||||
rt_test 5216
|
||||
scroll 6744
|
||||
seek 4188
|
||||
simple 955
|
||||
solidt 11531
|
||||
seek 4202
|
||||
simple 969
|
||||
solidt 11595
|
||||
spranim 9431
|
||||
spriteclip 4119
|
||||
sprites 6791
|
||||
stattest 7549
|
||||
stdlib 6643
|
||||
stest2 3644
|
||||
strtest 1340
|
||||
text_palette 5024
|
||||
timedir 5454
|
||||
stattest 7563
|
||||
stdlib 6657
|
||||
stest2 3658
|
||||
strtest 1354
|
||||
text_palette 5038
|
||||
timedir 5468
|
||||
w0page 8831
|
||||
w3bgfx 5243
|
||||
w3big 3673
|
||||
w3huge 3700
|
||||
w3probe 3556
|
||||
w3tiny 3471
|
||||
winrest 4459
|
||||
winrest 4473
|
||||
|
||||
|
@@ -61,7 +61,12 @@ int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||
static uint8_t atexit_armed;
|
||||
if (!atexit_armed) {
|
||||
atexit_armed = 1;
|
||||
atexit(cbl_close); /* CBL обязан умолкнуть до шелла */
|
||||
/* ИМЕННО shutdown, а не close: close оставляет bit7 включённым,
|
||||
* пока его держит gfx_wait_vsync, и железо крутит буфер уже под
|
||||
* шеллом. Подстраховка неполная — цепочку atexit разматывает
|
||||
* только exit(), а crt0 при возврате из main уходит прямо в ESTEX
|
||||
* EXIT, — поэтому программа обязана звать cbl_shutdown() сама. */
|
||||
atexit(cbl_shutdown); /* CBL обязан умолкнуть до шелла */
|
||||
}
|
||||
|
||||
IRQ_DISABLE();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
+13
-3
@@ -55,9 +55,14 @@
|
||||
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
|
||||
* поведение не меняет.
|
||||
*
|
||||
* cbl_close() обязателен (atexit подстрахует) — иначе CBL продолжит
|
||||
* прерывать шелл после выхода; заодно освобождает буфер тишины, если
|
||||
* он был аллоцирован.
|
||||
* cbl_close() обязателен — иначе CBL продолжит прерывать шелл после
|
||||
* выхода. НО ОН НЕ ГАСИТ ЖЕЛЕЗО ПОЛНОСТЬЮ: пока bit7 держит
|
||||
* gfx_wait_vsync() ради бита луча, порт остаётся включённым в "немом"
|
||||
* режиме, и буфер крутится под шеллом. Перед возвратом из main звать
|
||||
* cbl_shutdown() — он выключает всё и центрует ЦАП.
|
||||
*
|
||||
* На atexit тут полагаться НЕЛЬЗЯ: цепочку разматывает только exit(), а
|
||||
* crt0 при возврате из main уходит прямо в ESTEX EXIT.
|
||||
*
|
||||
* Требования как у <irq.h>: код/данные в W2 (tiny/big).
|
||||
*/
|
||||
@@ -67,6 +72,11 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Выключить звук ПОЛНОСТЬЮ перед завершением программы: закрыть сессию,
|
||||
* снять bit7 независимо от держателей и оставить ЦАП в центре. Звать
|
||||
* последним действием завершения; идемпотентно. */
|
||||
void cbl_shutdown(void);
|
||||
|
||||
/* Коды частоты дискретизации (bits 3..0 порта 0x4E; из форума
|
||||
* Sprinter Team; коды 0/1 — legacy, не использовать). */
|
||||
#define CBL_FREQ_7K8 0x8 /* 7.8125 кГц */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
.module crt0
|
||||
.globl _main
|
||||
.globl __atexit_hook
|
||||
|
||||
;; Linker-emitted symbols (resolved at link time).
|
||||
.globl s__INITIALIZER
|
||||
@@ -88,6 +89,22 @@ _start::
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; Returning from main is exit(status) per the C standard, so the
|
||||
;; atexit chain must run here too. The call is INDIRECT on purpose:
|
||||
;; atexit() fills __atexit_hook on its first registration, so programs
|
||||
;; that never register anything keep it NULL and link neither the
|
||||
;; unwinder nor the handler stack (see libc/sys/_atexit_hook.c).
|
||||
push de ; DE = int return of main
|
||||
ld hl, (__atexit_hook)
|
||||
ld a, h
|
||||
or a, l
|
||||
jr Z, atexit_done
|
||||
ld de, #atexit_done
|
||||
push de ; return address: Z80 has no `call (hl)`
|
||||
jp (hl)
|
||||
atexit_done:
|
||||
pop de
|
||||
|
||||
;; SDCC's int return → DE. Low byte is the exit code.
|
||||
ld a, e
|
||||
ld b, a
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
.module crt0_banked
|
||||
.globl _main
|
||||
.globl __atexit_hook
|
||||
.globl _n_banks
|
||||
.globl _bank_pages
|
||||
|
||||
@@ -218,6 +219,22 @@ skip_bank_load:
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; Returning from main is exit(status) per the C standard, so the
|
||||
;; atexit chain must run here too. The call is INDIRECT on purpose:
|
||||
;; atexit() fills __atexit_hook on its first registration, so programs
|
||||
;; that never register anything keep it NULL and link neither the
|
||||
;; unwinder nor the handler stack (see libc/sys/_atexit_hook.c).
|
||||
push de ; DE = int return of main
|
||||
ld hl, (__atexit_hook)
|
||||
ld a, h
|
||||
or a, l
|
||||
jr Z, atexit_done
|
||||
ld de, #atexit_done
|
||||
push de ; return address: Z80 has no `call (hl)`
|
||||
jp (hl)
|
||||
atexit_done:
|
||||
pop de
|
||||
|
||||
;; main returned: int return is in DE per SDCC 4.5 __sdcccall(1).
|
||||
ld a, e
|
||||
jr exit_with_a
|
||||
|
||||
Reference in New Issue
Block a user