До текущего момента, отладка С кода для 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.

Z80 может адресовать только 64К
Z80 может адресовать только 64К

Spectranet, будучи пользовательским картриджем, этим пользуется. Он имеет на борту чип Ethernet (W5100) со встроенным стеком TCP/IP, микросхему ПЛИС (CPLD, XC9572), один чип статической переменной памяти на 128K а также один чип 128K постоянной FLASH памяти.

Основные компоненты Spectranet
Основные компоненты Spectranet

В любой момент, когда Spectranet посчитает нужным, он подменяет первые 16K памяти на свои. Переключением занимается специальная микросхема ПЛИС (CLPD) по весьма сложным правилам (об этом позже). Память эта размечена следующим образом:

Механизм переключения страниц в Spectranet
Механизм переключения страниц в Spectranet
  • Первые 4K заблокированы на первую страницу ROM, на которой располагаются системные функции Spectranet. Эти функции исполняет сам просессор Z80.

  • Два блока 4..8K (блок А) и 8..12K (Блок Б) могут быть переключены в любую страницу RAM или ROM, по запросу пользователя. Как и остальные блоки, они доступны только когда сам спектранет "включен" в память спектрума.

  • Последний блок 12..16K заблокирован на последнюю страницу RAM. На ней размещена так называемая JUMP таблица (которая позволяет разработчикам менять Firmware с обратной совместимостью), системные переменные, включая переменные, которые отвечают за страницы А и Б, и другие буферы.

Стандартное меню NMI со списком модулей
Стандартное меню NMI со списком модулей

Spectranet имеет специальную систему "модулей", в которой пользователи могут устанавливать т.н. "плагины" в свободные ROM страницы, а система имеет специальный планировщик, который умеет вызывать нужный модуль по его коду. Интересно отметить, что включение страниц для модулей "стакуется", то есть один модуль может вызывать другой, не смотря на то, что оба модуля находятся в одном и том же адресном пространстве. Перед вызовом кода другого модуля, информация о странице кладется в стек, а после выполнения, страница переключается обратно. В какой-то момент автор этой статьи пытался написать SSL библиотеку которая состояла из двадцати 4К модулей, которые дружно вызывали друг друга. Можете себе только представить, что творилось в стеке и в памяти! Отладчик, о котором позже пойдет речь в этой статье, и реализован в виде одного такого модуля.

Также Spectranet имеет систему расширения BASIC, позволяя модулям устанавливать пользовательские токены (команды) в стандартный BASIC. Будь такая команда вызвана, Spectranet через планировщик включит нужную страницу и передаст управление пользователю.

ПЛИС и прерывания

Одна из цепей ПЛИС, которая реализует программируемую ловушку (TRAP)
Одна из цепей ПЛИС, которая реализует программируемую ловушку (TRAP)

После включения 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, которая и будет запускать отладку, мы ведь не хотим запускать отладку каждый раз и мешать другим.

Вводим команду %gdbserver, чтобы активировать отладчик
Вводим команду %gdbserver, чтобы активировать отладчик

После запуска команда перехватывает контроль над "магической" кнопкой (NMI) через системную переменную. Это необходимо для того, чтобы в любой момент пользователь мог сделать "BREAK" и остановить программу там где она есть, чтобы инспектровать ее состояние. Также команда перехватывает управление над прерыванием RST8, которое нужно для функционирования точек останова. После этий действий команда возрвращает управление в BASIC и дает пользователю загрузить программу для отладки и/или ждет нажатия кнопки NMI.

Если отладчик размещен в памяти ROM, где он хранит переменные? Вопрос правильный — он переиспользует четверную страницу Spectranet, размещая там блок размером почти в 500 байт, в котором рамещаются системые перменные, немного кода для обработки прерывания, состояние регистров на момент останова и тд.

Останов по запросу пользователя

К сожалению, останов по запросу клиента (z88dk-gdb) невозможен, для этого необходимо нажать "магическую" кнопку (NMI).

Ожидаем подключения
Ожидаем подключения

После нажатия на кнопку NMI отладчик перехватывает управление, сохраняет состояние экрана в страницу RAM (ведь мы можем делать отладку видеоигры, например, а картинку нужно после восстановить), и ждет подключения gdb клиента, z88dk-gdb. z88dk-gdb разрабатывался на основе z88dk-ticks и суть его работы описана в предудущей статье. Так как NMI может быть нажата в любой момент, критически важно сохранить состояние всех регистров, а также восстановить их на выходе.

Типичный интерфейс в z88dk-gdb
Типичный интерфейс в z88dk-gdb

Любопытно отметить, что меняя регистры из отладчика вы на самом деле их не меняете, а всего лишь меняете сохраненное состояние регистров, на то, которое будет использовано для восстановления после выхода из отладчика.

z88dk-gdb выполняет всю тяжелую работу по понимаю того, что байты значит и что такое стек. gdbserver же выполняет очень примитивные задачи уровня "подай принеси" и "поставь точку останова". Например, чтобы поставить точку останова в main, z88dk вычислит ее адрес, и затем передаст отладчику (через протокол gdb) приказ установить точку останова. Например, команда "restore", которая восстановит программу в первоначальное состояние это просто набор команд "запиши эти 16 байт по этому адресу" и поставь регистры в это значение.

Точки останова

Точки останова — это самый сложный и самый интересный момент всей задумки. Обычно программные точки останова реализуются следующим образом:

  • Мы заменяем первый байт необходимой иструкции на "программное прерывание"

  • Прерывание запускает отладчик, который восстанавливает оригинальный байт и ждет продолжения в какой-то момент

  • Перед самым завершением работы, следует сделать подъем-переворот: необходимо вызвать ОДНУ инструкцию и затем вернуть байт прерывания на место, чтобы точка останова могла быть вызвана еще раз

Z80 имеет целых 8 однобайтовых команд RST 0x0, RST 0x8, RST 0x10 и тд, которые являются сокращенными вызовами CALL по адресу x. RST8 имеет специальное назначение в spectranet, так как за ним подслушивает ПЛИС, она обрабатывает пользовательские команды BASIC. Что самое интересное — она может быть перезаписана! Таким образом, заменой первого байта инструции по адресу на RST8, мы можем добиться выполнения кода отладчика, который остановит весь процесс, восстановит оригинальный байт и будет ждать дальнейших инструкий.

Состояние кода main до срабатывания точки останова, и после
Состояние кода main до срабатывания точки останова, и после

Перед выходом из отладчика нужно выполнить очень важную задачу — вернуть в точку останова изначальную инструкцию 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

Комментарии (11)


  1. ukmsz
    06.02.2022 01:01
    +2

    Прекрасно справлялись с разработкой на самом железном спектруме. IDE с компилятором и отладчиком в котором видны метки и комментарии. На клонах с расширенной памятью исходники лежали в ней и были сразу доступны после хард ресета.


    1. desertkun Автор
      06.02.2022 01:09
      +1

      Речь о отладке кода на C или ассемблера? Хороший отладчик Си на самом спектруме, звучит как очень сложная задача. Один только файл с отладочной информацией занимает порой один мегабайт. Эта статья описывает удаленную отладку, когда исходники и gdb клиент на компьютере, а спекки только "stub" реализовывает.

       IDE с компилятором и отладчиком в котором видны метки и комментарии.

      А как там работали точки останова?


      1. ukmsz
        06.02.2022 01:51
        +4

        Все было реализовано софтово конечно. Да и разработка на си была непрактична, все на ассемблере писали.


        1. SSDDRR
          06.02.2022 18:57

          На "Макроассемблере" или самом простом?


          1. ukmsz
            06.02.2022 20:09

            Тогда в ходу были несколько ассемблеров со встроенной иде разной функциональности как правило с поддержкой макросов и загружаемых модулей.


            1. SSDDRR
              06.02.2022 20:13

              Хотлось бы посмотреть на такое в видео, например. А то встречается, в основном, рассказы о том, что люди писали ПО сначала на мини-ЭВМ и по внешней шине отправляли скомпилированное на целевую машину. Или, позднее, о такой же схеме, но с разработкой на "ИБМ"-совместимых ПЭВМ и дальнейшей выгрузкой кода. А вот про разработку приложений на самих целевых машинах данных маловато.


              1. mpa4b
                08.02.2022 18:05

                Вот что-то нашёл https://www.youtube.com/watch?v=_cdqJr97loA, правда там чел в эмуляторе юзает нативный ассемблер.


  1. NedoPC
    06.02.2022 12:11
    +8

    Для совместимости, особенно в будущем, надо писать сетевой код не под железо, а под операционную систему, причём сразу многозадачную. На платформе ZX Spectrum (речь, конечно, не про ZX Spectrum 48K, а про серьёзные клоны, распространённые в России) из таких операционных систем сейчас развивается, насколько мне известно, только NedoOS, где уже более 100 приложений, включая несколько нативных сред разработки. В NedoOS есть драйвер сетевой карты ZXNetUSB (основана на W5300), но пользователи других сетевых карт могут добавить свои драйверы по его образцу. Там уже есть примеры сетевых игр - сетевая змейка на двух игроков, q1 (deathmatch с видом сверху) и ZX Battle City. Прочий сетевой софт стандартный - ping, time, telnet-клиент, telnet-сервер, wget, файловый http-сервер, web-браузер, gopher-браузер, ftp-клиент, IRC-клиент, определялка своего IP. Есть ещё забавный сервис, который расшаривает AY по сети. Всё с открытыми исходниками - http://nedoos.ru/

    Обычно в NedoOS отлаживают код на Си с помощью NoICE, для него есть готовый сервер, работающий через COM-порт по стандарту Кондратьева. Он остался от прошлых проектов. Можно сделать и сервер, работающий через сеть, только рук на всё не хватает. Присоединяйтесь!


  1. Dimkama
    06.02.2022 17:29

    Я на спектруме(zx evolution) уже лет 10 юзаю програмно-аппаратный дебагер NoIce debugger. Умеет шагать си-код, асм, ставить бряки, изменять реги и память.

    Рекомендую.


  1. Overphase
    06.02.2022 20:21

    Глянув на большие буквы в заголовке статьи, сначала было решил, что это очередной перевод с английского, где такое выделение букв норма. Но не норма для русского.


    1. desertkun Автор
      06.02.2022 23:36

      Спасибо, поправил.