Дескриптор — это 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-битный шлюз ловушки |
Это большая группа регистров (более ста), назначение которых отличается в разных моделях процессоров 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).
До сих пор все наши программы работали в защищенном режиме с полностью отключенными прерываниями — ими нельзя было управлять с клавиатуры, они не могли работать с дисками и вообще не делали ничего, кроме чтения или записи в те или иные области памяти. Разумеется, ни одна программа не может сделать ничего серьезного в таком режиме — нам рано или поздно придется обрабатывать прерывания.
В реальном режиме адрес обработчика прерывания считывался процессором из таблицы, находящейся по адресу 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
бит 0: | 1, если причина ошибки — нарушение привилегий; 0, если было обращение к отсутствующей странице |
бит 1: | 1, если выполнялась операция записи, 0, если чтения |
бит 2: | 1, если операция выполнялась из CPL = 3, 0, если CPL < 3 |
бит 3: | 0, если ошибку вызвала попытка установить зарезервированный бит в каталоге страниц |
остальные биты зарезервированы |
Эти восемь 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 включена
Переключение задач осуществляется, если:
текущая задача выполняет дальний 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,,,
Мы будем пользоваться различными дескрипторами по мере надобности, а для начала выполним переключение в 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$"
Мы уже неоднократно сталкивались с защищенным режимом и даже программировали приложения, которые работали в нем (главы 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, процессор находится в защищенном режиме
Сегмент состояния задачи (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). Эта команда обычно используется при инициализации системы для загрузки первой задачи в многозадачной системе.
Команда: | STR приемник |
Назначение: | Прочитать регистр TR |
Процессор: | 80286 |
Команда: | LIDT источник |
Назначение: | Загрузить регистр IDTR |
Процессор: | 80286 |
Команда: | SIDT приемник |
Назначение: | Прочитать регистр IDTR |
Процессор: | 80286 |
Команда: | MOV приемник, источник |
Назначение: | Пересылка данных в/из управляющих и отладочных регистров |
Процессор: | 80386 |
Команда: | LMSW источник |
Назначение: | Загрузить слово состояния процессора |
Процессор: | 80286 |
Команда: | SMSW приемник |
Назначение: | Прочитать слово состояния процессора |
Процессор: | 80286 |
Команда: | CLTS |
Назначение: | Сбросить флаг TS в CR0 |
Процессор: | 80286 |
Команда: | ARPL приемник,источник |
Назначение: | Коррекция поля RPL селектора |
Процессор: | 80286 |
Команда: | LAR приемник,источник |
Назначение: | Прочитать права доступа сегмента |
Процессор: | 80286 |
Команда: | LSL приемник,источник |
Назначение: | Прочитать лимит сегмента |
Процессор: | 80286 |
Команда: | VERR источник |
Назначение: | Проверить права на чтение |
Команда: | VERW источник |
Назначение: | Проверить права на запись |
Процессор: | 80286 |
Команда: | INVD |
Назначение: | Сбросить кэш-память |
Команда: | WBINVD |
Назначение: | Записать и сбросить кэш-память |
Процессор: | 80486 |
Команда: | INVLPG источник |
Назначение: | Аннулировать страницу |
Процессор: | 80486 |
Команда: | HLT |
Назначение: | Остановить процессор |
Процессор: | 8086 |
Команда: | RSM |
Назначение: | Выйти из режима SMM |
Процессор: | Р5 |
Команда: | RDMSR |
Назначение: | Чтение из MSR-регистра |
Команда: | WRMSR |
Назначение: | Запись в MSR-регистр |
Процессор: | Р5 |
Команда: | RDTSC |
Назначение: | Чтение из счетчика тактов процессора |
Процессор: | Р5 |
Команда: | RDPMC |
Назначение: | Чтение из счетчика событий |
Процессор: | Р6 |
Команда: | SYSENTER |
Назначение: | Быстрый системный вызов |
Команда: | SYSEXIT |
Назначение: | Быстрый возврат из системного вызова |
Процессор: | РII |
Линейный адрес, который формируется процессором из логического адреса, соответствует адресу из линейного непрерывного пространства памяти. В обычном режиме в это пространство могут попадать области памяти, в которые нежелательно разрешать запись, — системные таблицы и процедуры, ПЗУ 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 (буфер с ассоциативной выборкой), так что, если к странице обращались не очень давно, процессор определит ее физический адрес сразу.
Следующий очень важный механизм, действующий только в защищенном режиме, — многозадачность. Задача — это элемент работы, который процессор может исполнять, запустить или отложить. Задачи используют для выполнения программ, процессов, обработчиков прерываний и исключений, ядра операционной системы и пр. Любая программа, выполняющаяся в защищенном режиме, должна осуществляться как задача (хотя мы пока игнорировали это требование). Процессор предоставляет средства для сохранения состояния задачи, запуска задачи и передачи управления из одной задачи в другую.
Задача состоит из сегмента состояния задачи (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
Команды 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.
Регистровый операнд всегда начинается с символа «%»:
// xor edx,edx xorl %eax,%eax
Непосредственный операнд всегда начинается с символа «$»:
// mov edx,offset variable movl $variable,%edx
Косвенная адресация использует немодифицированное имя переменной:
// push dword ptr variable pushl variable
Более сложные способы адресации удобнее рассматривать как варианты максимально сложного способа — по базе с индексированием, и сдвигом:
// mov eax,base_addr[ebx+edi*4] (наиболее общий случай) movl base_addr(%ebx,%edi,4),%еах // lea eax,[eax+eax*4] leal (%еах,%еах,4),%еах // mov ax,word ptr [bp-2] movw -2(%ebp),%ax // mov edx,dword ptr [edi*2] movl (,%edi,2),%edx
Повторить блок программы указанное число раз:
.rept число повторов .endr
Повторить блок программы для всех указанных значений символа:
.irp симол, значение... .endr
Повторить блок программы столько раз, сколько байт в строке, устанавливая символ равным каждому байту по очереди:
.irpc символ, строка .endr
Внутри блока повторения на символ можно ссылаться, начиная его с обратной косой черты, то есть как \символ, например такой блок:
.irp param,1,2,3 movl %st(0),%st(\param) . endr
как и такой:
.irpc param,123 movl %st(0),%st(\param) .endr
ассемблируется в:
movl %st(0),%st(1) movl %st(0),%st(2) movl %st(0),%st(3)
Все директивы ассемблера в UNIX всегда начинаются с символа «.» (точка). Из-за большого числа отличающихся операционных систем и ассемблеров для них возникло много часто встречающихся директив. Рассмотрим только наиболее полезные.
Эти директивы эквивалентны директивам db, dw, dd, df и т.п., применяющимся в ассемблерах для DOS/Windows. Основное отличие здесь состоит в том, чтобы дать имя переменной, значение которой определяется такой директивой; в ассемблерах для UNIX обязательно надо ставить полноценную метку, заканчивающуюся двоеточием.
Байты:
.byte выражение...
Слова:
.word выражение... или .hword выражение... или .short выражение...
Двойные слова:
.int выражение... или .long выражение...
Учетверенные слова (8-байтные переменные):
.quad выражение...
16-байтные переменные (окта-слова):
. octa выражение...
32-битные числа с плавающей запятой:
.float число... или .single число...
64-битные числа с плавающей запятой:
.double число...
80-битные числа с плавающей запятой:
.tfloat число...
Строки байтов:
.ascii строка...
Строки байтов с автоматически добавляемым нулевым символом в конце:
.asciz строка... или .string строка
Блоки повторяющихся данных:
.skip размер,значение или .space размер,значение
Заполняет области памяти указанного размера байтами с заданным значением
.fill повтор, размер, значение
Заполняет область памяти значениями заданного размера (0 – 8 байт) указанное число раз. По умолчанию размер принимается равным 1, а значение — 0.
Неинициализированные переменные:
.lcomm символ, длина, выравнивание
Зарезервировать указанное число байт для локального символа в секции .bss.
Текст программы делится на секции — кода, данных, неинициализированных данных, отладочных символов и т.д. Секции также могут делиться далее на подсекции, располагающиеся непосредственно друг за другом, но это редко используется.
.data подсекция
Следующие команды будут ассемблироваться в секцию данных. Если подсекция не указана, данные ассемблируются в нулевую подсекцию.
.text подсекция
Следующие команды будут ассемблироваться в секцию кода.
.section имя, флаги, @тип или .section "имя", флаги
Общее определение новой секции:
флаги (для ELF):
w или #write — разрешена запись;
х или #execinstr — разрешено исполнение;
а или #alloc — разрешено динамическое выделение памяти (.bss);
тип (для ELF):
©progbits — содержит данные;
@nobits — не содержит данные (только занимает место).
Включить текст другого файла в программу:
.include файл
Ассемблировать блок, если выполняется условие или определен или не определен символ:
.if выражение .ifdef символ .ifndef символ или .ifnotdef символ .else .endif
Выдать сообщение об ошибке:
.err
Немедленно прекратить ассемблирование:
.abort
Запретить листинг:
.nolist
Разрешить листинг:
.list
Конец страницы:
.eject
Размер страницы (60 строк, 200 столбцов по умолчанию):
.psize строки, столбцы
Заголовок листинга:
.title текст
Подзаголовок:
.sbttl текст
.align выражение, выражение, выражение
Выполняет выравнивание программного указателя до границы, указанной первым операндом. Второе выражение указывает, какими байтами заполнять пропускаемый участок (по умолчанию — ноль для секций данных и 90h для секций кода). Третье выражение задает максимальное число байт, которые может пропустить эта директива.
В некоторых системах первое выражение — не число, кратным которому должен стать указатель, а число бит в указателе, которые должны стать нулевыми (в нашем примере это было бы 4).
.org новое значение, заполнение
Увеличивает программный указатель до нового значения в пределах текущей секции. Пропускаемые байты заполняются указанными значениями (по умолчанию — нулями).
.code16
Следующие команды будут ассемблироваться как 16-битные.
.code32
Отменяет действие .code 16.
Присвоение значений символам:
.equ символ, выражение
Присваивает символу значение выражения.
.equiv символ, выражение
То же, что и .equ, но выдает сообщение об ошибке, если символ определен.
.set символ, выражение
То же, что и .equ, но можно делать несколько раз. Обычно, впрочем, бывает удобнее написать просто «символ = выражение».
Управление внешними символами:
.globl символ или .global символ
Делает символ видимым для компоновщика, а значит, и для других модулей программы.
.extern символ
Директива .extern обычно игнорируется — все неопределенные символы считаются внешними.
.comm символ, длина, выравнивание
Директива эквивалентна .lcomm, но, если символ с таким именем определен при помощи .lcomm в другом модуле, будет использоваться внешний символ.
Описание отладочных символов:
.def символ .endef
Блок описания отладочного символа.
Мы не коснемся описания отладочных символов, так как их форматы сильно различаются между разнообразными операционными системами и разными форматами объектных файлов.
Высшего приоритета:
* — умножение
/ — целочисленное деление
% — остаток
< или << — сдвиг влево
> или >> — сдвиг вправо
Среднего приоритета:
| — побитовое «ИЛИ»
& — побитовое «И»
^ — побитовое «исключающее ИЛИ»
! — побитовое «ИЛИ-НЕ» (логическая импликация)
Низшего приоритета:
+ — сложение
– — вычитание
Начало макроопределения:
.macro имя, аргументы
Конец макроопределения:
.endm
Преждевременный выход из макроопределения:
.exitm
Внутри макроопределения обращение к параметру выполняется аналогично блокам повторения, начиная его с обратной косой черты.
Хотя стандартные директивы и включают в себя такие вещи, как блоки повторений и макроопределения, их реализация достаточно упрощена, и при программировании для UNIX на ассемблере часто используют дополнительные препроцессоры. Долгое время было принято использовать С-препроцессор или М4, и многие ассемблеры даже могут вызывать их автоматически, но в рамках проекта GNU был создан специальный препроцессор для ассемблера — gasp. Gasp включает различные расширения вариантов условного ассемблирования, построения циклов, макроопределений, листингов, директив определения данных и так далее. Мы не будем заниматься реализацией таких сложных программ, которым может потребоваться gasp, мы даже не воспользуемся и половиной перечисленных директив, но существование этого препроцессора следует иметь в виду.
Как и в ассемблерах для DOS, ассемблеры для UNIX могут вычислять значения выражений в момент компиляции, например:
// поместить в ЕАХ число 320 * 200 movl $320*$200, %еах
В этих выражениях встречаются следующие операторы.
Итак, в ассемблере AT&T в качестве допустимых символов в тексте программы рассматриваются только латинские буквы, цифры и символы «%» (процент) «$» (доллар), «*» (звездочка) , «.» (точка), «,» (запятая) и «_» (подчеркивание). Помимо них существуют символы начала комментария, различные для разных ассемблеров и различные для комментария размером в целую строку или правую часть строки. Любые другие символы, кроме кавычек, двоеточия, пробела и табуляции, если они не часть комментария или не заключены в кавычки, считаются ошибочными.
Если последовательность допустимых символов, с которой начинается строка, не начинается со специального символа или цифры и не заканчивается двоеточием — это команда процессора:
// остановить процессор hlt
Если последовательность допустимых символов начинается с символа «%» — это название регистра процессора:
// поместить в стек содержимое регистра ЕАХ pushl %eax
Если последовательность начинается с символа «$» — это непосредственный операнд:
// поместить в стек 0, число 10h и адрес переменной variable pushl $0 pushl $0x10 pushl $variable
Если последовательность символов начинается с точки — это директива ассемблера:
.align 2
Если последовательность символов, с которой начинается строка, заканчивается двоеточием — это метка (внутренняя переменная ассемблера, значение которой соответствует адресу в указанной точке):
eternal_loop: jmp eternal_loop variable: .byte 7
Метки, состоящие из одной цифры от 0: до 9:, используются как локальные — обращение к метке 1f соответствует обращению к ближайшей из меток 1: вперед по тексту программы, обращение к метке 4b соответствует обращению к ближайшей из меток 4: назад по тексту программы.
Одни и те же метки могут использоваться без ограничений и как цель для команды перехода, и как переменные.
Специальная метка «.» (точка) всегда равна текущему адресу (в точности как «$» в ассемблерах для DOS/Windows).
Если последовательность символов начинается с символа «*» — это абсолютный адрес (для команд jmp и call), иначе — относительный.
– (минус) — отрицательное число
~ (тильда) — «логическое НЕ»
Может оказаться, что программа вынуждена многократно вызывать те или иные стандартные функции из libc в критическом участке, тормозящем выполнение всей программы. В этом случае стоит обратить внимание на то, что многие функции libc на самом деле всего лишь более удобный для языка С интерфейс к системным вызовам, предоставляемым самим ядром операционной системы. Такие операции, как ввод/вывод, вся работа с файловой системой, с процессами, с TCP/IP и т.п., могут выполняться путем передачи управления ядру операционной системы напрямую.
Чтобы осуществить системный вызов, надо передать его номер и параметры на точку входа ядра аналогично функции libc syscall(2). Номера системных вызовов (находятся в файле /usr/include/sys/syscall.h) и способ обращения к точке входа (дальний call по адресу 0007:00000000) стандартизированы SysV/386 ABI, но, например в Linux, используется другой механизм — прерывание 80h, так что получается, что обращение к ядру операционной системы напрямую делает программу привязанной к этой конкретной системе. Часть этих ограничений можно убрать, используя соответствующие #define, но в общем случае этот выигрыш в скорости оборачивается еще большей потерей переносимости, чем само использование ассемблера в UNIX.
Посмотрим, как реализуются системные вызовы в рассматриваемых нами примерах:
// hellolnx.s // Программа, выводящая сообщение "Hello world" на Linux // без использования libc // // Компиляция: // as -о hellolnx.o hellolnx.s // ld -s -o hellolnx hellolnx.o // .text .globl _start _start: // системный вызов #4 "write", параметры в Linux помещают слева направо, // в регистры %еах, %ebx, %ecx, %edx, %esi, %edi movl $4,%eax xorl %ebx,%ebx incl %ebx // %ebx = 1 (идентификатор stdout) movl $message,%ecx movl $message_l,%edx // передача управления в ядро системы - прерывание с номером 80h int $0x80
// системный вызов #1 "exit" (%еах = 1, %ebx = 0) xorl %eax,%eax incl %eax xorl %ebx,%ebx int $0x80 hlt
.data message: .string "Hello world\012" message_l = . - message
Операционная система MS-DOS, получившая дальнейшее развитие в виде Windows, долгое время была практически единственной операционной системой для персональных компьютеров на базе процессоров Intel. Но с течением времени мощность процессоров выросла настолько, что для них стало возможным работать под управлением операционных систем класса UNIX, использовавшихся обычно на более мощных компьютерах других компаний. В настоящее время существует более двадцати операционных систем для Intel, представляющих те или иные диалекты UNIX. Мы рассмотрим наиболее популярные из них.
Linux — бесплатно распространяемая операционная система, соединяющая в себе особенности двух основных типов UNIX-систем, System V и BSD приблизительно в равной мере. В ней достаточно много отличий и отступлений от любых стандартов, принятых для UNIX, но они более эффективны.
FreeBSD — бесплатно распространяемая операционная система, представляющая вариант BSD UNIX. Считается наиболее стабильной из UNIX-систем для Intel.
Solaris/x86 — коммерческая операционная система компании Sun Microsystems, представляющая вариант System V UNIX, изначально созданная для компьютеров Sun, существует в версии для Intel 80x86. Распространяется бесплатно для образовательных целей.
Несмотря на то что при программировании для UNIX обычно употребляется исключительно язык С, пользоваться ассемблером в этих системах можно, и даже крайне просто. Программы в UNIX выполняются в защищенном режиме с моделью памяти flat и могут вызывать любые функции из библиотеки libc или других библиотек точно так же, как это делают программы на С. Конечно, круг задач, для которых имеет смысл использовать ассемблер в UNIX, ограничен. Если вы не занимаетесь разработкой ядра операционной системы или, например, эмулятора DOS, практически все можно сделать и на С, но иногда встречаются ситуации, когда требуется что-то особенное. Написать процедуру, выполняющую что-то как можно быстрее (например, воспроизведение звука из файла в формате МР3), или программу, использующую память более эффективно (хотя это часто можно повторить на С), или программу, использующую возможности нового процессора, поддержка которого еще не добавлена в компилятор (если вы знаете ассемблер для UNIX), достаточно просто.
Все программы для UNIX, написанные на С, постоянно обращаются к различным функциям, находящимся в libc.so или других стандартных или нестандартных библиотеках. Программы и процедуры на ассемблере, естественно, могут делать то же самое. Вызов библиотечной функции выполняется обычной командой call, а передача параметров осуществляется в соответствии с С-конвенцией: параметры помещают в стек справа налево и очищают стек после вызова функции. Единственная сложность здесь состоит в том, что к имени вызываемой функции в некоторых системах, например FreeBSD, приписывается в начале символ подчеркивания, в то время как в других (Linux и Solaris) имя не изменяется. Если имена в системе модифицируются, имена процедур, включая main(), написанных на ассемблере, также должны быть изменены заранее.
Посмотрим на примере программы, выводящей традиционное сообщение «Hello world», как это делается:
// helloelf.s // Минимальная программа, выводящая сообщение "Hello world" // Для компиляции в формат ELF // // Компиляция: // as -о helloelf.o helloelf.s // Компоновка: // (пути к файлу crt1.o могут отличаться на других системах) // Solaris с SunPro С // ld -s -о helloelf.sol helloelf.o /opt/SUNWspro/SC4.2/lib/crt1.о -lс // Solaris с GNU С // ld -s -o helloelf.gso helloelf.o // /opt/gnu/lib/gcc-lib/i586-cubbi-solaris2.5.1/2.7.2.3.f.1/crt1.о -lс // Linux // ld -s -m elf_i386 -o helloelf.lnx /usr/lib/crt1.o /usr/lib/crti.o // -L/usr/lib/gcc-lib/i586-cubbi-linuxlibc1/2.7.2 helloelf.o -lc -lgcc // /usr/lib/crtn.o // .text // код, находящийся в файлах crt*.o, передаст управление на процедуру main // после настройки всех параметров .globl main main: // поместить параметр (адрес строки message) в стек pushl $message // вызвать функцию puts (message) call puts // очистить стек от параметров popl %ebx // завершить программу ret
.data message: .string "Hello world\0"
В случае FreeBSD придется внести всего два изменения — добавить символ подчеркивания в начало имен функций puts и main и заменить директиву .string на .ascii, так как версия ассемблера, обычно распространяемого с FreeBSD, .string не понимает.
// hellocof.s // Минимальная программа, выводящая сообщение "Hello world" // Для компиляции в вариант формата COFF, используемый во FreeBSD // Компиляция для FreeBSD: // as -о hellocof.o hellocof.s // ld -s -о hellocof.bsd /usr/lib/crt0.o hellocof.o -lc
Проблема в том, что ассемблер для UNIX кардинально отличается от того, что рассматривалось в этой книге до сих пор. В то время как основные ассемблеры для MS-DOS и Windows используют синтаксис, предложенный компанией Intel, изобилующий неоднозначностями, часть которых решается за счет использования поясняющих операторов типа byte ptr, word ptr или dword ptr, а часть не решается вообще (все те случаи, когда приходится указывать код команды вручную), в UNIX с самого начала используется вариант универсального синтаксиса AT&T, синтаксис SysV/386, который специально создавался с целью устранения неоднозначностей в толковании команд. Вообще говоря, существует и ассемблер для DOS/Windows, использующий АТ&Т-синтаксис, — это gas, входящий в набор средств разработки DJGPP, а также есть ассемблер, использующий Intel-синтаксис и способный создавать объектные файлы в формате ELF, применяемом в большинстве UNIX-систем, — это бесплатно распространяемый в сети Inetrnet ассемблер NASM. Мы будем рассматривать только ассемблеры, непосредственно входящие в состав операционных систем, то есть ассемблеры, которые вызываются стандартной командой as.
Названия команд, не принимающих операндов, совпадают с названиями, принятыми в синтаксисе Intel:
nop
К названиям команд, имеющих операнды, добавляются суффиксы, отражающие размер операндов:
b — байт;
w — слово;
l — двойное слово;
q — учетверенное слово;
s — 32-битное число с плавающей запятой;
l — 64-битное число с плавающей запятой;
t — 80-битное число с плавающей запятой.
// mov byte ptr variable,0 movb $0,variable // fild qword ptr variable fildq variable
Команды, принимающие операнды разных размеров, требуют указания двух суффиксов, сначала суффикса источника, а затем приемника:
// movsx edx,al movsbl %al,%edx
Команды преобразования типов имеют в AT&T названия из четырех букв — С, размер источника, Т и размер приемника:
// cbw cbtw // cwde cwtl // cwd cwtl // cdq cltd
Но многие ассемблеры понимают и принятые в Intel формы для этих четырех команд.
Дальние команды передачи управления (jmp, call, ret) отличаются от ближних префиксом l:
// call far 0007:00000000 lcall $7,$0 // retf 10 lret $10
Если команда имеет несколько операндов, операнд-источник всегда записывается первым, а приемник — последним, то есть в точности наоборот по сравнению с Intel-синтаксисом:
// mov ax,bx movw %bx,%ax // imul eax.ecx,16 imull $16,%ecx,%eax
Все префиксы имеют имена, которыми они задаются как обычные команды, — перед командой, для которой данный префикс предназначен. Имена префиксов замены сегмента — segcs, segds, segss, segfs, seggs, имена префиксов изменения разрядности адреса и операнда- addr16 и data 16:
segfs movl variable,%eax rep stosd
Кроме того, префикс замены сегмента будет включен автоматически, если используется оператор «:» в контексте операнда:
movl %fs:variable, %eax