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

         

Адресация в защищенном режиме


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

биты 15 – 3: номер дескриптора в таблице

бит 2: индикатор таблицы 0/1 — использовать GDT/LDT

биты 1 – 0: уровень привилегий запроса (RPL)

Уровень привилегий запроса — это число от 0 до 3, указывающее уровень защиты сегмента, для доступа к которому используется данный селектор. Если программа имеет более высокий уровень привилегий, при использовании этого сегмента привилегии понизятся до RPL. Уровни привилегий и весь механизм защиты в защищенном режиме нам пока не потребуется.

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

слово 3 (старшее):

биты 15 – 8: биты 31 – 24 базы

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

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

бит 5: 0

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

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

слово 2:

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

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

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

биты 11 – 8: тип сегмента

биты 7 – 0: биты 23 – 16 базы




слово 1: биты 15 – 0 базы

слово 0 (младшее): биты 15 – 0 лимита

Два основных поля в этой структуре, которые нам интересны, — это база и лимит сегмента. База представляет линейный 32-битный адрес начала сегмента, а лимит — это 20-битное число, которое равно размеру сегмента в байтах (от 1 байта до 1 мегабайта), если бит гранулярности сброшен в ноль, или в единицах по 4096 байт (от 4 Кб до 4 Гб), если он установлен в 1. Числа отсчитываются от нуля, так что лимит 0 соответствует сегменту длиной 1 байт, точно так же, как база 0 соответствует первому байту памяти.

Остальные элементы дескриптора выполняют следующие функции:

Бит разрядности: для сегмента кода этот бит указывает на разрядность операндов и адресов по умолчанию. То есть в сегменте с этим битом, установленным в 1, все команды будут интерпретироваться как 32-битные, а префиксы изменения разрядности адреса или операнда будут превращать их в 16-битные, и наоборот. Для сегментов данных этот бит управляет тем, какой регистр (SP или ESP) используют команды, работающие с этим сегментом данных как со стеком.

Поле DPL определяет уровень привилегий сегмента.

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

Бит типа дескриптора — если он равен 1, сегмент является обычным сегментом кода или данных. Если этот бит — 0, дескриптор является одним из 16 возможных видов, определяемых полем типа сегмента.

Тип сегмента: для системных регистров в этом поле находится число от 0 до 15, определяющее тип сегментов (LDT, TSS, различные шлюзы), которые рассмотрены в главе 9. Для обычных сегментов кода и данных эти четыре бита выполняют следующие функции:

бит 11: 0 — сегмент данных, 1 — сегмент кода



бит 10: для данных — бит направления роста сегмента
для кода — бит подчинения

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

бит 8: бит обращения

Бит обращения устанавливается в 1 при загрузке селектора этого сегмента в регистр.

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

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

Бит направления роста сегмента обращает смысл лимита сегмента. В сегментах с этим битом, сброшенным в ноль, допустимы все смещения от 0 до лимита, а если этот бит — 1, то допустимы все смещения, кроме смещений от 0 до лимита. Про такой сегмент говорят, что он растет сверху вниз, так как если лимит, например, равен –100, допустимы смещения от –100 до 0, а если лимит увеличить, станут допустимыми еще меньшие смещения.

Для обычных задач программирования нам не потребуется все многообразие возможностей адресации. Все, что нам нужно, — это удобный неограниченный доступ к памяти. Поэтому мы будем рассматривать простую модель памяти — так называемую модель flat, в которой базы всех регистров установлены в ноль, а лимиты — в 4 Гб. Именно в такой ситуации окажется, что можно забыть о сегментации и пользоваться только 32-битными смещениями.

Для создания flat-памяти нам потребуются два дескриптора с нулевой базой и максимальным лимитом — один для кода и один для данных.

Дескриптор кода:

лимит FFFFFh

база 000000000h

тип сегмента FAh

бит присутствия = 1

уровень привилегий = 3 (минимальный)

бит типа дескриптора = 1

тип сегмента: 1010b (сегмент кода, для выполнения/чтения)



бит гранулярности = 1

бит разрядности = 1

db 0FFh, 0FFh, 0h, 0h, 0h, 0FAh, 0CFh, 0h

Дескриптор данных:

лимит FFFFFh

база 00000000h

бит присутствия = 1

уровень привилегий = 3 (минимальный)

бит типа дескриптора = 1

тип сегмента: 0010b (сегмент данных, растет вверх, для чтения/записи)

бит гранулярности = 1

бит разрядности = 1

db 0FFh, 0FFh, 0h, 0h, 0h, 0F2h, 0CFh, 0h

Для того чтобы процессор знал, где искать дескрипторы, операционная система собирает их в таблицы, которые называются GDT (таблица глобальных дескрипторов — может быть только одна) и LDT (таблица локальных дескрипторов — по одной на каждую задачу), и загружает их при помощи привилегированных команд процессора. Так как мы пока не собираемся создавать операционные системы, нам потребуется только подготовить дескриптор и вызвать соответствующую функцию VCPI или DPMI.

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


Функции DPMI управления дескрипторами


INT 31h, AX = 0 — Выделить локальные дескрипторы

Ввод: АХ = 0
СХ = количество необходимых дескрипторов
Вывод: если CF = 0, АХ = селектор для первого из заказанных дескрипторов

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

INT 31h, AX = 1 — Удалить локальный дескриптор

Ввод: АХ = 1
ВХ = селектор
Вывод: CF = 0, если не было ошибки

Эта функция действует на дескрипторы, созданные при переключении в защищенный режим, и на дескрипторы, созданные функцией 0, но не на дескрипторы, созданные функцией 2.

INT 31h, АХ = 2 — Преобразовать сегмент в дескриптор

Ввод: АХ = 2
ВХ = сегментный адрес (A000h — для видеопамяти, 0040h — для данных BIOS)
Вывод: если CF = 0, АХ = готовый селектор на сегмент, начинающийся с указанного адреса, и с лимитом 64 Кб

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

INT 31h, AX = 6 — Определить базу сегмента

Ввод: АХ = 6
ВХ = селектор
Вывод: если CF = 0, CX:DX = 32-битный линейный адрес начала сегмента

INT 31h, AX = 7 — Сменить базу сегмента

Ввод: АХ = 7
ВХ = селектор
CX:DX = 32-битная база
Вывод: CF = 0, если не было ошибок

INT 31h, AX = 8 — Сменить лимит сегмента

Ввод: АХ = 8
ВХ = селектор
CX:DX = 32-битный лимит (длина сегмента минус 1)
Вывод: CF = 0, если не было ошибок
(чтобы определить лимит сегмента, можно пользоваться командой LSL)

INT 31h, AX = 9 — Сменить права доступа сегмента

Ввод: АХ = 9
ВХ = селектор
CL = права доступа/тип (биты 15 – 8 слова 2 дескриптора)
СН = дополнительные права (биты 7 – 4 соответствуют битам 7 – 4 слова 3 дескриптора, биты 3 – 0 игнорируются)
Вывод: CF = 0, если не было ошибок
(чтобы определить права доступа сегмента, можно пользоваться командой LAR)
<
/p> INT 31h, АХ = 0Ah — Создать копию дескриптора

Ввод: АХ = 000Ah
ВХ = селектор (сегмента кода или данных)
Вывод: если CF = 0, АХ = селектор на сегмент данных с теми же базой и лимитом
INT 31h, AX = 0Bh — Прочитать дескриптор

Ввод: АХ = 000Bh
ВХ = селектор
ES:EDI = селектор:смещение 8-байтного буфера
Вывод: если CF = 0, в буфер помещен дескриптор
INT 31h, AX = 0Ch — Загрузить дескриптор

Ввод: АХ = 000Ch
ВХ = селектор
ES:EDI = адрес 8-байтного дескриптора
Вывод: CF = 0, если не было ошибок
INT 31h, AX = 0Dh — Выделить определенный дескриптор

Ввод: АХ = 000Dh
ВХ = селектор на один из первых 16 дескрипторов (значения селектора 04h – 7Ch)
Вывод: CF = 0, если нет ошибок (CF = 1 и АХ = 8011h, если этот дескриптор занят)
Этого набора функций, а точнее пары функций 00 и 0Ch, достаточно для того, чтобы полностью настроить режим flat (или любой другой) после переключения в защищенный режим. Но прежде чем это осуществить, нам надо познакомиться с тем, как в DMPI сделан вызов обработчиков прерываний реального режима, иначе наша программа просто не сможет себя проявить.


Интерфейс DPMI


Спецификация DPMI создана в 1990 – 1991 годах и представляет собой развитую систему сервисов, позволяющих программам переключаться в защищенный режим, вызывать обработчики прерываний BIOS и DOS, передавать управление другим процедурам, работающим в реальном режиме, устанавливать обработчики аппаратных и программных прерываний и исключений, работать с памятью с разделяемым доступом, устанавливать точки останова и т.д. Операционные системы, такие как Windows 95 или, например, Linux, предоставляют DPMI-интерфейс для программ, запускаемый в DOS-задачах; многочисленные расширители DOS, о которых говорится в следующей главе, предоставляют DPMI для DOS-программ, запускаемых в любых условиях, так что сейчас можно считать DPMI основным интерфейсом, на который следует ориентироваться при программировании 32-битных приложений для DOS.



Интерфейс VCPI


Спецификация этого интерфейса была создана в 1989 году, вскоре после появления процессора 80386, компаниями Phar Lap Software и Quaterdeck Office Systems. Программа, пользующаяся VCPI для переключения в защищенный режим, должна поддерживать полный набор системных таблиц — GDT, LDT, IDT, таблицы страниц и т.д., то есть фактически VCPI-сервер обеспечивает следующее — процессор находится в защищенном режиме, и различные программы, пользующиеся им, не будут конфликтовать между собой.

VCPI является своего рода расширением интерфейса EMS, и все обращения к нему выполняются при помощи прерывания EMS, INT 67h с АН = 0DEh и кодом подфункции VCPI в AL.

INT 67h АХ = DE00h — Проверка наличия VCPI

Ввод: AX = 0DE00h
Вывод: АН = 00, если VCPI есть
ВН, BL — версия (старшая, младшая цифры)

INT 67h АХ = DE01h — Получить точку входа VCPI

Ввод: AX = 0DE01h
ES:DI = адрес 4-килобайтного буфера для таблицы страницDS:SI = адрес таблицы дескрипторов
Вывод: АН = 0, если нет ошибок
DS:SI — первые три дескриптора заполняются дескрипторами VCPI-сервера
ES:SI — адрес первой не используемой сервером записи в таблице страниц
ЕВХ — адрес точки входа VCPI относительно сегмента, дескриптор которого лежит первым в таблице DS:SI. Можно делать far call из 32-битного защищенного режима на этот адрес с АХ = DE00h – DE05h, чтобы пользоваться функциями VCPI из защищенного режима

INT 67h AX = DE0Ch — VCPI: переключиться в защищенный режим (для вызова из V86)

Ввод: AX = 0DE0Ch
ESI = линейный адрес таблицы со значениями для системных регистров (в первом мегабайте)

+00h: 4 байта — новое значение CR3

+04h: 4 байта — адрес 6-байтного значения для GDTR

+08h: 4 байта — адрес 6-байтного значения для IDTR

+0Ch: 2 байта — LDTR

+0Eh: 2 байта — TR

+10h: 6 байт — CS:EIP — адрес точки входа

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

Вывод: Загружаются регистры GDTR, IDTR, LDTR, TR. Теряются регистры ЕАХ, ESI, DS, ES, FS, GS. SS:ESP указывает на стек размером в 16 байт, и его надо изменить, прежде чем снова разрешать прерывания
<
/p> Точка входа VCPI, АХ = DE0Ch — Переключиться в режим V86 (для вызова из РМ)

Ввод: Перед передачей управления командой call в стек надо поместить регистры в следующем порядке (все значения — двойные слова): GS, FS, DS, ES, SS, ESP, 0, CS, EIP. Прерывания должны быть запрещены
Вывод: Сегментные регистры загружаются, значение ЕАХ не определено, прерывания запрещены
Остальные функции VCPI:

INT 67h AX = DE02h — Определить максимальный физический адрес

Ввод: АХ = 0DE02h
Вывод: АН = 0, если нет ошибок
EDX = физический адрес самой старшей 4-килобайтной страницы, которую можно выделить
INT 67h AX = DE03h — Определить число свободных страниц

Ввод: АХ = 0DE03h
Вывод: АН = 0, если нет ошибок
EDX = число свободных 4-килобайтных страниц для всех задач
INT 67h AX = DE04h — Выделить 4-килобайтную страницу (обязательно надо вызвать DE05h)

Ввод: АХ = 0DE04h
Вывод: АН = 0, если нет ошибок
EDX = физический адрес выделенной страницы
INT 67h AX = DE05h — Освободить 4-килобайтную страницу

Ввод: АХ = 0DE05h
EDX = физический адрес страницы
Вывод: АН = 0, если нет ошибок
INT 67h AX = DE06h — Определить физический адрес 4-килобайтной страницы в первом мегабайте

Ввод: АХ = 0DE06h
СХ = линейный адрес страницы, сдвинутый вправо на 12 бит
Вывод: АН = 0, если нет ошибок
EDX = физический адрес страницы
INT 67h AX = DE07h — Прочитать регистр CR0

Ввод: АХ = 0DE07h
Вывод: АН = 0, если нет ошибок
ЕВХ = содержимое регистра CR0
INT 67h АХ = DE08h — Прочитать регистры DR0 – DR7

Ввод: АХ = 0DE08h
ES:DI = буфер на 8 двойных слов
Вывод: АН = 0, если нет ошибок, в буфер не записываются DR4 и DR5
INT 67h AX = DE09h — Записать регистры DR0 – DR7

Ввод: АХ = 0DE09h
ES:DI = буфер на 8 двойных слов с новыми значениями для регистров
Вывод: АН = 0, если нет ошибок (DR4 и DR5 не записываются)
<


/p> INT 67h AX = DE0Ah — Определить отображение аппаратных прерываний

Ввод: АХ = 0DE0Ah
Вывод: АН = 0, если нет ошибок
ВХ = номер обработчика для IRQ0
СХ = номер обработчика для IRQ8
INT 67h AX = DE0Bh — Сообщить VCPI- серверу новое отображение аппаратных прерываний (вызывается после перепрограммирования контроллера прерываний)

Ввод: АХ = 0DE0Bh
ВХ = номер обработчика для IRQ0
СХ = номер обработчика для IRQ8
Вывод: АН = 0, если нет ошибок
Итак, чтобы использовать защищенный режим с VCPI, фактически надо уметь программировать его самостоятельно. Например, чтобы вызвать прерывание DOS или BIOS, нам пришлось бы переключаться в режим V86, вызывать прерывание и затем возвращаться обратно. Естественно, этот интерфейс не получил широкого развития и был практически повсеместно вытеснен более удобным DPMI.


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


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

INT 31h, AX = 0200h — Определить адрес реального обработчика прерывания

Ввод: АХ = 0200h
BL = номер прерывания
Вывод: CF = 0 всегда, CX:DX — сегмент:смещение обработчика прерывания в реальном режиме

INT 31h, АХ = 0201h — Установить реальный обработчик прерывания

Ввод: АХ = 0201h
BL = номер прерывания
CX:DX = сегмент:смещение обработчика прерывания в реальном режиме
Вывод: CF = 0 всегда

INT 31h, АХ = 0204h — Определить адрес защищенного обработчика прерывания

Ввод: АХ = 0204h
BL = номер прерывания
Вывод: CF = 0 всегда, CX:EDX = селектор:смещение обработчика

INT 31h, АХ = 0205h — Установить защищенный обработчик прерывания

Ввод: АХ = 0205h
BL = номер прерывания
CX:EDX = селектор:смещение обработчика
Вывод: CF = 0

INT 31h, АХ = 0202h — Определить адрес обработчика исключения

Ввод: АХ = 0202h
BL = номер исключения (00h – 1Fh)
Вывод: если CF = 0, CX:EDX = селектор:смещение обработчика исключения

INT 31h, АХ = 0203h — Установить обработчик исключения

Ввод: АХ = 0203h
BL = номер исключения (00h – 1Fh)
CX:EDX = селектор:смещение обработчика исключения
Вывод: CF = 0, если не было ошибок

Если обработчик исключения передает управление дальше по цепочке на стандартный обработчик DPMI-сервера, следует помнить, что только исключения 0, 1, 2, 3, 4, 5 и 7 передаются обработчикам из реального режима, а остальные исключения приводят к прекращению работы программы.



Передача управления между режимами в DPMI


Вызов любого программного прерывания, кроме INT 31h и INT 21h/АН = 4Ch (функция DPMI: завершение программы), приводит к тому, что DPMI-сервер переключается в режим V86 и вызывает это же самое прерывание, скопировав все регистры, кроме сегментных регистров и стека (состояние сегментных регистров не определено, а стек предоставляет сам DPMI-сервер). После того как обработчик прерывания возвратит управление, DPMI-сервер возвратиться в защищенный режим и вернет управление программе. Таким способом можно вызывать все прерывания, передающие параметры только в регистрах, например проверку нажатия клавиши или вывод символа на экран. Чтобы вызвать прерывание, использующее сегментные регистры, например вывод строки на экран, а также в других ситуациях, требующих вызова процедуры, работающей в другом режиме, применяются следующие функции.

INT 31h, AX = 0300h — Вызов прерывания в реальном режиме

Ввод: АХ = 0300h
ВН = 0, BL = номер прерывания
СХ = число слов из стека защищенного режима, которое будет скопировано в стек реального режима и обратно
ES:EDI = селектор:смещение структуры регистров v86_regs (см. ниже)
Вывод: если CF = 0, структура в ES:EDI модифицируется

Значения регистров CS и IP в структуре v86_regs игнорируются. Вызываемый обработчик прерывания должен восстанавливать стек в исходное состояние (например, INT 25h и INT 26h этого не делают).

INT 31h, АХ = 0301Н Вызов дальней процедуры в реальном режиме

Ввод: АХ = 0301h
ВН = 0
СХ = число слов из стека защищенного режима, которое будет скопировано в стек реального режима и обратно
ES:EDI = селектор:смещение структуры регистров v86_regs (см. ниже)
Вывод: если CF = 0, структура в ES:EDI модифицируется

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

INT 31h, AX = 0302h Вызов обработчика прерывания в реальном режиме

Ввод: АХ = 0302h
ВН = 0
СХ = число слов из стека защищенного режима, которое будет скопировано в стек реального режима и обратно
ES:EDI = селектор:смещение структуры регистров v86_regs (см. ниже)
Вывод: если CF = 0, структура в ES:EDI модифицируется
<
/p> Вызываемая процедура должна заканчиваться командой IRET.

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

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

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

+08h: 4 байта — ЕВР

+0Ch: 4 байта — игнорируются

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

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

+18h: 4 байта — ЕСХ

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

+20h: 2 байта — FLAGS

+22h: 2 байта — ES

+24h: 2 байта — DS

+26h: 2 байта — FS

+28h: 2 байта — GS

+2Ah: 2 байта — IP

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

+2Eh: 2 байта — SP

+30h: 2 байта — SS

Значения SS и SP могут быть нулевыми, тогда DPMI-сервер сам предоставит стек для работы прерывания.

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

INT 31h, АХ = 0303h — Выделить точку входа для вызова из реального режима

Ввод: АХ = 0303h
DS:ESI = селектор:смещение процедуры в защищенном режиме (заканчивающейся IRET), которую будут вызывать из реального режима
ES:EDI = селектор:смещение структуры v86_regs, которая будет использоваться для передачи регистров
Вывод: если CF = 0, CX:DX = сегмент:смещение точки входа
При передаче управления в такую процедуру DS:ESI указывает на стек реального режима, ES:EDI — на структуру v86_regs, SS:ESP — на стек, предоставленный DPMI-сервером, и остальные регистры не определены.

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

INT 31h, AX = 0304h — Освободить точку входа для вызова из реального режима

Ввод: АХ = 0304h
CX:DX = сегмент:смещение точки входа
Вывод: CF = 0, если точка входа удалена

Переключение в защищенный режим


Все основные сервисы DPMI доступны только в защищенном режиме через прерывание INT 31h, так что переключение режимов — первое, что должна сделать программа.

INT 2Fh AX = 1687h — Функция DPMI: получить точку входа в защищенный режим

Ввод: АХ = 1687h
Вывод: АХ = 0, если DPMI присутствует
ВХ: бит 0 = 1, если поддерживаются 32-битные программы, 0 — если нет
CL: тип процессора (02 — 80286, 03 — 80386 и т.д.)
DH:DL — версия DPMI в двоичном виде (обычно 00:90 (00:5Ah) или 01:00)
SI = размер временной области данных, требуемой для переключения в 16-байтных параграфах
ES:DI = адрес процедуры переключения в защищенный режим

Вызвав эту функцию, программа должна выделить область памяти размером SI * 16 байт и выполнить дальний CALL на указанный адрес. Единственные входные параметры — регистр ES, который должен содержать сегментный адрес области данных для DPMI и бит 0 регистра АХ. Если этот бит 1 — программа собирается стать 32-битным приложением, а если 0 — 16-битным. Если по возвращении из процедуры установлен флаг CF, переключения не произошло и программа все еще в реальном режиме. Если CF = 0, программа переключилась в защищенный режим и в сегментные регистры загружены следующие селекторы:

CS: 16-битный селектор с базой, совпадающей со старым CS, и лимитом 64 Кб

DS: селектор с базой, совпадающей со старым DS, и лимитом 64 Кб

SS: селектор с базой, совпадающей со старым SS, и лимитом 64 Кб

ES: селектор с базой, совпадающей с началом блока PSP, и лимитом 100h

FS и GS = 0

Разрядность сегментов данных определяется заявленной разрядностью программы. Остальные регистры сохраняются, а для 32-битных программ старшее слово ESP обнуляется. По адресу ES:[002Ch] записывается селектор сегмента, содержащего переменные окружения DOS.

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

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



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


Теперь воспользуемся интерфейсом DPMI для переключения в защищенный режим и вывода строки на экран. Этот пример будет работать только в системах, предоставляющих DPMI для запускаемых в них DOS-программ, например в Windows 95.

; dpmiex.asm ; Выполняет переключение в защищенный режим средствами DPMI .386 ; 32-битный защищенный режим появился в 80386 ; 16-битный сегмент - в нем выполняется подготовка и переход в защищенный режим RM_seg segment byte public use16 assume cs:RM_seg, ds:PM_seg, ss:RM_stack

; точка входа программы RM_entry: ; проверить наличие DPMI mov ax,1687h ; номер 1678h int 2Fh ; прерывание мультиплексора, test ax,ax ; если АХ не ноль, jnz DPMI_error ; произошла ошибка (DPMI отсутствует), test bl,1 ; если не поддерживается 32-битный режим, jz DPMI_error ; нам тоже нечего делать

; подготовить базовые адреса для будущих дескрипторов mov eax,PM_seg mov ds,ax ; DS - сегментный адрес PM_seg shl eax,4 ; EAX - линейный адрес начала сегмента PM_seg mov dword ptr PM_seg_addr,eax or dword ptr GDT_flatCS+2,eax ; дескриптор для CS or dword ptr GDT_flatDS+2,eax ; дескриптор для DS

; сохранить адрес процедуры переключения DPMI mov word ptr DPMI_ModeSwitch,di mov word ptr DPMI_ModeSwitch+2,es

; ES должен указывать на область данных для переключения режимов, ; у нас она будет совпадать с началом будущего 32-битного стека add eax,offset DPMI_data ; добавить к EAX смещение shr eax,4 ; и превратить в сегментный адрес inc ax mov es,ax

; перейти в защищенный режим mov ах, 1 ; АХ = 1 - мы будем 32 приложением ifdef _WASM_ db 67h ; поправка для wasm endif call dword ptr DPMI_ModeSwitch jc DPMI_error ; если переключения не произошло - выйти

; теперь мы находимся в защищенном режиме, но лимиты всех сегментов ; установлены на 64 Кб, а разрядности сегментов - на 16 бит. ; Нам надо подготовить два ; 32-битных селектора с лимитом 4 Гб - один для кода и один для данных push ds pop es ; ES вообще был сегментом PSP с лимитом 100h mov edi,offset GDT ; EDI - адрес таблицы GDT ; цикл по всем дескрипторам в нашей GDT, mov edx,1 ; которых всего два (0, 1) sel_loop: xor ах,ах ; функция DPMI 00 mov cx,1 int 31h ; создать локальный дескриптор mov word ptr selectors[edx*2],ax ; сохранить селектор mov bx,ax ; в таблицу selectors mov ax,000Ch ; функция DPMI OCh int 31h ; установить селектор add di,8 ; EDI - следующий дескриптор dec dx jns sel_loop


; загрузить селектор сегмента кода в CS при помощи команды RETF push dword ptr Sel_flatCS ; селектор для CS ifdef _WASM_ db 066h endif push offset PM_entry ; EIP db 066h ; префикс размера операнда retf ; выполнить переход в 32-битный сегмент
; сюда передается управление, если произошла ошибка при инициализации DPMI ; (обычно, если DPMI просто нет) DPMI_error: push cs pop ds mov dx,offset nodpmi_msg mov ah,9h ; вывод строки на экран int 21h mov ah,4Ch ; конец ЕХЕ-программы int 21h nodpmi_msg db "Ошибка DPMI$" RM_seg ends
; сегмент PM_seg содержит код, данные и стек для защищенного режима PM_seg segment byte public use32 assume cs:PM_seg,ds:PM_seg,ss:PM_seg ; таблица дескрипторов GDT label byte ; дескриптор для CS GDT_flatCS db 0FFh,0FFh,0h,0h,0h,0FAh,0CFh,0h ; дескриптор для DS GDT_flatDS db 0FFh,0FFh,0h,0h,0h,0F2h,0CFh,0h
; точка входа в 32-битный режим - загружен только CS PM_entry: mov ax,word ptr Sel_flatDS ; селектор для данных mov ds,ax ; в DS mov es,ax ; в ES mov ss,ax ; и в SS mov esp,offset PM_stack_bottom ; и установить стек ; отсюда начинается текст собственно программы, ; программа работает в модели памяти flat с ненулевой базой, ; база CS, DS, ES и SS совпадает и равна линейному адресу начала PM_seg ; все лимиты - 4 Гб mov ах,0300h ; функция DPMI 0300h mov bx,0021h ; прерывание DOS 21h xor есх,есх ; стек не копировать mov edi,offset v86_regs ; ES:EDI - адрес v86_regs int 31h ; вызвать прерывание
mov ah,4Ch ; Это единственный способ int 21h ; правильно завершить DPMI-программу
hello_msg db "Hello world из 32-битного защищенного режима!$"
v86_regs: ; значения регистров для функции DPMI ОЗООп dd 0,0,0,0,0 ; EDI, ESI, EBP, 0, ЕВХ v_86_edx dd offset hello_msg ; EDX dd 0 ; ЕСХ v86_eax dd 0900h ; EAX (AH = 09h, вывод строки на экран) dw 0,0 ; FLAGS, ES v86_ds dw PM_seg ; DS dw 0,0,0,0,0,0 ; FS, GS, 0, 0, SP, SS
; различные временные переменные, нужные для переключения режимов DPMI_ModeSwitch dd ? ; точка входа DPMI PM_seg_addr dd ? ; линейный адрес сегмента PM_seg


; значения селекторов selectors: Sel_flatDS dw ? Sel_flatCS dw ?
; стек для нашей 32-битной программы DPMI_data: ; и временная область данных DPMI одновременно db 16384 dup (?) PM_stack_bottom: PM_seg ends
; стек 16-битной программы, который использует DPMI-сервер при переключении режимов ; Windows 95 требует 16 байт ; CWSDPMI требует 32 байта ; QDPMI требует 96 байт ; мы выберем по максимуму RM_stack segment byte stack "stack" use16 db 96 dup (?) RM_stack ends end RM_entry ; точка входа для DOS - RM_entry
Несмотря на то что DPMI разрешает пользоваться многими прерываниями напрямую и всеми через функцию 0300h, он все равно требует некоторой подготовки для переключения режимов. Кроме того, программа, использующая DPMI для переключения режимов, должна сочетать в себе 16-битный и 32-битный сегменты, что неудобно с точки зрения практического программирования. На самом деле для написания приложений, идущих в защищенном режиме под DOS, никто не применяет переключение режимов вообще — это делают специальные программы, называющиеся расширителями DOS.

Программирование в защищенном режиме


Все, о чем рассказано до этой главы, рассчитано на работу под управлением DOS в реальном режиме процессора (или в режиме V86), унаследованном еще с семидесятых годов. В этом режиме процессор неспособен адресоваться к памяти выше границы первого мегабайта. Кроме того, из-за того, что для адресации используются 16-битные смещения, невозможно работать с массивами больше 65 536 байт. Защищенный режим лишен этих недостатков, в нем можно адресоваться к участку памяти размером 4 Гб как к одному непрерывному массиву и вообще забыть о сегментах и смещениях. Этот режим намного сложнее реального, поэтому, чтобы переключить в него процессор и поддерживать работу в этом режиме, надо написать небольшую операционную систему. Кроме того, если процессор уже находится под управлением какой-то операционной системы, которая перевела его в защищенный режим, например Windows 95, она, скорее всего, не разрешит программе устранить себя от управления компьютером. С этой целью были разработаны специальные интерфейсы, позволяющие программам, запущенным в режиме V86 в DOS, переключаться в защищенный режим простым вызовом соответствующего прерывания — VCPI и DPMI.



Расширители DOS


Расширитель DOS (DOS Extender) — это средство разработки (программа, набор программ, часть компоновщика или просто объектный файл, с которым нужно компоновать свою программу), позволяющее создавать 32-битные приложения, запускающиеся в защищенном режиме с моделью памяти flat и с работающими функциями DPMI. Расширитель сам выполняет переключение в защищенный режим, причем, если в системе уже присутствует DPMI, VCPI или другие средства переключения в защищенный режим из V86, он пользуется ими, а если программа запускается в настоящем реальном режиме, DOS-расширитель сам переводит процессор в защищенный режим и выполняет все необходимые действия для поддержки его работы. Кроме полного или частичного набора функций DPMI расширители DOS обычно поддерживают некоторые функции прерывания 21h, за что и получили свое название. В частности, практически всегда поддерживается функция DOS 09h вывода строки на экран: в DS:EDX помещают селектор:смещение начала строки, и расширитель это правильно интерпретирует (многие DPMI-серверы, включая встроенный сервер Windows 95, тоже эмулируют эту функцию DOS).

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



Способы объединения программы с расширителем


Первые популярные DOS-расширители, такие как Start32, Raw32, System64, 386Power, PMODE и другие, распространяются в виде исходных текстов (Start32 и PMODE оказали решающее влияние на развитие DOS-расширителей в целом). Чтобы использовать такой расширитель, надо скомпилировать его любым ассемблером в объектный файл, который необходимо скомпоновать вместе со своей программой. В большинстве случаев надо назвать точку входа своей программы main или _main и закончить модуль директивой end без параметра, тогда DOS-расширитель получит управление первым и передаст его на метку main после того, как будут настроены все сегменты для модели памяти flat.

Самым популярным из профессиональных компиляторов, поддерживающих расширители DOS, стал компилятор Watcom C/C++, использующий модификацию коммерческого DOS-расширителя DOS4G, названную DOS/4GW. Дело в том, что компоновщик wlink.exe поддерживает, среди большого числа различных форматов вывода, формат линейных исполнимых файлов LE, применяющийся в операционной системе OS/2 (а также, с небольшими модификациями, для драйверов в Windows). Оказалось, что достаточно просто дописать файл в формате OS/2 LE в конец загрузчика DOS-расширителя, написанного соответствующим образом, чтобы потом его запускать. Загрузчик расширителя можно указать прямо в командной строке wlink (командой op stub) или скопировать позже. В комплект поставки расширителей часто входит специальная утилита, которая заменяет загрузчик, находящийся в начале такой программы, на свой.

Чтобы скомпилировать, например, программу lfbfire.asm, которую мы рассмотрим далее, следует воспользоваться следующими командами:

Компиляция:

wasm lfbfire.asm

Компоновка с DOS/4GW (стандартный расширитель, распространяемый с Watcom С):

wlink file lfbfire.obj form os2 le op stub=wstub.exe

Компоновка с PMODE/W (самый популярный из бесплатных расширителей):

wlink file lfbfire.obj form os2 le op stub=pmodew.exe

Компоновка с ZRDX (более строгий с точки зрения реализации):

wlink file lfbfire.obj form os2 le op stub=zrdx.exe


Компоновка с WDOSX (самый универсальный расширитель):

wlink file lfbfire. obj form os2 le op stub=wdosxle.exe

И так далее.

К сожалению, формат исполнимых файлов DOS (так называемый формат MZ), который по умолчанию создают другие компиляторы, крайне неудобен для объединения с расширителями, хотя универсальный расширитель WDOSX способен обработать и такой файл, и даже просто файл с 32-битным кодом без всяких заголовков (какой можно получить, создав СОМ-файл с директивой org 0), и файл в формате РЕ (см. главу 7), хотя и не во всех случаях такие программы будут работать успешно.

И наконец, третий подход к объединению расширителя и программы можно видеть на примере DOS32, в состав которого входит программа dlink.exe, являющаяся компоновщиком, который вполне подойдет вместо link, tlink или wlink, чтобы получить исполнимый файл, работающий с этим расширителем.

Тем не менее популярность подхода, используемого в Watcom, настолько высока, что подавляющее большинство программ, применяющих идею расширителей DOS, написано именно на Watcom С или на ассемблере для WASM.

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


Управление памятью в DPMI


INT 31h, AX = 0100h — Выделить память ниже границы 1 Мб

Ввод: АХ = 0100h
ВХ = требуемый размер в 16-байтных параграфах
Вывод: если CF = 0,
АХ = сегментный адрес выделенного блока для использования в реальном режиме;
DX = селектор выделенного блока для применения в защищенном режиме

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

INT 31h, АХ = 0101h — Освободить память ниже границы 1 Мб

Ввод: АХ = 0101h
DX = селектор освобождаемого блока
Вывод: CF = 0, если не было ошибок

INT 31h, АХ = 0102h — Изменить размер блока, выделенного функцией 0100h

Ввод: АХ = 0102h
ВХ = новый размер блока в 16-байтных параграфах
DX = селектор модифицируемого блока
Вывод: CF = 0, если не было ошибок

INT 31h, АХ = 0500h — Получить информацию о свободной памяти

Ввод: АХ = 0500h
ES:EDI = адрес 48-байтного буфера
Вывод: CF = 0 всегда, буфер заполняется следующей структурой данных:

+00h: 4 байта — максимальный доступный блок в байтах

+04h: 4 байта — число доступных нефиксированных страниц

+08h: 4 байта — число доступных фиксированных страниц

+0Ch: 4 байта — линейный размер адресного пространства в страницах

+10h: 4 байта — общее число нефиксированных страниц

+14h: 4 байта — общее число свободных страниц

+18h: 4 байта — общее число физических страниц

+1Ch: 4 байта — свободное место в линейном адресном пространстве

+20h: 4 байта — размер swap-файла или раздела в страницах

+24h: 0Ch байт — все байты равны FFh

INT 31h, AX = 0501h — Выделить блок памяти

Ввод: АХ = 0501h
ВХ:СХ = размер блока в байтах, больше нуля
Вывод: если CF = 0,
ВХ:СХ = линейный адрес блока;
SI:DI = идентификатор блока для функций 0502 и 0503
<
/p> INT 31h, АХ = 0502h — Освободить блок памяти

Ввод: АХ = 0502h
SI:DI = идентификатор блока
Вывод: CF = 0, если не было ошибки
INT 31h, AX = 0503h — Изменить размер блока памяти

Ввод: АХ = 0503h
ВХ:СХ = новый размер в байтах
SI:DI = идентификатор блока
Вывод: если CF = 0,
ВХ:СХ = новый линейный адрес блока;
SI:DI = новый идентификатор
Нам потребуются еще две функции DPMI для работы с устройством, которое отображает свою память в физическое пространство адресов.

INT 31h, АХ = 0800h — Отобразить физический адрес выше границы 1 Мб на линейный

Ввод: АХ = 0800h
ВХ:СХ = физический адрес
SI:DI = размер области в байтах
Вывод: если CF = 0, ВХ:СХ = линейный адрес, который можно использовать для доступа к этой памяти
INT 31h, AX = 0801h — Отменить отображение, выполненное функцией 0800h

Ввод: АХ = 0801h
ВХ:СХ = линейный адрес, возвращенный функцией 0800h
Вывод: CF = 0, если не было ошибок

Вывод на экран через линейный кадровый буфер


; lfbfire.asm ; Программа, работающая с SVGA при помощи линейного кадрового буфера (LFB), ; демонстрирует стандартный алгоритм генерации пламени. ; Требуется поддержка LFB видеоплатой (или загруженный эмулятор univbe), ; для компиляции необходим DOS-расширитель ; .486p ; для команды xadd .model flat ; основная модель памяти в защищенном режиме .code assume fs:nothing ; нужно только для MASM _start: jmp short _main db "WATCOM" ; нужно только для DOS/4GW

; начало программы ; на входе обычно CS, DS и SS указывают на один и тот же сегмент с лимитом 4 Гб, ; ES указывает на сегмент с PSP на 100h байт, ; остальные регистры не определены _main: sti ; даже флаг прерываний не определен, cld ; не говоря уже о флаге направления

mov ax,0100h ; функция DPMI 0100h mov bx,100h ; размер в 16-байтных параграфах int 31h ; выделить блок памяти ниже 1 Мб jc DPMI_error mov fs,dx ; FS - селектор для выделенного блока

; получить блок общей информации о VBE 2.0 (см. главу 4.5.2) mov dword ptr fs:[0],'2EBV' ; сигнатура VBE2 в начало блока mov word ptr v86_eax,4F00h ; функция VBE 00h mov word ptr v86_es,ax ; сегмент, выделенный DPMI mov ax,0300h ; функция DPMI 300h mov bx,0010h ; эмуляция прерывания 10h xor есх,есх mov edi,offset v86_regs push ds pop es ; ES:EDI - структура v86_regs int 31h ; получить общую информацию VBE2 jc DPMI_error cmp byte ptr v86_eax,4Fh ; проверка поддержки VBE jne VBE_error movzx ebp,word ptr fs:[18] ; объем SVGA-памяти в EBP shl ebp,6 ; в килобайтах

; получить информацию о видеорежиме 101h mov word ptr v86_eax,4F01h ; номер функции INT 10h mov word ptr v86_ecx,101h ; режим 101h - 640x480x256 ; ES:EDI те же самые mov ax,0300h ; функция DPMI 300h - эмуляция mov bx,0010h ; прерывания INT 10h xor ecx,ecx int 31h ; получить данные о режиме jc DPMI_error cmp byte ptr v86_eax,4Fh jne VBE_error test byte ptr fs:[0],80h ; бит 7 байта атрибутов = 1 - LFB есть jz LFB_error

; построение дескриптора сегмента, описывающего LFB ; лимит mov eax,ebp ; видеопамять в килобайтах shl eax,10 ; теперь в байтах dec еах ; лимит = размер - 1 shr еах,12 ; лимит в 4-килобайтных единицах mov word ptr videodsc+0,ax ; записать биты 15-0 лимита shr еах,8 and ah,0Fh or byte ptr videodsc+6,ah ; и биты 19 - 16 лимита mov eax,ebp ; видеопамять в килобайтах shl eax,10 ; в байтах mov edx,dword ptr fs:[40] ; физический адрес LFB shld ebx,edx,16 ; поместить его в CX:DX, shld esi,eax,16 ; а размер - в SI:DI mov ax,800h ; и вызвать функцию DPMI 0800h int 31h ; отобразить физический адрес в линейный shrd edx,ebx,16 ; перенести полученный линейный адрес mov dx,cx ; из BS:CX в EDX mov word ptr videodsc+2,dx ; и записать биты 15-0 базы mov byte ptr videodsc+4,dl ; биты 23 - 16 mov byte ptr videodsc+7,dh ; и биты 31 -24 ; права mov bx,cs lar cx,bx ; прочитать права нашего сегмента and cx,6000h ; и перенести биты DPL or byte ptr videodsc+5,ch ; в строящийся дескриптор


; поместить наш дескриптор в LDT и получить селектор mov ex,1 ; получить один новый дескриптор mov ах,0 ; у DPMI int 31h jc DPMI_error mov word ptr videosel,ax ; записать его селектор push ds pop es mov edi,offset videodsc ; ES:EDI - адрес нашего дескриптора mov bx,ax ; BX - выданный нам селектор mov ax,0Ch ; функция DPMI 0Ch int 31h ; загрузить дескриптор в LDT jc DPMI_error ; теперь в videosel лежит селектор на LFB ; переключение в режим 4101h (101h + LFB) mov word ptr v86_eax,4F02h ; 4F02h - установить SVGA-режим mov word ptr v86_ebx,4101h ; режим 4101h = 101h + LFB mov edi,offset v86_regs ; ES:EDI - структура v86_regs mov ах,0300h ; функция DPMI 300h mov bx,0010h ; эмуляция прерывания 10h xor ecx,ecx int 31h
mov ax,word ptr videosel ; AX - наш селектор enter_flame: ; сюда придет управление с селектором ; в ах на A000h:0000, если произошла ; ошибка в любой VBE-функции mov es,ax ; ES - селектор видеопамяти или LFB
; отсюда начинается процедура генерации пламени
; генерация палитры для пламени xor edi,edi ; начать писать палитру с адреса ES:0000 xor есх,есх palette_gen: xor еах,еах ; цвета начинаются с 0, 0, 0 mov cl,63 ; число значений для одной компоненты palette_11: stosb ; записать байт inc eax ; увеличить компоненту cmpsw ; пропустить два байта loop palette_11 ; и так 64 раза
push edi mov cl,192 palette_12: stosw ; записать два байта inc di ; и пропустить один loop palette_12 ; и так 192 раза (до конца палитры) pop edi ; восстановить EDI inc di jns palette_gen
; палитра сгенерирована, записать ее в регистры VGA DAC (см. главу 5.10.4) mov al,0 ; начать с регистра 0 mov dx,03C8h ; индексный регистр на запись out dx,al push es push ds ; поменять местами ES и DS pop es pop ds xor esi,esi mov ecx,256*3 ; писать все 256 регистров mov edx,03C9h ; в порт данных VGA DAC rep outsb push es push ds ; поменять местами ES и DS pop es pop ds
; основной цикл - анимация пламени, пока не будет нажата любая клавиша, xor edx,edx ; должен быть равен нулю mov ebp,7777h ; любое число mov ecx,dword ptr scr_width ; ширина экрана main_loop: push es ; сохранить ES push ds pop es ; ES = DS - мы работаем только с буфером ; анимация пламени (классический алгоритм) inc ecx mov edi,offset buffer mov ebx,dword ptr scr_height shr ebx,1 dec ebx mov esi,scr_width animate_flame: mov ax,[edi+esi*2-1] ; вычислить среднее значение цвета add al,ah ; в данной точке (EDI) из значений setc ah ; цвета в точке слева и на две строки mov dl,[edi+esi*2+1] ; вниз, справа и на две строки вниз add ax,dx mov dl,[edi+esi*4] ; и на четыре строки вниз. add ax,dx ; причем первое значение shr ax,2 ; модифицировать jz already_zero ; уменьшить яркость цвета dec ax already_zero: stosb ; записать новый цвет в буфер add eax,edx shr eax,1 mov byte ptr [edi+esi-1],al loop animate_flame mov ecx,esi add edi,ecx dec ebx jnz animate_flame


; псевдослучайная полоска внизу экрана, которая служит генератором пламени generator_bar: xadd bp,ax stosw stosw loop generator_bar pop es ; восстановить ES для вывода на экран
;вывод пламени на экран xor edi,edi ; ES:EDI - LFB push esi add esi,offset buffer ; DS:ESI - буфер mov ecx,dword ptr scr_size ; размер буфера в двойных словах rep movsd ; скопировать буфер на экран pop esi mov ah,1 ; если не была нажата int 16h ; никакая клавиша, jz main_loop ; продолжить основной цикл, mov ah,0 ; иначе - int 16h ; считать эту клавишу
exit_all: mov ах,03h ; восстановить текстовый режим int 10h mov ax,4C00h ; AH = 4Ch int 21h ; выход из программы под расширителем DOS
; различные обработчики ошибок DPMI_error: ; ошибка при выполнении ; одной из функций DPMI mov edx,offset DPMI_error_msg mov ah,9 int 21h ; вывести сообщение об ошибке jmp short exit_all ; и выйти VBE_error: ; не поддерживается VBE mov edx,offset VBE_error_msg mov ah,9 int 21h ; вывести сообщение об ошибке jmp short start_with_vga ; и использовать VGA LFB_error: ; не поддерживается LFB mov edx,offset LFB_error_msg mov ah,9 ; вывести сообщение об ошибке int 21h start_with_vga: mov ah,0 ; подождать нажатия любой клавиши int 16h mov ax,13h ; переключиться в видеорежим 13h int 10h ; 320x200x256 mov ax,2 ; функция DPMI 0002h mov bx,0A000h ; построить дескриптор для реального int 31h ; сегмента mov dword ptr scr_width,320 ; установить параметры режима mov dword ptr scr_height,200 mov dword ptr scr_size,320*200/4 jmp enter_flame ; и перейти к пламени
.data ; различные сообщения об ошибках VBE_error_msg db "Ошибка VBE 2.0",0Dh,0Ah db "Будет использоваться режим VGA 320x200",0Dh,0Ah,'$' DPMI_error_msg db "Ошибка DPMI$" LFB_error_msg db "LFB недоступен",0Dh,0Ah db "Будет использоваться режим VGA 320x200",0Dh,0Ah,'$' ; параметры видеорежима scr_width dd 640 scr_height dd 480 scr_size dd 640*480/4 ; структура, используемая функцией DPMI 0300h v86_regs label byte v86_edi dd 0 v86_esi dd 0 v86_ebp dd 0 v86_res dd 0 v86_ebx dd 0 v86_edx dd 0 v86_ecx dd 0 v86_eax dd 0 v86_flags dw 0 v86_es dw 0 v86_ds dw 0 v86_fs dw 0 v86_gs dw 0 v86_ip dw 0 v86_cs dw 0 v86_sp dw 0 v86_ss dw 0 ; дескриптор сегмента, соответствующего LFB videodsc dw 0 ; биты 15-0 лимита dw 0 ; биты 15-0 базы db 0 ; биты 16-23 базы db 10010010b ; доступ db 10000000b ; биты 16-19 лимита и другие биты db 0 ; биты 24 - 31 базы ; селектор сегмента, описывающего LFB videosel dw 0
.data? ; буфер для экрана buffer db 640*483 dup(?)
; стек .stack 1000h end _start
Программирование с DOS-расширителями — один из лучших выходов для приложений, которые должны работать в любых условиях, включая старые версии DOS, и в то же время требуют 32-битный режим. Еще совсем недавно большинство компьютерных игр, в частности знаменитые Doom и Quake, выпускались именно как программы, использующие расширители DOS. Сейчас, с повсеместным распространением операционных систем для PC, работающих в 32-битном защищенном режиме, требование работы в любых версиях DOS становится менее актуальным и все больше программ выходят только в версиях для Windows 95 или NT, чему и будет посвящена следующая глава.

Диалоги


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

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

// windlg.rc // Файл ресурсов, описывающий диалог, используемый в программе windlg.asm. // Все следующие определения можно заменить на #include // <winuser.h>, если он есть:

// стили для диалогов #define DS_CENTER 0x0800L #define DS_MODALFRAME 0x80L #define DS_3DLOOK 0x0004L

// стили для окон #define WS_MINIMIZEBOX 0x00020000L #define WS_SYSMENU 0x00080000L #define WS_VISIBLE 0x10000000L #define WS_OVERLAPPED 0x00000000L #define WS_CAPTION 0xC00000L

// стили для редактора #define ES_AUTOHSCROLL 0x80L #define ES_LEFT 0 #define ZDLG_MENU 7

// идентификаторы контролов диалога #define IDC_EDIT 0 #define IDC_BUTTON 1 #define IDC_EXIT 2

// идентификаторы пунктов меню #define IDM_GETTEXT 10 #define IDM_CLEAR 11 #define IDM_EXIT 12

ZZZ_Dialog DIALOG 10,10,205,30 // x, у, ширина, высота STYLE DS_CENTER | DS_MODALFRAME | DS_3DLOOK | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED CAPTION "Win32 assembly dialog example" // заголовок MENU ZDLG_MENU // меню BEGIN // начало списка контролов EDITTEXT IDC_EDIT,15,7,111,13,ES_AUTOHSCROLL | ES_LEFT PUSHBUTTON "E&xit",IDC_EXIT,141,8,52,13 END


ZDLG_MENU MENU // меню BEGIN POPUP "Test" BEGIN MENUITEM "Get Text",IDM_GETTEXT MENUITEM "Clear Text",IDM_CLEAR MENUITEM SEPARATOR MENUITEM "E&xit",IDM_EXIT END END

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

; windlg.asm ; Графическое win32-приложение, демонстрирующее работу с диалогом

; идентификаторы контролов (элементов диалога) IDC_EDIT equ 0 IDC_BUTTON equ 1 IDC_EXIT equ 2

; идентификаторы элементов меню IDM_GETTEXT equ 10 IDM_CLEAR equ 11 IDM_EXIT equ 12

include def32.inc include kernel32.inc include user32.inc

.386 .model flat .data dialog_name db "ZZZ_Dialog",0 ; имя диалога в ресурсах .data? buffer db 512 dup(?) ; буфер для введенного текста .code _start: xor ebx,ebx ; в EBX будет 0 для команд push 0 ; (короче в 2 раза) ; определить идентификатор нашей программы push ebx call GetModuleHandle ; запустить диалог push ebx ; значение, которое перейдет как параметр WM_INITDIALOG push offset dialog_proc ; адрес процедуры типа DialogProc push ebx ; идентификатор окна-предка (0 - ничей диалог) push offset dialog_name ; адрес имени диалога в ресурсах push eax ; идентификатор программы, в ресурсах которой ; находится диалог (наш идентификатор в ЕАХ) call DialogBoxParam ; выход из программы push ebx call ExitProcess ; ; процедура dialog_proc ; вызывается диалогом каждый раз, когда в нем что-нибудь происходит ; именно здесь будет происходить вся работа программы ; ; процедура не должна изменять регистры EBP,EDI,ESI и ЕВХ! ; dialog_proc proc near ; так как мы получаем параметры в стеке, построим стековый кадр push ebp mov ebp,esp ; процедура типа DialogProc вызывается со следующими параметрами: dp_hWnd equ dword ptr [ebp+08h] ; идентификатор диалога dp_uMsg equ dword ptr [ebp+0Ch] ; номер сообщения dp_wParam equ dword ptr [ebp+10h] ; первый параметр dp_lParam equ dword ptr [ebp+14h] ; второй параметр



mov ecx,dp_hWnd ; ECX будет хранить идентификатор диалога, mov eax,dp_uMsg ; a EAX - номер сообщения, cmp eax,WM_INITDIALOG ; если мы получили WM_INITDIALOG jne not_initdialog push IDC_EDIT push dp_hWnd call GetDlgItem ; определить идентификатор push eax ; окна редактирования текста call SetFocus ; и передать ему фокус, not_initdialog: cmp eax,WM_CLOSE ; если мы получили WM_CLOSE, jne not_close push 0 push ecx call EndDialog ; закрыть диалог, not_close: cmp eax,WM_COMMAND ; если мы получили WM_COMMAND, jne not_command mov eax,dp_wParam ; EAX = wParam (номер сообщения), cmp dp_lParam,0 ; если lparam ноль - сообщение от меню, jne lParam_not_0 cmp ax,IDM_GETTEXT ; если это пункт меню Get Text jne not_gettext push 512 ; размер буфера push offset buffer ; адрес буфера push IDC_EDIT ; номер конрола редактирования push ecx call GetDlgItemText ; считать текст в buffer push MB_OK push offset dialog_name push offset buffer push dp_hWnd call MessageBox ; и показать его в MessageBox, not_gettext: cmp eax,IDM_CLEAR ; если это пункт меню Clear jne not_clear push 0 ; NULL push IDC_EDIT ; номер контрола push ecx call SetDlgItemText ; установить новый текст, not_clear: cmp eax,IDM_EXIT ; если это пункт меню Exit jne not_exit push 0 ; код возврата push ecx ; идентификатор диалога call EndDialog ; закрыть диалог lParam_not_0: ; lParam не ноль - сообщение от контрола, cmp eax,IDC_EXIT ; если сообщение от кнопки Exit, jne not_exit shr eax,16 cmp eax,BN_CLICKED ; если ее нажали push 0 ; код возврата push ecx ; идентификатор диалога call EndDialog ; закрыть диалог not_exit: xor eax,eax ; после обработки команды inc eax ; DialogProc должен возвращать TRUE (eax = 1) leave ret 16 ; конец процедуры not_command: ; сюда передается управление, если мы получили ; какое-то незнакомое сообщение xor еах,еах ; код возврата FALSE (eax = 0) leave ret 16 ; конец процедуры dialog_proc endp end _start

Добавления в наш user32.inc:

между ifdef _TASM_ и else:

extrn DialogBoxParamA:near extrn GetDlgItem:near extrn SetFocus:near extrn GetDlgItemTextA:near extrn SetDlgItemTextA:near extrn EndDialog:near DialogBoxParam equ DialogBoxParamA GetDlgltemText equ GetDlgItemTextA SetDlgltemText equ SetDlgItemTextA

между else и endif:

extrn __imp__DialogBoxParamA@20:dword extrn __imp__GetDlgItem@8:dword extrn __imp__SetFocus@4:dword extrn __imp__GetDlgItemTextA@16:dword extrn __imp__SetDlgItemTextA@12:dword extrn __imp__EndDialog@8:dword DialogBoxParam equ __imp__DialogBoxParamA@20 GetDlgItem equ __imp__GetDlgItem@8 SetFocus equ __imp__SetFocus@4 GetDlgItemText equ __imp__GetDlgItemTextA@16 SetDlgItemText equ __imp__SetDlgItemTextA@12 EndDialog equ __imp__EndDialog@8

Добавления к файлу def32.inc:

; из winuser.h WM_INITDIALOG equ 110h WM_CLOSE equ 10h BN_CLICKED equ 0


Динамические библиотеки


Кроме обычных приложений в Windows появился специальный тип файла — динамические библиотеки (DLL). DLL — это файл, содержащий процедуры и данные, которые доступны программам, обращающимся к нему. Например, все системные функции Windows, которыми мы пользовались, на самом деле были процедурами, входящими в состав таких библиотек, — kernel32.dll, user32.dll, comdlg32.dll и т.д. Динамические библиотеки позволяют уменьшить использование памяти и размер исполнимых файлов для тех случаев, когда несколько программ (или даже несколько копий одной и той же программы) используют одну и ту же процедуру. Можно считать, что DLL — это аналог пассивной резидентной программы, с тем лишь отличием, что DLL не находится в памяти, если ни одна программа, его использующая, не загружена.

С точки зрения программирования на ассемблере DLL — это самый обычный исполнимый файл формата РЕ, отличающийся только тем, что при входе в него в стеке находятся три параметра (идентификатор DLL-модуля, причина вызова процедуры и зарезервированный параметр), которые надо удалить, например командой ret 12. Кроме этой процедуры в DLL входят и другие, часть которых можно вызывать из других программ. Список этих экспортируемых процедур должен быть задан во время компиляции DLL, и поэтому команды для компиляции нашего следующего примера будут отличаться от обычных.

Компиляция MASM:

ml /с /coff /Cp /D_MASM_ dllrus.asm link dllrus.obj @dllrus.lnk

Содержимое файла dllrus.lnk:

/DLL /entry:start /subsystem:windows /export:koi2win_asm /export:koi2win /export:koi2wins_asm /export:koi2wins

Компиляция TASM:

tasm /m /x /ml /D_TASM_ dllrus.asm tlink32 -Tpd -c dllrus.obj,,,,dllrus.def

Содержимое файла dllrus.def:

EXPORTS koi2win_asm koi2win koi2wins koi2wins_asm

Компиляция WASM:

wasm dllrus.asm wlink @dllrus.dir

Содержимое dllrus.dir:

file dllrus.obj form windows nt DLL exp koi2win_asm,koi2win,koi2wins_asm,koi2wins

; dllrus.asm ; DLL для Win32 - перекодировщик из koi8 в ср1251 .386 .model flat ; функции, определяемые в этом DLL ifdef _MASM_ public _koi2win_asm@0 ; koi2win_asm - перекодирует символ в AL public _koi2win@4 ; CHAR WINAPI koi2win(CHAR symbol) public _koi2wins_asm@0 ; koi2wins_asm - перекодирует строку в [ЕАХ] public _koi2wins@4 ; VOID WINAPI koi2win(CHAR * string) else public koi2win_asm ; те же функции для TASM и WASM public koi2win public koi2wins_asm public koi2wins endif


.const ; таблица для перевода символа из кодировки KOI8-r (RFC1489) ; в кодировку Windows (cp1251), ; таблица только для символов 80h - FFh ; (то есть надо будет вычесть 80h из символа, преобразовать его командой xlat ; и снова добавить 80h) k2w_tbl db 16 dup(0) ; символы, не существующие в ср1251, db 16 dup(0) ; перекодируются в 80h db 00h, 00h, 00h, 38h, 00h, 00h, 00h, 00h db 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h db 00h, 00h, 00h, 28h, 00h, 00h, 00h, 00h db 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h db 7Eh, 60h, 61h, 76h, 64h, 65h, 74h, 63h db 75h, 68h, 69h, 6Ah, 6Bh, 6Ch, 6Dh, 6Eh db 6Fh, 7Fh, 70h, 71h, 72h, 73h, 66h, 62h db 7Ch, 7Bh, 67h, 78h, 7Dh, 79h, 77h, 7Ah db 5Eh, 40h, 41h, 56h, 44h, 45h, 54h, 43h db 55h, 48h, 49h, 4Ah, 4Bh, 4Ch, 4Dh, 4Eh db 4Fh, 5Fh, 50h, 51h, 52h, 53h, 46h, 42h db 5Ch, 5Bh, 47h, 58h, 5Dh, 59h, 57h, 5Ah

.code ; процедура DLLEntry. Получает три параметра - идентификатор, причину вызова ; и зарезервированный параметр. Нам не нужен ни один из них _start@12: mov al,1 ; надо вернуть ненулевое число в ЕАХ ret 12

; процедура BYTE WINAPI koi2win (BYTE symbol) - ; точка входа для вызова из С ifdef _MASM_ _koi2win@4 proc else koi2win proc endif pop ecx ; обратный адрес в ЕСХ pop eax ; параметр в ЕСХ (теперь стек очищен ; от параметров!) push ecx ; обратный адрес вернуть в стек для RET ; здесь нет команды RET - управление передается следующей процедуре ifdef _MASM_ _koi2win@4 endp else koi2win endp endif

; процедура koi2win_asm ; точка входа для вызова из ассемблерных программ: ; ввод: AL - код символа в KOI ; вывод: AL - код этого же символа в WIN ifdef _MASM_ _koi2win_asm@0 proc else koi2win_asm proc endif test al,80h ; если символ меньше 80h (старший бит 0), jz dont_decode ; не перекодировать, push ebx ; иначе - mov ebx,offset k2w_tbl sub al,80h ; вычесть 80h, xlat ; перекодировать add al,80h ; и прибавить 80h pop ebx dont_decode: ret ; выйти ifdef _MASM_ _koi2win_asm@0 endp else koi2win_asm endp endif

; VOID koi2wins(BYTE * koistring) - ; точка входа для вызова из С ifdef _MASM_ _koi2wins@4 proc else koi2wins proc endif pop ecx ; адрес возврата из стека pop eax ; параметр в ЕАХ push ecx ; адрес возврата в стек ifdef _MASM_ _koi2wins@4 endp else koi2wins endp endif ; точка входа для вызова из ассемблера: ; ввод: ЕАХ - адрес строки, которую надо преобразовать из KOI в WIN ifdef _MASM_ _koi2wins_asm@0 proc else koi2wins_asm proc endif push esi ; сохранить регистры, которые ; нельзя изменять push edi push ebx mov esi,eax ; приемник строк mov edi,eax ; и источник совпадают mov ebx,offset k2w_tbl decode_string: lodsb ; прочитать байт, test al,80h ; если старший бит 0, jz dont_decode2 ; не перекодировать, sub al,80h ; иначе - вычесть 80h, xlat ; перекодировать add al,80h ; и добавить 80h dont_decode2: stosb ; вернуть байт на место, test al,al ; если байт - не ноль, jnz decode_string ; продолжить pop ebx pop edi pop esi ret ifdef _MASM_ _koi2wins_asm@0 endp else koi2wins_asm endp endif end _start@l2



Как видно из примера, нам пришлось назвать все процедуры по-разному для различных ассемблеров. В случае MASM понятно, что все функции должны иметь имена типа _start@12, а иначе программам, использующим их, придется обращаться к функциям с именами типа _Jmp_start, то есть такой DLL нельзя будет использовать из программы, написанной на Microsoft С. В случае TASM и WASM процедуры могут иметь неискаженные имена (и более того, wlink.exe не позволяет экспортировать имя переменной, содержащее символ @), так как их компиляторы берут имена процедур не из библиотечного файла, а прямо из DLL при помощи соответствующей программы — implib или wlib.

Итак, чтобы воспользоваться полученным DLL, напишем простую программу, которая перекодирует одну строку из КОI-8r в Windows ср1251.

; dlldemo.asm ; Графическое приложение для Win32, демонстрирующее работу с dllrus.dll, ; выводит строку в KOI8 и затем в ср1251, перекодированную функцией koi2wins include def32.inc include user32.inc include kernel32.inc includelib dllrus.lib ifndef _MASM_ extrn koi2win__asm:near ; определения для функций из DLL для extrn koi2win:near ; TASM и WASM extrn koi2wins_asm:near ; (хотя для WASM было бы эффективнее extrn koi2wins:near ; использовать __imp__koi2win, выделив else ; его в отдельный условный блок), extrn __imp__koi2win_asm@0:dword ; а это для MASM extrn __imp__koi2win@4:dword extrn __imp__koi2wins_asm@0:dword extrn __imp__koi2wins@4: dword koi2win_asm equ __imp__koi2win_asm@0 koi2win equ __imp__koi2win@4 koi2wins_asm equ __imp__koi2wins_asm@0 koi2wins equ __imp__koi2wins@4 endif

.386 .model flat .const title_string1 db "koi2win demo: string in KOI8",0 title_string2 db "koi2win demo: string in cp1251",0

.data koi_string db 0F3h,0D4h,0D2h,0CFh,0CBh,0C1h,20h,0CEh,0C1h db 20h,0EBh,0EFh,0E9h,2Dh,28h,0 .code _start: push MB_OK push offset title_string1 ; заголовок окна MessageBox push offset koi_string ; строка на KOI push 0 call MessageBox mov eax,offset koi_string push eax call koi2wins push MB_OK push offset title_string2 push offset koi_string push 0 call MessageBox push 0 ; код выхода call ExitProcess ; конец программы

end _start

Этот небольшой DLL может оказаться очень полезным для расшифровки текстов, приходящих из сети Internet или других систем, в которых используется кодировка KOI8. Воспользовавшись таблицами из приложения 1, вы можете расширить набор функций dllrus.dll, вплоть до перекодировки из любого варианта в какой угодно.


Драйверы устройств


В Windows, так же как и в DOS, существует еще один вид исполнимых файлов — драйверы устройств. Windows 3.x и Windows 95 используют одну модель драйверов, Windows NT — другую, a Windows 98 — уже третью, хотя и во многом близкую к модели Windows NT. В Windows 3.x/Windows 95 используются два типа драйверов устройств — виртуальные драйверы (VxD), выполняющиеся с уровнем привилегий 0 (обычно имеют расширение .386 для Windows 3.x и .VXD для Windows 95), и непривилегированные драйверы, исполняющиеся, как и обычные программы, с уровнем привилегий 3 (обычно имеют расширение .DRV). Windows NT использует несовместимую модель драйверов, так называемую kernel-mode (режим ядра). На основе модели kernel-mode с добавлением поддержки технологии PNP и понятия потоков данных в 1996 году была основана модель WDM (win32 driver model), которая теперь используется в Windows 98 и NT и, по-видимому, будет играть главную роль в дальнейшем.

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

Чтобы самостоятельно создавать драйверы для любой версии Windows, необходим комплект программ, документации, включаемых файлов и библиотек, распространяемый Microsoft, который называется DDK (Drivers Development Kit). (DDK для Windows NT и Windows 98 распространяются бесплатно.)

Мы не будем рассматривать программирование драйверов для Windows в деталях, так как этой теме посвящено достаточно много литературы, не говоря уже о документации, прилагающейся к DDK. Чтобы создать драйвер, в любом случае лучше всего начать с одного из прилагающихся примеров и изменять/добавлять процедуры инициализации, обработчики сообщений, прерываний и исключений, обработчики для API, предоставляемого драйвером, и т.д. Рассмотрим только, как выглядит исходный текст драйвера, потому что он несколько отличается от привычных нам ассемблерных программ.


Любой драйвер начинается с директивы include vmm.inc, которая включает файл, содержащий определения используемых сегментов и макроопределений. Макроопределения вида VXD_LOCKED_CODE_SEG/VXD_LOCKED_CODE_ENDS соответствуют директивам начала и конца соответствующих сегментов (в данном случае сегмента _LTEXT). Другие два важных макроопределения Declare_Virtual_Device и VMMCall/WDMCall. Первое — это просто определение, получающее в качестве параметров идентификатор драйвера, название, версию, порядок загрузки и адреса основных процедур драйвера, из которых строится заголовок драйвера. Второе — это замена команды call, получающая в качестве параметра имя функции VMM или WDM, к которой надо обратиться. Например, элемент кода драйвера BIOSXLAT, перехватывающий прерывание 10h, выглядит следующим образом:

VxD_ICODE_SEG ; начало сегмента _ITEXT (сегмент кода ; инициализации, исполняющийся ; в защищенном режиме, который удаляется ; из памяти после сообщения Init_Complete) BeginProc BIOSXlat_Sys_Critical_Init ; процедура, которая вызывается ; для обработчика сообщения ; Sys_Critical_Init - первого сообщения, ; которое получает драйвер. ; Обычно обработчики сообщений должны ; сохранять регистры ЕВХ, EDI, ESI и ЕВР, ; хотя в этом случае этого можно не делать mov esi,OFFSET32 BIOSXlat_Int10 ; адрес обработчика INT 10h ; в регистр ESI. Важно использовать ; макроопределение OFFSET32 всюду ; вместо offset mov edx,10h ; любое число, которое будет ; помещаться в EDX при вызове ; регистрируемого обработчика VMMCall Allocate_PM_Call_Back ; зарегистрировать ; точку входа, обращаясь ; к которой, программы из защищенного ; режима в VM будут передавать ; управление процедуре в драйвере ;(но не win32-nporpaммa) - ; они должны использовать ; DeviceIOControl для работы ; с драйверами, jc BXLSCI_NoPM ; если CF = 1 - произошла ошибка xchg edx,eax ; точку входа - в EDX, ; число 10h - в ЕАХ ; (теперь это - номер перехватываемого прерывания для Set_PM_Int_Vector) mov ecx,edx shr ecx,10h ; селектор точки входа movzx edx,dx ; смещение точки входа VMMCall Set_PM_Int_Vector ; установить обработчик ; прерывания INT 10h. ; Если эта функция вызвана до Sys_VM_Init, установленный обработчик ; становится звеном цепочки обработчиков для всех виртуальных машин. ; После того как прерывание проходит по цепочке обработчиков ; в защищенном режиме, оно отображается в V86, точно так же, как в DPMI



; [код перехвата других прерываний]

EndProc BIOSXlat_Sys_Critical_Init ; конец процедуры VxD_ICODE_ENDS ; конец сегмента инициализации

Соответственно, чтобы эта процедура была вызвана для сообщения Sys_Critical_Init, в сегменте фиксированного кода _LTEXT должна быть следующая запись:

VxD_LQCKED_CODE_SEG ; начало сегмента _LTEXT BeginProc BIOSXlat_Control ; начало процедуры Control_Dispatch Sys_Critical_Init,\BIOSXlat_Sys_Critical_Init ; при помощи еще одного макроопределения из vmm.inc ; зарегистрировать процедуру BIOSXlat_Sys_Critical_Init ; как обработчик сообщения Sys_Critical_Init clc ; процедура-обработчик управляющих ; сообщений должна возвращать CF = 0 ret EndProc BIOSXlat_Control ; конец процедуры VxD_LOCKED_CODE_ENDS ; конец сегмента _LTEXT

И наконец, процедура BIOSXlat_Control регистрируется как процедура, получающая управляющие сообщения, в заголовке драйвера:

; первая строка после .386р и include vmm.inc: Declare_Virtual_Device BlOSXlat, 1, 0, BIOSXlat_Control,\ BIOSXiat_Device_ID, BIOSXlat_Init_Order

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


Программирование для Windows 95 и Windows NT


Несмотря на то что Windows 95 и Windows NT кажутся более сложными операционными системам по сравнению с DOS, программировать для них на ассемблере намного проще. С одной стороны, Windows-приложение запускается в 32-битном режиме (мы не рассматриваем Windows 3.11 и более старые версии, которые работали в 16-битном режиме) с моделью памяти flat, так что программист получает все те преимущества, о которых говорилось в предыдущей главе, а с другой стороны — нам больше не нужно изучать в деталях, как программировать различные устройства компьютера на низком уровне. В настоящих операционных средах приложения пользуются только системными вызовами, число которых здесь превышает 2000 (около 2200 для Windows 95 и 2434 для Windows NT). Все Windows-приложения используют специальный формат исполнимых файлов — формат PE (Portable Executable). Такие файлы начинаются как обычные EXE-файлы старого образца (их также называют MZ по первым двум символам заголовка), и, если такой файл запустить из DOS, он выполнится и выдаст сообщение об ошибке (текст сообщения зависит от используемого компилятора), в то время как Windows заметит, что после обычного MZ-заголовка файла идет PE-заголовок, и запустит приложение. Ёто будет означать только то, что для компиляции программ потребуются другие параметры в командной строке.



Консольные приложения


Исполнимые программы для Windows делятся на два основных типа — консольные и графические приложения. При запуске консольного приложения открывается текстовое окно, с которым программа может общаться функциями WriteConsole()/ReadConsole() и другими (соответственно при запуске из другого консольного приложения, например, файлового менеджера FAR, программе отводится текущая консоль и управление не возвращается к FAR, пока программа не закончится). Графические приложения соответственно не получают консоли и должны открывать окна, чтобы вывести что-нибудь на экран.

Для компиляции консольных приложений мы будем пользоваться следующими командами:

MASM:

ml /с /coff /Cp winurl.asm link winurl.asm /subsystem console

TASM:

tasm /m /ml /D_TASM_ winurl.asm tlink32 /Тре /ар /с /x winurl.obj

WASM:

wasm winurl.asm wlink file winurl.obj form windows nt runtime console op с

Попробуйте скомпилировать программу winurl.asm этим способом, чтобы увидеть, как отличается работа консольного приложения от графического.

В качестве примера полноценного консольного приложения напишем программу, которая перечислит все подключенные сетевые ресурсы (диски и принтеры), используя системные функции WNetOpenEnum(), WNetEnumResource() и WNetCloseEnum().

; netenum.asm ; Консольное приложение для win32, перечисляющее сетевые ресурсы include def32.inc include kernel32.inc include mpr.inc

.386 .model flat .const greet_message db 'Example win32 console program',0Dh,0Ah,0Dh,0Ah,0 error1_message db 0Dh,0Ah,'Could not get current user name',0Dh,0Ah,0 error2_message db 0Dh,0Ah,'Could not enumerate',0Dh,0Ah,0 good_exit_msg db 0Dh,0Ah,0Dh,0Ah,'Normal termination',0Dh,0Ah,0 enum_msg1 db 0Dh,0Ah,'Local ',0 enum_msg2 db ' remote - ',0 .data user_name db 'List of connected resources for user ' user_buff db 64 dup (?) ; буфер для WNetGetUser user_buff_l dd $-user_buff ; размер буфера для WNetGetUser enum_buf_l dd 1056 ; длина enum_buf в байтах enum_entries dd 1 ; число ресурсов, которые в нем помещаются .data? enum_buf NTRESOURCE <?,?,?,?,?,?,?,?> ; буфер для WNetEnumResource dd 256 dup (?) ; 1024 байт для строк message_l dd ? ; переменная для WriteConsole enum_handle dd ? ; идентификатор для WNetEnumResource .code _start: ; получим от системы идентификатор буфера вывода stdout push STD_OUTPUT_HANDLE call GetStdHandle ; возвращает идентификатор STDOUT в eax mov ebx,eax ; а мы будем хранить его в EBX


; выведем строку greet_message на экран mov esi,offset greet_message call output_string

; определим имя пользователя, которому принадлежит наш процесс mov esi,offset user_buff push offset user_buff_l ; адрес переменной с длиной буфера push esi ; адрес буфера push 0 ; NULL call WNetGetUser cmp eax,NO_ERROR ; если произошла ошибка jne error_exit1 ; выйти mov esi,offset user_name ; иначе - выведем строку на экран call output_string

; начнем перечисление сетевых ресурсов push offset enum_handle ; идентификатор для WNetEnumResource push 0 push RESOURCEUSAGE_CONNECTABLE ; все присоединяемые ресурсы push RESOURCETYPE_ANY ; ресурсы любого типа push RESOURCE_CONNECTED ; только присоединенные сейчас call WNetOpenEnum ; начать перечисление cmp eax,NO_ERROR ; если произошла ошибка jne error_exit2 ; выйти

; цикл перечисления ресурсов enumeration_loop: push offset enum_buf_l ; длина буфера в байтах push offset enum_buf ; адрес буфера push offset enum_entries ; число ресурсов push dword ptr enum_handle ; идентификатор от WNetOpenEnum call WNetEnumResource cmp eax,ERROR_NO_MORE_ITEMS ; если они закончились je end_enumeration ; завершить перечисление cmp eax,NO_ERROR ; если произошла ошибка jne error_exit2 ; выйти с сообщением об ошибке

; вывод информации ресурсе на экран mov esi,offset enum_msg1 ; первая часть строки call output_string ; на консоль mov esi,dword ptr enum_buf.lpLocalName ; локальное имя устройства call output_string ; на консоль mov esi,offset enum_msg2 ; вторая часть строки call output_string ; на консоль mov esi,dword ptr enum_buf.lpRemoteName ; удаленное имя устройства call output_string ; туда же

jmp short enumeration_loop ; продолжим перечисление ; конец цикла end_enumeration: push dword ptr enum_handle call WNetCloseEnum ; конец перечисления

mov esi,offset good_exit_msg exit_program: call output_string ; выведем строку push 0 ; код выхода call ExitProcess ; конец программы ; выходы после ошибок error_exit1: mov esi,offset error1_message jmp short exit_program error_exit2: mov esi,offset error2_message jmp short exit_program



; процедрура output_string ; выводит на экран строку ; ввод: esi - адрес строки ; ebx - идентификатор stdout или другого консольного буфера

output_string proc near

; определим длину строки cld xor eax,eax mov edi,esi repne scasb dec edi sub edi,esi ; пошлем ее на консоль push 0 push offset message_l ; сколько байт выведено на консоль push edi ; сколько байт надо вывести на консоль push esi ; адрес строки для вывода на консоль push ebx ; идентификатор буфера вывода call WriteConsole ; WriteConsole(hConsoleOutput,lpvBuffer,cchToWrite, ; lpcchWritten,lpvReserved) ret output_string endp

end _start

В файл kernel32.inc надо добавить между ifdef _TASM_ и else строки:

extrn GetStdHandle:near extrn WriteConsoleA:near WriteConsole equ WriteConsoleA

и между else и endif:

extrn __imp__GetStdHandle@4:dword extrn __imp__WriteConsoleA@20:dword GetStdHandle equ __imp__GetStdHandle@4 WriteConsole equ __imp__WriteConsoleA@20

Кроме того, надо создать файл mpr.inc:

; mpr.inc ; включаемый файл с определениями функций из mpr.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn WNetGetUserA:near extrn WNetOpenEnumA:near extrn WNetEnumResourceA:near extrn WNetCloseEnum:near ; присваивания для облегчения читаемости кода WNetGetUser equ WNetGetUserA WNetOpenEnum equ WNetOpenEnumA WNetEnumResource equ WNetEnumResourceA else includelib mpr.lib ; истинные имена используемых функций extrn __imp__WNetGetUserA@12:dword extrn __imp__WNetOpenEnumA@20:dword extrn __imp__WNetEnumResourceA@16:dword extrn __imp__WNetCloseEnum@4:dword ; присваивания для облегчения читаемости кода WNetGetUser equ __imp__WNetGetUserA@12 WNetOpenEnum equ __imp__WNetOpenEnumA@20 WNetEnumResource equ __imp__WNetEnumResourceA@16 WNetCloseEnum equ __imp__WNetCloseEnum@4 endif

Еще потребуется файл def32.inc, в который поместим определения констант и структур из разных включаемых файлов для языка С. Существует утилита h2inc, преобразующая эти файлы целиком, но мы создадим собственный включаемый файл, в который будем добавлять новые определения по мере надобности.

; def32.inc ; файл с определениями констант и типов для примеров программ под win32



; из winbase.h STD_OUTPUT_HANDLE equ -11

; из winerror.h NO_ERROR equ 0 ERROR_NO_MORE_ITEMS equ 259

; из winnetwk.h RESOURCEUSAGE_CONNECTABLE equ 1 RESOURCETYPE_ANY equ 0 RESOURCE_CONNECTED equ 1 NTRESOURCE struc dwScope dd ? dwType dd ? dwDisplayType dd ? dwUsage dd ? lpLocalName dd ? lpRemoteName dd ? lpComment dd ? lpProvider dd ? NTRESOURCE ends

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


Меню


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

Первое, что мы должны будем сделать, — это само меню. Меню, так же как и иконки, диалоги и другая информация (вплоть до версии программы), записывают в файлы ресурсов. Файл ресурсов имеет расширение *.RC для текстового файла или *.RES для бинарного файла, скомпилированного специальным компилятором ресурсов (RC, BRCC32 или WRC). И те, и другие файлы ресурсов можно редактировать специальными программами, входящими в дистрибутивы C/C++ или других средств разработки для Windows, но мы не будем делать слишком сложное меню и напишем RC-файл вручную, например так:

// winmenu.rc // файл ресурсов для программы winmenu.asm // #define ZZZ_TEST 0 #define ZZZ_OPEN 1 #define ZZZ_SAVE 2 #define ZZZ_EXIT 3

ZZZ_Menu MENU { POPUP "&File" { MENUITEM "&Open",ZZZ_OPEN MENUITEM "&Save", ZZZ_SAVE MENUITEM SEPARATOR MENUITEM "E&xit",ZZZ_EXIT } MENUITEM "&Edit",ZZZ_TEST }

Чтобы добавить этот файл в программу, его надо скомпилировать и указать имя скомпилированного *.RES-файла для компоновщика:

MASM:

ml /c /coff /Cp winmenu.asm rc /r winmenu.rc link winmenu.obj winmenu.res /subsystem:windows

TASM:

tasm /m /ml /D_TASM_ winmenu.asm brcc32 winmenu.rc tlink32 /Tpe /aa /c /x winmenu.obj,,,,,winmenu.res

WASM:

wasm winmenu.rc wrc /r /bt=nt winmenu.rc wlink file winmenu.obj res winmenu.res form windows nt

А теперь сам текст программы. Чтобы показать, как мало надо внести изменений в программу window.asm, комментарии для всех строк, перенесенных оттуда без изменений заменены, на символ «*».

; winmenu.asm ; Графическое win32-приложение, демонстрирующее работу с меню ; звездочками отмечены строки, скопированные из файла window.asm ;


ZZZ_TEST equ 0 ; сообщения от нашего меню ZZZ_OPEN equ 1 ; должны совпадать с определениями из winmenu.rc ZZZ_SAVE equ 2 ; кроме того в нашем примере их номера важны ZZZ_EXIT equ 3 ; потому что они используются как индекс для ; таблицы переходов к обработчикам

include def32.inc ;* include kernel32.inc ;* include user32.inc ;* .386 ;* .model flat ;* .data ;* class_name db "window class 1",0 ;* window_name db "win32 assembly example",0 ;* menu_name db "ZZZ_Menu",0 ; имя меню в файле ресурсов test_msg db "You selected menu item TEST",0 ; строки для open_msg db "You selected menu item OPEN",0 ; демонстрации работы save_msg db "You selected menu item SAVE",0 ; меню wc WNDCLASSEX <4*12,CS_HREDRAW or CS_VREDRAW,offset win_proc,0,0,?,?,?,\ COLOR_WINDOW+1,0,offset class_name,0> ;* .data? ;* msg_ MSG <?,?,?,?,?,?> ;* .code ;* _start: ;* xor ebx,ebx ;* push ebx ;* call GetModuleHandle ;* mov esi,eax ;* mov dword ptr wc.hInstance,eax ;* push IDI_APPLICATION ;* push ebx ;* call LoadIcon ;* mov wc.hIcon,eax ;* push IDC_ARROW ;* push ebx ;* call LoadCursor ;* mov wc.hCursor,eax ;* push offset wc ;* call RegisterClassEx ;* push offset menu_name ; имя меню push esi ; наш идентификатор call LoadMenu ; загрузим меню из ресурсов mov ecx,CW_USEDEFAULT ;* push ebx ;* push esi ;* push eax ; идентификатор меню или окна-потомка push ebx ;* push ecx ;* push ecx ;* push ecx ;* push ecx ;* push WS_OVERLAPPEDWINDOW ;* push offset window_name ;* push offset class_name ;* push ebx ;* call CreateWindowEx ;* push eax ;* push SW_SHOWNORMAL ;* push eax ;* call ShowWindow ;* call UpdateWindow ;* mov edi,offset msg_ ;* message_loop: ;* push ebx ;* push ebx ;* push ebx ;* push edi ;* call GetMessage ;* test eax,eax ;* jz exit_msg_loop ;* push edi ;* call TranslateMessage ;* push edi ;* call DispatchMessage ;* jmp short message_loop ;* exit_msg_loop: ;* push ebx ;* call ExitProcess ;*

; процедура win_proc ; вызывается окном каждый раз, когда окно получает какое-нибудь сообщение ; именно здесь будут происходить вся работа программы ; ; процедура не должна изменять регистры EBP,EDI,ESI и EBX ! win_proc proc ;* push ebp ;* mov ebp,esp ;* wp_hWnd equ dword ptr [ebp+08h] ;* wp_uMsg equ dword ptr [ebp+0Ch] ;* wp_wParam equ dword ptr [ebp+10h] ;* wp_lParam equ dword ptr [ebp+14h] ;* cmp wp_uMsg,WM_DESTROY ;* jne not_wm_destroy ;* push 0 ;* call PostQuitMessage ;* jmp short end_wm_check ;* not_wm_destroy: ;* cmp wp_uMsg,WM_COMMAND ; если мы получили WM_COMMAND jne not_wm_command ; это от нашего меню mov eax,wp_wParam ; и в wParam лежит наше подсообщение jmp dword ptr menu_handlers[eax*4] ; косвенный переход ; (в 32-битном режиме можно делать переход по любому регистру)



menu_handlers dd offset menu_test,offset menu_open dd offset menu_save,offset menu_exit

; обработчики событий test, open и save выводят MessageBox ; обработчик exit выходит из программы

menu_test: mov eax,offset test_msg ; сообщение для MessageBox jmp short show_msg menu_open: mov eax,offset open_msg ; сообщене для MessageBox jmp short show_msg menu_save: mov eax,offset save_msg ; сообщение для MessageBox show_msg: push MB_OK ; стиль для MessageBox push offset menu_name ; заголовок push eax ; сообщение push wp_hWnd ; идентификатор окна-предка call MessageBox ; вызов функции jmp short end_wm_check ; выход из win_proc menu_exit: ; если выбрали пункт EXIT push wp_hWnd call DestroyWindow ; уничтожим наше окно end_wm_check: leave ;* xor eax,eax ; вернем 0 как результат работы процедуры ret 16 ;* not_wm_command: ; not_wm_command, чтобы избавиться от лишнего jmp leave ;* jmp DefWindowProc ;* win_proc endp ;* end _start ;*

Итого: из 120 строк программы новыми оказались всего 36, в то время как программа, с точки зрения пользователя, стала намного сложнее. Так и выглядит все программирование под Windows на ассемблере — берется одна написанная раз и навсегда шаблонная программа, модифицируются ресурсы и пишутся обработчики для различных событий меню и диалогов. Фактически все программирование оказывается сосредоточенным именно в этих процедурах-обработчиках.

Добавления к включаемым файлам в этом примере тоже оказываются незначительными по сравнению с window.asm.

В user32.inc между ifdef _TASM_ и else:

extrn LoadMenuA:near extrn DestroyWindow:near LoadMenu equ LoadMenuA

и между else и endif:

extrn __imp__LoadMenuA@8:dword extrn __imp__DestroyWindow@4:dword LoadMenu equ __imp__LoadMenuA@8 DestroyWindow equ __imp__DestroyWindow@4

и в def32.inc:

; из winuser.h WM_COMMAND equ 111h MB_OK equ 0


Окна


Теперь, когда мы знаем, как просто выводится окно с предопределенным классом, возьмемся за вывод собственного окна — процедуры, на которой будут базироваться все последующие примеры, познакомимся с понятием сообщения. В DOS основным средством передачи управления программам в различных ситуациях служат прерьшания. В Windows прерывания используются системой для своих нужд, а для приложений существует аналогичный механизм — механизм событий. Так, нажатие клавиши на клавиатуре, если эта клавиша не используется Windows, генерирует сообщение WM_KEYDOWN или WM_KEYUP, которое можно перехватить, добавив в цепь обработчиков события собственное при помощи SetWindowHookEx(). События затем преобразуются в сообщения, которые рассылаются функциям — обработчикам сообщений и которые можно прочитать из основной программы при помощи вызовов GetMessage() и PeekMessage().

Нам пока потребуется только обработка сообщения закрытия окна (WM_DESTROY и WM_QUIT), по которому программа будет завершаться.

; window.asm ; Графическое win32-приложение, демонстрирующее базовый вывод окна ; include def32.inc include kernel32.inc include user32.inc .386 .model flat .data class_name db "window class 1",0 window_name db "win32 assembly example",0 ; структура, описывающая класс окна. wc WNDCLASSEX <4*12,CS_HREDRAW or CS_VREDRAW,offset win_proc,0,0,?,?,?, COLOR_WINDOW+1,0,offset class_name,0> ; здесь находятся следующие поля ; wc.cbSize = 4*12 - размер этой структуры ; wc.style - стиль окна (перерисовывать при изменении размера) ; wc.lpfnWndProc - обработчик событий окна (win_proc) ; wc.cbClsExtra - число дополнительных байтов после структуры (0) ; wc.cbWndExtra - число дополнительных байтов после окна (0) ; wc.hInstance - идентификатор нашего процесса (?) ; wc.hIcon - идентификатор иконки (?) ; wc.hCursor - идентификатор курсора (?) ; wc.hbrBackground - идентификатор кисти или цвет фона+1 ; (COLOR_WINDOW+1) ; wc.lpszMenuName - ресурс с основным меню (в этом примере - 0) ; wc.lpszClassName - имя класса (строка class_name) ; wc.hIconSm - идентификатор маленькой иконки (только в windows 95, ; для NT должен быть 0) .data? msg_ MSG <?,?,?,?,?,?> ; а это - структура, в которой возвращается ; сообщение после GetMessage .code _start: xor ebx,ebx ; в EBX будет 0 для команд push 0 ; (короче в 2 раза) ; определим идентификатор нашей программы push ebx call GetModuleHandle mov esi,eax ; и сохраним его в ESI ; заполним и зарегестрируем класс mov dword ptr wc.hInstance,eax ; идентификатор предка ; выберем иконку push IDI_APPLICATION ; стандартная иконка приложения push ebx ; идентификатор модуля с иконкой call LoadIcon mov wc.hIcon,eax ; идентификатор иконки для нашего класса ; выберем форму курсора push IDC_ARROW ; стандартная стрелка push ebx ; идентификатор модуля с курсором call LoadCursor mov wc.hCursor,eax ; идентификатор курсора для нашего класса push offset wc call RegisterClassEx ; зарегистрируем класс ; создадим окно mov ecx,CW_USEDEFAULT ; push ecx короче push N в пять раз push ebx ; адрес структуры CREATESTRUCT (здесь NULL) push esi ; идентификатор процесса, который будет получать ; сообщения от окна (то есть, наш) push ebx ; идентификатор меню или окна-потомка push ebx ; идентификатор окна-предка push ecx ; высота (CW_USEDEFAULT - по умолчанию) push ecx ; ширина (по умолчанию) push ecx ; y-координата (по умолчанию) push ecx ; x-координата (по умолчанию) push WS_OVERLAPPEDWINDOW ; стиль окна push offset window_name ; заголовок окна push offset class_name ; любой зарегистрированный класс push ebx ; дополнительный стиль call CreateWindowEx ; создать окно (eax - идентификатор окна) push eax ; идентификатор для UpdateWindow push SW_SHOWNORMAL ; тип показа для для ShowWindow push eax ; идентификатор для ShowWindow ; больше идентификатор окна нам не потребуется call ShowWindow ; показать окно call UpdateWindow ; и послать ему сообщение WM_PAINT


; основной цикл - проверка сообщений от окна и выход по WM_QUIT mov edi,offset msg_ ; push edi короче push N в 5 раз message_loop: push ebx ; последнее сообщение push ebx ; первое сообщение push ebx ; идентификатор окна (0 - любое наше окно) push edi ; адрес структуры MSG call GetMessage ; получить сообщение от окна с ожиданием ; - не забывайте использовать PeekMessage ; если нужно в этом цикле что-то выполнять test eax,eax ; если получено WM_QUIT jz exit_msg_loop ; выйти push edi ; иначе - преобразовать сообщения типа call TranslateMessage ; WM_KEYUP в сообщения типа WM_CHAR push edi call DispatchMessage ; и послать их процедуре окна (иначе его просто ; нельзя будет закрыть) jmp short message_loop ; продолжить цикл exit_msg_loop: ; выход из программы push ebx call ExitProcess

; процедура win_proc ; вызывается окном каждый раз, когда окно получает какое-нибудь сообщение ; именно здесь будут происходить вся работа программы ; ; процедура не должна изменять регистры EBP,EDI,ESI и EBX ! ; win_proc proc ; так как мы получаем параметры в стеке, построим стековый кадр push ebp mov ebp,esp ; процедура типа WindowProc вызывается со следующими параметрами wp_hWnd equ dword ptr [ebp+08h] ; идентификатор окна wp_uMsg equ dword ptr [ebp+0Ch] ; номер сообщения wp_wParam equ dword ptr [ebp+10h] ; первый параметр wp_lParam equ dword ptr [ebp+14h] ; второй параметр ; если мы получили сообщение WM_DESTROY (оно означает что окно уже удалили ; с экрана, нажав alt-F4 или кнопку в верхнем правом углу) ; то пошлем основной программе сообщение WM_QUIT cmp wp_uMsg,WM_DESTROY jne not_wm_destroy push 0 ; код выхода call PostQuitMessage ; послать WM_QUIT jmp short end_wm_check ; и выйти из процедуры not_wm_destroy: ; если мы получили другое сообщение - вызовем его обработчик по умолчанию leave ; восстановим ebp jmp DefWindowProc ; и вызовем DefWindowProc с нашими параметрами ; и адресом возврата в стеке end_wm_check: leave ; восстановим ebp ret 16 ; и вернемся сами, очистив стек от параметров win_proc endp end _start



Необходимые добавления в файл def32.inc:

; из winuser. h IDI_APPLICATION equ 32512 WM_DESTROY equ 2 CS_HREDRAW equ 2 CS_VREDRAW equ 1 CW_USEDEFAULT equ 80000000h WS_OVERLAPPEDWINDOW equ 0CF0000h IDC_ARROW equ 32512 SW_SHOWNORMAL equ 1 COLOR_WINDOW equ 5 WNDCLASSEX struc cbSize dd ? style dd ? lpfnWndProc dd ? cbClsExtra dd ? cbWndExtra dd ? hInstance dd ? hIcon dd ? hCursor dd ? hbrBackground dd ? lpszMenuName dd ? lpszClassName dd ? hIconSm dd ? WNDCLASSEX ends MSG struc hwnd dd ? message dd ? wParam dd ? lParam dd ? time dd ? pt dd ? MSG ends

Добавления в файл user32.inc:

между ifdef _TASM_ и else:

extrn DispatchMessageA:near extrn TranslateMessage:near extrn GetMessageA:near extrn LoadIconA:near extrn UpdateWindow:near extrn ShowWindow:near extrn CreateWindowExA:near extrn DefWindowProcA:near extrn PostQuitMessage:near extrn RegisterClassExA:near extrn LoadCursorA:near ; присваивания для облегчения читаемости кода DispatchMessage equ DispatchMessageA GetMessage equ GetMessageA LoadIcon equ LoadIconA CreateWindowEx equ CreateWindowExA DefWindowProc equ DefWindowProcA RegisterClassEx equ RegisterClassExA LoadCursor equ LoadCursorA

и между else и endif:

extrn __imp__DispatchMessageA@4:dword extrn __imp__TranslateMessage@4:dword extrn __imp__GetMessageA@16:dword extrn __imp__LoadIconA@8:dword extrn __imp__UpdateWindow@4:dword extrn __imp__ShowWindow@8:dword extrn __imp__CreateWindowExA@48:dword extrn __imp__DefWindowProcA@16:dword extrn __imp__PostQuitMessage@4:dword extrn __imp__RegisterClassExA@4:dword extrn __imp__LoadCursorA@8:dword ; присваивания для облегчения читаемости кода DispatchMessage equ __imp__DispatchMessageA@4 TranslateMessage equ __imp__TranslateMessage@4 GetMessage equ __imp__GetMessageA@16 LoadIcon equ __imp__LoadIconA@8 UpdateWindow equ __imp__UpdateWindow@4 ShowWindow equ __imp__ShowWindow@8 CreateWindowEx equ __imp__CreateWindowExA@48 DefWindowProc equ __imp__DefWindowProcA@16 PostQuitMessage equ __imp__PostQuitMessage@4 RegisterClassEx equ __imp__RegisterClassExA@4 LoadCursor equ __imp__LoadCursorA@8

а в файл kernel32.inc между ifdef _TASM_ и else:

extrn GetModuleHandleA:near GetModuleHandle equ GetModuleHandleA

и между else и endif:

extrn __imp__GetModuleHandleA@4:dword GetModuleHandle equ __imp__GetModuleHandleA@4

В начале главы говорилось, что программировать под Windows просто, а в то же время текст обычной программы вывода пустого окна на экран уже занимает больше места, чем, например, программа проигрывания wav-файла из главы 5.10.8. Где же обещанная простота? Так вот, оказывается, что, написав window.asm, мы уже создали большую часть всех последующих программ, а когда мы дополним этот текст полноценным диалогом, обнаружится, что больше не нужно писать все эти громоздкие конструкции, достаточно просто копировать отдельные участки текста.


Окно типа MessageBox


Для того чтобы вывести на экран любое окно, программа обычно должна сначала описать его внешний вид и все свойства, то есть то, что называется классом окна. О том, как это сделать, — немного позже, а для начала выведем одно из окон с предопределенным классом — окно типа MessageBox. MessageBox — это маленькое окно с указанным текстовым сообщением и одной или несколькими кнопками. В нашем примере сообщением будет традиционное «Hello world!», и кнопка будет всего одна — ОК.

; winhello.asm ; Графическое win32-приложениe ; Выводит окно типа mesagebox с текстом "Hello world!" ; include def32.inc include kernel32.inc include user32.inc

.386 .model flat .const ; заголовок окна hello_title db "First win32 GUI program",0 ; сообщение hello_message db "Hello world!",0 .code _start: push MB_ICONINFORMATION ; стиль окна push offset hello_title ; адрес строки с заголовком push offset hello_message ; адрес строки с сообщением push 0 ; идентификатор предка call MessageBox

push 0 ; код выхода call ExitProcess ; завершение программы end _start

Естественно, нам потребуется добавить к файлу def32.inc строку:

; из winuser.h MB_ICONINFORMATION equ 40h

и создать новый файл, user32.inc, в который будут входить определения функций из user32.dll — библиотеки, куда входят все основные функции, отвечающие за оконный интерфейс:

; user32.inc ; включаемый файл с определениями функций из user32.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn MessageBoxA:near ; присваивания для облегчения читаемости кода MessageBox equ MessageBoxA else includelib user32.lib ; истинные имена используемых функций extrn __imp__MessageBoxA@16:dword ; присваивания для облегчения читаемости кода MessageBox equ __imp__MessageBoxA@16

Теперь можно скомпилировать эту программу аналогично тому, как мы компилировали winurl.asm, и запустить — на экране появится маленькое окно с нашим сообщением и кнопкой ОК, которое пропадет после того, как будет нажата эта кнопка. Если скомпилировать winhello.asm как консольное приложение, ничего не изменится, текстовое окно с именем программы будет открыто до тех пор, пока окно с нашим сообщением не будет закрыто.



Первая программа


В качестве нашего первого примера посмотрим, насколько проще написать под Windows программу, которая загружает другую программу. В DOS (см. главу 4.10) нам приходилось изменять распределение памяти, заполнять специальный блок данных EPBВ и только затем вызывать DOS. Здесь же не только вся процедура сокращается до одного вызова функции, а еще оказывается, что можно точно так же загружать не только программы, но и документы, графические и текстовые файлы и даже почтовые и WWW-адреса — все, для чего в реестре Windows записано действие, выполняющееся при попытке открытия.

; winurl.asm ; Пример програмы для win32. ; Запускает установленный по умолчанию броузер на адрес, указанный в строке URL ; аналогично можно запускать любую программу, документ, и любой другой файл, ; для которого определена операция open ; include shell32.inc include kernel32.inc

.386 .model flat .const URL db 'http://www.lionking.org/~cubbi/',0 .code _start: ; метка точки входа должна начинаться с подчеркивания xor ebx,ebx push ebx ; для исполнимых файлов - способ показа push ebx ; рабочий каталог push ebx ; командная строка push offset URL ; имя файла с путем push ebx ; операция open или print (если NULL - open) push ebx ; идентификатор окна, которое получит сообщения call ShellExecute ; ShellExecute(NULL,NULL,url,NULL,NULL,NULL) push ebx ; код выхода call ExitProcess ; ExitProcess(0) end _start

Итак, в этой программе выполняется вызов двух системных функций Win32 — ShellExecute() (открыть файл) и ExitProcess() (завершить процесс). Чтобы вызвать системную функцию Windows, программа должна поместить в стек все параметры от последнего к первому и передать управление дальней командой CALL. Все эти функции сами освобождают стек (завершаясь командой RET N) и возвращают результат работы в регистре ЕАХ. Такая договоренность о передаче параметров называется STDCALL. С одной стороны, это позволяет вызывать функции с нефиксированным числом параметров, а с другой — вызывающая сторона не должна заботиться об освобождении стека. Кроме того, функции Windows сохраняют значение регистров ЕВР, ESI, EDI и EBX, этим мы пользовались в нащем примере — хранили 0 в регистре EBX и применили 1-байтную команду PUSH EBX вместо 2-байтной PUSH 0.


Прежде чем мы сможем скомпилировать winurl.asm, нужно создать файлы kernel32.inc и shell32.inc, в которые поместим директивы, описывающие вызываемые системные функции:

; kernel32.inc ; включаемый файл с определениями функций из kernel32.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ExitProcess:near else includelib kernel32.lib ; истинные имена используемых функций extrn __imp__ExitProcess@4:dword ; присваивания для облегчения читаемости кода ExitProcess equ __imp__ExitProcess@4 endif

; shell32.inc ; включаемый файл с определениями функций из shell32.dll ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ShellExecuteA:near ; присваивания для облегчения читаемости кода ShellExecute equ ShellExecuteA else includelib shell32.lib ; истинные имена используемых функции extrn __imp__ShellExecuteA@24:dword ; присваивания для облегчения читаемости кода ShellExecute equ __imp__ShellExecuteA@24 endif

Имена всех системных функций Win32 модифицируются так, что перед именем функции ставится подчеркивание, а после — знак «@» и число байт, которое занимают параметры, передаваемые ей в стеке, так ExitProcess() превращается в _ExitProcess@4(). Компиляторы с языков высокого уровня часто останавливаются на этом и вызывают функции по имени _ExitProcess@4(), но реально вызывается небольшая процедура-заглушка, которая ничего не делает, а только передает управление на такую же метку, но с добавленным «__imp_» — __imp__ExitProcess@4(). Во всех наших примерах мы будем обращаться напрямую к __imp__ExitProcess@4(). К сожалению, TASM (а точнее TLINK32) использует собственный способ вызова системных функций, который нельзя так обойти, и программы, скомпилированные с его помощью, оказываются намного больше и в некоторых случаях работают медленнее. Мы отделили описания функций для TASM во включаемых файлах при помощи директив условного ассемблирования, которые будут использовать их, если в командной строке ассемблера указать /D_TASM_.



Кроме этого, все функции, работающие со строками (как, например, ShellExecute()), существуют в двух вариантах. Если строка рассматривается в обычном смысле, как набор символов ASCII, к имени функции добавляется «A» (ShellExecuteA()). Другой вариант функции, использующий строки в формате UNICODE (два байта на символ), заканчивается буквой «U». Во всех наших примерах будем использовать обычные ASCII-функции, но, если вам потребуется перекомпилировать программы на UNICODE, достаточно только поменять «А» на «U» во включаемых файлах.

Итак, теперь, когда у нас есть все необходимые файлы, можно скомпилировать первую программу для Windows.

Компиляция MASM:

ml /с /coff /Cp winurl.asm link winurl.obj /subsystem:windows

(здесь и далее используется 32-битная версия link.exe)

Компиляция TASM:

tasm /m /ml /D_TASM_ winurl.asm tlink32 /Tpe /aa /c /x winurl.obj

Компиляция WASM:

wasm winurl.asm wlink file winurl.obj form windows nt op с

Также для компиляции потребуются файлы kernel32.lib и shell32.lib в первом и третьем случае и import32.lib — во втором. Ёти файлы входят в дистрибутивы любых средств разработки для Win32 от соответствующих компаний — Microsoft, Watcom (Sybase) и Borland (Inprise), хотя их всегда можно воссоздать из файлов kernel32.dll и shell32.dll, находящихся в каталоге WINDOWS/SYSTEM.

Иногда вместе с дистрибутивами различных средств разработки для Windows идет файл windows.inc, в котором дано макроопределение Invoke или заменена макросом команда call так, что они принимают список аргументов, первым из которых идет имя вызываемой функции, а затем через запятую — все параметры. С использованием этих макроопределений наша программа выглядела бы так:

_start: xor ebx,ebx Invoke SnellExecute, ebx, ebx, offset URL, ebx, \ ebx, ebx Invoke ExitProcess, ebx end _start

И этот текст компилируется в точно такой же код, что и у нас, но выполняется вызов не функции __imp__ExitProcess@4(), а промежуточной функции _ExitProcess@4(). Использование этой формы записи не позволяет применять отдельные эффективные приемы оптимизации, которые мы будем приводить в наших примерах, — помещение параметров в стек заранее и вызов функции командой JMP. И наконец, файла windows.inc у вас может просто не оказаться, так что будем писать push перед каждым параметром вручную.


Полноценное приложение


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

// winpad95.rc // Файл ресурсов для программы winpad95.asm // идентификаторы сообщений от пунктов меню #define IDM_NEW 0x1001 #define IDM_OPEN 0x101L #define IDM_SAVE 0x102L #define IDM_SAVEAS 0x103L #define IDM_EXIT 0x104L #define IDM_ABOUT 0x105L #define IDM_UNDO 0x106L #define IDM_CUT 0x107L #define IDM_COPY 0x108L #define IDM_PASTE 0x109L #define IDM_CLEAR 0x10AL #define IDM_SETSEL 0x10BL

// идентификаторы основных ресурсов #define IDJ1ENU 0x700L #define ID_ACCEL 0x701L #define ID_ABOUT 0x702L

// если есть иконка - можно раскомментировать эти две строки // #define ID_ICON 0x703L // ID_ICON ICON "winpad95.ico"

// основное меню ID_MENU MENU DISCARDABLE { POPUP "&File" { MENUITEM "&New\tCtrl+N", IDM_NEW MENUITEM "&Open...\tCtrl+O", IDM_OPEN MENUITEM "&Save\tCtrl+S", IDM_SAVE MENUITEM "Save &As...\tCtrl+Shift+S", IDM_SAVEAS MENUITEM SEPARATOR MENUITEM "E&xit\tCtrl+Q", IDM_EXIT } POPUP "&Edit" { MENUITEM "&Undo\tCtrl-Z", IDM_UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tCtrl-X", IDM_CUT MENUITEM "&Copy\tCtrl-C", IDM_COPY MENUITEM "&Paste\tCtrl-V", IDM_PASTE MENUITEM "&Delete\tDel", IDM_CLEAR MENUITEM SEPARATOR MENUITEM "Select &All\tCtrl-A", IDM_SETSEL } POPUP "&Help" { MENUITEM "About", IDM_ABOUT } }

// комбинации клавиш ID_ACCEL ACCELERATORS DISCARDABLE { "N", IDM_NEW, CONTROL, VIRTKEY "O", IDM_OPEN, CONTROL, VIRTKEY "S", IDM_SAVE, CONTROL, VIRTKEY "S", IDM_SAVEAS, CONTROL, SHIFT, VIRTKEY "Q", IDM_EXIT, CONTROL, VIRTKEY "Z", IDM_UNDO, CONTROL, VIRTKEY "A", IDM_SETSEL, CONTROL, VIRTKEY }


// все эти определения можно заменить на #include <winuser.h> #define DS_MODALFRAME 0x80L #define DS_3DLOOK 4 #define WS_POPUP 0x80000000L #define WS_CAPTION 0xC00000L #define WS_SYSMENU 0x80000L #define IDOK 1 #define IDC_STATIC -1 #define IDI_APPLICATION 32512 #define WS_BORDER 0x800000L

// стандартный диалог "About" ID_ABOUT DIALOG DISCARDABLE 0, 0, 125, 75 STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "About Asmpad95" { ICON IDI_APPLICATION,IDC_STATIC,12,15,21,20 CTEXT "Asmpad95",IDC_STATIC,0,30,40,8 CTEXT "Prototype notepad-style editor for Windows 95 written entirely in assembly language",IDC_STATIC,45,10,70,45,WS_BORDER DEFPUSHBUTTON "OK",IDOK,35,60,40,10 }

Теперь рассмотрим текст программы:

; winpad95.asm ; графическое win32-приложение - текстовый редактор ; include def32.inc include user32.inc include kernel32.inc include comdlg32.inc

ID_MENU equ 700h ID_ACCEL equ 701h ID_ABOUT equ 702h

MAXSIZE equ 260 ; максимальное имя файла MEMSIZE equ 65535 ; максимальный размер временного буфера в памяти

EditID equ 1

.386 .model flat .const c_w_name db "Asmpad95",0 ; это и имя класса, и имя основного окна edit_class db "edit",0 ; предопределенное имя класса для редактора changes_msg db "Save changes?",0 filter_string db "All Files",0,'*.*',0 ; маски для Get * FileName db "Text Files",0, '*.txt',0,0

.data ; структура, использующаяся Get * FileName ofn OPENFILENAME <SIZE ofn,?,?,offset filter_string,?,?,?,offset buffer,\ MAXSIZE,0,?,?,?,?,?,?,0,?,?,?> ; структура, описывающая наш основной класс wc WNDCLASSEX <SIZE WNDCLASSEX,CS_HREDRAW or CS_VREDRAW,offset win_proc,0,\ 0,?,?,?,COLOR_WINDOW+1,ID_MENU,offset c_w_name,0> flag_untitled db 1 ; = 1, если имя файла не определено (новый файл)

.data? h_editwindow dd ? ; идентификатор окна редактора h_accel dd ? ; идентификатор массива акселераторов p_memory dd ? ; адрес буфера в памяти SizeReadWrite dd ? msg_ MSG <> rec RECT <> buffer db MAXSIZE dup(?) ; имя файла window_title db MAXSIZE dup(?), 12 dup(?)



.code _start: call GetCommandLine ; получить нашу командную строку mov edi,eax mov al,' ' mov ecx,MAXSIZE repne scasb ; найти конец имени нашей программы cmp byte ptr [edi],0 je cmdline_empty mov esi,edi mov edi,offset buffer rep movsb mov flag_untitled,0 cmdline_empty: ; подготовить и зарегистрировать класс xor ebx,ebx call GetModuleHandle ; определить наш идентификатор mov esi,eax mov wc.hInstance,eax ; и сохранить его в wc.hInstance mov ofn._hInstance,eax push IDI_APPLICATION ; или IDI_ICON, если иконка есть в ресурсах, push ebx ; или esi, если иконка есть в ресурсах call LoadIcon mov wc.hIcon,eax push IDC_ARROW ; предопределенный курсор (стрелка) push ebx call LoadCursor mov wc.hCursor,eax push offset wc call RegisterClassEx ; создать основное окно push ebx push esi push ebx push ebx push 200 push 300 push CW_USEDEFAULT push CW_USEDEFAULT push WS_OVERLAPPEDWINDOW push offset c_w_name push offset c_w_name push WS_EX_CLIENTEDGE call СreateWindowEx push eax ; для pop esi перед message_loop push eax push SW_SHOWNORMAL push eax call ShowWindow call UpdateWindow ; инициализировать акселераторы push ID_ACCEL push esi call LoadAcceleratоrs mov h_accel,eax ; цикл ожидания сообщения pop esi ; ESI - идентификатор основного окна mov edi,offset msg_ ; EDI - структура с сообщением от него message_loop: push ebx push ebx push ebx push edi call GetMessage ; получить сообщение, test eax,eax ; если это WM_OUIT, - jz exit_msg_loop ; выйти из цикла push edi push h_accel push esi ; hWnd call TranslateAccelerator ; преобразовать акселераторы в IDM* test eax,eax jnz message_loop push edi call TranslateMessage ; преобразовать сообщения от клавиш push edi call DispatchMessage ; и отослать обратно jmp short message_loop exit_msg_loop: push msg_.wParam call ExitProcess ; конец программы

; процедура win_proc ; ; процедура не должна изменять регистры EBP,EDI,ESI и ЕВХ! win_proc proc near ; параметры (с учетом push ebp) wp_hWnd equ dword ptr [ebp+08h] wp_uMsg equ dword ptr [ebp+0Ch] wp_wParam equ dword ptr [ebp+10h] wp_lParam equ dword ptr [ebp+14h] ; инициализируем стековый кадр push ebp mov ebp,esp ; создать стековый кадр pusha ; сохранить все регистры xor ebx,ebx ; 0 для команд push 0 mov esi,wp_hWnd ; для команд push hWnd mov eax,wp_uMsg ; обработать пришедшее сообщение cmp eax,WM_CREATE je h_wm_create cmp eax,WM_SIZE je h_wm_size cmp eax,WM_DESTROY je h_wm_destroy cmp eax,WM_COMMAND je h_wm_command cmp eax,WM_ACTIVATE je h_wm_activate cmp eax,WM_CLOSE je h_wm_close def_proc: popa leave ; если это ненужное сообщение, jmp DefWindowProc ; оставить его обработчику по умолчанию



; обработчик WM_CLOSE, ; если нужно, спрашивает, сохранить ли файл h_wm_close: call save_contents jmp short def_proc

; обработчик WM_CREATE ; h_wm_create: ; здесь также можно создать toolbar и statusbar ; создать окно редактора push ebx push wc.hInstance ; идентификатор основной программы push EditID push esi ; hWnd push ebx ; 0 push ebx ; 0 push ebx ; 0 push ebx ; 0 push WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or \ ES_AUTOHSCROLL or ES_AUTOVSCROLL push ebx ; 0 push offset edit_class push ebx ; 0 call CreateWindowEx mov h_editwindow,eax ; передать ему фокус push eax call SetFocus cmp flag_untitled,1 je continue_create call skip_getopen ; открыть файл, указанный в командной строке continue_create: call set_title jmp end_wm_check

; обработчик WM_COMMAND ; h_wm_command: mov eax,wp_wParam cwde ; младшее слово содержит IDM_* sub eax,100h jb def_proc ; обработать сообщения от пунктов меню call dword ptr menu_handlers[eax*4] jmp end_wm_check

menu_handlers dd offset h_idm_new,offset h_idm_open,offset h_idm_save dd offset h_idm_saveas,offset h_idm_exit,offset h_idm_about dd offset h_idm_undo, offset h_idm_cut, offset h_idm_copy dd offset h_idm_paste, offset h_idm_clear, offset h_idm_setsel ; сообщения от пунктов меню должны быть описаны в win95pad.rc именно в таком ; порядке - от IDM_NEW 100h до IDM_CLEAR 10Ah

h_idm_setsel: push -1 ; -1 push ebx ; 0 push EM_SETSEL ; выделить весь текст push h_editwindow call SendMessage ret

; обработчики сообщений из меню EDIT: h_idm_clear: mov eax,WM_CLEAR jmp short send_to_editor h_idm_paste: mov eax,WM_PASTE jmp short send_to_editor h_idm_copy: mov eax,WM_COPY jmp short send_to_editor h_idm_cut: mov eax,WM_CUT jmp short send_to_editor h_idm_undo: mov eax,EM_UNDO send_to_editor: push ebx ; 0 push ebx ; 0 push eax push h_editwindow call SendMessage ret

; обработчик IDM_NEW h_idm_new: call save_contents ; записать файл, если нужно mov byte ptr flag_untitled,1 call set_title ; отметить, что файл не назван push ebx push ebx push WM_SETTEXT push h_editwindow call SendMessage ; послать пустой WM_SETTEXT редактору ret



; обработчик IDM_ABOUT h_idm_about: push ebx ; 0 push offset about_proc push esi ; hWnd push ID_ABOUT push wc.hInstance call DialogBoxParam ret

; обработчик IDM_SAVEAS и IDM_SAVE h_idm_save: cmp flag_untitled, 1 ; если файл назван, jne skip_getsave ; пропустить вызов GetSaveFileName h_idm_saveas: ; спросить имя файла mov ofn.Flags,OFN_EXPLORER or OFN_OVERWRITEPROMPT push offset ofn call GetSaveFileName test eax,eax jz file_save_failed skip_getsave: ; создать его push ebx push FILE_ATTRIBUTE_ARCHIVE push CREATE_ALWAYS push ebx push FILE_SHARE_READ or FILE_SHARE_WRITE push GENERIC_READ or GENERIC_WRITE push offset buffer call CreateFile mov edi,eax ; выделить память push MEMSIZE push GMEM_MOVEABLE or GMEM_ZEROINIT call GlobalAlloc push eax ; hMemory для GlobalFree push eax ; hMemory для GlobalLock call GlobalLock mov esi,eax ; адрес буфера в ESI ; забрать текст из редактора push esi push MEMSIZE-1 push WM_GETTEXT push h_editwindow call SendMessage ; записать в файл push esi ; pMemory call lstrlen push ebx push offset SizeReadWrite push eax ; размер буфера push esi ; адрес буфера push edi ; идентификатор файла call WriteFile push esi ; pMemory call GlobalUnlock call GlobalFree ; hMemory уже в стеке push edi ; идентификатор файла call CloseHandle ; сбросить флаг модификации в редакторе push ebx push ebx push EM_SETMODIFY push h_editwindow call SendMessage mov byte ptr flag_untitled,0 call set_title file_save_failed: push h_editwindow call SetFocus ret

; обработчик IDM_OPEN h_idm_open: call save_contents ; вызвать стандартный диалог выбора имени файла mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or \ OFN_EXPLORER push offset ofn call GetOpenFileName test eax,eax jz file_open_failed skip_getopen: ; открыть выбранный файл push ebx push FILE_ATTRIBUTE_ARCHIVE push OPEN_EXISTING push ebx push FILE_SHARE_READ or FILE_SHARE_WRITE push GENERIC_READ or GENERIC_WRITE push offset buffer call CreateFile mov edi,eax ; идентификатор для ReadFile ; выделить память push MEMSIZE push GMEM_MOVEABLE or GMEM_ZEROINIT call GlobalAlloc push eax ; hMemory для GlobalFree push eax ; hMemory для GlobalLock call GlobalLock ; получить адрес выделенной памяти push eax ; pMemory для GlobalUnlock push eax ; pMemory для SendMessage ; прочитать файл push ebx push offset SizeReadWrite push MEMSIZE-1 push eax ; pMemory для ReadFile push edi call ReadFile ; послать окну редактора сообщение wm_settext, чтобы он забрал текст из буфера push ebx ; pMemory уже в стеке push WM_SETTEXT push h_editwindow call SendMessage ; а теперь можно закрыть файл и освободить память call GlobalUnlock ; pMemory уже в стеке call GlobalFree ; hMemory уже в стеке push edi ; hFile call CloseHandle mov byte ptr flag_untitled,0 call set_title file_open_failed: push h_editwindow call SetFocus ret



; обработчик IDM_EXIT h_idm_exit: call save_contents push esi ; hWnd call DestroyWindow ; уничтожить наше окно ret

; обработчик WM_SIZE h_wm_size: ; здесь также надо послать WM_SIZE окнам toolbar и statusbar, ; изменить размер окна редактора так, чтобы оно по-прежнему было на все окно push offset rec push esi ; hWnd call GetClientRect push 1 ; true push rec. bottom ; height push rec. right ; width push ebx ; у push ebx ; x push h_editwindow call MoveWindow jmp short end_wm_check

; обработчик WM_ACTIVATE h_wm_activate: push h_editwindow call SetFocus jmp short end_wm_check

; обработчик WM_DESTROY h_wm_destroy: push ebx call PostQuitMessage ; послать WM_QUIT основной программе

; конец процедуры window_proc end_wm_check: рора xor еах,еах ; вернуть 0 leave ret 16

; процедура set_title ; устанавливает новый заголовок для основного окна set_title: push esi push edi mov edi, offset window_title cmp byte ptr flag_untitled,1 ; если у файла нет имени, je untitled ; использовать Untitled mov esi,ofn.lpstrFile ; [ESI] - имя файла с путем movzx eax,ofn.nFileOffset ; eax - начало имени файла add esi,eax copy_filename: lodsb ; скопировать файл побайтно в название окна, test al,al jz add_progname ; пока не встретится ноль stosb jmp short copy_filename add_progname: mov dword ptr [edi],'-' ; приписать минус add edi,3 mov esi,offset c_w_name mov ecx,9 ; и название программы rep movsb pop edi pop esi push offset window_title push esi ; идентификатор окна call SetWindowText ret untitled: mov dword ptr [edil'itnU' ; дописать "Unti" mov dword ptr [edi+4],'delt' ; дописать "tled" add edi,8 jmp short add_progname

; процедура save_contents ; EBX = 0, ESI = hWnd save_contents: ; спросить редактор, изменялся ли текст push ebx push ebx push EM_GETMODIFY push h_editwindow call SendMessage test eax,eax jz not_modified ; спросить пользователя, сохранять ли его push MB_YESNO + MB_ICONWARNING push offset c_w_name push offset changes_msg push esi call MessageBox cmp eax,IDYES jne not_modified ; сохранить его call h_idm_save not_modified: ret



win_proc endp

about_proc proc near ; параметры ( с учетом push ebp) ap_hDlg equ dword ptr [ebp+08h] ap_uMsg equ dword ptr [ebp+0Ch] ap_wParam equ dword ptr [ebp+10h] ap_lParam equ dword ptr [ebp+14h] push ebp mov ebp,esp ; создать стековый кадр cmp ap_uMsg,WM_COMMAND jne dont_proceed cmp ap_wParam,IDOK jne dont_proceed push 1 push ap_hDlg call EndDialog dont_proceed: xor eax,eax ; не обрабатывается leave ret 16 about_proc endp

end _start

Размер этой программы — 6,5 Кб (скомпилированной ml/link), и даже версия, в которую добавлено все, что есть в Notepad (вывод файла на печать и поиск по тексту), все равно более чем в три раза меньше notepad.exe. Чем большее Windows-приложение создается, тем сильнее сказывается выигрыш в размерах при использовании ассемблера, даже несмотря на то, что мы только вызываем системные функции, практически не занимаясь программированием.

Прежде чем можно будет скомпилировать winpad95.asm, конечно, надо внести необходимые дополнения в наши включаемые файлы.

Добавления в файл def32.inc:

; из winuser.h WM_CREATE equ 1 WM_ACTIVATE equ 6 WM_SETTEXT equ 0Ch WM_GETTEXT equ 0Dh WM_CUT equ 300h WM_COPY equ 301h WM_PASTE equ 302h WM_CLEAR equ 303h WM_UNDO equ 304h WM_SIZE equ 5 WS_VISIBLE equ 10000000h WS_CHILD equ 40000000h WS_EX_CLIENTEDGE equ 200h ES_LEFT equ 0 ES_MUITILINE equ 4 ES_AUTOHSCROLL equ 80h ES_AUTOVSCROLL equ 40h EM_GETHANDLE equ 0BDh EM_GETMODIFY equ 0B8h EM_SETMODIFY equ 0B9h EM_UNDO equ 0C7h EM_SETSEL equ 0B1h MB_YESNO equ 4 MB_ICONWARNING equ 30h IDOK equ 1 IDYES equ 6

; из winnt.h GENERIC_READ equ 80000000h GENERIC_WRITE equ 40000000h FILE_SHARE_READ equ 1 FILE_SHARE_WRITE equ 2 FILE_ATTRIBUTE_ARCHIVE equ 20h

; из commdlg.h OFN_PATHMUSTEXIST equ 800h OFN_FILEMUSTEXIST equ 1000h OFN_EXPLORER equ 80000h OFN_OVERWRITEPROMPT equ 2 OPENFILENAME struc IStructSize dd ? hwndOwner dd ? _hInstance dd ? lpstrFilter dd ? lpstrCustomFilter dd ? nMaxCustFilter dd ? nFilterlndex dd ? lpstrFile dd ? nMaxFile dd ? lpstrFileTitle dd ? nMaxFileTitle dd ? lpstrInitialDir dd ? lpstrTitle dd ? Flags dd ? nFileOffset dw ? nFileExtension dw ? lpstrDefExt dd ? lCustData dd ? lpfnHook dd ? lpTemplateName dd ? OPENFILENAME ends



; из windef. h RECT struc left dd ? top dd ? right dd ? bottom dd ? RECT ends

; из winbase.h GMEM_MOVEABLE equ 2 GMEM_ZEROINIT equ 40h OPEN_EXISTING equ 3 CREATE_ALWAYS equ 2

Добавления в файл kernel32.inc между ifdef _TASM_ и else:

extrn lstrlen:near extrn GetCommandLineA:near extrn CloseHandle:near extrn GlobalAlloc:near extrn GlobalLock:near extrn GlobalFree:near extrn CreateFileA:near extrn ReadFile:near extrn WriteFile:near GetCommandLine equ GetCommandLineA CreateFile equ CreateFileA

и между else и endif:

extrn __imp__lstrlen@4:dword extrn __imp__GetCommandLineA@0:dword extrn __imp__CloseHandle@4:dword extrn __imp__GlobalAlloc@8:dword extrn __imp__GlobalLock@4:dword extrn __imp__GlobalFree@4:dword extrn __imp__CreateFileA@28:dword extrn __imp__ReadFile@20:dword extrn __imp__WriteFile@20:dword lstrlen equ __imp__lstrlen@4 GetCommandLine equ __imp__GetCommandLineA@0 CloseHandle equ __imp__CloseHandle@4 GlobalAlloc equ __imp__GlobalAlloc@8 GlobalLock equ __imp__GlobalLock@4 GlobalFree equ __imp__GlobalFree@4 CreateFile equ __imp__CreateFileA@28 ReadFile equ __imp__ReadFile@20 WriteFile equ __imp__WriteFile@20

Добавления в файл user32.inc:

extrn LoadAcceleratorsA:near extrn TranslateAccelerator:near extrn SendMessageA:near extrn SetWindowTextA:near extrn MoveWindow:near extrn GetClientRect:near extrn GlobalUnlock:near LoadAccelerators equ LoadAcceleratorsA SendMessage equ SendMessageA SetWindowText equ SetWindowTextA

и между else и endif:

extrn __imp__LoadAcceleratorsA@8:dword extrn __imp__ТranslateAccelerator@12:dword extrn __imp__SendMessageA@16:dword extrn __imp__SetWindowTextA@8:dword extrn __imp__MoveWindow@24:dword extrn __imp__GetClientRect@8:dword extrn __imp__GlobalUnlock@4:dword LoadAccelerators equ __imp__LoadAcceleratorsA@8 TranslateAccelerator equ __imp__TranslateAccelerator@12 SendMessage equ __imp__SendMessageA@16 SetWindowText equ __imp__SetWindowTextA@8 MoveWindow equ __imp__MoveWindow@24 GetClientRect equ __imp__GetClientRect@8 GlobalUnlock equ __imp__GlobalUnlock@4



Кроме того, нам потребуется новый включаемый файл, comdlg32.inc, описывающий функции, связанные с вызовами стандартных диалогов (выбор имени файла, печать документа, выбор шрифта и т.д.):

; comdlg32.inc ; включаемый файл с функциями из comdlg32.dll ifdef _TASM_ includelib import32.lib extrn GetOpenFileNameA:near extrn GetSaveFileNameA:near GetOpenFileName equ GetOpenFileNameA GetSaveFileName equ GetSaveFileNameA else includelib comdlg32.lib ; истинные имена используемых функций extrn __imp__GetOpenFileNameA@4:dword extrn __imp__GetSaveFileNameA@4:dword ; присваивания для удобства использования GetOpenFileName equ __imp__GetOpenFileNameA@4 GetSaveFileName equ __imp__GetSaveFileNameA@4 endif

Конечно, эту программу можно еще очень долго развивать — добавить toolbar и statusbar, написать документацию, можно сделать так, чтобы выделялось не фиксированное небольшое количество памяти для переноса файла в редактор, а равное его длине. Можно также воспользоваться функциями отображения части файла в память (CreateFileMapping, OpenFileMapping, MapViewOfFile, UnmapViewOfFile), позволив работать с неограниченно большими файлами. Win32 API настолько богат функциями, что можно довольно долго заниматься только их изучением, а это относится к теме программирования на ассемблере ровно настолько, насколько относится к программированию на любом другом языке.


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


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



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


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

some_proc();

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

call _some_proc

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

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

some_proc(a:word);

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

push a call _some_proc@4



Конвенция Pascal


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

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

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

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

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

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

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

ret 10 some_proc endp

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

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

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

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

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



Конвенция С


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

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

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

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

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

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

pop bp ret some_proc endp

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

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

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

ret some_proc endp

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

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

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

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

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

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



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


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



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


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

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

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

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

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



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


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



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


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



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


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

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



AGI


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

add edx,4 mov esi,[edx]

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

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

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

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

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

sub esp,24 push ebx ; *AGI*

или

mov esp,ebp pop ebp ; *AGI*

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

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



Кэш-память


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

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

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

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

Команда LEA


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

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

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

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

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

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



Конвейер FPU


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

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



Конвейер ММХ


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

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



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


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

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

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



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


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

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



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


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

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

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

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

Оптимизация


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

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

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

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

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



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


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



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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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


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



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


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

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

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

inc dx jmp loop_start

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

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

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

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



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


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

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

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

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

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

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



Префиксы


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



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


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

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

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

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

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



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


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



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


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

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

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

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

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

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

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

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

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

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

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



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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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


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

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

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

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

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

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



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


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