Как уже отмечалось, при разработке 16-разрядных программ реального режима, предназначенных для выполнения по управлением операционной системы MS-DOS, вполне допустимо использование ряда дополнительных возможностей 32-разрядных процессоров. В реальном режиме можно использовать:
32-разрядные операнды;
дополнительные команды и расширенные возможности команд МП 86;
дополнительные режимы адресации;
четыре сегментных регистра для адресации данных вместо двух.
Для того, чтобы транслятор распознавал все эти средства, необходимо начать программу с директивы .586 (или, при желании, .486 или .386) и указать при этом для сегментов команд и данных описатель use 16, чтобы программа осталась 16-разрядной.
Следует заметить, что возможности использования в программах реального режима дополнительных средств 32-разрядных процессоров, хотя и кажутся привлекательными, в действительности весьма ограничены. Новых команд не так уж много, и они не имеют принципиального характера; 32-разрядные данные используются в прикладных программах относительно редко (если не касаться вычислительных программ, содержащих действительные числа, но такие программы редко пишут на языке ассемблера); расширенные возможности адресации в полной мере проявляются лишь в 32-разрядных программах, не работающих в DOS. Тем не менее в каких-то случаях привлечение средств 32-разрядных процессоров может оказаться полезным и в 16-разрядных программах, и мы приведем несколько примеров их использования.
Среди системных данных DOS и BIOS есть данные, требующие для своего размещения 2 слов. К таким данным, в частности, относится системное время, накапливаемое в 4х-байтовой ячейке с абсолютным адресом 46Ch. Выше, в разделе 3.5, уже описывалась системная процедура отсчета текущего времени. В процессе начальной загрузки компьютера в ячейку с адресом 46Ch переносится из часов реального времени время, истекшее от начала суток, а затем содержимое этой ячейки увеличивается на 1 каждым прерыванием от системного таймера, подключенного к вектору 8. Чтение ячейки 46Ch позволяет определить текущее время с погрешностью приблизительно в 1/18 секунды, что позволяет достаточно точно измерять интервалы времени. Арифметические действия с системным временем удобно выполнять в расширенных 32-разрядных регистрах.
Рассмотрим программу, которая позволяет установить требуемый временной интервал и отработать некоторым образом его окончание. Поскольку MS-DOS является однозадачной системой, единственным способом организации параллельных процессов - выполнения программы и ожидания окончания временного интервала - является использование механизма прерываний. В нашем случае программа содержит обработчик прерываний от системного таймера, который 18 раз в секунду читает системное время и сравнивает его значение с заданной заранее величиной. При достижении равенства обработчик прерываний либо сам отрабатывает это событие, либо устанавливает флаг окончания временного интервала, который периодически тестируется в основной программе. Первый вариант позволяет измерить временной интервал с большей точностью, но второй предоставляет больше возможностей, так как в обработчике прерываний нельзя обращаться к функциям DOS, а основная программа может делать все, что ей заблагорассудится.
Приведенный ниже пример выполнен в виде программы типа .СОМ. Такая организация программы упрощает обработчик прерываний и облегчает его написание. Дело заключается в том, что процессор, переходя по аппаратному прерыванию на обработчик прерывания, модифицирует только регистры CS:IP (значениями, полученными из вектора прерываний). Все остальные регистры, в том числе и сегментные, сохраняют те значения, которые они имели на момент прерывания. Значения эти могут быть какими угодно, особенно, если основная программа вызывает функции DOS. Поэтому, если в обработчике прерываний необходимо обратиться к данным, хранящимся в основной программе, нам необходимо настроить какой-либо из сегментных регистров (например, DS или ES) на сегментный адрес сегмента данных основной программы. Если же программа написана в формате .СОМ, то ее поля данных входят в тот же (единственный) сегмент, где расположены команды, и для обращения к данным можно воспользоваться регистром CS, который при вызове обработчика настраивается процессором.
Пример 4-1. Чтение и сравнение системного времени по прерываниям от таймера
.586 ;Будут 32-разрядные операндыassume CS : code, DS:codecode segment use16 ;16-разрядное приложениеorg 100h ;Формат .COMmain proc;Сохраним исходный векторmov AX,3508hint 21hmov word ptr old_08,BXmov word ptr old_08+2,ES;Установим наш обработчикmov AX,2508hmov DX,offset new_08int 21h;Прочитаем системное время, прибавим требуемый интервал;и сохраним в двухсловной ячейке памятиmov AX,40h ;Настройка ES наmov ES,AX ;область данных BIOSmov EAX, ES : 6Ch ;Получаем системное времяadd EAX,time_int ;Прибавить интервалmov time_end,EAX ;Сохраним в памяти;Имитация рабочего цикла программы с опросом флагаagain: test flag,0FFh ;Проверка флага готовностиjnz ok ;Если установлен, на OKmov АН,02h ;B ожидании окончанияmov DL,'.' ;временного интервалаint 2 In ;выводим на экран точкиjmp again ;И снова на проверку флагаok: mov АН,09h ;Интервал завершен.mov DX,offset msg ;Выполним, что хотелиint 2 In;Завершим программу, восстановив сначала исходный векторlds DX,old_08mov AX,2508hint 21hmov AX,4C00hint 21nmain endp;Наш обработчик прерываний от системного таймераnew_08 procpushf ;Запишем флаги в стекcall CS:old_08 ;и вызовем системный обработчикpush EAX ;Сохраним используемыеpush ES ;регистрыmov AX,40h ;Настроим ESmov ES,AX ;на область данных BIOSmov EAX,ES:6Ch;Прочитаем текущее времяcmp EAX,CS:time_end ;Сравним с вычисленнымjb ex ;Если меньше, на выходinc CS:flag ;Интервал истек, установим флагex: mov AL,20h ;Команда конца прерыванияout 20h,AL ;в контроллер прерыванийpop ES ;Восстановимpop EAX ;сохраненные регистрыiret ;Выход из обработчикаnew_08 endp;Поля данных программыold_08 dd 0 ;Для хранения исходного вектораtime_int dd 18*2 ;Требуемый интервал (~2с)time_end dd 0 ;Момент истечения интервалаflag db 0 ;Флаг истечения интервалаmsg db "Время истекло !$' ;Информационное сообщениеcode endsend main
Организация программного комплекса с обработчиком прерываний от системного таймера уже рассматривалась в примере 3-3 в гл. 3. Установка обработчика в рассматриваемом примере выполняется немного проще, так как нет необходимости настраивать регистр DS на сегмент данных - он и так уже настроен на единственный сегмент программы. Установив обработчик, программа настраивает регистр ES на область данных BIOS и считывает из ячейки с адресом 46Ch текущее системное время командой add к нему прибавляется заданный в ячейке time_int интервал (в примере - приблизительно 2 с), и результат сохраняется в ячейке timc_cnd.
Действия по установке обработчика закончены, и программа может приступить к выполнению запланированных для нее действий. В данном примере программа в цикле вызывает функцию DOS 02h вывода на экран символа точки; в действительности она может, например, выполнять обработку и вывод на экран некоторых данных. В каждом шаге цикла происходит тестирование флага окончания временного интервала flag, который должен быть установлен обработчиком прерываний по истечении заданного временного интервала. Пока флаг сброшен, цикл продолжается. Как только флаг окажется установлен, программа приступает к выполнению действий по отработке этого события. В рассматриваемом примере выполняется вывод на экран информационного сообщения и завершение программы с обязательным восстановлением исходного содержимого вектора 8.
Обработчик прерываний new_08 прежде всего выполняет вызов исходного обработчика, адрес которого мы сохранили в ячейке old_08. Методика сцепления обработчиков прерываний рассматривалась в гл.З (см. пример 3-4). В данном случае сцепление обработчиков необходимо, так как подключение к вектору 8 нашего обработчика не должно нарушить ход системных часов.
После возврата из системного обработчика выполняется сохранение используемых регистров, настройка регистра ES на область данных BIOS, чтение текущего времени и сравнение его с записанным в ячейке time_end. Пока текущее время меньше заданного, обработчик просто завершается командой iret, послав предварительно в контроллер прерываний команду конца прерывания EOI и восстановив сохраненные ранее регистры. Если же заданный временной интервал истек, и текущее время оказывается равным (или большим) значению в ячейке time_end, обработчик перед своим завершением устанавливает флаг flag, инициируя в основной программе запланированные для этого события действия. Если такими действиями должно быть, например, включение или выключение аппаратуры, подключенной к компьютеру, это можно сделать в самом обработчике прерываний. В этом случае флаг flag не нужен, и действия основной программы и обработчика прерывании протекают параллельно и независимо.
Рассмотренную программу нетрудно модифицировать так, чтобы флаг flag устанавливался не после истечения заданного интервала, а в заданный момент календарного времени. Эта задача позволит нам проиллюстрировать приемы выполнения арифметических операций с 32-разрядными операндами.
Пример 4-2 отличается от предыдущего только изменением алгоритма вычисления времени и служебными полями данных. Процедуры установки обработчика прерываний, цикла ожидания установки флага и самого обработчика прерываний полностью совпадают с примером 4-1.
Для получения требуемого значения времени в тех же единицах, которые используются системой при работе с ячейкой 46Ch, надо сначала вычислить время в секундах от начала суток, а затем для получения времени в тактах таймера умножить эту величину на 18,2065 (см. раздел 3.5). Для того, чтобы не привлекать арифметический сопроцессор и оставаться в рамках целых 32-битовых чисел, умножение числа секунд на 18.2065 выполняется по следующей формуле:
Такты = t*18 + t/5 + t/154
Отлаживая на машине пример 4-2, надо следить за тем, чтобы заданное время было больше текущего по машинным часам, иначе программа будет вечно ожидать установки флага. Попытка завершить ее нажатием комбинации / приведет к зависанию системы, так как в этом случае не будут выполнены строки восстановления исходного содержимого перехваченного программой вектора. По-настоящему в программах, содержащих обработчики каких-либо прерываний, используемых системой, необходимо предусматривать собственные средства обработки нажатия /, чтобы аварийное завершение программы выполнялось так же корректно, как и штатное, с предварительным с восстановлением векторов.
Пример 4-2. Ожидание заданного момента времени по прерываниям от таймера
.586assume CS:code,DS:codecode segment use16org 100hmain proc;Сохраним исходный вектор...;Установим наш обработчик...;Преобразуем требуемое календарное время в количество;интервалов по 55 мсmov EAX,hour ;Возьмем часыmov EBX,3600 ; Коэффициент преобразования в секундыmul EBX ;Преобразуем часы в секунды в EDX:EAXmov temp,EAX ;Сохраним часы в tempmov EAX,min ;Возьмем минутыmov EBX,60 ;Коэффициент преобразования в секундыmul EBX ;Преобразуем минуты в секунды в EDX:EAXadd temp,EAX ;Прибавим минуты в tempmov EAX,sec ;Возьмем секундыadd temp,EAX ;Прибавим секунды в tempmov EAX,temp ;Число секундmov EBX,18 ;Будем умножать на 18mul EBX ;Умножим на 18mov time,EAX ;Сохраним в timexor EDX,EDX ;Подготовимся к делениюmov EAX,temp ;Будем делить число секундmov EBX,5 ;Будем делить на 5div EBX ;Поделимadd time,EAX ;Прибавим к timexor EDX,EDX ;Подготовимся к делениюmov EAX,temp ;Будем делить число секундmov EBX,154 ;Будем делить на 154div EBX ;Поделимadd time,EAX ;Прибавим к time;Имитация рабочего цикла программы с опросом флага...;Завершим программу, восстановив сначала исходный вектор...main endpnew_08 proc...new_08 endpold_08 dd 0hour dd 13 ;Часыmin dd 45 ;Минутыsec dd 0 ;Секундыtime dd 0 ;Вычисленное время в тактах таймераtemp dd 0 ;Ячейка для промежуточного результатflag db 0 ;Флаг наступления заданного времениmsg db "Время наступило!$'code endsend main
Рассмотрим некоторые детали приведенного примера.
Три ячейки для хранения заданного времени (часов, минут и секунд) объявлены оператором dd, как двойные слова для упрощения программы и ускорения загрузки этих значений в расширенный регистр ЕАХ. Если бы мы, экономя память, отводимую по данные, объявили бы эти ячейки как байты, то загрузка, например, числа часов в регистр ЕАХ выглядела бы следующим образом:
xor EAX,EAXmov AL,hour
Для преобразования часов в секунды мы должны число часов умножить на 3600. Оба сомножителя (3600 и максимум 23) представляют собой небольшие числа, которые поместились бы в 16-разрядный регистр. Однако результат может достигнуть величины 82800, которая в регистр АХ уже не поместится. Если бы мы выполнили умножение двух 16-разрядных регистров, например, АХ на ВХ, то результат (по правилам выполнения команды mul) оказался бы в паре регистров DX:AX, и нам пришлось бы эти два числа объединять в одно несколькими операциями переноса и сдвига:
push AX ; Сохраняем на время АХmov AX,DX ;Старшая половина произведенияsal ЕАХ,1б ;Сдвигаем в старшую половину ЕАХpop AX ;Младшая половина произведения
Выполняя умножение с использованием 32-разрядных регистров, мы получаем результат опять же в паре регистров EDX:EAX, но поскольку в нашем случае произведение никогда не превысит 4 Г, все оно целиком будет находиться в одном регистре ЕАХ, и мы избавляемся от приведенной выше процедуры. Результат умножения сохраняется во вспомогательной ячейке temp.
Аналогичным образом выполняется перевод числа минут в секунды; полученный результат прибавляется к содержимому ячейки temp.
Число секунд преобразовывать не надо, оно просто прибавляется к содержимому temp.
Полученное число секунд умножается на 18, и результат помещается в ячейку time, которая затем будет опрашиваться в обработчике прерываний.
К полученному числу тактов таймера надо прибавить еще две корректирующих величины - результаты деления числа секунд на 5 и на 154. При использовании в операции деления 32-разрядных регистров делимое помещается в пару регистров EDX:EAX. В нашем случае делимое целиком помещается в ЕАХ, и регистр EDX необходимо обнулить. Для этого можно было выполнить команду
mov ЕАХ,0
но более эффективна операция
хоr ЕАХ,ЕАХ
которая при любом содержимом ЕАХ оставляет в нем 0.
При делении EDX:EAX на ЕВХ частное помещается в ЕАХ, остаток в EDX. Остаток нас не интересует, а частное (первая корректирующая величина) прибавляется к содержимому ячейки temp.
Аналогичным образом то же число секунд из ячейки tmp делится на 154, и результат прибавляется к содержимому time. Преобразование закончено.
В заключение рассмотрим пример упорядочивания массива 32-разрядных чисел в убывающем порядке методом пузырьковой сортировки. В приведенном алгоритме используются расширенные возможности адресации 32-разрядных процессоров.
Пример 4-3. Пузырьковая сортировка
.586assume CS:code,DS:datacode segment use16main procmov AX, data ;Настроим DSmov DS,AX ;на сегмент данныхmov ESI,offset list ;ESI-> начало массиваmov ECX,1000 ;Число элементов в массивеstart: mov EDX, 0 ;Индекс сравниваемой парыsort: cmp EDX,ECX ;Индекс пары дошел доjge stop ;индекса массива? К следующей пареmov EAX,[ESI+EDX*4+4];Второй элемент парыcmp [ESI+EDX*4],EAX ;Сравним с предыдущимjge noswap ;Если первый больше, то хорошоxchg [ESI+EDXM] , EAX ;Первый меньше. Обменятьmov [ESI+EDXM + 4],EAX ;первый на второйnoswap: inc EDX ;Увеличим индекс парыjmp sort ;И на сравнениеstop: loop start ;Цикл по всем элементамmov AX,4C00hint 21hmain endpcode endsdata segmentlist label ;Имя тестового массиваnmb=0 ;Заполним массив на этапеrept 1000 /трансляции числами от 0ddnmb /до 990nmb=nmb+10 /через 10endmdata endsstk segment stackdw 128 dup (0)stk endsend main
Алгоритм пузырьковой сортировки предусматривает выполнение двух вложенных циклов. Во внутреннем цикле сравниваются пары элементов. Первый элемент берется по адресу [ESI + EDX * 4], второй - по следующему адресу [ESI + EDX * 4 + 4]. Если второй элемент больше первого, происходит обмен значений этих элементов, и элемент с меньшим значением "всплывает" на одно место выше (т.е. перемещается по большему адресу). После этого увеличивается индекс пары и выполняется сравнение второго элемента со следующим. Если оказывается, что следующий элемент больше предыдущего, они меняются местами. В результате элемент с самым маленьким значением всплывает на самый верх списка.
Внутренний цикл, пройдясь по всем парам, повторяется сначала, обеспечивая всплывание следующего по величине элемента. Каждый следующий проход внутреннего цикла требует на 1 меньше шагов, чем предыдущий. После завершения упорядочивания элементы выстраиваются по возрастающим адресам в порядке уменьшения их значений.
В примере 4-3 тестовый массив данных образован из возрастающих (на 10) чисел от 0 до 990. В результате упорядочивания они должны расположиться в обратном порядке, от больших к меньшим. В примере не предусмотрены средства вывода на экран элементов массива, поэтому его изучение следует проводить в отладчике, наблюдая всплывание каждого элемента.
Как уже отмечалось, в 32-разрядных процессорах увеличено до 4 число сегментных регистров данных. Это дает возможность совместной работы с четырьмя сегментами данных (общим объемом до 256 Кбайт) без перенастройки сегментных регистров. Структура такого рода программы может выглядеть следующим образом:
.586datal segment use16first dw 7000h dup(')datal endsdata2 segment use6second dw 7000h dup (')data2 endsdata3 segment use16third dw 7000h dup (')data3 endsdata4 segment use16forth dw 7000h dup (')data4 endscode segment use16assume DS:datal,ES:data2,FS:data3,GS:data4main proc;Настроим все 4 сегментных регистра на базовые адреса; соответствующих сегментовmov AX,datal ;DS->datalmov word ptr[BX],1111h ;Обращение через DS по умолчанию;Обращение к разным сегментам с явным указанием;требуемого сегментного регистра (замена сегмента)mov word ptr ES:[BX],2222hmov word ptr FS:[BX],3333hmov word ptr GS:[BX],4444h;Обращение по именам полей данных разных сегментов ; с учетом действия директивы assumemov first,1 ;Запись в сегмент datalmov second,2 ;Запись в сегмент data2mov third,3 ;Запись в сегмент data3mov fourth,4 ;Запись в сегмент data4; Перенос данных из сегмента в сегментpush firstpop second+2push thirdpop fourth+2...main endpcode ends
В программе объявлены 4 сегмента данных с именами datal, data2, data3 и data4, содержащие массивы 16-разрядных данных с именами first, second, third и fourth. Длина каждого массива составляет 56 Кбайт, и, таким образом, общий объем данных, доступных программе в любой момент, составляет более 200 Кбайт. Сегменты данных описаны до сегмента команд, что в данном случае имеет значение. В сегменте команд с помощью директивы assume указано соответствие каждому из сегментов своего сегментного регистра (DS, ES, FS и GS). Это даст нам возможность обращаться по именам полей сегментов без явного указания соответствующих этим сегментам сегментных регистров.
Программа начинается с обычной практически для всех программ процедуры настройки всех сегментных регистров. Стоит еще раз повторить, что директива assume лишь обеспечивает правильную трансляцию программы, но не инициализирует сегментные регистры. "Правильная трансляция" в данном случае заключается в том, что при обработке команд, в которых упоминаются имена данных того или иного сегмента, ассемблер автоматически предваряет эти команды, префиксом замены сегмента, выбирая для замены сегментный регистр, указанный в директиве assume для данного сегмента. Так, команда
mov first, I
преобразуется в последовательность кодов (по листингу' трансляции)
С7 06 0000r 0001
где С7 06 - это код команды mov в случае прямой адресации памяти и использования непосредственного операнда, 0000г - смещение адресуемой ячейки, а 0001 - непосредственный операнд (все числа, разумеется, шестнадцатеричные). Здесь нет префикса замены сегмента, потому что адресуется сегмент, которому соответствует регистр DS, используемый процессором по умолчанию. Однако команды с обращением к другим сегментам транслируются с включением в их коды соответствующих пре фиксов, несмотря на то, что в исходных предложениях не указаны сегментные регистры, а содержатся только ссылки на (уникальные) имена ячеек тех или иных сегментов:
mov second, 2 ; Код команды 26: С7 06 0000r 0002mov third, 3 ;Код команды 64: С7 06 0000r 0003mov fourth, 4 ; Код команды 65: С7 06 0000r 0004
Настроив сегментные регистры, мы можем обращаться к полям данных всех четырех сегментов с использованием любых способов адресации. В приведенном фрагменте в регистр ВХ помещается смещение последней ячейки любого из массивов, после чего с помощью косвенной базовой адресации в последние слова всех четырех массивов записываются произвольные числа 1111h, 2222h, 3333h и 4444h. Во всех случаях требуется описатель word ptr, так как по виду команды ассемблер не может определить, хотим ли мы занести в память байт, слово или двойное слово. При обращении к сегментам, адресуемых не через DS, необходимо явное указание сегментного регистра (которое будет преобразовано в код префикса замены сегмента), потому что по виду команды с адресацией через регистры транслятор не может определить, к какому сегменту происходит обращение.
Проще обстоит дело, если в команде указаны имена ячеек сегментов. В этом случае, как уже говорилось выше, транслятор автоматически включает в код команды требуемый префикс замены сегмента.
Наконец, в конце программы приведены строки пересылки данных из сегмента в сегмент через стек. Они убедительны в том отношении, что в четырех последовательных командах производится обращение к четырем различным сегментам программы без перенастройки сегментных регистров, которую пришлось бы выполнить, если бы мы ограничились возможностями МП 86.
Комментариев нет:
Отправить комментарий