§ Вход в защищённый режим
Когда программа переводит процессор в защищённый режим, при всём том богатом потенциале, который предоставляют 32-разрядные процессоры, она оказывается практически беспомощной. В основном это связано с тем, что в защищённом режиме совершенно другая система прерываний и воспользоваться ресурсами, предоставляемыми операционной системой, в которой вы запускаете такую программу, невозможно. Более того, недоступными окажутся прерывания BIOS и IRQ. Подробно работа прерываний описана в разделе "Прерывания в защищённом режиме", там же вы найдёте примеры использования прерываний всех типов (программные, аппаратные и исключения), но пока наша программа не сможет ими воспользоваться.В предыдущих главах не раз упоминалась фраза "операционная система", когда шла речь о программе, работающей в защищённом режиме. Дело в том, что после перевода процессора в P-Mode, программа должна определить действия и условия для всех ситуаций, вплоть до того, что определить драйвера (т.е. управляющие процедуры) для таких устройств, как клавиатура, мышь, видеоадаптер, дисковые накопители и даже таймер. Существует два способа реализации таких драйверов - либо написать их самому (что, вообще говоря, не очень сложно), либо обращаться к BIOS-у в режиме виртуального процессора 8086. Оба этих способа будут описаны в соответствующих разделах, где будет подразумеваться, что вы разобрались с разделом "Защищённый режим".
Прежде, чем мы перейдём к примеру, давайте определимся с тем, что нам надо сделать:
- Подготовиться к переходу в P-Mode.
- Перейти в P-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.asm".
При вызове этой функции подразумевается, что в паре регистров DS:BX находится указатель на текущую позицию в GDT. Функция после создания дескриптора переведёт указатель на позицию для следующего дескриптора. Все необходимые параметры передаются через регистры и функция всего лишь "перетасовывает" их в нужном порядке.
Файл "pmode.lib.asm"
1set_descriptor: 2 3 ; Создаёт дескриптор. 4 ; DS:BX = дескриптор в GDT 5 ; EAX = адрес сегмента 6 ; EDX = предел сегмента 7 ; CL = байт прав доступа (access_rights) 8 9 push eax 10 push ecx ; Регистры EAX и ECX мы будем использовать. 11 push cx ; Временно сохраняем значение access_rights. 12 mov cx, ax ; Копируем младшую часть адреса в CX, 13 shl ecx, 16 ; и сдвигаем её в старшую часть ECX. 14 mov cx, dx ; Копируем младшую часть предела в CX. 15 ; Теперь ECX содержит младшую часть дескриптора 16 mov [bx], ecx ; Записываем младшую половину дескриптора в GDT. 17 shr eax, 16 ; EAX хранит адрес сегмента, младшую часть 18 ; которого мы уже использовали, теперь будем 19 ; работать со старшей, для чего сдвигаем её в 20 ; младшую часть EAX, т.е. в AX. 21 mov cl, ah ; Биты адреса с 24 по 31 22 shl ecx, 24 ; сдвигаем в старший байт ECX, а биты адреса 23 mov cl, al ; с 16 по 23 - в младший байт. 24 pop ax ; Возвращаем из стека в AX значение access_rights 25 mov ch, al ; и помещаем его во второй (из четырёх) байт ECX. 26 ; Всё, дескриптор готов. Старшую часть предела и биты GDXU 27 ; мы не устанавливаем и они будут иметь нулевые значения. 28 mov [bx+4], ecx ; Дописываем в GDT вторую половину дескриптора 29 add bx,8 ; Переводим указатель в GDT на следующий дескриптор 30 pop ecx 31 pop eax 32 retВторую функцию "putzs" добавьте в файл "pmode.lib.asm" после описания первой. Эта функция предназначена для вывода на экран строки, оканчивающейся нулевым байтом (т.е. байтом, равным 00h). Такая строка везде на этом сайте называется ZS-строка или просто ZS (от Zero-String). Также вам будут встречаться строки другого типа - LS (от Lenght-String), длина которых будет задана первым байтом.
Функция "putzs" получила своё название от комбинации слов "Put" и "ZS", по аналогии с похожими Си-функциями. В этой функции сохраняются используемые регистры - в приводимом далее примере это никакого эффекта не вызовет - процессор сразу после вывода зависнет, но для других примеров это будет полезно.
1putzs: 2 3 ; DS:BX = ZS ; ZS = Zero-String - строка, оканчивающаяся нулевым (00h) байтом. 4 ; ES:DI = позиция вывода ; ES описывает сегмент видеопамяти, DI - смещение в нём. 5 6 push ax 7 push bx 8 push es 9 push di 10 mov ah, 1bh ; В AH будет атрибут вывода - светло-циановые символы на синем фоне. 11 12putzs_loop: 13 14 mov al, [bx] ; Читаем байт из ZS-строки. 15 inc bx ; Переводим указатель на следующий байт. 16 cmp al,0 ; Если байт равен 0, 17 je putzs_end ; то переходим в конец процедуры. 18 stosw ; Иначе - записываем символ вместе с атрибутом в видеопамять 19 jmp putzs_loop ; Повторяем процедуру для следующего байта из ZS-строки. 20 21putzs_end: 22 23 pop di 24 pop es 25 pop bx 26 pop ax 27 retТеперь сам пример. Прежде чем приступить к его изучению, хочу сделать следующие замечания:
- Программа написана на fasm
- Программа протестирована и работает, поэтому, если она у вас не заработает, проверьте, ВЫ не ошиблись?
- Программа протестирована как отдельный .com-файл. В принципе, вы её можете без изменений перенести в образ .exe-файла или встроить в программу языка высокого уровня - всё должно работать.
- Обратите внимание, что таблица GDT и сегменты не выровнены в памяти и программа всё равно работает. Это сделано специально, для демонстрации возможностей P-Mode. Для повышения производительности программы, конечно же следует выровнять все структуры данных, используемые процессором непосредственно (у нас пока это только GDT) и сегменты.
Пример 2. Переход в защищённый режим.
Файл "example2.asm":
1 org 100h ; Либо 7C00h, если запуск из boot-сектора 2;----------------------------------------------------------------------- 3; Определяем селекторы как константы. У всех у них биты TI = 0 (выборка 4; дескрипторов производится из GDT), RPL = 00B - уровень привилегий - 5; нулевой. 6 7 Code_selector equ 8 8 Stack_selector equ 16 9 Data_selector equ 24 10 Screen_selector equ 32 11 12;----------------------------------------------------------------------- 13 mov bx, GDT + 8 ; Нулевой дескриптор устанавливать не будем - всё равно он не используется. 14 xor eax, eax ; EAX = 0 15 mov edx, eax ; EDX = 0 16 push cs ; AX = CS = сегментный адрес текущего 17 pop ax ; сегмента кода. 18 19 ; EAX = физический адрес начала сегмента кода. Эта программа, работая в среде операционной системы 20 ; режима реальных адресов (подразумевается, что это - MS-DOS) уже имеет в IP смещение относительно 21 ; текущего сегмента кода. Мы определим дескриптор кода для защищённого режима с таким же адресом 22 ; сегмента кода, чтобы при переходе через команду дальнего перехода фактически переход произошёл 23 ; на следующую команду. 24 25 shl eax, 4 26 mov dx, 65535 ; Предел сегмента кода может быть любым, лишь бы он 27 ; покрывал весь реально существующий код (64kb) 28 mov cl, 10011000b ; Права доступа сегмента кода (P = 1, 29 ; DPL = 00b, S = 1, тип = 100b, A = 0) 30 call set_descriptor ; Конструируем дескриптор кода. 31 lea dx, [Stack_seg_start] ; EDX = DX = начало стека (см. саму метку). 32 add eax, edx ; EAX уже содержит адрес начала сегмента 33 ; кода, сегмент стека начнётся с последней 34 ; метки программы Stack_seg_start. 35 mov dx, 1024 ; Предел стека. Также любой (в данном 36 ; примере), лишь бы его было достаточно. 37 mov cl, 10010110b ; Права доступа дескриптора сегмента 38 ; стека (P = 1, DPL = 00b, S = 1, 39 ; тип = 011b, A = 0). 40 call set_descriptor ; Конструируем дескриптор стека. 41 xor eax, eax ; EAX = 0 42 mov ax, ds 43 shl eax, 4 ; EAX = физический адрес начала сегмента данных. 44 xor ecx, ecx ; ECX = 0 45 lea cx, [PMode_data_start] ; ECX = CX 46 add eax, ecx ; ECX = физический адрес начала сегмента данных 47 48 lea dx, [PMode_data_end] 49 50 sub dx, cx ; DX = PMode_data_end - PMode_data_start (это 51 ; размер сегмента данных, в данном примере 52 ; он равен 26 байтам). Этот размер мы и 53 ; будем использовать как предел. 54 mov cl, 10010010b ; Права доступа сегмента данных (P = 1, 55 ; DPL = 00b, S = 1, тип = 001, A = 0). 56 call set_descriptor ; Конструируем дескриптор данных. 57 mov eax, 0b8000h ; Физический адрес начала сегмента 58 ; видеопамяти для цветного текстового 59 ; режима 80 символов, 25 строк 60 ; (используется по умолчанию в MS-DOS). 61 mov edx, 4000 ; Размер сегмента видеопамяти (80*25*2 = 4000). 62 mov cl, 10010010b ; Права доступа - как сегмент данных 63 call set_descriptor ; Конструируем дескриптор сегмента видеопамяти. 64 65; Устанавливаем GDTR: 66 67 xor eax, eax ; EAX = 0 68 mov edx, eax ; EDX = 0 69 70 mov ax, ds 71 shl eax, 4 ; EAX = физический адрес начала сегмента данных. 72 lea dx, [GDT] 73 add eax, edx ; EAX = физический адрес GDT 74 mov [GDT_adr], eax ; Записываем его в поле адреса образа GDTR. 75 mov dx,39 ; Предел GDT = 8 * (1 + 4) - 1 76 mov [GDT_lim], dx ; Записываем его в поле предела образа GDTR. 77 cli ; Запрещаем прерывания. Для того, чтобы прерывания 78 ; работали в защищённом режиме их нужно специально 79 ; определять, что в данном примере не делается. 80 lgdt [GDTR] ; Загружаем образ GDTR в сам регистр GDTR. 81 82; Переходим в защищённый режим: 83 84 mov eax, cr0 85 or al, 1 86 mov cr0, eax 87 88; Процессор в защищённом режиме! 89 90 db 0eah ; Этими пятью байтами кодируется команда 91 dw P_Mode_entry ; jmp far Code_selector : P_Mode_entry 92 dw Code_selector 93 94; Подразумевается, что файл находится 95; в том же каталоге, что и example2.asm 96 97 include "pmode.lib.asm" 98 99;----------------------------------------------------------------------- 100 101P_Mode_entry: 102 103; В CS находится уже не сегментный адрес сегмента кода, а селектор его 104; дескриптора. 105 106; Загружаем сегментные регистры. Это обеспечит правильную работу программы 107; на любом 32-разрядном процессоре. 108 109 mov ax, Screen_selector 110 mov es, ax 111 mov ax, Data_selector 112 mov ds, ax 113 mov ax, Stack_selector 114 mov ss, ax 115 mov sp, 0 116 117; Выводим ZS-строку: 118; ---------------------------------------------------------------------- 119; DS:BX = указатель на начало ZS-строки. Адрес 120; сегмента данных определён по метке 121; PMode_data_start, а строка начинается сразу после 122; этой метки, её смещение от метки равно 0, 123; следовательно, это и будет смещение от начала 124; сегмента данных. 125 126 mov bx, 0 127 mov di, 480 ; Выводим ZS-строку со смещения 480 в 128 ; видеопамяти (оно соответствует началу 129 ; 3-й строки на экране в текстовом режиме). 130 call putzs 131 132; Зацикливаем программу: 133 134 jmp $ 135 136;----------------------------------------------------------------------- 137; Образ регистра GDTR: 138 139GDTR: 140GDT_lim dw ? 141GDT_adr dd ? 142 143;-------------------------------------------------------------------------- 144GDT: 145 dd ?,? ; 0-й дескриптор 146 dd ?,? ; 1-й дескриптор (кода) 147 dd ?,? ; 2-й дескриптор (стека) 148 dd ?,? ; 3-й дескриптор (данных) 149 dd ?,? ; 4-й дескриптор (видеопамяти) 150;-------------------------------------------------------------------------- 151; Начало сегмента данных для защищённого режима. 152 153PMode_data_start: 154 155 db "I am in protected mode!!!",0 ; ZS-строка для вывода в P-Mode. 156 157;-------------------------------------------------------------------------- 158PMode_data_end: ; Конец сегмента данных. 159;-------------------------------------------------------------------------- 160 db 1024 dup (?) ; Зарезервировано для стека. 161 162Stack_seg_start: ; Последняя метка программы - отсюда будет расти стек.По сути это все, что надо, чтобы зайти в защищенный режим. Конечно, есть и другой, более короткий вариант, но он пока не рассматривается, поскольку он не наглядный.