§ Вход в защищённый режим

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