В этом выпуске мы наконец-то применим часть полученных знаний на практике. Сейчас мы попробуем переключиться в защищенный режим. И все. Возвращаться назад (в реальный) мы не будем...
Ты сам убедишься, что для переключения в защищенный режим нужно выполнить ряд простых и незамысловатых по сути действий.
Конечно, тебе наверняка хотелось бы побыть ТАМ подольше :), выполнить какие-нибудь невероятные, головокружительные трюки, чего-нибудь такое, чего в реальном сделать просто нереально. К сожалению, пока тех знаний, которыми мы обладаем, недостаточно, поэтому ограничимся малым - выведем на экран надпись.
Итак, для начала определимся с моделью памяти. Пусть это будет flat-модель (все сегменты имеют базу ноль и лимит 4 Гб). Рассмотрим сегментную адресацию. Страничную – в следующем выпуске.
Главное – это начать; начать можно с чего угодно, но я предлагаю с заполнения таблиц дескрипторов (надеюсь, еще помнишь что это такое? Дескриптор – описывает сегмент, селектор – указатель на дескриптор… Ну сейчас по ходу дела все вспомнится).
И еще, если у кого то в данном выпуске разъехалось форматирование - пишите, вышлю нормальный вариант (это уже к subscribe.ru все претензии, они не любят тег pre).
Поехали!
Таблица, которую нам нужно заполнить – таблица глобальных дескрипторов (GDT). У нас не будет таблицы локальных дескрипторов. Пусть в дескрипторах GDT будут описаны следующие сегменты:
- сегмента кода (код, который будет исполняться в защ. режиме)
- сегмент данных (данные, которые мы будем выводить на экран)
- сегмент видеопамяти (фактически это сегмент, в который мы будем выводить наш текст)
ТАБЛИЦА ГЛОБАЛЬНЫХ ДЕСКРИПТОРОВ:
GDT:
; нулевой дескриптор (обязательно должен присутствовать в GDT!)
NULL_descr db 8 dup (0)
; дескриптор 32-разрядного сегмента кода: база = 00000000h, размер = FFFFFFFFh
CODE_descr db 0FFh, 0FFh, 00h, 00h, 00h, 10011010b, 11001111b , 00h
; дескриптор 32-разрядного сегмента данных: база = 00000000h, размер = FFFFFFFFh
DATA_descr db 0FFh, 0FFh, 00h, 00h, 00h, 10010010b, 11001111b , 00h
; дескриптор сегмента видеопамяти: база = 000B8000h, размер = 0000FFFFh
VIDEO_descr db 0FFh, 0FFh, 00h, 80h, 0Bh, 10010010b, 01000000b , 00h
; размер таблицы GDT:
GDT_size db $-GDT
; а следующие три слова (размер GDT и линейный адрес начала таблицы) мы должны будем попозже загрузить в GDTR:
GDTR dw GDT_size-1
dd ?
Вычислим линейный адрес таблицы GDT. Зачем? Чтоб загрузить регистр GDTR. Линейный адрес GDT = линейный адрес базы сегмента RM_CODE (потому что GDT расположена именно в этом сегменте у нас в программе) + смещение метки GDT в нем.
xor EAX,EAX ; обнуление регистра EAX
mov AX,RM_CODE ; теперь AX = номер сегмента RM_CODE
shl EAX,4 ; сдвигаем значение в EAX на 4 влево
; вот теперь EAX = линейный адрес базы сегмента RM_CODE
add AX,offset GDT ; теперь EAX = линейный адрес GDT
; линейный адрес GDT кладем в заранее подготовленную переменную:
mov dword ptr GDTR+2,EAX
; собственно, загрузка регистра GDTR:
lgdt fword ptr GDTR
(вообще желательно просмотреть весь исходник целиком, потому что RM_CODE сбивает с толку)
Теперь нам еще нужно вычислить точно таким же макаром линейный адрес т.н. ТОЧКИ ВХОДА В ЗАЩИЩЕННЫЙ РЕЖИМ. Дело в том, что после переключения в защ. режим мы окажемся в неком подвешенном состоянии – когда регистр CS еще содержит номер сегмента из реального режима, а не селектор. Мы не можем просто сделать mov CS, селектор, CS можно изменить только дальним jmp-ом либо iret-ом, но щас не про то. Поэтому нам все же придется делать дальний jmp для изменения CS. НО КУДА? Вот именно на ТОЧКУ ВХОДА В ЗАЩИЩЕННЫЙ РЕЖИМ, см. исходник.
Мы уже почти готовы переключится в защ. режим, единственное, что мы обязаны еще сделать – ЗАПРЕТИТЬ ВСЕ ПРЕРЫВАНИЯ (причем, как маскируемые, так и немаскируемые), потому что пока мы не умеем с ними работать, и у нас нет ни одного обработчика, первый же тик таймера после переключения в PM завесит нам всю систему...
; запрещаем сначала маскируемые прерывания:
cli
; а затем и немаскируемые:
in AL,70h ; читаем 70h порт
or AL,80h ; ставим восьмой бит
out 70h,AL ; запихиваем на место
; теперь все прерывания запрещены
Теперь нада обязательно посидеть на дорожку :)... все. Теперь можно. Для переключения в PM нужно установить нулевой бит регистра CR0:
mov EAX,CR0 ; EAX = CR0
or AL,1 ; ставим нулевой бит
mov CR0,EAX ; пихаем назад...
Кто играл в Half Life, помните, в первой части, когда взрыв на заводе в самом начале, Гордон падает в обморок, а потом на несколько секунд приходит в себя и оказывается в другом мире, вокруг какие то существа едят траву, вот что то типа этого произошло сейчас... :)
Теперь давайте осмотримся кругом, что мы видим? Ага, вон таблица GDT во тьме. Память? Оперативная память на месте. Регистры, где вы? Мы все тута! Ну слава Богу... мы здесь не одни. Где таблица векторов? Но нет ответа... Ладно, и без нее обойдемся...
; загрузим в CS селектор на подготовленный сегмент кода. Мы не можем просто взять и написать mov CS,селектор:
dd 66h ; префикс изменения разрядности операнда
db 0EAh ; опкод команды jmp far
dd ? ; смещение в сегменте, на которое мы jmp-аем
dw 00001000b ; селектор сегмента, в который мы jmp-аем
Итак, давай разберемся, куда же мы все-таки прыгнули. Селектор равен 00001000b. Младшие два бита – нули, это уровень привилегий, на него не смотрим. Второй бит (TI) – нолик. Вспоминаем. Значит этот селектор указывает на дескриптор из таблицы GDT... На какой же? Смотрим левее... 001! На первый! А что у нас описывает первый дескриптор в GDT? Правильно, сегмент кода.
; значит, мы перелетели на некую «точку входа в защищенный режим», смещение которой мы уже высчитали (см. исходник):
ENTRY_POINT:
; загрузим сегментные регистры селекторами на соответствующие дескрипторы:
mov AX,00010000b ; AX = селектор дескриптора данных (№2)
mov DS,AX ; кладем его в DS
mov AX,00011000b ; AX = селектор дескриптора видеопамяти (№3)
mov ES,AX ; кладем его в ES
xor SI,SI ; обнуляем SI
mov SI,PM_DATA ; SI = номер сегмента PM_DATA
shl ESI,4 ; ESI = линейный адрес сегмента PM_DATA
add ESI,offset message ; ESI = линейный адрес строки message
xor EDI,EDI ; EDI = позиция на экране (относительно 0B8000h)
mov ECX,mes_len ; длина текста в ECX
; вывод на экран:
rep movsb ; DS:ESI (наше сообщение) -> ES:EDI (видеопамять)
jmp $ ; погружаемся в вечный цикл
Теперь единственное, что мы можем сделать – это перезагрузить компьютер (причем, только кнопкой RESET). Я умышленно убрал часть кода, которая отвечает за переключение обратно в реальный режим, потому что она сильно все затуманит (надо в таблице GDT еще подготовить 16-битные дескрипторы), но если кому интересно – пишите.
Да, еще что. В полном исходнике (см. ниже) сразу может напугать «Линия А20». Что это такое? Дело в том, что после запуска компа для совместимости с 8086 используются 20-разрядные адреса (адресные линии А0-А19), т.ч. попытка записать что-то по линейному адресу 100000h приведет к записи по адресу 0h. Вот этот режим нам и нужно отменить (установив бит 2 в 92h порту) для использования 32-х разрядной адресации. Линия А20 не стоит того, чтобы о ней думать.
Самое главное – СПРАШИВАЙТЕ, СПРАШИВАЙТЕ И ЕЩЕ РАЗ СПРАШИВАЙТЕ! Приветствуются любые вопросы, начиная с «Что такое привилегированные инструкции?» и заканчивая «Почему так много пробелов в конце выводимой фразы?». На второй вопрос отвечу сразу. Дело в том, что при организации блоков повторений через irpc TASM почему то неправильно вычисляет его длину, если мы делаем это через equ. Посмотрите сами и напишите, если вы знаете в чем тут причина.
Скачать исходник целиком можно здесь: PM.asm
; ------------------------CUT HERE----------------------
; TASM:
; TASM /m PM.asm
; TLINK /x /3 PM.obj
; PM.exe
; MASM:
; ML /c PM.asm
; LINK PM.obj,,NUL,,,
; PM.exe
.386p ; разрешить привилегированные инструкции i386
; СЕГМЕНТ КОДА (для Real Mode)
; ---------------------------------------------------------------------------------------------------------
RM_CODE segment para public 'CODE' use16
assume CS:RM_CODE,SS:RM_STACK
@@start:
; очистка экрана:
mov AX,3
int 10h
; открываем линию А20 (для 32-х битной адресации):
in AL,92h
or AL,2
out 92h,AL
; вычисляем линейный адрес метки ENTRY_POINT (точка входа в защищенный режим,
; находится в PM_CODE сегменте, поэтому от него и пляшем):
xor EAX,EAX ; обнуляем регистра EAX
mov AX,PM_CODE ; AX = номер сегмента PM_CODE
shl EAX,4 ; EAX = линейный адрес PM_CODE
add EAX,offset ENTRY_POINT ; EAX = линейный адрес ENTRY_POINT
mov dword ptr ENTRY_OFF,EAX ; сохраняем его в переменной ENTRY_OFF
; (кстати, подобный "трюк" называется SMC или Self Modyfing Code - самомодифицирующийся код)
; теперь надо вычислить линейный адрес GDT (для загрузки регистра GDTR):
xor EAX,EAX
mov AX,RM_CODE ; AX = номер сегмента RM_CODE
shl EAX,4 ; EAX = линейный адрес RM_CODE
add AX,offset GDT ; теперь EAX = линейный адрес GDT
; линейный адрес GDT кладем в заранее подготовленную переменную:
mov dword ptr GDTR+2,EAX
; а подобный трюк назвать SMC уже нельзя, потому как по сути мы модифицируем данные :)
; собственно, загрузка регистра GDTR:
lgdt fword ptr GDTR
; запрет маскируемых прерываний:
cli
; запрет немаскируемых прерываний:
in AL,70h
or AL,80h
out 70h,AL
; переключение в защищенный режим:
mov EAX,CR0
or AL,1
mov CR0,EAX
; загрузить новый селектор в регистр CS
db 66h ; префикс изменения разрядности операнда
db 0EAh ; опкод команды JMP FAR
ENTRY_OFF dd ? ; 32-битное смещение
dw 00001000b ; селектор первого дескриптора (CODE_descr)
; ТАБЛИЦА ГЛОБАЛЬНЫХ ДЕСКРИПТОРОВ:
GDT:
; нулевой дескриптор (обязательно должен присутствовать в GDT!):
NULL_descr db 8 dup(0)
CODE_descr db 0FFh,0FFh,00h,00h,00h,10011010b,11001111b,00h
DATA_descr db 0FFh,0FFh,00h,00h,00h,10010010b,11001111b,00h
VIDEO_descr db 0FFh,0FFh,00h,80h,0Bh,10010010b,01000000b,00h
GDT_size equ $-GDT ; размер GDT
GDTR dw GDT_size-1 ; 16-битный лимит GDT
dd ? ; здесь будет 32-битный линейный адрес GDT
RM_CODE ends
; ---------------------------------------------------------------------------------------------------------
; СЕГМЕНТ СТЕКА (для Real Mode)
; ---------------------------------------------------------------------------------------------------------
RM_STACK segment para stack 'STACK' use16
db 100h dup(?) ; 256 байт под стек - это даже много
RM_STACK ends
; ---------------------------------------------------------------------------------------------------------
; СЕГМЕНТ КОДА (для Protected Mode)
; ---------------------------------------------------------------------------------------------------------
PM_CODE segment para public 'CODE' use32
assume CS:PM_CODE,DS:PM_DATA
ENTRY_POINT:
; загрузим сегментные регистры селекторами на соответствующие дескрипторы:
mov AX,00010000b ; селектор на второй дескриптор (DATA_descr)
mov DS,AX ; в DS его
mov AX,00011000b ; селектор на третий дескриптор (VIDEO_descr)
mov ES,AX ; а этого в ES
xor SI,SI ; обнуляем SI
mov SI,PM_DATA ; SI = номер сегмента PM_DATA
shl ESI,4 ; ESI = линейный адрес сегмента PM_DATA
add ESI,offset message ; ESI = линейный адрес строки message
xor EDI,EDI ; EDI = позиция на экране (относительно 0B8000h)
mov ECX,mes_len ; длина текста в ECX
; вывод на экран:
rep movsb ; DS:ESI (наше сообщение) -> ES:EDI (видеопамять)
jmp $ ; погружаемся в вечный цикл
PM_CODE ends
; ---------------------------------------------------------------------------------------------------------
; СЕГМЕНТ ДАННЫХ (для Protected Mode)
; ---------------------------------------------------------------------------------------------------------
PM_DATA segment para public 'DATA' use32
assume CS:PM_DATA
; сообщение, которое мы будем выводить на экран (оформим его в виде блока повторений irpc):
message:
irpc mes,
db '&mes&',0Dh
endm
mes_len equ $-message ; длина в байтах
PM_DATA ends
; ---------------------------------------------------------------------------------------------------------
end @@start
; ------------------------CUT HERE----------------------
Находчивый подписчик сразу спросит: «А не могли бы мы, допустим, сделать базу видеосегмента в нуле, а при выводе на экран значение 0B8000h поместить в EDI?». Тот, у кого возникнет данный вопрос – уже видит защищенный режим насквозь. Конечно могли бы, это то же самое по сути!
Ну и напоследок еще одна деталька (на всякий случай): ВЫ МОЖЕТЕ ПЕРЕКЛЮЧИТСЯ В ЗАЩИЩЕННЫЙ РЕЖИМ НАХОДЯСЬ ТОЛЬКО В РЕАЛЬНОМ РЕЖИМЕ !!!
В следующем выпуске нас ждет страничная адресация, поэтому настоятельно рекомендую до конца разобраться с сегментной.
|