Тема: Ошибки в Бейсике-60
После багов в операторах TAB и POS решил поискать баги и в других операторах Бейсика, связанных с управлением курсором.
И нашел :) Поигравшись с аргументами оператора HTAB мне удалось завесить Бейсик.
Сразу скажу, что удалось мне это только на комбинации из ранней версии ALV DOS (которая свиристит при запуске) и Бейсика-60. Впоследствии выяснилось, что в этой версии ALV DOS просто сломан обработчик BRK. Так что дальше речь пойдет только про штатный DOS и Бейсик-60 (и немножко Бейсик-67).
Итак, загружаем Бейсик-60 и вводим следующие команды:
HTAB 200
HTAB 255
Интерпретатор вылетает в обработчик BRK. Баг есть и в режиме выполнения программы:
10 HTAB 200: HTAB 255
Пишем RUN, интерпретатор также вылетает в обработчик BRK.
В чем дело? Почему оператор HTAB дает такой эффект?
Разбираться я начал, как обычно, с исходников Бейсика Apple.
Оператор HTAB находится по адресу $F7E7. В Apple он работает так: сначала вычитается единица из аргумента. Если результат больше длины экранной строки (40), то выполняется цикл: из аргумента вычитается длина экранной строки, вызывается функция печати символа перевода строки и проверка повторяется. Когда значение делается меньше 40, оно записывается в ячейку $24 (горизонтальная позиция курсора). Грубо говоря, HTAB 201 вызовет 6 переводов на новую строку и выглядеть это будет так, будто нажали стрелку вправо 200 раз.
В Бейсик-60 функция переехала на адрес $F3CD. В тексте два исправления - длина экранной строки равна 32, а перед записью в ячейку $24 значение удваивается. (Поэтому в режиме Т64 курсор движется двойными шагами.)
F3CD 20 B3 E6 JSR $E6B3 ; в X аргумент HTAB
F3D0 CA DEX
F3D1 8A TAX
F3D2 C9 20 CMP #$20 ; начало цикла
F3D4 90 0A BCC $F3E0
F3D6 E9 20 SBC #$20
F3D8 48 PHA
F3D9 20 F8 DA JSR $DAF8
F3DC 68 PLA
F3DD 4C A7 E7 JMP $E7A7 ; ???
F3E0 0A ASL
F3E1 85 24 STA $24
F3E3 60 RTS
Но самое интересное - это адрес $F3DD. В Apple тут просто стоит переход на начало цикла. В Бейсик-60 это должен был быть адрес $F3D2. Но на деле переход происходит на адрес $E7A7. То есть в середину процедуры FADD, которая выполняет сложение двух чисел с плавающей точкой!
Это явный косяк. Примерно понятно, как он мог возникнуть: исходники Бейсика слишком большие, чтобы их уместить в один файл и скомпилировать за один раз. Поэтому, скорее всего, Бейсик компилировали по частям. Адреса внешних процедур, видимо, прописывали руками в тексте. Ну и ошиблись.
Что получилось? Если аргумент HTAB превышает 32, то управление улетает в процедуру FADD.
Для выполнения операций с плавающей точкой в нулевой странице выделено несколько аккумуляторов. Каждый аккумулятор - это временная переменная, содержащая 40-битное число с плавающей точкой. При вычислениях может понадобиться несколько таких переменных, поэтому у процедуры FADD первый аргумент лежит по фиксированному адресу, а второй аргумент задается регистром X. А когда из HTAB выполняется переход, то в регистре X у нас находится аргумент функции HTAB... Сюрприз!
Но это полбеды. Будь тут сложение целых чисел, то ничего бы страшного не случилось - процедура сложила бы значение первого аккумулятора со случайным числом и никто бы от этого не пострадал. Но дело в том, что для сложения чисел с плавающей точкой нужно сначала выровнять их мантиссы...
Если кто не знает, кратко опишу устройство числа с плавающей точкой в Бейсике. Там есть две части - экспонента и мантисса. Экспонента - это первый байт числа, мантисса - это следующие 4 байта.
Представим, что у нас есть большое двоичное число с фиксированной точкой. Первые 127 бит - это целая часть, потом точка, а потом еще 159 бит дробной части. А теперь мы берем бумажку и вырезаем в ней окошечко, через которое видно только 32 бита. Это и будет мантисса. А экспонента - это просто число разрядов, на которое нужно сдвинуть это окошечко относительно точки, чтобы попасть на начало числа.
В результате у двух разных чисел мантиссы могут быть на разном расстоянии от точки. И чтобы сложить эти числа, нужно сначала сделать так, чтобы "окошечки" находились на одинаковом расстоянии от точки. Это и есть выравнивание мантисс.
Делается оно сдвигом мантиссы меньшего числа вправо. Величина сдвига равна разности экспонент и помещается в регистр Y. И вот, когда HTAB делает переход внутрь FADD, он заодно передает и величину сдвига. Очень немаленькую. Поэтому мантисса числа уезжает вправо так сильно, что полностью обнуляется.
В результате получаем такой прикольный эффект: когда аргумент HTAB превышает 32, то происходит обнуление 4 байт в нулевой странице, начиная с адреса, равного аргументу HTAB. Круто?
Спрашивается, а почему же интерпретатор падает в обработчик BRK?
Тут тоже интересная история. Вспомним, что у Apple объем памяти мог быть от 4 до 48 Кбайт. Бейсик должен был работать с любой конфигурацией. В результате его код написан так, чтобы использовать как можно меньше памяти. Но это привело к тормозам при выполнении программы.
Microsoft постаралась поднять производительность за счет оптимизации. Какие процедуры нужно оптимизировать в первую очередь? Те, которые вызываются чаще всего. Какая процедура используется чаще всего во время выполнения программы на Бейсике? Процедура получения следующего токена CHRGET. Вот ее и перенесли, в рамках оптимизации, в нулевую страницу. Адрес ее начала $B1.
Оптимизация, кстати, довольно заметная: если бы код был в ПЗУ, то пришлось бы использовать косвенную адресацию, а значит освобождать и перезагружать индексный регистр. В общем, сэкономили не меньше десятка тактов. При выполнении программы средней длины с циклами ускорение может составлять секунды.
Ну а оператор HTAB 200 успешно затер часть кода этой процедуры и поэтому при выполнении следующего за ним оператора интерпретатор упал.
***
Что интересно, бага сохранилась и в Бейсик-67. Полностью от нее избавились только в ИКП-7, причем самым суровым образом: цикл выпилили, а при превышении аргументом значения 32 (или 64 в режиме Т64) сделали переход на обработчик ошибок.
***
Напоследок вот несколько интересных эффектов, которые можно вызвать с помощью этого бага.
HTAB 33 - обнуляет переменные, в которых находится размер текстового окна. В результате размер окна делается 1x1 символ. Очень прикольно потом что-то вводить с клавиатуры :)
HTAB 54 - обнуляет стандартные векторы ввода-вывода ($36, $38). В результате попытка вывода на экран вызывает падение в обработчик BRK, который тоже падает, потому что ему тоже нужен вывод на экран. Со стороны это выглядит как зависание, лечится через сброс.
HTAB 104 : NEW - обнуляет адреса расположения текста программы на Бейсике. После этого оператор NEW пытается расположить программу по нулевому адресу, что полностью убивает Бейсик. После сброса экран будет отображать мусор.