Voyager
#03
19 октября 1998 |
|
Скелет - Недокументированная особенность процессора Z80.
(C) Иван Рощин, Москва Music: WORLD without LIMIT/KENOTRON ─────────────────────────────────────────────────── ╔════════════════════════════════════╗ ║ ║ ║ Недокументированная особенность ║ ║ процессора 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). Я стал замечать, что при работе этого фраг- мента BestViev зависает - не всегда и даже не слиш- ком часто, а в очень редких случаях. Но, все равно, это было не очень-то приятно. Программа, вроде бы, не содержала никаких ошибок, по крайней мере с пер- вого взгляда ничего подозрительного я не заметил. Оставалось лишь прибегнуть к более сильным средс- твам... 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'а прерывания всегда будут запре- щены, и, соответственно, BestView всегда будет за- висать. .... 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-го рода? Добавляю к программе несколько команд, устанавливающих ре- жим IM2 с обработчиком, состоящим всего из двух команд: 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 Запускаю... И что я вижу? Верхняя часть bor- der'а мигает зеленым цветом: ┌─────────────────────────────────┐ │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│ │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│ │▒▒▒▒ ▒▒▒▒│ │▒▒▒▒ ▒▒▒▒│ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────────────┘ Это говорит о том, что, во-первых, команда LD A,R действительно иногда неверно устанавливает бит P/V, и во-вторых, что это происходит в момент прихода прерывания, а не когда угодно (действитель- но, тогда бы border мигал зеленым в совершенно про- извольных местах). То, что верхняя часть border'а мигает, а не постоянно окрашена в зеленый цвет, тоже получает свое объяснение. По-видимому, команда LD A,R непра- вильно работает лишь тогда, когда импульс прерыва- ния приходит во время ее выполнения, а так бывает далеко не всегда - прерывание может произойти и во время выполнения другой команды. * ПРОДОЛЖЕНИЕ СЛЕДУЕТ * ─────────────────────────────────────────────────── ому, команда LD A,R непра- вильно работает лишь тогда, когда импульс прерыва- ния приходит во время ее выполнения, а так бывает далеко не всегда - прерывание может произойти и во время выполнения другой команды. * ПРОДОЛЖЕНИЕ СЛЕДУЕТ * (C) Иван Рощин, Москва Music: BURATINO remixed by KENOTRON ─────────────────────────────────────────────────── ╔═════════════════════════════════════╗ ║ ║ ║ Недокументированная особенность ║ ║ процессора Z80 ║ ║ (окончание) ║ ║ ║ ╚═════════════════════════════════════╝ 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 AND A 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_BP1EXX 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 6C #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, и надо скомпенсировать это изменение. После этого остается только записать изменен- ный файл на диск. ─────────────────────────────────────────────────── ь в програм- му был добавлен дополнительный фрагмент, увеличива- ющий регистр R на 7, и надо скомпенсировать это изменение. После этого остается только записать изменен- ный файл на диск.
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября