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

         

Дескрипторы


Дескриптор — это 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.



Адресация


Регистровый операнд всегда начинается с символа «%»:

// 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 в критическом участке, тормозящем выполнение всей программы. В этом случае стоит обратить внимание на то, что многие функции 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


Linux — это довольно уникальный случай в отношении системных вызовов. В более традиционных UNIX-системах — FreeBSD и Solaris — системные вызовы реализованы согласно общему стандарту SysV/386, и различие в программах заключается только в том, что ассемблер, поставляемый с FreeBSD, не поддерживает некоторые команды и директивы.

// hellobsd.s // Программа, выводящая сообщение "Hello world" на FreeBSD // без использования libc // // Компиляция: // as -о hellobsd.o hellobsd.s // ld -s -o hellobsd hellobsd.o // .text .globl _start _start: // системная функция 4 "write" // в FreeBSD номер вызова помещают в %еах, а параметры - в стек // справа налево плюс одно двойное слово pushl $message_l // параметр 4 - длина буфера pushl $message // параметр 3 - адрес буфера pushl $1 // параметр 2 - идентификатор устройства movl $4,%еах // параметр 1 - номер функции в еах pushl %eax // в стек надо поместить любое двойное слово, но мы поместим номер вызова // для совместимости с Solaris и другими строгими операционными системами // lcall $7,$0 - ассемблер для FreeBSD не поддерживает эту команду .byte 0x9a .long 0 .word 7 // восстановить стек addl $12,%esp // системный вызов 1 "exit" xorl %eax,%eax pushl %eax incl %eax pushl %eax // lcall $7,$0 .byte 0x9A .long 0 .word 7 hlt

.data message: .ascii "Hello world\012" message_l = . - message

И теперь то же самое в Solaris:

// hellosol.s // Программа, выводящая сообщение "Hello world" на Solaris/x86 // без использования libc // // Компиляция: // as -о hellosol.o hellosol.s // ld -s -o hellosol hellosol.o .text .globl _start _start: // комментарии - см. hellobsd.s pushl $message_l pushl $message movl $4,%eax pushl %eax lcall $7,$0 addl $16,%esp

xorl %eax,%eax pushl %eax incl %eax pushl %eax lcall $7,$0 hit

.data message: .string "Hello world\012" message_l = . - message

Конечно, создавая эти программы, мы нарушили спецификацию SysV/386 ABI несколько раз, но из-за того, что мы не обращались ни к каким разделяемым библиотекам, это прошло незамеченным. Требования к полноценной программе сильно разнятся в различных операционных системах, и все они выполнены с максимально возможной тщательностью в файлах crt*.o, которые мы подключали в примере с использованием библиотечных функций. Поэтому, если вы не ставите себе цель сделать программу абсолютно минимального размера, гораздо удобнее назвать свою процедуру main (или _main) и добавлять crt*.o и -lс при компоновке.


Программирование на ассемблере в среде UNIX


Операционная система 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), достаточно просто.



Программирование с использованием libc


Все программы для 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


.text .globl _main _main: pushl $message call _puts popl %ebx ret

.data message: .ascii "Hello world\0"

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

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


Синтаксис AT&T


Проблема в том, что ассемблер для 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



прочитав эту книгу, вы познакомились


Итак, прочитав эту книгу, вы познакомились с программированием на языке ассемблера во всей широте его проявлений — от создания простых программ и процедур, вызываемых из программ на других языках, до драйверов устройств и операционных систем. Теперь должно быть очевидно, что ассемблер не только не сдает свои позиции, но и не может их сдать — ассемблер неотъемлемо связан с компьютером, и всюду, как только мы опускаемся с уровня абстракций языков высокого уровня, рано или поздно встречаемся с ним. В то же время и абстракции, и сложные управляющие структуры, и структуры данных выражаются на языке ассемблера наиболее эффективно — не зря же Дональд Кнут, автор знаменитой книги «Искусство программирования», использовал для иллюстрации всех таких структур и алгоритмов только ассемблер.
Ассемблер настолько многогранен, что нет никакой возможности описать все, для чего он может быть использован, в одной книге. Методы защиты от копирования и противодействия отладчикам, строение различных файловых систем, программирование на уровне портов ввода-вывода таких устройств, как IDE- или SCSI-диски, и многое другое осталось в стороне, и это правильно, так как мир ассемблера не заканчивается вместе с этой книгой, а только начинается.