к содержанию

1.2. Адресация памяти в защищённом режиме

Основная трудность, с которой сталкивается программист, изучающий защищённый режим - это сложная схема преобразования адресов. Поэтому мы уделим много внимания тому, как в этом режиме происходит адресация памяти. Для облегчения восприятия мы вначале опустим некоторые детали, отложив на время их полное описание.

Преобразование адресов в защищённом режиме

В защищённом режиме, также как и в реальном, существуют понятия логического и физического адреса. Логический адрес в защищённом режиме (иногда используется термин "виртуальный адрес") состоит из двух 16-разрядных компонент - селектора и смещения. Селектор записывается в те же сегментные регистры, что и сегментный адрес в реальном режиме. Однако преобразование логического адреса в физический выполняется не простым сложением со сдвигом, а при помощи специальных таблиц преобразования адресов.

В первом приближении можно считать, что для процессора i80286 селектор является индексом в таблице, содержащей базовые 24-разрядные физические адреса сегментов. В процессе преобразования логического адреса в физический процессор прибавляет к базовому 24-разрядному адресу 16-разрядное смещение, т.е. вторую компоненту логического адреса.

На рис.4 показана сильно упрощённая схема преобразования логического адреса в физический.

Рис. 4. Упрощённая схема преобразования логического адреса в физический в защищённом режиме.

Такая схема формирования физического адреса позволяет непосредственно адресовать 16 мегабайт памяти с помощью 16-разрядных компонент логического адреса.

Заметьте, что селектор - это не сегментный адрес. Это индекс, с помощью которого процессор извлекает из специальной таблицы 24-разрядный базовый адрес сегмента. В реальном режиме мы имеем дело с сегментным адресом и смещением, а в защищённом - с селектором и смещением.

На самом деле не все 16 бит селектора используются для индексации по таблице базовых адресов. В качестве индекса выступают старшие 13 бит. Два младших бита (бит 0 и бит 1) используются системой защиты памяти, о чём мы подробно поговорим в следующем разделе. Бит 2 позволяет выбирать для преобразования адреса один из двух типов таблиц преобразования адресов.

  • Полный формат селектора показан на рис. 5.
  • Рис. 5. Формат селектора адреса.

    На этом рисунке два младших бита обозначены как RPL (Requested Privilege Level). Это поле является запрошенным программой уровнем привилегий и его мы будем обсуждать позже. Поле TI (Table Indicator) состоит из одного бита. Если этот бит равен нулю, для преобразования адреса используется так называемая глобальная таблица дескрипторов GDT (Global Descriptor Table), в противном случае - локальная таблица дескрипторов LDT (Local Descriptor Table).

    Таблица дескрипторов - это просто таблица преобразования адресов, содержащая базовые 24-разрядные физические адреса сегментов и некоторую другую информацию. То есть каждый элемент таблицы дескрипторов (дескриптор) содержит 24-разрядный базовый адрес сегмента и другую информацию, описывающую сегмент.

    Таблица GDT - единственная в системе. Обычно в ней находятся описания сегментов операционной системы. Таблиц LDT может быть много. Эти таблицы содержат описания сегментов программ, работающих под управлением операционной системы, т.е. отдельных задач. В каждый данный момент времени процессор может использовать только одну таблицу LDT.

    Процессор имеет два регистра, предназначенных для адресации используемых в настоящий момент таблиц GDT и LDT. Регистр GTDR описывает расположение и размер таблицы GDT, а регистр LDTR содержит ссылку на использующуюся в настоящее время таблицу LDT.

    На рис. 6 показана уточнённая схема преобразования адресов в защищённом режиме. Из рисунка видно, что регистры процессора GDTR и LDTR определяют расположение в памяти таблиц GDT и LDT соответственно. Таблицы GDT и LDT содержат дескрипторы, описывающие сегменты памяти. В этих дескрипторах, помимо другой информации (заштрихованная область) содержится 24-разрядный базовый адрес сегмента.

    Старшие 13 битов селектора (индекс) выбирают элемент из таблицы GDT или LDT в зависимости от состояния бита TI селектора.

    Извлечённый из таблицы дескрипторов базовый адрес сегмента складывается процессором для получения 24-разрядного физического адреса.

    Рис. 6. Уточнённая схема преобразования адресов.

    Селектор 0000h адресует самый первый дескриптор в глобальной таблице дескрипторов GDT. Поле RPL для этого дескриптора равно 0, поле TI также равно 0. Селектор 0008h указывает на второй элемент таблицы GDT, а селектор 0014h указывает на третий дескриптор в локальной таблице дескрипторов LDT, т.к. поле TI в нём равно 1.

    Детальное описание схемы преобразования адресов

    Теперь перейдём к более строгому описанию схемы преобразования адресов в защищённом режиме.

    Регистр 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 байт. Он состоит из следующих полей:

    В реальном режиме начало сегмента в памяти определяется сегментным адресом, длина сегмента составляет 64 килобайта. В защищённом режиме можно задавать сегменты меньшего размера (процессоры i80386 и i80486 допускают также существование сегментов с размером, значительно превышающим 64 килобайта). При этом процессор автоматически отслеживает попытки программы обратиться за пределы сегмента, заданные в дескрипторе. При обнаружении такой попытки выполнение программы прерывается.

    Ограничение размера сегментов и контроль за попытками адресации памяти вне пределов сегментов сильно повышает надёжность системы. Программа не может разрушить чужие сегменты, в частности, сегменты операционной системы. Подробнее о защите мы расскажем в следующем разделе.

    Поле доступа, занимающее в дескрипторе один байт (байт доступа) служит для классификации дескрипторов. На рис. 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

    Мы на время отложим детальное описание всех типов системных дескрипторов, так же как и поля DPL, использующееся для организации защиты сегментов, для того чтобы сконцентрировать своё внимание на схеме преобразования адресов в процессоре i80286.

    Итак, перед переводом процессора в защищённый режим нам необходимо сформировать в памяти таблицу 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 позволяет полностью изолировать адресные пространства отдельных задач. Но об этом мы будем говорить в разделе книги, посвящённом мультизадачности.

    Подведём итоги.

    к содержанию
    Hosted by uCoz