Детальное описание схемы преобразования адресов
Теперь перейдём к более строгому описанию схемы преобразования адресов в защищённом режиме.
Регистр GDTR, указывающий расположение в физической памяти и размер глобальной таблицы дескрипторов GDT является ключевым в схеме адресации защищённого режима.
Формат регистра GDTR процессора i80286 приведён на рис.7.
Рис. 7. Формат регистра GDTR процессора i80286.
Из рисунка видно, что регистр GDTR имеет длину 5 байт. Старшие 3 байта содержат 24-разрядный физический адрес таблицы GDT, младшие два байта - длину таблицы GDT, уменьшенную на 1.
Длина GDT, уменьшенная на единицу, называется пределом таблицы GDT (GDT limit). Она используется для проверки правильности задаваемых программой селекторов. Поле индекса селектора должно содержать ссылки только на существующие элементы таблицы GDT, в противном случае произойдет прерывание. Зная размер GDT, процессор блокирует использование селекторов со значениями поля индекса, выходящее за рамки разрешённых для таблицы GDT. Аналогичный механизм используется и для проверки селекторов, ссылающихся на LDT.
Перед переходом в защищённый режим программа должна создать в оперативной памяти таблицу GDT и загрузить регистр GDTR при помощи специальной команды LGDT (синтаксис транслятора Turbo Assembler, режим IDEAL):
lgdt [QWORD gdt_ptr]Перед выдачей команды LGDT gdt_ptr необходимо подготовить область памяти с адресом gdt_ptr, записав в неё физический адрес таблицы GDT и её размер, уменьшенный на 1 (предел):
gdt_ptr dw GDT_LIMIT ; предел таблицы GDT base_lo dw ? ; младшее слово базового адреса GDT base_hi dw ? ; старшее слово базового адреса GDTЗаметьте, что несмотря на то что размер регистра GDTR составляет 5 байт, в качестве операнда для команды LGDT используется адрес области памяти размером 6 байт. Здесь нет никакой ошибки, процессор i80286 использует только 5 байт из этой области, так как физический адрес содержит 24 разряда. Процессоры i80386 и i80486 в 32-разрядном режиме используют все 6 байтов, загружая в 6-байтный регистр GDTR 32-битовый физический адрес таблицы GDT и её 16-битовый предел.
Однако мы ещё не знаем точную структуру таблицы GDT. Сначала мы познакомим вас со структурой таблицы GDT (и соответственно, с идентичной ей структурой таблицы LDT), а затем на примере фрагмента программы покажем, как создать таблицу GDT и загрузить регистр GDTR.
Как мы уже говорили, таблицы GDT и LDT представляют собой массивы дескрипторов - описателей сегментов. Кроме дескрипторов, описывающих сегменты памяти, таблица GDT может содержать специальные типы дескрипторов - вентили вызова (call gate), задач (task gate) и ловушек (trap gate).
Вентили определяют точки входа в соответствующие процедуры. Например, вентиль вызова описывает адрес подпрограммы, вызываемой, например, по команде CALL. При вызове подпрограммы через вентиль в качестве операнда для команды CALL используется селектор, адресующий соответствующий дескриптор в таблице GDT (или в таблице LDT).
На рис. 8 приведён формат дескриптора сегмента для процессора i80286:
Рис. 8. Дескриптор сегмента для процессора i80286.
Длина дескриптора составляет 8 байт. Он состоит из следующих полей:
- поле базового адреса длиной 24 бита содержит физический адрес сегмента, описываемого данным дескриптором;
- поле предела содержит размер сегмента в байтах, уменьшенный на единицу;
- поле доступа описывает тип сегмента (сегмент кода, сегмент данных и др.);
- зарезервированное поле длиной 16 бит для процессора i80286 должно содержать нули, это поле используется процессорами i80386 и i80486 (там, в частности, хранится старший байт 32-разрядного базового адреса сегмента).
Ограничение размера сегментов и контроль за попытками адресации памяти вне пределов сегментов сильно повышает надёжность системы. Программа не может разрушить чужие сегменты, в частности, сегменты операционной системы. Подробнее о защите мы расскажем в следующем разделе.
Поле доступа, занимающее в дескрипторе один байт (байт доступа) служит для классификации дескрипторов. На рис. 9 приведены форматы поля доступа для трёх типов дескрипторов - дескрипторов сегментов кода, сегментов данных и системных.
Рис. 9. Форматы поля доступа дескриптора.
Поле доступа дескриптора сегментов кода содержит битовое поле R, называемое битом разрешения чтения сегмента. Если этот бит установлен в 1, программа может считывать содержимое сегмента кода. В противном случае процессор может только выполнять этот код.
Программа не может модифицировать сегмент кода. Это означает невозможность создания самомодифицирующихся программ для защищённого режима (распространённая, но порочная практика среди программистов, создающих программы для MS-DOS). Впрочем, есть обходной путь. Для сегмента кода можно создать ещё один, алиасный дескриптор, в котором этот сегмент отмечен как сегмент данных. Если для этого сегмента будет разрешена запись, ничто, кроме здравого смысла, не помешает вам модифицировать код программы во время её выполнения.
Бит C называется битом подчинения, он будет рассмотрен в следующем разделе.
Биты P и A предназначены для организации виртуальной памяти. Их назначение будет описано в разделе, посвящённом виртуальной памяти. Сейчас отметим, что бит P называется битом присутствия сегмента в памяти. Для тех сегментов, которые находятся в физической памяти (мы будем иметь дело в основном с такими сегментами) этот бит должен быть установлен в 1.
Любая попытка программы обратиться к сегменту памяти, в дескрипторе которого бит P установлен в 0, приведёт к прерыванию.
Бит A называется битом обращения к сегменту и для всех наших программ должен быть установлен в 0.
Поле доступа дескриптора сегмента данных имеет битовые поля W и D.
Поле W называется битом разрешения записи в сегмент. Если этот бит установлен в 1, наряду с чтением возможна и запись в данный сегмент. В противном случае при попытке чтения выполнение программы будет прервано.
Поле D задаёт направление расширения сегмента. Обычный сегмент данных расширяется в область старших адресов (расширение вверх). Если же в сегменте расположен стек, расширение происходит в обратном направлении - в область младших адресов (расширение вниз). Для сегментов, в которых организуются стеки, необходимо устанавливать поле D равным 1.
Дескрипторы системных сегментов содержат поле TYPE, определяющее тип системного сегмента. В таблице 1 приведены возможные для этого поля значения.
Таблица 1. Типы системных сегментов.
Поле TYPE | Тип сегмента |
0 | Запрещённое значение |
1 | Доступный TSS для процессора i80286 |
2 | Сегмент LDT |
3 | Занятый TSS для процессора i80286 |
4 | Вентиль вызова для процессора i80286 |
5 | Вентиль задачи для процессоров i80286 и i80386 |
6 | Вентиль прерывания для процессора i80286 |
7 | Вентиль исключения для процессора i80286 |
8 | Запрещённое значение |
9 | Доступный TSS для процессора i80386 |
A | Зарезервировано |
B | Занятый TSS для процессора i80386 |
C | Вентиль вызова для процессора i80386 |
D | Зарезервировано |
E | Вентиль прерывания для процессора i80386 |
F | Вентиль ловушки для процессора i80386 |
Итак, перед переводом процессора в защищённый режим нам необходимо сформировать в памяти таблицу GDT и загрузить в регистр GTDR её адрес и предел при помощи команды LGDT.
В терминах языка ассемблера структура дескриптора может быть описана следующим образом:
STRUC desc_struc limit dw 0 ; предел сегмента base_lo dw 0 ; младшее слово 24-битового ; физического адреса сегмента base_hi db 0 ; старший байт 24-битового ; физического адреса сегмента access db 0 ; поле доступа reserved dw 0 ; зарезервировано, для сегментов ; процессора i80286 должно быть ; равно нулю ENDS desc_struc Тогда мы можем определить таблицу GDT как набор дескрипторов со структурой desc_struc:
GDT_BEG = $ ; отмечаем начало GDT LABEL gdtadr WORD gdt_0 desc_struc <0,0,0,0,0> ; первый элемент не используется gdt_gdt desc_struc <GDT_SIZE-1 ,,,DATA_ACC, 0> gdt_ds desc_struc <DSEG_SIZE-1 ,,,DATA_ACC,0> gdt_cs desc_struc <CSEG_SIZE-1 ,,,CODE_ACC,0> gdt_ss desc_struc <STACK_SIZE-1,,,DATA_ACC,0> GDT_SIZE = ($ - GDT_BEG) ; вычисляем размер GDT В этом примере самый первый дескриптор инициализируется нулями. Так делается всегда. Самый первый дескриптор в таблицах GDT и LDT никогда не используется. Программа может загрузить в сегментный регистр селектор, соответствующий первому дескриптору (поле индекса в таком селекторе равно нулю), однако при попытке использовать такой селектор произойдёт прерывание работы программы. Селектор с нулевым полем индекса (пустой селектор) загружается операционной системой в неинициализированные сегментные регистры перед передачей управления запущенной программе.
Второй дескриптор описывает саму таблицу GDT, в поле предела стоит значение GDT_SIZE-1. Это предел таблицы GDT. В поле доступа стоит значение, соответствующее сегменту данных.
Следующие три дескриптора описывают сегменты, адресуемые регистрами ds, cs и ss соответственно (сегменты данных, кода и стека). В них заполнены поля предела и доступа. Эти поля могут быть определены, например, следующим образом:
CODE_ACC equ 10011000b DATA_ACC equ 10010000b В нашем примере мы заполнили не все поля дескрипторов в таблице GDT. Остались незаполненными поля base_lo и base_hi, т.е. физический адрес сегмента данных.
Физический адрес сегмента данных должен быть вычислен в реальном режиме на основании значений сегментного адреса и смещения, т.е. на основании двух компонент логического адреса реального режима. Можно предложить следующую процедуру для вычисления физического адреса (на примере вычисления физического адреса таблицы GDT) и записи вычисленного адреса в дескриптор:
; Загружаем в ax адрес сегмента данных DGROUP mov ax,DGROUP ; Формируем в dl:ax физический адрес, соответствующий ; сегментному адресу DGROUP mov dl,ah shr dl,4 shl ax,4 ; Складываем со смещением add ax,OFFSET gdtadr adc dl,0 ; Записываем физический адрес GDT в элемент GDT, ; описывающий саму GDT mov bx,OFFSET gdt_gdt mov [(desc_struc bx).base_l],ax mov [(desc_struc bx).base_h],dl Аналогично заполняются и другие элементы таблицы GDT.
Так как дескриптор с адресом gdt_gdt описывает саму таблицу GDT (и формат этого дескриптора подходит для команды LGDT), его можно использовать для загрузки регистра GDTR:
lgdt [QWORD gdt_gdt] Если вы создаёте программу на языке Си, глобальная таблица дескрипторов GDT может быть определена с помощью типа descriptor следующим образом:
descriptor gdt[5]; В этом примере создаётся таблица GDT, содержащая пять дескрипторов. Тип descriptor определяется так:
typedef struct descriptor { word limit; word base_lo; unsigned char base_hi; unsigned char access; unsigned reserved; } descriptor; Инициализацию дескрипторов в таблице GDT можно выполнить, например, с помощью следующей функции:
void init_gdt_descriptor(descriptor *descr, // указатель // на инициализируемый // дескриптор unsigned long base, // базовый адрес сегмента word limit, // предел сегмента unsigned char acc_byte) // поле доступа { descr->base_lo = (word)base; descr->base_hi = (unsigned char)(base >> 16); descr->access = acc_byte; descr->limit = limit; descr->reserved = 0; } Приведём пример использования этой функции для записи в третий по счёту элемент GDT информации о сегменте данных с сегментным адресом _DS и пределом 0xffff:
init_gdt_descriptor(&gdt[2], MK_LIN_ADDR(_DS, 0), 0xffffL, TYPE_DATA_DESCR | SEG_PRESENT_BIT | SEG_WRITABLE); Преобразовать логический адрес реального режима (сегмент:смещение) в физический адрес можно с помощью следующей макрокоманды:
#define MK_LIN_ADDR(seg,off) (((unsigned long)(seg))<<4)+(word)(off) Для формирования поля доступа в нашем примере используются такие определения:
#define TYPE_CODE_DESCR 0x18 #define TYPE_DATA_DESCR 0x10 #define SEG_WRITABLE 0x02 #define SEG_READABLE 0x02 #define SEG_PRESENT_BIT 0x80 К сожалению, встроенный в Borland C 3.0 Inline-ассемблер не позволяет использовать команду LGDT в программе, составленной на языке Си. Аналогичное ограничение имеется и в Microsoft Quick C. Поэтому для загрузки этого и некоторых других регистров приходится использовать отдельные модули, составленные полностью на языке ассемблера.
В отличие от регистра GDTR, регистр LDTR имеет только 16 разрядов. Он содержит не адрес и размер таблицы LDT, а селектор дескриптора, описывающего таблицу LDT. Это системный дескриптор, который должен находиться в таблице GDT и иметь в поле TYPE значение 2.
Дескриптор сегмента LDT содержит базовый адрес и предел таблицы LDT.
Мы уже говорили, что простейшие системы защищённого режима могут не использовать таблицу LDT вовсе. Все приведённые в этой книге примеры программ пользуются только GDT. Практическая польза от применения таблиц LDT появляется только в мультизадачных системах. В этом случае назначение каждой задаче собственной LDT позволяет полностью изолировать адресные пространства отдельных задач. Но об этом мы будем говорить в разделе книги, посвящённом мультизадачности.
Подведём итоги.
- Вы узнали, что в защищённом режиме применена новая схема преобразования логического адреса в физический, сильно отличающаяся от используемой в реальном режиме. Эта схема даёт, в частности, возможность адресовать непосредственно до 16 мегабайт физической памяти.
- Для преобразования адреса процессор i80286 использует таблицы дескрипторов, в которых хранятся базовые адреса сегментов, размеры сегментов и другая информация.
- Одновременно процессор может использовать две таблицы дескрипторов - локальную (LDT) и глобальную (GDT). Используемая таблица определяется полем TI селектора.
- Расположение и размер таблицы GDT должны быть загружены в специальный регистр процессора - GDTR. Это можно сделать командой LGDT.
- В таблице GDT могут находиться дескрипторы сегментов кода, данных и системные дескрипторы. В частности, там может находиться дескриптор, описывающий таблицу локальных дескрипторов LDT (если эта таблица используется).
- Перед переключением в защищённый режим программа должна подготовить таблицу GDT и загрузить её адрес в регистр GDTR.