Программирование процессоров Intel x86 в защищённом режиме
Обзор защищённого режима

Автор: sergh
The RSDN Group
Опубликовано: 06.04.2004
Версия текста: 1.0

Новая жизнь
Новые мысли
Мысль #1: Многозадачная ОС
Мысль #2: Обратная совместимость
Почему изменения нужны?
Новая архитектура
Блок #1: Многозадачные ОС
Блок #2: Режим полной совместимости
Регистры
Переключение режимов
Под капотом
Алгоритм в первом приближении
Пример
Hello, world! [Protected Mode version]
А был ли мальчик?

Это новая жизнь, шорох новых листьев,
Шёпот весеннего льда.

Борис Гребенщиков

Новая жизнь

Для системного программиста защищённый режим очень, очень сильно отличается от реального.

Несколько обобщая, можно сказать, что полностью поменялась модель процессора, которую должен держать в своей голове программист. Если раньше процессор можно было представлять себе как «продвинутый арифмометр», по какому-то недоумению умеющий обрабатывать прерывания (очевидно, к нему сбоку привинчен специальный маленький блок), то теперь всё, амба. Процессор, работающий в защищённом режиме, выполняет слишком много всяких непонятных вещёй и уже никак не влезает в модель арифмометра. И, хуже того, вы в своей собственной программе уже не можете делать с ним, что хотите. Даже в ответ на простую и понятную запись в COM-порт командой out, процессор может выбросить исключение #GP (General Protection Fault), а уж про перехват прерываний и говорить не хочется…

Как и в любом нормальном детективе, ключевым является вопрос: «Зачем?» Попытаемся на него ответить:

Итак, пожалуйста, оставьте эмоции и запомните основную мысль этого диалога:

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

ПРИМЕЧАНИЕ

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

А теперь попытаемся понять, что же именно пришло в голову разработчикам Intel, чем их не устраивала старая архитектура процессора, и что у них получилось в итоге.

Новые мысли

Урок в грузинской школе.
- Гиви, кто такой ОС?
- Учитель, ОС это такой большой полосатый мух.

Старый анекдот обретает новые грани...

Как уже было сказано, основная мысль – поддержка многозадачных ОС. Чуть более подробно о том, что такое многозадачная ОС, что ей надо, и о чем ещё пришлось побеспокоиться разработчикам.

ПРЕДУПРЕЖДЕНИЕ

Естественно, о том, что приходило в головы разработчиков, и какие задачи ставило перед ними руководство можно судить только по результатам их труда. То есть, грубо говоря, это всё мои домыслы, основанные на косвенных признаках.

К сожалению, даже если принять все «домыслы» за истину и считать, что мы понимаем цели разработчиков, это не поможет вам получить «единую, стройную картину» – система слишком сложна. Но, возможно, вы сможете понять, какие требования предъявлялись к каждой подсистеме процессора, увидеть, как именно эти требования учитывались и, тем самым, вместо «набора необъяснимых фактов» получить «набор объяснимых фактов», что уже гораздо лучше.

Мысль #1: Многозадачная ОС

Основное предназначение многозадачной ОС – позволять одновременное выполнение нескольких пользовательских приложений и не позволять им мешать друг другу. Кроме этого, ОС должна обеспечивать некоторое удобство программирования, иначе она вряд ли будет пользоваться успехом. Разберём по пунктам.

«Позволять одновременное выполнение нескольких пользовательских приложений» это:

«Не позволять им мешать друг другу»:

ПРИМЕЧАНИЕ

Это не паранойя и речь идёт не только и не столько о вирусах и прочих «зловредных» программах. Одна из основных задач изоляции приложений друг от друга – уменьшение влияния программных ошибок на систему. Классический пример: если в ОС оперативная память доступна всем, случайная запись по неверному адресу, произведённая одним приложением, может привести к краху совершенно другого приложения, данные которого оказались испорчены.

Но, очевидно, что сама ОС должна иметь доступ и к портам, и к памяти, и к прерываниям, иначе она просто не сможет работать. Отсюда вытекает:

«Удобство программирования»:

Окончательный список основных требований к ПО, которое, по предположениям разработчиков Intel, будет выполняться на их процессоре (да, ещё раз почти то же самое, но всё вместе и иногда другими словами; повторение – мать учения):

Мысль #2: Обратная совместимость

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

Обратная совместимость в новом режиме

Для прикладных программистов и их программ в новом режиме должны сохраняться почти все старые правила, иначе программистам придётся переучиваться (чего они не любят), а программы придётся переписывать (а этого не любят пользователи, которые за программы платили). А Apple уже выпустила свои Mac-и…

Естественно, подобная совместимость невозможна без поддержки со стороны ОС (например, она должна поддерживать старый формат исполняемых файлов), но процессор должен дать ОС возможность обеспечить такую поддержку. Это значит, что в новом режиме сохраняются, как минимум:

Режим полной совместимости

Даже не смотря на сохранение команд/регистров и т.п., старое системное ПО (и даже часть прикладного) абсолютно неработоспособно в новом режиме. А пользователи очень не скоро согласятся полностью от него отказаться (хотя бы потому, что производители очень нескоро напишут новое). Поэтому:

Как естественное следствие существования двух режимов работы процессора:

Почему изменения нужны?

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

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

ПРЕДУПРЕЖДЕНИЕ

Цифра 100 не является результатом исследований или экспертных оценок, а взята «с потолка».

Проблема в том, что при использовании процессора подобного Intel 8086 существует только один способ реализации всех требуемых механизмов – запускать приложения на «виртуальной машине» и интерпретировать их код. То есть, приложение не выполняется на процессоре напрямую, вместо этого на процессоре выполняется ОС, которая читает из исполняемого файла команды, анализирует их и пытается выполнить. Это замечательный и очень прогрессивный подход (в частности, он реализован в Java Virtual Machine), обладающий одним существенным недостатком: скорость выполнения снижается на порядки.

Поскольку увеличить тактовую частоту процессора сразу в 100 раз довольно трудно, пришлось менять его архитектуру. Естественно, в обязанности процессора не входит реализация всех пунктов, перечисленных выше в разделе «Мысль #1: Многозадачная ОС», но он должен обеспечить ОС поддержку, достаточную для реализации этих пунктов самостоятельно, без лишнего напряжения и без применения виртуальных машин.

Новая архитектура

Эмерсон, с присущей ему изобретательной памятью, приписал Гёте идею о том, 
что архитектура – это застывшая музыка. Это изречение и наша личная
 неудовлетворённость созданиями современной эпохи внушает нам порой мечту об
 архитектуре, которая, подобно музыке, была бы прямым языком страстей, не
 стесненным требованиями, предъявляемыми к жилью или общественным зданиям.

Хроники Бустоса Домека

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

Блок #1: Многозадачные ОС

Именно эта часть называется защищённым режимом (Protected Mode; наконец-то я ввёл термин, который уже столько раз использовал!). Единственное, что стоит сказать об этом блоке в этой главе – он есть. То есть, в архитектуре процессора поддержаны все пункты, перечисленные выше в разделе «Мысль #1: Многозадачная ОС» и реализованы ещё кое-какие детали. Подробностям посвящены все остальные главы.

Как и обещалось в разделе «Обратная совместимость в новом режиме», старые регистры и команды сохранены.

Блок #2: Режим полной совместимости

А это – режим реальной адресации (Real-Address Mode) или просто реальный режим (Real Mode). Он не только существует, но именно в нём начинает свою работу процессор после включения питания, для переключения в защищённый режим необходимо выполнить специальные действия. «Правильное» переключение режимов нетривиально и описывается на протяжении всего курса, начало этому положено ниже, в разделе «Переключение режимов».

ПРЕДУПРЕЖДЕНИЕ

Полной совместимости, конечно, не получилось. Реальный режим Intel386 и следующих поколений процессоров имеет многочисленные, хотя и не очень крупные отличия от 8086.

Но, поскольку подробное рассмотрение реального режима выходит за рамки курса, нигде далее эти отличия даже не упоминаются. Кому интересно, идите на http://wasm.ru, ищите там пользователя The Svin и спрашивайте его лично, он всё знает :)

Большое спасибо ему за это замечание.

Регистры

Регистры являются неотъемлемой частью процессоров Intel x86 и объединяют оба блока, не относясь к какому-либо одному, поэтому краткая сводка по ним приведена здесь.

Все регистры можно поделить на следующие группы (FPU/MMX/SSE/… - не рассматриваем! Ни здесь, ни далее):

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

ПРИМЕЧАНИЕ

В старых программах все команды работы с регистрами (в том числе pusha/popa, pushf/popf, call/ret/retf, …) будут работать точно так же, как и раньше, используя только младшие 16 разрядов 32-х разрядных регистров, у новых программ есть выбор. О том, как это получается, написано в приложении, в разделе «32-х разрядные сегменты кода»

Переключение режимов

Под капотом

Переключение режима работы процессора выполняется изменением флага PE, который находится в младшем бите регистра CR0.

ПРИМЕЧАНИЕ

Регистр CR0 появился в Intel386 и относится к группе управляющих регистров (Control Registers). Он содержит различные флаги, но кроме PE нам из него пока ничего не нужно.

Примерно так:

; Устанавливает флаг PE, переводит процессор в защищённый режим
set_PE:
        mov     eax, cr0 ; прочитать регистр CR0
        or      al, 1    ; установить бит PE,
        mov     cr0, eax ; с этого момента мы в защищенном режиме
        ret

; Сбрасывает флаг PE, переводит процессор в реальный режим
clear_PE:
        mov     eax, cr0 ; прочитать CR0
        and     al, 0FEh ; сбросить бит PE
        mov     cr0, eax ; с этого момента мы в реальном режиме
        ret

Как это обычно бывает при изменении флагов, в результате некоторым образом меняется логика работы процессора (а иначе зачем этот флаг?). Отличие от известных вам по реальному режиму флагов (Carry, Direction, Overflow, Interrupt Enable, …) только в том, что в данном случае «изменение логики работы процессора» по масштабу напоминает революцию, так как в него вовлечены практически все механизмы управления системой.

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

ПРИМЕЧАНИЕ

Революция («классическая» революция, а не то, что сейчас называется этим словом) – очень хорошая аналогия. Как в процессорах Intel, так и в жизни, для успешной смены режима нужно захватить и перевести на свою сторону различные системные структуры, только в одном случае это вокзалы, телефон и телеграф, а в другом – система прерываний и механизмы управления памятью.

Длительность, многоступенчатость процесса, в котором «изменение флага» служит некой формальной границей, но ни в коем случае не окончанием процесса, также имеет прямые параллели.

В результате, между фактическим переключением режима процессора (установка/сброс флага) и окончательной инициализацией соответствующих системных структур, есть некоторое количество команд, которые выполняются в «не до конца инициализированном» режиме. Но у процессора такого состояния нет! Он может находиться либо в реальном режиме, либо в защищённом, никаких «промежуточных» режимов не предусмотрено. Поэтому, с точки зрения процессора, эти команды выполняются в нормальном, полноценном режиме, хотя им и не доступны все его возможности (доступны только те, которые уже успели инициализировать).

Отсюда следует, что, если все возможности не нужны, вполне можно ограничиться «не до конца инициализированным» режимом. А, поскольку «правильная» (как советует [Intel 2004]) инициализация защищённого режима дело трудное и неблагодарное, мы будем активно использовать этот факт, инициализируя защищённый режим лишь настолько, насколько нам необходимо.

ПРЕДУПРЕЖДЕНИЕ

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

Ещё один важный момент: в реальном режиме любая программа может переключить процессор в защищённый режим, но обратное переключение может выполнить только ОС (иначе о надёжности и безопасности можно было бы забыть), при попытке изменить флаг PE пользовательской программой будет выброшено исключение. Подробнее эта тема рассмотрена в главе «Теоретическое введение в защиту».

Алгоритм в первом приближении

Как уже было сказано выше, «переключение режимов – непростой, довольно длительный, многоступенчатый процесс». Пока что мы сократим его до минимума, оставив всего две ступени. Точно так же окажутся урезанными и возможности нашей программы, но зато она будет проста, понятна и работоспособна.

Последовательность действий:

  1. Запрет маскируемых и немаскируемых прерываний (Nonmaskable Interrupt, NMI; что такое NMI в общем-то не важно, но если интересно – смотрите описание в приложении).
  2. Переключение процессора в защищённый режим (установка флага PE).
  3. Ура, мы в защищённом режиме.
  4. Переключение процессора в реальный режим (сброс флага PE).
  5. Разрешение прерываний.

В данном случае, пункты (1), (2) это переключение из реального в защищённый режим, а пункты (4), (5) – из защищённого в реальный.

Запрет/разрешение прерываний

С маскируемыми прерываниями всё просто – достаточно вызвать cli/sti. Про немаскируемые в [Intel 2004] написано, что они должны блокироваться «внешней схемой» (NMI interrupts can be disabled with external circuitry). В [Зубков 1999] описан следующий вариант:

; Запрещает маскируемые и немаскируемые прерывания
disable_interrupts:
        cli              ; запретить маскируемые прерывания
        in      al, 70h  ; индексный порт CMOS
        or      al, 80h  ; установка бита 7 в нем запрещает NMI
        out     70h, al
        ret

; Разрешает маскируемые и немаскируемые прерывания
enable_interrupts:
        in      al, 70h  ; индексный порт CMOS
        and     al, 7Fh  ; сброс бита 7 отменяет блокирование NMI
        out     70h, al
        sti              ; разрешить маскируемые прерывания
        ret
ПРЕДУПРЕЖДЕНИЕ

Очевидно, что для проверки работоспособности этого кода необходимо добиться возникновения немаскируемого прерывания. Поскольку, насколько я знаю, эта задача не решается программно, я просто поверил Сергею Владимировичу на слово. При написании коммерческой ОС проверьте самостоятельно!

Пример

Теория это, конечно, хорошо, но надо же когда-то начинать программировать. Для начала, по традиции, напишем небольшой Hello world.

Hello, world! [Protected Mode version]

ПРЕДУПРЕЖДЕНИЕ

Этот и все последующие примеры должны запускаться только из реального режима! Иначе, в лучшем случае, они не будут работать, в худшем (хотя и очень маловероятном) – снесут то, что работало.

СОВЕТ

При сборке com-программ MASM-ом, командная строка выглядит так:

ml.exe /AT <имя файла исходного кода>

; hello.asm
; Программа, выполняющая переход в защищенный режим и немедленный возврат.

        .model  tiny
        .code
        .386p           ; все наши примеры рассчитаны на 80386
        org     100h    ; это COM-программа
start:
        ; подготовить сегментные регистры
        push    cs
        pop     ds              ; DS - сегмент данных (и кода) нашей программы
        push    0B800h
        pop     es              ; ES - сегмент видеопамяти

        ; запретить прерывания
        call    disable_interrupts
        ; перейти в защищенный режим
        call    set_PE

        ; Мы в защишённом режиме!

        ; вывод на экран
        xor     di, di                   ; ES:DI - начало видеопамяти
        mov     si, offset message       ; DS:SI - выводимый текст
        mov     cx, message_l
        rep movsb                ; вывод текста
        mov     ax, 0720h        ; пробел с атрибутом 07h
        mov     cx, rest_scr     ; заполнить этим символом остаток экрана
        rep stosw
        
        ; переключиться в реальный режим
        call    clear_PE
        ; разрешить прерывания
        call    enable_interrupts

        ; подождать нажатия любой клавиши
        mov     ah, 0
        int     16h

        ; выйти из COM-программы
        ret

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Служебные функции
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Запрещает маскируемые и немаскируемые прерывания
disable_interrupts:
        cli              ; запретить прерывания
        in      al, 70h  ; индексный порт CMOS
        or      al, 80h  ; установка бита 7 в нем запрещает NMI
        out     70h, al
        ret

; Разрешает маскируемые и немаскируемые прерывания
enable_interrupts:
        in      al, 70h  ; индексный порт CMOS
        and     al, 7Fh  ; сброс бита 7 отменяет блокирование NMI
        out     70h, al
        sti              ; разрешить прерывания
        ret

; Устанавливает флаг PE
set_PE:
        mov     eax, cr0 ; прочитать регистр CR0
        or      al, 1    ; установить бит PE,
        mov     cr0, eax ; с этого момента мы в защищенном режиме
        ret

; Сбрасывает флаг PE
clear_PE:
        mov     eax, cr0 ; прочитать CR0
        and     al, 0FEh ; сбросить бит PE
        mov     cr0, eax ; с этого момента мы в реальном режиме
        ret

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Данные
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; текст сообщения с атрибутом после каждого символа для прямого вывода на экран
message db      'H',7,'e',7,'l',7,'l',7,'o',7,',',7,' ',7,'w',7
        db      'o',7,'r',7,'l',7,'d',7,'!',7,
; его длина в байтах
message_l = $ - message
; длина оставшейся части экрана в словах
rest_scr = (80*25) - (message_l / 2)

        end     start

А был ли мальчик?

После того, как программа напечатала на экране «Hello, world!» и успешно завершилась, возникает резонный вопрос: а было ли переключение в защищённый режим? Ведь вывести на экран такую строчку несложно и из реального…

К сожалению, на текущем этапе наших знаний недостаточно для получения «положительного» подтверждения, то есть мы пока не можем сделать что-либо, недоступное в реальном режиме. Зато можно получить «отрицательное»! Для этого достаточно записать число больше 7 (почему больше 7 – в следующей главе) в любой сегментный регистр, скорее всего, это закончится повисанием компьютера или аварийной перезагрузкой. Чувствуете мощь? В реальном режиме добиться такого результата значительно сложнее! :)


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.