Assembler - язык неограниченных возможностей

         

Ассемблер и языки высокого уровня


В предыдущем разделе, занимаясь программированием для Windows, мы уже обращались к процедурам, написанным на языке высокого уровня из программ на ассемблере, и создавали процедуры на ассемблере, к которым можно обращаться из языков высокого уровня. Для этого нужно было соблюдать определенные договоренности о передаче параметров — параметры помещались в стек справа налево, результат возвращался в ЕАХ, стек освобождался от переданных параметров самой процедурой. Эта договоренность, известная как STDCALL, конечно, не единственная, и разные языки высокого уровня используют разнообразные способы передачи параметров.



Искажение имен


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

some_proc();

то реально компилятор пишет

call _some_proc

и это означает, что, если эта процедура написана на ассемблере, она должна называться именно _some_proc (или использовать сложную форму записи директивы proc).

Названия процедур, использующих STDCALL, как можно было видеть из примера DLL-программы в разделе 7.4, искажаются еще более сложным образом: спереди к называнию процедуры добавляется символ подчеркивания, а сзади — символ @ и размер занимаемой параметрами области стека в байтах, (то есть в точности число, стоящее после команды ret в конце процедуры).

some_proc(a:word);

превращается в

push a call _some_proc@4



Конвенция Pascal


Самый очевидный способ выражения вызова процедуры или функции языка высокого уровня, после того как решено, что параметры передаются в стеке и возвращаются в регистре АХ/ЕАХ, — это способ, принятый в языке PASCAL (а также в BASIC, FORTRAN, ADA, OBERON, MODULA2), — просто поместить параметры в стек в естественном порядке. В этом случае запись

some_proc(a,b,c,d,e)

превращается в

push a push b push с push d push e call some_proc

Это значит, что процедура some_proc, во-первых, должна очистить стек по окончании работы (например, завершившись командой ret 10) и, во-вторых, параметры, переданные ей, находятся в стеке в обратном порядке:

some_proc proc push bp mov bp,sp ; создать стековый кадр a equ [bp+12] ; определения для простого ; доступа к параметрам b equ [bp+10] c equ [bp+8] d equ [bp+6] e equ [bp+4]

; текст процедуры, использующей параметры а, Ь, с, d, e

ret 10 some_proc endp

Этот код в точности соответствует усложненной форме директивы proc, которую поддерживают все современные ассемблеры:

some_proc proc PASCAL,а:word,b:word,с:word,d:word,e:word

; текст процедуры, использующей параметры а, Ь, с, d, e. ; Так как ВР используется в качестве указателя стекового кадра, ; его использовать нельзя!



ret ; эта команда RET будет заменена на RET 10 some_proc endp

Главный недостаток этого подхода — сложность создания функции с изменяемым числом параметров, аналогичных функции языка С printf. Чтобы определить число параметров, переданных printf, процедура должна сначала прочитать первый параметр, но она не знает его расположения в стеке. Эту проблему решает подход, используемый в С, где параметры передаются в обратном порядке.



Конвенция С


Этот способ передачи параметров используется в первую очередь в языках С и C++, а также в PROLOG и других. Параметры помещаются в стек в обратном порядке, и, в противоположность PASCAL-конвенции, удаление параметров из стека выполняет вызывающая процедура. Запись

some_proc(a,b,c,d,e)

превращается в

push e push d push с push b push a call some_proc add sp,10 ; освободить стек

Вызванная таким образом процедура может инициализироваться так: some_proc proc push bp mov bp,sp ; создать стековый кадр a equ [bp+4] ; определения для простого доступа к параметрам b equ [bp+6] с equ [bp+8] d equ [bp+10] e equ [bp+12]

; текст процедуры, использующей параметры a, b, с, d, e

pop bp ret some_proc endp

Ассемблеры поддерживают и такой формат вызова при помощи усложненной формы директивы proc с указанием языка С:

some_proc proc С,а:word,b:word,с:word,d:word,e:word

; текст процедуры, использующей параметры a, b, с, d, e. ; Так как BP применяется как указатель стекового кадра, ; его использовать нельзя!

ret some_proc endp

Мы не пользовались до сих пор этими формами записи процедур в ассемблере потому, что они скрывают от нас тот факт, что регистр ВР используется для хранения параметров и его ни в коем случае нельзя изменять, и, в случае PASCAL, что команда ret на самом деле — команда ret N.

Преимущество по сравнению с PASCAL-конвенцией заключается в том, что освобождение стека от параметров в С возлагается на вызывающую процедуру, что позволяет лучше оптимизировать код программы. Например, если мы должны вызвать несколько функций, принимающих одни и те же параметры подряд, можно не заполнять стек каждый раз заново:

push param2 push param1 call proc1 call proc2 add sp,4

эквивалентно

proc1(param1,param2); proc2(param1,param2);

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



Передача параметров


Большинство языков высокого уровня передают параметры вызываемой процедуре в стеке и ожидают возвращения параметров в регистре АХ (ЕАХ) (иногда используется DX:AX (EDX:EAX), если результат не умещается в одном регистре, и ST(0), если результат число с плавающей запятой).



Смешанные конвенции


В главе 7 мы встречались с договоренностью о передаче параметров STDCALL, отличавшейся и от С, и от PASCAL-конвенций, которая применяется для всех системных функций Win32 API. Здесь параметры помещаются в стек в обратном порядке, как в С, но процедуры должны очищать стек сами, как в PASCAL.

Еще одно интересное отклонение от С-конвенции можно наблюдать в Watcom С. Этот компилятор активно использует регистры для ускорения работы программы, и параметры в функции также передаются по возможности через регистры. Например, при вызове функции с шестью параметрами

some_proc(a,b,с,d,e,f);

первые четыре параметра передаются соответственно в (Е)АХ, (E)DX, (Е)ВХ, (Е)СХ, а только начиная с пятого, параметры помещают в стек в обычном обратном порядке:

e equ [bp+4] f equ [bp+6]



Встроенный ассемблер


Если требуется выполнить совсем небольшую операцию на ассемблере, например вызвать какое-то прерывание или преобразовать сложную битовую структуру, часто нерационально создавать отдельный файл ради нескольких строк на ассемблере. Чтобы этого избежать, многие языки высокого уровня поддерживают возможность вставки ассемблерного кода непосредственно в программу. Например, напишем процедуру, возвращающую слово, находящееся по адресу 0040h:006Ch, в BIOS — счетчик сигналов системного таймера, который удобно использовать для инициализации генераторов случайных чисел.



Встроенный ассемблер в Pascal


function get_seed:longint var seed:longint begin asm push es mov ax,0040h mov es,ax mov ax,es:[006Ch] mov seed,ax pop es end; get_seed:=seed; end;



Встроенный ассемблер в С


int get_seed() int seed; { _asm { push es mov ax,0040h mov es,ax mov ax,es:[006Ch] mov seed,ax pop es }; return(seed); };

В этих ситуациях ассемблерная программа может свободно пользоваться переменными из языка высокого уровня, так как они автоматически преобразуются в соответствующие выражения типа word ptr [bp+4].



AGI


AGI— это ситуация, при которой регистр, используемый командой для генерации адреса как базовый или индексный, был приемником предыдущей команды. В этой ситуации процессор тратит один дополнительный такт. Последовательность команд

add edx,4 mov esi,[edx]

выполняется с AGI на любом процессоре.

Последовательность команд

add esi,4 ; U-конвейер - 1 такт (на Pentium) pop ebx ; V-конвейер - 1 такт inc ebx ; V-конвейер - 1 такт mov edi,[esi] ; в U-конвейер - *AGI*, затем 1 такт

выполняется с AGI на Pentium за три такта процессора.

Кроме того, AGI может происходить неявно, например при изменении регистра ESP и обращении к стеку:

sub esp,24 push ebx ; *AGI*

или

mov esp,ebp pop ebp ; *AGI*

но изменение ESP, производимое командами PUSH и POP, не приводит к AGI, если следующая команда тоже обращается к стеку.

Процессоры Pentium Pro и Pentium II не подвержены AGI.



Кэш-память


Процессор Pentium включает в себя два 8-килобайтных блока кэш-памяти, один для кода и один для данных с длиной линейки 32 байта. Кэш данных состоит из восьми банков, причем он доступен из обоих конвейеров одновременно, только если обращения происходят к разным банкам. Если данные или код не находятся в кэше, минимальная дополнительная задержка составляет 4 такта.

Если происходит два кэш-промаха одновременно при записи в память из обоих конвейеров и обе записи попадают в одно учетверенное слово (например, при записи двух слов в последовательные адреса), процессор затрачивает столько же времени, сколько и на один кэш-промах.

Процессоры Pentium Pro включают в себя 8-килобайтный кэш L1 для данных и 8-килобайтный кэш L1 для кода, а процессоры Pentium II соответственно по 16 Кб, но не все кэш-промахи приводят к чтению из памяти — существует кэш второго уровня — L2, который маскирует промахи L1. Минимальная задержка при промахе в оба кэша составляет 10 – 14 тактов в зависимости от состояния цикла обновления памяти.

Микрооперации чтения и записи могут произойти одновременно, если они обращаются к разным банкам кэша L1.

Команда LEA


LEA можно использовать (кроме прямого назначения— вычисления адреса сложно адресуемой переменной) для следующих двух ситуаций:

быстрое умножение

lea еах,[еах*2] ; ЕАХ = ЕАХ * 2 (shl eax,1 ; лучше) lea еах,[еах+еах*2] ; ЕАХ = ЕАХ * 3 lea еах,[еах*4] ; ЕАХ = ЕАХ * 4 (shl eax,2 ; лучше) lea еах,[еах+еах*4] ; ЕАХ = ЕАХ * 5 lea еах,[еах+еах*8] ; ЕАХ = ЕАХ * 9

трехоперандное сложение

lea ecx,[eax+ebx] ; ЕСХ = ЕАХ * ЕВХ

Единственный недостаток LEA — увеличивается вероятность AGI с предыдущей командой (см. ниже).



Конвейер FPU


Конвейер исполнения команд FPU состоит из трех участков, на каждом из которых команда тратит по крайней мере один такт. Многие команды, однако, построены таким образом, что позволяют другим командам выполняться на ранних участках конвейера, пока эти команды выполняются на более поздних. Кроме того, параллельно с длинными командами FPU, например FDIV, могут выполняться команды в целочисленных конвейерах.

Команда FXCH может выполняться одновременно почти с любой командой FPU, что позволяет использовать ST(n) как неупорядоченный набор регистров практически без потерь в производительности.



Конвейер ММХ


Команды ММХ, так же как команды FPU, используют дополнительный конвейер, содержащий два блока целочисленной арифметики (и логики), один блок умножения, блок сдвигов, блок доступа к памяти и блок доступа к целочисленным регистрам. Все блоки, кроме умножителя, выполняют свои стадии команды за один такт, умножение требует трех тактов, но имеет собственный буфер, позволяющий принимать по одной команде каждый такт. Так как блоков арифметики два, соответствующие операции могут выполняться одновременно в U- или V-конвейере. Команды, использующие блок сдвигов или умножитель, способны осуществляться в любом конвейере, но не одновременно с другими командами, использующими тот же самый блок. А команды, обращающиеся к памяти или обычным регистрам, в состоянии выполняться только в U-конвейере и только одновременно с ММХ-командами.

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



Обращение к частичному регистру


Если команда обращается к 32-битному регистру, например ЕАХ, сразу после команды, выполнявшей запись в соответствующий частичный регистр (АХ, AL, АН), происходит пауза минимум в 7 тактов на Pentium Pro и Pentium II и в 1 такт — на 80486, но не на Pentium:

mov ax,8 add ecx,eax ; пауза

На Pentium Pro и Pentium II эта пауза не появляется, если сразу перед командой записи в АХ была команда XOR ЕАХ,ЕАХ или SUB ЕАХ,ЕАХ.



Общие принципы низкоуровневой оптимизации


Так как процессоры Intel используют весьма сложный набор команд, большинство операций можно выполнить на низком уровне очень многими способами. При этом иногда оказывается, что наиболее очевидный способ— не самый быстрый или короткий. Часто простыми перестановками команд, зная механизм выполнения команд на современных процессорах, реально заставить ту же процедуру выполняться на 50 – 200% быстрее. Разумеется, переходить к этому уровню оптимизации можно только после того, как текст программы окончательно написан и максимально оптимизирован на среднем уровне.

Перечислим основные рекомендации, которым нужно следовать при оптимальном программировании для процессоров Intel Pentium, Pentium MMX, Pentium Pro и Pentium II.



Очередь предвыборки


Перед тем как команды распределяются по конвейерам, они загружаются из памяти в одну из четырех 32-байтных очередей предвыборки. Если загружается команда перехода и блок предсказания переходов утверждает, что переход произойдет, начинает работать следующая очередь предвыборки. Это сделано для того, чтобы, если предсказание было неверным, первая очередь продолжила бы выборку команд после невыполненной команды перехода.

Если условный переход не был предугадан, затрачивается 3 такта, если команда перехода находилась в U-конвейере, и 4 такта, если в V.

Если безусловный переход или вызов процедуры не был предугадан, затрачивается 3 такта в любом случае.

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

Оптимизация


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

Проблему оптимизации принято делить на три основных уровня:

Выбор наиболее оптимального алгоритма — «высокоуровневая оптимизация».

Наиболее оптимальная реализация алгоритма — «оптимизация среднего уровня».

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



Оптимизация на среднем уровне


Реализация алгоритма на данном конкретном языке программирования — самая ответственная стадия оптимизации. Именно здесь можно получить выигрыш в скорости в десятки раз или сделать программу в десятки раз медленнее, при серьезных ошибках в реализации. Многие элементы, из которых складывается оптимизация, уже упоминались — хранение переменных, с которыми выполняется активная работа, в регистрах, использование таблиц переходов вместо длинных последовательностей проверок и условных переходов и т.п. Тем не менее даже плохо реализованные операции не вносят заметных замедлений в программу, если они не повторяются в цикле. Практически можно говорить, что все проблемы оптимизации на среднем уровне так или иначе связаны с циклами, и именно поэтому мы рассмотрим основные правила, которые стоит иметь в виду при реализации любого алгоритма, содержащего циклы.



Основные рекомендации


Используйте регистр ЕАХ всюду, где возможно. Команды с непосредственным операндом, с операндом — абсолютным адресом переменной и команды XCHG с регистрами занимают на один байт меньше, если другой операнд — регистр ЕАХ.

Используйте регистр DS всюду, где возможно. Префиксы переопределения сегмента увеличивают размер программы на 1 байт и время на 1 такт.

Если к переменной в памяти, адресуемой со смещением, выполняется несколько обращений — загрузите ее в регистр.

Не используйте сложные команды — ENTER, LEAVE, LOOP, строковые команды, если аналогичное действие можно выполнить небольшой последовательностью простых команд.

Не используйте команду MOVZX для чтения байта — это требует 4 тактов для выполнения. Заменой может служить такая пара команд:

xor еах,еах mov al,source

Используйте TEST для сравнения с нулем или для других проверок равенства:

test eax,eax jz if_zero ; переход, если ЕАХ = 0 test eax,source jz if_zero ; переход, если ЕАХ = source

Исцользуйте команду XOR, чтобы обнулять регистр (конечно, если текущее состояние флагов больше не потребуется), эта команда официально поддерживается Intel как команда обнуления регистра:

xor еах,еах ; ЕАХ = 0

Не используйте умножение или деление на константу — его можно заменить другими командами, например:

; ЕАХ = ЕАХ * 10 shl eax,1 ; умножение на 2 lea eax,[eax+eax*4] ; умножение на 5 ; ЕАХ = ЕАХ * 7 mov ebx,eax shl еах,3 ; умножение на 8 sub eax,ebx ; и вычитание сохраненного ЕАХ ; АХ = АХ/10 mov dx,6554 ; DX = 65 536/10 mul dx ; DX = AX/10 (умножение ; выполняется быстрее деления) ; ЕАХ = ЕАХ mod 64 (остаток от деления на степень двойки) and eax,3Fh

Используйте короткую форму команды jmp, где возможно (jmp short метка).

Как можно реже загружайте сегментные регистры.

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



Особенности архитектуры процессоров PentiumPro и Pentium II


Процессоры Pentium Pro и Pentium II включают в себя целый набор средств для ускорения выполнения программ. В них применяется выполнение команд не по порядку, предсказание команд, аппаратное переименование регистров и предсказание переходов.



Перенос проверки условия в конец цикла


Циклы типа WHILE или FOR, которые так часто применяются в языках высокого уровня, оказываются менее эффективными по сравнению с циклами типа UNTIL из-за того, что в них требуется лишняя команда перехода:

; цикл типа WHILE mov si,counter ; число повторов mov dx,start_i ; начальное значение loop_start: cmp dx,si ; пока dx < si - выполнять jbn exit_loop

; [тело цикла]

inc dx jmp loop_start

; почти такой же цикл типа UNTIL mov si,counter mov dx,start_i loop_start: ; выполнять

; [тело цикла]

inc dx cmp dx,si ; пока dx < si jb loop_start

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



Предсказание переходов


Процессор поддерживает 512-байтный буфер выполненных переходов и их целей. Система предсказания может обнаруживать последовательность до четырех повторяющихся переходов, то есть четыре вложенных цикла будут иметь процент предсказания близкий к 100%. Кроме того, дополнительный буфер адресов возврата позволяет правильно предсказывать циклы, из которых происходят вызовы подпрограмм.

На неправильно предсказанный переход затрачивается как минимум девять тактов (в среднем — от 10 до 15). На правильно предсказанный невыполняющийся переход не затрачивается никаких дополнительных тактов вообще. На правильно предсказанный выполняющийся переход затрачивается один дополнительный такт. Именно поэтому минимальное время выполнения цикла на Pentium Pro или Pentium II — два такта, и, если цикл может выполняться быстрее, он должен быть развернут.

Если команда перехода не находится в буфере, система предсказания делает следующие предположения:

безусловный переход предсказывается как происходящий, и на его выполнение затрачивается 5 – 6 тактов;

условный переход назад предсказывается как происходящий, и на его выполнение также затрачивается 5 – 6 тактов;

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



Префиксы


Префиксы LOCK, переопределения сегмента и изменения адреса операнда увеличивают время выполнения команды на 1 такт.



Разворачивание циклов


Для небольших циклов время выполнения проверки условия и перехода на начало цикла может оказаться значительным по сравнению с временем выполнения самого тела цикла. Более того, на Pentium Pro/Pentium II цикл не в состоянии выполняться меньше, чем за два такта процессора, хотя его тело может выполняться даже меньше, чем за такт. С этим легко справиться, вообще не создавая цикл, а просто повторив его тело нужное число раз (разумеется, только в случае, если нам заранее известно это число!). Для очень коротких циклов можно, например, удваивать или утраивать тело цикла, если, конечно, число повторений кратно двум или трем. Кроме того, бывает удобно часть работы сделать в цикле, а часть развернуть, например продолжая цепочку циклов из предыдущего примера:

; цикл от 10 до -1 mov dx,10 loop_start:

; [тело цикла]

dec dx ; уменьшить DX, jns loop_start ; если DX не отрицательный - ; продолжить цикл ; [тело цикла]

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



Вычисление констант вне цикла


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



Выполнение цикла задом наперед


Циклы, в которых значение счетчика растет от двойки, единицы или нуля до некоторой константы, можно реализовать вообще без операции сравнения, выполняя цикл в обратном направлении (и мы пользовались этим приемом неоднократно в наших примерах). Дело в том, что команда DECcounter устанавливает флаги точно так же, как и команда СМР counter,1, то есть следующая команда условного перехода будет обрабатывать результат сравнения счетчика с единицей:

; цикл от 10 до 2 mov dx,10 loop_start:

; [тело цикла]

dec dx ; уменьшить DX, ja loop_start ; если DX больше 1 - продолжить цикл

; цикл от 10 до 1 mov dx,10 loop_start:

; [тело цикла]

dec dx ; уменьшить DX, jae loop_start ; если DX больше или равно 1 - продолжить цикл

; цикл от 10 до 0 mov dx,10 loop_start:

; [тело цикла]

dec dx ; уменьшить DX, jns loop_start ; если DX не отрицательный - продолжить цикл

Конечно, не все циклы можно заставить выполняться в обратном направлении сразу. Например, иногда приходится изменять формат хранения массива данных также на обратный, иногда приходится вносить другие изменения, но в целом, если это возможно, всегда следует стремиться к циклам, выполняющимся задом наперед. Кроме того, если цикл построен в этой манере, выполняется до значения счетчика, равного нулю, и регистр СХ можно освободить для выполнения роли счетчика, есть вариант воспользоваться командой LOOP, хотя в некоторых случаях в низкоуровневой оптимизации команды DEC/JNZ оказываются более эффективными.



Выполнение команд


Процессор Pentium содержит два конвейера исполнения целочисленных команд (U и V) и один конвейер для команд FPU. Он может выполнять две целочисленные команды одновременно и поддерживает механизм предсказания переходов, значительно сокращающий частоту сброса очереди предвыборки из-за передачи управления по другому адресу.

Процессор перед выполнением команды анализирует сразу две следующие команды, находящиеся в очереди и, если возможно, выполняет одну из них в U-конвейере, а другую в V. Если это невозможно, первая команда загружается в U-конвейер, а V-конвейер пустует.

V-конвейер имеет определенные ограничения на виды команд, которые могут в нем исполняться. Приложение 2 содержит для каждой команды информацию о том, может ли она выполняться одновременно с другими командами и в каком конвейере. Кроме того, две команды не будут запущены одновременно, если:

команды подвержены одной из следующих регистровых зависимостей:

Первая команда пишет в регистр, а вторая читает из него.

Обе команды пишут в один и тот же регистр (кроме записи в EFLAGS).

Исключения из этих правил— пары PUSH/PUSH, PUSH/POP и PUSH/CALL, выполняющие запись в регистр ESP;

одна из команд не находится в кэше команд (кроме случая, если первая команда — однобайтная);

одна из команд длиннее семи байт (для Pentium);

одна команда длиннее восьми байт, а другая — семи (для Pentium ММХ).

Помните, что простыми перестановками команд можно выиграть до 200% скорости в критических ситуациях.

Каждый такт процессора до трех команд может быть прочитан и декодирован на микрооперации из очереди предвыборки. В этот момент работают три декодера, первый из которых может декодировать команды, содержащие до четырех микроопераций, а другие два— только команды из одной микрооперации. Если в ассемблерной программе команды упорядочены в соответствии с этим правилом (4–1–1), то все время на каждый такт будет происходить декодирование трех команд, например, если в последовательности команд

add eax,[ebx] ; 2m - в декодер 0 на первом такте mov есх,[еах] ; 2m - пауза 1 такт, пока декодер 0 ; не освободится add edx,8 ; 1m - декодер 1 на втором такте

переставить вторую и третью команды, команда add edx,8 будет декодирована в тот же такт, что и первая команда.

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

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


Время выполнения команд в пяти конвейерах исполнения приведено в таблице 21.

Таблица 21. Конвейеры процессора Pentium Pro/Pentium II

  Время выполнения Скорость
Конвейер 0
Блок целочисленной арифметики 1 1
Блок команд LEA 1 1
Блок команд сдвига 1 1
Блок целочисленного умножения 4 1
Блок команд FADD 3 1
Блок команд FMUL 5 2
Блок команд FDIV 17 для 32-битных
36 для 64-битных
56 для 80-битных
17
36
56
Блок MMX-арифметики 1 1
Блок MMX-умножений 3 1
Конвейер 1
Блок целочисленной арифметики 1 1
Блок MMX-арифметики 1 1
Блок MMX-сдвигов 1 1
Конвейер 2
Блок чтения 3 при кэш-попадании 1
Конвейер 3
Блок записи адреса не меньше 3 1
Конвейер 4
Блок записи данных не меньше 1 1
Указанное в таблице время — это время, требующееся для выполнения микрооперации, а скорость — с какой частотой элемент может принимать микрооперации в собственный конвейер (1 — каждый такт, 2 — каждый второй такт). То есть, например, одиночная команда FADD выполняется за три такта, но три последовательные команды FADD выполнятся также за 3 такта.

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

Существует особая группа синхронизирующих команд, любая из которых начинает выполняться только после того, как завершатся все микрооперации, находящиеся в состоянии выполнения. К таким командам относятся привилегированные команды WRMSR, INVD, INVLPG, WBINVD, LGDT, LLDT, LIDT, LTR, RSM и MOV в управляющие и отладочные регистры, а также две непривилегированные команды — IRET и CPUID. В тех случаях, когда, например, измеряют скорость выполнения процедуры при помощи команды RDTSC (глава 10.2), полезно выполнить одну из синхронизирующих команд, чтобы убедиться, что все измеряемые команды полностью завершились.


Выравнивание


Восьмибайтные данные должны быть выравнены по восьмибайтным границам (то есть три младших бита адреса должны быть равны нулю).

Четырехбайтные данные должны быть выравнены по границе двойного слова (то есть два младших бита адреса должны быть равны нулю).

Двухбайтные данные должны полностью содержаться в выравненном двойном слове (то есть два младших бита адреса не должны быть равны единице).

80-битные данные должны быть выравнены по 16-байтным границам.

Когда нарушается выравнивание при доступе к данным, находящимся в кэше, теряются 3 такта на каждое невыравненное обращение на Pentium и 9 – 12 тактов — на Pentium Pro/Pentium II.

Так как линейка кэша кода составляет 32 байта, метки для переходов, особенно метки, отмечающие начало цикла, должны быть выравнены по 16-байтным границам, а массивы данных, равные или большие 32 байт, должны начинаться с адреса, кратного 32.



Высокоуровневая оптимизация


Выбор оптимального алгоритма для решения задачи всегда приводит к лучшим результатам, чем любой другой вид оптимизации. Действительно, при замене пузырьковой сортировки, время выполнения которой пропорционально N2, на быструю сортировку, выполняющуюся как N * log(N), всегда найдется такое число сортируемых элементов N, что вторая программа будет выполняться быстрее, как бы она ни была реализована. Поиск лучшего алгоритма — универсальная стадия, и она относится не только к ассемблеру, но и к любому языку программирования, поэтому будем считать, что оптимальный алгоритм уже выбран.



Дескрипторы


Дескриптор — это 64-битная (восьмибайтная) структура данных, которая может встречаться в таблицах GDT и LDT. Дескриптор способен описывать сегмент кода, сегмент данных, сегмент состояния задачи, быть шлюзом вызова, ловушки, прерывания или задачи. В GDT также может находиться дескриптор LDT.

Дескриптор сегмента данных или кода (подробно рассмотрен в главе 6.1)

байт 7: биты 31 – 24 базы сегмента

байт 6:

бит 7: бит гранулярности (0 — лимит в байтах, 1 — лимит в 4-килобайтных единицах)

бит 6: бит разрядности (0 — 16-битный, 1 — 32-битный сегмент)

бит 5: 0

бит 4: зарезервировано для операционной системы

биты 3 – 0: биты 19 – 16 лимита

байт 5: (байт доступа)

бит 7: бит присутствия сегмента

биты 6 – 5: уровень привилегий дескриптора (DPL)

бит 4: 1 (тип дескриптора — не системный)

бит 3: тип сегмента (0 — данных, 1 — кода)

бит 2: бит подчиненности для кода, бит расширения вниз для данных

бит 1: бит разрешения чтения для кода, бит разрешения записи для данных

бит 0: бит доступа (1 — к сегменту было обращение)

байт 4: биты 23 – 16 базы сегмента

байты 3 – 2: биты 15 – 0 базы

байты 1 – 0: биты 15 – 0 лимита

Если в дескрипторе бит 4 байта доступа равен 0, дескриптор называется системным. В этом случае биты 0 – 3 байта доступа определяют один из шестнадцати возможных типов дескриптора (табл. 22).

Таблица 22. Типы системных дескрипторов

0 Зарезервированный тип 8 Зарезервированный тип
1 Свободный 16-битный TSS 9 Свободный 32-битный TSS
2 Дескриптор таблицы LDT A Зарезервированный тип
3 Занятый 16-битный TSS B Занятый 16-битный TSS
4 16-битный шлюз вызова C 32-битный шлюз вызова
5 Шлюз задачи D Зарезервированный тип
6 16-битный шлюз прерывания E 32-битный шлюз прерывания
7 16-битный шлюз ловушки F 32-битный шлюз ловушки
<
/p> Дескрипторы шлюзов

Дальние CALL или JMP на адрес с любым смещением и с селектором, указывающим на дескриптор шлюза вызова, приводят к передаче управления по адресу, указанному в дескрипторе. Обычно такие дескрипторы используются для передачи управления между сегментами с различными уровнями привилегий (см. главу 10.7).

CALL или JMP на адрес с селектором, указывающим на шлюз задачи, приводят к переключению задач (см. главу 10.8).

Шлюзы прерываний и ловушек используются для вызова обработчиков соответственно прерываний и исключений типа ловушки (см. главу 10.5).

байты 7 – 6: биты 31 – 16 смещения (0 для 16-битных шлюзов и шлюза задачи)

байт 5: (байт доступа)

бит 7: бит присутствия сегмента

биты 6 – 5: DPL — уровень привилегий дескриптора

бит 4: 0

биты 3 – 0: тип шлюза (3, 4, 5, 6, 7, В, С, Е, 7)

байт 4:

биты 7 – 5: 000

биты 4 – 0: 00000 или (для шлюза вызова) число двойных слов, которые будут скопированы из стека вызывающей задачи в стек вызываемой

байты 3 – 2: селектор сегмента

байты 1 – 0: биты 15 – 0 смещения (0 для шлюза задачи)

Дескрипторы TSS и LDT

Эти два типа дескрипторов применяются в многозадачном режиме, о котором рассказано далее. TSS — сегмент состояния задачи, используемый для хранения всей необходимой информации о каждой задаче в многозадачном режиме. LDT — таблица локальных дескрипторов, своя для каждой задачи.

Форматы этих дескрипторов совпадают с форматом дескриптора для сегмента кода или данных, за исключением того, что бит разрядности всегда равен нулю и, естественно, системный бит равен нулю, и биты 3 – 0 байта доступа содержат номер типа сегмента (1, 2, 3, 9, В). Команды JMP и CALL на адрес с селектором, соответствующим TSS незанятой задачи, приводят к переключению задач.


Машинно-специфичные регистры


Это большая группа регистров (более ста), назначение которых отличается в разных моделях процессоров Intel и даже иногда в процессорах одной модели, но разных версий. Например, регистры Pentium Pro MTRR (30 регистров) описывают, какой механизм страничной адресации используют различные области памяти — не кэшируются, защищены от записи, кэшируются прозрачно и т.д. Регистры Pentium Pro MCG/MCI (23 регистра) используются для автоматического обнаружения и обработки аппаратных ошибок, регистры Pentium TR (12 регистров) используются для тестирования кэша и т.п. Мы рассмотрим при описании соответствующих команд только регистр Pentium TSC — счетчик тактов процессора и группу из четырех регистров Pentium Pro, использующуюся для подсчета различных событий (число обращений к кэшу, умножений, команд ММХ и тому подобное), так как эти регистры оказались настолько полезными, что для работы с ними появились дополнительные команды — RDTSC и RDPMC.



Механизм защиты


Теперь рассмотрим механизм, который дал название режиму процессора, — механизм защиты. Защита может действовать как на уровне сегментов, так и на уровне страниц, ограничивая доступ в зависимости от уровня привилегий (4 уровня привилегий для сегментов и два для страниц). Защита предотвращает возможность для программ вносить изменения в области памяти, занятые операционной системой или более привилегированной программой. Процессор проверяет привилегии непосредственно перед каждым обращением к памяти и, если происходит нарушение защиты, вызывает исключение #GP.

Если процессор находится в защищенном режиме, проверки привилегий выполняются всегда и их нельзя отключить, но можно использовать во всех дескрипторах и селекторах один и тот же максимальный уровень привилегий — нулевой, и создастся видимость отсутствия защиты. Именно так мы и поступали во всех примерах до сих пор — все поля DPL и RPL инициализировались нулями. Чтобы сделать незаметной проверку прав на уровне страничной адресации, надо установить биты U и W во всех элементах таблиц страниц, что мы также делали в программе pm3.asm.

За механизм защиты отвечают следующие биты и поля:

в дескрипторах сегментов:

бит S (системный сегмент)

поле типа (тип сегмента, включая запреты на чтение/запись)

поле лимита сегмента

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

в селекторах сегментов:

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

поле RPL селектора, загруженного в CS, называется CPL и является текущим уровнем привилегий программы

в элементах таблиц страниц:

бит U (определяет уровень привилегий страницы)

бит W (разрешает/запрещает запись)

Уровни привилегий в процессорах Intel определены как:

0 — максимальный (для операционной системы);

1 и 2 — промежуточные (для вспомогательных программ);

3 — минимальный (для пользовательских приложений).

Перед обращением к памяти процессор выполняет несколько типов проверок, использующих все указанные флаги и поля. Рассмотрим их по порядку.



Модель памяти в защищенном режиме


Мы уже неоднократно описывали сегментную адресацию — рассказывая о назначении сегментных регистров в реальном режиме или о программировании для расширителей DOS в защищенном режиме, но каждый раз требовалась для немедленных нужд только часть всей этой сложной модели. Теперь самое время рассмотреть ее полностью.

Для любого обращения к памяти в процессорах Intel используется логический адрес, состоящий из 16-битного селектора, определяющего сегмент, и 32- или 16-битного смещения — адреса внутри сегмента. Отдельный сегмент памяти — это независимое защищенное адресное пространство, для которого определены размер, разрешенные способы доступа (чтение/запись/исполнение кода) и уровень привилегий (см. главу 10.7). Если доступ к памяти удовлетворяет всем условиям защиты, процессор преобразует логический адрес в 32- или 36-битный (на Р6) линейный. Линейный адрес — это адрес в несегментированном непрерывном адресном пространстве, который совпадает с физическим адресом в памяти, если отключен режим страничной адресации (см. главу 10.6). Чтобы получить линейный адрес из логического, процессор добавляет к смещению линейный адрес начала сегмента, который хранится в поле базы в сегментном дескрипторе. Сегментный дескриптор — это восьмибайтная структура данных, расположенная в таблице GDT или LDT; адрес таблицы находится в регистре GDTR или LDTR, а номер дескриптора в таблице определяется из значения селектора.

Дескриптор для селектора, находящегося в сегментном регистре, не считывается из памяти при каждом обращении, а хранится в скрытой части сегментного регистра и загружается только при выполнении команд MOV в сегментный регистр, POP в сегментный регистр, LDS, LES, LSS, LGS, LFS и дальных команд перехода.



Нереальный режим


Как мы уже знаем, при изменении режима скрытые части сегментных регистров сохраняют содержимое своих дескрипторов и ими можно пользоваться. Мы осуществили эту возможность в нашем первом примере, когда значения, занесенные в сегментные регистры в реальном режиме, использовались в защищенном. Возникает вопрос — а если сделать наоборот? В защищенном режиме загрузить сегментные регистры дескрипторами 4-гигабайтных сегментов с базой 0 и перейти в реальный режим? Оказывается, что это прекрасно срабатывает, и мы попадем в особый режим, который был обнаружен одновременно разными программистами и называется нереальным режимом (unreal mode), большим реальным режимом (BRM) или реальным flat-режимом (RFM). Чтобы перейти в нереальный режим, надо загрузить в CS перед переходом в реальный режим дескриптор 16-битного сегмента кода с базой 0 и лимитом 4 Гб и в остальные сегментные регистры — точно такие же дескрипторы сегментов данных.

Теперь весь дальнейший код программы, написанный для реального режима, больше не ограничен рамками 64-килобайтных сегментов и способен работать с любыми массивами. Можно подумать, что первый же обработчик прерывания от таймера загрузит в CS нормальное значение и все станет как обычно, но нет. Оказывается, что при создании дескриптора в скрытой части сегментного регистра в реальном режиме процессор не трогает поле лимита, а только изменяет базу: что бы мы ни записали в сегментный регистр, сегмент будет иметь размер 4 Гб. Если попробовать вернуться в DOS — DOS будет по-прежнему работать. Можно запускать программы такого рода:

.model tiny .code org 100h start: xor ax,ax mov ds,ax ; DS = 0 ; вывести символ в видеопамять: mov word ptr ds:[0B8000h],8403h ret end start

и они тоже будут работать. Единственное, что отключает этот режим, — программы, переключающиеся в защищенный режим и обратно, устанавливающие границы сегментов в 64 Кб, например любые программы, использующие расширители DOS.

Нереальный режим — идеальный вариант для программ, которые хотят пользоваться 32-битной адресацией и свободно обращаться ко всем прерываниям BIOS и DOS (традиционный способ состоял бы в работе в защищенном режиме с переключением в V86 для вызова BIOS или DOS, как это делается в случае DPMI).


Для переключения в этот режим можно воспользоваться, например, такой процедурой:

; область данных: GDT label byte db 8 dup(0) ; нулевой дескриптор ; 16-битный 4 Гб сегмент: db 0FFh,0FFh,0,0,0,1001001b,11001111b,0 gdtr dw 16 ; размер GDI gdt_base dd ? ; линейный адрес GDT

; код программы ; определить линейный адрес GDT xor еах,еах mov ax,cs shl eax,4 add ax,offset GDT ; загрузить GDT из одного дескриптора (не считая нулевого) mov gdt_base,eax lgdt fword ptr gdtr ; перейти в защищенный режим cli mov eax,cr0 or al,1 mov cr0,eax jmp start_PM ; сбросить очередь предвыборки ; Intel рекомендует start_PM: ; делать jmp после каждой смены режима ; загрузить все сегментные регистры дескриптором с лимитом 4 Гб mov ax,8 ; 8 - селектор нашего дескриптора mov ds,ax mov es,ax mov fs,ax mov gs,ax ; перейти в реальный режим mov eax,cr0 and al,0FEh mov cr0,eax jmp exit_PM exit_PM: ; записать что-нибудь в каждый сегментный регистр хог ах,ах mov ds,ax mov es,ax mov fs,ax mov gs,ax sti mov ax,cs mov ds,ax ; и все - теперь процессор находится в реальном режиме ; с неограниченными сегментами


Обработка прерываний и исключений


До сих пор все наши программы работали в защищенном режиме с полностью отключенными прерываниями — ими нельзя было управлять с клавиатуры, они не могли работать с дисками и вообще не делали ничего, кроме чтения или записи в те или иные области памяти. Разумеется, ни одна программа не может сделать ничего серьезного в таком режиме — нам рано или поздно придется обрабатывать прерывания.

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

Шлюзы прерываний и ловушек указывают точку входа обработчика, а также его разрядность и уровень привилегий. При передаче управления обработчику процессор помещает в стек флаги и адрес возврата, так же как и в реальном режиме, но для некоторых исключений после этого в стек помещается дополнительный код ошибки, так что не все обработчики можно завершать простой командой IRETD (или IRET для 16-битного варианта). Единственное различие между шлюзом прерывания и ловушки состоит в том, что при передаче управления через шлюз прерывания автоматически запрещаются дальнейшие прерывания, пока обработчик не выполнит IRETD. Этот механизм считается предпочтительным для обработчиков аппаратных прерываний, в то время как шлюз ловушки, который не запрещает прерывания на время исполнения обработчика, предпочтителен для обработки программных прерываний (которые фактически и являются исключениями типа ловушки). Кроме того, в защищенном режиме при вызове обработчика прерывания сбрасывается флаг трассировки ТF.

Сначала рассмотрим пример программы, обрабатывающей только аппаратное прерывание клавиатуры при помощи шлюза прерываний. Для этого надо составить IDT, загрузить ее адрес командой LIDT и не забыть загрузить то, что содержится в регистре IDTR в реальном режиме, — адрес 0 и размер 4 * 256, соответствующие таблице векторов прерываний реального режима.

; pm2.asm ; Программа, демонстрирующая обработку аппаратных прерываний в защищенном ; режиме, переключается в 32-битный защищенный режим и позволяет набирать ; текст при помощи клавиш от 1 до +. Нажатие Backspace стирает предыдущий ; символ, нажатие Esc - выход из программы. ; ; Компиляция TASM: ; tasm /m /D_TASM_ pm2.asm ; (или, для версий 3.x, достаточно tasm /m pm2.asm) ; tlink /x /3 pm2.obj ; Компиляция WASM: ; wasm /D pm2.asm ; wlink file pm2.obj form DOS ; ; Варианты того, как разные ассемблеры записывают смещение из 32-битного ; сегмента в 16-битную переменную: ifdef _TASM_ so equ small offset ; TASM 4.x else so equ offset ; WASM endif ; для MASM, по-видимому, придется добавлять лишний код, который преобразует ; смещения, используемые в IDT


. 386р RM_seg segment para public "CODE" use16 assume cs:RM_seg,ds:PM_seg,ss:stack_seg start: ; очистить экран mov ax,3 int 10h ; подготовить сегментные регистры push PM_seg pop ds ; проверить, не находимся ли мы уже в РМ mov еах,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,so v86_msg err_exit: mov ah,9 int 21h mov ah,4Ch int 21h ; может быть, это Windows 95 делает вид, что РЕ = О? no_V86: mov ax,1600h int 2Fh test al,al jz no_windows ; сообщить и выйти mov dx,so win_msg jmp short err_exit ; итак, мы точно находимся в реальном режиме no_windows: ; вычислить базы для всех используемых дескрипторов сегментов xor еах,еах mov ax,RM_seg shl eax,4 mov word ptr GDT_16bitCS+2,ax ; базой 16bitCS будет RM_seg shr eax,16 mov byte ptr GDT_16bitCS+4,al mov ax,PM_seg shl eax,4 mov word ptr GDT_32bitCS+2,ax ; базой всех 32bit* будет mov word ptr GDT_32bitSS+2,ax ; PM_seg mov word ptr GDT_32bitDS+2,ax shr eax,16 mov byte ptr GDT_32bitCS+4,al mov byte ptr GDT_32bitSS+4,al mov byte ptr GDT_32bitDS+4,al ; вычислить линейный адрес GDT xor еах,еах mov ax,PM_seg shl eax,4 push eax add eax,offset GDT mov dword ptr gdtr+2,eax ; загрузить GDT lgdt fword ptr gdtr ; вычислить линейный адрес IDT pop eax add eax,offset IDT mov dword ptr idtr+2,eax ; загрузить IDT lidt fword ptr idtr ; если мы собираемся работать с 32-битной памятью, стоит открыть А20 in al,92h or al,2 out 92h,al ; отключить прерывания, cli ; включая NMI, in al,70h or al,80h out 70h,al ; перейти в РМ mov еах,cr0 or al,1 mov cr0,eax ; загрузить SEL_32bitCS в CS db 66h db 0EAh dd offset PM_entry dw SEL_32bitCS RM_return: ; перейти в RM mov eax,cr0 and al,0FEh mov cr0,eax ; сбросить очередь и загрузить CS реальным числом db 0EAh dw $+4 dw RM_seg ; установить регистры для работы в реальном режиме mov ax,PM_seg mov ds,ax mov es,ax mov ax,stack_seg mov bx,stack_l mov ss,ax mov sp,bx ; загрузить IDTR для реального режима mov ax,PM_seg mov ds,ax lidt fword ptr idtr_real ; разрешить NMI in al,70h and al,07FH out 70h,al ; разрешить прерывания sti ; и выйти mov ah,4Ch int 21h RM_seg ends



; 32- битный сегмент PM_seg segment para public "CODE" use32 assume cs:PM_seg ; таблицы GDI и IDT должны быть выравнены, так что будем их размещать ; в начале сегмента GDT label byte db 8 dup(0) ; 32-битный 4-гигабайтный сегмент с базой = 0 GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; 16-битный 64-килобайтный сегмент кода с базой RM_seg GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 ; 32-битный 4-гигабайтный сегмент кода с базой PM_seg GDT_32bitCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 ; 32-битный 4-гигабайтный сегмент данных с базой PM_seg GDT_32bitDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; 32-битный 4-гигабайтный сегмент данных с базой stack_seg GDT_32bitSS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 gdt_size = $ - GDT gdtr dw gdt_size-1 ; лимит GDT dd ? ; линейный адрес GDT ; имена для селекторов SEL_flatDS equ 001000b SEL_16bitCS equ 010000b SEL_32bitCS equ 011000b SEL_32bitDS equ 100000b SEL_32bitSS equ 101000b

; таблица дескрипторов прерываний IDT IDT label byte ; все эти дескрипторы имеют тип 0Eh - 32-битный шлюз прерывания ; INT 00 - 07 dw 8 dup(so int_handler,SEL_32bitCS,8E00h,0) ; INT 08 (irq0) dw so irq0_7_handler,SEL_32bitCS,8E00h,0 ; INT 09 (irq1) dw so irq1_handler,SEL_32bitCS,8E00h,0 ; INT 0Ah - 0Fh (IRQ2 - IRQ8) dw 6 dup(so irq0_7_handler,SEL_32bitCS,8E00h,0) ; INT 10h - 6Fh dw 97 dup(so int_handler,SEL_32bitCS,8E00h,0) ; INT 70h - 78h (IRQ8 - IRQ15) dw 8 dup(so irq8_15_handler,SEL_32bitCS,8E00h,0) ; INT 79h - FFh dw 135 dup(so int_handler,SEL_32bitCS,8E00h,0) idt_size = $ - IDT ; размер IDT idtr dw idt_size-1 ; лимит IDT dd ? ; линейный адрес начала IDT ; содержимое регистра IDTR в реальном режиме idtr_real dw 3FFh,0,0

; сообщения об ошибках при старте v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"

; таблица для перевода 0Е скан-кодов в ASCII scan2ascii db 0,1Bh,'1','2','3','4','5','6','7','8','9','0','-','=',8 screen_addr dd 0 ; текущая позиция на экране



; точка входа в 32-битный защищенный режим PM_entry: ; установить 32-битный стек и другие регистры mov ax,SEL_flatDS mov ds,ax mov es,ax mov ax,SEL_32bitSS mov ebx,stack_l mov ss,ax mov esp,ebx ; разрешить прерывания sti ; и войти в вечный цикл jmp short $

; обработчик обычного прерывания int_handler: iretd ; обработчик аппаратного прерывания IRQ0 - IRQ7 irq0_7_handler: push eax mov al,20h out 20h,al pop eax iretd ; обработчик аппаратного прерывания IRQ8 - IRQ15 irq8_15_handler: push eax mov al,20h out 0A1h,al pop eax iretd ; обработчик IRQ1 - прерывания от клавиатуры irq1_handler: push eax ; это аппаратное прерывание - сохранить регистры push ebx push es push ds in al,60h ; прочитать скан-код нажатой клавиши, cmp al,0Eh ; если он больше, чем максимальный ja skip_translate ; обслуживаемый нами, - не обрабатывать, cmp al,1 ; если это Esc, je esc_pressed ; выйти в реальный режим, mov bx,SEL_32bitDS ; иначе: mov ds,bx ; DS:EBX - таблица для перевода скан-кода mov ebx,offset scan2ascii ; в ASCII xlatb ; преобразовать mov bx,SEL_flatDS mov es,bx ; ES:EBX - адрес текущей mov ebx,screen_addr ; позиции на экране, cmp al,8 ; если не была нажата Backspace, je bs_pressed mov es:[ebx+0B8000h],al ; послать символ на экран, add dword ptr screen_addr,2 ; увеличить адрес позиции на 2, jmp short skip_translate bs_pressed: ; иначе: mov al,' ' ; нарисовать пробел sub ebx,2 ; в позиции предыдущего символа mov es:[ebx+0B8000h],al mov screen_addr,ebx ; и сохранить адрес предыдущего символа skip_translate: ; как текущий ; разрешить работу клавиатуры in al,61h or al,80h out 61h,al ; послать EOI контроллеру прерываний mov al,20h out 20h,al ; восстановить регистры и выйти pop ds pop es pop ebx pop eax iretd ; сюда передается управление из обработчика IRQ1, если нажата Esc esc_pressed: ; разрешить работу клавиатуры, послать EOI и восстановить регистры in al,61h or al,80h out 61h,al mov al,20h out 20h,al pop ds pop es pop ebx pop eax ; вернуться в реальный режим cli db 0EAh dd offset RM_return dw SEL_16bitCS PM_seg ends



; Сегмент стека. Используется как 16-битный в 16-битной части программы и как ; 32-битный (через селектор SEL_32bitSS) в 32- битной части stack_seg segment para stack "STACK" stack_start db 100h dup(?) stack_l = $ - stack_start ; длина стека для инициализации ESP stack_seg ends end start

В этом примере обрабатываются только 13 скан-кодов клавиш для сокращения размеров программы — полную информацию для преобразования скан-кодов в ASCII можно получить, воспользовавшись таблицами, приведенными в приложении 1 (рис. 18, табл. 25 и 26). Кроме того, в этом примере курсор все время остается в нижнем левом углу экрана — для его перемещения можно воспользоваться регистрами 0Eh и 0Fh контроллера CRT (см. главу 5.10.4).

Как уже упоминалось в главе 5.8, кроме прерываний от внешних устройств процессор может вызывать исключения при различных внутренних ситуациях, механизм обслуживания которых похож на механизм обслуживания аппаратных прерываний. Номера прерываний, на которые отображаются аппаратные прерывания, вызываемые первым контроллером по умолчанию, совпадают с номерами некоторых исключений. Конечно, можно из обработчика опрашивать контроллер прерываний, чтобы определить, выполняется ли обработка аппаратного прерывания или это исключение, но Intel рекомендует перенастраивать контроллер прерываний (мы это делали в главе 5.10.10) так, чтобы никакие аппаратные прерывания не попадали на область от 0 до 1Fh. В нашем примере исключения не обрабатывались, но, если программа планирует запускать другие программы или задачи, без обработки исключений обойтись нельзя.

Часть исключений (исключения типа ошибки) передает в качестве адреса возврата команду, вызвавшую исключение, а часть — адрес следующей команды. Кроме того, некоторые исключения помещают в стек код ошибки, который нужно считать, прежде чем выполнять IRETD. Поэтому пустой обработчик из одной команды IRETD в нашем примере не был корректным и многие исключения привели бы к немедленному зависанию системы.



Рассмотрим исключения в том виде, как они определены для защищенного режима.

Формат кода ошибки:

биты 15 – 3: биты 15 – 3 селектора, вызвавшего исключение

бит 2: TI — установлен, если причина исключения — дескриптор, находящийся в LDT, и сброшен, если в GDT

бит 1: IDT — установлен, если причина исключения — дескриптор, находящийся в IDT

бит 0: ЕХТ — установлен, если причина исключения — аппаратное прерывание

INT 00 — ошибка #DE «Деление на ноль»

Вызывается командами DIV или IDIV, если делитель — ноль или если происходит переполнение.

INT 01 — исключение #DB «Отладочное прерывание»

Вызывается как ловушка при пошаговой трассировке (флаг TF = 1), при переключении на задачу с установленным отладочным флагом и при срабатывании точки останова во время доступа к данным, определенной в отладочных регистрах.

Вызывается как ошибка при срабатывании точки останова по выполнению команды по адресу, определенному в отладочных регистрах.

INT 02 — прерывание NMI

Немаскируемое прерывание.

INT 03 — ловушка #ВР «Точка останова»

Вызывается однобайтной командой INT3.

INT 04 — ловушка #OF «Переполнение»

Вызывается командой INT0, если флаг OF = 1.

INT 05 — ошибка #ВС «Переполнение при BOUND»

Вызывается командой BOUND при выходе операнда за допустимые границы.

INT 06 — ошибка #UD «Недопустимая операция»

Вызывается, когда процессор пытается исполнить недопустимую команду или команду с недопустимыми операндами.

INT 07 — ошибка #NM «Сопроцессор отсутствует»

Вызывается любой командой FPU, кроме WAIT, если бит ЕМ регистра CR0 установлен в 1, и командой WAIT, если МР и TS установлены в 1.

INT 08 — ошибка #DF «Двойная ошибка»



Вызывается, если одновременно произошли два исключения, которые не могут быть обслужены последовательно. К таким исключениям относятся #DE, #TS, #NP, #SS, #GP и #РЕ

Обработчик этого исключения получает код ошибки, который всегда равен нулю.

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

INT 09 — зарезервировано

Эта ошибка вызывалась сопроцессором 80387, если происходило исключение #PF или #GP при передаче операнда команды FPU.

INT 0Ah — ошибка #TS «Ошибочный TSS»

Вызывается при попытке переключения на задачу с ошибочным TSS.

Обработчик этого исключения должен вызываться через шлюз задачи.

Обработчик этого исключения получает код ошибки.

Бит ЕХТ кода ошибки установлен, если переключение пыталось выполнить аппаратное прерывание, использующее шлюз задачи, индекс ошибки равен селектору TSS, если TSS меньше 67h байт, селектору LDT, если LDT отсутствует или ошибочен, селектору сегмента стека, кода или данных, если ими нельзя пользоваться (из-за нарушений защиты или ошибок в селекторе).

INT 0Bh — ошибка #NP «Сегмент недоступен»

Вызывается при попытке загрузить в регистр CS, DS, ES, FS или GS селектор сегмента, в дескрипторе которого сброшен бит присутствия сегмента (загрузка в SS вызывает исключение #SS), а также при попытке использования шлюза, помеченного как отсутствующий, или при загрузке такой таблицы локальных дескрипторов командой LLDT (загрузка при переключении задач приводит к исключению #TS).

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

Обработчик этого исключения получает код ошибки.

Бит ЕХТ кода ошибки устанавливается, если причина ошибки — внешнее прерывание, бит IDT устанавливается, если причина ошибки — шлюз из IDT, помеченный как отсутствующий. Индекс ошибки равен селектору отсутствующего сегмента.



INT 0Ch — ошибка #SS «Ошибка стека»

Это исключение вызывается при попытке выхода за пределы сегмента стека при выполнении любой команды, работающей со стеком, — как явно (POP, PUSH, ENTER, LEAVE), так и неявно (MOV AX,[BP + 6]), а также при попытке загрузить в регистр SS селектор сегмента, помеченного как отсутствующий (не только при выполнении команд MOV, POP и LSS, но и при переключении задач, вызове и возврате из процедуры на другом уровне привилегий).

Обработчик этого исключения получает код ошибки.

Код ошибки равен селектору сегмента, вызвавшего ошибку, если она произошла из-за отсутствия сегмента или при переполнении нового стека в межуровневой команде CALL. Во всех остальных случаях код ошибки — ноль.

INT 0Dh — исключение #GP «Общая ошибка защиты»

Все ошибки и ловушки, не приводящие к другим исключениям, вызывают #GP — в основном нарушения привилегий.

Обработчик этого исключения получает код ошибки.

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

INT 0Eh — ошибка #PF «Ошибка страничной адресации»

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

Обработчик этого исключения получает код ошибки.

Код ошибки использует формат, отличный от кода ошибки для других исключений:

бит 0: 1, если причина ошибки — нарушение привилегий;
0, если было обращение к отсутствующей странице
бит 1: 1, если выполнялась операция записи,
0, если чтения
бит 2: 1, если операция выполнялась из CPL = 3,
0, если CPL < 3
бит 3: 0, если ошибку вызвала попытка установить зарезервированный бит в каталоге страниц
  остальные биты зарезервированы
Кроме кода ошибки обработчик этого исключения может прочитать из регистра CR2 линейный адрес, преобразование которого в физический вызвало исключение.



Исключение #PF — основное исключение для создания виртуальной памяти с использованием механизма страничной адресации.

INT 0Fh — зарезервировано

INT 10h — ошибка #MF «Ошибка сопроцессора»

Вызывается, только если бит NE в регистре CR0 установлен в 1 при выполнении любой команды FPU, кроме управляющих команд и WAIT/FWAIT, если в FPU произошло одно из исключений FPU (см. главу 2.4.3).

INT 11h — ошибка #АС «Ошибка выравнивания»

Вызывается, только если бит AM в регистре CR0 и флаг АС из EFLAGS установлены в 1, если CPL = 3 и произошло невыравненное обращение к памяти. (Выравнивание должно быть по границе слова при обращении к слову, к границе двойного слова, к двойному слову и т.д.)

Обработчик этого исключения получает код ошибки, равный нулю.

INT 12h — останов #МС «Машинно-зависимая ошибка»

Вызывается (начиная с Pentium) при обнаружении некоторых аппаратных ошибок с помощью специальных машинно-зависимых регистров MCG_*. Наличие кода ошибки, так же как и способ вызова этого исключения, зависит от модели процессора.

INT 13h – 1Fh — зарезервировано Intel для будущих исключений

INT 20h – FFh — выделены для использования программами

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


Отладочные регистры


Эти восемь 32-битных регистров (DR0 – DR7) позволяют программам, выполняющимся на уровне привилегий 0, определять точки останова, не модифицируя код программ, например для отладки ПЗУ или программ, применяющих сложные схемы защиты от трассировки. Пример отладчика, использующего эти регистры, — SoftICE.

DR7 (DCR) — регистр управления отладкой

биты 31 – 30: поле LEN для точки останова 3 (размер точки останова)

00 — 1 байт

01 — 2 байта

10 — не определен (например, для останова при выполнении)

11 — 4 байта

биты 29 – 28: поле R/W для точки останова 3 (тип точки останова)

00 — при выполнении команды

01 — при записи

10 — при обращении к порту (если бит DE в регистре CR4 = 1)

11 — при чтении или записи

биты 27 – 26: иоле LEN для точки останова 2

биты 25 – 24: поле R/W для точки останова 2

биты 23 – 22: поле LEN для точки останова 1

биты 21 – 20: поле R/W для точки останова 1

биты 19 – 18: поле LEN для точки останова 0

биты 17 – 16: поле R/W для точки останова 0

биты 15 – 14: 00

бит 13: бит GD — включает режим, в котором любое обращение к любому отладочному регистру, даже из кольца защиты 0, вызывает исключение #DB (этот бит автоматически сбрасывается внутри обработчика этого исключения)

биты 12 – 10: 001

бит 9: бит GE — если этот бит 0, точка останова по обращению к данным может не сработать или сработать на несколько команд позже, так что лучше всегда сохранять его равным 1

бит 7: бит G3 — точка останова 3 включена

бит 5: бит G2 — точка останова 2 включена

бит 3: бит G1 — точка останова 1 включена


бит 2: бит G0 — точка останова 0 включена

биты 8, 6, 4, 2, 0: биты LE, L3, L2, L1, L0 — действуют так же, как GE – G0, но обнуляются при переключении задачи (локальные точки останова)

DR6 (DSR) — регистр состояния отладки — содержит информацию о причине отладочного останова для обработчика исключения #DB

биты 31 – 16: единицы

бит 15: ВТ — причина прерывания — отладочный бит в TSS задачи, в которую только что произошло переключение

бит 14: BS — причина прерывания — флаг трассировки ТF из регистра FLAGS

бит 13: BD — причина прерывания — следующая команда собирается писать или читать отладочный регистр, и бит GD в DR7 установлен в 1

бит 12: 0

биты 11 – 4: единицы

бит 3: B3 — выполнился останов в точке 3

бит 2: B2 — выполнился останов в точке 2

бит 1: B1 — выполнился останов в точке 1

бит 0: B0 — выполнился останов в точке 0

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

DR4 – DR5 зарезервированы. На процессорах до Pentium, или если бит DE регистра CR4 равен нулю, обращение к этим регистрам приводит к обращению к DR6 и DR7 соответственно. Если бит DE = 1, происходит исключение #UD

DR0 – DR3 содержат 32-битные линейные адреса четырех возможных точек останова по доступу к памяти

Если условия для отладочного останова выполняются, процессор вызывает исключение #DB.


Переключение задач


Переключение задач осуществляется, если:

текущая задача выполняет дальний JMP или CALL на шлюз задачи или прямо на TSS;

текущая задача выполняет IRET, если флаг NT равен 1;

происходит прерывание или исключение, в качестве обработчика которого в IDT записан шлюз задачи.

При переключении процессор выполняет следующие действия:

Для команд CALL и JMP проверяет привилегии (CPL текущей задачи и RPL селектора новой задачи не могут быть больше, чем DPL шлюза или TSS, на который передается управление).

Проверяется дескриптор TSS (его бит присутствия и лимит).

Проверяется, что новый TSS, старый TSS и все дескрипторы сегментов находятся в страницах, отмеченных как присутствующие.

Сохраняется состояние задачи.

Загружается регистр TR. Если на следующих шагах происходит исключение, его обработчику придется доделывать переключение задач, вместо того чтобы повторять ошибочную команду.

Тип новой задачи в дескрипторе изменяется на занятый и устанавливается флаг TS в CR0.

Загружается состояние задачи из нового TSS: LDTR, CR3, EFLAGS, EIP, регистры общего назначения и сегментные регистры.

Если переключение задачи вызывается командами JUMP, CALL, прерыванием или исключением, селектор TSS предыдущей задачи записывается в поле связи новой задачи и устанавливается флаг NT. Если флаг NT установлен, команда IRET выполняет обратное переключение задач.

При любом запуске задачи ее тип изменяется в дескрипторе на занятый. Попытка вызвать такую задачу приводит к #GP, сделать задачу снова свободной можно, только завершив ее командой IRET или переключившись на другую задачу командой JMP.

Покажем, как создавать задачи и переключаться между ними на следующем примере.

; pm4.asm ; Пример программы, выполняющей переключение задач. ; Запускает две задачи, передающие управление друг другу 80 раз, задачи выводят ; на экран символы ASCII с небольшой задержкой ; ; Компиляция: ; TASM: ; tasm /m pm4.asm ; tlink /x /3 pm4.obj ; WASM: ; wasm pm4.asm ; wlink file pm4.obj form DOS ; MASM: ; ml /c pm4.asm ; link pm4.obj,,NUL,,,


. 386p RM_seg segment para public "CODE" use16 assume cs:RM_seg,ds:PM_seg,ss:stack_seg start: ; подготовить сегментные регистры push PM_seg pop ds ; проверить, не находимся ли мы уже в РМ mov eax,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,offset v86_msg err_exit: push cs pop ds mov ah,9 int 21h mov ah,4Ch int 21h

; убедиться, что мы не под Windows no_V86: mov ax,1600h int 2Fh test al,al jz no_windows ; сообщить и выйти mov dx,offset win_msg jmp short err_exit

; сообщения об ошибках при старте v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"

; итак, мы точно находимся в реальном режиме no_windows: ; очистить экран mov ax,3 int 10h ; вычислить базы для всех дескрипторов сегментов данных xor еах,еах mov ax,RM_seg shl eax,4 mov word ptr GDT_16bitCS+2,ax shr eax,16 mov byte ptr GDT_16bitCS+4,al mov ax,PM_seg shl eax,4 mov word ptr GDT_32bitCS+2,ax mov word ptr GDT_32bitSS+2,ax shr eax,16 mov byte ptr GDT_32bitCS+4,al mov byte ptr GDT_32bitSS+4,al ; вычислить линейный адрес GDT xor eax,eax mov ax,PM_seg shl eax,4 push eax add eax,offset GDT mov dword ptr gdtr+2,eax ; загрузить GDT lgdt fword ptr gdtr ; вычислить линейные адреса сегментов TSS наших двух задач pop eax push eax add eax,offset TSS_0 mov word ptr GDT_TSS0+2,ax shr eax,16 mov byte ptr GDT_TSS0+4,al pop eax add eax,offset TSS_1 mov word ptr GDT_TSS1+2,ax shr eax,16 mov byte ptr GDT_TSS1+4,al ; открыть А20 mov al,2 out 92h,al ; запретить прерывания cli ; запретить NMI in al,70h or al,80h out 70h,al ; переключиться в РМ mov eax,cr0 or al,1 mov cr0,eax ; загрузить CS db 66h db 0EAh dd offset PM_entry dw SEL_32bitCS

RM_return: ; переключиться в реальный режим RM mov eax,cr0 and al,0FEh mov cr0,eax ; сбросить очередь предвыборки и загрузить CS db 0EAh dw $+4 dw RM_seg ; настроить сегментные регистры для реального режима mov ax,PM_seg mov ds,ax mov es,ax mov ax,stack_seg mov bx,stack_l mov ss,ax mov sp,bx ; разрешить NMI in al,70h and al,07FH out 70h,al ; разрешить прерывания sti ; завершить программу mov ah,4Ch int 21h RM_seg ends



PM_seg segment para public "CODE" use32 assume cs:PM_seg

; таблица глобальных дескрипторов GDT label byte db 8 dup(0) GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 GDT_32bitCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 GDT_32bitSS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; сегмент TSS задачи 0 (32-битный свободный TSS) GDT_TSS0 db 067h,0,0,0,0,10001001b,01000000b,0 ; сегмент TSS задачи 1 (32-битный свободный TSS) GDT_TSS1 db 067h,0,0,0,0,10001001b,01000000b,0 gdt_size = $ - GDT gdtr dw gdt_size-1 ; размер GDT dd ? ; адрес GDT ; используемые селекторы SEL_flatDS equ 001000b SEL_16bitCS equ 010000b SEL_32bitCS equ 011000b SEL_32bitSS equ 100000b SEL_TSS0 equ 101000b SEL_TSS1 equ 110000b

; сегмент TSS_0 будет инициализирован, как только мы выполним переключение ; из нашей основной задачи. Конечно, если бы мы собирались использовать ; несколько уровней привилегий, то нужно было бы инициализировать стеки TSS_0 db 68h dup(0) ; сегмент TSS_1. В него будет выполняться переключение, так что надо ; инициализировать все, что может потребоваться: TSS_1 dd 0,0,0,0,0,0,0,0 ; связь, стеки, CR3 dd offset task_1 ; EIP ; регистры общего назначения dd 0,0,0,0,0,stack_l2,0,0,0B8140h ; (ESP и EDI) ; сегментные регистры dd SEL_flatDS,SEL_32bitCS,SEL_32bitSS,SEL_flatDS,0,0 dd 0 ; LDTR dd 0 ; адрес таблицы ввода-вывода

; точка входа в 32-битный защищенный режим PM_entry: ; подготовить регистры xor еах,еах mov ax,SEL_flatDS mov ds,ax mov es,ax mov ax,SEL_32bitSS mov ebx,stack_l mov ss,ax mov esp,ebx ; загрузить TSS задачи 0 в регистр TR mov ax,SEL_TSS0 ltr ax ; только теперь наша программа выполнила все требования к переходу ; в защищенный режим xor еах,еах mov edi,0B8000h ; DS:EDI - адрес начала экрана task_0: mov byte ptr ds:[edi],al ; вывести символ AL на экран ; дальний переход на TSS задачи 1 db 0EAh dd 0 dw SEL_TSS1 add edi,2 ; DS:EDI - адрес следующего символа inc al ; AL - код следующего символа, cmp al,80 ; если это 80, jb task_0 ; выйти из цикла ; дальний переход на процедуру выхода в реальный режим db 0EAh dd offset RM_return dw SEL_16bitCS



; задача 1 task_1: mov byte ptr ds:[edi],al ; вывести символ на экран inc al ; увеличить код символа add edi,2 ; увеличить адрес символа ; переключиться на задачу 0 db 0EAh dd 0 dw SEL_TSS0 ; сюда будет приходить управление, когда задача 0 начнет выполнять переход ; на задачу 1 во всех случаях, кроме первого mov ecx,02000000h ; небольшая пауза, зависящая от скорости loop $ ; процессора jmp task_1

PM_seg ends

stack_seg segment para stack "STACK" stack_start db 100h dup(?) ; стек задачи 0 stack_l = $ - stack_start stack_task2 db 100h dup(?) ; стек задачи 1 stack_l2 = $ - stack_start stack_seg ends

end start

Чтобы реализовать многозадачность в реальном времени в нашем примере, достаточно создать обработчик прерывания системного таймера IRQ0 в виде отдельной (третьей) задачи и поместить в IDT шлюз этой задачи. Текст обработчика для нашего примера мог быть крайне простым:

task_3: ; это отдельная задача - не нужно сохранять регистры! mov al,20h out 20h,al jmp task_0 mov al,20h out 20h,al jmp task_1 jmp task_3

Но при вызове обработчика прерывания старая задача помечается как занятая в GDT и повторный JMP на нее приведет к ошибке. Вызов задачи обработчика прерывания, так же как и вызов задачи командой CALL, подразумевает, что она завершится командой IRET. Именно команду IRET оказывается проще всего вызвать для передачи управления из такого обработчика — достаточно только подменить селектор вызвавшей нас задачи в поле связи и выполнить IRET.

task_3: ; при инициализации DS должен быть установлен на PM_seg mov al,20h out 20h,al mov word ptr TSS_3,SEL_TSS0 iret mov al,20h out 20h,al mov word ptr TSS_3,SEL_TSS1 iret jmp task_3

Единственное дополнительное изменение, которое нужно внести, — инициализировать дескриптор TSS задачи task_1 уже как занятый, так как управление на него будет передаваться командой IRET, что, впрочем, не составляет никаких проблем.

Помните, что во вложенных задачах команда IRET не означает конца программы — следующий вызов задачи всегда передает управление на следующую после IRET команду.


Если происходит прерывание или исключение


Если происходит прерывание или исключение в режиме V86, процессор анализирует биты IOPL регистра флагов, бит VME регистра CR4 (Pentium и выше) и соответствующий бит из карты перенаправления прерываний данной задачи (только если VME = 1).

Эта карта — 32-байтное поле, находящееся в регистре TSS данной задачи, на первый байт за концом которой указывает смещение в TSS по адресу +66h. Каждый из 256 бит этого поля соответствует одному номеру прерывания. Если он установлен в 1, прерывание должно подготавливаться обработчиком из IDT в защищенном режиме, если он 0 — то 16-битным обработчиком из реального режима.

Если VME = 0, прерывание обрабатывается (обработчиком из IDT), только если IOPL = 3, иначе вызывается исключение #GP.

Если бит VME = 1 и IOPL = 3, обработка прерывания определяется битом из битовой карты перенаправления прерываний.

Если VME = 1, IOPL < 3 и бит в битовой карте равен единице, вызывается обработчик из IDT.

Если VME = 1, IOPL < 3 и бит в битовой карте равен нулю, происходит следующее:

если VIF = 0 или если VIF = 1, но произошло исключение или NMI — вызывается обработчик из реального режима;

если VIF = 1 и произошло аппаратное прерывание — вызывается обработчик #GP из защищенного режима, который должен обработать прерывание, установить флаг VIP в копии EFLAGS в стеке и вернуться в V86;

если VIP = 1 и VIF = 0 из-за выполненной в V86 команды CLI, вызывается обработчик #GP из реального режима, который должен обнулить VIF и VIP в копии EFLAGS в стеке.

Бит VIF — это флаг, появившийся в Pentium для облегчения поддержки команд CLI и STI в V86-задачах. Если в регистре CR4 установлен бит VME, команды CLI/STI изменяют значение именно этого флага, оставляя IF нетронутым для того, чтобы операционная система могла обрабатывать прерывания и управлять другими задачами.

При вызове обработчика, располагающегося в защищенном режиме, из реального в стек нулевого уровня привилегий помещаются GS, FS, DS, ES, SS, EFLAGS, CS, EIP и код ошибки для некоторых исключений в этом порядке, и обнуляются флаги VM, TF и IF, если вызывается шлюз прерывания.


Пример программы


Мы будем пользоваться различными дескрипторами по мере надобности, а для начала выполним переключение в 32-битную модель памяти flat, где все сегменты имеют базу 0 и лимит 4 Гб. Нам потребуются два дескриптора — один для кода и один для данных. Кроме того, нужны два 16-битных дескриптора с лимитами 64 Кб, чтобы загрузить их в CS и DS перед возвратом в реальный режим.

В комментариях к примеру pm0.asm мы заметили, что его можно выполнять в DOS-окне Windows 95, хотя программа и запускается уже в защищенном режиме. Это происходит потому, что Windows 95 перехватывает обращения к контрольным регистрам и позволяет программе перейти в защищенный режим, но только с минимальным уровнем привилегий. Все следующие наши примеры в этом разделе будут рассчитаны на работу с максимальными привилегиями, поэтому добавим в программу проверку на запуск из-под Windows (функция 1600h прерывания мультиплексора INT 2Fh).

Еще одно дополнительное действие, которое будем теперь выполнять при переключении в защищенный режим, — управление линией А20. После запуска компьютера для совместимости с 8086 используются 20-разрядные адреса (работают адресные линии А0 – А19), так что попытка записать что-то по линейному адресу 100000h приведет к записи по адресу 0000h. Этот режим отменяется установкой бита 2 в порту 92h и снова включается сбрасыванием этого бита в 0. (Существуют и другие способы, зависящие от набора микросхем, используемых на материнской плате, но они бывают необходимы, только если требуется максимально возможная скорость переключения.)

; pm1.asm ; Программа, демонстрирующая работу с сегментами в защищенном режиме, ; переключается в модель flat, выполняет вывод на экран и возвращается в DOS ; ; Компиляция: TASM: ; tasm /m pm1.asm ; tlink /x /3 pm1.obj ; MASM: ; ml /c pm1.asm ; link pm1.obj,,NUL,,, ; WASM: ; wasm pm1.asm ; wlink file pm1.obj form DOS

.386p ; 32-битный защищенный режим появился в 80386

; 16-битный сегмент, в котором находится код для входа ; и выхода из защищенного режима RM_seg segment para public "code" use16 assume CS:RM_seg,SS:RM_stack start: ; подготовить сегментные регистры push cs pop ds ; проверить, не находимся ли мы уже в РМ mov еах,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,offset v86_msg err_exit: mov ah,9 int 21h mov ah,4Ch int 21h, v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"


; может быть, это Windows 95 делает вид, что РЕ = 0? no_V86: mov ax,1600h ; Функция 1600h int 2Fh ; прерывания мультиплексора, test al,al ; если AL = 0, jz no_windows ; Windows не запущена ; сообщить и выйти, если мы под Windows mov dx,offset win_msg jmp short err_exit
; итак, мы точно находимся в реальном режиме no_windows: ; если мы собираемся работать с 32-битной памятью, стоит открыть А20 in al,92h or al,2 out 92h,al ; вычислить линейный адрес метки PM_entry xor еах,еах mov ax,PM_seg ; АХ - сегментный адрес PM_seg shl eax,4 ; ЕАХ - линейный адрес PM_seg add eax,offset PM_entry ; EAX - линейный адрес PM_entry mov dword ptr pm_entry_off,eax ; сохранить его ; вычислить базу для GDT_16bitCS и GDT_16bitDS xor eax,eax mov ax,cs ; AX - сегментный адрес RM_seg shl eax,4 ; ЕАХ - линейный адрес RM_seg push eax mov word ptr GDT_16bitCS+2,ax ; биты 15 - 0 mov word ptr GDT_16bitDS+2,ax shr eax,16 mov byte ptr GDT_16bitCS+4,al ; и биты 23 - 16 mov byte ptr GDT_16bitDS+4,al ; вычислить абсолютный адрес метки GDT pop eax ; EAX - линейный адрес RM_seg add ax,offset GDI ; EAX - линейный адрес GDT mov dword ptr gdtr+2,eax ; записать его для GDTR ; загрузить таблицу глобальных дескрипторов lgdt fword ptr gdtr ; запретить прерывания cli ; запретить немаскируемое прерывание in al,70h or al,80h out 70h,al ; переключиться в защищенный режим mov eax,cr0 or al,1 mov cr0,eax ; загрузить новый селектор в регистр CS db 66h ; префикс изменения разрядности операнда db 0EAh ; код команды дальнего jmp pm_entry_off dd ? ; 32-битное смещение dw SEL_flatCS ; селектор RM_return: ; сюда передается управление при выходе из защищенного режима ; переключиться в реальный режим mov еах,cr0 and al,0FEh mov cr0,eax ; сбросить очередь предвыборки и загрузить CS реальным сегментным адресом db 0EAh ; код дальнего jmp dw $+4 ; адрес следующей команды dw RM_seg ; сегментный адрес RM_seg ; разрешить NMI in al,70h and al,07Fh out 70h,al ; разрешить другие прерывания sti ; подождать нажатия любой клавиши mov ah,0 int 16h ; выйти из программы mov ah,4Ch int 21h ; текст сообщения с атрибутами, который мы будем выводить на экран message db 'Н',7,'е',7,'l',7,'l',7,'о',7,' ',7,'и',7,'з',7,' ',7 db '3',7,'2',7,'-',7,'б',7,'и',7,'т',7,'н',7,'о',7,'г',7 db 'о',7,' ',7,'Р',7,'М' message_l = $ - message ; длина в байтах rest_scr = (80*25*2-message_l)/4 ; длина оставшейся части экрана ; в двойных словах ; таблица глобальных дескрипторов GDT label byte ; нулевой дескриптор (обязательно должен быть на первом месте) db 8 dup(0) ; 4-гигабайтный код, DPL = 00: GDT_flatCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 ; 4-гигабайтные данные, DPL = 00: GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; 64-килобайтный код, DPL = 00: GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 ; 64-килобайтные данные, DPL = 00: GDT_16bitDS db 0FFh,0FFh,0,0,0,10010010b,0,0 GDT_l = $ - GDT ; размер GDT

Процессоры Intel в защищенном режиме


Мы уже неоднократно сталкивались с защищенным режимом и даже программировали приложения, которые работали в нем (главы 6 и 7). но мы пользовались только средствами, которые предоставляла операционная система, и до сих пор не рассматривали то, как процессор переходит и функционирует в защищенном режиме, то есть как работают современные операционные системы. Дело в том, что управление защищенным режимом в современных процессорах Intel — это самый сложный раздел программирования и самая сложная глава в этой книге. Но материал можно легко освоить, если рассматривать этот раздел шаг за шагом — отдельные механизмы работы процессора достаточно мало перекрываются друг с другом. Прежде чем рассматривать собственно программирование, познакомимся с регистрами и командами процессора, которые пока были от нас скрыты.



Проверка лимитов


Поле лимита в дескрипторе сегмента запрещает доступ к памяти за пределами сегмента. Если бит G дескриптора равен нулю, значения лимита могут быть от 0 до FFFFFh (1 Мб). Если бит G установлен — от FFFh (4 Кб) до FFFFFFFFh (4 Гб). Для сегментов, растущих вниз, лимит принимает значения от указанного плюс 1 до FFFFh для 16-битных сегментов данных и до FFFFFFFFh — для 32-битных. Эти проверки отлавливают такие ошибки, как неправильные вычисления адресов.

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

Во всех случаях вызывается исключение #GP с кодом ошибки, равным индексу селектора, вызвавшего нарушение защиты.



Проверка привилегий


Все неравенства здесь арифметические, то есть А > В означает, что уровень привилегий А меньше, чем В:

при загрузке регистра DS, ES, FS или GS должно выполняться условие: DPL >= max(RPL,CPL);

при загрузке регистров SS должно выполняться условие: DPL = CPL = RPL;

при дальних JMP, CALL, RET на неподчиненный сегмент кода должно выполняться условие: DPL = CPL (RPL игнорируется);

при дальних JMP, CALL, RET на подчиненный сегмент кода должно выполняться условие: CPL >= DPL. При этом CPL не изменяется;

при дальнем CALL на шлюз вызова должны выполняться условия: CPL =< DPL шлюза, RPL =< DPL шлюза, CPL >= DPL сегмента;

при дальнем JMP на шлюз вызова должны выполняться условия: CPL =< DPL шлюза, RPL =< DPL шлюза, CPL >= DPL сегмента, если он подчиненный, CPL = DPL сегмента, если он неподчиненный.

При вызове процедуры через шлюз на неподчиненный сегмент кода с другим уровнем привилегий процессор выполняет переключение стека. В сегменте TSS текущей задачи всегда хранятся значения SS:ESP для стеков уровней привилегий 0, 1 и 2 (стек для уровня привилегий 3, потому что нельзя выполнять передачу управления на уровень 3, кроме как при помощи команд RET/IRET). При переключении стека в новый стек помещаются, до обратного адреса, параметров (их число указано в дескрипторе шлюза вызова), флагов или кода ошибки (в случае INT), старые значения SS:ESP, которые команда RET/IRET использует для обратного переключения. То, что надо выполнить возврат из процедуры, RET определяет так: RPL селектора, оставленного в стеке, больше (менее привилегированный), чем CPL.

Даже если операционная система не поддерживает многозадачность, она должна оформить сегмент TSS с действительными SS:ESP для стеков всех уровней, если она собирается использовать уровни привилегий.



Проверка типа сегмента


Загрузка селектора (и дескриптора) в регистр:

в CS можно загрузить только сегмент кода;

в DS, ES, FS, GS можно загрузить только селектор сегмента данных, сегмента кода, доступного для чтения, или нулевой селектор;

в SS можно загрузить только сегмент данных, доступный для записи;

в LDTR можно загрузить только сегмент LDT;

в TR можно загрузить только сегмент TSS.

Обращение к памяти:

никакая команда не может писать в сегмент кода;

никакая команда не может писать в сегмент данных, защищенный от записи;

никакая команда не может читать из сегмента кода, защищенного от чтения;

нельзя обращаться к памяти, если селектор в сегментном регистре нулевой.

Исполнение команды, использующей селектор в качестве операнда:

дальние CALL и JMP могут выполняться только в сегмент кода, шлюз вызова, шлюз задачи или сегмент TSS;

команда LLDT может обращаться только к сегменту LDT;

команда LTR может обращаться только к сегменту TSS;

команда LAR может обращаться только к сегментам кода и данных, шлюзам вызова и задачи, LDT и TSS;

команда LSL может обращаться только к сегментам кода, данных, LDT и TSS;

элементами IDT могут быть только шлюзы прерываний, ловушек и задач.

Некоторые внутренние операции:

при переключении задач целевой дескриптор может быть только TSS или шлюзом задачи;

при передаче управления через шлюз сегмент, на который шлюз указывает, должен быть сегментом кода (или TSS для шлюза задачи);

при возвращении из вложенной задачи селектор в поле связи TSS должен быть селектором сегмента TSS.



Регистры


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



Регистры управления памятью


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

GDTR: 6-байтный регистр, в котором содержатся 32-битный линейный адрес начала таблицы глобальных дескрипторов (GDT) и ее 16-битный размер (минус 1). Каждый раз, когда происходит обращение к памяти, по селектору, находящемуся в сегментном регистре, определяется дескриптор из таблицы GDT или LDT, в котором записан адрес начала сегмента и другая информация (см. главу 6.1).

IDTR: 6-байтный регистр, в котором содержатся 32-битный линейный адрес начала таблицы глобальных дескрипторов обработчиков прерываний (IDT) и ее 16-битный размер (минус 1). Каждый раз, когда происходит прерывание или исключение, процессор передает управление на обработчик, описываемый дескриптором из IDT с соответствующим номером.

LDTR: 10-байтный регистр, в котором содержатся 16-битный селектор для GDT и весь 8-байтный дескриптор из GDT, описывающий текущую таблицу локальных дескрипторов (LDT).

TR: 10-байтный регистр, в котором содержатся 16-битный селектор для GDT и весь 8-байтный дескриптор из GDT, описывающий TSS текущей задачи.



Регистры управления процессором


Пять 32-битных регистров CR0 – CR4 управляют функционированием процессора и работой отдельных его внутренних блоков.

CR0: флаги управления системой

бит 31: бит PG — включает и выключает режим страничной адресации

бит 30: бит CD — запрещает заполнение кэша. При этом чтение из кэша все равно будет происходить

бит 29: бит NW — запрещает сквозную запись во внутренний кэш — данные, записываемые в кэш, не появляются на внешних выводах процессора

бит 18: бит AM — разрешает флагу АС включать режим, в котором невыровненные обращения к памяти на уровне привилегий 3 вызывают исключение #АС

бит 16: бит WP — запрещает запись в страницы, помеченные как только для чтения на всех уровнях привилегий (если WP = 0, защита распространяется только на уровень 3). Этот бит предназначен для реализации метода создания копии процесса, популярного в UNIX, в котором вся память нового процесса сначала полностью совпадает со старым, а затем, при попытке записи со стороны нового процесса, создается копия страницы, в которую произошла запись

бит 5: бит NE — включает режим, в котором ошибки FPU вызывают исключение #MF, а не IRQ13

бит 4: бит ЕТ — использовался только на 80386DX и указывал, что FPU присутствует

бит 3: бит TS — устанавливается процессором после переключения задачи. Если затем выполнить любую команду FPU, произойдет исключение #NM, обработчик которого может сохранить/восстановить состояние FPU, очистить этот бит командой CLTS и продолжить программу

бит 2: бит ЕМ — эмуляция сопроцессора. Каждая команда FPU вызывает исключение #NM

бит 1: бит МР — управляет тем, как исполняется команда WAIT. Должен быть установлен для совместимости с программами, написанными для 80286 и 80386 и использующими эту команду

бит 0: бит РЕ — если он равен 1, процессор находится в защищенном режиме


(остальные биты зарезервированы, и программы не должны изменять их значения)

CR1: зарезервирован

CR2: регистр адреса ошибки страницы

Когда происходит исключение #PF, из этого регистра можно прочитать линейный адрес, обращение к которому вызвало исключение.

CR3 (PDBR): регистр основной таблицы страниц

биты 31 – 11: 20 старших бит физического адреса начала каталога страниц, если бит РАЕ в CR4 равен нулю, или

биты 31 – 5: 27 старших бит физического адреса таблицы указателей на каталоги страниц, если бит РАЕ = 1

бит 4 (80486+): бит PCD (запрещение кэширования страниц) — этот бит запрещает загрузку текущей страницы в кэш-память (например, если произошло прерывание и система не хочет, чтобы обработчик прерывания вытеснил основную программу из кэша)

бит 3 (80486+): бит PWT (бит сквозной записи страниц) — управляет методом записи страниц во внешний кэш

CR4: этот регистр (появился только в процессорах Pentium) управляет новыми возможностями процессоров. Все эти возможности необязательно присутствуют, и их надо сначала проверять при помощи команды CPUID

бит 9: бит FSR — разрешает команды быстрого сохранения/восстановления состояния FPU/MMX FXSAVE и FXRSTOR (Pentium II)

бит 8: бит РМС — разрешает выполнение команды RDPMC для программ на всех уровнях привилегий (его PMC = 0, но только на уровне 0) (Pentium Pro и выше)

бит 7: бит PGE — разрешает глобальные страницы (бит 8 атрибута страницы), которые не удаляются из TLB при переключении задач и записи в CR3 (Pentium Pro и выше)

бит 6: бит МСЕ — разрешает исключение #МС

бит 5: бит РАЕ — включает 36-битное физическое адресное пространство (Pentium Pro и выше)

бит 4: бит PSE — включает режим адресации с 4-мегабайтными страницами

бит 3: бит DE — запрещает отладочные прерывания по обращению к портам

бит 2: бит TSD — запрещает выполнение команды RDTSC для всех программ, кроме программ, выполняющихся на уровне привилегий 0

бит 1: бит PVI — разрешает работу флага VIF в защищенном режиме, что может позволить некоторым программам, написанным для уровня привилегий 0, работать на более низких уровнях

бит 0: бит VME — включает расширения режима V86 — разрешает работу флага VIF для V86-приложений


в которой флаг VM регистра


Режим V86 — это задача, исполняющаяся в защищенном режиме, в которой флаг VM регистра EFLAGS равен единице. Внутри этой задачи процессор ведет себя так, как если бы он находился в реальном режиме, за исключением того, что прерывания и исключения передаются обработчикам защищенного режима вне этой задачи (кроме случая, когда используется карта перенаправления прерываний).

Программы не могут изменить флаг VM. Его можно установить, только записав образ EFLAGS с установленным VM при создании TSS новой задачи и затем переключившись на нее. Кроме этой задачи для нормальной реализации V86 требуется монитор режима (VMM) — модуль, который выполняется с CPL = 0 и обрабатывает прерывания, исключения и обращения к портам ввода-вывода из задачи V86, выполняя фактически эмуляцию всего компьютера.

Чтобы выполнять в системе сразу несколько V86-задач, применяется страничная адресация. Каждая V86-задача использует ровно один мегабайт линейного адресного пространства, который можно отобразить на любую область физического.

Процессор переключается в V86 в трех ситуациях:

при переключении в задачу, в TSS которой установлен флаг VM;

при выполнении команды IRET, если NT = 0 и установлен VM в копии EFLAGS в стеке;

при выполнении команды IRET, если NT = 1 и установлен VM в копии EFLAGS в TSS.


Сегмент состояния задачи


Сегмент состояния задачи (TSS) — это структура данных, в которой сохраняется вся информация о задаче, если ее выполнение временно прерывается.

TSS имеет следующую структуру:

+00h: 4 байта — селектор предыдущей задачи (старшее слово содержит нули — здесь и для всех остальных селекторов)

+04h: 4 байта — ESP для CPL = 0

+08h: 4 байта — SS для CPL = 0

+0Ch: 4 байта — ESP для CPL = 1

+10h: 4 байта — SS для CPL = 1

+14h: 4 байта — ESP для CPL = 2

+18h: 4 байта — SS для CPL = 2

+1Сh: 4 байта — CR3

+20h: 4 байта — EIP

+24h: 4 байта — EFLAGS

+28h: 4 байта — ЕАХ

+2Ch: 4 байта — ЕСХ

+30h: 4 байта — EDX

+34h: 4 байта — ЕВХ

+38h: 4 байта — ESP

+3Ch: 4 байта — ЕВР

+40h: 4 байта — ESI

+44h: 4 байта — EDI

+48h: 4 байта — ES

+4Ch: 4 байта — CS

+50h: 4 байта — SS

+54h: 4 байта — DS

+58Н: 4 байта — FS

+5Ch: 4 байта — GS

+60h: 4 байта — LDTR

+64h: 2 байта — слово флагов задачи. Бит 0 — флаг Т: вызывает #DB при переключении на задачу остальные биты не определены и равны нулю

+66h: 2 байта — адрес битовой карты ввода-вывода. Это 16-битное смещение от начала TSS, по которому начинается битовая карта разрешения ввода-вывода (см. главы 10.7.4 и 10.9.2) и заканчивается битовая карта перенаправления прерываний (см. главу 10.9.1) данной задачи.

TSS является полноценным сегментом и описывается сегментным дескриптором, формат которого мы приводили раньше (в главе 10.4.3). Кроме того, лимит TSS не может быть меньше 67h — обращение к такому дескриптору приводит к исключению #TS. Размер TSS может быть больше, если в него входят битовые карты ввода-вывода и перенаправления прерываний и если операционная система хранит в нем дополнительную информацию. Дескриптор TSS способен находиться только в GDT — попытка загрузить его из LDT вызывает исключение #GP. Для передачи управления задачам удобнее использовать дескрипторы шлюза задачи, которые можно помещать как в GDT, так и в LDT или IDT.



Селектор


Селектор — это 16-битное число следующего формата:

биты 16 – 3: номер дескриптора в таблице (от 0 до 8191)

бит 2: 1 — использовать LDT, 0 — использовать GDT

биты 1 – 0: запрашиваемый уровень привилегий при обращении к сегменту и текущий уровень привилегий для селектора, загруженного в CS

Селектор, содержащий нулевые биты 16 – 3, называется нулевым и используется для загрузки в неиспользуемые сегментные регистры. Любое обращение в сегмент, адресуемый нулевым селектором, приводит к исключению #GP(0), в то время как даже загрузка в сегментный регистр ошибочного селектора вызывает исключение #GР(селектор). Попытка загрузки нулевого селектора в SS или CS также вызывает #GP(0), так как эти селекторы используются всегда.



Системные флаги


Регистр флагов EFLAGS — это 32-битный регистр, в то время как в главе 2.1.4 рассмотрена только часть из младших 16 бит. Теперь мы можем обсудить все:

биты 31 – 22: нули

бит 21: флаг идентификации (ID)

бит 20: флаг ожидания виртуального прерывания (VIP)

бит 19: флаг виртуального прерывания (VIF)

бит 18: флаг контроля за выравниванием (АС)

бит 17: флаг режима V86 (VM)

бит 16: флаг продолжения задачи (RF)

бит 15: 0

бит 14: флаг вложенной задачи (NT)

биты 13 – 12: уровень привилегий ввода-вывода (IOPL)

бит 11: флаг переполнения (OF)

бит 10: флаг направления (DF)

бит 9: флаг разрешения прерываний (IF)

бит 8: флаг трассировки (TF)

биты 7 – 0: флаги состояния (SF, ZF, AF, PF, CF) были рассмотрены подробно раньше

Флаг TF: если он равен 1, перед выполнением каждой команды генерируется исключение #DB (INT 1).
Флаг IF: если он равен 0, процессор не реагирует ни на какие маскируемые аппаратные прерывания.
Флаг DP: если он равен 1, регистры EDI/ESI при выполнении команд строковой обработки уменьшаются, иначе — увеличиваются.
Поле IOPL: уровень привилегий ввода-вывода, с которым выполняется текущая программа или задача. Чтобы программа могла обратиться к порту ввода-вывода, ее текущий уровень привилегий (CPL) должен быть меньше или равен IOPL. Это поле можно модифицировать, только имея нулевой уровень привилегий.
Флаг NT: равен 1, если текущая задача является вложенной по отношению к какой-то другой — в обработчиках прерываний и исключений и вызванных командой call задачах. Флаг влияет на работу команды IRET.
Флаг RF: когда этот флаг равен 1, отладочные исключения временно запрещены. Он устанавливается командой IRETD из обработчика отладочного прерывания, чтобы #DB не произошло перед выполнением команды, которая его вызвала, еще раз. На флаг не влияют команды POPF, PUSHF и IRET.
Флаг VM: установка этого флага переводит процессор в режим V86 (виртуальный 8086).
Флаг АС: если установить этот флаг и флаг AM в регистре CR0, каждое обращение к памяти из программ, выполняющихся с CPL = 3, не выровненное на границу слова для слов и на границу двойного слова для двойных слов, будет вызывать исключение #АС.
Флаг VIF: это виртуальный образ флага IF (только для Pentium и выше).
Флаг VIP: этот флаг указывает процессору, что произошло аппаратное прерывание. Флаги VIF и VIP используются в многозадачных средах для того, чтобы каждая задача имела собственный виртуальный образ флага IF (только для Pentium и выше — см. главу 5.9.1).
Флаг ID: если программа может изменить значение этого флага — процессор поддерживает команду CPUID (только для Pentium и выше).



Системные и привилегированные команды


Команда: LGDT источник
Назначение: Загрузить регистр GDTR
Процессор: 80286

Команда загружает значение источника (6-байтная переменная в памяти) в регистр GDTR. Если текущая разрядность операндов 32 бита, в качестве размера таблицы глобальных дескрипторов используются младшие два байта операнда, а в качестве ее линейного адреса — следующие 4. Если текущая разрядность операндов — 16 бит, для линейного адреса используются только байты 3, 4, 5 из операнда, а в самый старший байт адреса записываются нули.

Команда выполняется только в реальном режиме или при CPL = 0.

Команда: SGDT приемник
Назначение: Прочитать регистр GDTR
Процессор: 80286

Помещает содержимое регистра GDTR в приемник (6-байтная переменная в памяти). Если текущая разрядность операндов — 16 бит, самый старший байт этой переменной заполняется нулями (начиная с 80386, а 286 заполнял его единицами).

Команда: LLDT источник
Назначение: Загрузить регистр LDTR
Процессор: 80286

Загружает регистр LDTR, основываясь на селекторе, находящемся в источнике (16-битном регистре или переменной). Если источник — 0, все команды, кроме LAR, LSL, VERR и VERW, обращающиеся к дескрипторам из LDT, будут вызывать исключение #GP.

Команда выполняется только в защищенном режиме с CPL = 0.

Команда: SLDT приемник
Назначение: Прочитать регистр LDTR
Процессор: 80286

Помещает селектор, находящийся в регистре LDTR, в приемник (16- или 32-битный регистр или переменная). Этот селектор указывает на дескриптор в GDT текущей LDT. Если приемник 32-битный, старшие 16 бит обнуляются на Pentium Pro и не определены на предыдущих процессорах.

Команда выполняется только в защищенном режиме.

Команда: LTR источник
Назначение: Загрузить регистр TR
Процессор: 80286

Загружает регистр задачи TR, основываясь на селекторе, находящемся в источнике (16-битном регистре или переменной), указывающем на сегмент состояния задачи (TSS). Эта команда обычно используется при инициализации системы для загрузки первой задачи в многозадачной системе.


Команда выполняется только в защищенном режиме с CPL = 0.

Команда: STR приемник
Назначение: Прочитать регистр TR
Процессор: 80286
Помещает селектор, находящийся в регистре TR, в приемник (16- или 32-битный регистр или переменная). Этот селектор указывает на дескриптор в GDT, описывающий TSS текущей задачи. Если приемник 32-битный, старшие 16 бит обнуляются на Pentium Pro и не определены на предыдущих процессорах.

Команда выполняется только в защищенном режиме.

Команда: LIDT источник
Назначение: Загрузить регистр IDTR
Процессор: 80286
Загружает значение источника (6-байтная переменная в памяти) в регистр IDTR. Если текущая разрядность операндов — 32 бита, в качестве размера таблицы глобальных дескрипторов используются младшие два байта операнда, а в качестве ее линейного адреса — следующие 4. Если текущая разрядность операндов — 16 бит, для линейного адреса используются только байты 3, 4, 5 из операнда, а самый старший байт адреса устанавливается нулевым.

Команда выполняется только в реальном режиме или при CPL = 0.

Команда: SIDT приемник
Назначение: Прочитать регистр IDTR
Процессор: 80286
Помещает содержимое регистра GDTR в приемник (6-байтная переменная в памяти). Если текущая разрядность операндов — 16 бит, самый старший байт этой переменной заполняется нулями (начиная с 80386, а 286 заполнял его единицами).

Команда: MOV приемник, источник
Назначение: Пересылка данных в/из управляющих и отладочных регистров
Процессор: 80386
Приемником или источником команды MOV могут быть регистры CR0 – CR4 и DR0 – DR7. В этом случае другой операнд команды обязательно должен быть 32-битным регистром общего назначения. При записи в регистр CR3 сбрасываются все записи в TLB, кроме глобальных страниц в Pentium Pro. При модификации бит РЕ или PG в CR0 и PGE, PSE или РАЕ в CR4 сбрасываются все записи в TLB без исключения.

Команды выполняются только в реальном режиме или с CPL = 0.



Команда: LMSW источник
Назначение: Загрузить слово состояния процессора
Процессор: 80286
Копирует младшие четыре бита источника (16-битный регистр или переменная) в регистр CR0, изменяя биты РЕ, МР, ЕМ и TS. Кроме того, если бит РЕ = 1, этой командой его нельзя обнулить, то есть нельзя выйти из защищенного режима. Команда LMSW существует только для совместимости с процессором 80286, и вместо нее всегда удобнее использовать mov cr0,еах.

Команда выполняется только в реальном режиме или с CPL = 0.

Команда: SMSW приемник
Назначение: Прочитать слово состояния процессора
Процессор: 80286
Копирует младшие 16 бит регистра CR0 в приемник (16- или 32-битный регистр или 16-битная переменная). Если приемник 32-битный, значения его старших бит не определены. Команда SMSW существует только для совместимости с процессором 80286, и вместо нее удобнее использовать mov еах,cr0.

Команда: CLTS
Назначение: Сбросить флаг TS в CR0
Процессор: 80286
Команда сбрасывает в 0 бит TS регистра CR0, который устанавливается процессором в 1 после каждого переключения задач. CLTS предназначена для синхронизации сохранения/восстановления состояния FPU в многозадачных операционных системах: первая же команда FPU в новой задаче при TS = 1 вызовет исключение #NM, обработчик которого сохранит состояние FPU для старой задачи и восстановит сохраненное ранее для новой, после чего выполнит команду CLTS и вернет управление.

Команда выполняется только в реальном режиме или с CPL = 0.

Команда: ARPL приемник,источник
Назначение: Коррекция поля RPL селектора
Процессор: 80286
Команда выполняет сравнение полей RPL двух сегментных селекторов. Приемник (16-битный регистр или переменная) содержит первый, а источник (16-битный регистр) содержит второй. Если RPL приемника меньше, чем RPL источника, устанавливается флаг ZF, и RPL приемника становится равным RPL источника. В противном случае ZF = 0 и никаких изменений не происходит. Обычно эта команда используется операционной системой, чтобы увеличить RPL селектора, переданного ей приложением, с целью удостовериться, что он соответствует уровню привилегий приложения (который система может взять из RPL сегмента кода приложения, находящегося в стеке).



Команда выполняется только в защищенном режиме (с любым CPL).

Команда: LAR приемник,источник
Назначение: Прочитать права доступа сегмента
Процессор: 80286
Копирует байты, отвечающие за права доступа из дескриптора, описываемого селектором, находящимся в источнике (регистр или переменная), в источник (регистр) и устанавливает флаг ZF Если используются 16-битные операнды, копируется только байт 5 дескриптора в байт 1 (биты 8 – 15) приемника. Для 32-битных операндов дополнительно копируются старшие 4 бита (для сегментов кода и данных) или весь шестой байт дескриптора (для системных сегментов) в байт 2 приемника. Остальные биты приемника обнуляются. Если CPL > DPL или RPL > DPL — для неподчиненных сегментов кода, если селектор или дескриптор ошибочны или в других ситуациях, в которых программа не сможет пользоваться этим селектором, команда LAR возвращает ZF = 0.

Команда выполняется только в защищенном режиме.

Команда: LSL приемник,источник
Назначение: Прочитать лимит сегмента
Процессор: 80286
Копирует лимит сегмента (размер минус 1) из дескриптора, селектор для которого находится в источнике (регистр или переменная), в приемник (регистр) и устанавливает флаг ZF в 1. Если бит гранулярности в дескрипторе установлен и лимит хранится в единицах по 4096 байт, команда LSL переведет его значение в байты. Если используются 16-битные операнды и лимит не умещается в приемнике, его старшие биты теряются. Так же, как и в случае LAR, эта команда проверяет доступность сегмента из текущей программы, и, если сегмент недоступен, в приемник ничего не загружается и флаг ZF сбрасывается в 0.

Команда выполняется только в защищенном режиме.

Команда: VERR источник
Назначение: Проверить права на чтение
Команда: VERW источник
Назначение: Проверить права на запись
Процессор: 80286
Команды проверяют, доступен ли сегмент кода или данных, селектор которого находится в источнике (16-битный регистр или переменная) для чтения (VERR) или записи (VERW), с текущего уровня привилегий. Если сегмент доступен, эти команды возвращают ZF = 1, иначе — ZF = 0.



Команды выполняются только в защищенном режиме.

Команда: INVD
Назначение: Сбросить кэш-память
Команда: WBINVD
Назначение: Записать и сбросить кэш-память
Процессор: 80486
Эти команды объявляют все содержимое внутренней кэш-памяти процессора недействительным и подают сигнал для сброса внешнего кэша, так что после этого все обращения к памяти приводят к заполнению кэша заново. Команда WBINVD предварительно сохраняет содержимое кэша в память, команда INVD приводит к потере всей информации, которая попала в кэш, но еще не была перенесена в память.

Команды выполняются только в реальном режиме или с CPL = 0.

Команда: INVLPG источник
Назначение: Аннулировать страницу
Процессор: 80486
Аннулирует (объявляет недействительным) элемент буфера TLB, описывающий страницу памяти, содержащую источник (адрес в памяти). Команда выполняется только в реальном режиме или с CPL = 0.

Команда: HLT
Назначение: Остановить процессор
Процессор: 8086
Переводит процессор в состояние останова, из которого его может вывести только аппаратное прерывание или перезагрузка. Если его выводит прерывание, то адрес возврата, помещаемый в стек для обработчика прерывания, указывает на следующую после HLT команду.

Команда выполняется только в реальном режиме или с CPL = 0.

Команда: RSM
Назначение: Выйти из режима SMM
Процессор: Р5
Применяется для вывода процессора из режима SMM, использующегося для сохранения состояния системы в критических ситуациях (например, при выключении электроэнергии). При входе в SMM (исполняется при поступлении соответствующего сигнала на процессор от материнской платы) все регистры, включая системные, и другая информация сохраняются в специальном блоке памяти — SMRAM, а при выходе (который и осуществляется командой RSM) все восстанавливается.

Команда выполняется только в режиме SMM.

Команда: RDMSR
Назначение: Чтение из MSR-регистра
Команда: WRMSR
Назначение: Запись в MSR-регистр
Процессор: Р5
<


/p> Помещает содержимое машинно-специфичного регистра с номером, указанным в ЕСХ, в пару регистров EDX:EAX (старшие 32 бита в EDX и младшие в ЕАХ) (RDMSR) или содержимое регистров EDX:EAX — в машинно-специфичный регистр с номером в ЕСХ. Попытка чтения/записи зарезервированного или отсутствующего в данной модели MSR приводит к исключению #GP(0).

Команда выполняется только в реальном режиме или с CPL = 0.

Команда: RDTSC
Назначение: Чтение из счетчика тактов процессора
Процессор: Р5
Помещает в регистровую пару EDX:EAX текущее значение счетчика тактов — 64-битного машинно-специфичного регистра TSC, значение которого увеличивается на 1 каждый такт процессора с момента его последней перезагрузки. Этот машинно-специфичный регистр доступен для чтения и записи при помощи команд RDMSR/WRMSR как регистр номер 10h, причем на Pentium Pro при записи в него старшие 32 бита всегда обнуляются. Так как машинно-специфичные регистры могут отсутствовать на отдельных моделях процессоров, их наличие всегда следует определять при помощи команды CPUID (бит 4 в EDX — наличие TSC).

Команда выполняется на любом уровне привилегий, если бит TSD в регистре CR0 равен нулю, и только в реальном режиме или с CPL = 0, если бит TSD = 1.

Команда: RDPMC
Назначение: Чтение из счетчика событий
Процессор: Р6
Помещает значение одного из двух программируемых счетчиков событий (40-битные машинно-специфичные регистры C1h и C2h для Pentium Pro и Pentium II) в регистровую пару EDX:EAX. Выбор читаемого регистра определяется числом 0 или 1 в ЕСХ. Аналогичные регистры есть и на Pentium (и Cyrix 6х86МХ), но они имеют номера 11h и 12h, и к ним можно обращаться только при помощи команд RDMSR/WRMSR.

Способ выбора типа подсчитываемых событий тоже различается между Pentium и Pentium Pro — для Pentium надо выполнить запись в 64-битный регистр MSR 011h, разные двойные слова которого управляют выбором режима каждого из счетчиков и типа посчитываемых событий, а для Pentium Pro/Pentium II надо выполнить запись в регистр 187h для счетчика 0 и 188h — для счетчика 1. Соответственно и наборы событий между этими процессорами сильно различаются: 38 событий на Pentium, 83 — на Pentium Pro и 96 — на Pentium II.



Команда: SYSENTER
Назначение: Быстрый системный вызов
Команда: SYSEXIT
Назначение: Быстрый возврат из системного вызова
Процессор: РII
Команда SYSENTER загружает в регистр CS число из регистра MSR #174h, в регистр EIP — число из регистра MSR #176h, в регистр SS — число, равное CS + 8 (селектор на следующий дескриптор), и в регистр ESP — число из MSR #175h. Эта команда предназначена для передачи управления операционной системе — ее можно вызывать с любым CPL, а вызываемый код должен находиться в бессегментной памяти с CPL = 0. На самом деле SYSENTER модифицирует дескрипторы используемых сегментов — сегмент кода будет иметь DPL = 0, базу 0, лимит 4 Гб, станет доступным для чтения и 32-битным, а сегмент стека также получит базу 0, лимит 4 Гб, DPL = 0, 32-битный режим, доступ для чтения/записи и установленный бит доступа. Кроме того, селекторы CS и SS получают RPL = 0.

Команда SYSEXIT загружает в регистр CS число, равное содержимому регистра MSR #174h плюс 16, в EIP — число из EDX, в SS — число, равное содержимому регистра MSR #174h плюс 24, и в ESP — число из ЕСХ. Эта команда предназначена для передачи управления в бессегментную модель памяти с CPL = 3 и она тоже модифицирует дескрипторы. Сегмент кода получает DPL = 3, базу 0, лимит 4 Гб, доступ для чтения, перестает быть подчиненным и становится 32-битным. Сегмент стека также получает базу 0, лимит 4 Гб, доступ для чтения/записи и 32-битную разрядность. Поля RPL в CS и SS устанавливаются в 3.

Поддержку команд SYSENTER/SYSEXIT всегда следует проверять при помощи команды CPUID (бит 11). Кроме того, надо убедиться, что номер модели процессора не меньше трех, так как Pentium Pro (тип процессора 6, модель 1) не имеет команд SYSENTER/SYSEXIT, но бит в CPUID возвращается равным 1.

SYSENTER выполняется только в защищенном режиме, SYSEXIT выполняется только с CPL = 0.


Страничная адресация


Линейный адрес, который формируется процессором из логического адреса, соответствует адресу из линейного непрерывного пространства памяти. В обычном режиме в это пространство могут попадать области памяти, в которые нежелательно разрешать запись, — системные таблицы и процедуры, ПЗУ BIOS и т.д. Чтобы этого избежать, система может разрешать программам создавать только небольшие сегменты, но тогда теряется такая привлекательная идея flat-памяти. Сегментация — не единственный вариант организации памяти, который поддерживают процессоры Intel. Существует второй, совершенно независимый механизм — страничная адресация (pagination).

При страничной адресации непрерывное пространство линейных адресов памяти разбивается на страницы фиксированного размера (обычно 4 Кб (4096 или 1000h байт), но Pentium Pro может поддерживать и страницы по 4 Мб). При обращении к памяти процессор физически обращается не по линейному адресу, а по тому физическому адресу, с которого начинается данная страница. Описание каждой страницы из линейного адресного пространства, включающее в себя ее физический адрес и дополнительные атрибуты, хранится в одной из специальных системных таблиц, как и в случае сегментации, но в отличие от сегментации страничная адресация абсолютно невидима для программы.

Страничная адресация включается при установке бита PG регистра CR0, если бит РЕ установлен в 1 (попытка установить PG, оставаясь в реальном режиме, приводит к исключению #GP(0)). Кроме того, предварительно надо поместить в регистр CR3 физический адрес начала каталога страниц — главной из таблиц, описывающих страничную адресацию. Каталог страниц имеет размер 4096 байт (ровно одна страница) и содержит 1024 4-байтных указателя на таблицы страниц. Каждая таблица страниц тоже имеет размер 4096 байт и содержит указатели до 1024 4-килобайтных страниц. Если одна страница описывает 4 килобайта, то полностью заполненная таблица страниц описывает 4 мегабайта, а полный каталог полностью заполненных таблиц — 4 гигабайта, то есть все 32-битное линейное адресное пространство. Когда процессор выполняет обращение к линейному адресу, он сначала использует его биты 31 – 22 как номер таблицы страниц в каталоге, затем биты 21 – 12 как номер страницы в выбранной таблице, а затем биты 11 – 0 как смещение от физического адреса начала страницы в памяти. Так как этот процесс занимает достаточно много времени, в процессоре предусмотрен специальный кэш страниц — TLB (буфер с ассоциативной выборкой), так что, если к странице обращались не очень давно, процессор определит ее физический адрес сразу.


Элементы каталога страниц и таблиц страниц имеют общий формат:

биты 31 – 12: биты 31 – 12 физического адреса (таблицы страниц или самой страницы)

биты 11 – 9: доступны для использования операционной системой

бит 8: G — «глобальная страница» — страница не удаляется из буфера TLB при переключении задач или перезагрузке регистра CR3 (только на Pentium Pro, если установлен бит PGE регистра CR4)

бит 7: PS — размер страницы. 1 — для страницы размером 2 или 4 мегабайта, иначе — 0

бит 6: D — «грязная страница» — устанавливается в 1 при записи в страницу; всегда равен нулю для элементов каталога страниц

бит 5: А — бит доступа (устанавливается в 1 при любом обращении к таблице страниц или отдельной странице)

бит 4: PCD — бит запрещения кэширования

бит 3: PWT — бит разрешения сквозной записи

бит 2: U — страница/таблица доступна для программ с CPL = 3

бит 1: W — страница/таблица доступна для записи

бит 0: Р — страница/таблица присутствует. Если этот бит — 0, остальные биты элемента система может использовать по своему усмотрению, например, чтобы хранить информацию о том, где физически находится отсутствующая страница

Процессоры Pentium Pro (и старше) могут поддерживать расширения страничной адресации. Если установлен бит РАЕ, физический адрес оказывается не 32-битным (до 4 Гб), а 36-битным (до 64 Гб). Если установлен бит PSE регистра CR4, включается поддержка расширенных страниц размером 4 Мб для РАЕ = 0 и 2 Мб для РАЕ = 1. Такие страницы описываются не в таблицах страниц, а прямо в основном каталоге. Intel рекомендует помещать ядро операционной системы и все, что ему необходимо для работы, на одну 4-мегабайтную страницу, а для приложений пользоваться 4-килобайтными страницами. Расширенные страницы кэшируются в отдельном TLB, так что, если определена всего одна расширенная страница, она будет оставаться в TLB все время.



Для расширенных страниц формат элемента каталога совпадает с форматом для обычной страницы (кроме того, что бит PS = 1), но в качестве адреса используются только биты 31 – 22 — они соответствуют битам 31 – 22 физического адреса начала страницы (остальные биты адреса — нули).

Для расширенного физического адреса (РАЕ = 1) изменяется формат регистра CR3 (см. главу 10.1.3), размеры всех элементов таблиц становятся равными 8 байтам (причем используются только биты 0 – 3 байта 4), так что их число сокращается до 512 элементов в таблице и вводится новая таблица — таблица указателей на каталоги страниц. Она состоит из четырех 8-байтных элементов, каждый из которых может указывать на отдельный каталог страниц. В этом случае биты 31 – 30 линейного адреса выбирают используемый каталог страниц, биты 29 – 21 — таблицу, биты 20 – 12 — страницу, а биты 11 – 0 — смещение от начала страницы в физическом пространстве (следовательно, если биты 29 – 21 выбрали расширенную страницу, биты 20 – 0 соответствуют смещению в ней).

Основная цель страничной адресации — организация виртуальной памяти в операционных системах. Система может использовать внешние устройства — обычно диск — для расширения виртуального размера памяти. При этом, если к какой-то странице долгое время нет обращений, система копирует ее на диск и помещает отсутствующей в таблице страниц. Затем, когда программа обращается по адресу в отсутствующей странице, вызывается исключение #РЕ Обработчик исключения читает адрес, вызвавший ошибку из CR2, определяет, какой странице он соответствует, загружает ее с диска, устанавливает бит присутствия, удаляет копию старой страницы из TLB командой INVLPG и возвращает управление (не забыв снять со стека код ошибки). Команда, вызывавшая исключение типа ошибки, выполняется повторно.



Кроме того, система может периодически сбрасывать бит доступа и, если он не установится за достаточно долгое время, копировать страницу на диск и объявлять ее отсутствующей. Если при этом бит D равен нулю, в страницу не выполнялось никаких записей (с того момента, как этот бит последний раз обнулили) и ее вообще можно не сохранять.

Второе не менее важное применение страничной адресации — безопасная реализация flat-модели памяти. Операционная система может разрешить программам обращаться к любому линейному адресу, но отображение линейного пространства на физическое не будет взаимно однозначным. Скажем, если система использует первые 4 Кб памяти, физическим адресом нулевой страницы будет не ноль, а 4096 и пользовательская программа даже не узнает, что обращается не к нулевому адресу. В этом случае, правда, и сама система не сможет обращаться к первой физической странице без изменения таблицы страниц, но эта проблема решается при применении механизма многозадачности, о котором рассказано далее.

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

; pm3.asm ; Программа, демонстрирующая страничную адресацию. ; Переносит одну из страниц, составляющих видеопамять, и пытается закрасить ; экран ; ; Компиляция: ; TASM: ; tasm /m pm3.asm ; tlink /x /3 pm3.obj ; MASM: ; ml /с pm3.asm ; link pm3.obj,,NUL,,, ; WASM: ; wasm pm3.asm ; wlink file pm3.obj form DOS

.386р RM_seg segment para public "CODE" use16 assume cs:RM_seg,ds:PM_seg,ss:stack_seg start: ; подготовить сегментные регистры push PM_seg pop ds ; проверить, не находимся ли мы уже в РМ mov еах,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,offset v86_msg err_exit: push cs pop ds mov ah,9 int 21h mov ah,4Ch int 21h



; убедиться, что мы не под Windows no_V86: mov ax,1600h int 2Fh test al,al jz no_windows ; сообщить и выйти mov dx,offset win_msg jmp short err_exit

; сообщения об ошибках при старте v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"

; итак, мы точно находимся в реальном режиме no_windows: ; очистить экран и переключиться в нужный видеорежим mov ax,13h int 10h ; вычислить базы для всех дескрипторов xor еах,еах mov ax,RM_seg shl eax,4 mov word ptr GDT_16bitCS+2,ax shr eax,16 mov byte ptr GDT_16bitCS+4,al mov ax,PM_seg shl eax,4 mov word ptr GDT_32bitCS+2,ax shr eax,16 mov byte ptr GDT_32bitCS+4,al ; вычислить линейный адрес GDT xor eax,eax mov ax,PM_seg shl eax,4 push eax add eax,offset GDT mov dword ptr gdtr+2,eax ; загрузить GDT lgdt fword ptr gdtr ; открыть А20 - в этом примере мы будем пользоваться памятью выше 1 Мб mov al,2 out 92h,al ; отключить прерывания cli ; и NMI in al,70h or al,80h out 70h,al ; перейти в защищенный режим (пока без страничной адресации) mov еах,cr0 or al,1 mov cr0,eax ; загрузить CS db 66h db 0EAh dd offset PM_entry dw SEL_32bitCS RM_return: ; переключиться в реальный режим с отключением страничной адресации mov eax,cr0 and eax,7FFFFFFEh mov cr0,eax ; сбросить очередь и загрузить CS db 0EAh dw $+4 dw RM_seg ; загрузить остальные регистры mov ax,PM_seg mov ds,ax mov es,ax ; разрешить NMI in al,70h and al,07FH out 70h,al ; разрешить другие прерывания sti ; подождать нажатия клавиши mov ah,1 int 21h ; переключиться в текстовый режим mov ax,3 int 10h ; и завершить программу mov ah,4Ch int 21h RM_seg ends

PM_seg segment para public "CODE" use32 assume cs:PM_seg ; таблица глобальных дескрипторов GDT label byte db 8 dup(0) GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 GDT_32bitCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 gdt_size = $ - GDT gdtr dw gdt_size-1 ; ее лимит dd ? ; и адрес SEL_flatDS equ 001000b ; селектор 4-гигабайтного сегмента данных SEL_16bitCS equ 010000b ; селектор сегмента кода RM_seg SEL_32bitCS equ 011000b ; селектор сегмента кода PM_seg



; точка входа в 32-битный защищенный режим PM_entry: ; загрузить сегментные регистры, включая стек xor еах,еах mov ax,SEL_flatDS mov ds,ax mov es,ax ; создать каталог страниц mov edi,00100000h ; его физический адрес - 1 Мб mov eax,00101007h ; адрес таблицы 0 = 1 Мб + 4 Кб stosd ; записать первый элемент каталога mov ecx,1023 ; остальные элементы каталога - xor еах,еах ; нули rep stosd ; заполнить таблицу страниц 0 mov eax,00000007h ; 0 - адрес страницы 0 mov ecx,1024 ; число страниц в таблице page_table: stosd ; записать элемент таблицы add еах,00001000h ; добавить к адресу 4096 байт loop page_table ; и повторить для всех элементов ; поместить адрес каталога страниц в CR3 mov еах,00100000h ; базовый адрес = 1 Мб mov cr3,еах ; включить страничную адресацию mov eax,cr0 or eax,80000000h mov cr0,eax ; а теперь изменить физический адрес страницы A1000h на А2000h mov eax,000A2007h mov es:00101000h+0A1h*4,eax ; если закомментировать предыдущие две команды, следующие четыре команды ; закрасят весь экран синим цветом, но из-за того, что мы переместили одну ; страницу, остается черный участок mov ecx,(320*200)/4 ; размер экрана в двойных словах mov edi,0A0000h ; линейный адрес начала видеопамяти mov eax,01010101h ; код синего цвета в VGA - 1 rep stosd ; вернуться в реальный режим db 0EAh dd offset RM_return dw SEL_16bitCS PM_seg ends

; Сегмент стека - используется как 16-битный stack_seg segment para stack "STACK" stack_start db 100h dup(?) stack_seg ends end start


Управление задачами


Следующий очень важный механизм, действующий только в защищенном режиме, — многозадачность. Задача — это элемент работы, который процессор может исполнять, запустить или отложить. Задачи используют для выполнения программ, процессов, обработчиков прерываний и исключений, ядра операционной системы и пр. Любая программа, выполняющаяся в защищенном режиме, должна осуществляться как задача (хотя мы пока игнорировали это требование). Процессор предоставляет средства для сохранения состояния задачи, запуска задачи и передачи управления из одной задачи в другую.

Задача состоит из сегмента состояния задачи (TSS), сегмента кода, одного или нескольких (для разных уровней привилегий) сегментов стека и одного или нескольких сегментов данных.

Задача определяется селектором своего сегмента TSS. Когда задача выполняется, ее селектор TSS (вместе с дескриптором в скрытой части) загружен в регистр TR процессора.

Запуск задачи осуществляется при помощи команды CALL или JMP на сегмент TSS или на шлюз задачи, а также при запуске обработчика прерывания или исключения, который описан как шлюз задачи. При этом автоматически осуществляется переключение задач. Состояние текущей задачи записывается в ее TSS, состояние вызываемой задачи считывается из ее TSS, и управление передается на новые CS:EIP. Если задача не была запущена командой JMP, селектор сегмента TSS старой задачи сохраняется в TSS новой и устанавливается флаг NT, так что следующая команда IRET выполнит обратное переключение задач.

Задачи не могут вызываться рекурсивно. В дескрипторе TSS-задачи, которая была запущена, но не была завершена, тип изменяется на «занятый TSS» и переход на такой TSS невозможен.

Задача может иметь собственную таблицу дескрипторов (LDT) и полный комплект собственных таблиц страниц, так как регистры LDTR и CR3 входят в состояние задачи.



Вход и выход из защищенного режима


Итак, чтобы перейти в защищенный режим, достаточно установить бит РЕ — нулевой бит в управляющем регистре CR0, и процессор немедленно окажется в защищенном режиме. Единственное дополнительное требование, которое предъявляет Intel, — чтобы в этот момент все прерывания, включая немаскируемое, были отключены.

; pm0.asm ; Программа, выполняющая переход в защищенный режим и немедленный возврат. ; Работает в DOS в реальном режиме и в DOS-окне Windows 95 (Windows ; перехватывает исключения, возникающие при попытке перехода в защищенный ; режим из V86, и позволяет нам работать, но только на минимальном уровне ; привилегий) ; ; Компиляция: ; TASM: ; tasm /m pm0.asm ; tlink /x /t pm0.obj ; MASM: ; ml /c pm0.asm ; link pm0.obj,,NUL,,, ; exe2bin pm0.exe pm0.com ; WASM: ; wasm pm0.asm ; wlink file pm0.obj form DOS COM

.model tiny .code .386p ; все наши примеры рассчитаны на 80386 org 100h ; это СОМ-программа start: ; подготовить сегментные регистры push cs pop ds ; DS - сегмент данных (и кода) нашей программы push 0B800h pop es ; ES - сегмент видеопамяти ; проверить, находимся ли мы уже в защищенном режиме mov еах,cr0 ; прочитать регистр CR0 test al,1 ; проверить бит РЕ, jz no_V86 ; если он ноль - мы можем продолжать, ; иначе - сообщить об ошибке и выйти mov ah,9 ; функция DOS 09h mov dx,offset v86_msg ; DS:DX - адрес строки int 21h ; вывод на экран ret ; конец СОМ-программы ; (раз это защищенный режим, в котором работает наша DOS-программа, это должен ; быть режим V86) v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$"

; сюда передается управление, если мы запущены в реальном режиме no_V86: ; запретить прерывания cli ; запретить немаскируемое прерывание in al,70h ; индексный порт CMOS or al,80h ; установка бита 7 в нем запрещает NMI out 70h,аl ; перейти в защищенный режим mov еах,cr0 ; прочитать регистр CRO or al,1 ; установить бит РЕ, mov cr0,eax ; с этого момента мы в защищенном режиме ; вывод на экран 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 ; переключиться в реальный режим mov еах,cr0 ; прочитать CR0 and al,0FEh ; сбросить бит РЕ mov cr0,eax ; с этого момента процессор работает в ; реальном режиме ; разрешить немаскируемое прерывание in al,70h ; индексный порт CMOS and al,07Fh ; сброс бита 7 отменяет блокирование NMI out 70h,al ; разрешить прерывания sti ; подождать нажатия любой клавиши mov ah,0 int 16h ; выйти из СОМ-программы ret ; текст сообщения с атрибутом после каждого символа для прямого вывода на экран message db 'Н',7,'е',7,'l',7,'l',7,'о',7,' ',7,'и',7,'з',7 db ' ',7,'Р',7,'М',7 ; его длина в байтах message_l = $ - message ; длина оставшейся части экрана в словах rest_scr = (80*25)-(2*message_l) end start


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

Дело в том, что, начиная с процессора 80286, размер каждого сегментного регистра — CS, SS, DS, ES, FS и GS — не два байта, а десять, восемь из которых недоступны для программ, точно так же, как описанные выше регистры LDTR и TR. В защищенном режиме при записи селектора в сегментный регистр процессор копирует весь определяемый этим селектором дескриптор в скрытую часть сегментного регистра и больше не пользуется этим селектором вообще. Таблицу дескрипторов можно уничтожить, а обращения к памяти все равно будут выполняться, как и раньше. В реальном режиме при записи числа в сегментный регистр процессор сам создает соответствующий дескриптор в его скрытой части. Этот дескриптор описывает 16-битный сегмент, начинающийся по указанному сегментному адресу с границей 64 Кб. Когда мы переключились в защищенный режим в программе pm0.asm, эти дескрипторы остались на месте и мы могли обращаться к памяти, не принимая во внимание то, что у нас написано в сегментном регистре. Разумеется, в этой ситуации любая попытка записать в сегментный регистр число привела бы к немедленной ошибке (исключение #GP с кодом ошибки, равным загружаемому значению).


В режиме V86 текущий уровень


В режиме V86 текущий уровень привилений, CPL, всегда равен трем. В соответствии с правилами защиты выполнение команд CLI, STI, PUSHF, POPF, INT и IRET приводит к исключению #GP, если IOPL < 3. Однако команды IN, OUT, INS, OUTS, чувствительные к IOPL в защищенном режиме, в V86, управляются битовой картой ввода-вывода, расположенной в TSS задачи. Если бит, соответствующий порту, установлен в 1, обращение к нему из V86-задачи приводит к исключению #GP, если бит сброшен — команды работы с портами ввода-вывода выполняются.

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


Выполнение привилегированных команд


Команды LGDT, LLDT, LTR, LIDT, MOV CRn, LMSW, CLTS, MOV DRn, INVD, WBINVD, INVLPG, HLT, RDMSR, WRMSR, RDPMC, RDTSC, SYSEXIT могут выполняться, только если CPL = 0 (хотя биты РСЕ и TSD регистра CR4 разрешают использование команд RDPMC и RDTSC с любого уровня).

Команды LLDT, SLDT, LTR, STR, LSL, LAR, VERR, VERW и ARPL можно выполнять только в защищенном режиме — в реальном и V86 возникает исключение #UD.

Команды CLI и STI выполняются, только если CPL =< IOPL (IOPL — это двухбитная область в регистре флагов). Если установлен бит PVI в регистре CR4, эти команды выполняются с любым CPL, но управляют флагом VIF, а не IF.

Команды IN, OUT, INSB, INSW, INSD, OUTSB, OUTSW, OUTSD выполняются, только если CPL =< IOPL и если бит в битовой карте ввода-вывода, соответствующий данному порту, равен нулю. (Эта карта — битовое поле в сегменте TSS, каждый бит которого отвечает за один порт ввода-вывода. Признаком ее конца служит слово, в котором все 16 бит установлены в 1.)



Защита на уровне страниц


Обращение к странице памяти с битом U в атрибуте страницы или таблицы страниц, равным нулю, приводит к исключению #PF, если CPL = 3.

Попытка записи в страницу с битом W в атрибуте страницы или таблицы страниц, равным нулю, с CPL = 3 приводит к исключению #РF.

Попытка записи в страницу с битом W в атрибуте страницы или таблицы страниц, равным нулю, если бит WP в регистре CR0 равен 1, приводит к исключению #РF.