Files
Sprinter-SDCC/docs/bugs/sdcc-z80-ret-const-lost/REPORT-ru.md
T
snark13 5d31ed086f Резидент: pop_redraw + холодная половина pop_level в банки (902 -> 3525 Б кучи)
MEM-COLD3, шаг 1-2 из плана разгрузки резидента:

* pop_redraw.c целиком -> банк 7 (к pop_room.c: он и есть единственный
  потребитель разбора пометок).  Два модуля в одном банке линкуются в один
  сегмент — проверено на .map (BANK7 7928 -> 8556).
* pop_level.c расколот по частоте вызова: горячая половина (чтение тайлов,
  связи, дверные таблицы, живые стражи — зовут все банки, местами на каждый
  тайл) осталась в резиденте, холодная (чтение файла уровня, разбор комнаты,
  рестарт, потабличные различия) уехала в pop_level_cold.c -> банк 8.
  Общее состояние объявлено в _pop_level.h: данные банковых модулей всё
  равно линкуются в общий _DATA, в банк уехал только КОД.

Побочно найден баг кодогена SDCC: `return 1;` из ветки не кладёт 1 в A —
до __banked он маскировался случайным ненулевым остатком в A.  Репро и
разбор — docs/bugs/sdcc-z80-ret-const-lost/, стаб t_char починен одним
выходом через переменную.  Аудит всех .asm roomtest: других мест нет.

_CODE 25628 -> 23005, куча 902 -> 3525 Б.  tests-host 6/6.
2026-08-12 21:53:08 +03:00

3.6 KiB
Raw Blame History

SDCC 4.5 z80: return <константа> из ветки теряется — функция возвращает мусор

Компилятор: SDCC 4.5.0 (z80), флаги -mz80 --std-c99 --opt-code-size (воспроизводится и с --no-std-crt0, и в __banked, и без него).

Симптом. У функции, возвращающей uint8_t, ветка с return 1; НЕ кладёт 1 в A. Вызывающий читает A (__sdcccall(1): uint8 возвращается в A) и получает то, что там осталось от предыдущей операции — то есть мусор.

Репро

repro.c (полный текст рядом; собран в repro.asm):

uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
                        uint8_t *color, uint8_t *skill) __banked
{
    if (room < 1 || room > TK_ROOMS) return 0;
    if (tk_gs_tile[room - 1] < 30) {
        *tile = tk_gs_tile[room - 1]; *dir = 0; *color = 0; *skill = 1;
        return 1;                      /* <-- значение теряется */
    }
    if (room == tk_guard_room) {
        *tile = 11; *dir = 0; *color = 0; *skill = 1;
        return 1;                      /* <-- и здесь */
    }
    return 0;
}

Сгенерированный хвост второй ветки (repro.asm):

;repro.c:15: *tile = 11; *dir = 0; *color = 0; *skill = 1;
	ld	a, #0x0b
	ld	(de), a
	xor	a, a          ; A := 0  (это *dir = 0)
	ld	(bc), a
	pop	hl
	ld	(hl), #0x00
;repro.c:16: return 1;
	ex	de,hl
	pop	hl
	push	hl
	push	de
	ld	(hl), #0x01   ; это *skill = 1, НЕ возврат
	jr	00108$
00107$:
	xor	a, a
00108$:
	ld	sp, ix
	pop	ix
	ret                   ; A = 0 — вернулась ЛОЖЬ вместо 1

Кода ld a, #0x01 для самого return 1 нет ни в одной из двух веток.

Когда прячется, когда стреляет

Баг присутствует и БЕЗ __banked, но там часто маскируется: если последним в ветке A случайно оказался ненулевой байт (например ld a,#0x0b для *tile = 11), вызывающий с проверкой «истина/ложь» получает правильный ответ. __banked меняет распределение регистров (указатели уезжают в IX-кадр, *dir = 0 компилируется в xor a,a), A обнуляется — и та же функция начинает возвращать 0 вместо 1.

Именно так это и вылезло у нас: набор tests-host/t_char годами был зелёным и покраснел ровно в тот момент, когда стаб пометили __banked — хотя код стаба не менялся.

Обход

Один выход и явная переменная результата (workaround.c / workaround.asm):

uint8_t res = 0;
...
    res = 1;
...
return res;      /* -> ld a, -5 (ix) — корректно */

Признак для аудита

Грепать функции, у которых return <константа> стоит в ветке, где последней операцией была запись по указателю или xor a,a. Проверять по сгенерированному .asm: перед jr на общий выход обязан быть ld a,#… (или загрузка результата), иначе возврат — мусор.