Pull to refresh

Что такое Protected Mode и с чем его едят

Reading time 5 min
Views 27K
Для того, чтобы писать операционку, нужно разбираться во многих деталях. Вот давайте я вас немного просвещу, (но давайте договоримся, что маны вы будете читать сами, чтобы было о чём побеседовать).
Честно говоря, на просторах сети есть туча тучная материалов по PM, да и ileyи pehat несколько рассказали об этом режиме, но меня попросили всё равно описать в общих рамках его. Сейчас кратко выдам теорию (вообще то специально для этого Intel маны писала), потом начнём писать код.


Введение в защищённый режим.
Итак, PM значительно отличается от всем привычного ещё со времён DOS’a реального режима (RM). Теперь придётся привыкать: здесь нет статичных, 64 килобайтных сегментов, таблицы прерываний в 1’ом килобайте, адресов баз сегментов в сегментных регистрах, в общем совершенно новый мир.
Теперь сегменты описываются в Global Descriptor Table (GDT). Сия таблица может быть только в одном экземпляре. Она структура в памяти. Не сегмент! Может располагаться в памяти где угодно, но её адрес и лимит записываются в регистр GDTR. Вот его структура:

image

Сама таблица состоит из записей следующей структуры (кстати нулевая запись пустая. Это важно. При обращении к памяти, ‘описываемой’ нулевым дескриптором, получите #GP – General Protection Fault):
Давайте рассмотрим эту структуру повнимательней.
image

1. Segment Limit:
Назначение этого поля понятно по названию, но есть тонкость. Собака зарыта в бите G (Granularity).
Если он неустановлен, то память ‘отсчитывается’ в байтах. В таком случае размер сегмента может варьироваться от 1 байта до 1 мегабайта на размер в 1 байт.
Если установим его в 1, то будет введена страничная адресация памяти. Тогда мы сможем адресовать от 4 килобайт до 4 гигабайт оперативки с изменением размера на 4 килобайта (размер страницы). Вообще страничная адресация предпочтительней (сравните (1Мб+64Кб-16байт) и 4Гб ). Давайте в этом посте поговорим только о сегментной адресации. Paging заслуживает отдельного разговора.

2. Base Address:
Здесь указываем физический адрес базы.

3. Type field:
Комбинации битов определяют тип сегмента:
image

4. S (descriptor type):
В документации интеловской сказано, что если этот бит не установлен, то этот дескриптор для системного сегмента, иначе – кода или данных. Под системным подразумевается LDT, TSS, Interrupt Gates и иже с ними (о них позже).

5. DPL (Descriptor Privilege Level):
Привилегии описываемого сегмента. Всем знакомые Rings.

6. P (segment present):
Если этот бит установлен, то процессор ‘знает’, что сегмент в уже памяти (хотя лучше сказать валидный). Если загрузите в сегментный регистр селектор дескриптора с неустановленным битом P, то произойдёт исключение #NP (not present). Вообще смысл этой витиеватой фразы объясню чуть позже.

7. D/B:
Для сегментов разного типа по-разному трактуется.
1. Для сегментов кода:
32 или 16 битная длина эффективного адреса и размерность операндов.
(1-32; 0-16);
2. Для стека:
Указатель стека 32 или 16 битный. (1-32; 0-16);

8. G:
Влияет на то, в каких единицах (байты, страницы) измеряется лимит сегмента. Вообще Paging можно включить при переходе в PM, установив 31 бит регистра CR0.

Ещё немного информации:
Догадываемся, что слово Global поставили не напрасно. Значит есть ещё какая-то табличка. Верно, есть также Local Descriptor Table. Их может быть великое множество. К примеру они могут использоваться в реализации задач и.т.д. А вот LDT уже представляет собой сегмент! Так что привыкайте к фразам типа ‘дескриптор сегмента локальной таблички дескрипторов’.

После того, как мы описали таблицу, нужно ей загрузить в регистр GDTR. Это делается далеко не mov’ом. GDTR заполняется командой lgdt fword (значение). То есть надо сформировать самостоятельно эту структуру и загрузить в вышеупомянутый регистр. Есть ещё команды работы с этим регистром, но мы несёмся галопом по Европам.

Ещё один момент. В PM в сегментных регистрах хранятся не базовые адреса сегментов (как в RM), а специально обученные штуки, под названием селекторы. Их структура такова:
image
Здесь Index – порядковый номер дескриптора в таблице.
TI показывает где искать дескриптор (в GDT или LDT).

Теперь, когда уже понятно как строить таблицу, поговорим о том, как перейти в PM (замечу, это можно сделать только из RM). Вообще … нужно всего установить бит 0 управляющего регистра CR0. Хотя вру. Для начала нужно запретить все прерывания (NMI (Non Maskable Interrupts) в том числе), открыть адресную линию A20 (чтобы была доступна 32-битная адресация), загрузить GDTR, и прыгнуть на метку – старт.

Давайте воспользуемся загрузчиком (можно KOLIBRI’ский взять), который будет грузить наш код по адресу 1000h:0 (RM’овский, замечу, адрес).
Здесь будет не всё так гладко, как в тех манах, когда в PM переходят прям из бутлоадера. Всё чуточку сложнее. Но сначала давайте разберём код, который бутлоадер будет загружать (всё пишем на FASM'е). Это своеобразный helloworld. Загрузимся, перейдём в PM и напечатаем приветствие. Всё.

format binary
xor ax,ax
cli ;реинициализируем сегментные регистры
mov ss,ax
xor sp,sp
sti
mov ax,3
int 10h

jmp 1000h:r_start

r_start:

mov ax,1000h;перенастраиваем регистры
mov ds,ax
mov es,ax

in al, 0x92;включаем A20
or al, 2
out 0x92, al

cli ;запрещаем прерывания
mov al,8Fh;запрещаем NMI
out 70h,al
in al,71h

lgdt fword [GDTR];загружаем регистр GDTR
mov eax,cr0
or al,1;устанавливаем 0-вой бит
mov cr0,eax;включаем PM

jmp fword 08h:Startup32; прыгаем в PM

align 8 ;процессор быстрее обращается с выравненной табличкой
GDT:
dq 0 ;пустой
db 0FFh,0FFh,0,0,0,9Ah,0CFh,0 ;код
db 0FFh,0FFh,0,0,0,92h,0CFh,0;данные
db 0FFh,0FFh,0,80h,0Bh,92h,40h,0 ;видеосегмент
label GDT_SIZE at $-GDT
GDTR:
dw GDT_SIZE-1
dd GDT+10000h
; нужно записать 32-битный адрес. Сейчас мы находимся в сегменте 1000h, база которого 1000h*10h (по ;физическому адресу) => физический адрес GDTR (метки!) = 10000h (физический адрес базы сегмента)+offset

virtual ;теперь, фактически, забиваем пространство до конца сегмента
rb 10000h-$;
end virtual
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;PM32 Entry;;;;;;;;;;;;;;;;;;;
use32
org $+10000h;вот для чего: в PM мы работаем с Flat-сегментами, и если мы оставим код ;для PM перед org’ом, то ;внутрисегментный адрес не будет совпадать с Flat адресом. Так вот.

Startup32: ;точка входа в PM
mov ax,10h ;здесь пихаем селекторы. Зачастую (! не забываем про порядковый номер в
mov es,ax ;таблице) селектор сегмент кода - 08h. данных - 10h, видеосегмент - 18h
mov ds,ax
mov fs,ax
mov ss,ax
mov esp,10000h;стек
mov ax,18h
mov gs,ax

mov esi,hi_string ;покажем, что мы удачно перешли
call print
jmp $

;ESI - адрес строки
print:
pushad
xor ebx,ebx
mov ah,07h;атрибут
puts:
mov al,[esi+ebx]
mov [gs:(ebx*2)],ax
inc ebx
test al,al
jnz puts
popad
ret
hi_string db ‘Welcome to PM, dude’,0


Что мы сделали? Загрузчик нас успешно загрузил по адресу 1000h:0, откуда мы и продолжили выполнение. Сначала включили А20, запретили все прерывания, загрузили в GDTR подходящее значение, прыгнули на метку входа. Замечу, что прыгали мы на
jmp fword 08h:Startup32
Т.е 08h — селектор дескриптора кода. Привыкайте.

Теперь как сие чудо запустить. Лично я пользуюсь WinImage и VirtualBox. Запихиваем загрузчик в бутсектор дискеты и кладём .bin’овский файл в корень. Сохраняем в .vfd, прописываем путь к образу дискеты в свойствах виртуальной машины, запускаем и видим результат.

В следующем выпуске рассмотрим interrupts, faults, traps, aborts и как они работают, ловятся и отлаживаются. Начнём говорить об архитектуре.

Источники информации.
1) Сразу хочу выразить благодарность Phantom_84 aka egos за то, что указал на путь истинный и помог мне в самом начале. Без него мне было бы гораздо труднее разобраться.
2) Статьи BrokenSword’a Статьи BrokenSword’a. На них стоит обратить внимание.
3) Intel System Programming Guides
Tags:
Hubs:
+55
Comments 16
Comments Comments 16

Articles