Начала программирования в защищённом режиме. Часть 1
Предисловие
Данный цикл статей расчитан на пользователей персональный компьютеров типа IBM PC, которые хотели бы познакомиться с архитектурой процессоров семейства Intel в защищённом режиме и написать свою собственную программу, функционирующую как вполне самостоятельная операционная система.
От читателя требуется хотя бы минимальный опыт программирования на языках Assembler и C++, потому что будет предполагаться, что с некоторыми моментами читатель знаком, и очень подробно они рассматриваться не будут. Но даже новички, не знающие хорошо языки программирования, получат очень много информации для размышления и осознания.
Особенно ценным материал покажется людям, любящим поломать свою голову, потому что дальнейшее его изложение будет основано на методе проб и ошибок. Оно и ясно: если человек однажды наткнулся на проблему и смог её логично решить, то он практически никогда больше не будет с ней сталкиваться.
Введение
Процессоры семейства x86 наиболее распространены по всему миру. Если касаться истории раннего развития процессоров Intel, то можно выделить следующие этапы:
Конечно же, за многие годы своей эволюции процессоры Intel и AMD претерпели много изменений, основными из которых были повышение скорости обработки информации и выполнения команд. Сегодняшние процессоры (80486, Cyrix x586, AMD K5, Intel Pentium, Intel Pentium Pro, AMD K6, Intel Pentium 2, AMD K7, Intel Pentium 3, Intel Pentium 4, Intel Celeron, AMD Athlon, AMD Duron) ― прямые потомки процессора 80386, полностью совместимые с ним. Конечно, сейчас эпоха настольных компьютеров с процессорами, реализующими IA32 (Intel Architecture 32), подходит к концу, и обе компании-лидера (AMD и Intel) выдвигают новую архитектуру процессоров ― Intel Itanium (Intel Extended Memory 64 Technology) и AMD Athlon 64 (AMD 64 Technology), открывающими новые возможности. Но производители сохраняют новые процессоры полностью совместимыми с IA32. Однако стоит помнить о том, что большинство настольных компьютеров остаются пока ещё на базе 32-разрядных процессоров (бывают случаи, когда в России местами встречаются ещё и 386-ые процессоры), и резкого скачка к 64-разрядным процессорам не намечается. Поэтому тема "защищённого режима" по сей день остаётся актуальной.
- В 1971 году был выпущен первый процессор 4004, не получивший широкого распространения из-за малого набора команд.
- В 1974 году появился широко распространённый по сей день процессор 8080. Но если в то время процессор управлял компьютером ZX-Spectrum, то на сегодняшний день его можно встретить в различных бытовых устройствах.
- В 1978 году компания Intel выпустила революционный процессор 8086, положивший начало дальнейшему прогрессивному развитию компьютерной техники.
- В 1985 году компания Intel сделала прорыв, выпустив процессор 80386. В нём была впервые реализована 32-битная шина данных и реализована полная поддержка так называемого "защищённого режима" работы процессора, о котором и пойдёт в дальнейшем речь.
- Но нельзя считать, что "защищённый режим" появился в 386-ом процессоре. Впервые он был реализован в его предке ― Intel 80286, но его недостатками были сложность реализации и наличие множества недоработок. Появление процессора 80386 знаменовало, что пришло время многозадачных программ и операционных систем.
Итак, с появлением процессора 80386, началась эпоха "защищённого режима". Что такое "защищённый режим"? Этому термину можно дать следующее определение: "новый механизм распределения и защиты памяти". Конечно, это очень узкое толкование, для обрисовки полной картины следует перечислить все новые возможности, предоставленные процессором Intel 80386:
Кроме защищённого режима в процессорах третьего поколения появились ещё три режима работы: реальный, виртуальный 8086 и нереальный режим. Об этих режимах речь пойдёт в следующих статьях.
- Работа с памятью, большей 1 мегабайта, и возможность реализации виртуальной памяти, общий объём которой достигает 64 терабайт.
- Механизм защиты памяти, который позволяет приложениям работать только с выделенной для них областью памяти.
- Многозадачность, то есть организация одновременного выполнения нескольких программ или процессов, запущенных на компьютере.
Конечно, ограничиваться этими статьями не следует, и есть ещё много литературы, которую обязательно следует почитать. Например, документация от разработчика даёт самые обширные знания по архитектуре 386-х процессоров и их потомков. У нас же будет больше практики, всего того, что автор собственными силами смог добиться, и, что самое ценное, эти статьи предназначены для того, чтобы дать читателю больше практики, нежели теории. Порой автору встречались теоретики, которые довольно хорошо знали архитектуру защищённого режима, но не могли написать элементарной программы, корректно работающей в нём, по причине недостаточного практического опыта. Наша же задача ― твёрдо встать на ноги, после чего будет легче идти дальше самостоятельно. Но чтобы встать на ноги, нужно сначала приложить максимум усилий и внимательности. Помогут в этом нам подручные средства, которые и будут перечислены в следующей статье.
Инструментарий
Порой для достижения целей приходится пользоваться многими подручными средствами, которые позволяют исключить множество ошибок на этапе разработки программы. Надо помнить, что, допустим, встроенный в среду Watcom C++ дизассемблер не покажет ошибку, которую покажет профессиональный дизассемблер IDA, но порой даже дизассемблер не сможет отыскать ошибку, которую "словит" отладчик. Поэтому лучший способ решения проблем ― быть во всеоружии и не удивляться тому, что, вроде бы, текст программы написан грамотно (с точки зрения алгоритма), а она не хочет корректно функционировать.
Итак, приступим: эта статья предназначена для того, чтобы познакомить читателя с минимальным инструментарием, необходимым для разработки программ, функционирующих в защищённом режиме.
Язык программирования Assembler
Для дальнейшего изучения материала и практики потребуется компилятор языка Assembler, который способен генерировать 32-х битный код. Для этой цели хорошо подходят такие известные компиляторы как MASM (Macro Assembler от компании Microsoft), TASM (Turbo Assembler от компании Borland) и WASM (Watcom Assembler от компании Watcom). Конечно, мир ассемблера огромен и ограничиваться лишь этими тремя компиляторами не следует. Найти компиляторы языка Assembler сейчас совсем не сложно ― достаточно посетить Internet, откуда можно скачать совершенно различные их версии и реализации. Но особое внимание следует уделить компилятору FASM (Flat Assembler), специально созданного для написания операционной системы (и не только). Взять последнюю версию дистрибутива программы можно с официального сайта FASM: http://www.flatassembler.net. Она распространяется как самокомпилирующийся OpenSource-проект. Забегая вперёд, сообщу, что знакомство с защищённым режимом у нас начнётся именно с этого компилятора (я пользовался версией 1.51).
В дальнейшем мы будем больше пользоваться компилятором WASM, потому что мы будем иметь дело со средой программирования Watcom C++, о которой пойдёт речь в одной из следующих статей. Итак, вот перечень файлов, которые нам могут потребоваться:
Дизассемблер
- fasm.exe ― компилятор языка FASM.
- wasm.exe ― компилятор языка WASM.
- wlink.exe ― компоновщик языка WASM.
Одним из важных компонентов разработки полноценных программ является дизассемблер. Именно с помощью него можно найти практически все ошибки, возникающие при компиляции проекта или написании программы. Конечно, одним дизассемблером не обойтись, и порой приходится прибегать к работе с отладчиком. Но об отладчике речь пойдёт позже, а сейчас следует уделить внимание хорошему дизассемблеру. Самым лучшим, на мой взгляд, оказался Interactive Disassembler Pro, который я использовал для исследования сгенерированного компилятором кода (версию 6.8.150423). Внешне интерфейс IDA выглядит так:
При очередной загрузке дизассемблер предлагает открыть файл для дизассемблирования, после выбора он предлагает способ дизассемблирования файла из всех возможных. Когда файл и способ дизассемблирования выбран, перед нами открывается окошко с готовым текстом программы на ассемблере. В этом тексте нам порой и придётся разбираться.
Отладчик
Как было уже сказано в начале статьи, одним дизассемблером не обойтись, поэтому порой приходится прибегать к работе с отладчиком. Одним из удобных отладчиков можно назвать Turbo Debugger от компании Borland. Это классическое DOS-приложение, выглядящее следующим образом:
Опять же, с работой в отладчике мы встретимся в следующих статьях. Пока достаточно того факта, что он должен быть в нашем наборе инструментов.
Среда разработки Watcom C++
Вот мы и дошли до самого главного ― компилятора языка C++. Почему именно Watcom C++? Дело в том, что языков, на которых можно написать самостоятельную операционную систему, не так много, и их можно перечислить по пальцам. К ним можно причислить GNU C/C++ и Watcom C++. Остальные либо не столь популярны, либо являются коммерческой тайной фирм-разработчиков операционных систем. Другие же компиляторы не способны генерировать самостоятельный код, а создают программы, адаптированные под конкретную операционную систему. Хорошие качества Watcom C++ заключаются в следующем:
Рекомендуемая литература
- GNU C++ больше используется для написания операционных систем на базе ядра UNIX, и у него есть свой, специфический синтаксис. Во-вторых, ассемблерные вставки в этом языке UNIX-стандарта, что, конечно, приводит к некоторым неудобствам, допустим, при переносе какого-либо кода из уже написанных программ под MS-DOS.
- Watcom C++ прекрасно генерирует 32-битный код, при чём с превосходной оптимизацией. В этом можно убедиться, допустим, дизассемблировав полученный исполняемый файл. Конечно, у оптимизации есть свои "подводные камни", которые будут обязательно рассмотрены в статьях, по чему можно будет сделать вывод: "Самая лучшая оптимизация не всегда самая качественная". Но по некоторым параметрам Watcom C++ проигрывает в оптимизации GNU C++.
- Для Watcom C++ очень легко пишется startup-код (код запуска системы), однако, как всякому системному программисту, нам придётся отказаться от стандартных библиотек.
- Код других компиляторов хорошо переносится на Watcom C++. Достаточно привести пример, что код на Borland C++ вносится в проект и компилируется с незначительными изменениями текста программы.
- У Watcom C++ есть собственный (конечно, не совсем удобный) редактор кода. Конечно, можно написать свой компилятор языка C++, но это будет титаническим трудом, нежели воспользоваться разработками целых компаний.
- Ещё одним плюсом Watcom C++ (как и GNU C++) является его Freeware-распространение. Скачать последнюю версию компилятора и среду разработки можно с официального сайта http://www.openwatcom.org. Я пользовался версией 1.3.
- Зубков С.В. Assembler для DOS, Windows и UNIX. ― М:. ДМК Пресс, 2000. ― 608 с.: ил. (Серия "Для программистов").
- Юров В.И. Assembler. Учебник для вузов. 2-е изд. ― СПб.: Питер, 2004. ― 637 с.: ил.
- Финогенов К.Г. Основы языка Ассемблера. ― М.: Радио и связь, 2000. ― 288 с.: ил.
- Страуструп Б. Язык программирования C++, 3-е изд./Пер. с англ. ― СПб.; М.: "Невский диалект" ― "Издательство БИНОМ", 1999 г. - 991 с., ил.
- Фридман А.Л. Основы объектно-ориентированного программирования на языке Си++. ― М.: Горячая линия ― Телеком, Радио и связь, 1999. - 208 с.: ил.
Начала программирования в защищённом режиме. Часть 2
Эта статья представляет собой некоторое введение в разработку программ, функционирующих в защищённом режиме процессора. Здесь будут поставлены основные задачи любой программы, работающей в защищённом режиме, и приведены их решения. В основном, программы будут написаны на языке FASM.
Режимы работы процессора Intel 80386
С появлением процессора Intel 80386 возникла архитектура IA32. Она предполагала появление нового режима работы процессора ― защищённого ("Protected Mode"). Для совместимости с предыдущими процессорами линейки Intel 80x86 процессор 80386 не запускался сразу в защищённом режиме, а работал в так называемом реальном режиме ("Real Mode"). Кроме этого, у каждого режима есть один или несколько подрежимов. Разберём их.
Реальный режим
В этом режиме процессор находится сразу после нажатия кнопки "Power" компьютера. Доступ к памяти в реальном режиме осуществляется конструкцией "сегмент:смещение", которая описывает логический адрес. Значение сегмента, как и смещения, лежит в пределах от 0 до 0FFFFh.
Так как адресоваться можно только в пределах одного сегмента, то максимальный размер сегмента равен 64 килобайт. Физический адрес, который выставляется на адресную шину процессора, считается по формуле:
линейный адрес = сегмент × 16 + смещениеВ реальном режиме процессоров 80186 и 8086 значение сегмента лежало в пределах от 0 до 0F000h. Таким образом, максимальный выставленный адрес на адресную шину равен 0FFFFFh, что соответствует 220―1, т.е. 1 мегабайту.
Конечно, поначалу объём такой памяти казался колоссальным, но со временем одного мегабайта стало не хватать. С появлением процессора 80286 стал доступен так называемый блок памяти UMB, начинающийся с адреса 0FFFFh:0010h и заканчивающийся адресом 0FFFFh:0FFFFh (65520 байт за пределами одного мегабайта). Теперь можно было переконфигурировать операционную систему MS-DOS так, чтобы она занимала этот блок, освобождая в оперативной памяти 64 килобайт.
Защищённый режим
Этот режим имеет сложную конструкцию по сравнению с реальным. Логический адрес представляется конструкцией "селектор:смещение". Селектор находится в пределах от 0 до 0FFFFh (на самом деле, селекторов в 4 раза меньше ― об этом подробнее в следующих статьях). Смещение, в отличие от реального режима, является 32-разрядным, что позволяет адресовать сегменты размером 4 гигабайт. Логический адрес преобразуется в линейный по следующей схеме:
линейный адрес = база сегмента + смещениеЛинейный адрес в дальнейшем выставляется на адресную шину, если не включен режим страничной адресации. В противном случае линейный адрес преобразуется в физический, и только после этого выставляется на адресную шину. Кроме этого, защищённый режим позволяет организовать виртуальную память, достигающую размера до 64 терабайт и зависящую лишь от объёма жёсткого диска (например, тот же файл подкачки в Windows реализует виртуальную память). В защищённом режиме функционируют практически все современные операционные системы.
Мультизадачный подрежим защищённого режима
Этот режим позволяет организовать мультизадачность, то есть возможность одновременного выполнения нескольких задач или многопользовательской системы.
Режим виртуального 8086
Это также подрежим защищённого режима, который позволяет создать виртуальную машину, функционирующую как будто бы она находилась в реальном режиме, но, на самом деле, работающей в защищённом режиме.
Нереальный режим
Это особый подрежим реального режима. Процессор находится в реальном режиме, но адресуется к памяти путём конструкции "селектор: смещение". Таким образом доступна память выше 1 мегабайта. В дальнейшем будет рассматриваться защищённый режим и его подрежимы.
Первая программа: переход в защищённый режим
Переход в защищённый режим осуществляется установкой бита 0 регистра CR0. Переход в реальный режим осуществляется сбросом того же нулевого бита. Рассмотрим программу, которая выполняет эту операцию (язык ― Flat Assembler):Однако эта программа является совсем "сырой", потому что в ней нельзя реализовать зацикливание. Если написать команды вроде hlt или jmp $, то по срабатыванию первого же прерывания компьютер перезагрузится.Код (ASM):
use16 ; Используются 16-разрядные команды org 100h start: ; Мы находимся в реальном режиме mov eax, cr0 ; Считываем значение регистра CR0 or al, 1 ; Устанавливаем нулевой бит mov cr0, eax ; Записываем новое значение CR0 ; Мы в защищённом режиме mov eax, cr0 ; Считываем значение CR0 and al, 0FEh ; Сбрасываем нулевой бит в 0 mov cr0, eax ; Переходим в реальный режим ; Мы в реальном режиме ret ; Выходим из программы
В нашем случае команды срабатывают достаточно быстро, но не исключено, что в промежутке между выполнением каких-либо команд нашей программы прерывание всё-таки сработает, что приведёт к моментальному сбою и перезагрузке. Поэтому следует позаботиться о прерываниях. Итак, посмотрим ещё раз листинг. Это нельзя назвать первой программой (скорее, она пошла бы за нулевую), поскольку в ней не реализуются основные действия по переходу в защищённый режим. Чтобы полноценно перейти в защищённый режим с минимальными настройками, нужно выполнить следующие действия:
Но первой программе достаточно выполнить действия 3, 4, 5. Тогда её зацикливание не приведёт к перезагрузке компьютера. Разберёмся в каждом действии.
- проверить, возможно ли переключиться в защищённый режим;
- инициализировать таблицы дескрипторов;
- запретить прерывания (как маскируемые, так и не маскируемые);
- открыть линию A20;
- загрузить регистры управления памятью;
- установить нулевой бит (далее он будет называться PE) регистра CR0;
- выполнить переход на 32-битный сегмент кода, переопределив регистр CS.
Запрещение прерываний предохраняет нас от перезагрузки. Прерывания делятся как на маскируемые, так и немаскируемые. Чтобы запретить маскируемые прерывания, надо сбросить флаг IF регистра EFLAGS командой cli, разрешение же прерываний выполняется командой sti. Немаскируемые прерывания запрещаются несколько иначе. Для этого существуют два способа: программирование регистров контроллера прерываний (этот способ будет рассмотрен несколько позже) или изменение седьмого бита порта 70h: если бит установлен, то прерывания запрещены, если бит сброшен ― прерывания могут выполняться.
Теперь зададимся вопросом, в чём заключается функция линии A20, и что это такое. Линия A20 ― одна из 32 адресных линий. При загрузке компьютера линия A20 закрыта. Это приводит к генерации 20-разрядных адресов (то есть всё адресное пространство получается равным 220=1 мегабайт). Введено это для совместимости с процессором 8086: таким образом, пытаясь записать по линейному адресу 12345678h, мы, на самом деле, запишем по адресу (12345678h)&0FFFFFh=00045678h, что может привести к совершенно неожиданному результату. Поэтому для полноценного функционирования 32-разрядного приложения линия A20 обязательно должна быть открыта. Осуществляется это установкой бита 1 порта 92h, закрытие линии A20 ― сброс этого бита.
С последним действием читатель уже знаком, и оно уже не должно вызывать у него вопросов.
Итак, рассмотрим листинг нашей новой, первой, программы, в которой уже реализуется небольшой цикл. Строки, которые добавлены к предыдущему листингу, помечены звёздочкой (*).Убедиться в том, что программа работает, можно, запустив исполняемый файл из чистого MS-DOS. Если программа корректно завершается, то всё в порядке.Код (ASM):
use16 org 100h start: ; Мы находимся в реальном режиме ; Запретить маскируемые прерывания cli ;* ; Запретить немаскируемые прерывания (NMI) in al, 70h ;* or al, 80h ;* out 70h, al ;* ; Открыть линию A20 in al, 92h ;* or al, 2 ;* out 92h, al ;* ; Переключиться в защищённый режим mov eax, cr0 or al, 1 mov cr0, eax ; Теперь находимся в защищённом режиме ; Небольшой двойной цикл mov cx, 20 ;* cycle: ;* mov ax, cx ;* or cx, -1 ;* loop $ ;* mov cx, ax ;* loop cycle ;* ; Переключиться в реальный режим mov eax, cr0 and al, 0FEh mov cr0, eax ; Закрыть линию A20 in al, 92h ;* and al, 0FDh ;* out 92h, al ;* ; Разрешить немаскируемые прерывания (NMI) in al, 70h ;* and al, 7Fh ;* out 70h, al ;* ; Разрешить маскируемые прерывания sti ;* ; Мы снова находимся в реальном режиме ret ; завершить программу
Однако могут возникнуть и следующие проблемы:
Это может возникнуть из-за следующих причин:
- компьютер "зависает";
- компьютер перезагружается.
Поэтому перед запуском любой программы, работающей в защищённом режиме, следует проверить возможность дальнейшей работы программы. Об этом будет рассказано дальше.
- программа запущена в режиме V86 (режим виртуального 8086);
- программа запущена в защищённом режиме или под конкретной операционной системой.
Проверка возможности перехода в защищённый режим
В предыдущей главе у нас возникла следующая проблема: программа не распознаёт то, что она находится в защищённом режиме или режиме V86, что приводит к зависанию системы или её перезагрузке. Если же мы попытаемся запустить программу под управлением операционной системы Windows, то Windows отловит попытку программы перейти в защищённый режим и предложит перезагрузиться в режиме эмуляции MS-DOS (для платформы 9x), либо завершит работу программы принудительно (платформа NT).
Итак, для проверки того, что мы действительно находимся в реальном режиме, следует выполнить следующие операции:
Первая операция осуществляется прямым чтением регистра CR0 с дальнейшей проверкой нулевого бита регистра EAX, AX или AL. Если бит не установлен, то мы находимся в реальном режиме. В противном случае дальнейшее выполнение программы становится бессмысленным.
- проверить нулевой бит регистра CR0;
- убедиться, что не загружена операционная система Windows.
Второе действие осуществляется вызовом функции 1600h прерывания 2Fh. Эта функция позволяет получить текущую версию операционной системы Windows. Если после вызова функции в регистре AL содержится ноль, то операционная система не загружена. В противном случае, опять же, нашей программе бессмысленно продолжать какие-либо дальнейшие действия.
Рассмотрим пример следующей программы. Она является модификацией предыдущей программы, все новые инструкции помечены звёздочкой (*).Этот пример тоже пока не показывает реализацию 32-разрядных команд. Для этого следует ещё познакомиться с материалом последующей главы. Кроме этого, у примера есть следующий недостаток: вызываются функции DOS (int 21h), что уже начинает противоречить независимости нашей программы от операционной системы MS-DOS. В дальнейшем придётся избавиться от использования функций операционной системы и перейти к использованию функций BIOS. Но пока достаточно ограничиться и таким кодом.Код (ASM):
use16 org 100h start: ; Настроить сегментные регистры mov ax, cs ;* mov ds, ax ;* ; Проверка того, что мы действительно в реальном режиме mov eax, cr0 ;* проверка нулевого бита test al, 1 ;* регистра CR0 jz no_pm ;* ; Вывести сообщение об ошибке mov ah, 9 ;* функция DOS 09h mov dx, pm_msg ;* вывод строки int 21h ;* ret ;* и выйти no_pm: ; Проверка: не запущена ли программа под Windows mov ax, 1600h ;* функция 1600h мультиплексорного int 2Fh ;* прерывания - получить версию Windows test al, al ;* если не 0 - ошибка jz no_windows ; Вывести сообщение об ошибке mov ah, 9 ;* mov dx, win_msg ;* int 21h ;* ret ;* no_windows: ; Мы точно находимся в реальном режиме ; Запретить маскируемые прерывания cli ; Запретить немаскируемые прерывания (NMI) in al, 70h or al, 80h out 70h, al ; Открыть линию A20 in al, 92h or al, 2 out 92h, al ; Переключиться в защищённый режим mov eax, cr0 or al, 1 mov cr0, eax ; Теперь находимся в защищённом режиме ; Небольшой двойной цикл mov cx, 20 cycle: mov ax, cx or cx,-1 loop $ mov cx, ax loop cycle ; Переключиться в реальный режим mov eax, cr0 and al, 0FEh mov cr0, eax ; Закрыть линию A20 in al, 92h ;* and al, 0FDh ;* out 92h, al ;* ; Разрешить немаскируемые прерывания (NMI) in al, 70h ;* and al, 7fh ;* out 70h, al ;* ; Разрешить маскируемые прерывания sti ;* ; Мы снова находимся в реальном режиме ret ; завершить программу ; Сообщения об ошибках pm_msg: ;* db "Error: already running in protected mode!$" ;* win_msg: ;* db "Error: Microsoft Windows detected!$" ;*
Сегментация памяти в защищённом режиме. Дескрипторы. Таблицы дескрипторов. Часть 3
В предыдущей статье была реализована программа, осуществляющая переход в защищённый режим с проверкой на возможное дальнейшее функционирование в защищённом режиме и реализующая простой цикл с использованием 16-разрядных команд. Теперь задача следующей программы – переход в защищённый режим и использование уже 32-разрядных команд. Для реализации этого следует познакомиться с новой информацией, касающейся защищённого режима.
Как говорилось раньше, сегментация памяти защищённого режима представляет собой совсем другую форму адресации: вместо привычной для реального режима адресации «сегмент:смещение» используется адресация «селектор:смещение». Рассмотрим структуру селектора:
Биты 3-15 указывают номер дескриптора в выбранной таблице дескрипторов (о таблицах чуть ниже). Таким образом, селектор может описать 213=8192 дескрипторов для одной таблицы. Каждый дескриптор в таблице описывает сегмент, при чём он не обязательно должен быть сегментом кода или данных (о типах сегментов будет написано ниже).
Рисунок 11. Формат селектора
Бит 2 – флаг TI (Table Indicator). Этот флаг указывает, какая из двух таблиц дескрипторов будет использоваться для загрузки дескриптора. Если флаг равен нулю, то используется глобальная таблица дескрипторов (GDT – Global Descriptor Table), в другом случае используется локальная таблица дескрипторов (LDT – Local Descriptor Table). Теперь поймём, что значит фраза «загрузка дескриптора». Дело в том, что, начиная с процессора 80286, размер сегментных регистров стал равным не двум байтам, а десяти, доступными из которых остались только два младших (в них и грузится селектор). Однако при загрузке селектора в младшие два байта сегментного регистра, в старшие 8 байт автоматически загружается дескриптор из используемой таблицы, по которому и ведётся дальнейшая адресация.
Биты 0-1 содержат запрашиваемый уровень привилегий (RPL – Requested Privilege Level) доступа к сегменту, то есть с какими привилегиями программа обращается к сегменту, описанному дескриптором. О привилегиях доступа будет сказано позже, и пока будем считать их равными нулю (то есть, самыми высокими).
Пока в наших примерах достаточно пользоваться сегментными дескрипторами. Рассмотрим структуру сегментного дескриптора:
Теперь рассмотрим формат дескриптора поподробнее.
Рисунок 12. Формат сегментного дескриптора
База – это линейный адрес, с которого начинается сегмент.
Лимит – это максимальное смещение от начала сегмента, то есть лимит равен размеру сегмента минус один байт. Таким образом, получается, что минимальный размер сегмента может быть равен одному байту, а максимальный – одному мегабайту или четырём гигабайтам (это зависит от, того, умножается размер сегмента на 4 килобайта или нет).
G – флаг гранулярности (Granularity), который, как раз, указывает, в чём измеряется размер сегмента: если G=0, то размер сегмента считается в байтах (то есть, максимальный размер сегмента получается 220=1 мегабайт), если G=1, то размер сегмента считается в 4-килобайтных блоках (страницах), то есть, максимальный размер сегмента равен 220×212=4 гигабайт, при чём размер сегмента всегда кратен 4 килобайтам.
D/B – флаг, указывающий разрядность сегмента:
Бит 53 зарезервирован и всегда должен быть равен нулю.
- 0 – сегмент 16-разрядный,
- 1 – сегмент 32-разрядный (этот флаг ещё называют BIG).
Бит AVL является свободным (Available) и может использоваться по усмотрению программы.
Биты DPL определяют привилегии доступа к сегменту (Descriptor Privelege Level). Всего существует 4 уровня доступа, в которых 0 – самый высокий приоритет, 3 – самый низкий приоритет. Таким образом, чтобы программе было возможным использовать сегмент, описанный данным дескриптором, в селекторе, используемом программой биты RPL должны принимать не меньшее значение, чем биты DPL дескриптора. В противном случае возникает аппаратная ошибка (прерывание), названная исключением, которая может быть обработана ядром ОС.
Флаг S и тип сегмента следует рассматривать вместе. Если S установлен, то это значит, что сегмент является системным (System), в противном случае сброшенный флаг S означает, что дескриптор описывает сегмент данных или кода. Если сегмент является системным, то указывается его тип (о типах системных дескрипторов будет написано позже), в другом случае дескриптор описывает либо сегмент данных, либо сегмент кода.
Поле «тип сегмента» для сегмента данных выглядит так:
Для сегмента кода поле выглядит немного иначе:
Рисунок 13. Поле «тип сегмента» сегмента данных
Флаг А также используется для организации виртуальной памяти и является флагом доступа (Accessed), то есть при любом обращении к описываемому сегменту флаг будет автоматически установлен (то есть, по этому биту операционная система решает, какие сегменты следует временно сбросить на диск в swap-файл, а с какими сегментами следует повременить). То есть, ОС может периодически сбрасывать этот бит, и если он через некоторое время не станет равным единице, то данный сегмент можно сбросить, допустим, на жёсткий диск, освободив тем самым память.
Рисунок 14. Поле «тип сегмента» сегмента кода
Для сегмента данных определяются два характерных флага:
Для сегмента кода определяются следующие два характерных флага:
- Флаг D задаёт направление роста сегмента (Direction). Обычно, если D=1, то выбранный сегмент является сегментом стека и растёт «задом наперёд». Если же D=0, то выбранный сегмент является сегментом данных. На самом деле, значение D=1 практически не используется.
- Флаг W определяет возможность записи в сегмент (Writable). То есть, сброшенный флаг указывает, что сегмент данных доступен только для чтения. Если флаг установлен, то сегмент доступен также и для записи.
Из рассмотрения этих двух полей можно сделать вывод: писать прямо в сегмент кода категорически запрещено, как и запускать программу, находящуюся в сегменте данных.
- Бит C называется битом подчинения (Conforming), но рассмотрен он будет позже.
- Флаг R указывает на доступность сегмента кода для чтения (Readable). То есть, если флаг сброшен, читать из сегмента кода нельзя.
Поля P, DPL, S и тип сегмента образуют единую группу – права доступа сегмента, и для операции с этой группой введены специальные команды процессора, которые будут рассмотрены позже.
Теперь перейдём к рассмотрению структуры таблиц дескрипторов. Таблицы LDT (Local Descriptor Table), IDT (Interrupt Descriptor Table) и GDT (Global Descriptor Table) имеют одинаковую структуру – это массивы дескрипторов. Сами таблицы не являются сегментами – это области физической памяти. Отличием IDT от GDT и LDT является то, что в ней хранятся особые дескрипторы, которые мы пока затрагивать не будем. Единственным отличием GDT от LDT является то, что таблица GDT может быть всего лишь одна, а таблиц LDT может быть много. Кроме этого, нулевой дескриптор в таблице GDT не может быть использован и должен содержать нули (так называемый «нулевой дескриптор»). Структуры таблиц выглядят следующим образом:
Остаётся ещё один вопрос: каким образом процессор находит в памяти эти таблицы. Вспомним структуру реального режима: таблица прерываний хранится, начиная с нулевого логического адреса и занимает 400h байт. Процессору не составляло труда её обнаружить, так как на любой машине под таблицу были зарезервированы самые младшие 400h байт адресного пространства. В защищённом режиме всё несколько иначе: таблицы могут располагаться в любом месте оперативной памяти, и их адреса нужно где-то фиксировать. Для этого в процессорах Intel (начиная с модели 80286) появились новые машинно-специфичные регистры: GDTR(определяет расположение и размер таблицы GDT), LDTR (определяет селектор таблицы LDT), IDTR (определяет расположение и размер таблицы IDT). Регистры GDTR и LDTR схожи по строению: они имеют одинаковый размер (6 байт) и содержат в себе одинаковую структуру данных о расположении и размере таблицы в памяти. Регистр LDTR отличается от этих двух регистров по размеру (10 байт) и содержит в себе селектор дескриптора сегмента данных таблицы LDT и сам дескриптор, который находится в таблице GDT.
Рисунок 15. Структура таблиц дескрипторов
Рассмотрим структуру данных, хранящихся в регистрах GDTR и IDTR:
Рисунок 16. Структура данных регистров GDTR и IDTR
Начала программирования в защищённом режиме (часть 1)
Дата публикации 24 авг 2017
| Редактировалось 10 янв 2018