В это трудно поверить, но иногда ошибки в процессорах по сути живут дольше, чем сами процессоры. Недавно мне довелось в этом убедиться на примере 16-разрядного микропроцессора 1801ВМ1А, на основе которого в свое время в СССР было создано семейство бытовых компьютеров БК-0010/11М. Об этом семействе на Хабре неоднократно писали.
Период активной жизни "БэКашек" приходится на конец 80-х — начало 90-х годов прошлого века. В эти годы усилиями многочисленных энтузиастов-одиночек а также групп кружковцев и кооператоров был наработан основной массив прикладных программ для БК: игры, утилиты, разнообразные "ДОСы" (дисковые операционные системы). Параллельно с развитием софта создавалась периферия, под которую писался свой системный софт. В целом, экосистема этих 16-разрядных PDP-подобных ЭВМ развивалась по схожим принципам, как, например, развивались ранние 8-битные открытые архитектуры на основе Intel 8080 и шины S-100. Позже, по мере отхода от утилитарной роли БК, фокус в программировании сместился в сторону демосцены.
Объем программного обеспечения для БК можно оценить, посетив общедоступные сайты с коллекциями программ. Конечно, по сравнению, например, с ZX-Spectrum, этот объем значительно скромнее. Тем не менее, даже такого объема, казалось бы, должно было хватить, чтобы обойти все мыслимые закоулки машинного кода. Можно ли найти что-то необычное в поведении процессора после более чем тридцатилетней практики его использования? Как оказалось — да! Об этом и пойдет речь ниже.
Пожалуй, имеет смысл рассказать эту историю в хронологическом порядке. Прежде всего, должен сразу заметить, что я вовсе не "программист со стажем", ни по роду занятий, ни по принадлежности к когорте энтузиастов БК, о которых я написал выше. К БК я пришел окольными путями, частично через ностальгию по увлечениям детства и юности (аналоговая и цифровая электроника, журнал Юный Техник, ЮТ-88 и прочие поделки и недоделки), а частично через интерес к архитектуре и системе команд PDP-11. БК "в железе" у меня нет и программы под БК я, как правило, запускаю и отлаживаю в эмуляторе bkemu на планшете под Андроид.
Некоторое время назад, я заинтересовался программой "Kaleidoscope" за авторством Li-Chen Wang-a. Программа была написана в 1976 г. в машинных кодах, под микропроцессор Intel 8080 в составе компьютера Altair 8800 с графическим адаптером Cromemco Dazzler. Мне захотелось детально разобрать алгоритм Li-Chen Wang-a и заодно портировать его на БК. Надо сказать, что желание портировать Калейдоскоп под БК высказывалось в среде демосценеров и ранее, и даже были попытки разобрать алгоритм, но они не увенчались успехом.
В своей следующей статье, я, возможно, разберу этот алгоритм в деталях (а для нетерпеливых тут выложу ссылку на исходники кросс-платформенного Калейдоскопа под libSDL на С). Для дальнейшего же, достаточно будет указать, что задача была решена, и Калейдоскоп был успешно портирован на БК. Более того, в алгоритм на БК была добавлена генерация звука, причем, поскольку и картинка, и звук генерируются одним и тем же кодом, можно сказать, что сама картинка и звучит (вся демка уместилась менее, чем в 256 байт машинного кода, и, надеюсь, будет представлена общественности на CAFe Demoparty 2019 в Казани в конце октября).
Закончив с написанием и отладкой моей программы в эмуляторе, я обратился к Дамиру ("Адамычу") Насырову (он один из организаторов CAFe Demoparty и весьма известный в среде демосценеров человек) с просьбой проверить выполнение программы на реальной БК. Особенно меня интересовало воспроизведение звука, так как тайминги в эмуляторе могли отличаться от таймингов на реальном железе. Каково же было мое разочарование, когда Дамир сообщил мне, что на реальной БК изображение есть, а звука нет!
Последующие несколько вечеров прошли в попытках вычитать из системной документации на БК-0011М и принципиальной схемы, где же могла быть ошибка со звуком. Звук в БК организован довольно просто: 6-й бит в регистре ввода/вывода с восьмеричным адресом 177716 (регистр управления магнитофоном) выведен через буфер на пьезоэлектрический динамик (бипер). В дополнение к 6-му разряду, разряды 2 и 5 того же самого регистра заведены на простейший цифро-аналоговый преобразователь на 4-х резисторах. С выхода этого преобразователя звук может идти на магнитофон. Все исключительно ясно и логично, но звука на реальной БК упорно не было, независимо от комбинаций битовых масок, которые я пытался применить к данным, выводимым в этот регистр. Параллельно были установлены и протестированы все известные мне эмуляторы БК — и звук работал во всех!
В какой-то момент мне даже почти удалось убедить Дамира, что его БК неисправна, однако поведение повторилось на другой живой БК-0011М, а также и на БК-0010. У меня закончились идеи, и обитатели телеграм-канала по БК-тематике тоже ничего не могли подсказать… Однако помог, как водится, случай. В процессе одного из экспериментов, Дамир запустил демку на эмуляторе, чтобы убедиться, что звук в эмуляторе есть. И вот тут ему удалось заметить, что не только звук в эмуляторе есть, а на БК нет, но еще и картинки в эмуляторе и на живой БК отличаются! Здесь я должен вам напомнить, что в моей программе и картинка, и звук генерируются одним кодом. Соответственно, все это время я искал причину не в том месте: причина была в коде, который генерировал данные для содержимого экрана.
Дамир прислал мне снимок экрана, и стало понятно, что алгоритм производит байты с нулевым содержимым старших 4-х разрядов, и, по стечению обстоятельств, именно эти биты выводились на звук (т.е., всегда нули). Однако причина, почему алгоритм так себя ведет, оставалась туманной. Вот это место в коде (ассемблер macro11 от PDP-11, регистры r0-r5 переименованы!):
; renamed registers
a = %0
b = %1
c = %2
d = %3
e = %4
h = %5
...
...
asr b ; sets CF
bic #177760, b
bis b, c
bis (h)+, c ; screen address in c
movb (c), a ; get a byte from screen RAM
bcc 1$ ; check CF
bic #177760, a ; keep bits 0-3, clear rest
bisb d, a ; fill bits 4-7
br 2$
1$:
bic #177417, a ; keep bits 4-7, clear rest
bisb e, a ; fill bits 0-3
2$:
...
...
По какой-то причине на реальной БК всегда выполнялся условный переход по метке 1$. То есть инструкция bcc всегда воспринимала флаг переноса как сброшенный, хотя инструкция сдвига ASR могла этот флаг установить как в 0, так и в 1. Как такое могло быть, ведь по документации на процессор, ни BIC, ни BIS, ни MOVB не должны оказывать влияние на флаг переноса?!
Причем, во всех эмуляторах (которые ведь писались по документации на процессор!) так и есть: эти инструкции не трогают флаг С. Стало ясно, что реальный процессор 1801ВМ1А работает в этом случае не по документации. Осталось это подтвердить.
Для начала, очевидный quick fix:
...
asr b ; sets CF
mfps -(sp) ; store PSW on stack
bic #177760, b
bis b, c
bis (h)+, c ; screen address in c
movb (c), a ; get a byte from screen RAM
mtps (sp)+ ; restore PSW from stack
bcc 1$ ; check CF
...
Сохранение флагов в стеке сразу после инструкции сдвига и их восстановление перед условным переходом тут же решило проблему, что показало, что я на верном пути. Осталось сузить "круг подозреваемых". Для проверки гипотезы был сначала написан такой синтетический тест (тут регистры не переименованы; начальная инициализация опущена, чтобы не загромождать код; emt 64 — программное прерывание для печати строки):
...
mov #1, r1
jsr pc, test
clr r1
jsr pc, test
halt
test:
mov #40000, r2 ; r2 points to screen RAM
mov #dummy, r5 ; r5 points to dummy = 200
; *** begin ***
asr r1 ; affects CF
bic #177760, r1
bis r1, r2
bis (r5)+, r2
movb (r2), r0
; *** end ***
jsr pc, prt
rts pc
prt:
mov #msg1, r0
bcs l1
mov #msg2, r0
l1:
emt 64
rts pc
msg1:
.asciz /Flag CF set/
msg2:
.asciz /Flag CF clear/
dummy:
.word 200
...
И тест … не сработал! Программа напечатала на экране
Flag CF set
Flag CF clear
Что же оказалось? Оказалось, что исходное предположение, что фрагмент кода между begin и end просто портит флаг C — неверно, и требует уточнения. Чем же так отличается этот тест от исходного кода? А тем, что между блоком "подозрительных" команд и условным переходом появились другие инструкции. Не влияющие на флаг С, но тем не менее, меняющие внутреннее состояние процессора. Поэтому, следующий тест был такой:
...
mov #1, r1
jsr pc, test
clr r1
jsr pc, test
halt
test:
mov #40000, r2
mov #dummy, r5
; *** begin ***
asr r1 ; affects CF
bic #177760, r1
bis r1, r2
bis (r5)+, r2
movb (r2), r0
bcc l1
; *** end ***
mov #msg1, r0
emt 64
rts pc
l1:
mov #msg2, r0
emt 64
rts pc
msg1:
.asciz /Flag CF set/
msg2:
.asciz /Flag CF clear/
dummy:
.word 200
...
И вот этот тест уже напечатал на реальной БК-0011М:
Flag CF clear
Flag CF clear
На эмуляторе же, по-прежнему,
Flag CF set
Flag CF clear
Дальнейшее — дело техники. Путем постепенных упрощений был получен вот такой минимальный тест, на котором воспроизводится баг (привожу исходник целиком):
.title test
.psect code
.=.+1000
mov #15, r0
emt 63
sec
jsr pc, test
clc
jsr pc, test
halt
test:
movb r0, r0
bcc l1
mov #msg1, r0
emt 64
rts pc
l1:
mov #msg2, r0
emt 64
rts pc
msg1:
.asciz /Flag CF set/
msg2:
.asciz /Flag CF clear/
.end
На реальной БК-0011М этот тест выводит
Flag CF clear
Flag CF clear
То есть, виновата оказалась инструкция MOVB, стоящая прямо перед командой условного перехода, причем вид первого операнда не суть важен. Если же между MOVB и BCC вставить, например, NOP, то поведение вернется к документированному, и программа напечатает
Flag CF set
Flag CF clear
Что позволило сформулировать уточненную гипотезу (цитирую себя, из телеграм-канала):
… По поводу бага: поведение вроде прояснилось. Как я себе представляю, MOVB src, dst (кстати, похоже что операнды не суть важны) из-за каких-то особенностей архитектуры временно портит флаг С внутрях проца, но не фатально, ибо проц, похоже, сохраняет копию этого флага. В результате, если между MOVB и условным переходом есть другие команды (не влияющие на С), например, NOP, то поведение как по документации.
Что же было дальше? Дальше, коллеги из канала помогли привлечь к обсуждению Вячеслава (@K1801ВМ1, легендарного человека, который ранее отреверсил этот процессор на уровне транзисторов). Реакция Вячеслава (Yuot), когда он протестировал поведение на стенде с реальным 1801ВМ1A (орфография и пунктуация сохранены):
Stanislav Maslovski:
минимум для воспроизводства нужны две команды
movb и условный переход по С
ну и перед этим флаг С выставить в известное состояние
Yuot:
Флаг с всегда сброшен получается
Stanislav Maslovski:
да
теперь вставь ноп
Yuot:
А теперь ни всигда
Yuot:
Чередуется 0 1
Это какой-то позор
С помощью Вячеслава выяснились детали, а именно, что причина бага в том, что в процессоре, помимо PSW есть еще один 4-х битный регистр, который, в норме, хранит копию флагов из PSW. Регистр этот связан с автоматом микропрограмм и условные переходы берут значения флагов из него. При выполнении же инструкций МОVB, SWAB, MFPS с регистром-приемником, из-за особенностей обработки знакового расширения и из-за ошибки в микрокоде, копия флага С в этом регистре сбрасывается и условные переходы по этому флагу работают некорректно. Однако при выполнении следующей инструкции значение временного регистра восстанавливается из PSW. Именно поэтому вставка NOP восстанавливает правильное поведение.
В заключение, хотел бы также поблагодарить подписчиков телеграм-канала "БК0010/11М World" за участие в обсуждении этого бага, и за высказанные замечания по тексту статьи. Заглавное фото к статье любезно предоставлено Manwe_SandS. Что еще более интересно, Manwe был близок к обнаружению того же самого бага, практически в то же время, когда мы с Дамиром бились над решением проблемы звука!
Теперь же дело за малым (шучу) — привести все эмуляторы в соответствие с реальным поведением процессора. Ведь сам процессор, увы, уже не исправишь.
На этом я и закончу. Надеюсь, было интересно.
P.S.:
Как говорится, не прошло и полгода… И честь БКшечников старой школы была восстановлена! Оказывается, что баг с флагом C был найден еще в 90х годах и описан в БКшной газете SCRIP. Вот это описание:
И тем не менее баг остался неизвестен большинству БК-программистов!