From 8610a8c1786fcf7adbc02c9dbf4c9325fa5e1d25 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 2 Sep 2026 14:59:26 +0300 Subject: [PATCH] =?UTF-8?q?toolchain:=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B0=D1=82=D1=8C,=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B3=D0=B4=D0=B0=20ISR-=D1=81=D1=82=D0=B0=D0=B1=20W0-?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=20=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=D1=81=D1=8F=20=D0=B2=20W1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правка пользователя, разобранная по диффу и закоммиченная как есть. Программа может временно маппить свою EMM-страницу в окно 0 (атласы спрайтов, gfx_w0_map). Пока страница там, любое прерывание уходит на #0038 ЭТОЙ страницы, поэтому gfx_w0_page_prepare прошивает туда переход на _gfx_w0_isr. Сам стаб живёт в _CODE, а в режимах small и huge _CODE начинается с 0x4100 — то есть попадает в W1, окно, которое DSS перемаплет на время СВОИХ вызовов. Прерывание в этот момент уходит по адресу, которого сейчас нет: W1 читается как #FF. Ловится тяжело: собирается молча, проявляется недетерминированно — зависанием примерно на каждом третьем холодном старте. Поймано вживую 2026-09-01. check_w0_isr.py берёт адрес __gfx_w0_isr из .map: символа нет — W0-путь не слинкован, молчим; адрес >= 0x8000 — он в W2, всё хорошо; иначе WARNING. Сборку не валит намеренно: свои страницы в W0 кладут не все. Вызов добавлен в app.mk рядом с check_bank_calls. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4 --- app.mk | 10 ++++- toolchain/check_w0_isr.py | 83 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100755 toolchain/check_w0_isr.py diff --git a/app.mk b/app.mk index 7cfbde5..fcba1ef 100644 --- a/app.mk +++ b/app.mk @@ -92,11 +92,19 @@ RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s) # bankN_*.asm; обычным небанковым программам ничего не стоит. BANK_CHECK := $(PROJ_ROOT)/toolchain/check_bank_calls.py +# ПРОВЕРКА ISR-СТАБА W0-СТРАНИЦ — там же, по свежей карте. Ловит стаб +# _gfx_w0_isr, оставшийся в W1: программа, кладущая свои страницы в W0 +# (атласы спрайтов, gfx_w0_map), получает недетерминированные зависания, +# когда прерывание приходит во время вызова DSS и W1 перемаплен. Только +# предупреждение: страницы в W0 кладут не все. Разбор — в шапке скрипта. +W0ISR_CHECK := $(PROJ_ROOT)/toolchain/check_w0_isr.py + $(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS) $(if $(strip $(BUILD_DIR)),@mkdir -p $(dir $@)) $(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES) @d=$(dir $@).sprinter-cc-$(EXAMPLE); \ - if ls $$d/bank*_*.asm >/dev/null 2>&1; then python3 $(BANK_CHECK) $$d; fi + if ls $$d/bank*_*.asm >/dev/null 2>&1; then python3 $(BANK_CHECK) $$d; fi; \ + if ls $$d/*.map >/dev/null 2>&1; then python3 $(W0ISR_CHECK) $$d; fi $(MKEXE): $(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe diff --git a/toolchain/check_w0_isr.py b/toolchain/check_w0_isr.py new file mode 100755 index 0000000..cf37256 --- /dev/null +++ b/toolchain/check_w0_isr.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +check_w0_isr.py — предупредить, что ISR-стаб W0-страниц оказался в W1. + +Зачем. Программа может временно маппить свою EMM-страницу в W0 (атласы +спрайтов в sprite_update, ресурсы через gfx_w0_map). Пока страница там, +ЛЮБОЕ прерывание уходит на #0038 ЭТОЙ страницы, поэтому gfx_w0_page_prepare() +прошивает туда `JP _gfx_w0_isr`, а стаб возвращает страницу ядра DSS, зовёт +честный обработчик и восстанавливает страницу обратно. + +Мина. Сам стаб живёт в _CODE. В режимах small и huge _CODE начинается с +0x4100, то есть попадает в W1 — окно, которое DSS перемаппит на время СВОИХ +вызовов (docs/new/04-memory.md §4.2, §4.9). Если прерывание придёт, пока +наша страница в W0 И идёт вызов DSS, переход уйдёт по адресу, которого в этот +момент нет: W1 читается как #FF, процессор уползает в бесконечный пролог +системного прерывания. Само libbgi это документирует в common/_gfx_w0_isr.c +(«обязан быть в W2»). + +Собирается это МОЛЧА и проявляется НЕДЕТЕРМИНИРОВАННО — зависанием примерно +на каждом третьем холодном старте, что легко списать на случайность. Поймано +вживую 2026-09-01 в порте Loom (huge): стаб был на 0x6769. + +Как проверяем. По .map берём адрес __gfx_w0_isr. Если символа нет — W0-путь +в программу не слинкован, молча выходим. Если адрес >= 0x8000 — он в W2, всё +хорошо. Иначе печатаем WARNING: сборку не валим, потому что программа могла +и не класть свои страницы в W0. + +Лечение (со стороны приложения): вернуть модули из банков в резидент или +разгрузить _DATA так, чтобы _CODE перевалил за 0x8000 и хвост libbgi уехал в +W2. Порядок линковки сам по себе не помогает: пока весь _CODE влезает в W1 +(ёмкость 0x8000-0x4100 = 16128 Б), расстановка модулей ничего не меняет. +""" +import re +import sys +from pathlib import Path + +W2_BASE = 0x8000 +SYM = '__gfx_w0_isr' +MAP_RE = re.compile(r'^\s+([0-9A-Fa-f]{4,8})\s+(\S+)\s') + + +def find_symbol(map_path, name): + for line in map_path.read_text(errors='replace').splitlines(): + m = MAP_RE.match(line) + if m and m.group(2) == name: + return int(m.group(1), 16) + return None + + +def main(argv): + if len(argv) != 2: + print(__doc__.strip()) + return 2 + build = Path(argv[1]) + maps = list(build.glob('*.map')) + if not maps: + print(f'check_w0_isr: нет .map в {build}', file=sys.stderr) + return 2 + + addr = find_symbol(maps[0], SYM) + if addr is None: + return 0 # W0-путь не слинкован — проверять нечего + if addr >= W2_BASE: + print(f'check_w0_isr: {SYM} @ 0x{addr:04X} — в W2, ок') + return 0 + + print(f'check_w0_isr: ВНИМАНИЕ — {SYM} @ 0x{addr:04X} лежит в W1.', + file=sys.stderr) + print(' Если программа маппит свои страницы в W0 (атласы спрайтов, ' + 'gfx_w0_map,', file=sys.stderr) + print(' atlas_attach), прерывание во время вызова DSS уйдёт в ' + 'перемапленное W1 —', file=sys.stderr) + print(' недетерминированные зависания на старте. Стаб обязан быть в W2 ' + f'(>= 0x{W2_BASE:04X}).', file=sys.stderr) + print(' Лечение: увести _CODE за 0x8000 (модули из банков в резидент / ' + 'разгрузить', file=sys.stderr) + print(' _DATA). Подробности — в шапке этого скрипта и ' + 'docs/new/04-memory.md §4.2.', file=sys.stderr) + return 0 # предупреждение, сборку не валим + + +if __name__ == '__main__': + sys.exit(main(sys.argv))