Использование средств 32-разрядных процессоров в программировании
Как уже отмечалось, при разработке 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:code
code segment use16 ;16-разрядное приложение
org 100h ;Формат .COM
main proc
;Сохраним исходный вектор
mov AX,3508h
int 21h
mov word ptr old_08,BX
mov word ptr old_08+2,ES
;Установим наш обработчик
mov AX,2508h
mov DX,offset new_08
int 21h
;Прочитаем системное время, прибавим требуемый интервал
;и сохраним в двухсловной ячейке памяти
mov AX,40h ;Настройка ES на
mov ES,AX ;область данных BIOS
mov EAX, ES : 6Ch ;Получаем системное время
add EAX,time_int ;Прибавить интервал
mov time_end,EAX ;Сохраним в памяти
;Имитация рабочего цикла программы с опросом флага
again: test flag,0FFh ;Проверка флага готовности
jnz ok ;Если установлен, на OK
mov АН,02h ;B ожидании окончания
mov DL,'.' ;временного интервала
int 2 In ;выводим на экран точки
jmp again ;И снова на проверку флага
ok: mov АН,09h ;Интервал завершен.
mov DX,offset msg ;Выполним, что хотели
int 2 In
;Завершим программу, восстановив сначала исходный вектор
lds DX,old_08
mov AX,2508h
int 21h
mov AX,4C00h
int 21n
main endp
;Наш обработчик прерываний от системного таймера
new_08 proc
pushf ;Запишем флаги в стек
call CS:old_08 ;и вызовем системный обработчик
push EAX ;Сохраним используемые
push ES ;регистры
mov AX,40h ;Настроим ES
mov ES,AX ;на область данных BIOS
mov 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 ends
end 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, надо следить за тем, чтобы заданное время было больше текущего по машинным
часам, иначе программа будет вечно ожидать установки флага. Попытка завершить
ее нажатием комбинации <Ctrl>/<C> приведет к зависанию системы,
так как в этом случае не будут выполнены строки восстановления исходного содержимого
перехваченного программой вектора. По-настоящему в программах, содержащих обработчики
каких-либо прерываний, используемых системой, необходимо предусматривать собственные
средства обработки нажатия <Ctrl>/<C>, чтобы аварийное завершение
программы выполнялось так же корректно, как и штатное, с предварительным с восстановлением
векторов.
Пример 4-2. Ожидание заданного момента времени по прерываниям от таймера
.586
Рассмотрим некоторые детали
приведенного примера.
Три ячейки для хранения заданного времени (часов, минут и секунд) объявлены
оператором dd, как двойные слова для упрощения программы и ускорения загрузки
этих значений в расширенный регистр ЕАХ. Если бы мы, экономя память, отводимую
по данные, объявили бы эти ячейки как байты, то загрузка, например, числа часов
в регистр ЕАХ выглядела бы следующим образом:
xor EAX,EAX
mov 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. Пузырьковая сортировка
.586
assume CS:code,DS:data
code segment use16
main proc
mov AX, data ;Настроим DS
mov 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,4C00h
int 21h
main endp
code ends
data segment
list label ;Имя тестового массива
nmb=0 ;Заполним массив на этапе
rept 1000 /трансляции числами от 0
ddnmb /до 990
nmb=nmb+10 /через 10
endm
data ends
stk segment stack
dw 128 dup (0)
stk ends
end main
Алгоритм пузырьковой сортировки
предусматривает выполнение двух вложенных циклов. Во внутреннем цикле сравниваются
пары элементов. Первый элемент берется по адресу
[ESI + EDX * 4], второй - по
следующему адресу [ESI + EDX * 4 + 4]. Если второй элемент больше первого, происходит
обмен значений этих элементов, и элемент с меньшим значением "всплывает"
на одно место выше (т.е. перемещается по большему адресу). После этого увеличивается
индекс пары и выполняется сравнение второго элемента со следующим. Если оказывается,
что следующий элемент больше предыдущего, они меняются местами. В результате
элемент с самым маленьким значением всплывает на самый верх списка.
Внутренний цикл, пройдясь по всем парам, повторяется сначала, обеспечивая всплывание
следующего по величине элемента. Каждый следующий проход внутреннего цикла требует
на 1 меньше шагов, чем предыдущий. После завершения упорядочивания элементы
выстраиваются по возрастающим адресам в порядке уменьшения их значений.
В примере 4-3 тестовый массив данных образован из возрастающих (на 10) чисел
от 0 до 990. В результате упорядочивания они должны расположиться в обратном
порядке, от больших к меньшим. В примере не предусмотрены средства вывода на
экран элементов массива, поэтому его изучение следует проводить в отладчике,
наблюдая всплывание каждого элемента.
Как уже отмечалось, в 32-разрядных процессорах увеличено до 4 число сегментных
регистров данных. Это дает возможность совместной работы с четырьмя сегментами
данных (общим объемом до 256 Кбайт) без перенастройки сегментных регистров.
Структура такого рода программы может выглядеть следующим образом:
.586
datal segment use16
first dw 7000h dup(')
datal ends
data2 segment use6
second dw 7000h dup (')
data2 ends
data3 segment use16
third dw 7000h dup (')
data3 ends
data4 segment use16
forth dw 7000h dup (')
data4 ends
code segment use16
assume DS:datal,ES:data2,FS:data3,GS:data4
main proc
;Настроим все 4 сегментных регистра на базовые адреса
; соответствующих сегментов
mov AX,datal ;DS->datal
mov word ptr[BX],1111h ;Обращение через DS по умолчанию
;Обращение к разным сегментам с явным указанием
;требуемого сегментного регистра (замена сегмента)
mov word ptr ES:[BX],2222h
mov word ptr FS:[BX],3333h
mov word ptr GS:[BX],4444h
;Обращение по именам полей данных разных сегментов ; с учетом действия директивы assume
mov first,1 ;Запись в сегмент datal
mov second,2 ;Запись в сегмент data2
mov third,3 ;Запись в сегмент data3
mov fourth,4 ;Запись в сегмент data4
; Перенос данных из сегмента в сегмент
push first
pop second+2
push third
pop fourth+2
...
main endp
code 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 0002
mov third, 3 ;Код команды 64: С7 06 0000r 0003
mov fourth, 4 ; Код команды 65: С7 06 0000r 0004
Настроив сегментные регистры,
мы можем обращаться к полям данных всех четырех сегментов с использованием любых
способов адресации. В приведенном фрагменте в регистр ВХ помещается смещение
последней ячейки любого из массивов, после чего с помощью косвенной базовой
адресации в последние слова всех четырех массивов записываются произвольные
числа 1111h, 2222h, 3333h и 4444h. Во всех случаях требуется описатель word
ptr, так как по виду команды ассемблер не может определить, хотим ли мы занести
в память байт, слово или двойное слово. При обращении к сегментам, адресуемых
не через DS, необходимо явное указание сегментного регистра (которое будет преобразовано
в код префикса замены сегмента), потому что по виду команды с адресацией через
регистры транслятор не может определить, к какому сегменту происходит обращение.
Проще обстоит дело, если в команде указаны имена ячеек сегментов. В этом случае,
как уже говорилось выше, транслятор автоматически включает в код команды требуемый
префикс замены сегмента.
Наконец, в конце программы приведены строки пересылки данных из сегмента в сегмент
через стек. Они убедительны в том отношении, что в четырех последовательных
командах производится обращение к четырем различным сегментам программы без
перенастройки сегментных регистров, которую пришлось бы выполнить, если бы мы
ограничились возможностями МП 86.