Files
Sprinter-SDCC/applications/PoP/docs/resident_budget.md
T
snark13 90255737c2 Откат bank-data для pop_trob: указатель на его статику уходил наружу
Симптом: через несколько комнат живого прохода перезагружался DSS.

pop_trob_modif() возвращает указатель на room_modif[24][30], а зовут её из
банков 2, 3, 7 и резидента.  После переноса массив лежит по 0xC000+ в
странице банка 6, но разыменовывает указатель ЧУЖОЙ код — когда замаплена
его собственная страница.  Значит чтение и запись идут поверх кода соседнего
банка.

Анализ Def/Ref такое не ловит: снаружи нет ссылки на символ, есть ссылка на
функцию, которая отдаёт его адрес.  Условий для кандидата два, и второе
проверяется только чтением кода — ни один указатель на статику не должен
уходить наружу.

pop_room оба условия проходит (_mobs не читает никто; bake_copy статическая;
atlas_load(&pop_env[i]) берёт адрес глобала из резидентного pop_tile.c).
Остаётся −1620 Б: данные в W2 6774 -> 5157, свободно 129 -> 1746 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:21:44 +03:00

7.5 KiB
Raw Blame History

Бюджет резидента W1/W2 и как его освобождать

Резидент huge-режима — это окно 0x4100..0xBB00 (стек с 0xBB00): код в W1, данные в W2, между концом данных и стеком остаётся куча. Всё, что туда не влезло, обязано жить в банках.

Как СМОТРЕТЬ, а не гадать

Атрибуция по карте (roomtest.map) ВРЁТ: file-static SDCC в карту не попадает, и «дырка» между двумя именованными символами приписывается предыдущему модулю целиком. На этом легко получить «у pop_bg 1529 байт данных», когда на самом деле у него 289.

Правильный источник — объектные файлы: в .rel есть строки A <area> size <n> flags <f> с ТОЧНЫМ размером каждой области модуля.

# по каждому .rel: _DATA/_BSS/_INITIALIZED = резидентные данные модуля,
#                  _BANKn                  = его код в банке
re.match(r'A\s+(\S+)\s+size\s+([0-9A-Fa-f]+)\s+flags', line)

Кто на кого ссылается — оттуда же: S <sym> Def... — модуль определяет символ, S <sym> Ref... — ссылается на него.

Замер 2026-08-20 (до правки)

Данные банковых модулей, лежавшие в РЕЗИДЕНТЕ:

модуль данные в W2 разделяемые символы
pop_room (банк 7) 1620 нет (_mobs не читает никто)
pop_trob (банк 6) 929 НЕТ ВООБЩЕ
pop_bg (банк 2) 289 pop_bg_obj_col/row читает pop_room
pop_map (банк 3) 227 hitp_*, pop_upside*, pop_loose_modif, … — читают 6 модулей
pop_cdraw (банк 4) 175 pop_cd читают 4 модуля

Что сделано: данные банка — в страницу банка

sprinter-cc --bank-data=SRC (повторяемый) кладёт писучие данные ОДНОГО банкового модуля в его же 16-КБ страницу. Выборочность принципиальна: глобал, который читает соседний банк или резидент, обязан остаться замапленным всегда, иначе чтение придёт из чужой страницы.

Включено для pop_room.c. Результат:

было стало
данные в W2 6774 5157
свободно до стека 129 1746
BANK7 12684 14301 (87 %)

Проверено в MAME: уровень 1, комнаты 1 и 2 — фон, факелы, решётки, проваливающиеся полы и переход между комнатами работают.

ГЛАВНАЯ ЛОВУШКА: указатель на банковую статику

Первый заход включал ещё и pop_trob.c — по Def/Ref он выглядел ИДЕАЛЬНЫМ кандидатом (экспортируемых данных нет вообще). Через несколько комнат живого прохода перезагрузился DSS.

Причина: pop_trob_modif() ВОЗВРАЩАЕТ УКАЗАТЕЛЬ на room_modif[24][30], а зовут её из банков 2, 3, 7 и резидента. После переноса массив лежит по 0xC000+ в странице банка 6, но разыменовывает указатель чужой код — когда замаплена ЕГО страница. То есть чтение и запись идут по тем же смещениям поверх кода соседнего банка. Отсюда и уход системы.

Анализ Def/Ref этого не видит: снаружи нет ссылки на символ, есть ссылка на функцию, которая отдаёт его адрес. Поэтому условий для кандидата ДВА, и второе проверяется только чтением кода:

  1. ни один чужой модуль не Ref его глобалы (по .rel);
  2. ни один указатель на его статику не уходит наружу — ни возвратом, ни через out-параметр, ни записью в общий глобал.

У pop_room второе выполнено: единственные &-выражения наружу — atlas_load(&pop_env[i]), но pop_env определён в pop_tile.c, то есть в резиденте; bake_copy статическая.

Внутри своей страницы указатель безопасен всегда: любой вызов резидентной функции, которая перемапливает W3 (atlas_load, pop_page_read), восстанавливает окно перед возвратом.

Два условия, без которых это молча ломается

  1. Нулевая инициализация. crt0 зануляет только резидентный _DATA, до банковых страниц он не дотягивается, а mkexe по умолчанию забивает пустоты образа 0xFF. По правилам проекта file-scope переменные никто не инициализирует явно (crt0 же зануляет) — значит вся банковая статика поднялась бы с 0xFF. Поэтому sprinter-cc при любом --bank-data САМ добавляет mkexe -p 0.
  2. _INITIALIZED остаётся в резиденте. --dataseg переименовывает только _DATA; инициализированные переменные и их _INITIALIZER живут как раньше, и gsinit копирует их без участия банков. У pop_room таких 3 байта — они и остались в W2.

Что осталось на столе

  • malloc287 Б резидента, который никто не зовёт: его тянет cbl_open, где malloc/free стоят в мёртвой ветке CBL_UNDERRUN_SILENCE. Вынести аллокацию буфера тишины в отдельный модуль libc — и линкер выбросит malloc у всех приложений на CBL_UNDERRUN_APP.
  • pop_bg 289 Б — уедут, если вынести pop_bg_obj_col/row (2 байта!) в общий резидентный модуль состояния.
  • pop_tile5972 Б кода в резиденте, самый крупный жилец. Он там потому, что его листья зовут обе половины слоя фона из РАЗНЫХ банков. Резать имеет смысл только по этому признаку: что зовут из одного банка — туда и переносить.
  • BANK7 после правки на 87 %: следующий кандидат на разгрузку — pop_redraw.c (981 Б) в другой банк.