Процессор Intel в защищенном режиме #8 — Архив WASM.RU

Все статьи

Процессор Intel в защищенном режиме #8 — Архив WASM.RU

В этом выпуске мы наконец-то применим часть полученных знаний на практике. Сейчас мы попробуем переключиться в защищенный режим. И все. Возвращаться назад (в реальный) мы не будем... Ты сам убедишься, что для переключения в защищенный режим нужно выполнить ряд простых и незамысловатых по сути действий. Конечно, тебе наверняка хотелось бы побыть ТАМ подольше :), выполнить какие-нибудь невероятные, головокружительные трюки, чего-нибудь такое, чего в реальном сделать просто нереально. К сожалению, пока тех знаний, которыми мы обладаем, недостаточно, поэтому ограничимся малым - выведем на экран надпись.

Итак, для начала определимся с моделью памяти. Пусть это будет flat-модель (все сегменты имеют базу ноль и лимит 4 Гб). Рассмотрим сегментную адресацию. Страничную – в следующем выпуске.

Главное – это начать; начать можно с чего угодно, но я предлагаю с заполнения таблиц дескрипторов (надеюсь, еще помнишь что это такое? Дескриптор – описывает сегмент, селектор – указатель на дескриптор… Ну сейчас по ходу дела все вспомнится).

Поехали!

Таблица, которую нам нужно заполнить – таблица глобальных дескрипторов (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-х разрядной адресации.

Самое главное – СПРАШИВАЙТЕ, СПРАШИВАЙТЕ И ЕЩЕ РАЗ СПРАШИВАЙТЕ! Приветствуются любые вопросы, начиная с «Что такое привилегированные инструкции?» и заканчивая «Почему так много пробелов в конце выводимой фразы?». На второй вопрос отвечу сразу. Дело в том, что при организации блоков повторений через irpc TASM почему то неправильно вычисляет его длину, если мы делаем это через equ. Посмотрите сами и напишите, если вы знаете в чем тут причина.

Скачать исходник целиком можно здесь: http://brokensword.hotbox.ru/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 (точка входа в защищенный режим):
		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		; сохраняем его в переменной  	
; (кстати, подобный "трюк" называется 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?». Тот, у кого возникнет данный вопрос – уже видит защищенный режим насквозь. Конечно могли бы, это то же самое по сути!

Ну и напоследок еще одна деталька (на всякий случай): ВЫ МОЖЕТЕ ПЕРЕКЛЮЧИТСЯ В ЗАЩИЩЕННЫЙ РЕЖИМ НАХОДЯСЬ ТОЛЬКО В РЕАЛЬНОМ РЕЖИМЕ !!!

2002-2013 (c) wasm.ru