Deja Vu #08
31 мая 1999

CODING - Недокументированные особенности процессора Z80.

AY-Track: Muzic theme from game"FAIRLIGHT"
__________________________________________


(C) Иван Рощин
__________________________________________


   ╔─────────────────────────────────╗
   │                                 │
    Недокументированная особенность
           процессора Z80         
   │                                 │
   ╚─────────────────────────────────╝


   Разрешается  свободное  распространение
этой статьи при условии не внесения каких-
либо изменений и сохранения моего копирай-
та.

1. С чего все началось
──────────────────────


   Писал  я  как-то очередную версию прог-
раммы BestView (v2.4) и  использовал в ней
вот такой фрагмент:

      ....
      EI
      CALL SUBR1
      HALT
      ....


SUBR1 LD A,R
      PUSH AF
      DI


      ....


      POP AF
      DI
      RET PO
      EI
      RET


   В этом  фрагменте происходит вызов про-
цедуры SUBR1, которая на время своей рабо-
ты запрещает прерывания, а при выходе вос-
станавливает прежний режим их работы.

   Проверка того, разрешены  или запрещены
прерывания при вызове процедуры, и восста-
новление режима прерываний происходит сле-
дующим образом:

 - команда LD A,R заносит во флаг P/V сос-
   тояние триггера прерываний IFF2;
 - регистровая пара AF запоминается в сте-
   ке (PUSH AF);
 - запрещаются прерывания (DI);
 - выполняются, собственно,те функции, для
   которых   и  предназначалась  процедура
   SUBR1;
 - содержимое  AF  восстанавливается  (POP
   AF);
 - прерывания запрещаются (DI);
 - если флаг P/V сброшен, происходит выход
   из процедуры с запрещенными прерывания-
   ми (RET PO);
 - иначе происходит выход  с  разрешенными
   прерываниями (EI:RET).


   Я стал замечать, что при  работе  этого
фрагмента BestView зависает - не всегда, и
даже не  слишком  часто, а в очень  редких
случаях. Но все равно,это было не очень-то
приятно. Программа, вроде бы, не содержала
никаких ошибок, по крайней мере  с первого
взгляда ничего подозрительного я не  заме-
тил. Оставалось лишь  прибегнуть  к  более
сильным средствам...

2. Ситуация начинает проясняться
────────────────────────────────


   После очередного  зависания, я  вставил
чистый диск и уверенно нажал кнопку MAGIC.
Затем  загрузил  отладчик "STS 6.2 +@" (не
зря я его переделывал - теперь  с  его по-
мощью после загрузки @-файла можно восста-
новить содержимое регистров  процессора на
момент сброса программы на  диск). Нажатие
пары клавиш - и вот я вижу, в каком  месте
программы произошло зависание.

....
EI
CALL SUBR1
HALT <------------ вот здесь!
....


   Типичный случай - прерывания запрещены,
и процессор прекратил выполнение программы
на команде HALT. Но почему прерывания ока-
зались запрещены - неясно. Ведь перед  вы-
зовом процедуры SUBR1 они  были  разрешены
командой  EI,  а  после  окончания  работы
SUBR1 они тоже должны быть разрешены -про-
цедура SUBR1 не должна  оказывать  влияния
на режим их работы.
   Трассирую SUBR1. Все идет как  положено
- и при входе, и при выходе прерывания ос-
таются разрешенными. Повторяю трассировку:
раз,другой, ... десятый. Все идет нормаль-
но.
   А  может  быть, дело в том, что в SUBR1
что-то происходит со стеком? И из-за этого
иногда  неправильно  восстанавливается со-
держимое AF? Надо бы проверить...

3. Зависания: дубль второй
──────────────────────────


   Ну вот, переделал программу. Теперь  уж
точно буду знать, в чем дело:

SUBR1 LD A,R
      PUSH AF
      DI


      PUSH HL
      PUSH AF
      POP HL
      LD (WR_HH1),HL
      POP HL


      ....


      POP AF


      PUSH HL
      PUSH AF
      POP HL
      LD (WR_HH2),HL
      POP HL


      DI
      RET PO
      EI
      RET


WR_HH1 DW  0
WR_HH2 DW  0


   Содержимое AF  теперь  запоминается  не
только в стеке, но и в  переменной  WR_HH1
(для  контроля), а при выходе из процедуры
- снятое со  стека значение запоминается в
WR_HH2. Если процедура работает правильно,
WR_HH1 и WR_HH2 должны  совпадать, а  флаг
P/V быть установленным.
   Запускаю... Вот уже минуту BestView ра-
ботает нормально... Просматриваю с ее  по-
мощью тот самый файл, при просмотре  кото-
рого она зависла в прошлый раз...  Ну вот,
опять! Да и неудивительно, ведь причину  я
не устранил. Ладно, будем разбираться.
   Снова нажимаю  MAGIC, загружаю "STS 6.2
+@" и сразу же проверяю значения WR_HH1  и
WR_HH2. И там, и там записано  #5908. Зна-
чения совпадают - следовательно, при рабо-
те со стеком ошибок не было. Но если в ре-
гистре флагов содержится #08 - значит,флаг
P/V  сброшен, и при вызове процедуры SUBR1
прерывания были запрещены.
   Но это же совершенно невозможно! Ведь в
программе  стоит  EI:CALL SUBR1! Наверное,
просто Спектрум перегрелся  и потому такие
глюки. Ничего более умного я в  этот  день
так и не придумал.

4. Ложный след
──────────────


   На следующий  день  я  нашел  возможное
объяснение таинственному запрещению преры-
ваний. Допустим, после команды  EI, но  до
выполнения команды LD A,R, произошло  пре-
рывание. Как известно, процедура его обра-
ботки должна заканчиваться  командами  EI:
RET (потому что в начале обработки  проис-
ходит автоматическое  запрещение  прерыва-
ний). Если же обработчик прерываний завер-
шается просто командой RET, то  прерывания
останутся запрещенными.
   Конечно, вероятность того, что прерыва-
ние произойдет именно между командами EI и
LD A,R очень мала,но ведь и зависания про-
исходят  очень редко. Так  что  это лишний
раз подтверждало мою гипотезу.
   Тем не менее, оставалось неясным, с че-
го бы это обработчик прерываний завершался
командой RET, а не EI:RET. Я решил  прове-
рить, действительно ли в  этом все дело, и
для этого добавил после  EI  команду  HALT
(см.ниже). Если обработчик прерываний дей-
ствительно завершается некорректно,то пос-
ле добавленного HALT'а  прерывания  всегда
будут запрещены, и, соответственно,BestVi-
ew всегда будет зависать.

      ....
      EI
      HALT <------ добавленная команда
      CALL SUBR1
      HALT
      ....



SUBR1 LD A,R
      PUSH AF
      DI


      ....


      POP AF
      DI
      RET PO
      EI
      RET


   Компилирую, запускаю...  Совершенно не-
ожиданный результат - зависания  полностью
прекратились! Хотел все так и оставить, но
все же решил разобраться, почему так полу-
чается.

5. Прием "упрощение программы"
──────────────────────────────


   Когда  найти ошибку обычными средствами
не  удается, я удаляю из программы все что
можно,  но  чтобы  ошибка при этом остава-
лась. В итоге, когда от программы остается
с десяток строк, ошибка заметна сразу. Так
я поступил и в этот раз:

      ORG #6000


      EI


M1    CALL SUBR1
      JR M1


SUBR1 LD A,R
      DI
      JP PO,M2
      EI
      RET
M2    LD A,4
      OUT (254),A
      RET



   Вот  такая  программа, всего  19  байт.
Разрешаются  прерывания, и  в  бесконечном
цикле вызывается процедура SUBR1. Эта про-
цедура  устанавливает зеленый border, если
при входе в нее прерывания были запрещены,
и  не  меняет цвет border, если прерывания
разрешены. Таким образом, если  произойдет
самопроизвольное   запрещение  прерываний,
это сразу же будет заметно.

   Запускаю  - да, border меняет свой цвет
на  зеленый. Причина столь странного пове-
дения  программы остается неизвестной. Мо-
жет  быть, в этом виновата процедура обра-
ботки  прерываний  1-го  рода?  Добавляю к
программе  несколько команд, устанавливаю-
щих  режим  IM 2 с обработчиком, состоящим
всего из двух команд: EI:RET.


      ORG #6000


      LD HL,#8000
      LD (HL),#81
      LD DE,#8001
      LD BC,#100
      LDIR
      LD A,#80
      LD I,A
      IM 2


      EI


M1    CALL SUBR1
      JR M1


SUBR1 LD A,R
      DI
      JP PO,M2
      EI
      RET
M2    LD A,4
      OUT (254),A
      RET


      ORG #8181


      EI
      RET


   Запускаю  -  тот же результат! Хотя при
трассировке и этой, и предыдущей программы
в отладчике border остается черным. Внима-
тельное   изучение  программы  приводит  к
предположению:  может быть, команда LD A,R
иногда устанавливает бит P/V так, как буд-
то прерывания запрещены, в то время как на
самом деле они разрешены?

6. Неужели ошибка в процессоре?
───────────────────────────────


   Еще  раз изменяю программу. Теперь пре-
рывания вообще не будут запрещаться (убра-
на команда DI). Если при выполнении коман-
ды  LD A,R бит P/V станет равным 0, на не-
которое  время  border станет зеленым (для
этого предусмотрена задержка):

      ORG #6000


      LD HL,#8000
      LD (HL),#81
      LD DE,#8001
      LD BC,#100
      LDIR
      LD A,#80
      LD I,A
      IM 2


      EI


M1    CALL SUBR1
      JR M1


SUBR1 LD A,R
      RET PE


      LD A,4
      OUT (254),A
      LD HL,0
      LD DE,0
      LD BC,#600
      LDIR ;WAIT
      XOR A
      OUT (254),A
      RET


      ORG #8181


      EI
      RET


   Запускаю... И что я вижу? Верхняя часть
border'а мигает зеленым цветом:

   ┌─────────────────────────────────┐
   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
   │▒▒▒▒                         ▒▒▒▒│
   │▒▒▒▒                         ▒▒▒▒│
   │                                 │
   │                                 │
   │                                 │
   │                                 │
   │                                 │
   │                                 │
   └─────────────────────────────────┘


   Это  говорит о том, что, во-первых, ко-
манда  LD A,R действительно иногда неверно
устанавливает  бит  P/V, и во-вторых - что
это  происходит  в момент прихода прерыва-
ния,  а  не  когда  угодно (действительно,
тогда бы border мигал зеленым в совершенно
произвольных местах).
   То,  что верхняя часть border'а мигает,
а  не  постоянно  окрашена в зеленый цвет,
тоже  получает свое объяснение. По-видимо-
му,  команда  LD  A,R неправильно работает
лишь  тогда, когда импульс прерывания при-
ходит во время ее выполнения, а так бывает
далеко  не всегда - прерывание может прои-
зойти  и во время выполнения другой коман-
ды.

7. Окончательное подтверждение
──────────────────────────────


   Проверим  этот  факт. Пусть  обработчик
прерывания  определяет, в каком месте была
прервана программа. Если она была прервана
именно  после команды LD A,R, пусть бордюр
на некоторое время станет желтым:

       ORG #6000


       LD HL,#8000
       LD (HL),#81
       LD DE,#8001
       LD BC,#100
       LDIR
       LD A,#80
       LD I,A
       IM 2


       EI


M1     CALL SUBR1
       JR M1


SUBR1  LD A,R
BP1    RET PE


       LD A,4
       OUT (254),A
       LD HL,0
       LD DE,0
       LD BC,#600
       LDIR  ;WAIT
       XOR A
       OUT (254),A
       RET



       ORG #8181


       EXX
       EX AF,AF'


       POP HL
       PUSH HL
       LD DE,BP1
       SBC HL,DE
       JR NZ,NE_BP1


       LD A,6
       OUT (254),A
       LD HL,0
       LD DE,0
       LD BC,#600
       LDIR  ;WAIT


NE_BP1 EXX
       EX AF,AF'


       EI
       RET


   Если неправильная работа команды LD A,R
не связана с тем, что во время ее выполне-
ния  приходит  импульс  прерывания,  то мы
увидим,  как  верхняя часть border'а будет
мигать  то  зеленым  цветом, то желтым. Но
если связь между этими двумя событиями су-
ществует,   то   мы  должны  увидеть,  как
верхняя  часть border'а мигает желтым цве-
том,  а нижняя часть - зеленым, причем они
должны мигать совершенно синхронно.
   Запускаю  -  и  вижу  именно  то, что и
предполагал.  Действительно,  такая  связь
существует:

   ┌─────────────────────────────────┐
   │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
   │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
   │▓▓▓▓                         ▓▓▓▓│
   │▓▓▓▓                         ▓▓▓▓│
   │▒▒▒▒                         ▒▒▒▒│
   │▒▒▒▒                         ▒▒▒▒│
   │▒▒▒▒                         ▒▒▒▒│
   │▒▒▒▒                         ▒▒▒▒│
   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
   │                                 │
   └─────────────────────────────────┘


   Но  как могут быть связаны прерывания и
работа команды LD A,R? Эта команда помеща-
ет  во флаг P/V содержимое триггера преры-
ваний  IFF2.  При  разрешенных прерываниях
этот триггер равен 1, а когда приходит им-
пульс прерывания, он автоматически сбрасы-
вается  в 0, чтобы исключить повторную об-
работку  прерывания.  Но обработка запроса
на прерывание начинается во время выполне-
ния  последнего  такта выполняемой команды
(т.е.  команды  LD  A,R).  И,  видимо, уже
сброшенный триггер IFF2 копируется во флаг
P/V (действительно, с точки зрения процес-
сора,  прерывания в этот момент уже запре-
щены).

   Все  сказанное относится и к команде LD
A,I. Приведенная информация была проверена
на оригинальном процессоре Z80 фирмы ZILOG
и на отечественном аналоге КР1858ВМ1.


8. К чему это приводит и что делать?
────────────────────────────────────


   Применение  команд  LD A,R и LD A,I для
определения состояния триггера прерываний,
вообще   говоря,  используется  во  многих
программах  (и даже в ПЗУ TR-DOS). Вот вам
и объяснение некоторого числа странных за-
висаний. Кажется, будто вероятность прихо-
да импульса прерывания именно во время вы-
полнения команды LD A,R невелика. Но, во--
первых,  вероятность увеличивается за счет
того,  что эта команда может выполняться в
программе  не  один  раз  (а для зависания
достаточно  единственного неверного выпол-
нения), и, во-вторых - если в программе до
этого   встречалась   команда  HALT,  т.е.
синхронизация  с  прерываниями, может слу-
читься  так, что команда LD A,R будет каж-
дый раз выполняться в то время, когда наи-
более вероятно очередное прерывание (так и
было в BestView).


   Итак,  этот способ ненадежен. Но как же
быть? Оказывается, можно со 100% точностью
определять  состояние  триггера прерываний
по следующему простому правилу:

 - выполнить команду LD A,R;
 - если  флаг P/V = 1 - значит, прерывания
в самом деле разрешены;
 - если  флаг  P/V = 0 - либо прерывания в
самом  деле запрещены, либо они разрешены,
но команда LD A,R неверно установила флаг.
Чтобы  устранить неопределенность, еще раз
выполняем  команду  LD  A,R. Если и теперь
флаг P/V = 0 - значит, прерывания запреще-
ны (в самом деле, не может быть так, чтобы
и  во  время  выполнения второй команды LD
A,R  произошло  прерывание  -  между двумя
прерываниями проходит 1/50 секунды, а меж-
ду двумя командами LD A,R - гораздо меньше
времени). Если  же  флаг P/V = 1 - значит,
прерывания разрешены.

   Вот соответствующий фрагмент программы:

SUBR1  LD A,R
       JP PE,M1
       LD A,R
M1     PUSH AF
       DI


       ....


       POP AF
       DI
       RET PO
       EI
       RET



9. Как это можно использовать?
──────────────────────────────


   С  помощью команды LD A,R удобно выпол-
нять  тестирование  процессора, чтобы рас-
познать  выполнение программы под эмулято-
ром. Эмулятор выполняет команды Z80 после-
довательно,  одну  за другой, и команда LD
A,R  всегда  будет правильно устанавливать
флаг P/V. А в реальном Z80 это не так.

   Вот простейшая процедура для тестирова-
ния процессора, которая возвращает в акку-
муляторе  1, если она запущена на реальном
Z80,  и 0 в противном случае. Она пытается
65536  раз  прочитать регистр R при разре-
шенных  прерываниях,  и если при этом хотя
бы один раз флаг P/V установится в 0 - де-
лается  вывод,  что  процедура работает на
реальном Z80.

TESTZ80


       EI
       LD BC,0
M1     LD A,R
       JP PO,QUIT
       INC BC
       LD A,B
       OR C
       JR NZ,M1
       RET
QUIT   LD A,1
       RET


   Если  распознался  эмулятор, можно либо
прекращать  выполнение  программы (своеоб-
разная  защита),  либо отключать некоторые
участки  программы,  которые могут неверно
работать  под эмулятором (например, вместо
прямой  работы  с  ВГ93 использовать точку
входа #3D13 и т.п.).

10. Исправление STS 6.2
───────────────────────


   В  известном  отладчике STS определение
состояния триггера прерываний также проис-
ходит с помощью команды LD A,R. Из-за это-
го  может  неправильно выполняться трасси-
ровка  программы.  При трассировке STS за-
пускает каждую команду (кроме команд пере-
дачи  управления)  с  помощью резидента, а
после  окончания  ее выполнения запоминает
содержимое  регистров процессора и состоя-
ние триггера прерываний. Вот тут и возмож-
ны ошибки.

   Допустим,  прерывания разрешены и трас-
сируется такая простейшая программа:

#8000 NOP
#8001 JR #8000


   Прекратив  трассировку  через некоторое
время  (при отключенной опции Indicate од-
ной  минуты вполне достаточно), мы увидим,
что  прерывания  оказались запрещены. Если
бы  трассировалась реальная программа, та-
кое  запрещение  прерываний  могло оказать
влияние  на весь ход ее дальнейшего выпол-
нения  и  даже  привести к зависанию (если
при трассировке попалась бы команда HALT).


   Совершенно  очевидно,  что  в  STS надо
внести  исправления. Вот  как  это сделать
для версии 6.2:

   Сначала нужно запустить STS и загрузить
файл  "sts6.2 <C>", в котором и будут про-
изводиться исправления.
   Затем  нужно отыскать свободные 14 байт
- их назначение будет объяснено ниже. Мож-
но использовать буфер функции пользователя
(с адреса #FE37). Но в той версии STS, ко-
торую  я  использую,  этот буфер занят под
процедуру дизассемблирования с метками ас-
семблера ZX ASM, поэтому я решил сократить
некоторые текстовые сообщения:

 'Block'    -> 'Bl.'  (экономится 2 байта)
 'Save'     -> 'S.'   (----/----- 2 --/--)
 'Load'     -> 'L.'   (----/----- 2 --/--)
 'DEFB'     -> ' '    (----/----- 4 --/--)
 'FileName' -> 'Name' (----/----- 4 --/--)

   Для  этого  с адреса #EB24 нужно ввести
следующую последовательность байт:

    #EB24: AE 46 72 6F ED 54 EF 46
    #EB2C: 69 6C E5 53 65 63 74 6F
    #EB34: F2 53 AE 4C AE 53 74 6F
    #EB3C: 70 20 69 E6 42 61 6E EB
    #EB44: 51 75 69 F4 54 72 61 63
    #EB4C: E5 53 74 61 72 F4 44 69
    #EB54: 73 61 73 ED A0 46 69 6C1
    #EB5C: E5 42 41 53 49 C3 20 44
    #EB64: 4F D3


   По  адресу  #E702 заменяем значение #0A
на  #0E,  чтобы  правильно  печаталось имя
файла  (т.к.  вместо строки FileName оста-
лось просто Name).


   Итак, теперь с адреса #EB66 свободно 14
байт. Смотрим, где  в STS происходит опре-
деление состояния триггера прерываний:

#DFFE: LD (#5BA1),SP
       LD SP,#5BA1
       PUSH BC
       PUSH AF
       LD A,R
       DI
       LD BC,#7FFD
       LD A,#1F
       OUT (C),A
       LD B,#BF
       LD A,#00
       OUT (C),A
       JP #E028


   Заменим  команды  LD  A,R: DI на NOP, а
команду  JP  #E028 - на JP #EB66. С адреса
#EB66 поместим такой фрагмент:

     #EB66: LD A,R
            JP PO,#EB6E┐
            NOP        │
       ┌─   JR #EB70   │
       │    LD A,R <───┘
       └>   DI
            JP #E028


   Обратите внимание - этот фрагмент в лю-
бом  случае  при  своей работе увеличивает
регистр  R  на одинаковую величину (на 7).
Дело  в том, что далее будет выполнена еще
одна  команда  LD  A,R, на этот раз нужная
уже для определения значения регистра R, и
будет  произведена  коррекция  полученного
значения,  т.к. значение регистра R увели-
чивается  с каждой выполненной командой, а
надо узнать его значение на момент оконча-
ния  выполнения  трассируемой команды. Вот
как это выглядит:

#DCA2: LD A,#5A
       LD HL,#FEF4
       SLA (HL)
       RLA
       ADD A,(HL)
       RRCA
       LD (HL),A
       RET


   Константу  #5A  по адресу #DCA3 следует
заменить  на  #53,  т.е.  уменьшить на 7 -
ведь  в  программу  был  добавлен дополни-
тельный  фрагмент, увеличивающий регистр R
на  7,  и надо скомпенсировать это измене-
ние.

   После  этого  остается  только записать
измененный файл на диск.




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Лаборатория - О доработках Speccy: Аппаратная пауза, квази-турбо, Кварц 14300.
Абзац - Вот и появилось еще одно информационное издание на нашей любимом Спектруме.
Система - обзор системных программ: KSA Sound Tracker, Mega Screen, Pro Tracker.

В этот день...   14 августа