Дополнительные режимы адресации

Режимы адресации 32-разрядных процессоров разработаны, исходя из требований образования 32-битового смещения. Другими словами, они предназначены для 32-разрядных приложений, в которых сегменты данных или стека (как, впрочем, и сегменты команд) могут иметь размеры до 232 = 4 Гбайт. Однако в реальном режиме размер любого сегмента ограничивается величиной 216 = 64 Кбайт, и 32-битовые смещения не имеют смысла. С другой стороны, ничто не мешает нам использовать для образования 16-битового смещения 32-разрядные регистры (ЕВХ, ESI и проч.), если, конечно, их реальное содержимое не будет превышать величины FFFFh. Указание в качестве операндов команд 32-разрядных регистров позволяет использовать дополнительные возможности 32-разрядных процессоров по части адресации памяти, что в некоторых случаях может оказаться полезным. Следует подчеркнуть, что речь идет здесь только о тех операндах, или, правильнее сказать, аргументах команды, которые описывают косвенную (через регистры) адресацию памяти.

В отличие от МП 86, где базовыми регистрами могут быть только ВХ и ВР, а индексными только SI и DI, 32-разрядные процессоры допускают использование в качестве и базовых, и индексных практически всех регистров общего назначения. Таким образом, вполне законна команда вида

mov ЕАХ,[ЕСХ][EDX]

Второе отличие заключается в возможности масштабирования содержимого индексного регистра, т.е. умножения его на заданный в команде коэффициент, который может принимать значения 1, 2, 4 или 8. Пример такой адресации:

inc word ptr [ЕАХ] [ЕСХ*2]

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

inc word ptr [AX] [ECX*2]

или

inc word ptr [ЕАХ] [СХ*2]

рассматриваются ассемблером, как неправильные.

Режимы косвенной адресации памяти, предоставляемые 32-разрядными процессорами при использовании 32-разрядных регистров, изображены на рис. 4.2.

Из рисунка видно, что в качестве базового можно использовать все регистры общего назначения, включая даже указатель стека ESP. При этом, если в качестве базового выступает один из регистров ESP или ЕВР, то по умолчанию адресация осуществляется через сегментный регистр SS, хотя возможна замена сегмента. Во всех остальных случаях адресация по умолчанию осуществляется через сегментный регистр DS. Использование регистра ЕВР в качестве индексного не адресует нас к стеку: адресация попрежнему осуществляется с помощью регистра DS.

clip_image001

Рис. 4.2. Режимы косвенной адресации с использованием 32-разрядных регистров.

Прочерк во второй колонке подчеркивает, что регистр ESP нельзя использовать в качестве индексного. Это не означает, что ESP нельзя указывать в качестве второго операнда:

mov ЕАХ,[ЕСХ][ESP]

Недопустима только конструкция, в которой содержимое ESP умножается на масштабирующий множитель:

mov ЕАХ,[ЕСХ][ESP*8]

Полезно также отметить, что смещение в команде вида

mov ЕАХ,[ЕВХ][ЕСХ]+20

может быть только или 8-битовым, или 32-битовым. 16-битовые смещения не образуются. Если указанная в команде величина смещения помещается в байт, как это имеет место в приведенном выше примере команды, то смещение в коде команды занимает 1 байт. Если же величина смещения больше 255, то под него в коде команды отводится сразу 32 бит.

Таким образом, понятия базовой и индексной адресации в 32-разрядных процессорах несколько размываются. Если регистр указывается с масштабирующим множителем, то это, конечно, индексная адресация. Если же множитель отсутствует, то адресацию и через ЕВХ, и через ESI с равным успехом можно отнести как к базовой, так и к индексной.

Использование для адресации памяти 16-разрядных регистров резко сужает возможности адресации 32-разрядных процессоров (рис. 4.3). В этом случае мы фактически имеем дело с МП 86.

clip_image002

Рис. 4.3. Режимы косвенной адресации с использованием 16-разрядных регистров.

Напомним, что в 16-разрядном режиме допустимы не все сочетания базовых и индексных регистров. В качестве базового регистра можно использовать только ВХ или ВР, а в качестве индексного только SI или DI.

Использование средств 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

assume CS:code,DS:code

code segment use16

org 100h

main proc

;Сохраним исходный вектор

...

;Установим наш обработчик

...

;Преобразуем требуемое календарное время в количество

;интервалов по 55 мс

mov EAX,hour ;Возьмем часы

mov EBX,3600 ; Коэффициент преобразования в секунды

mul EBX ;Преобразуем часы в секунды в EDX:EAX

mov temp,EAX ;Сохраним часы в temp

mov EAX,min ;Возьмем минуты

mov EBX,60 ;Коэффициент преобразования в секунды

mul EBX ;Преобразуем минуты в секунды в EDX:EAX

add temp,EAX ;Прибавим минуты в temp

mov EAX,sec ;Возьмем секунды

add temp,EAX ;Прибавим секунды в temp

mov EAX,temp ;Число секунд

mov EBX,18 ;Будем умножать на 18

mul EBX ;Умножим на 18

mov time,EAX ;Сохраним в time

xor EDX,EDX ;Подготовимся к делению

mov EAX,temp ;Будем делить число секунд

mov EBX,5 ;Будем делить на 5

div EBX ;Поделим

add time,EAX ;Прибавим к time

xor EDX,EDX ;Подготовимся к делению

mov EAX,temp ;Будем делить число секунд

mov EBX,154 ;Будем делить на 154

div EBX ;Поделим

add time,EAX ;Прибавим к time

;Имитация рабочего цикла программы с опросом флага

...

;Завершим программу, восстановив сначала исходный вектор

...

main endp

new_08 proc

...

new_08 endp

old_08 dd 0

hour dd 13 ;Часы

min dd 45 ;Минуты

sec dd 0 ;Секунды

time dd 0 ;Вычисленное время в тактах таймера

temp dd 0 ;Ячейка для промежуточного результат

flag db 0 ;Флаг наступления заданного времени

msg db "Время наступило!$'

code ends

end main

Рассмотрим некоторые детали приведенного примера.

Три ячейки для хранения заданного времени (часов, минут и секунд) объявлены оператором 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.

Основы защищенного режима

Микропроцессоры Pentium, так же, как и его предшественники (начиная с 80268), могут работать в двух режимах: реального адреса и виртуального защищенного адреса. Обычно эти режимы называют просто реальным и защищенным. В реальном режиме 32-разрядные микропроцессоры функционируют фактически так же, как МП 86 с повышенным быстродействием и расширенным набором команд. Многие весьма привлекательные возможности микропроцессоров принципиально не реализуются в реальном режиме, который введен лишь для обеспечения совместимости с предыдущими моделями процессоров. Характерной особенностью реального режима является ограничение объема адресуемой оперативной памяти величиной 1 Мбайт.

Только перевод микропроцессора в защищенный режим позволяет полностью реализовать все возможности, загаженные в его архитектуру и недоступные в реальном режиме. Сюда можно отнести:

- увеличение адресуемого пространства до 4 Гбайт;

- возможность работать в виртуальном адресном пространстве, превышающем максимально возможный объем физической памяти и составляющем огромную величину 64 Тбайт;

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

- страничная организация памяти, повышающая уровень защиты задач

друг от друга и эффективность их выполнения.

При включении микропроцессора в нем автоматически устанавливается режим реального адреса. Переход в защищенный режим осуществляется программно путем выполнения соответствующей последовательности команд. Поскольку многие детали функционирования микропроцессора в реальном и защищенном режимах существенно различаются, программы, предназначенные для защищенного режима, должны быть написаны особым образом. При этом различия реального и защищенного режимов настолько велики, что программы реального режима не могут выполняться в защищенном режиме и наоборот. Другими словами, реальный и защищенный режимы не совместимы.

Архитектура современного микропроцессора необычайно сложна. Столь же сложными оказываются и средства защищенного режима, а также программы, использующие эти средства. С другой стороны, при решении многих прикладных задач (например, при отладке приложений Windows, работающих, естественно, в защищенном режиме), нет необходимости в понимании всех деталей функционирования защищенного режима; достаточно иметь знакомство с его основными понятиями. В настоящем разделе дается краткое описание основ защищенного режима и его наиболее важных структур и алгоритмов функционирования.

Пожалуй, наиболее важным отличием защищенного режима от реального является иной принцип формирования физического адреса. Вспомним, что в реальном режиме физический адрес адресуемой ячейки памяти состоит из двух компонентов - сегментного адреса и смещения. Оба компонента имеют размер 16 бит, и процессор, обращаясь к памяти, пользуется следующим правилом вычисления физического адреса:

Физический адрес = сегментный адрес * 16 + смещение

И сегментный адрес, и смещение не могут быть больше FFFFh, откуда следуют два важнейших ограничения реального режима: объем адресного пространства составляет всего 1 Мбайт, а сегменты не могут иметь размер, превышающий 64 Кбайт.

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

В сегментные регистры в защищенном режиме записываются не сегментные адреса, а так называемые селекторы, биты 3...15 которых рассматриваются, как номера (индексы) ячеек специальной таблицы, содержащей дескрипторы сегментов программы. Таблица дескрипторов обычно создастся операционной системой защищенного режима (например, системой Windows) и, как правило, недоступна программе. Каждый дескриптор таблицы дескрипторов имеет размер 8 байт, и в нем хранятся все характеристики, необходимые процессору для обслуживания этого сегмента. Среди этих характеристик необходимо выделить в первую очередь две: адрес сегмента и его длину (рис. 4.4).

clip_image001

Рис. 4.4. Дескрипторы сегментов и их селекторы.

Под адрес сегмента в дескрипторе выделяется 32 бит, и, таким образом, сегмент может начинаться в любой точке адресного пространства объемом 23- = 4 Гбайт. Это адресное пространство носит название линейного. В простейшем случае, когда выключено страничное преобразование, о котором речь будет идти позже, линейные адреса отвечают физическим. Таким образом, процессор может работать с оперативной памятью объемом до 4 Гбайт.

Как и в реальном режиме, адрес адресуемой ячейки вычисляется процессором, как сумма базового адреса сегмента и смещения:

Линейный адрес = базовый адрес сегмента + смещение

В 32-разрядных процессорах смещение имеет размер 32 бит, поэтому максимальная длина сегмента составляет 2" = 4 Гбайт.

На рис. 4.4 приведен гипотетический пример программы, состоящей из трех сегментов, первый из которых имеет длину 1 Мбайт и расположен в начале адресного пространства, второй, размером 100 Кбайт, вплотную примыкает к первому, а третий, имеющий размер всего 256 байт, расположен в середине девятого по счету мегабайта.

Адреса, используемые программой для обращения к ячейкам памяти, и состоящие всегда из двух компонентов - селектора и смещения - иногда называются виртуальными. Система сегментной адресации преобразует виртуальные адреса в линейные. Поскольку таблица дескрипторов, с помощью которой осуществляется это преобразование, обычно недоступна программе, программа может не знать, в каких именно участках логического адресного пространства находятся се компоненты. Фактически это сводится к тому, что, загружая программу в память, вы не знаете, в каких местах памяти будут находиться ее сегменты, и каков будет порядок их размещения. Программисту доступны только виртуальные адреса, преобразование же их в линейные и затем в физические берет на себя операционная система.

Каков объем виртуального адресного пространства? Программа указывает номер нужного ей дескриптора с помощью селектора, в котором для индекса дескриптора отведено 13 бит. Отсюда следует, что в дескрипторной таблице может быть до 1" = 8. К дескрипторов. Однако в действительности их в два раза больше, так как программа может работать не с одной, а с двумя дескрипторными таблицами - одной глобальной, разделяемой всеми выполняемыми задачами, и одной локальной, принадлежащей конкретной задаче. В селекторе предусмотрен специальный бит (бит 2), состояние которого говорит о типе требуемой программе дескрипторной таблицы. Таким образом, всего программе могут быть доступны 214 = 16 К дескрипторов, т.е. 16 К сегментов. Поскольку размер каждого сегмента, определяемый максимальной величиной смещения, может достигать 2-1 = 4 Гбайт, объем виртуального адресного пространства оказывается равным 16 К * 4 Кбайт = = 64 Тбайт.

Реально, однако, оперативная память компьютера с 32-разрядной адресной шиной не может быть больше 4 Гбайт, т.е. при сделанных выше предположениях (16 К сегментов размером 4 Гбайт каждый) в памяти может поместиться максимум один сегмент из более чем 16 тысяч. Где же будут находиться все остальные?

Полный объем виртуального пространства может быть реализован только с помощью многозадачной операционной системы, которая хранит все неиспользуемые в настоящий момент сегменты на диске, загружая их в память по мере необходимости. Разумеется, если мы хотим полностью реализовать возможности, заложенные в современные процессоры, нам потребуется диск довольно большого объема - 64 Тбайт. Однако и при нынешних более скромных технических средствах (память до 100 Мбайт, жесткий диск до 10 Гбайт) принцип виртуальной памяти используется всеми многозадачными операционными системами с большой эффективностью. С другой стороны, для прикладного программиста этот вопрос не представляет особого интереса, так как сброс сегментов на диск и подкачка их с диска осуществляются операционной системой, а не программой, и вмешательство эту процедуру вряд ли целесообразно.

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

clip_image002

Рис. 4.5. Цепочка преобразований виртуального адреса в физический.

Страницей называется связный участок линейного или физического адресного пространства объемом 4 Кбайт. Программа работает в линейном адресном пространстве, не подозревая о существовании страничного преобразования или даже самих страниц. Механизм страничной трансляции отображает логические страницы на физические в соответствии с информацией, содержащейся в страничных таблицах. В результате отдельные 4х-килобайтовыс участки программы могут реально находиться в любых несвязных друг с другом 4х-килобайтовых областях физической памяти (рис. 4.6). Порядок размещения физических страниц в памяти может не соответствовать (и обычно не соответствует) порядку следования логических страниц. Более того, некоторые логические страницы могут перекрываться, фактически сосуществуя в одной и той же области физической памяти.

Страничная трансляция представляет собой довольно сложный механизм, в котором принимают участие аппаратные средства процессора и находящиеся в памяти таблицы преобразования. Назначение и взаимодействие элементов системы страничной трансляции схематически изображено на рис. 4.7.

Система страничных таблиц состоит из двух уровней. На первом уровне находится каталог таблиц страниц (или просто каталог страниц) - резидентная в памяти таблица, содержащая 1024 4х-байтовых поля с адресами таблиц страниц. На втором уровне находятся таблицы страниц, каждая из которых содержит так же 1024 4х-байтовых поля с адресами физических страниц памяти. Максимально возможное число таблиц страниц определяется числом полей в каталоге и может доходить до 1024. Поскольку размер страницы составляет 4 Кбайт, 1024 таблицы по 1024 страницы перекрывают все адресное пространство (4 Гбайт).

clip_image003

Рис. 4.6. Отображение логических адресов на физические.

clip_image004

Рис. 4.7. Страничная трансляция адресов.

Не все 1024 таблицы страниц должны обязательно иметься в наличии (кстати, они заняли бы в памяти довольно много места - 4 Мбайт). Если программа реально использует лишь часть возможного линейного адресного пространства, а так всегда и бывает, то неиспользуемые поля в каталоге страниц помечаются, как отсутствующие. Для таких полей система, экономя память, не выделяет страничные таблицы.

При включенной страничной трансляции линейный адрес рассматривается, как совокупность трех полей: 10-битового индекса в каталоге страниц, 10-битовбго индекса в выбранной таблице страниц и 12-битового смещения в выбранной странице. Напомним, что линейный адрес образуется путем сложения базового адреса сегмента, взятого из дескриптора сегмента, и смещения в этом сегменте, предоставленного программой.

Старшие 10 бит линейного адреса образуют номер поля в каталоге страниц. Базовый адрес каталога хранится в одном из управляющих регистров процессора, конкретно, в регистре CR3. Из-за того, что каталог сам представляет собой страницу и выровнен в памяти на границу 4 Кбайт, в регистре CR3 для адресации к каталогу используются лишь старшие 20 бит, а младшие 12 бит зарезервированы для будущих применений.

Поля каталога имеют размер 4 байт, поэтому индекс, извлеченный из линейного адреса, сдвигается влево на 2 бит (т.е. умножается на 4) и полученная величина складывается с базовым адресом каталога, образуя адрес конкретного поля каталога. Каждое поле каталога содержит физический базовый адрес одной из таблиц страниц, причем, поскольку таблицы страниц сами представляют собой страницы и выровнены в памяти на границу 4 Кбайт, в этом адресе значащими являются только старшие 20 бит.

Далее из линейного адреса извлекается средняя часть (биты 12...21), сдвигается влево на 2 бит и складывается с базовым адресом, хранящимся в выбранном поле каталога. В результате образуется физический адрес страницы в памяти, в котором опять же используются только старшие 20 бит. Этот адрес, рассматриваемый, как старшие 20 бит физического адреса адресуемой ячейки, носит название страничного кадра. Страничный кадр дополняется с правой стороны младшими 12 битами линейного адреса, которые проходят через страничный механизм без изменения и играют роль смещения внутри выбранной физической страницы.

Рассмотрим абстрактный пример, позволяющий проследить цепочку преобразования виртуального адреса в физический. Пусть программа выполняет команду

mov ЕАХ,DS:[ЕВХ]

при этом содержимое DS (селектор) составляет 1167И, а содержимое ЕВХ (смещение) 31678U.

Старшие 13 бит селектора (число 116U) образуют индекс дескриптора в системной дескрипторной таблице. Каждый дескриптор включает в себя довольно большой объем информации о конкретном сегменте и, в частности, его линейный адрес. Пусть в ячейке дескрипторной таблицы с номером 116h записан линейный адрес (базовый адрес сегмента) 0l0Sl000h.

Тогда полный линейный адрес адресуемой ячейки определится, как сумма базового адреса и смещения:

Базовый адрес сегмента 0l0Sl000h

Смещение 0003167811

Полный линейный адрес 0108267811

При выключенной табличной трансляции величина 010826У811 будет представлять собой абсолютный физический адрес ячейки, содержимое которой должно быть прочитано приведенной выше командой mov. Легко сообразить, что эта ячейка находится в самом начале 17-го мегабайта оперативной памяти.

Посмотрим, как будет образовываться физический адрес при использовании страничной трансляции адресов. Полученный линейный адрес надо разделить на три составляющие для выделения индексов и смещения (рис. 4.8)

clip_image005

Рис. 4.8. Пример линейного адреса.

Индекс каталога составляет 4h. Умножение его на 4 даст смещение от начала каталога. Это смещение равно 10h.

Индекс таблицы страниц оказался равным 82h. После умножения на 4 получаем смещение в таблице страниц, равное в данном случае 210h.

Предположим, что регистр CR3 содержит число S000h. Тогда физический адрес ячейки в каталоге, откуда надо получить адрес закрепленной за данным участком программы таблицы страниц, составит S000h + l0h = 8010h. Пусть по этому адресу записано число 4602lh. Его 12 младших битов составляют служебную информацию (в частности, бит 1 свидетельствует о присутствии этой таблицы страниц в памяти, а бит 5 говорит о том, что к этой таблице уже были обращения), а старшие биты, т.е. число 46000h образуют физический базовый адрес таблицы страниц. Для получения адреса требуемой ячейки этой таблицы к базовому адресу надо прибавить смещение 210h. Результирующий адрес составит 462101г.

Будем считать, что по адресу 4621011 записано число 01FF502111. Отбросив служебные биты, получим адрес физической страницы в памяти 01FF5000U. Этот адрес всегда оканчивается тремя нулями, так как страницы выровнены в памяти на границу 4 Кбайт. Для получения физического адреса адресуемой ячейки следует заполнить 12 младших бит полученного адреса битами смещения из линейного адреса нашей ячейки, в которых в нашем примере записано число 678h. В итоге получаем физический адрес памяти 01FF567811, расположенный в конце 32-го Мбайта.

Как видно из этого примера, и со страничной трансляцией, и без нее вычисление физических адресов адресуемых ячеек выполняется в защищенном режиме совсем не так, как в реальном. Неприятным практическим следствием правил адресации защищенного режима является уже упоминавшаяся "оторванность" прикладной программы от физической памяти. Программист, отлаживающий программу защищенного режима (например, приложение Windows), может легко заглянуть в сегментные регистры и определить селекторы, выделенные программе. Однако селекторы абсолютно ничего не говорят о физических адресах, используемых программой. Физические адреса находятся в таблицах дескрипторов, а эти таблицы недоступны прикладной программе. Таким образом, программист не знает, где в памяти находится его программа или используемые ею области данных.

С другой стороны, использование в процессе преобразования адресов защищенных системой таблиц имеет свои преимущества. Обычно многозадачная операционная система создает для каждой выполняемой задачи свой набор таблиц преобразования адресов. Это позволяет каждой из задач использовать весь диапазон виртуальных адресов, при этом, хотя для разных задач виртуальные адреса могут совпадать (и, как правило, по крайней мере частично совпадают), однако сегментное и страничное преобразования обеспечивают выделение для каждой задачи несовпадающих областей физической памяти, надежно изолируя виртуальные, адресные пространства задач друг от друга.

Вернемся теперь к таблицам дескрипторов и рассмотрим их более детально. Существует два типа дескрипторных таблиц: таблица глобальных дескрипторов (GDT от Global Descriptor Table) и таблицы локальных дескрипторов (LDT от Local Descriptor Table).Обычно для каждой из этих таблиц в памяти создаются отдельные сегменты, хотя в принципе это не обязательно. Таблица глобальных дескрипторов существует в единственном экземпляре и обычно принадлежит операционной системе, а локальных таблиц может быть много (это типично для многозадачного режима, в котором каждой задаче назначается своя локальная таблица).

Виртуальное адресное пространство делится на две равные половины. К одной половине обращение происходит через GDT, к другой половине через LDT. Как уже отмечалось, все виртуальное пространство состоит из 214 сегментов, из которых 213 сегментов адресуются через GDT, и еще 213 - чрез LDT.

Когда многозадачная система переключает задачи, глобальная таблица остается неизменной, а текущая локальная таблица заменяется на локальную таблицу новой задачи. Таким образом, половина виртуального пространства в принципе доступна всем задачам в системе, а половина переключается от одной задачи к другой по мере переключения самих задач.

Для программирования защищенного режима и даже для отладки прикладных программ, работающих в защищенном режиме, полезно представлять себе структуру дескриптора и смысл его отдельных полей. Следует заметить, что существует несколько типов дескрипторов, которым присущи разные форматы. Так, дескриптор сегмента памяти (наиболее распространенный тип дескриптора) отличается от дескриптора шлюза, используемого, в частности, для обслуживания прерываний. Рассмотрим формат дескриптора памяти (рис. 4.9).

clip_image006

Рис. 4.9. Формат дескриптора памяти.

Как видно из рисунка, дескриптор занимает 8 байт. В байтах 2...4 и 7 записывается линейный базовый адрес сегмента. Полная длина базового адреса - 32 бит. В байтах 0-1 записываются младшие 16 бит границы сегмента, а в младшие четыре бита байта атрибутов 2 - оставшиеся биты 16...19. Границей сегмента называется номер его последнего байта. Мы видим, что граница описывается 20-ю битами, и ее численное значение не может превышать 1М. Однако, единицы, в которых задается граница, можно изменять, что осуществляется с помощью бита дробности G (бит 7 байта атрибутов 2). Если G=0, граница указывается в байтах; если 1 - в блоках по 4 Кбайт. Таким образом, размер сегмента можно задавать с точностью до байта, но тогда он не может быть больше 1 Мбайт; если же установить G=l, то сегмент может достигать 4 Гбайт, однако его размер будет кратен 4 Кбайт. База сегмента и в том, и в другом случае задастся с точностью до байта.

Рассмотрим теперь атрибуты сегмента, которые занимают два байта дескриптора.

Бит A (Accessed, было обращение) устанавливается процессором в тот момент, когда в какой-либо сегментный регистр загружается селектор данного сегмента. Далее процессор этот бит не сбрасывает, однако его может сбросить программа (разумеется, если она имеет доступ к содержимому дескриптора, что обычно является прерогативой операционной системы). Анализируя биты обращения различных сегментов, программа может судить о том, было ли обращение к данному сегменту' после того, как она сбросила бит А.

Тип сегмента занимает 3 бит (иногда бит А включают в поле типа, и тогда тип занимает 4 бит) и может иметь 8 значений. Тип определяет правила доступа к сегменту. Так, если сегмент имеет тип 1, для него разрешены чтение и запись, что характерно для сегментов данных. Назначив сегменту тип 0, мы разрешим только чтение этого сегмента, защитив его тем самым от любых модификаций. Тип 4 обозначает разрешение исполнения, что характерно для сегментов команд. Используются и другие типы сегментов.

Подчеркнем, что защита сегментов памяти от несанкционированных его типом действий выполняется не программой, и даже не операционной системой, а процессором на аппаратном уровне. Так, при попытке записи в сегмент типа 0 возникнет так называемое исключение общей защиты. Исключением называется внутреннее прерывание, возбуждаемое процессором при возникновении каких-либо неправильных с его точки зрения ситуаций. Попытка записи в сегмент, для которого запись запрещена, и относится к такого рода ситуациям. Исключению общей защиты соответствует вектор 13, в котором должен находиться адрес обработчика этого исключения.

Стоит еще обратить внимание на тип 4. Для сегмента команд разрешается только исполнение, но не запись и даже не чтение. Это значит, что в защищенном режиме программа не может случайно залезть в свой сегмент команд и затереть его; не может она также и сознательно модифицировать команды в процессе своего выполнения - методика, иногда используемая в программах реального режима для защиты от их расшифровки любознательными программистами

Бит 4 байта атрибутов 1 является идентификатором сегмента. Если он равен 1, как это показано на рис. 4.9, дескриптор описывает сегмент памяти. Значение этого бита 0 характеризует дескриптор системного сегмента.

Поле DPL (Descriptor Privilege Level, уровень привилегий дескриптора) служит для защиты программ друг от друга. Уровень привилегий может принимать значения от 0 (максимальные привилегии) до 3 (минимальные). Программам операционной системы обычно назначается уровень 0, прикладным программам - уровень 3, в результате чего исключается возможность некорректным программам разрушить операционную систему. С другой стороны, если прикладная программа сама выполняет функции операционной системы, переводя процессор в защищенный режим и работая далее в этом режиме, ее сегментам следует назначить наивысший (нулевой) уровень привилегий, что откроет ей доступ ко всем средствам защищенного режима.

Бит Р говорит о присутствии сегмента в памяти. В основном он используется для организации виртуальной памяти. С помощью этого бита система может определить, находится ли требуемый сегмент в памяти, и при необходимости загрузить его с диска. В процессе выгрузки ненужного пока сегмента на диск бит Р в его дескрипторе сбрасывается.

Младшая половина байта атрибутов 2 занята старшими битами границы сегмента. Бит AVL (от Available, доступный) не используется и не анализируется процессором и предназначен для использования прикладными программами.

Бит D (Default, умолчание) определяет действующий по умолчанию размер для операндов и адресов. Он изменяет характеристики сегментов двух типов: исполняемых и стека. Если бит D сегмента команд равен 0, в сегменте по умолчанию используются 16-битовые адреса и операнды, если 1 - 32-битовые.

Атрибут сегмента, действующий по умолчанию, можно изменить на противоположный с помощью префиксов замены размера операнда (66h) и замены размера адреса (67п). Таким образом, для сегмента с D=0 префикс 66h перед некоторой командой заставляет ее рассматривать свои операнды, как 32-битовые, а для сегмента с D=l тот же префикс 66h, наоборот, сделает операнды 16-битовыми. В некоторых случаях транслятор сам включает в объектный модуль необходимые префиксы, в других случаях их приходится вводить в программу "вручную".

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

Для того, чтобы наша программа могла бы хоть что-то сделать в защищенном режиме, для нее необходимо создать среду защищенного режима, в первую очередь, таблицу глобальных дескрипторов с описанием всех сегментов, с которыми программа будет работать. Кроме нас никто эту таблицу (при работе в DOS) не создаст. Таким образом, наша программа будет в какой-то мере выполнять функции операционной системы защищенного режима.

Для практического исследования защищенного режима придется выполнить некоторую работу по переконфигурированию компьютера. В наше время компьютеры обычно конфигурируются так, что при их включении сразу загружается система Windows. Работы, для которых требуется DOS, выполняются либо в режиме эмуляции DOS, либо в сеансе DOS, организуемом системой Windows. Для запуска прикладной программы защищенного режима такой способ не годится. Нам понадобится DOS в "чистом виде", без следов Windows. Более того, перед запуском программы необходимо выгрузить все драйверы обслуживания расширенной памяти (HIMEM.SYS и EMM386.EXE) и программы, использующие расширенную память, например, SMARTDRV.EXE. Лучше всего загружать DOS с системной дискеты, подготовив файлы CONFIG.SYS и AUTOEXEC.BAT в минимальном варианте.

Обсуждая в начале этого раздела основы защищенного режима, мы не затронули многие, в том числе принципиальные вопросы, с которыми придется столкнуться при написании работоспособной программы. Необходимые пояснения будут даны в конце этого раздела.

Пример 4-4. Программирование защищенного режима

.586Р ;Разрешение трансляции всех команд МП 586

;Структура для описания дескрипторов сегментов

dcr struc ;Имя структуры

limit dw 0 ;Граница (биты 0...15)

base_l dw 0 ;База, биты 0...15

base_m db 0 ;База, биты 16...23

attr_l db 0 ;Байт атрибутов 1

attr_2 db ;Граница (биты 16...19) и атрибуты 2

base_h db 0 ;База, биты 24...31

dcr ends ;

data segment use16 ;

;Таблица глобальных дескрипторов GDT

gdt_null dcr <0,0,0,0,0,0> ;Селектор 0-обязательный

;нулевой дескриптор

gdt_data dcr <data_size-l,0,0,92h,0,0> ;Селектор 8,

;сегмент данных

gdt_code dcr <code_size-l,0,0,98h,0,0>;Селектор 16,

;сегмент команд

gdt_stack dcr <511,0,0,92h,0,0> ;Селектор 24 -

;сегмент стека

gdt_screen dcr <4095,B000h,OBh,92h,0,0> ;Селектор 32,

;видеобуфер

pdescr df 0 ;Псевдодескриптор для команды Igdt

data_size=$-gdt_null ;Размер сегмента данных

data ends ;Конец сегмента данных

text segment use16 ;Сегмент команд, 16-разрядный режим

assume CS:text,DS:data;

main proc ;

xor EAX,EAX ;Очистим ЕАХ

mov AX,data ;Загрузим в DS сегментный

mov DS,AX ;адрес сегмента данных

;Вычислим 32-битовый линейный адрес сегмента данных

;и загрузим его в дескриптор сегмента данных в GDT.

;В регистре АХ уже находится сегментный адрес.

;Умножим его на 16 сдвигом влево на 4 бита

shl ЕАХ,4 ;В ЕАХ линейный базовый адрес

mov EBP, ЕАХ ;Сохраним его в ЕВР для будущего

mov BX,offset gdt_data ;В ВХ адрес дескриптора

mov [BX].base_l,AX ;Загрузим младшую часть базы

rol ЕАХ,16 ;Обмен старшей и младшей половин ЕАХ

mov [BX].base_m,AL ;Загрузим среднюю часть базы

;Вычислим 32-битовый линейный адрес -сегмента команд

;и загрузим его в дескриптор сегмента команд в GDT

хог ЕАХ, ЕАХ ;Очистим ЕАХ

mov AX,CS ;Сегментный адрес сегмента команд

shl ЕАХ,4 ;В ЕАХ линейный базовый адрес

mov BX,offset gdt_code ;В ВХ адрес дескриптора

mov [BX] .base_l,AX ;Загрузим младшую часть базы

rol ЕАХ,16 ;Обмен старшей и младшей половин ЕАХ

mov [BX].base_m,AL ;Загрузим среднюю часть базы

;Вычислим 32-битовый линейный адрес сегмента стека

хог ЕАХ, ЕАХ ;Все, как и для других

mov AX,SS ;дескрипторов

shl ЕАХ,4

mov BX,offset gdt_stack

mov [BX].base_l,AX

rol EAX,16

mov [BX].base_m,AL

;Подготовим псевдодескриптор pdescr для загрузки регистра GDTR

mov dword ptr pdescr+2,EBP ;База GDT

mov word ptr pdescr, 39 ;Граница GDT

Igdt pdescr ;Загрузим регистр GDTR

cli ;Запрет прерываний

;Переходим в защищенный режим

mov EAX,CR0 ;Получим содержимое CR0

or EAX,1 ;Установим бит защищенного режима

mov CRO,ЕАХ ;Запишем назад в CR0

;---------------------------------------------------------;

;Теперь процессор работает в защищенном режиме ;

;---------------------------------------------------------;

;Загружаем в CS:IP селектор:смещение точки continue

db OEAh ;Код команды far jmp

dw offset continue ;Смещение

dw 16 ;Селектор сегмента команд

continue:

;Делаем адресуемыми данные

mov AX, 8 ;Селектор сегмента данных

mov DS,AX ;Загрузим в DS

;Делаем адресуемым стек

mov AX,24 ;Селектор сегмента стека

mov SS,AX ;Загрузим в SS

;Инициализируем ES и выводим символ

mov AX,32 ;Селектор сегмента видеобуфера

mov ES,AX ;Загрузим в ES

mov BX,2000 ;Начальное смещение на экране

mov AX,09FOFh ;Символ с атрибутом

mov ES : [BX] , АХ;Вывод в видеобуфер

;Вернемся в реальный режим

mov gdt_data.limit,0FFFFh ;Установим

mov gdt_code.limit,0FFFFh ;значение границы

mov gdt_stack.limit,0FFFFh;для реального

mov gdt_screen.limit,0FFFFh ;режима

mov AX,8 ;Загрузим теневой регистр

mov DS,AX ;сегмента данных

mov AX,24 ;To же для

mov SS,AX ;стека

mov AX,32 ;To же

mov ES, AX ;для регистра ES

;Выполним дальний переход, чтобы заново загрузить

;селектор в CS и модифицировать его теневой регистр

db0Eah ;Код команды jmp far

dwoffset go ;Смещение точки перехода

dw!6 ;Селектор сегмента команд

;Переключим режим процессора

go: mov EAX,CR0 ;Получим содержимое CR0

and EAX,0FFFFFFFEh;Сбросим бит РЕ

mov CR0,EAX ;Запишем назад в CR0

db0Eah ; Код команды far jmp

dwoffset return ;Смещение точки перехода

dwtext ;Сегментный адрес

;---------------------------------------------;

;Теперь процессор снова работает в реальном режиме ;

;---------------------------------------------;

;Восстановим операционную среду реального режима

return: mov AX,data ;Загрузим сегментный

mov DS,AX ;регистр DS

mov AX,stk ;Загрузим сегментный

mov SS,AX ;регистр SS

mov SP,512 ;Восстановим SP

sti ;Разрешим прерывания

mov AX,4C00h ;Завершим программу

;обычным образом

int 2 In main endp

code_size=$-main ;Размер сегмента команд

text ends /Конец сегмента команд

stk segment stack ;Сегмент

db 512 dup (') ;стека stk ends

end main ;Конец программы и точка входа

Для того, чтобы разрешить использование всех, в том числе привилегированных команд 32-разрядных процессоров, в программу включена директива .586Р.

Программа начинается с объявления структуры dcr, с помощью которой будут описываться дескрипторы сегментов. Сравнивая описание структуры dcr в программе с рис. 4.9, нетрудно проследить их соответствие друг другу. Для удобства программного обращения в структуре dcr база описывается тремя полями: младшим словом (base_l) и двумя байтами: средним (base_m) и старшим (base_h).

В байте атрибутов 1 задается ряд характеристик сегмента. В примере 4.4 используются сегменты двух типов: сегмент команд, для которого байт attr_l должен иметь значение 98h (присутствующий, только исполнение, DPL=0), и сегмент данных (или стека) с кодом 92h (присутствующий, чтение и запись, DPL=0).

Некоторые дополнительные характеристики сегмента указываются в старшем полубайте байта attr_2. Для всех наших сегментов значение этого полубайта равно 0 (бит G=0, так как граница указывается в байтах, а D=0, так как программа 16-разрядная).

Сегмент данных data начинается с описания важнейшей системной структуры - таблицы глобальных дескрипторов. Как уже отмечалось выше, обращение к сегментам в защищенном режиме возможно исключительно через дескрипторы этих сегментов. Таким образом, в таблице дескрипторов должно быть описано столько дескрипторов, сколько сегментов использует программа. В нашем случае в таблицу включены, помимо обязательного нулевого дескриптора, всегда занимающего первое место в таблице, четыре дескриптора для сегментов данных, команд, стека и дополнительного сегмента данных, который мы наложим на видеобуфер, чтобы обеспечить возможность вывода в него символов. Порядок дескрипторов в таблице (кроме нулевого) не имеет значения.

Поля дескрипторов для наглядности заполнены конкретными данными явным образом, хотя объявление структуры dcr с нулями во всех полях позволяет описать дескрипторы несколько короче, например:

gdt_null dcr <> ;Селектор 0 - обязательный

;нулевой дескриптор

gdt_data dcr <data_size - l, , , 92h> ;Селектор 8 - сегмент данных

В дескрипторе gdt_data, описывающем сегмент данных программы, заполняется поле границы сегмента (фактическое значение размера сегмента data_size будет вычислено транслятором, см. последнее предложение сегмента данных), а также байт атрибутов 1. База сегмента, т.е. линейный адрес его начата, в явной форме в программе отсутствует, поэтому ее придется программно вычислить и занести в дескриптор уже на этапе выполнения.

Дескриптор gdt_codc сегмента команд заполняется схожим образом.

Дескриптор gdt_stack сегмента стека имеет, как и любой сегмент данных, код атрибута 92h, что разрешает его чтение и запись, и явным образом заданную границу - 255 байт, что соответствует размеру стека. Базовый адрес сегмента стека так же придется вычислить на этапе выполнения программы.

Последний дескриптор gdt_scrcen описывает страницу 0 видеобуфера. Размер видеостраницы, как известно, составляет 4096 байт, поэтому в поле границы указано число 4095. Базовый физический адрес страницы известен, он равен BS000h. Младшие 16 бит базы (число 8000И) заполняют слово base_l дескриптора, биты 16...19 (число OBU) - байт base_m. Биты 20...31 базового адреса равны 0, поскольку видеобуфер размещается в первом мегабайте адресного пространства.

Первая половина программы посвящена подготовке перехода в защищенный режим. Прежде всего надо завершить формирование дескрипторов сегментов программы, в которых остались незаполненными базовые адреса сегментов.

Базовые (32-битовые) адреса определяются путем умножения значений сегментных адресов на 16. После обнуления регистра ЕАХ и инициализации сегментного регистра DS, которая позволит нам обращаться к полям данных программы в реальном режиме, содержимое ЕАХ командой sill сдвигается влево на 4 бита, образуя линейный 32-битовый адрес. Поскольку этот адрес будет использоваться и в последующих фрагментах программы, он запоминается в регистре ЕВР (или любом другом свободном регистре общего назначения). В ВХ загружается адрес дескриптора данных, после чего в дескриптор заносится младшая половина линейного адреса из регистра АХ. Поскольку к старшей половине регистра ЕАХ (где нас интересуют биты 17...24) обратиться невозможно, над всем содержимым ЕАХ с помощью команды rol выполняется циклический сдвиг на 16 бит, в результате которого младшая и старшая половины ЕАХ меняются местами.

После сдвига содержимое AL (где теперь находятся биты 17...24 линейного адреса) заносится в поле base_m дескриптора. Аналогично Вычисляются линейные адреса сегмента команд и сегмента стека.

Следующий этап подготовки к переходу в защищенный режим - загрузка в регистр процессора GDTR (Global Descriptor Table Register, регистр таблицы глобальных дескрипторов) информации о таблице глобальных дескрипторов. Эта информация включает в себя линейный базовый адрес таблицы и ее границу и размещается в 6 байтах поля данных, называемого иногда псевдодескриптором. Для загрузки GDTR предусмотрена специальная привилегированная команда Igdt (load global descriptor table, загрузка таблицы глобальных дескрипторов), которая требует указания в качестве операнда имени псевдодескриптора. Формат псевдодескриптора приведен на рис. 4.10.

clip_image007

Рис. 4.10. Формат псевдодескриптора.

В нашем примере заполнение псевдодескриптора упрощается вследствие того, что таблица глобальных дескрипторов расположена в начале сегмента данных, и ее базовый адрес совпадает с базовым адресом всего сегмента, который мы благоразумно сохранили в регистре ЕВР. Границу GDT в нашем случае легко вычислить в уме: 5 дескрипторов по 8 байт занимают объем 40 байт, и , следовательно, граница равна 39. Команда Igdt загружает регистр GDTR и сообщает процессору о местонахождении и размере GDT.

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

нет, и па время работы нашей программы прерывания придется запретить. Запрет всех аппаратных прерываний осуществляется командой cli.

Теперь, наконец, можно перейти в защищенный режим, что делается на удивление просто. Для перевода процессора в защищенный режим достаточно установить бит 0 в управляющем регистре CRO. Всего в процессоре имеется 4 программно-адресуемых управляющих регистра с мнемоническими именами CRO, CR1, CR2 и CR3. Регистр CR1 зарезервирован, регистры CR2 и CR3 управляют страничным преобразованием, которое у нас выключено, а регистр CRO содержит целый ряд управляющих битов, из которых нас сейчас будут интересовать только биты 31 (разрешение страничного преобразования) и 0 (включение защиты). При включении процессора оба эти бита сбрасываются, и в процессоре устанавливается реальный режим с выключенным страничным преобразованием. Установка в 1 младшего бита CR0 переводит процессор в защищенный режим, сброс этого бита возвращает его в режим реальных адресов.

Для того, чтобы в процессе установки бита 0 не изменить состояние других битов регистра CR0, сначала его содержимое считывается командой mov в регистр ЕАХ, там с помощью команды or устанавливается младший бит, после чего второй командой mov измененное значение загружается в CR0. Процессор начинает работать по правилам защищенного режима.

Хотя защищенный режим установлен, однако действия по настройке системы еще не закончены. Действительно, во всех используемых в программе сегментных регистрах хранятся не селекторы дескрипторов сегментов, а базовые сегментные адреса, не имеющие смысла в защищенном режиме. Между прочим, отсюда можно сделать вывод, что после перехода в защищенный режим программа не должна работать, так как в регистре CS пока еще нет селектора сегмента команд, и процессор не может обращаться к этому сегменту. В действительности это не совсем так.

В процессоре для каждого из сегментных регистров имеется так называемый теневой регистр дескриптора, который имеет формат дескриптора (рис. 4.11). Теневые регистры недоступны программисту; они автоматически загружаются процессором из таблицы дескрипторов каждый раз, когда процессор загружает соответствующий сегментный регистр. Таким образом, в защищенном режиме программист имеет дело с селекторами, т.е. номерами дескрипторов, а процессор - с самими дескрипторами, хранящимися в теневых регистрах. Именно содержимое теневого регистра (в первую очередь, линейный адрес сегмента) определяет область памяти, к которой обращается процессор при выполнении конкретной команды.

В реальном режиме теневые регистры заполняются не из таблицы дескрипторов, а непосредственно самим процессором. В частности, процессор заполняет поле базы каждого теневого регистра линейным базовым адресом сегмента, полученным путем умножения па 16 содержимого сегментного регистра, как это и положено в реальном режиме. Поэтому после перехода в защищенный режим в теневых регистрах находятся правильные линейные базовые адреса, и программа будет выполняться правильно, хотя с точки зрения правил адресации защищенного режима содержимое сегментных регистров лишено смысла.

clip_image008

Рис. 4.11. Сегментные регистры и теневые регистры дескрипторов.

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

Загрузить селекторы в сегментные регистры DS, SS и ES не представляет труда. Но как загрузить селектор в регистр CS, в который запрещена прямая запись? Для этого можно воспользоваться искусственно сконструированной командой дальнего перехода, которая, как известно, приводит к смене содержимого и IP, и CS. Фрагмент

db OEAh ;Код команды far jmp

dw offset continue ;Смещение

dw 16 ;Селектор сегмента команд

выглядящий совершенно нелепо в сегменте команд, как раз и демонстрирует эту методику. В реальном режиме мы поместили бы во второе слово адреса сегментный адрес сегмента команд, в защищенном же мы записываем в него селектор этого сегмента (число 16).

Команда дальнего перехода, помимо загрузки в CS селектора, выполняет еще одну функцию - она очищает очередь команд в блоке предвыборки команд процессора. Как известно, в современных процессорах с целью повышения скорости выполнения программы используется конвейерная обработка команд программы, позволяющая совместить во времени фазы их обработки. Одновременно с выполнением текущей (первой) команды осуществляется выборка операндов следующей (второй), дешифрация третьей и выборка из памяти четвертой команды. Таким образом, в момент перехода в защищенный режим уже могут быть расшифрованы несколько следующих команд и выбраны из памяти их операнды. Однако эти действия выполнялись, очевидно, по правилам реального, а не защищенного режима, что может привести к нарушениям в работе программы. Команда перехода очищает очередь предвыборки, заставляя процессор заполнить ее заново уже в защищенном режиме.

Далее выполняется загрузка в сегментные регистры DS и SS значений соответствующих селекторов, и на этом, наконец, заканчивается процедура перехода в защищенный режим.

Следующий фрагмент программы является, можно сказать, диагностическим. В нем инициализируется (по правилам защищенного режима!) сегментный регистр ES и в видеобуфер из регистра АХ выводится один символ. Код 0Fh соответствует изображению большой звездочки, а атрибут 9Fh - ярко-белому мерцающему символу на синем поле. Появление этого символа на экране служит подтверждением правильного функционирования программы в защищенном режиме.

Почему мы не предусмотрели вывод на экран хотя бы одной содержательной строки? Дело в том, что в защищенном режиме запрещены любые обращения к функциям DOS или BIOS. Причина этого совершенно очевидна - и DOS, и BIOS являются программами реального режима, в которых широко используется сегментная адресация реального режима, т.е. загрузка в сегментные регистры сегментных адресов. В защищенном же режиме в сегментные регистры загружаются не сегментные адреса, а селекторы. Кроме того, обращение к функциям DOS и BIOS осуществляется с помощью команд программного прерывания int с определенными номерами, а в защищенном режиме эти команды приведут к совершенно иным результатам. Поэтому в программе, работающей в защищенном режиме и не имеющей специальных и довольно сложных средств перехода в так называемый режим виртуального 86-го процессора, вывод на экран можно осуществить только прямым программированием видеобуфера. Нельзя также выполнить запись или чтение файла; более того, нельзя даже завершить программу средствами DOS. Сначала се надо вернуть в реальный режим.

Возврат в реальный режим можно осуществить разными способами. Мы воспользуемся для этого тем же регистром CRO, с помощью которого мы перевели процессор а защищенный режим. Казалось бы, для возврата в реальный режим достаточно сбросить бит 0 этого регистра. Однако дело обстоит не так просто. Для корректного возврата в реальный режим надо выполнить некоторые подготовительные операции, рассмотрение которых позволит нам глубже вникнуть в различия реального и защищенного режимов.

При работе в защищенном режиме в дескрипторах сегментов записаны, среди прочего, их линейные адреса и границы. Процессор при выполнении команды с адресацией к тому или иному сегменту сравнивает полученный им относительный адрес с границей сегмента и, если команда пытается адресоваться за пределами сегмента, формирует прерывание (исключение) нарушения общей защиты. Если в программе предусмотрена обработка исключений, такую ситуацию можно обнаружить и как то исправить. Таким образом, в защищенном режиме программа не может выйти за пределы объявленных ею сегментов, а также не может выполнить действия, запрещенные атрибутами сегмента. Так, если сегмент объявлен исполняемым (код атрибута 1 981т), то данные из этого сегмента нельзя читать или модифицировать; если атрибут сегмента равен 92h, то в таком сегменте не может быть исполняемых команд, на зато данные можно как читать, так и модифицировать. Указав для какого-то сегмента код атрибута 90h, мы получим сегмент с запрещением записи. При попытке записи в этот сегмент процессор сформирует исключение общей защиты.

Как уже отмечалось, дескрипторы сегментов хранятся в процессе выполнения программы в теневых регистрах (см. рис. 4.11), которые загружаются автоматически при записи в сегментный регистр селектора.

При работе в реальном режиме некоторые поля теневых регистров должны быть заполнены вполне определенным образом. В частности, поле границы любого сегмента должно содержать число FFFFh, а бит дробности сброшен. Следует подчеркнуть, что границы всех сегментов должны быть точно равны FFFFh; любое другое число, например, FFFEh, "не устроит" реальный режим.

Если мы просто перейдем в реальный режим сбросом бита 0 в регистре CR0, то в теневых регистрах останутся дескрипторы защищенного режима и при первом же обращении к любому сегменту программы возникнет исключение общей защиты, так как ни один из наших сегментов не имеет границы, равной FFFFh. Поскольку мы не обрабатываем исключения, произойдет либо сброс процессора и перезагрузка компьютера, либо зависание. Таким образом, перед переходом в реальный режим необходимо исправить дескрипторы всех наших сегментов: команд, данных, стека и видеобуфера К сегментным регистрам FS и GS мы не обращались, и о них можно не заботиться.

Теневые регистры, куда, собственно, надо записать значение границы, нам недоступны. Для из модификации придется прибегнуть к окольному маневру: записать в поля границ всех четырех дескрипторов значение FFFFh, а затем повторно загрузить селекторы в сегментные регистры, что приведет к перезаписи содержимого теневых регистров. С сегментным регистром CS так поступить нельзя, поэтому его загрузку придется выполнить, как и ранее, с помощью искусственно сформированной команды дальнего перехода.

Настроив все использовавшиеся в программе сегментные регистры, можно сбросить бит 0 в CR0. После перехода в реальный режим нам придется еще раз выполнить команду дальнего перехода, чтобы очистить очередь команд в блоке предвыборки и загрузить в регистр CS вместо хранящегося там селектора обычный сегментный адрес регистра команд.

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

push DS

...

pop DS

выполнение программы будет нарушено, так как команда pop DS загрузит в DS не сегментный адрес реального режима, а селектор, т.е. число 8 в нашем случае. Это число будет рассматриваться процессором, как сегментный адрес, и дальнейшие обращения к полям данных приведут к адресации физической памяти начиная с абсолютного адреса 80h, что, конечно, лишено смысла. Даже если в нашей программе нет строк сохранения и восстановления сегментных регистров, они неминуемо встретятся, как только произойдет переход в DOS по команде int 21h, так как диспетчер DOS сохраняет, а затем восстанавливает все регистры задачи, в том числе и сегментные. Поэтому после перехода в реальный режим необходимо загрузить в используемые далее сегментные регистры соответствующие сегментные адреса, что и выполняется в программе для регистров DS и SS. Надо также не забыть разрешить запрещенные нами ранее аппаратные прерывания (команда sti).

Можно еще заметить, что в той части программы, которая выполняется в защищенном режиме, не используется стек. Учитывая это, можно было несколько сократить текст программы, удалив из нее строки настройки регистра SS как при подготовке перехода в защищенный режим, так и при возврате в реальный. Не было также необходимости заново инициализировать указатель стека, так как его исходное содержимое - смещение дна стека, равное 512, никуда из SP не делось бы.

Программа завершается обычным образом функцией DOS 4Ch. Нормальное завершение программы и переход в DOS в какой-то мере свидетельствует о ее правильности.

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

В приведенном примере проиллюстрированы лишь базовые средства программирования защищенного режима: понятие селекторов и дескрипторов, создание глобальной таблицы дескрипторов, переход в защищенный режим и обратно, адресация в защищенном режиме. За кадром остались такие важные вопросы, как обработка исключений и аппаратных прерываний, уровни привилегий и защита по привилегиям, раздельные операционные среды и таблицы локальных дескрипторов, создание и взаимодействие задач, режим виртуального процессора 8086 и другие.

Препроцессор команд ХММ-расширения

Задачу адаптации транслятора TASM к новым командам микропроцессора, и в частности к ХММ-командам, можно решить двумя способами.

  • * Можно разработать включаемый файл, в котором дляя каждой ХММ-ком ды реализовать макрокоманду, моделирующую на ббазе существующих к манд нужную ХММ-команду. Кроме этого, фирма Irintel, зная об инерцио ности процесса разработки новых версий трансляторров ассемблера, вмест с подмножеством новых команд разрабатывает соотгветствующий включа мый файл для их поддержки в ассемблерных прогрэаммах. Для подмножества ХММ-команд такой файл называется iaxmm.inac. Он ориентирован на транслятор MASM (фирмы Microsoft) и не пригодеен (требует доработки") для TASM. Однако при доработке TASM необходимую иметь в виду вопрос об авторских правах. Некоторые проблемы использоования файла iaxmm.inc совместно с TASM обсуждены ниже.
  • * Можно разработать программу-препроцессор, на ввход которой подавать исходный файл с программой на ассемблере, содеряжащей новые команды процессора, а на выходе получать текст, адаптированнный для компиляции старым транслятором ассемблера. Этот путь имеет' то преимущество, что теперь при появлении новых команд можно, не вноося больших корректив в технологию разработки программ, всего лишь орпределенным образом модифицировать файл-препроцессор, дополнив его i возможностями по обработке новых команд процессора. Более того, дополяяив препроцессор средствами распознавания микропроцессора (Intel или /AMD), можно разрабатывать программы с использованием расширения 31DNo\v!.

Первый способ был реализован в уроке «ММХ-техномюгия микропроцессоров Intel» учебника для ММХ-команд. Там же были привзедены примеры включаемых файлов, полностью пригодных для использованияя как 16-, так и 32-разрядными приложениями на ассемблере. Поэтому основносе внимание мы уделим второму способу организации поддержки ХММ-команд — препроцессорному. Но вначале рассмотрим структуру и содержание включаеемого файла iaxmm.inc. Текст этого файла можно загрузить с официального сайта компании Intel (http:// www.intel.com).

Поддержка ХММ-команд в файле iaxmm.inc

С точки зрения структуры включаемый файл iaxmm.inc представляет собой набор макрокоманд двух типов — основных и вспомогательньях. Названия основных макрокоманд полностью совпадают с названиями ХММ-команд, и эти макрокоманды обеспечивают моделирование определенных XlMM-команд. Вспомогательные макрокоманды расположены в начале файла и предназначены для обеспечения работы основных макрокоманд. В частности, :эти макрокоманды устанавливают тип операндов, указанных при обращении к (основной макрокоманде, причем делают это исходя из режима функционировашия транслятора — 16-или 32-разрядного. Другое важное действие — установление соответствия между названиями ХММ-регистров и регистров общего назначения. Дело в том, что для моделирования ХММ-команд в 16- или 32-разрядном режиме работы ассемблера используются разные регистры общего назначения — 1 6-разрядные регистры в 16-разрядном режиме, и 32-разрядные в 32-разрядном режиме.

Рассмотрим процесс моделирования ХММ-команд. В! качестве основы для моделирования выступает команда основного процессора!. Эта команда должнаудовлетворять определенным требованиям. Каковы они? В поисках ответа посмотрим на машинные коды ХММ-команд в литературе [40, 41]. Видно, что общими у них являются два момента:

  • *поле кода операции ХММ-команд состоит из двух или трех байтов, один из которых равен Ofh;
  • *большинство ХММ-команд использует форматы адресации с байтами modR/M и sib и соответственно допускает сочетание операндов как обычных двух-операндных команд целочисленного устройства — регистр-регистр или память-регистр.

Для моделирования ХММ-команд нужно подобрать такую команду основного процессора, которая удовлетворяет этим двум условиям. Во включаемом файле iaxmm.inc в качестве таких команды присутствуют две — CMPXCHG и ADD. В процессе моделирования на место нужного байта кода операции этих команд помещаются байты со значениями кода операции соответствующей ХММ-команды. Когда микропроцессор «видит», что очередная команда является ХММ-командой, то он начинает трактовать коды регистров в машинной команде как коды ХММ-регистров и ссылки на память, размерностью соответствующей данной команде. В машинном формате команды нет символических названий регистров, которыми мы пользуемся при написании исходного текста программы, например АХ или ВХ. В этом формате они определенным образом кодируются. Например, регистр АХ кодируется в поле REG машинной команды как 000. Если заменить код операции команды, в которой одним из операндов является регистр АХ, на код операции некоторой ХММ-команды, то это же значение в поле reg микропроцессор будет трактовать как регистр RXMMO. Таким образом, в ХММ-командах коды регистров воспринимаются соответственно коду операции. В табл. 10.1 приведены коды регистров общего назначения и соответствующих им ХММ-регистров. В правом столбце этой таблицы содержится условное обозначение ХММ-регистров, принятое в файле iaxmm.inc. Это же соответствие закреплено рядом определений в этом файле, которые иллюстрирует следующая программа.

DefineXMMxRegs Macro IFDEF APPJ.6BIT

rxmmO TEXTEQU<AX>

rxmml TEXTEQU<CX>

rxmm2 TEXTEQU<DX>

rxmm3 TEXTEQU<BX>

rxmm4 TEXTEQU<SP>

rxmm5 TEXTEQU<BP>

гхттб TEXTEQU<SI>

rxmm7 TEXTEQU<DI>

RXMMO TEXTEQU<AX>

RXMM1 TEXTEQU<CX>

RXMM2 TEXTEQU<DX>

RXMM3 TEXTEQU<BX>

RXMM4 TEXTEQU<SP>

RXMM5 TEXTEQU<BP>

P.XMM6 TEXTEQU<SI>

RXMM7 TEXTEQU<DI>

rxmml TEXTEQU<ECX>

rxmm2 TEXTEQU<EDX>

rxmm3 TEXTEQU<EBX>

rxmm4 TEXTEQU<ESP>

rxmm5 TEXTEQU<EBP>

гхттб TEXTEQU<ESI>

ГХШП7 TEXTEQU<EDI>

RXMMO TEXTEQU<EAX>

RXMM1 TEXTEQU<ECX>

NRXMM2 TEXTEQU<EDX>

RXMM3 TEXTEQU<EBX>

RXMM4 TEXTEQU<ESP>

RXMM5 TEXTEQU<EBP>

RXMM6 TEXTEQU<ESI>

RXMM7 TEXTEQU<EDI> ENDIF endm

Таблица 10.1. Кодировка регистров в машинном коде команды

Код в поле reg

Регистр целочисленного

устройства

ХММ-регистр

000

АХ/ЕАХ

RXMM0

001

СХ/ЕСХ

RXMM1

010

DX/EDX

RXMM2

011

ВХ/ЕВХ

RXMM3

100

SP/ESP

RXMM4

101

ВР/ЕВР

RXMM5

110

SI/ESI

RXMM6

111

DI/EDI

RXMM7

Теперь в исходном тексте программы можно использовать символические имена ХММ-регистров в качестве аргументов макрокоманд, моделирующих ХММ-команды.

Рассмотрим, как в файле iammx.inc описано макроопределение для моделирования ХММ-команды скалярной пересылки MOVSS.

:F3 OF 10 /г movss xrrnil. xmm2/m32 :F3 OF 11 /r movss xmm2/m32. xnrnl movss macro dst:req. src:req

XMMld_st_f3 opc_Mo«s. dst, src endm

Понимание структуры приведенного макроопределения не должно вызвать у читателя трудностей. Начать следует с того, что данная команда содержит вложенный вызов макрокоманды XMMld_st_f3, у которой две задачи — определить вариант сочетания операндов, после чего сформировать правильный код операции и подставить его на место соответствующих байтов в команде CMPXCHG. В результате этих действий команда CMPXCHG «превращается» в ХММ-команду MOVSS.

1. XMMld_St f3 macro op:req.dst:req, src:req

2. local x. у

3. Defin'eXMMxRegs

4. IF (OPATTR(dst)) AND OOOlOOOOy -.register

5. x: lock cmpxchg src. dst

6. у: org x

7. byte OF3H.0Fh. op& Id

8. org у

9. ELSE

10. x: lock cmpxchgdst. src

11. y: orgx

12. byte 0F3H.0Fh. op&_st

13. orgy

14. ENDIF

15. UnDefineXMMxRegs

16. endm

Центральное место в макроопределении ХММ1 d_st_f3 занимают команда целочисленного устройства (в данном случае — CMPXCHG) и директива ORG. Первое действие данной макрокоманды — выяснить тип операнда приемника (dst) в макрокоманде MOVSS, так как он может быть и регистром, и ячейкой памяти. Это необходимо для правильного определения кода операции, которая будет управлять направлением потока данных. После того как определен приемник данных, с помощью условного перехода осуществляется переход на ветвь программы, где будет выполняться собственно формирование соответствующего ХММ-команде

MOVSS кода операции.

Формирование кода операции ХММ-команды MOVSS производится с помощью директивы org, которая предназначена для изменения значения счетчика адреса. В строках 6 или 11 директива org устанавливает значение счетчика адреса равным адресу метки х. Адрес метки х является адресом первого байта машинного кода команды CMPXCHG. Директива db в следующих строках размещает по этому адресу байтовые значения 0F3H,0Fh, ор&_1 d или 0F3H,0Fh, op&st, в зависимости от того, какое действие производится — загрузка (_ld) или сохранение (_st). Значение opc_Movss, с помощью которого формируются значения op&_st и ор&_1 d, определены в начале файла iaxmm.inc:

opcjtovssjd - 010Н

opc_Movss_st - 011H

Для дотошных читателей заметим еще один характерный момент. Для его полного понимания необходимо хорошо представлять себе формат машинной команды и назначение его полей. Достаточно полная информация об этом приведена в литературе [39, 40]. Обратите внимание на порядок следования операндов в заголовке макрокоманды, который построен по обычной для команд ассемблера схеме: коп назначение, источник. В команде CMPXCHG порядок обратный. Этого требует синтаксис команды. Это хорошо поясняет назначение бита d во втором байте кода операции, который характеризует направление передачи данных в микропроцессор (то есть в регистр) или в память (из микропроцессора (регистра)). Вы можете провести эксперимент. Проанализируйте машинные коды команды MOV:

  • Команды с непосредственным операндом:
  • CMPPS RXMM1. RXMM2/ml28, 18 CMPSS RXMM1, RXMM2/m32. i8
  • Однооперандные команды: FXRSTOR m512 FXSAVE m512 LDMXCSR m32 STMXCSR m32

Из перечисленных выше групп команд можно вывести следующую обобщенную структуру команды:

метка: код_операции операнд1. операнд2, операндЗ] ;текст комментария

Данная структура почти совпадает со структурой обычных команд ассемблера. В соответствии с общими принципами трансляции препроцессор будет работать с исходной программой в несколько этапов.

  1. 1. Лексический анализ (сканирование) исходного текста.
  2. 2. Синтаксический анализ.
  3. 3. Генерация кода.

Необходимо отметить, что по принципу действия разрабатываемый нами препроцессор относится к интерпретаторам. Читатель наверняка понимает, в чем состоит разница между интерпретатором и компилятором. Объект для работы компилятора — исходный текст программы в полном объеме. Выход компилятора — объектный модуль, то есть машинное представление исходной программы, пригодное для компоновки с другими модулями или получения исполняемого модуля. Интерпретатор работает с отдельными строками исходной программы. Распознав синтаксическую правильность строки, интерпретатор исполняет ее. В частности, интерпретация характерна для обработки входных строк командного процессора. Поэтому на примере данной задачи читатель может научиться достаточно профессионально организовывать языковое взаимодействие с пользователями своих программ.

В главе 2 описаны основные шаги разработки компилятора. Для интерпретатора разница невелика, в чем мы убедимся ниже.

Для распознавания лексем входной программы разработаем сканер, следуя для этого следующему алгоритму.

  1. 1. Выделить классы лексем.
  2. 2. Определить классы литер.
  3. 3. Определить условия выхода из сканера для каждого класса лексем.
  4. 4. Каждому классу лексем поставить в соответствие грамматику класса 3.
  5. 5. Для каждой грамматики, построенной на шаге 4, построить конечный автомат, который будет распознавать лексему данного класса.
  6. 6. Выполнить объединение («склеивание») конечных автоматов для всех классов лексем.
  7. 7. Составить матрицу переходов для «склеенного» конечного автомата.
  8. 8. «Навесить» семантику на дуги «склеенного» конечного автомата.
  9. 9. Выбрать коды лексической свертки для терминалов грамматики и формат таблицы идентификаторов.
  10. 10. Разработать программу сканера.

Язык описания команд ассемблера

Множество команд ассемблера можно описать с помощью следующего языка:

ASM_LENG={

Vt=(+ - AL АН BL BH CL CH DL DH AX EAX BX EBX CX ECX DX EDX BP EBP SP ESP DI EDI SI ESI BYTE SBYTE WORD SWORD DWORD SDWORD FWORD QWORD TBYTE REAL4 REAL8 REAL10 0 12 3 4 56789abcdefABCDEF NEAR16 NEAR32 FAR16 FAR32 AND NOT HIGH LOW HIGHWORO LOWWORD OFFSET SEG LROFFSET TYPE THIS PTR :.{)[] WIDTH MASK SIZE SIZEOF LENGTH LENGTHOF ST SHORT .TYPE OPATTR . название_команды * / MOD NEAR FAR OR XOR " 'hoqt у H 0 Q T Y { } < > :; EQ NE LT LE GT GE CS DS ES FS GS SS SHR SHL CRO CR2 CR3 DRO DR1 DR2 DR3 DR6 DR7 TR3 TR4 TR5 TR6 TR7 А5СП_символ_буква любой_символ_кроме_кавычки). Vn-( addOp asmlnstruction byteRegister constant constExpr dataType decdigit digits distance expr exprtist Expr eOl eO2 eO3 eO4 eO5 eO6 eO7 eO8 eO9 eOlO eOll hexdigit id mnemonic mulOp nearfar radixOverride orOp oldRecordFieldList relOp recordConst recordFieldList register shiftOp sizeArg string type segmentRegister specialRegister stext string stringChar structTag quote type typeld unionTag). P.

Z=(<asmlnstruction>) }

Множество правил Р грамматики ASM_LENG выглядит следующим образом:

smlnstruction => mnemonic [[ exprList ]] AddOp => + | -

byteRegister => AL | AH | BL | BH 1 CL j CH | DL j DH constant => digits [[ radixOverride ]] constExpr => Expr dataType => BYTE | SBYTE | WORD | SWORD | DWORD | SDWORD | FWORD | QWORD |

TBYTE | REAL4 | REAL8 | REAL 10

decdigit => 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 digits => decdigit | digits decdigit | digits hexdigit distance => nearfar | NEAR16 | NEAR32 | FAR16 | FAR32 eOl => eOl orOp eO2 | eO2 eO2 => eO2 AND еОЗ | еОЗ eO3 => NOT eO4 | eO4 eO4 => eO4 relOp eO5 | eO5 eO5 => eO5 addOp eO6 | eO6 eO6 => eO6 mulOp eO7 | eO6 shiftOp eO7 | eQ7 eO7 => eO7 addOp eO8 | eO8

eO8 => HIGH eO9 | LOW eO9 | HIGHWORD eO9 | LOWWORD eO9 | eO9 eO9 => OFFSET elO | SEG elO | LROFFSET elO | TYPE elO ] THIS elO | eO9 PTR

elO | eO9 : elO | elO

elO => elO . ell | elO [[ expr ]] | ell

, ell => ( expr ) | [ expr ] | WIDTH id | MASK id | SIZE SizeArg | SIZEOF

sizeArg | LENGTH id | LENGTHOF id | recordConst | string | constant | type | id | $ | segmentRegister | register | ST | ST ( expr ) expr => SHORT eO5 | .TYPE eOl | OPATTR eOl | eOl exprList => expr | exprList . expr gpRegister => AX | EAX | BX | EBX | CX | ECX | DX 1 EDX | BP | EBP | SP | ESP I

DI | EDI | SI | ESI

hexdigit =>a|b|c|d|e|f|A|B|C|D|E|F

id => А5С11_символ_буква | id А5СП_символ_буква | id decdigit

mnemonic => название_команды

mulOp => * | / | MOD

nearfar => NEAR | FAR

oldRecordFieldList=> [[ constExpr ]] | oldRecordFieldList . [[ constExpr ]]

За основу языка ASMLENG было взято описание языка MASM (из документации на него), ассемблер, поддерживаемый TASM, незначительно отличается от этого описания (в основном это касается некоторых операторов, типа OPATTR). Подчеркнем тот факт, что язык ASMLENG описывает лишь правило построения команд ассемблера, не затрагивая синтаксиса всей программы ассемблера в целом. Все строки, не являющиеся командами, будут просто игнорироваться и включаться в выходной файл в своем изначальном виде.

Выделение классов лексем

Для грамматики языка ASMLENG можно определить следующие классы лексем:

  • идентификатор — id;
  • ключевые слова - AL АН BL ВН CL СН DL DH АХ ЕАХ ВХ ЕВХ СХ ЕСХ DX EDX ВР ЕВР SP ESP DI EDI SI ESI BYTE SBYTE WORD SWORD DWORD SDWORD FWORD QWORD TBYTE REAL4 REAL8 REAL10 NEAR16 NEAR32 FAR16 FAR32 AND NOT HIGH LOW HIGHWORD LOWWORD OFFSET SEG LROFFSET TYPE THIS PTR WIDTH MASK SIZE SIZEOF LENGTH LENGTHOF ST SHORT .TYPE OPATTR MOD NEAR FAR OR XOR EQ NE LT LE GT GE CS DS ES FS GS SS SHR SHL CRO CR2 CR3 DRO DR1 DR2 DR3 DR6 DR7 TR3 TR4 TR5 TR6 TR7 на-звание_команды;
  • целые числа (константы) — 0123456789abcdefABCDEF;
  • однолитерные разделители — +-/: . ()[] ,*" ' {}<>hoqtyHOQ Т Y;
  • двулитерный разделитель — ;;
  • символьные строки — А5СП_символ_буква, любой_символ_кроме_кавычки.

Классы литер

В случае грамматики языка ASMLENG можно определить следующие классы литер:

  • б — цифра;
  • 1 — буква;
  • b — литеры, которые игнорируются (к ним отнесем пробел); ¦ .
  • si - одиночные разделители: + -/:.()[],*"'{}<>;;
  • s2 — двулитерный разделитель: ;;.

Определение условий выхода из сканера для каждого класса лексем

Для каждого класса лексем определим условия, при которых сканер переходит в конечное состояние:

  • для идентификаторов — появление во входном потоке сканера любого символа, отличного от d (цифра) или 1 (буква);
  • ключевые слова — появление пробела и нахождение соответствия введенной лексемы одному из ключевых слов языка;
  • целые числа (константы) — появление любого символа, отличного от d;
  • однолитерные разделители — появление любого символа;
  • двулитерные разделители — появление любого символа; ,;;.
  • символьные строки — появление завершающей кавычки. ......г.

Построение автоматных грамматик для выделенных классов лексем

Для каждого класса лексем строится отдельная автоматная грамматика, соответствующая грамматике типа 3 по Хомско.му. В нашем случае набор таких грамматик может выглядеть так, как показано ниже:

  • идентификаторы — id, к которым по принципу построения можно отнести и ключевые слова — название_команды:

id=>ASCII_CMMBon_6yKea | id

ASCII_символ_буква | id decdigit :,; decdigit ^ 0|l|2|...8|9

  • целые числа — chint:

digits => decdigit | digits decdigit | digits hexdigit decdigit =>0|l|2|3|4|5|6|7|8|9 ¦

  • Oднолитерные разделители — +-/:.()[]. *"¦'---

SEPiL^. . | . | : |: | + | - | * I 4 ) / I L I ] Г Г I

  • двулитерный разделитель — ;;:

SEP2L=> : Q

«Склеивание» конечных автоматов для всех классов лексем

Мы не будем пытаться получить склеенный автомат, учитывающий все возможные случаи синтаксиса строки с командой ассемблера. Попытаемся получить объединенный конечный автомат для анализа типичной строки программы с ХММ-командой. С учетом этих упрощений «склеенный» конечный автомат может выглядеть так, как показано на рис. 10.2.

clip_image001

Рис. 10.2. Упрощенный вариант «склеенного» конечного автомата

Для представленного на рисунке склеенного конечного автомата таблица (матрица) переходов показана ниже.

L D [ ] пробел

SO SI S2 S4 S4 SO S6

SI SI SI S5 S4

со S2 S2 S4

S3

S4

S5 S3 S3 S3 S3 S3 S3 S4

S6 SG S6 S6 S6 S6 S6 S6 S6

Как обычно, два из этих состояний являются конечными:

  • S3 — состояние ошибки;
  • S4 — конечное состояние.

Состояние S6 — состояние, соответствующее комментарию, то есть допустимы любые символы. Ограничение комментария — конец строки. Строки в таблице переходов соответствуют состояниям склеенного конечного автомата, основа столбцов — классы лексем. Логика работы сканера с использованием таблицы переходов описана в главе 2.

После заполнения таблицы переходов можно навешивать семантику на « дуги» переходов из одного состояния в другое. Основная задача при этом — не брать на себя ничего лишнего. Главное — локализовать поле с названием команды, определить принадлежность ее к группе ХММ-команд. Если это не так, то дальнейший процесс сканирования строки можно прекращать, копировать ее в выходной поток (пусть транслятор ассемблера разбирается с ней сам) и переходить к анализу очередной строки исходного текста ассемблерной программы.

В самом простом случае нашу задачу можно решить легко — в очередной строке выделить метку, если она есть, затем выделить название команды, и если она является ХММ-командой, то продолжить обработку строки. Если очередная строка не является ХММ-командой, то копируем ее полностью в выходной файл. Если очередная строка — ХММ-команда, то локализуем операнды и определяем их тип. По крайней мере один из операндов должен быть регистром. Если строка синтаксически верна для конкретной ХММ-команды, то формируем ее аналог, понятный для восприятия используемым нами транслятором ассемблера. Этот процесс может быть похожим на первый способ формирования ХММ-команд с помощью включаемого файла iaxmm.inc.

Синтаксический анализ

Если веденная строка исходной программы является строкой с ХММ-командой, то необходимо получить ее лексическую свертку для проведения синтаксической проверки и подготовки к дальнейшему преобразованию. Здесь простор для творческих изысканий весьма велик. Поэтому мы не будем повторять соответствующий материал главы 2, а обратимся к тексту программы, который выполняет такое преобразование. Его описание содержит множество технических подробностей, не относящихся к предмету книги. По этой причине программа-препроцессор с соответствующими пояснениями деталей реализации вынесена на дискету, прилагаемую к книге. Протестировать эту программу вы можете на примерах программ с ХММ-командами, приведенными в первой части данной главы.