Навигация сайта

  • Главная
  • Статьи
  • Учебники
  • Программы
  • Друзья сайта

Глава 9. Вход в защищённый режим

Когда программа переводит процессор в защищённый режим, при всём том богатом потенциале, который предоставляют 32-разрядные процессоры, она оказывается практически беспомощной. В основном это связано с тем, что в защищённом режиме совершенно другая система прерываний и воспользоваться ресурсами, предоставляемыми операционной системой, в которой вы запускаете такую программу, невозможно. Более того, недоступными окажутся прерывания BIOS и IRQ. Подробно работа прерываний описана в разделе "Прерывания в защищённом режиме", там же вы найдёте примеры использования прерываний всех типов (программные, аппаратные и исключения), но пока наша программа не сможет ими воспользоваться.
В предыдущих главах не раз упоминалась фраза "операционная система", когда шла речь о программе, работающей в защищённом режиме. Дело в том, что после перевода процессора в P-Mode, программа должна определить действия и условия для всех ситуаций, вплоть до того, что определить драйвера (т.е. управляющие процедуры) для таких устройств, как клавиатура, мышь, видеоадаптер, дисковые накопители и даже таймер. Существует два способа реализации таких драйверов - либо написать их самому (что, вообще говоря, не очень сложно), либо обращаться к BIOS-у в режиме виртуального процессора 8086. Оба этих способа будут описаны в соответствующих разделах, где будет подразумеваться, что вы разобрались с разделом "Защищённый режим".

Прежде, чем мы перейдём к примеру, давайте определимся с тем, что нам надо сделать:

1. Подготовиться к переходу в P-Mode.
2. Перейти в P-Mode.
3. Сообщить в программе о переходе в P-Mode.

В третьем пункте мы заставим программу вывести на экран строку "I am in protected mode!!!", после чего программа зациклит процессор (компьютер повиснет). Дело в том, что возврат в режим реальных адресов немного сложнее, чем переход в защищённый режим и это требует дополнительного объяснения, поэтому в этом примере приводится только переход в P-Mode.
Вывод на экран будет происходить в текстовом режиме посредством прямой записи в видеопамять, для чего будет описана отдельная процедура - вы увидите, что код, предназначенный для выполнения в защищённом режиме не требует специальных определений (это будет 16-разрядный код; 32-разрядный определяется немного сложнее).
Остаётся добавить, что пример должен выполняться из простой операционной системы, например, MS-DOS, работающей в режиме реальных адресов. Если вы попытаетесь запустить программу из-под ОС, работающей в защищённом режиме (например, Windows), то программа не заработает, т.к. процессор уже будет работать в P-Mode и не допустит повторного входа в этот режим.
Что касается зависания процессора в конце выполнения нашего примера - так это нормальное явление при отладке программ в защищённом режиме. Этот режим тем и характерен, что допускает работу только одного "хозяина" одновременно, а наша программа, войдя в защищённый режим, как раз и станет таким "хозяином процессора".

Хочу обратить ваше внимание на то, что первой командой после перехода в защищённый режим должна быть команда дальнего перехода (far jump), в которой будет указан селектор дескриптора сегмента кода и смещение в этом сегменте. При работе в защищённом режиме процессор может использовать в сегментных регистрах только селекторы существующих дескрипторов, любые другие значения (например, сегментный адрес) использовать нельзя - процессор сгенерирует исключение общей защиты. Тем не менее, при переходе в защищённый режим регистр CS будет содержать сегментный адрес, который использовался в режиме реальных адресов, поэтому выполнение следующей команды, какой бы она ни была, должно было бы привести к генерации процессором исключения. На самом деле этого не происходит, так как эта команда не выбирается из памяти - она уже находится в конвейере процессора (даже в таком процессоре, как i386, есть конвейер) и поэтому вы можете выполнить эту команду.
Команда дальнего перехода обязательно очистит конвейер процессора и заставит его обратится к таблице GDT, выбрать оттуда дескриптор, селектор которого указан в адресе команды и начать выборку команд со смещения, также указанного в этом адресе. Это критический момент в работе программы. Если в GDT, селекторе, смещении или самой команде будет обнаружена ошибка, то процессор сгенерирует исключение, а так как систему прерываний мы для него пока не определяли, то он попросту зависнет либо произойдёт сброс - это уже зависит от "железа".
Если вы не выполните первой команду дальнего перехода, а другую, которая не изменит содержимое регистра CS (а это - все остальные команды), то процессор произведёт выборку в конвейер новой команды, используя текущие значения CS:IP, а так как в CS содержится не селектор (процессор уже в защищённом режиме!), то произойдёт исключение и зависание.
Это - теория. Такие условия перехода в P-Mode рекомендует Intel и опыт показывает, что лучше придерживаться этих рекомендаций. На практике наблюдаются некоторые чудеса. Например, можно сделать переход не дальний, а короткий (без смены значения в CS) - и программа будет работать, мало того, можно даже не определять стек - всё равно программа работает - процедуры вызываются, можно оперировать стеком, вот только нет уверенности, что стек будет отображаться на ту же область памяти, что была до перехода.
С регистрами данных ситуация обстоит хуже - DS можно не инициализировать, но при работе через него вы получите совсем не те данные, что должны были бы, а обращение к ES, отображённому на видеопамять подвешивает процессор.
Эта ситуация была обнаружена при тестировании примера 2 на процессоре 80386 DX-40 и причина, на мой взгляд, в ошибках архитектуры, допущенных при проектировании этого процессора. Сам Intel подобные ошибки называет errata и сообщает обо всех обнаруженных багах в процессорах, самую свежую информацию о них вы можете найти на www.intel.com и www.intel.ru.

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

И всё же, возвращаясь к примеру, давайте определимся: мы изучаем "правильное" программирование 32-разрядных процессоров, при этом не используя никаких ошибок в архитектуре и никаких недокументированных особенностей и команд. Это нам обеспечит уверенность в том, что наши программы будут надёжно работать на любых интеловских процессорах и их клонах.

Прежде, чем мы перейдём к примеру программы, давайте определим две функции, которые мы будем в дальнейшем использовать.

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

Функция первая, "set_descriptor" предназначенная для создания дескриптора, приводится сразу в том виде, в каком она будет находится в подключаемом файле "pmode.lib".

При вызове этой функции подразумевается, что в паре регистров DS:BX находится указатель на текущую позицию в GDT. Функция после создания дескриптора переведёт указатель на позицию для следующего дескриптора. Все необходимые параметры передаются через регистры и функция всего лишь "перетасовывает" их в нужном порядке.



Файл "pmode.lib":

;-------------------------------------------------------------------------
init_set_descriptor macro

set_descriptor proc near
; Создаёт дескриптор.
; DS:BX = дескриптор в GDT
; EAX = адрес сегмента
; EDX = предел сегмента
; CL = байт прав доступа (access_rights)

push eax
push ecx ; Регистры EAX и ECX мы будем использовать.


push cx ; Временно сохраняем значение access_rights.

mov cx,ax ; Копируем младшую часть адреса в CX,
shl ecx,16 ; и сдвигаем её в старшую часть ECX.

mov cx,dx ; Копируем младшую часть предела в CX.
; Теперь ECX содержит младшую часть
; дескриптора (т.е. первые 4 байта -
; см. рис. 4-1).

mov [ bx ],ecx ; Записываем младшую половину дескриптора в GDT.

shr eax,16 ; EAX хранит адрес сегмента, младшую часть
; которого мы уже использовали, теперь будем
; работать со старшей, для чего сдвигаем её в
; младшую часть EAX, т.е. в AX.

mov cl,ah ; Биты адреса с 24 по 31
shl ecx,24 ; сдвигаем в старший байт ECX,
mov cl,al ; а биты адреса с 16 по 23 - в младший байт.

pop ax ; Возвращаем из стека в AX значение
; access_rights
mov ch,al ; и помещаем его во второй (из четырёх)
; байт ECX.
; Всё, дескриптор готов. Старшую часть
; предела и биты GDXU мы не устанавливаем и
; они будут иметь нулевые значения.

mov [ bx + 4 ],ecx ; Дописываем в GDT вторую половину
; дескриптора.

add bx,8 ; Переводим указатель в GDT на следующий
; дескриптор.

pop ecx
pop eax

ret

endp

endm
;-------------------------------------------------------------------------


Вторую функцию "putzs" добавьте в файл "pmode.lib" после описания первой. Эта функция предназначена для вывода на экран строки, оканчивающейся нулевым байтом (т.е. байтом, равным 00h). Такая строка везде на этом сайте называется ZS-строка или просто ZS (от Zero-String). Также вам будут встречаться строки другого типа - LS (от Lenght-String), длина которых будет задана первым байтом.
Функция "putzs" получила своё название от комбинации слов "Put" и "ZS", по аналогии с похожими Си-функциями. В этой функции сохраняются используемые регистры - в приводимом далее примере это никакого эффекта не вызовет - процессор сразу после вывода зависнет, но для других примеров это будет полезно.



putzs proc near
; DS:BX = ZS ; ZS = Zero-String - строка, оканчивающаяся
; нулевым (00h) байтом.
; ES:DI = позиция вывода ; ES описывает сегмент видеопамяти,
; DI - смещение в нём.

push ax
push bx
push es
push di


mov ah,1bh ; В AH будет атрибут вывода - светло-циановые
; символы на синем фоне.

putzs_1:
mov al,[ bx ] ; Читаем байт из ZS-строки.
inc bx ; Переводим указатель на следующий байт.
cmp al,0 ; Если байт равен 0,
je putzs_end ; то переходим в конец процедуры.

mov es:[ di ],ax ; Иначе - записываем символ вместе с
; атрибутом в видеопамять по заданному
; смещению - цветной символ появится на
; экране.

add di,2 ; Переводим указатель в видеопамяти на
; позицию следующего символа.

jmp putzs_1 ; Повторяем процедуру для следующего байта
; из ZS-строки.

putzs_end:

pop di
pop es
pop bx
pop ax

ret

endp
;--------------------------------------------------------------------------

Теперь сам пример. Прежде чем приступить к его изучению, хочу сделать следующие замечания:

1. Программа протестирована и работает, поэтому, если она у вас не заработает, проверьте, ВЫ не ошиблись? Если нет - пишите, разберёмся.
2. Программа протестирована как отдельный .com-файл. В принципе, вы её можете без изменений перенести в образ .exe-файла или встроить в программу языка высокого уровня - всё должно работать.
3. Обратите внимание, что таблица GDT и сегменты не выровнены в памяти и программа всё равно работает. Это сделано специально, для демонстрации возможностей P-Mode. Для повышения производительности программы, конечно же следует выровнять все структуры данных, используемые процессором непосредственно (у нас пока это только GDT) и сегменты.

Далее полностью приводится файл "example2.asm", который содержит пример перехода в защищённый режим, включая необходимые для tasm-а атрибуты.




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

Файл "example2.asm":


include pmode.lib ; Подразумевается, что файл "pmode.lib" находится
; в том же каталоге, что и "example2.asm".
;-------------------------------------------------------------------------
.386p
pmode segment use16
assume cs:pmode, ds:pmode, es:pmode

org 100h

main proc far
start:
;-------------------------------------------------------------------------
;-------------------------------------------------------------------------
; Определяем селекторы как константы. У всех у них биты TI = 0 (выборка
; дескрипторов производится из GDT), RPL = 00B - уровень привилегий -
; нулевой.

Code_selector = 8
Stack_selector = 16
Data_selector = 24
Screen_selector = 32
;--------------------------------------------------------------------------

mov bx,offset GDT + 8 ; Нулевой дескриптор устанавливать
; не будем - всё равно он не
; используется.

xor eax,eax ; EAX = 0
mov edx,eax ; EDX = 0

push cs
pop ax ; AX = CS = сегментный адрес текущего
; сегмента кода.

shl eax,4 ; EAX = физический адрес начала сегмента кода.
; Эта программа, работая в среде операционной системы
; режима реальных адресов (подразумевается, что это -
; MS-DOS) уже имеет в IP смещение относительно
; текущего сегмента кода. Мы определим дескриптор
; кода для защищённого режима с таким же адресом
; сегмента кода, чтобы при переходе через команду
; дальнего перехода фактически переход произошёл
; на следующую команду.

mov dx,1024 ; Предел сегмента кода может быть любым,
; лишь бы он покрывал весь реально
; существующий код.

mov cl,10011000b ; Права доступа сегмента кода (P = 1,
; DPL = 00b, S = 1, тип = 100b, A = 0)

call set_descriptor ; Конструируем дескриптор кода.

lea dx,Stack_seg_start ; EDX = DX = начало стека (см. саму
; метку).

add eax,edx ; EAX уже содержит адрес начала сегмента
; кода, сегмент стека начнётся с последней
; метки программы Stack_seg_start.

mov dx,1024 ; Предел стека. Также любой (в данном
; примере), лишь бы его было достаточно.

mov cl,10010110b ; Права доступа дескриптора сегмента
; стека (P = 1, DPL = 00b, S = 1,
; тип = 011b, A = 0).

call set_descriptor ; Конструируем дескриптор стека.

xor eax,eax ; EAX = 0
mov ax,ds
shl eax,4 ; EAX = физический адрес начала сегмента данных.
xor ecx,ecx ; ECX = 0
lea cx,PMode_data_start ; ECX = CX
add eax,ecx ; ECX = физический адрес начала сегмента
; данных.

lea dx,PMode_data_end
sub dx,cx ; DX = PMode_data_end - PMode_data_start (это
; размер сегмента данных, в данном примере
; он равен 26 байтам). Этот размер мы и
; будем использовать как предел.

mov cl,10010010b ; Права доступа сегмента данных (P = 1,
; DPL = 00b, S = 1, тип = 001, A = 0).

call set_descriptor ; Конструируем дескриптор данных.

mov eax,0b8000h ; Физический адрес начала сегмента
; видеопамяти для цветного текстового
; режима 80 символов, 25 строк
; (используется по умолчанию в MS-DOS).

mov edx,4000 ; Размер сегмента видеопамяти (80*25*2 = 4000).
mov cl,10010010b ; Права доступа - как сегмент данных
call set_descriptor ; Конструируем дескриптор сегмента
; видеопамяти.

; Устанавливаем GDTR:

xor eax,eax ; EAX = 0
mov edx,eax ; EDX = 0

mov ax,ds
shl eax,4 ; EAX = физический адрес начала сегмента данных.
lea dx,GDT
add eax,edx ; EAX = физический адрес GDT
mov GDT_adr,eax ; Записываем его в поле адреса образа GDTR.

mov dx,39 ; Предел GDT = 8 * (1 + 4) - 1
mov GDT_lim,dx ; Записываем его в поле предела образа GDTR.

cli ; Запрещаем прерывания. Для того, чтобы прерывания
; работали в защищённом режиме их нужно специально
; определять, что в данном примере не делается.

lgdt GDTR ; Загружаем образ GDTR в сам регистр GDTR.

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

mov eax,cr0
or al,1
mov cr0,eax

; Процессор в защищённом режиме!

db 0eah ; Этими пятью байтами кодируется команда
dw P_Mode_entry ; jmp far Code_selector:P_Mode_entry
dw Code_selector

;--------------------------------------------------------------------------
P_Mode_entry:
; В CS находится уже не сегментный адрес сегмента кода, а селектор его
; дескриптора.

; Загружаем сегментные регистры. Это обеспечит правильную работу программы
; на любом 32-разрядном процессоре.

mov ax,Screen_selector
mov es,ax

mov ax,Data_selector
mov ds,ax

mov ax,Stack_selector
mov ss,ax
mov sp,0

; Выводим ZS-строку:

mov bx,0 ; DS:BX = указатель на начало ZS-строки. Адрес
; сегмента данных определён по метке
; PMode_data_start, а строка начинается сразу после
; этой метки, её смещение от метки равно 0,
; следовательно, это и будет смещение от начала
; сегмента данных.

mov di,480 ; Выводим ZS-строку со смещения 480 в
; видеопамяти (оно соответствует началу
; 3-й строки на экране в текстовом режиме).
call putzs

; Зацикливаем программу:

loop_1:
jmp loop_1

;--------------------------------------------------------------------------
init_set_descriptor
init_putzs
;--------------------------------------------------------------------------
; Образ регистра GDTR:

GDTR label fword
GDT_lim dw ?
GDT_adr dd ?
;--------------------------------------------------------------------------
GDT:
dd ?,? ; 0-й дескриптор
dd ?,? ; 1-й дескриптор (кода)
dd ?,? ; 2-й дескриптор (стека)
dd ?,? ; 3-й дескриптор (данных)
dd ?,? ; 4-й дескриптор (видеопамяти)
;--------------------------------------------------------------------------
PMode_data_start: ; Начало сегмента данных для защищённого режима.
;--------------------------------------------------------------------------
db "I am in protected mode!!!",0 ; ZS-строка для вывода в P-Mode.
;--------------------------------------------------------------------------
PMode_data_end: ; Конец сегмента данных.
;--------------------------------------------------------------------------
db 1024 dup (?) ; Зарезервировано для стека.
Stack_seg_start: ; Последняя метка программы - отсюда будет расти стек.
;--------------------------------------------------------------------------
main endp
pmode ends
end start
 

Техническая поддержка и аудит сайта.