К тексту программы добавлены номера строк, которые облегчат описание отдельных команд в тексте. Следует иметь в виду, что перед трансляцией показанного текста, необходимо удалить все номера строк, так как компилятор на каждый номер будет выдавать ошибку.
32-разрядные микропроцессоры отличаются расширенным набором команд, часть которых относится к привилегированным. Для того, чтобы разрешить транслятору обрабатывать эти команды, в текст программы необходимо включить директиву ассемблера.386Р.
Программа начинается с описания структуры дескриптора сегмента. В отличие от реального режима, в котором сегменты определяются их базовыми адресами, задаваемыми программистом в явной форме, в защищенном режиме для каждого сегмента программы должен быть определен дескриптор – 8-байтовое поле, в котором в определенном формате записываются базовый адрес сегмента, его длина и некоторые другие характеристики (рисунок 1.8) [8].
Рисунок 1.8 – Дескриптор сегмента
Рисунок 1.9 – Селектор дескриптора
Теперь для обращения к требуемому сегменту программист заносит в сегментный регистр не сегментный адрес, а так называемый селектор (рисунок 1.9), в состав которого входит номер (индекс) соответствующего сегмента дескриптора.
Процессор по этому номеру находит нужный дескриптор, извлекает из него базовый адрес сегмента и, прибавляя к нему указанное в конкретной команде смещение (относительный адрес), формирует адрес ячейки памяти. Индекс дескриптора (0, 1, 2 и т.д.) записывается в селектор, начиная с бита 3, что эквивалентно умножению его на 8. Таким образом, можно считать, что селекторы последовательных дескрипторов представляют собой числа 0, 8, 16, 24 и т.д. Другие поля селектора, которые для нашего случая принимают значения 0, будут описаны ниже.
Структура descr (строка 8 листинга 1.1) предоставляет шаблон для дескрипторов сегментов, облегчающий их формирование. Сравнивая описание структуры descr в программе с рисунком 1.8, нетрудно заметить их соответствие друг другу.
Рассмотрим вкратце содержимое дескриптора. Граница (limit) сегмента представляет собой номер последнего байта сегмента. Так, для сегмента размером 375 байт граница равна 374. Поле границы состоит из 20 бит и разбито на две части. Как видно из рисунка 1.8, младшие 16 бит границы занимают байты 0 и 1 дескриптора, а старшие 4 бита входят в байт атрибутов 2, занимая в нем биты 0...3. Получается, что размер сегмента ограничен величиной 1 Мбайт. На самом деле это не так. Граница может указываться либо в байтах (и тогда, действительно, максимальный размер сегмента равен 1 Мбайт), либо в блоках по 4 Кбайт (и тогда размер сегмента может достигать 4 Гбайт). В каких единицах задается граница, определяет старший бит байта атрибутов 2, называемый битом дробности. Если он равен 0, граница указывается в байтах; если 1 – в блоках по 4 килобайта.
База сегмента (32 бита) определяет начальный линейный адрес сегмента в адресном пространстве процессора. Линейным называется адрес, выраженный не в виде комбинации сегмент-смещение, а просто номером байта в адресном пространстве. Казалось бы, линейный адрес – это просто другое название физического адреса. Для нашего примера это так и есть, в нем линейные адреса совпадают с физическими. Однако если в процессоре включен блок страничной организация памяти, то процедура преобразования адресов усложняется. Отдельные блоки размером 4 Кбайт (страницы) линейного адресного пространства могут произвольным образом отображаться на физические адреса, в частности и так, что большие линейные адреса отображаются на начало физической памяти, и наоборот. Страничная адресация осуществляется аппаратно (хотя для ее включения требуются определенные программные усилия) и действует независимо от сегментной организации программы. Поэтому во всех программных структурах защищенного режима фигурируют не физические, а линейные адреса. Если страничная адресация выключена, эти линейные адреса совпадают с физическими, если включена – могут и не совпадать.
Страничная организация повышает эффективность использования памяти программами, однако практически она имеет смысл лишь при выполнении больших по размеру задач, когда объем адресного пространства задачи (виртуального адресного пространства) превышает наличный объем памяти. В рассмотренном примере используется чисто сегментная адресация без деления на страницы, и линейные адреса совпадают с физическими.
Поскольку в дескриптор записывается 32-битовый линейный базовый адрес (номер байта), сегмент в защищенном режиме может начинаться на любом байте, а не только на границе параграфа, и располагаться в любом месте адресного пространства 4 Гбайт.
Поле базы, как и поле границы, разбито на 2 части: биты 0...23 занимают байты 2, 3 и 4 дескриптора, а биты 24...31 – байт 7. Для удобства программного обращения в структуре descr база описывается тремя полями: младшим словом (base_l – строка 10 листинга) и двумя байтами: средним (base_m – строка 11 листинга) и старшим (base_h – строка 15 листинга).
В байте атрибутов 1 задается ряд характеристик сегмента. Не вдаваясь пока в подробности этих характеристик, укажем, что в рассмотренном примере используются сегменты двух типов: сегмент команд, для которого байт attr_1 (строка 12 листинга) должен иметь значение 98h, и сегмент данных (или стека) с кодом 92h.
Некоторые дополнительные характеристики сегмента указываются в старшем полубайте байта attr_2 (в частности, бит дробности). Для всех наших сегментов значение этого полубайта равно 0.
Сегмент данных data (строка 18 листинга), который для удобства изучения функционирования программы расположен в начале программы, до сегмента команд, объявлен с типом использования use16 (так же будет объявлен и сегмент команд). Этот описатель объявляет, что в данном сегменте будут по умолчанию использоваться 16-битовые адреса. Если бы мы готовили нашу программу для работы под управлением операционной системы защищенного режима, реализующей все возможности микропроцессора, тип использования был бы use32. Однако наша программа будет запускаться под управлением DOS, которая работает в реальном режиме с 16-битовыми адресами и операндами.
Сегмент данных начинается с описания важнейшей системной структуры – таблицы глобальных дескрипторов. Как уже отмечалось выше, обращение к сегментам в защищенном режиме возможно только через дескрипторы этих сегментов. Таким образом, в таблице дескрипторов должно быть описано столько дескрипторов, сколько сегментов использует программа. В нашем случае в таблицу включены, помимо обязательного нулевого дескриптора, всегда занимающего первое место в таблице, четыре дескриптора для сегментов данных, команд, стека и дополнительного сегмента данных, который мы наложим на видеобуфер, чтобы обеспечить возможность вывода в него символов. Порядок дескрипторов в таблице (кроме нулевого) не имеет значения.
Помимо единственной таблицы глобальных дескрипторов, обозначаемой GDT от Global Descriptor Table, в памяти может находиться множество таблиц локальных дескрипторов (LDT от Local Descriptor Table). Разница между ними в том, что сегменты, описываемые глобальными дескрипторами, доступны всем задачам, выполняемым процессором, а к сегментам, описываемым локальными дескрипторами, может обращаться только та задача, в которой эти дескрипторы описаны. Поскольку пока мы имеем дело с однозадачным режимом, локальная таблица нам не нужна.
Поля дескрипторов для наглядности заполнены конкретными данными явным образом, хотя объявление структуры descr с нулями во всех полях позволяет описать дескрипторы несколько короче [8], например:
gdt_null descr<>; Селектор 0 – обязательный нулевой дескриптор
gdt_data descr<data_size-1,,,92h>; Селектор 8 – сегмент данных
В дескрипторе gdt_data (строка 23 листинга), описывающем сегмент данных программы, заполняется поле границы сегмента (фактическое значение размера сегмента data_size будет вычислено компилятором, строка 30 листинга), а также байт атрибутов 1. Код 92h говорит о том, что это сегмент данных с разрешением записи и чтения. Базу сегмента, т.е. физический адрес его начала, придется вычислить программно и занести в дескриптор уже на этапе выполнения.
Дескриптор gdt_code (строка 25 листинга) сегмента команд заполняется схожим образом. Код атрибута 98h обозначает, что это исполняемый сегмент, к которому, между прочим, запрещено обращение с целью чтения или записи. Таким образом, сегменты команд в защищенном режиме нельзя модифицировать по ходу выполнения программы.
Дескриптор gdt_stack (строка 27 листинга) сегмента стека имеет, как и любой сегмент данных, код атрибута 92h, что разрешает его чтение и запись, и явным образом заданную границу 255 байтов, что соответствует размеру стека. Базовый адрес сегмента стека так же будет вычислен на этапе выполнения программы.
Последний дескриптор gdt_screen (строка 29 листинга) описывает страницу 0 видеобуфера. Размер видеостраницы, как известно, составляет 4096 байтов, поэтому в поле границы указано число 4095. Базовый физический адрес страницы известен, он равен B8000h. Младшие 16 разрядов базы (число 8000h) заполняют слово base_l дескриптора, биты 16... 19 (число 0bh) – байт base_m. Биты 20...31 базового адреса равны 0, поскольку видеобуфер размещается в первом мегабайте адресного пространства.
Перед переходом в защищенный режим процессору надо будет сообщить физический адрес таблицы глобальных дескрипторов и ее размер (точнее, границу). Размер GDT определяется на этапе трансляции в строке 30.
Назначение оставшихся строк сегмента данных станет ясным в процессе рассмотрения программы.
Сегмент команд text (строка 41 листинга) начинается, как и всегда, оператором segment, в котором указывается тип использования use16, так как мы составляем 16-разрядное приложение. Указание описателя usel6. He запрещает использовать в программе 32-битовые регистры.
Фактически вся программа примера, кроме ее завершающих строк, а также фрагмента, выполняемого в защищенном режиме, посвящена подготовке перехода в защищенный режим. Прежде всего, надо завершить формирование дескрипторов сегментов программы, в которых остались незаполненными базовые адреса сегментов. Базовые (32-битовые) адреса определяются путем умножения значений сегментных адресов на 16. Сначала производится очистка 32-разрядного аккумулятора (строка 44), так как в нем будет сформирован позднее базовый 32-разрядный адрес (фактически эта команда нужна для очистки старшего слова расширенного аккумулятора). После обычной инициализации сегментного регистра DS (строки 45, 46), которая позволит нам обращаться к полям данных программы (в реальном режиме!) выполняется сдвиг на 4 разряда содержимого регистра EАХ. Эта операция выполняется командой shl eax,4. Команда сдвигает влево содержимое 32-разрядного аккумулятора на указанное константой число бит (4). Следующая команда сохраняет получившееся 32-разрядное значение адреса в регистре ebp. После этого в регистр bx помещается смещение дескриптора сегмента кода. Следующими тремя командами (строки 53 – 55) младшее и старшее слова регистра EAX отправляется в поля base_l и base_m дескриптора gdt_data, соответственно. Аналогично вычисляются 32-битовые адреса сегментов команд и стека, помещаемые в дескрипторы gdt_code (строки 57 – 63) и gdt_stack (строки 65 – 71).
Следующий этап подготовки к переходу в защищенный режим – загрузка в регистр процессора GDTR (Global Descriptor Table Register, регистр таблицы глобальных дескрипторов) информации о таблице глобальных дескрипторов. Эта информация включает в себя линейный базовый адрес таблицы и ее границу и размещается в 6 байтах поля данных, называемого псевдодескриптором. Для загрузки GDTR предусмотрена специальная привилегированная команда lgdt (load global descriptor table, загрузка таблицы глобальных дескрипторов), которая требует указания в качестве операнда имени псевдодескриптора. Формат псевдодескриптора приведен на рисунке 1.10.
Рисунок 1.10 – Формат псевдодескриптора
В нашем примере заполнение псевдодескриптора упрощается вследствие того, что таблица глобальных дескрипторов расположена в начале сегмента данных, и ее базовый адрес совпадает с базовым адресом всего сегмента, который уже был вычислен и помещен в дескриптор gdt_data. В строках 73, 74 базовый адрес и граница помещаются в требуемые поля pdescr, а в строке 75 командой lgdt загружается регистр GDTR, сообщая, таким образом, процессору о местонахождение и размер GDT.
В принципе теперь можно перейти в защищенный режим. Однако мы запускаем нашу программу под управлением DOS (а как ее еще можно запустить?) и естественно завершить ее также обычным образом, чтобы не нарушить работоспособность системы. Но в защищенном режиме запрещены любые обращения к функциям DOS или BIOS. Причина этого совершенно очевидна – и DOS, и BIOS являются программами реального режима, в которых широко используется сегментная адресация реального режима, т.е. загрузка в сегментные регистры сегментных адресов. В защищенном же режиме в сегментные регистры загружаются не сегментные адреса, а селекторы. Кроме того, обращение к функциям DOS и BIOS осуществляется с помощью команд int с определенными номерами, а в защищенном режиме эти команды приведут к совершенно другим результатам. Следовательно, программу, работающую в защищенном режиме, нельзя завершить средствами DOS. Сначала ее надо вернуть в реальный режим.
Возврат в реальный режим можно осуществить сбросом процессора. Действия процессора после сброса определяются одной из ячеек КМОП-микросхемы – байтом состояния отключения, располагаемым по адресу Fh. В частности, если в этом байте записан код Ah, после сброса управление немедленно передается по адресу, который извлекается из двухсловной ячейки 40h:67h, расположенной в области данных BIOS. Таким образом, для подготовки возврата в реальный режим необходимо в ячейку 40h:67h записать адрес возврата, а в байт Fh КМОП-микросхемы занести код Ah. Приведенный способ возврата в реальный режим использовался в процессорах i286 (так как другого способа возврата в нем предусмотрено не было).
В процессорах, начиная с i386, переход из реального режима в защищенный и обратно может осуществляться с использованием управляющего регистра CR0. Так как встретить сейчас «живой» 286 процессор практически не реально, воспользуемся именно таким способом.
Всего в микропроцессорах i386 и выше имеется 4 программно адресуемых управляющих регистра CR0, CR1, CR2 и CR3 [8]. Регистр CR1 зарезервирован, регистры CR2 и CR3 управляют страничным преобразованием, которое в рассматриваемой программе не используется, а регистр CR0 содержит набор управляющих битов, из которых нам интересны биты 0 (включение и выключение защищенного режима) и 31 (разрешение страничного преобразования).
После сброса процессора оба эти бита сброшены, благодаря чему процессор начинает работать в реальном режиме с выключенным страничным преобразованием. Установка младшего бита CR0 в 1 переводит процессор в защищенный режим, а сброс его возвращает процессор в реальный режим. Следует отметить, что младшая половина регистра CR0 совпадает со словом состояния 286 процессора, поэтому команды чтения и записи в регистр CR0 аналогичны по результату командам smsw и lmsw, которые сохранены в старших процессорах из соображений совместимости.
Еще один важный шаг, который необходимо выполнить перед переходом в защищенный режим, заключается в запрете всех аппаратных прерываний. Дело в том, что в защищенном режиме процессор выполняет процедуру обработки прерывания иначе, чем в реальном. При поступлении сигнала прерывания процессор не обращается к таблице векторов прерываний в первом килобайте памяти, как в реальном режиме, а извлекает адрес программы обработки прерывания из таблицы дескрипторов прерываний, построенной аналогично таблице глобальных дескрипторов и располагаемой в программе пользователя (или в операционной системе). В нашем примере такой таблицы нет, и на время работы программы прерывания придется запретить. Запрет всех аппаратных прерываний осуществляется командой cli (строка 77).
В строках 78, 79 в порт 70h засылается код 80h, который запрещает немаскируемые прерывания (которые не запрещаются командой cli).
В строках 81...83 осуществляется перевод процессора в защищенный режим. Этот перевод можно выполнить различными способами. В рассматриваемом примере для этого используется команда mov. Сначала содержимое управляющего CR0 регистра считывается в аккумулятор EAX, затем его младший бит устанавливается в 1 с помощью команды or, затем содержимое аккумулятора опять записывается в управляющий регистр CR0 с уже модифицированным младшим битом. Все последующие команды выполняются уже в защищенном режиме.
Хотя защищенный режим установлен, однако действия по настройке системы еще не закончены. Действительно, во всех используемых в программе сегментных регистрах хранятся не селекторы дескрипторов сегментов, а базовые сегментные адреса, не имеющие смысла в защищенном режиме.
Рисунок 1.11 – Сегментные и теневые регистры
Отсюда можно сделать вывод, что после перехода в защищенный режим программа не должна работать, так как в регистре CS пока еще нет селектора сегмента команд, и процессор не может обращаться к этому сегменту. В действительности это не совсем так.
В процессоре для каждого из сегментных регистров имеется так называемый теневой регистр дескриптора, который имеет формат дескриптора (рисунок 1.11). Теневые регистры недоступны программисту. Они автоматически загружаются процессором из таблицы дескрипторов каждый раз, когда процессор загружает соответствующий сегментный регистр. Таким образом, в защищенном режиме программист имеет дело с селекторами, т.е. номерами дескрипторов, а процессор – с самими дескрипторами, хранящимися в теневых регистрах. Именно содержимое теневого регистра (в первую очередь, линейный адрес сегмента) определяет область памяти, к которой обращается процессор при выполнении конкретной команды.
В реальном режиме теневые регистры заполняются не из таблицы дескрипторов, а непосредственно самим процессором. В частности, процессор заполняет поле базы каждого теневого регистра линейным базовым адресом сегмента, полученным путем умножения на 16 содержимого сегментного регистра, как это и положено в реальном режиме. Поэтому после перехода в защищенный режим в теневых регистрах находятся правильные линейные базовые адреса, и программа будет выполняться правильно, хотя с точки зрения правил адресации защищенного режима содержимое сегментных регистров лишено смысла.
Тем не менее, после перехода в защищенный режим, прежде всего, следует загрузить в используемые сегментные регистры (и, в частности, в регистр CS) селекторы соответствующих сегментов. Это позволит процессору правильно заполнить все поля теневых регистров из таблицы дескрипторов. Пока эта операция не выполнена, некоторые поля теневых регистров (в частности, границы сегментов) могут содержать неверную информацию.
Загрузить селекторы в сегментные регистры DS, SS и ES не представляет труда (строки 93 – 101). Но как загрузить селектор в регистр CS? Для этого можно воспользоваться искусственно сконструированной командой дальнего перехода, которая, как известно, приводит к смене содержимого и IP, и CS. Строки 89 – 91 демонстрируют эту методику. В реальном режиме мы поместили бы во второе слово адреса сегментный адрес сегмента команд, в защищенном же мы записываем в него селектор этого сегмента (число 16).
Команда дальнего перехода, помимо загрузки в CS селектора, выполняет еще одну функцию – она очищает очередь команд в блоке предвыборки команд процессора. Как известно, в современных процессорах с целью повышения скорости выполнения программы используется конвейерная обработка команд программы, позволяющая совместить во времени фазы их обработки. Одновременно с выполнением текущей (первой) команды осуществляется выборка операндов следующей (второй), дешифрация третьей и выборка из памяти четвертой команды. Таким образом, в момент перехода в защищенный режим уже могут быть расшифрованы несколько следующих команд и выбраны из памяти их операнды. Однако эти действия выполнялись, очевидно, по правилам реального, а не защищенного режима, что может привести к нарушениям в работе программы. Команда перехода очищает очередь предвыборки, заставляя процессор заполнить ее заново уже в защищенном режиме.
Следующий фрагмент примера программы (строки 100 – 113) является чисто иллюстративным. В нем инициализируется (по правилам защищенного режима!) сегментный регистр ES и в видеобуфер выводится сообщение 'A message in protected mode' (зеленым цветом на синем фоне), так, что оно располагается в середине пятой строки экрана, чем подтверждается правильное функционирование программы в защищенном режиме.
Как уже отмечалось выше, для того, чтобы не нарушить работоспособность DOS, процессор следует вернуть в реальный режим, после чего можно будет завершить программу обычным образом. Перейти в реальный режим можно разными способами. Можно, например, осуществить сброс процессора, заслав команду FEh в порт 64h контроллера клавиатуры. Эта команда возбуждает сигнал на одном из выводов контроллера клавиатуры, который, в конечном счете, приводит к появлению сигнала сброса на выводе RESET микропроцессора. Этот способ (единственный для 286 процессора) неудобен тем, что для выполнения сброса необходимо несколько микросекунд. Если выполнять таким образом переход в реальный режим достаточно часто, можно потерять много времени.
В нашем примере переход в реальный режим осуществляется простым сбросом младшего бита в управляющем регистре CR0.
Если просто перейти в реальный режим сбросом бита 0 в регистре CR0, в теневых регистрах останутся дескрипторы защищенного режима, и при первом же обращении к любому сегменту программы возникнет исключение общей защиты, так как ни один из определенных ранее сегментов не имеет границы FFFh. Так как обработка исключений не производится, произойдет сброс процессора и перезагрузка компьютера, так как в данном случае не настраивался байт состояния перезагрузки, и не заполнялись соответствующие ячейки области данных BIOS. Следовательно, перед переходом в реальный режим необходимо исправить дескрипторы всех используемых сегментов: кодов, данных, стека и видеобуфера. Сегментные регистры FS и GS не использовались, поэтому о них можно не заботиться.
В строках 118 – 121 в поля границ всех четырех дескрипторов записывается значение FFFFh, а в строках 124 – 129 выполняется загрузка селекторов в сегментные регистры, что приводит к перезаписи содержимого теневых регистров. Так как сегментный регистр CS программно недоступен, его загрузку приходится опять выполнять с помощью искусственно сформированной команды дальнего перехода (строки 133 – 135).
После настройки всех использованных в защищенном режиме сегментных регистров, можно сбрасывать бит 0 управляющего регистра CR0 (строки 137 – 139). После перехода в реальный режим опять надо выполнить искусственно сформированную команды дальнего перехода для того, чтобы очистить очередь команд в блоке предвыборки процессора и загрузить в регистр CS вместо хранящегося там селектора обычный сегментный адрес регистра команд (строки 140 – 142).
Искусственно сформированная команда дальнего перехода передает управление на метку return.
Теперь процессора опять работает в реальном режиме. При этом, хотя в сегментных регистрах DS, ES и SS остались недействительные для реального режима селекторы, программа пока работает корректно, так как в теневых регистрах находятся правильные линейные адреса (оставшиеся от защищенного режима) и законные для реального режима границы (загруженные в строках 118 – 121). Однако, если в программе встретится любая команда сохранения или восстановления какого-либо сегментного регистра, нормальное выполнение программы нарушится, так как в сегментном регистре окажется не сегментный адрес, как это должно быть в реальном режиме, а селектор. Это значение будет трактоваться процессором как сегментный адрес, что приведет в дальнейшем к неверной адресации соответствующего сегмента.
Если даже в оставшемся тексте программы и нет команд чтения или записи сегментных регистров, неприятности все равно возникнут, так как простой вызов DOS'овского прерывания int 21h приведет к этим неприятностям, так как диспетчер DOS выполняет сохранение и восстановление регистров (в том числе и сегментных) при выполнении функций DOS.
Поэтому после перехода в реальный режим необходимо загрузить в используемые далее сегментные регистры соответствующие сегментные адреса. В строках 148 – 151 в регистры DS и SS записываются сегментные адреса соответствующих сегментов.
При рассмотренном варианте возврата в реальный режим (без сброса процессора) не надо сохранять кадр стека, так как содержимое регистра SP в этом случае не разрушается, а регистр SS мы уже инициализировали.
Для восстановления работоспособности системы следует также разрешить прерывания (маскируемые – строка 155, немаскируемые – строки 156, 157), после чего программа может продолжаться уже в реальном режиме. В рассмотренном примере для проверки работоспособности системы в этом режиме на экран выводится сообщение ‘ Real mode now ’ с помощью функции DOS 09h. Для наглядности в сообщение включены Esc последовательности для смены цвета символов и фона (красные символы на зеленом фоне). Осуществлена смена цвета символов и фона может быть лишь в том случае, если в DOS установлен драйвер ANSI.SYS. Если после перехода в реальный режим при установленном драйвере ANSI.SYS сообщение будет выведено без изменения цветов, это может говорить об ошибках защищенного режима.
Перед завершением программы, она ожидает ввода с клавиатуры (функция 0 прерывания int 16h), чтобы можно было успеть увидеть содержимое экрана.
Программа завершается обычным образом функцией DOS 4Ch. Нормальное завершение программы и переход в DOS тоже в какой-то мере свидетельствует о ее правильности.
1.3 Вопросы для самопроверки
1. Какие режимы работы поддерживают 32-разрядные процессоры х86?
2. Какие регистры в 32-разрядных микропроцессорах х86 являются 16-разрядными?
3. Какие новые флаги добавились у 32-разрядных микропроцессоров х86?
4. Какие разряды управляющего регистра CR0 микропроцессора указывают состояние и режимы работы процессора?
5. Что такое бит страничного преобразования?
6. Что такое бит сопроцессора?
7. Для чего нужен бит переключения задачи?
8. Что такое бит эмуляции сопроцессора?
9. Для чего нужен бит присутствия сопроцессора?
10. Что такое бит разрешения защиты?
11. Какие регистры микропроцессора используются для поддержки страничного преобразования?
12. Что такое регистры системных адресов?
13. Для чего нужен регистр таблицы глобальных дескрипторов?
14. Для чего нужен регистр таблицы дескрипторов прерываний?
15. Для чего нужен регистр таблицы локальных дескрипторов?
16. Для чего нужен регистр состояния задачи?
17. Какие действия надо выполнить, чтобы в компилируемой программе можно было использовать 32-разрядные операнды?
18. Какие команды появились в 32-разрядных микропроцессорах (примеры)?
19. Каково главное ограничение реального режима работы процессора?
20. Какие дополнительные возможности появляются в защищенном режиме работы микропроцессора?
2 Использование 32-разрядной адресации в реальном режиме
Большое количество процессоров, используемых в настоящее время, ставит перед программистами проблемы оптимального использования ресурсов конкретного процессора в своих разработках. У изготовителей микропроцессоров стало традицией публиковать описания регистров и команд через Интернет в виде pdf-файлов, но не давать при этом рекомендаций по их применению. Хорошо, если из названия (или описания) можно сделать совершенно определенные выводы о назначении команды или регистра. А если нет?
Столь же вредная традиция — не описывать в общедоступной документации режимы работы, которых современные процессоры имеют великое множество. Безусловным чемпионом в этой области является Intel — значительная часть потенциальных возможностей процессоров класса Pentium и последующих модификаций не используется потребителями, поскольку эти возможности в документации только упоминаются, но не рассматриваются. Программистам приходится искать наработки энтузиастов, которые тратят свое время на углубленное исследование режимов работы процессоров и применения конкретных, плохо описанных изготовителями, команд и регистров процессора [1].
2.1 Линейная адресация данных в реальном режиме DOS
В литературе по программированию описано три режима работы микропроцессоров серии 80x86 — реальный режим (режим совместимости с архитектурой 8086), защищенный режим и режим виртуальных процессоров 8086 (являющийся неким подвидом защищенного режима).
Основной недостаток реального режима состоит в том, что адресное пространство имеет размер всего в 1 Мбайт и при этом сегментировано — «нарезано» на кусочки размером по 64 Кбайт. Одного мегабайта очень мало для современных ресурсоемких прикладных программ (текстовых и графических редакторов, геоинформационных систем, систем проектирования и т. д.), а сегментация не позволяет нормально работать с видеопамятью и большими массивами данных.
Что можно сказать о защищенном и виртуальном режимах? Многие книги и учебники по микропроцессорам Intel заканчиваются главой «Переход в защищенный режим». Недостаток этого режима — необходимость заново создавать программное обеспечение для работы с периферийными устройствами на низком уровне, то есть фактически полностью переписывать все основные функции DOS. Можно, конечно, использовать Windows, но эта операционная система предназначена для офисных целей и плохо адаптируется к решению задач оперативного управления техническими системами. Кроме того, Windows забирает для собственных нужд изрядную часть ресурсов компьютера и ограничивает доступ к периферийным устройствам.
В некоторых случаях универсальные многозадачные операционные системы типа Windows и Unix неприменимы по причинам, не относящимся напрямую к области вычислительной техники. Первая причина — лицензионные соглашения между изготовителями и потребителями программ. Прочтите внимательно любую лицензию: разработчик программы не несет ответственности ни за что. Следовательно, за все сбои и неисправности расплачивается потребитель. Например, за
аварию в системе управления транспортом разработчикам этой системы придется отвечать по статьям Уголовного кодекса. Что касается систем военного назначения, то вообще сомнительно, что на таких лицензионных условиях какая-либо программа может быть официально принята в эксплуатацию на территории России.
Вторая причина — огромный объем универсальных операционных систем — десятки миллионов строк на языках высокого уровня! Полностью протестировать такие системы невозможно — у фирмы Microsoft, например, хватает сил только на доскональную проверку небольшого ядра Windows! Тем более на это не способен потребитель, у которого нет всей документации. Даже в случае открытой системы типа Linux, если документация есть и все исходные коды доступны — попробуйте доказать военным или банкирам, что в системе нет скрытых ловушек и «черного хода»!
Создать собственную программу для переключения в защищенный режим и работы в нем — непростая задача. При работе с аппаратурой в защищенном режиме программист должен четко понимать, какими возможностями аппаратуры пользоваться опасно. Например, приводимые в учебниках примеры программ для защищенного режима часто проявляют несовместимость с определенными конфигурациями оборудования, поскольку их авторы не имели достаточно широкой лабораторной базы для тестирования. Дело в том, что периферийные устройства всегда имеют какие-нибудь нестандартные особенности, добавляемые их изготовителями в рекламных целях. При работе в реальном режиме DOS такие особенности не применяются и потому никак не проявляются. Однако они могут показать себя с самой неприятной стороны при переключении в защищенный режим, когда программисту приходится перенастраивать периферийные устройства на новую модель организации оперативной памяти, перезаписывая при этом множество различных регистров аппаратуры. Возможно две ситуации: либо в стандартных регистрах некоторые разряды применяются нестандартным образом, но программисту об этом ничего не известно, либо вообще имеются какие-либо дополнительные регистры, не описанные в документации, но влияющие на режим работы системы. Возникает абсурдная ситуация: простой (реальный) режим работы задается процедурами BIOS фирмы-изготовителя системной платы, которая обычно хорошо осведомлена об особенностях применяемого на этой плате чипсета, а программы для перехода в защищенный режим вынуждены писать совершенно посторонние люди, не располагающие документацией в полном объеме. В BIOS включено некоторое количество процедур для работы в защищенном режиме, но они охватывают лишь часть необходимых операций.
Вообще говоря, изобилие управляющих регистров в современных персональных компьютерах (их общее количество достигает нескольких тысяч) — явление совершенно ненормальное, теоретически приводящее к увеличению количества возможных режимов работы до бесконечности. Поскольку протестировать функционирование системы в миллиардах различных режимов технически невозможно, разработчики программного обеспечения не могут использовать дополнительные средства, и ограничены несколькими общепринятыми (стандартными) режимами. Чтобы убедиться в этом, достаточно сравнить полный набор команд любого периферийного устройства с реально используемым (например, в BIOS) подмножеством команд данного набора. Большая часть регистров в настоящее время в принципе не нужна — установкой режима работы периферийного устройства должен заниматься его встроенный специализированный процессор, а не центральный процессор компьютера. Однако переход на новые технологии произойдет, вероятно, только после очередного кризиса в развитии компьютерной индустрии, а пока что приходится приспосабливаться к сложившейся ситуации.
Изложенные выше причины приводят к тому, что программисты вынуждены искать различные обходные пути. Один из возможных приемов — использование линейной адресации памяти. Линейная адресация — это наиболее простой, с точки зрения программиста, способ работы непосредственно с аппаратурой ЭВМ (логические адреса при этом совпадают с физическими). Различия в организации памяти в реальном, защищенном и линейном режимах работы процессора иллюстрирует рисунок 2.1.
Рисунок 2.1 – Организация адресного пространства в реальном, защищенном и линейном режимах работы процессора х86
Линейную адресацию можно использовать в специализированных программах, активно эксплуатирующих ресурсы ЭВМ — как в компьютерных играх, так и в системах автоматики, измерительных системах, системах управления, связи и т. п. Применение линейной адресации целесообразно в том случае, если проектируемая система предназначена для выполнения ограниченного, заранее известного набора функций и требует высокого быстродействия и надежности. Разработчики процессоров начали внедрять линейную адресацию (в качестве одного из возможных режимов работы) при переходе с 16-разрядной архитектуры на 32-разрядную. Фирма Intel ввела такой режим в процессоре 80386, после чего он стал фактически стандартным (поддерживается не только всеми последующими моделями, но и всеми клонами архитектуры х86), однако остался недокументированным (почти не описан в литературе и не рассматривается в фирменном руководстве по программированию).
Для пользователей обычных персональных компьютеров линейная адресация в чистом виде интереса не представляет по тем же причинам, что и защищенный режим: DOS и BIOS функционируют только в реальном режиме с 64-килобайт-ными сегментами, и при переходе в любой другой режим программист оказывается один на один с аппаратурой ЭВМ — без документации. Однако кроме чистых режимов процессоры Intel способны работать и в режимах гибридных. Еще в 1989 году Томас Роден (Thomas Roden) предложил использовать интересную комбинацию сегментной (для кода и данных) и линейной (только для данных) адресации [2]. Предложенный им метод позволяет, находясь в обычном режиме DOS, работать со всей доступной памятью в пределах четырехгигабайтного адресного пространства процессора Intel 80386. Чтобы включить режим линейной адресации данных, необходимо снять ограничения на размер сегмента в теневом регистре, соответствующем одному из дополнительных сегментных регистров FS или G5 (при необходимости описание архитектуры процессора Pentium можно найти в документации [3-5], размещенной в Интернете на сервере Intel для разработчиков). Через избранный регистр можно обращаться к любой области памяти с помощью прямой адресации или используя в качестве индексного любой 32-разрядный регистр общего назначения. После снятия ограничения запись в выделенный для линейной адресации сегментный регистр выполнять нельзя, иначе нарушится информация в соответствующем ему теневом регистре (предел сегмента сохранится, но начальный адрес будет перезаписан новым значением). Однако стандартные компиляторы и функции DOS с регистрами FS и GS не работают, и соответственно, при вызове процедур эти регистры можно вообще «не трогать» — их не нужно сохранять и восстанавливать. Достаточно один раз снять ограничение на размер адресного пространства, и после выхода из программы (до перезагрузки компьютера) линейную адресацию можно будет использовать из любой другой программы DOS, как поступил в своем примере Томас Роден.
Рассмотрим более подробно процедуру переключения одного из дополнительных сегментных регистров в режим линейной адресации. Каждый сегментный регистр, как указано в документации [5], состоит из видимой и невидимой (теневой) частей. Информацию в видимую часть можно записывать напрямую при помощи обычных команд пересылки данных (M0V и др.), а для записи в невидимую часть применяются специальные команды, которые доступны только в защищенном режиме. Теневая часть представляет собой так называемый дескриптор (описатель) сегмента, длина которого равна 8 байтам.
При переходе от 16-разрядной архитектуры к 32-разрядной (то есть от i286 к i386) разработчики нового процессора попытались сохранить совместимость снизу вверх по структуре системных регистров, в результате чего дескрипторы сегментов приобрели довольно уродливый (с точки зрения технической эстетики) вид — поля предела и базового адреса разделены на несколько частей. Кроме того, поле предела оказалось ограничено 20 разрядами, что вынудило разработчиков применить еще один радиолюбительский трюк — ввести бит гранулярности G, чтобы можно было задавать размер сегмента, превышающий 16 Мбайт.
Рисунок 2.2 – Формат дескриптора сегмента
Формат дескриптора сегмента показан на рисунке 2.2. Дескриптор состоит из следующих полей.
- Базовый адрес — 32-разрядное поле, задающее начальный адрес сегмента (в линейном адресном пространстве).
- Предел сегмента — 20-разрядное поле, которое определяет размер сегмента в байтах или 4-килобайтных страницах (в зависимости от значения бита гранулярности G). Поле предела содержит значение, которое должно быть на единицу меньше реального размера сегмента в байтах или страницах.
- Тип — 4-разрядное поле, определяющее тип сегмента и типы операций, которые допустимо с ним выполнять.
- Бит S — признак системного объекта (0 — дескриптор описывает системный объект, 1 — назначение сегмента описывается полем типа).
- DPL — 2-разрядное поле, определяющее уровень привилегий описываемого дескриптором сегмента.
- Бит Р — признак присутствия сегмента в оперативной памяти компьютера (0 — сегмент «сброшен» на диск, 1 — сегмент присутствует в оперативной памяти).
- Бит AVL — свободный (available) бит, который может использоваться по усмотрению системного программиста.
- Бит D — признак используемого по умолчанию режима адресации данных (0 — 16-разрядная адресация, 1 — 32-разрядная).
- Бит G — гранулярности сегмента (0 — поле предела задает размер сегмента в байтах, 1 — в 4-килобайтных страницах).
В нашем случае признак используемого по умолчанию режима адресации данных D можно установить в 0 (использовать по умолчанию 16-разрядные операнды), но особой роли его значение не играет — в смешанном режиме сегментно-линейной адресации при работе с линейным сегментом строковые команды, использующие значение этого разряда, применять нельзя. Бит гранулярности G должен быть установлен в 1, чтобы обеспечить охват всего адресного пространства процессора.
Рисунок 2.3 – Формат прав доступа для сегмента данных
Для сегментов данных формат байта прав доступа (включающего поле типа сегмента) имеет вид, показанный на рисунке 2.3. Как видно из рисунка, поле S для сегментов данных должно быть установлено в 1, а старший разряд поля типа должен иметь значение 0. Поля Р и DPL уже упоминались выше. Бит присутствия сегмента Р следует установить в 1 (сегмент присутствует в памяти), а в поле DPL нужно установить максимальный уровень привилегий (значение 00). Бит расширения вниз ED для сегментов данных имеет значение 0 (в отличие от стековых сегментов, для которых ED=1). Бит разрешения записи W следует установить в единицу, чтобы можно было не только считывать, но и записывать информацию в сегмент. Бит А фиксирует обращение к сегменту и автоматически устанавливается в единицу всякий раз, когда процессор производит операции считывания или записи с сегментом, описываемым данным дескриптором. При инициализации регистра бит А можно сбросить в 0.
Рисунок 2.4 – Формат управляющего регистра CR0
Осуществить загрузку теневых регистров можно только в защищенном режиме. Для переключения режимов работы процессора используется регистр управления CR0, формат которого показан на рисунке 2.4 [7]. Регистр CR0 содержит флаги, отражающие состояние процессора и управляющие режимами его работы. Назначение флагов следующее.
- РЕ (Protection Enable) —разрешение защиты Установка этого флага инструкцией LMSW или LOAD CR0 переводит процессор в защищенный режим. Сброс флага (возврат в реальный режим) возможет только по инструкции LOAD CR0. Сброс бита РЕ является частью довольно длинной последовательности инструкций, подготавливающих корректное переключение в реальный режим.
- МР (Monitor Processor Extension) — мониторинг математического сопроцессора. Позволяет вызывать исключение #NM по каждой команде WAIT при TS=1. При выполнении программ для процессоров 286/287 и 386/387 на процессорах 486 DX и старше бит MP должен быть установлен.
- ЕМ (Processor Extension Emulated) — эмуляция математического сопроцессора. Установка этого флага вызывает исключение #NM при каждой команде, относящейся к сопроцессору, что позволяет прозрачно осуществлять его программную эмуляцию.
- TS (Task Switched) — признак переключения задачи (флаг устанавливается в 1 при каждом переключении задач и проверяется перед выполнением команд математического сопроцессора).
- ЕТ (Extension Туре) — индикатор поддержки набора инструкций математического сопроцессора (0 — выключена, 1 — включена). В процессорах Р6 флаг всегда установлен в 1.
- NE (Numeric Error) — встроенный механизм контроля ошибок математического сопроцессора (0 — выключен, 1 —включен).
- WP (Write Protect) — защита от записи информации в страницы уровня пользователя из процедур супервизора (0 — выключена, 1 — включена).
- АН (Alignment Mask) — разрешение контроля выравнивания (контроль выравнивания выполняется только на уровне привилегий 3 при AM =1 и флаге AC =1. (0 — запрещен, 1 — разрешен).
- NW (Not Writethrough) — запрещение сквозной записи и циклов аннулирования. (0 — сквозная запись разрешена, 1 — запрещена).
- CD (Cache Disable) — запрет заполнения кэш-памяти (попадание в ранее заполненные строки при этом обслуживаются кэшем). (0 — использование кэш-памяти разрешено, 1 — запрещено).
- PG (Paging) — включение страничного преобразования памяти (0 — запрещено, 1 — разрешено).
Набор подпрограмм, необходимых для переключения сегментного регистра GS в режим линейной адресации, показан в листинге 2.1 [1]. Как сказано выше, перезапись содержимого теневого регистра процессора возможна только в защищенном режиме, а переход в этот режим, как видно из листинга, требует ряда дополнительных операций, выполняемых процедурой Initialization. В частности, нужно перенастроить на специально выделенные в кодовом сегменте области данных регистры DS, SS и SP. В момент перенастройки регистров стека должны быть запрещены прерывания, поскольку некоторые обработчики прерываний пишут информацию в стек прерываемой программы.
Процедура SetLAddrModeForGS, непосредственно осуществляющая перенастройку регистра GS в режим линейной адресации, воспроизводит (с незначительными изменениями) метод Родена. Прежде чем осуществить переключение, нужно вначале подготовить таблицу GDT (настроить на текущие сегменты кода и данных) и загрузить ее. Затем нужно войти в защищенный режим — установить в единицу бит РЕ регистра CR0, а остальные разряды сохранить без изменений (в том виде, в котором они находились при работе в реальном режиме). В защищенном режиме необходимо перезагрузить сегментные регистры, сняв при этом ограничения с GS, и сразу же вернуться в реальный режим DOS, сбросив в ноль бит РЕ. Длительное пребывание в защищенном режиме нежелательно, поскольку переключение в него выполнялось по упрощенной схеме: таблица прерываний не создавалась, а сами прерывания были просто отключены.
После выполнения процедуры SetLAddrModeForGS обязательно следует отменить замыкание адресного пространства, то есть разблокировать адресную линию А20, которая управляется контроллером клавиатуры. Для этого необходимо послать в порт А контроллера соответствующую команду. Посылка команды осуществляется при помощи Enable_A20 и Wait8042Buffer Empty.
Листинг 2.1 – Подпрограмма, устанавливающая режим линейной адресации данных
; Порт, управляющий запретом немаскируемых прерываний