Изучаем архитектуру Intel x86-64 при помощи ассемблера (Часть 3 — Переход в защищенный режим)

В предыдущих заметках мы запускали программу HelloWorld на голом железе. Непосредственно после старта процессор x86 находится в т. н. реальном режиме работы процессора (real mode). Этот режим имеет следующие особенности:

  • Режим 16-разрядный, т. е. длина машинного слова в нем равна 16 битам.
  • 20-разрядный физический адрес памяти, что позволяет адресовать не более 1 мегабайта оперативной памяти.
  • Сегментная адресация памяти: физический адрес = линейный адрес = segreg * 16 + offset, где физический адрес — это то число, которое выдается процессором на шину адреса, segreg — содержимое 16-разрядного сегментного регистра, offset — т. н. смещение относительно начала сегмента.
  • Отсутствует система уровней привилегий, имеющаяся в защищенном режиме.
  • Отсутствует поддержка виртуальной памяти, имеющаяся в защищенном режиме.

Реальный режим называется еще «режимом реальных адресов» — потому, что в нем физический адрес равен линейному. Подробнее читайте в книге Аблязова «Программирование на ассемблере на платформе х86-64» в разделе 1.1.5. «Память».
Современные операционные системы работают в т. н. защищенном режиме работы процессора (protected mode) либо, если ОС 64-разрядная — в т. н. длинном режиме (long mode или IA-32e). Особенности защищенного режима:

  • Длина машинного слова составляет 32 бита.
  • Разрядность шины адреса тоже 32 бита, что позволяет адресовать до 4 гигабайт памяти.
  • Поддерживается система уровней привилегий, что необходимо, чтобы отделить ядро операционной системы от программ пользователя.
  • Поддерживается виртуальная память, что позволяет разделить адресные пространства различных программ и создать для них иллюзию того, что вся оперативная память находится в их распоряжении.

Так что нам пора переключаться в защищенный режим. План действий таков:

  • Реальный режим

    1. Настройка сегментных регистров.
    2. Открывание вентиля A20.
    3. Запрет всех прерываний.
    4. Загрузка регистра GDTR.
    5. Переключение в защищенный режим.
  • Защищенный режим

    1. Настройка сегментных регистров.
    2. Копирование текстовой строки «Hello World!» в видеопамять.

Как обычно, сначала — код, потом — объяснения.

; HelloWorld protected mode

; ============================================ REAL MODE =======================================================
use16               ; generate 16-bit code
org 7C00h           ; the code starts at 0x7C00 memory address

start:
    jmp far dword 0x0000:entr ; makes CS=0, IP=0x7c00
entr:
    xor  ax, ax     ; ax = 0
    mov  ds, ax     ; setup data segment ds=ax=0
    cli             ; when we set up stack we need disable interrupts because stack is involved in interrupts handling
    mov  ss, ax     ; setup stack segment ss=ax=0
    mov  sp, 0x7C00 ; stack will grow starting from 0x7C00 memory address
    sti             ; enable interrupts
   
    ; Clear screen
    mov ax, 0x0003  ; ah=0 means clear screen and setup video mode, al=3 means text mode, 80x25 screen, CGA/EGA adapter, etc.
    int 0x10        ; call BIOS standard video service function
   
    ; Open gate A20 through the System Control Port A
    in  al, 0x92    ; read the content of port 0x92 (System Control Port A) into al
    or  al, 0x02    ; set 1st bit in al to 1
    out 0x92, al    ; write al into port 0x92
   
    ; Disable maskable interrupts
    cli ; clear flag IF in EFLAGS register
   
    ; Disable non-maskable interrupts
    in  al, 0x70
    or  al, 0x80
    out 0x70, al
   
    lgdt [gdtr]     ; load the address and size of Global Descriptor Table (GDT) into GDTR register
   
    ; Switch to protected mode
    mov  eax, cr0   ; read the content of register cr0 (Machine Status Word - MSW) into eax
    or   al,  0x01  ; set 0th bit to 1 (0th bit of cr0 is called PE - Protection Enable)
    mov  cr0, eax   ; write eax to cr0

    ; Load protected mode entry point into CS:EIP
    jmp far dword 0000000000001000b:pm_entry ; 0000000000001000b is a segment selector which is loaded into CS register
    ; Segment selector's format:
    ;  [0:1]  RPL              = 00b            - requested privilege level = 0 (most privileged)
    ;      2  TI               = 0              - chooses descriptor table; 0 means Global Descriptor Table
    ; [3:15]  Descriptor Index = 0000000000001b - index of descriptor inside the descriptor table = 1

; ========================================== PROTECTED MODE ====================================================
use32               ; generate 32-bit code

; Protected mode entry point
pm_entry:
    ; Initialize segment registers (except CS which is already litialized)
    mov ax, 0000000000010000b ; segment selector: RPL = 0, TI = 0, Descriptor Index = 2
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    mov edi, 0xB8000            ; 0xB8000 is the beginning of video-memory in 0x3 video-mode
    mov esi, message            ; the message which is going to be printed on the screen
    cld                         ; clear direction flag (DF is a flag used for string operations)

; Message-printing loop
.loop:                          
    lodsb                       ; load to al the next charachter located at [esi] address in memory (si is incremented automatically because the direction flag DF = 0)
    test al, al                 ; test al against zero
    jz .exit                    ; exit if al is zero
    stosb                       ; otherwise move al to memory at address [edi]
    mov al, 7                   ; 7 is the so-called attribute of the symbol
    stosb                       ; move al to memory at address [edi]
    jmp .loop

.exit:
    cli                         ; disable interrupts before halting the processor
    hlt                         ; halt the processor

message db 'Hello World!', 0

; ========================================= GLOBAL DESCRIPTOR TABLE ============================================
align 8 ; we align global descriptor table to the 8-bytes boundary (for the sake of processor's performance)
gdt:
    ; Segment descriptor format
    ; BITS  | SIZE | FIELD
    ; ------+------+------
    ; 0-15  |  16  | Limit[0:15]
    ; 16-39 |  24  | Base[0:23]
    ; 40-47 |   8  | P DPL[0:1] S Type[0:3]
    ; 48-55 |   8  | G D/B L AVL Limit[16:19]
    ; 56-63 |   8  | Base[24:31]

NULL_SEG_DESCRIPTOR db 8 dup(0)
   
CODE_SEG_DESCRIPTOR:
    dw 0xFFFF           ; Limit[0:15]
    db 0x00, 0x00, 0x00 ; Base[0:23]
    db 10011010b        ; P DPL[0:1] S Type[0:3]
    db 11001111b        ; G D/B L AVL Limit[16:19]
    db 0x00             ; Base[24:31]

    ; Detailed description of the segment descriptor:
    ; Base  = 0x00000000 - segment base address = 0
    ; Limit = 0xFFFFF    - segment size = 2^20
    ; P     = 1          - presence: segment is present in physical memory
    ; DPL   = 00b        - descriptor privilege level = 0 (most privileged)
    ; S     = 1          - system: segment is not a system segment
    ; Type  = 1010b      - code segment (1), C=0 R=1 A=0 execution and reading allowed
    ; G     = 1          - granularity: the size of the segment is measured in 4 kilobyte pages, i. e. it's equal to 2^20*4 KiB = 4 GiB
    ; D/B   = 1          - default size: operands and addresses are 32-bit wide
    ; L     = 0          - 64-bit code segment: in protected mode this bit is always zero
    ; AVL   = 0          - available: it's up to the programmer how to use this bit

DATA_SEG_DESCRIPTOR:
    dw 0xFFFF           ; Limit[0:15]
    db 0x00, 0x00, 0x00 ; Base[0:23]
    db 10010010b        ; P DPL[0:1] S Type[0:3]
    db 11001111b        ; G D/B L AVL Limit[16:19]
    db 0x00             ; Base[24:31]

    ; Detailed description of the segment descriptor:
    ; Base  = 0x00000000 - segment base address = 0
    ; Limit = 0xFFFFF    - segment size = 2^20
    ; P     = 1          - presence: segment is present in physical memory
    ; DPL   = 00b        - descriptor privilege level = 0 (most privileged)
    ; S     = 1          - system: segment is not a system segment
    ; Type  = 0010b      - data segment (0), E=0 W=1 A=0 reading and writing are allowed, expand-up data segment (offset ranges from 0 to Limit)
    ; G     = 1          - granularity: the size of the segment is measured in 4 kilobyte pages, i. e. it's equal to 2^20*4 KiB = 4 GiB
    ; D/B   = 1          - default size: stack pointer is 32-bit wide (concerns stack segment) and the upper bound of the segment is 4 GiB (concerns data segment)
    ; L     = 0          - 64-bit code segment: in protected mode this bit is always zero
    ; AVL   = 0          - available: it's up to the programmer how to use this bit

gdt_size equ $ - gdt

; data to be loaded to GDTR register
gdtr:
    dw gdt_size - 1         ; 16-bit limit of the global descriptor table
    dd gdt                  ; 32-bit base address of the global descriptor table

; ======================================== BOOT SECTOR SIGNATURE ===============================================
finish:
    ; The size of a disk sector is 512 bytes. Boot sector signature occupies the two last bytes.
    ; The gap between the end of the source code and the boot sector signature is filled with zeroes.
    times 510-finish+start db 0
    db 55h, 0AAh    ; boot sector signature

Порты ввода-вывода

В этой программе мы впервые сталкиваемся с командами in и out, поэтому проясним их смысл. Персональный компьютер состоит из нескольких частей: процессор, оперативная память и периферийные устройства. Эти устройства соединяются друг с другом шинами. Шин в компьютере много: шина памяти, AGP, PCI, PCI-Express, USB, SATA и пр. Разные шины имеют разные протоколы обмена данными. Но для процессора обмен данными со всеми устройствами выглядит одинаково, независимо от того, к какой шине подключено устройство. Это становится возможным благодаря тому факту, что процессор общается с устройствами не непосредственно, а через посредников, которыми являются специальные микросхемы — так называемые мосты (северный и южный). Непосредственно процессор соединен только с северным мостом — шиной, которая называется Front-Side Bus (FSB). Северный мост соединяется с высокоскоростными устройствами — оперативной памятью и видеокартой. Также северный мост соединен с южным мостом, который в свою очередь соединяется со сравнительно медленными всеми остальными периферийными устройствами. Но, как я уже говорил, для процессора все выглядит прозрачно — как будто все устройства подсоединены к шине FSB. Обмен данным по шине FSB выглядит как работа с памятью: процессор выдает на шину адрес и данные для записи; либо выдает адрес для чтения и получает данные от устройства. Периферийными устройствами управляют микросхемы, которые называются контроллерами — именно с ними общается процессор. У этих контроллеров есть регистры (память), а у регистров есть адреса (каждый контроллер периферийного устройства декодирует адрес на шине, т. е. определяет, обращается ли процессор к его регистрам). Процессор управляет периферийными устройствами путем записи чисел в их регистры и чтения чисел из регистров. Совокупность адресов, которые процессор может выдавать на шину, называется адресным пространством. И таких адресных пространств у процессоров Intel — два: адресное пространство памяти и адресное пространство ввода-вывода. То, к каком адресному пространству из двух обращается в данный момент процессор, определяют некие управляющие линии (или одна линия) шины. Для обращения к адресному пространству памяти используются команды типа mov, а для обращения к адресному пространству ввода-вывода — команды in и out. Регистры контроллеров периферийных устройств могут находиться как в пространстве ввода-вывода, так и в пространстве памяти. В последнем случае это называется memory-mapped input-output. Так что, если вы выполняете команду mov, это еще не значит, что вы обращаетесь к оперативной памяти. Пространство ввода-вывода состоит из 64 килобайт. Отдельные байты (а также слова и двойные слова) из этого пространства называются портами ввода-вывода. Команды in и out перемещают данные между портом ввода-вывода и регистром AL (или AX или EAX), команда in считывает число из порта в регистр, а команда out — записывает число из регистра в порт. Операндом команд in и out является адрес порта. В защищенном режиме не всем программам разрешено выполнять команды in и out, а только тем, у которых уровень привилегий не ниже установленного в поле IOPL (input-output privilege level) в регистре EFLAGS. Подробнее о вводе-выводе читайте Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 1: Basic Architecture, Chapter 14 — Input/Output.

Ветиль A20

Подробно о проблеме открывания вентиля A20 вы можете прочесть в Википедии. Если вкратце, то история такова. Процессоры Intel 8086, 8088, 80186, выпускавшиеся в конце 70-х — начале 80-х годов 20-го века, имели 20-разрядную шину адреса (т. н. существовали линии адреса A0-A19), что позволяло имя адресовать до 1 MiB памяти. При этом использовалась сегментная адресация памяти, т. е. физический адрес вычислялся как сумма seg*16 + offset, где seg — 16-разрядный сегментный регистр, а offset — 16-разрядное смещение. Такой способ адресации теоретически позволял залезть в адресное пространство выше, чем 1 MiB (например, если seg=0xFFFF и offset=0xFFFF, то 0xFFFF*0x10 + 0xFFFF = 0x10FFEF), однако отсутствие 20-й линии адреса приводило к так называемому свертыванию (wrap around) адреса (например, если seg=0xFFFF и offset=0xFFFF, то физический адрес = 0xFFFF*0x10 + 0xFFFF - 0x100000 = 0xFFEF). Некоторые программы в ОС DOS пользовались этим явлением в своих целях. Но в 1982 году вышел процессор 80286 с 24-разрядной шиной адреса с возможностью адресации до 16 MiB памяти в защищенном режиме (да, защищенный режим появился именно тогда). И в этом процессоре была допущена ошибка: в реальном режиме он не обнулял принудительно линию адреса A20, что привело к устранению эффекта свертывания адреса, которым пользовались ранее некоторые программы в ОС DOS. Intel решили проблему путем ввода на материнскую плату логического вентиля под названием «A20», который мог либо разрешить, либо запретить (занулить) линию адреса A20. Программирование вентиля изначально осуществлялось через контроллер клавиатуры Intel 8042, сейчас существуют и другие способы, например, System Control Port A.

Запрет всех прерываний

Когда компьютер стартует, то он стартует в реальном режиме работы процессора. При этом в определенную область адресного пространства процессора проецируется содержимое BIOS. BIOS в частности содержит обработчики прерываний. Адреса этих обработчиков называются «векторами прерываний» и находятся в самом начале оперативной памяти компьютера. У каждого прерывания есть номер. Когда возникает прерывание, то процессор останавливает выполнение текущей программы и узнает контроллера прерываний номер произошедшего прерывания. По этому номеру процессор находит вектор прерывания, т. е. адрес обработчика прерывания. Процессор выполняет этот обработчик, а затем возвращается к прерванной программе. Таким образом, сразу же при старте компьютера в реальном режиме, он в состоянии обрабатывать прерывания. В защищенном режиме механизм обработки прерываний реализуется иначе. Программист должен определенным образом подготовить компьютер. Если он этого не сделает, то при возникновении прерывания, возникнет «аварийная ситуация» (triple fault), и процессор перезагрузится. Поэтому перед тем, как переключиться в защищенный режим мы должны все прерывания запретить.
Прерывания в процессорах Intel x86-64 бывают маскируемые и немаскируемые (non-maskable interrupts — NMI). Маскируемые прерывания можно запретить либо разрешить путем сброса либо установки флага IF в регистре флагов EFLAGS. Немаскируемые прерывания можно разрешить либо запретить при помощи обращения к микросхеме CMOS, которое производится через порт 0x70 (подробности см. в статье OSDev.org — CMOS).

Переход в защищенный режим

Чтобы перейти в защищенный режим, формально, надо всего лишь установить в единицу младший бит регистра CR0. Этот бит называется PE — Protection Enable. Регистр CR0 хранит так называемое слово состояния Machine Status Word (MSW). Но всё не так просто. В защищенном режиме процессор использует совсем другой способ адресации памяти команд и данных. И чтобы этот способ нормально работал, от программиста требуется произвести некоторую предварительную работу.
Режим адресации, используемый в защищенном режиме может показаться замысловатым. Защищенный режим призван защитить операционную систему от прикладных программ (т. н. программ пользователя). Так, чтобы ошибки или даже злой умысел, имеющиеся в этих программах не приводили к краху операционной системы. То есть надо сделать так, чтобы пользовательские программы…

  • не могли производить чтение из или запись в область оперативной памяти, где ОС хранит свои данные;
  • не могли производить чтение из или запись в область оперативной памяти, где находится машинный код ОС;
  • могли выполнять лишь специальную ограниченную часть машинного кода ОС, которая предоставляет пользовательским программам т. н. сервисы (см. системные вызовы).

Чтобы эту задачу решить надо во-первых как-то идентифицировать машинный код операционной системы и машинный код пользовательских программ. Для этого введена концепция уровней привилегий (privelege levels), они также называются кольцами (rings). В процессорах Intel x86-64 колец четыре, хотя операционные системы Windows и Linux например используют только два из них. Уровень 0 — самый привилегированный, им обладает операционная система. Уровень 3 — наименее привилегированный, им довольствуются пользовательские программы.

О каких же таких привилегиях идет речь? На уровне 0 код может выполнять любые машинные инструкции, на уровне 3 — не все, в частности, команды in и out на уровне 3 недоступны — попытка их выполнения пользовательской программой приведет к исключению General Protection Fault и передаст управление операционной системе.

А еще пользовательскому коду не разрешается обращаться (читать/писать/выполнять) к некоторым областям оперативной памяти. Как это работает? Дело в том, что в защищенном режиме различным областям памяти назначаются различные атрибуты, я бы сказал, «права доступа». Например к области памяти X может обращаться только операционная система, а к области памяти Y могут обращаться и ОС, и пользовательские программы. Или: область X — это область кода: можно производить ее чтение и выполнение расположенных в ней машинных команд, но нельзя производить запись. А область Y — это область данных: из нее можно читать, в нее можно писать, но нельзя интерпретировать ее содержимое как машинные команды и выполнять их. Где же находятся эти атрибуты, которые описывают различные области памяти? Ответ: в определенном месте оперативной памяти расположена так называемая глобальная таблица дескрипторов (Global Descriptor Table — GDT). Записи в этой таблице называются дескрипторами, они имеют размер 8 байт. Те дескрипторы, которые описывают области памяти, называются дескрипторами сегментов. Дескриптор сегмента памяти указывает, что можно делать с этим сегментом и кто имеет право это делать. Вот эту таблицу GDT и должен подготовить программист, и перед переключением в защищенный режим поместить адрес этой таблицы и ее размер в регистр GDTR (это делает команда lgdt). Первым дескриптором в таблице GDT непременно должен быть т. н. нулевой дескриптор (8-байтовое целое число равное нулю) — он выполняет ту же функцию, что и нулевой указатель (null pointer) в обычных программах. Формат дескриптора сегмента приведен ниже:

31 24 23 22 21 20 19 16 15 14 13 12 11 10 9 8 7 0
Base Address[31:24] G D/B L AVL Segment Limit[19:16] P DPL S Type Base Address[23:16] 4
Base Address[15:0] Segment Limit[15:0] 0

Подробное описание формата сегментных дескрипторов читайте в Intel 64 and IA-32 Architectures Software Developer’s Manual Vol. 3A раздел 3.4.5 Segment Descriptors. Для нас же сейчас важны пожалуй только следующие поля:

Base Address Адрес начала сегмента в оперативной памяти.
Segment Limit Размер сегмента. Измеряется либо в байтах (если G=0) либо в страницах по 4 KiB (если G=1).
DPL Descriptor Privilege Level. Уровень привилегий дескриптора: 0 — наиболее привилегированный, 3 — наименее привилегированный.
Type Тип сегмента. Старший бит этого поля определяет, является ли он сегментом данных (0) или кода (1). Остальные биты интерпретируются по разному в зависимости от значения этого старшего бита. Загляните в раздел 3.4.5.1 Code- and Data-Segment Descriptor Types руководства Intel 64 and IA-32 Architectures Software Developer’s Manual, чтобы узнать подробности.

В конце исходного кода нашей программы вы видите объявление двух дескрипторов сегментов (не считая нулевого дескриптора в начале) — это и есть глобальная таблица дескрипторов — GDT. Еще ниже вы видите метку gdtr, которая указывает на 48-разрядное значение, которое представляет собой базовый адрес размер таблицы GDT в байтах. Это 48-разрядное значение должно быть загружено в регистр GDTR перед переходом в защищенный режим, что мы и делаем командой lgdt [gdtr].

Как же процессор определяет уровень привилегий того кода, который он в данный момент выполняет? Всё просто: уровень привилегий закодирован двумя битами в регистре CS (эти два бита составляют поле, которое называется RPL — Requested Privilege Level). Вообще, все сегментные регистры содержат так называемые селекторы сегментов. Формат селектора сегмента представлен ниже. Селектор сегмента содержит индекс дескриптора сегмента в таблице дескрипторов (есть две таблицы: глобальная GDT и локальная LDT; если бит TI селектора равен 0, то дескриптор находится в GDT, в противном случае — в LDT), т. е. селектор «выбирает» дескриптор из таблицы.

15 3 2 1 0
Index TI RPL

В регистр CS можно загрузить непосредственное значение селектора командой jmp far selector:offset либо call far selector:offset, где selector — непосредственное значение селектора, а offset — смещение внутри сегмента. В остальные сегментные регистры можно загрузить значение из регистра общего назначения командой mov seg, reg16, где seg — сегментный регистр, а reg16 — 16-разрядный регистр общего назначения. Вот мы и загружаем в регистр CS селектор сегмента кода, в котором Index=1 (в таблице GDT сегмент кода у нас идет 1-м), TI=0 (дескриптор сегмента кода находится в таблице GDT, а не в LDT, о которой позже) и RPL=0 (наивысший запрашиваемый уровень привилегий:

jmp far dword 0000000000001000b:pm_entry

pm_entry — это смещение точки входа в защищенный режим, которое задается относительно базового адреса сегмента кода (а он у нас равен 0). В защищенном режиме линейный адрес памяти формируется так:

линейный адрес = базовый адрес сегмента + смещение

Причем в нашей программе мы не используем страничную адресацию (о которой — позднее), поэтому у нас физический адрес равен линейному.

А может ли пользовательский код сам повысить свой уровень привилегий, изменив поле RPL в регистре CS? Конечно нет: содержимое регистра CS могут изменять только команды переходов (jmp, call), но процессор разрешит переход с повышением уровня привилегий только на машинный код, принадлежащий операционной системе, а никак не пользовательской программе. Операционная система может предоставить пользовательским программам ряд своих «сервисов» — функций, точки входа которых описываются так называемыми дескрипторами шлюзов. Вот на эти точки входа и разрешено прыгать пользовательскому коду. При этом уровень привилегий кода повышается, но это будет уже не пользовательский код, а код операционной системы.

А операционная система может понизить свой уровень привилегий? ОС может передавать управление пользовательскому коду. При этом текущий уровень привилегий понижается. Большего пока сказать не могу, потому как не знаю.

Вы уже поняли, что защита памяти в защищенном режиме осуществляется на аппаратном уровне, и преодолеть ее у пользовательских программ нет никакой возможности. Но нужно не только защищать ОС от пользовательских программ, но и пользовательские программы друг от друга: программа X не должна иметь возможность производить запись или чтение областей оперативной памяти программы Y. Эта задача решается при помощи концепции виртуальной памяти, но об этом — в следующих заметках.

Мы в защищенном режиме

Оказавшись в защищенном режиме (после метки pm_entry), мы должны первым делом проинициализировать все сегментные регистры кроме CS, который уже проинициализирован. Во все сегментные регистры (кроме CS) мы загружаем селектор сегмента данных. Таким образом сегмент стека и сегмент данных у нас совпадают.
И осталось вывести на экран строку «Hello World!». Но как это сделать? Оказывается, память видеоадаптера проецируется на адресное пространство процессора начиная с адреса 0xB8000. Видеоадаптер находится в текстовом режиме, поэтому нам надо просто записать в его память строку «Hello World!» символ за символом. Впрочем, рядом с каждым символом должен быть еще и так называемый атрибут, который определяет цвет фона и цвет символа (он у нас равен 7, что означает «белый символ на черном фоне»).

Макрос для определения дескриптора сегмента

Объявление дескриптора сегмента довольно громоздкое и в нем легко допустить ошибку из-за того, что поля разбросаны по дескриптору. Можно написать макрос, который возьмет на себя труд вычисления значений отдельных полей дескриптора (о том, как писать макросы в FASM, см. flat assembler g User Manual):

; The following macro defines a code or a data segment descriptor.
; The following assumtions take place:
; P=1 (the segment is present in physical memory)
; S=1 (the segment is not a system segment)
; G=1 (the Limit of the segment is measured in 4-kiB pages)
; D/B=1 L=0 (the segment is a 32-bit segment)
; AVL=0
macro SEGMENT_DESCRIPTOR _Base, _Limit, _DPL, _Type
{
    dw _Limit and 0xFFFF                   ; Limit[0:15]
    dw _Base  and 0xFFFF                   ; Base[0:15]
    db (_Base shr 16) and 0xFF             ; Base[15:23]
    db _Type or (_DPL shr 5) or 10010000b  ; P DPL[0:1] S Type[0:3]
    db 11000000b or (_Limit shr 16)        ; G D/B L AVL Limit[16:19]
    db (_Base shr 24)                      ; Base[24:31]
}

; Segment descriptor types (see Intel 64 and IA-32 Architectures Software Developer’s Manual - 3.4.5.1 Code- and Data-Segment Descriptor Types).
; Can be used as the _Type argument of SEGMENT_DESCRIPTOR macro
DATA_READ_ONLY equ 0000b
DATA_READ_WRITE equ 0010b
DATA_READ_ONLY_EXPAND_DOWN equ 0100b
DATA_READ_WRITE_EXPAND_DOWN equ 0110b
CODE_EXECUTE_ONLY equ 1000b
CODE_EXECUTE_READ equ 1010b
CODE_EXECUTE_ONLY_CONFORMING equ 1110b
CODE_EXECUTE_READ_CONFORMING equ 1111b

Тогда вместо вот этого:

CODE_SEG_DESCRIPTOR:
    dw 0xFFFF           ; Limit[0-15]
    db 0x00, 0x00, 0x00 ; Base[0-23]
    db 10011010b        ; P DPL[0:1] S Type[0:3]
    db 11001111b        ; G D/B L AVL Limit[16:19]
    db 0x00             ; Base[24:31]

DATA_SEG_DESCRIPTOR:
    dw 0xFFFF           ; Limit[0:15]
    db 0x00, 0x00, 0x00 ; Base[0:23]
    db 10010010b        ; P DPL[0:1] S Type[0:3]
    db 11001111b        ; G D/B L AVL Limit[16:19]
    db 0x00             ; Base[24:31]

пишем вот это:

CODE_SEG_DESCRIPTOR:
    ;                  Base        Limit    DPL  Type
    SEGMENT_DESCRIPTOR 0x00000000, 0xFFFFF, 00b, CODE_EXECUTE_READ

DATA_SEG_DESCRIPTOR:
    ;                  Base        Limit    DPL  Type
    SEGMENT_DESCRIPTOR 0x00000000, 0xFFFFF, 00b, DATA_READ_WRITE

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

P.S. В какой именно момент процессор оказывается в защищенном режиме?

Есть небольшой вопрос, который меня беспокоит: оказывается ли процессор в защищенном режиме непосредственно после установки бита Protection Enable в регистре CR0? Если да, то любая дальнейшая инструкция должна приводить к аварии (triple fault). Ведь регистр CS в нашей программе содержит число 0, т. е. если его интерпретировать как селектор, то этот селектор будет выбирать нулевой дескриптор, а обращение к нулевому дескриптору вызовет исключение. Но аварии не происходит — ведь команда jmp far 0000000000001000b:pm_entry успешно выполняется. Можно найти хотя бы частичный ответ в статье Jean Gareau — Embedded X86 Programming: Protected Mode и в разделе 9.9.1. Switching to Protected Mode руководства Intel 64 and IA-32 Architectures Software Developer’s Manual. В частности, в статье Jean Gareau сказано:

As soon as the bit is set in CR0, protected mode kicks in and the CPU starts executing 16-bit instructions, but in protected mode (segment registers become indexes into table). The content of all segment registers is unknown at this point. However, it is guaranteed that they can still be used to access subsequent instructions or data.

Там же сказано, что процессор после установки бита PE в регистре CR0 находится в 16-разрядном защищенном режиме, и чтобы перевести его в 32-разрядный режим, надо выполнить far jump или far call.
В разделе 9.9.1. руководства Intel сказано:


3. Execute a MOV CR0 instruction that sets the PE flag (and optionally the PG flag) in control register CR0.
4. Immediately following the MOV CR0 instruction, execute a far JMP or far CALL instruction. (This operation is typically a far jump or call to the next instruction in the instruction stream.)

Random failures can occur if other instructions exist between steps 3 and 4 above.

Я делаю следующее предположение: непосредственно после команды MOV CR0 процессор работает в 16-разрядном защищенном режиме. Это значит, что существует дескриптор 16-разрядного сегмента кода (бит D/B=0) и он реально используется (что дает процессору возможность продолжать нормально работать несмотря на неправильное значение в регистре CS), т. е. он загружен в теневой регистр (shadow register или descriptor cache), связанный с сегментным регистром CS. Перезапись теневого регистра происходит только в момент загрузки нового значения в регистр CS, т. е. при выполнении команды jmp far 0000000000001000b:pm_entry.

В следующей заметке напишем загрузчик (bootloader), который будет загружать с дискеты нашу программу.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *