Железный отладчик для ZX Spectrum
До текущего момента, отладка С кода для ZX Spectrum была возможна только в некоторых эмуляторах. В предыдущей статье описан процесс написания отладчика, который использовал цепочку gdb-клиент (z88dk) <-> gdb-протокол <-> gdbserver (эмулятор Fuse).
В этой статье описан процесс написания "железного" отладчика для ZX Spectrum, то есть такого, в котором и непосредственно целевой код и код отладчика выполняет один и тот же z80. Как и в случае с эмулятором, используется сетевой протокол gdb, для этого требуется сетевой адаптер Spectranet, который постепенно собирает вокруг себя значительное (англоязычное) сообщество.
Но зачем?
Автор преследует две цели — понизить порог вхождения в разработку ПО для Spectranet и решить сложную задачу, потому что "а почему бы и нет?". Авось кто-нибудь решится сделать какую-нибудь многопользовательскую онлайн игру для ZX Spectrum? Кто знает.
Как это работает?
Начнем с того, как вообще устроена память в ZX Spectrum? Для простоты остановимся на Spectrum 48K, которому выделено первые 16K под ROM, где размещены функции BASIC (и другие), а остальные 48K — RAМ (обычно динамическая, но в современных клонах статическая), из которых 32К+ (в зависимости от аппетита) доступны пользователю. ZX Spectrum имеет пользовательский порт для картриджей, на который выведены шины данных и адресации, а также специальный пин ROMCS, который позволяет пользовательским устройствам подменять первые 16K на свои, обычно это ROM, но может быть и RAM.
Spectranet, будучи пользовательским картриджем, этим пользуется. Он имеет на борту чип Ethernet (W5100) со встроенным стеком TCP/IP, микросхему ПЛИС (CPLD, XC9572), один чип статической переменной памяти на 128K а также один чип 128K постоянной FLASH памяти.
В любой момент, когда Spectranet посчитает нужным, он подменяет первые 16K памяти на свои. Переключением занимается специальная микросхема ПЛИС (CLPD) по весьма сложным правилам (об этом позже). Память эта размечена следующим образом:
Первые 4K заблокированы на первую страницу ROM, на которой располагаются системные функции Spectranet. Эти функции исполняет сам просессор Z80.
Два блока 4..8K (блок А) и 8..12K (Блок Б) могут быть переключены в любую страницу RAM или ROM, по запросу пользователя. Как и остальные блоки, они доступны только когда сам спектранет "включен" в память спектрума.
Последний блок 12..16K заблокирован на последнюю страницу RAM. На ней размещена так называемая JUMP таблица (которая позволяет разработчикам менять Firmware с обратной совместимостью), системные переменные, включая переменные, которые отвечают за страницы А и Б, и другие буферы.
Spectranet имеет специальную систему "модулей", в которой пользователи могут устанавливать т.н. "плагины" в свободные ROM страницы, а система имеет специальный планировщик, который умеет вызывать нужный модуль по его коду. Интересно отметить, что включение страниц для модулей "стакуется", то есть один модуль может вызывать другой, не смотря на то, что оба модуля находятся в одном и том же адресном пространстве. Перед вызовом кода другого модуля, информация о странице кладется в стек, а после выполнения, страница переключается обратно. В какой-то момент автор этой статьи пытался написать SSL библиотеку которая состояла из двадцати 4К модулей, которые дружно вызывали друг друга. Можете себе только представить, что творилось в стеке и в памяти! Отладчик, о котором позже пойдет речь в этой статье, и реализован в виде одного такого модуля.
Также Spectranet имеет систему расширения BASIC, позволяя модулям устанавливать пользовательские токены (команды) в стандартный BASIC. Будь такая команда вызвана, Spectranet через планировщик включит нужную страницу и передаст управление пользователю.
ПЛИС и прерывания
После включения ZX Spectrum, Spectranet пехватывает управление, устанавливает LINK на сетевом интерфейсе, получает локальный IP через DHCP и ... передает управление в родному Spectrum ROM, "выключая" себя из первой страницы 16K. Далее микросхема ПЛИС (CLPD) подслушивает то, что творится на адресной шине / шине данных, чтобы стриггериться на правильные вызовы и включиться обратно в первые 16K:
0x0008 (т.н. RST 08), чтобы поймать команды расширения BASIC
0x0066 (NMI), переход сюда происходит по прерыванию по нажатию на "волшебную кнопку" (NMI)
Инструкции CALL 0x3FF8 ... 0x3FFF. Эти адреса соответсвуют четвертой странице RAM — здесь размещена JUMP таблица, которая состоит целиком из вызовов "JP A; JP B; JP C". Таким образом разработчики могут переписывать код не волнуясь за обратную совместимость, а также пользователи могут переписывать ее по своему усмотрению. Через работу этих функцикй и реализовано общение "родного" кода спектрума, включая кода Spectrum ROM, к коду Spectranet. Как только ПЛИС обнаруживает инструкцию CALL, она включает Spectranet вместо стандартного ROMa. Чтобы вернуть стандартный ROM на место, функция, которую вызвали, обязана сделать CALL PAGEOUT (0x007C).
Атакуем проблему
Предположим, модуль отладчика был установлен в систему и готов к работе. Первым делом, отладчик регистрирует в системе новую команду basic %gdbserver
, которая и будет запускать отладку, мы ведь не хотим запускать отладку каждый раз и мешать другим.
После запуска команда перехватывает контроль над "магической" кнопкой (NMI) через системную переменную. Это необходимо для того, чтобы в любой момент пользователь мог сделать "BREAK" и остановить программу там где она есть, чтобы инспектровать ее состояние. Также команда перехватывает управление над прерыванием RST8, которое нужно для функционирования точек останова. После этий действий команда возрвращает управление в BASIC и дает пользователю загрузить программу для отладки и/или ждет нажатия кнопки NMI.
Если отладчик размещен в памяти ROM, где он хранит переменные? Вопрос правильный — он переиспользует четверную страницу Spectranet, размещая там блок размером почти в 500 байт, в котором рамещаются системые перменные, немного кода для обработки прерывания, состояние регистров на момент останова и тд.
Останов по запросу пользователя
К сожалению, останов по запросу клиента (z88dk-gdb) невозможен, для этого необходимо нажать "магическую" кнопку (NMI).
После нажатия на кнопку NMI отладчик перехватывает управление, сохраняет состояние экрана в страницу RAM (ведь мы можем делать отладку видеоигры, например, а картинку нужно после восстановить), и ждет подключения gdb клиента, z88dk-gdb. z88dk-gdb разрабатывался на основе z88dk-ticks и суть его работы описана в предудущей статье. Так как NMI может быть нажата в любой момент, критически важно сохранить состояние всех регистров, а также восстановить их на выходе.
Любопытно отметить, что меняя регистры из отладчика вы на самом деле их не меняете, а всего лишь меняете сохраненное состояние регистров, на то, которое будет использовано для восстановления после выхода из отладчика.
z88dk-gdb выполняет всю тяжелую работу по понимаю того, что байты значит и что такое стек. gdbserver же выполняет очень примитивные задачи уровня "подай принеси" и "поставь точку останова". Например, чтобы поставить точку останова в main, z88dk вычислит ее адрес, и затем передаст отладчику (через протокол gdb) приказ установить точку останова. Например, команда "restore", которая восстановит программу в первоначальное состояние это просто набор команд "запиши эти 16 байт по этому адресу" и поставь регистры в это значение.
Точки останова
Точки останова — это самый сложный и самый интересный момент всей задумки. Обычно программные точки останова реализуются следующим образом:
Мы заменяем первый байт необходимой иструкции на "программное прерывание"
Прерывание запускает отладчик, который восстанавливает оригинальный байт и ждет продолжения в какой-то момент
Перед самым завершением работы, следует сделать подъем-переворот: необходимо вызвать ОДНУ инструкцию и затем вернуть байт прерывания на место, чтобы точка останова могла быть вызвана еще раз
Z80 имеет целых 8 однобайтовых команд RST 0x0, RST 0x8, RST 0x10 и тд, которые являются сокращенными вызовами CALL по адресу x. RST8 имеет специальное назначение в spectranet, так как за ним подслушивает ПЛИС, она обрабатывает пользовательские команды BASIC. Что самое интересное — она может быть перезаписана! Таким образом, заменой первого байта инструции по адресу на RST8, мы можем добиться выполнения кода отладчика, который остановит весь процесс, восстановит оригинальный байт и будет ждать дальнейших инструкий.
Перед выходом из отладчика нужно выполнить очень важную задачу — вернуть в точку останова изначальную инструкцию RST8. Но как это сделать? Инструкции разные бывают. В других процессорах, через системные средства (ptrace) возможно выполнить ровно одну инструкцию, а у z80 такой возможности нет. Можно попробовать поиграться с Interrupt Mode 0, но как это сделать кодом самого z80, непонятно. На выход опять приходит Spectranet — у него есть целая одна "хардварная" точка останова. Подобно инструциям по адресу 0x08, 0x66, микросхему ПЛИС (CLPD) можно настроить подслушивать любой адрес, после чего та вызовет прерывание NMI. Работает это весьма интересно — система срабатывает не перед выполнением инструкции, а после нее, что нам и нужно: перед выходом мы просто настраиваем "железную" точку останова на тот же адрес, на котором и была оригинальная точка останова, и как только эта инструкция будет выполнена, будет вызвано новое NMI прерывание, в котором мы сможем восстановить RST8 обратно.
Два прерывания — поведение одно
Отладчик может быть вызван как через NMI (если пользователь нажмет на магическую кнопку) так и по программному прерыванию (RST8, на точке останова). Обе ситуации имеют совершенно разный стек, но код обработки один. Выход из ситуации — скопировать состояние регистров в предсказуемое место в обоих случаях, а затем вызвать отладчик. По завершению отладчик, регистры восстанавливаются так же по разному, но из одного источника. Если прогграмист решит поменять значение регистров, включая регистр стрека — так тому и быть, вернемся куда сказали, а там уж программа знает лучше.
В заключение
Все материалы, утилиты и др. я собрал на специальном ресурсе специально для такой отладки — speccytools.org. На реализацию кода отладчика можно посмотреть здесь. Если вы столкнулись с ошибкой или проблеммой — не стесняйтесь открыть Issue или Pull Request.
Не смотря на то, что отладчик требует Spectranet, программам не обязательно им пользоваться, и они могут рассчитывать на привычную "ванильную среду". Но если хочется увидеть "стек вызова", то нужно собирать в режиме -debug.
Данный инструмент можно запустить на эмуляторе, но он предназначен в первую очередь для физической отладки, а отладка в эмуляторе Fuse доступна своими средствами в этом форке. Так как оба отладчика используют один и тот же протокол, gdb-клиенту без разницы к кому подключаться. Также отладка доступна в эмуляторе Mame, как это сделать — описано здесь.h