УДК 004.431.4:004.451
ББК 32.973.26-018.1
П33

Пирогов Владислав Юрьевич.
Ассемблер для Windows. — М.: Издатель Молгачева С.В., 2002. —552 с., ил.

Издатель Молгачева С.В.
Лицензия ИД N 03567 от 19 декабря 2000 г.

ISBN 5-94740-003-0

© Пирогов В.Ю., 2002.
© Издатель Молгачева С.В., 2002.


Содержание

Краткое содержание глав
Предисловие
Введение

Часть 1. Основы 32-битного программирования в Windows
Глава 1. Средства программирования в Windows
Глава 2. Основы программирования в операционной системе Windows
Глава 3. Примеры простых программ на языке ассемблера
Глава 4. Экскурс в 16-битное программирование
Глава 5. Ассемблеры MASM и TASM

Часть II. Более подробное описание программирования в среде Windows
Глава 1. Примеры простейших программ
Глава 2. Консольные приложения
Глава 3. Понятие ресурса. Редакторы и трансляторы ресурсов
Глава 4. Примеры программ использующих ресурсы
Глава 5. Управление файлами
Глава 6. Макросредства ассемблера и программирование в Windows

Часть III. Более сложные примеры программирования в Windows
Глава 1. Примеры программ, использующих таймер
Глава 2. Многозадачное программирование
Глава 3. Создание динамических библиотек
Глава 4. Взаимодействие с ресурсами локальной сети
Глава 5. Разрешение некоторых проблем программирования в Windows
Глава 6. Некоторые вопросы системного программирования в Windows
Глава 7. Использование ассемблера с языками высокого уровня

Часть IV. Отладка, анализ кода программ, драйверы
Глава 1. Структура исполняемых модулей
Глава 2. Обзор отладчиков и дизассемблеров
Глава 3. Введение в Turbo Debugger
Глава 4. Описание работы с дизассемблером W32Dasm и отладчиком ICE
Глава 5. Основы анализа кода программ
Глава 6. Исправление исполняемых модулей
Глава 7. Структура и написание драйверов .VXD

Приложения
Приложение 1. Справочник API-функций и сообщений Windows
Приложение 2. Справочник по командам и архитектуре Pentium
Приложение 3. Защищенный режим микропроцессора Pentium


Краткое содержание глав

Часть I. Основы 32-битного программирования в Windows
Глава 1. Средства программирования в Windows.
Дается краткое описание средств программирования на ассемблере: трансляторов, компоновщиков, отладчиков и т.п.
Глава 2. Основы программирования в операционной системе Windows.
Излагаются принципы программирования в Windows, описываются основные структуры на языке ассемблера.
Глава 3. Примеры простых программ на языке ассемблера.
Приводятся примеры программ для Windows с подробным их комментарием.
Глава 4. Экскурс в 16-битное программирование.
Дается описание 16-битного программирования. Приводятся примеры программ в 16-битном исполнении.
Глава 5. Ассемблеры MASM и TASM.
Дано описание команд двух наиболее популярных ассемблеров, их развитие и основные отличия.

Часть II. Более подробное описание программирования в Windows
Глава 1. Примеры простейших программ.
Приводятся примеры простейших 32-битных программ для Windows с подробными пояснениями. Основной упор делается на работу с текстом и графику.
Глава 2. Консольные приложения.
Излагаются основы консольного программирования и технология создания консольных приложений на ассемблере. Приводятся примеры консольных программ, в частности использование таймера в консольном приложении.
Глава 3. Понятие ресурса. Редакторы и трансляторы ресурсов.
Дается понятие ресурса, его структуры, редакторы ресурсов, язык ресурсов, трансляторы ресурсов и их компоновка в исполняемый модуль.
Глава 4. Примеры программ, использующих ресурсы.
Приводятся примеры программ с ресурсами (диалог, списки, кнопки и т.п.).
Глава 5. Управление файлами.
Излагаются основы файловой системы Windows (FAT32). Дается описание основных API-функций работы с файлами, приводятся примеры программ с файловой обработкой, пример рекурсивного поиска файлов по дереву каталогов.
Глава 6. Макросредства ассемблера и программирование в Windows.
Дается описание основных директив и команд MASM и TASM. Приводятся примеры использования макросредств и условного ассемблирования.

Часть III. Сложные примеры программирования в Windows
Глава 1. Примеры программ, использующих таймер.
Даются примеры более сложных программ для Windows. Излагаются некоторые приемы программирования. Рассматривается теория всплывающих подсказок и др.
Глава 2. Многозадачное программирование.
Рассматривается многозадачность операционной системы Windows, способы взаимодействия между задачами. Рассматривается многозадачность в рамках одной задачи (threads).
Глава 3. Создание динамических библиотек.
Рассматривается структура динамических библиотек, взаимодействие с динамической библиотекой, обмен информацией с динамической библиотекой, примеры динамических библиотек.
Глава 4. Взаимодействие с ресурсами локальной сети.
Описываются базовые функции, позволяющие получать доступ к ресурсам локальной сети. Приводятся примеры программирования в локальной сети.
Глава 5. Разрешение некоторых проблем программирования в Windows.
Излагаются некоторые "трудные" вопросы программирования в среде Windows и методы их разрешения. В основу взяты наиболее часто задаваемые программистами вопросы (FAQ'и).
Глава 6. Некоторые вопросы системного программирования в Windows.
Излагаются отдельные вопросы системного программирования (структура операционных систем, структура памяти в Windows 9x, Windows NT, Windows 2000 и т.п.).
Глава 7. Использование ассемблера с языками высокого уровня.
В данной главе излагаются способы взаимодействия современных языков высокого уровня с языком ассемблера. Показывается возможности использования языка ассемблера с языками высокого уровня (Delphi, Borland C++, Visual C++).

Часть IV. Отладка, анализ кода программ, драйверы
Глава 1. Структура исполняемых модулей.
Излагается структура 32-битных загружаемых и объектных модулей.
Глава 2. Обзор отладчиков и дизассемблеров.
Дается краткий обзор наиболее популярных отладчиков и дизассемблеров, описание работы с ними.
Глава 3. Введение в Turbo Debugger.
Дается краткое описание работы в Turbo Debugger для 32-битных приложений.
Глава 4. Описание работы с дизассемблером W32Dasm и отладчиком SoftICE.
Излагаются основы дизассемблирования и отладки в программах W32Dasm и SoftICE.
Глава 5. Основы анализа кода программ.
Приводятся базовые структуры программного кода и методы анализа кода.
Глава 6. Исправление исполняемых модулей.
Излагаются некоторые методы исправления программного кода.
Глава 7. Структура и написание драйверов .VXD.
Излагается теория драйверов для Windows, приводятся примеры драйверов для операционной системы Windows.


Предисловие

Если Вы, дорогой читатель, знакомы с книгой "Assembler: учебный курс" Вашего покорного слуги, то, наверное, обратили внимание, что программированию в операционной системе Windows было посвящено всего две главы. Это немного и может служить лишь введением в данную область. Пришло время заняться этим серьезно.

Прежде всего, как и полагается в предисловии, отвечу на возможное замечание: зачем нужен ассемблер в Windows, если есть, например, Си и другие языки. Зачем нужен ассемблер, я уже писал в упомянутой выше книге. Позволю себе процитировать ее: "Зачем нужен язык ассемблера? - спросят меня. Самой простой и убедительный ответ на поставленный вопрос такой - затем, что это язык процессора и, следовательно, он будет нужен до тех пор, пока будут существовать процессоры. Более пространный ответ на данный вопрос содержал бы в себе рассуждение о том, что ассемблер может понадобиться для оптимизации кода программ, написания драйверов, трансляторов, программирования некоторых внешних устройств и т.д. Для себя я, однако, имею и другой ответ: программирование на ассемблере дает ощущение власти над компьютером, а жажда власти - один из сильнейших инстинктов человека".

Что касается операционной системы Windows1, то здесь, как ни странно это прозвучит для уха некоторых программистов, программировать на ассемблере гораздо легче, чем в операционной системе MS DOS. В данной книге я берусь доказать, что программировать на ассемблере в Windows ничуть не сложнее чем на Си, и при этом получается компактный, эффективный и быстрый код. Работая с языками высокого уровня, мы теряем определенные алгоритмические навыки. И процесс заходит все дальше. Честное слово, только ради повышения своего профессионального уровня стоит заниматься программированием на ассемблере.

Как и предыдущая, эта книга будет содержать только работающие программы с подробным разбором и комментарием.

Сейчас существует два основных конкурирующих ассемблера MASM (Macro Assembler) и TASM (Turbo Assembler)2. Для всех программ будет оговорено, как транслировать их с помощью и MASM, и TASM.

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

Надо сказать, что в литературе по программированию для Windows 9Х образовалась некоторая брешь - авторы очень быстро перешли от чистого API-программирования 3 к описанию визуальных компонент тех или иных языков. Автору известна лишь одна, да и то переводная, книга по "чистому" программированию для Windows: Герберт Шилдт, Программирование на С и C++ для Windows 954 (см. также [12]). В своей книге я пытаюсь прикрыть эту брешь, рассматривая некоторые малоосвещенные в литературе вопросы: программирование в локальной сети, использование многозадачности, написание VXD-драйверов, обработка файлов и др.

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

1 Под термином "операционная система Windows" в данной книге я буду подразумевать сразу несколько по сути разных операционных систем: Windows 95-98, Windows NT, Windows 2000 (см. ниже). При необходимости мы будем оговаривать, какую ОС имеем в виду.

2 Я имею в виду операционные системы MS DOS - Windows.

3 Под API-программированием мы понимаем программирование с использованием одних API-функций.

4 Справедливости ради заметим, что для старой ОС Windows (версии 3.1 или ниже) вышла довольно обширная литература.


Введение

Изложу некоторые принципы, на которые я опирался, когда писал данную книгу.

1) Детальное изложение рассматриваемых вопросов. Я по-прежнему "не жалую" макросредства5 и считаю, что начинающим программистам их не стоит использовать. Однако цель, которую я преследую в книге: сблизить позиции MASM и TASM — потребует от нас знания и макросредств. Итак, макросредства будут играть в моей книге достаточно узкую подчиненную роль. Впрочем, в книге имеется глава, где подробно разбираются директивы и макросредства ассемблера.

2) Чтобы сделать изложение максимально полезным, все программы будут излагаться либо в двух вариантах - для MASM и для TASM, либо с подробным объяснением того, как перейти к другому ассемблеру. За основу взят пакет MASM версии 6.14 и TASM (TASM32.EXE версии 5.0, TLINK32.EXE версии 1.6.71). Читателям рекомендуем пользоваться версиями не ниже указанных.

3) Книга содержит в себе изложение материала, начиная с простых программ и заканчивая элементами системного программирования. Поэтому книгу можно считать специальным учебным курсом по программированию в операционной системе Windows. Желательно (хотя не обязательно) знакомство читателя с языком Си и весьма желательно наличие начальных знаний по языку ассемблера. В качестве учебников по языку ассемблера можно рекомендовать книги [1,13].

4) Книга содержит обширный справочный материал для того, чтобы читатель не отвлекался на поиски его в других книгах и в Интернете. В Приложении 2 имеется справочник по командам микропроцессора с пояснениями. Более подробное объяснение команд можно найти в книгах [1,3,13].

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

Начинающим программировать на языке ассемблера рекомендую начинать с глав первой части. Я не останавливаюсь на описании команд микропроцессора и отсылаю читателя к книгам [1,3,13] (см. также Приложение 2). Я также подробно разъясняю структуру программ для Windows.

Несколько слов скажу о нумерации глав в данной книге. Книга разбита на части, а те, в свою очередь, на главы. В каждой части своя нумерация глав. Полный номер главы состоит из номера части и номера главы в ней. Таким образом, Гл. 2.3 означает главу 3 из части 2. Номер рисунка содержит в себе номер части, номер главы и номер рисунка в главе.

Синонимами в данной книге являются такие термины, как: ассемблер и язык ассемблера; процедура, функция6, подпрограмма.

5 См. книгу автора "Assembler: учебный курс".

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


Часть I. Глава 1. Средства программирования в Windows

Часть I. Основы 32-битного программирования в Windows

Глава 1. Средства программирования в Windows

I

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

Прежде всего замечу, что в названии главы есть некоторая натяжка, т.к. технологии трансляции и в MS DOS, и в Windows весьма схожи. Однако программирование в MS DOS уходит в прошлое.

Рис.1.1.1. Схема трансляции ассемблерного модуля.

Двум стадиям трансляции (Рис.1.1.1) соответствуют две основные программы: ассемблер ML.EXE и редактор связей LINK.EXE 7 (или TASM32.EXE и TLINK32.EXE в Турбо Ассемблере).

Пусть файл с текстом программы на языке ассемблера называется PROG.ASM, тогда, не вдаваясь в подробный анализ, две стадии трансляции будут выглядеть следующим образом: c:\masm32\bin\ml /c /coff PROG.ASM - в результате появляется модуль PROG.OBJ, а также c:\masm32\bin\link /SUBSYSTEM:WINDOWS PROG.OBJ - в результате появляется исполняемый модуль PROG.EXE. Как Вы, я надеюсь, догадались /с и /coff являются параметрами программы ML.EXE, a /SUBSYSTEM:WINDOWS является параметром для программы LINK.EXE. О других ключах этих программ более подробно см. Гл. 1.5.

Чем больше я размышляю об этой схеме трансляции, тем более совершенной она мне кажется. Действительно, формат конечного модуля зависит от операционной системы. Установив стандарт на структуру объектного модуля, мы получаем возможность: а) использовать уже готовые объектные модули, б) стыковать между собой программы, написанные на разных языках. Но самое прекрасное здесь то, что если стандарт объектного модуля распространить на разные операционные системы, то можно использовать модули, написанные в разных операционных системах8.

Чтобы процесс трансляции сделать для Вас привычным, рассмотрим несколько простых, "ничего не делающих" программ.

.386P
; плоская модель
.MODEL FLAT, STDCALL
;--------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
     RET   ; выход
_TEXT ENDS
END START

Рис.1.1.2. "Ничего не делающая" программа.

На Рис. 1.1.2 представлена "ничего не делающая" программа. Назовем ее PROG1. Сразу отмечу на будущее: команды микропроцессора и директивы макроассемблера будем писать заглавными буквами.

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

ML /c /coff PROG1.ASM
LINK /SUBSYSTEM:WINDOWS PROG1

или для Турбо Ассемблера

TASM32 /ml PROG1.ASM
TLINK32 -аа PROG1.OBJ

Примем пока параметры трансляции программ как некую данность и продолжим наши изыскания.

Часто удобно разбить текст программы на несколько частей и объединять эти части еще на 1-й стадии трансляции. Это достигается посредством директивы INCLUDE. Например, один файл будет содержать код программы, а константы, данные (определение переменных) и прототипы внешних процедур помещаются в отдельные файлы. Часто такие файлы записывают с расширением .INC.

Именно такая разбивка демонстрируется в следующей программе (Рис.1.1.3).

;файл CONS.INC
CONS1  EQU 1000
CONS2  EQU 2000
CONS3  EQU 3000
CONS4  EQU 4000
CONS5  EQU 5000
CONS6  EQU 6000
CONS7  EQU 7000
CONS8  EQU 8000
CONS9  EQU 9000
CONS10 EQU 10000
CONS11 EQU 11000
CONS12 EQU 12000

;файл DAT.INC
DAT1  DWORD 0
DAT2  DWORD 0
DAT3  DWORD 0
DAT4  DWORD 0
DAT5  DWORD 0
DAT6  DWORD 0
DAT7  DWORD 0
DAT8  DWORD 0
DAT9  DWORD 0
DAT10 DWORD 0
DAT11 DWORD 0
DAT12 DWORD 0

;файл PROG1.ASM
.386P
;плоская модель
.MODEL FLAT, STDCALL
;подключить файл констант
INCLUDE CONS.INC
;--------------------------------------------------
;сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
;подключить файл данных
INCLUDE DAT.INC
_DATA ENDS
;сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
     MOV EAX,CONS1
     SHL EAX,1 ; умножение на 2
     MOV DAT1,EAX
;-----------------------------------
     MOV EAX,CONS2
     SHL EAX,2 ; умножение на 4
     MOV DAT2,EAX
;-----------------------------------
     MOV EAX, CONS3
     ADD EAX, 1000 ; прибавим 1000
     MOV DAT3, EAX
;-----------------------------------
     MOV EAX, CONS4
     ADD EAX, 2000 ; прибавим 2000
     MOV DAT4, EAX
;-----------------------------------
     MOV EAX, CONS5
     SUB EAX, 3000 ; вычесть 3000
     MOV DAT5, EAX
;-----------------------------------
     MOV EAX, CONS6
     SUB EAX, 4000 ; вычесть 4000
     MOV DAT6, EAX
;-----------------------------------
     MOV EAX, CONS7
     MOV EDX, 3
     IMUL EDX ; умножение на 3
     MOV DAT7, EAX
;-----------------------------------
     MOV EAX, CONS8
     MOV EDX, 7 ; умножение на 7
     IMUL EDX
     MOV DAT8, EAX
;-----------------------------------
     MOV EAX, CONS9
     MOV EBX, 3 ; деление на 3
     MOV EDX, 0
     IDIV EBX
     MOV DAT9, EAX
;-----------------------------------
     MOV EAX, CONS10
     MOV EBX, 7 ; деление на 7
     MOV EDX, 0
     IDIV EBX
     MOV DAT10, EAX
;-----------------------------------
     MOV EAX, CONS11
     SHR EAX, 1 ; деление на 2
     MOV DAT11, EAX
;-----------------------------------
     MOV EAX, CONS12
     SHR EAX, 2 ; деление на 4
     MOV DAT12, EAX
;-----------------------------------
     RET      ; выход
_TEXT ENDS
END START

Рис. 1.1.3. Пример использование директивы INCLUDE.

Программа на Рис. 1.1.3 также достаточно бессмысленна (как и все программы данной главы), но зато демонстрирует удобства использования директивы INCLUDE. Напомню, что мы не останавливаемся в книге на очевидных командах микропроцессора. Замечу только по поводу команды IDIV. В данном случае команда IDIV осуществляет операцию деления над операндом, находящемся в паре регистров EDX:EAX. Обнуляя EDX, мы указываем, что операнд целиком находится в регистре EAX.

Трансляция программы осуществляется так, как это было указано ранее для ассемблеров MASM и TASM.

Замечание о типах данных. В данной книге Вы встретитесь в основном с тремя типами данных (простых): байт, слово, двойное слово. При этом используются следующие стандартные обозначения. Байт - BYTE или DB, слово - WORD или DW, двойное слово - DWORD или DD. Выбор, скажем, в одном случае DB, а в другом BYTE, продиктован лишь желанием автора несколько разнообразить изложение.

7 Программу LINK.EXE называют также компоновщиком или просто линковщиком.

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

II

Перейдем теперь к вопросу о подсоединении других объектных модулей и библиотек во второй стадии трансляции. Прежде всего замечу, что, сколько бы ни подсоединялось объектных модулей, один объектный модуль является главным. Смысл этого весьма прост: именно с этого модуля начинается исполнение программы. На этом различие между модулями заканчивается. Условимся далее, что главный модуль всегда в начале сегмента кода будет содержать метку START, ее мы указываем после директивы END - транслятор должен знать точку входа программы, чтобы указать ее в заголовке загружаемого модуля (см. Гл.5.1).

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

.386P
;модуль PROG2.ASM
;плоская модель
.MODEL FLAT, STDCALL
PUBLIC PROC1
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
PROC1 PROC
    MOV EAX, 1000
    RET
PROC1 ENDP
_TEXT ENDS
END

Рис. 1.1.4. Модуль PROG2.ASM, процедура которого PROC1 будет вызываться из основного модуля.

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

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

Итак, выполняем команду ML /coff /c PROG1.ASM. В результате на диске появляется объектный модуль PROG2.OBJ.

А теперь проведем маленькое исследование. Просмотрим объектный модуль с помощью какого-нибудь простого Viewer'a, например того, что есть у программы Far. И что же мы обнаружим: вместо имени PROC1 мы увидим имя _PROC1@0. Это особый разговор — будьте сейчас внимательны! Во-первых, подчеркивание спереди отражает стандарт ANSI, предписывающий всем внешним именам (доступным нескольким модулям) автоматически добавлять символ подчеркивания. Здесь ассемблер будет действовать автоматически, и у нас по этому поводу не будет никаких забот.

Сложнее с припиской @0. Что она значит? На самом деле все просто: цифра после знака @ означает количество байт, которые необходимо передать в стек в виде параметров при вызове процедуры. В данном случае ассемблер понял так, что наша процедура параметров не требует. Сделано это для удобства использования директивы INVOKE. Но о ней речь пойдет ниже, а пока попытаемся сконструировать основной модуль PROG1.ASM.

.386P
; плоская модель
.MODEL FLAT, STDCALL
;--------------------------------------------------
; прототип внешней процедуры
EXTERN   PROC1@0:NEAR

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
     CALL PROC1@0
     RET   ; выход
_TEXT ENDS
END START

Рис. 1.1.5. Модуль PROG1.ASM с вызовам процедуры из модуля PROG2.ASM.

Как Вы понимаете, процедура, вызываемая из другого модуля, объявляется как EXTERN. Далее, вместо имени PROC1 нам приходится использовать имя PROC1@0. Здесь пока ничего нельзя сделать. Может возникнуть вопрос о типе NEAR. Дело в том, что в операционной системе MS DOS тип NEAR означал, что вызов процедуры (или безусловный переход) будет происходить в пределах одного сегмента. Тип FAR означал, что процедура (или переход) будет вызываться из другого сегмента. В операционной системе Windows реализована так называемая плоская модель, когда всю память можно рассматривать как один большой сегмент. И здесь логично использовать тип NEAR.

Выполним команду ML /coff /c PROG1.ASM, в результате получим объектный модуль PROG1.OBJ. Теперь можно объединить модули и получить загружаемую программу PROG1.EXE:

LINK /SUBSYSTEM:WINDOWS PROG1.OBJ PROG2.OBJ

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

III

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

Удобство ее заключается, во-первых, в том, что мы сможем забыть о добавке @N. Во-вторых, эта команда сама заботится о помещении передаваемых параметров в стек. Последовательность команд

PUSH par1
PUSH par2
PUSH par3
PUSH par4
CALL NAME_PROC@N ; N-количество отправляемых в стек байт

заменяется на

INVOKE NAME_PROC, par4, par3, par2, par1

Причем параметрами могут являться регистр, непосредственно значение или адрес. Кроме того, для адреса может использоваться как оператор OFFSET, так и оператор ADDR. Видоизменим теперь модуль PROG1.ASM (модуль PROG2.ASM изменять не придется).

.386P
; плоская модель
.MODEL FLAT, STDCALL
;-----------------------------------
; прототип внешней процедуры
PROC1 PROTO
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
     INVOKE PROC1
     RET   ; выход
_TEXT ENDS
END START

Рис. 1.1.6. Пример использования оператора INVOKE.

Как видите, внешняя процедура объявляется теперь при помощи директивы PROTO. Данная директива позволяет при необходимости указывать и наличие параметров. Например, строка

PROC1 PROTO :DWORD, :WORD

будет означать, что процедура требует два параметра длиной в четыре и два байта (всего 6, т.е. @6).

Как уже говорилось, я буду редко использовать оператор INVOKE. Теперь я назову первую причину такого пренебрежения к данной возможности. Дело в том, что я сторонник чистоты языка ассемблера и любое использование макросредств вызывает у меня чувство несовершенства. На мой взгляд, и начинающим программистам не стоит увлекаться макросредствами, иначе не чувствуется вся красота этого языка. О второй причине Вы узнаете ниже.

На нашей схеме, на Рис. 1.1, говорится не только о возможности подсоединения объектных модулей, но и библиотек. Собственно, если объектных модулей несколько, то это по понятным причинам вызовет неудобства. Поэтому объектные модули объединяются в библиотеки. Для подсоединения библиотеки в MASM удобнее всего использовать директиву INCLUDELIB, которая сохраняется в объектном коде и используется программой LINK.EXE.

Но как создать библиотеку из объектных модулей? Для этого имеется специальная программа, называемая библиотекарем. Предположим, мы хотим создать библиотеку LIB1.LIB, состоящую из одного модуля - PROG2.OBJ. Выполним для этого следующую команду: LIB /OUT:LIB1.LIB PROG2.OBJ.

Если необходимо добавить в библиотеку еще один модуль (MODUL.OBJ), то достаточно выполнить команду: LIB LIB1.LIB MODUL.OBJ.

Вот еще два полезных примера использования библиотекаря:
LIB /LIST LIB1.LIB - выдает список модулей библиотеки.
LIB /REMOVE:MODUL.OBJ LIB1.LIB - удаляет из библиотеки модуль MODUL.OBJ.

Вернемся теперь к нашему примеру. Вместо объектного модуля мы используем теперь библиотеку LIB1.LIB. Видоизмененный текст программы PROG1.ASM представлен на Рис. 1.7.

.386P
; плоская модель
.MODEL FLAT, STDCALL
;--------------------------------------------------
; прототип внешней процедуры
EXTERN   PROC1@0:NEAR
;--------------------------------------------------
INCLUDELIB LIB1.LIB
;--------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
     CALL PROC1@0
     RET ; выход
_TEXT ENDS
END START

Рис. 1.1.7. Пример использование библиотеки.

IV

Рассмотрим теперь менее важный (для нас) вопрос об использовании данных (переменных), определенных в другом объектном модуле. Здесь читателю, просмотревшему предыдущий материал, должно быть все понятно, а модули PROG2.ASM и PROG1.ASM, демонстрирующие технику использования внешних9 переменных, приводятся на Рис.1.8-1.9.

.386P
; модуль PROG2.ASM
; плоская модель
.MODEL FLAT, STDCALL
PUBLIC PROC1
PUBLIC ALT
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     ALT DWORD 0
_DATA ENDS
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
PROC1 PROC
     MOV EAX,ALT
     ADD EAX,10
     RET
PROC1 ENDP
_TEXT ENDS
END

Рис. 1.1.8. Модуль, содержащий переменную ALT, которая используется в другом модуле (PROG1.ASM).

.386P

; модуль PROG1.ASM
; плоская модель
.MODEL FLAT, STDCALL
;-----------------------------------
; прототип внешней процедуры
EXTERN   PROC1@0:NEAR
; внешняя переменная
EXTERN   ALT: DWORD
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
     MOV ALT, 10
     CALL PROC1@0
     MOV EAX, ALT
     RET   ; выход
_TEXT ENDS
END START

Рис. 1.1.9. Модуль, использующий переменную ALT, определенную в другом модуле (PROG2.ASM).

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

9 Термин "внешняя переменная" используется нами по аналогии с термином "внешняя процедура".

V

А теперь проверим все представленные в данной главе программы на предмет их трансляции средствами пакета TASM.

С программами на Рис. 1.2-1.3 дело обстоит просто. Для их трансляции достаточно выполнить команды:

TASM32 /ml PROG1.ASM
TLINK32 -аа PROG1.OBJ

Обратимся теперь к модулям PROG2.ASM и PROG1.ASM, приведенным на Рис. 1.4 и 1.5 соответственно.

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

Следовательно, единственное, что нам следует сделать, это заменить в модуле PROC1.ASM имя PROC1@0 на PROC1.

Объединение модулей далее производится элементарно:

TLINK32 -аа PROG1.OBJ PROG2.OBJ

Для работы с библиотекой в пакете TASM имеется программа — библиотекарь TLIB.ЕХЕ. Создание библиотеки, состоящей из модуля PROG2.OBJ, производится по команде

TLIB LIB1.LIB+PROG2.OBJ

В результате на диске появится библиотека LIB1.LIB. Далее компонуем модуль PROG1.OBJ с этой библиотекой:

TLINK32 -аа PROG1,PROG1,PROG1,LIB1

В результате получается загружаемый модуль PROG1.EXE.

Вообще, стоит разобраться с командной строкой TLINK32 более подробно. В расширенном виде11 эта строка выглядит следующим образом:

TLINK32 -аа OBJFILES, EXEFILE, MAPFILE, LIBFILES

OBJFILES - один или несколько объектных файлов (через пробел). Первый главный модуль.

EXEFILE - исполняемый модуль.

MAPFILE - МАР-файл, содержащий информацию о структуре модуля.

LIBFILES - одна или несколько библиотек (через пробел).

В TASM отсутствует директива INVOKE, поэтому в дальнейшем я буду избегать ее использования12.

В начале книги я объявил о своем намерении примирить два ассемблера. Поскольку различие между ними заключается в директивах и макрокомандах (см. Гл.1.5), то напрашивается вывод, что совместимости можно добиться, избегая таких директив и макрокоманд. Основой программы для Windows является вызов API-функций (см. Гл. 1.2), а мы знаем, что различие в вызове внешней процедуры заключается в том, что в именах для MASM в конце есть добавка @N. И здесь не обойтись без макроопределений, и начинается самое интересное. Но об этом, дорогой читатель, Вы узнаете в свое время.

И MASM и TASM поддерживают так называемую упрощенную сегментацию. Я являюсь приверженцем классической структуры ассемблерной программы и должен признаться, что упрощенная сегментация довольно удобная штука, особенно при программировании под Windows. Суть такой сегментации в следующем: начало сегмента определяется директивой .CODE, а сегмента данных - .DATA 13. Причем обе директивы могут появляться в тексте программы несколько раз. Транслятор затем собирает код и данные вместе, как положено. Основной целью такого подхода, по-видимому, является возможность приблизить в тексте программы данные к тем строкам, где они используются. Такая возможность, как известно, в свое время была реализована в C++. На мой взгляд, она приводит к определенному неудобству при чтении текста программы. Кроме того, не сочтите меня за эстета, но когда я вижу данные, перемешанные в тексте программы с кодом, у меня возникает чувство дискомфорта.

Ниже представлена программа, демонстрирующая упрощенный режим сегментации.

.386P
; плоская модель
.MODEL FLAT, STDCALL
;--------------------------------------------------
; сегмент данных
.DATA
SUM DWORD 0
; сегмент кода
.CODE
START:
; сегмент данных
.DATA
     A   DWORD 100
; сегмент кода
.CODE
     MOV EAX,A
; сегмент данных
.DATA
     B   DWORD 200
; сегмент кода
.CODE
     ADD EAX,B
     MOV SUM,EAX
     RET   ; выход
END START

Рис. 1.1.10. Пример программы, использующей упрощенную сегментацию.

11 Все же в несколько упрощенном виде.

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

13 Разумеется, есть директива и для стека — это .STACK, но мы ее почти не будем использовать.

VI

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

Редакторы. Хотя сам я никогда не использую специализированные редакторы для написания программ на ассемблере, полноты ради кратко расскажу о двух известных мне редакторах. Начну с редактора QEDITOR.EXE, который поставляется вместе с пакетом MASM32. Сам редактор и все сопутствующие ему утилиты написаны на ассемблере. Анализ их размера и возможностей действительно впечатляет. Например, сам редактор имеет длину всего 27 Кб, а утилита, используемая для просмотра отчетов о трансляции — всего 6 Кб. Редактор вполне годится для работы с небольшими одномодульными приложениями. Для работы с несколькими модулями он не очень удобен. Работа редактора основана на взаимодействии с различными утилитами посредством пакетных файлов. Например, трансляцию программ осуществляет пакетный файл ASSMBL.BAT, который использует ассемблер ML.EXE, а результат ассемблирования направляется в текстовый файл ASMBL.TXT. Далее для просмотра этого файла используется простая утилита THEGUN.EXE. Аналогично осуществляется редактирование связей. Для дизассемблирования исполняемого модуля используется утилита DUMPPE.EXE, результат работы этой утилиты помещается в текстовый файл DISASM.TXT. Аналогично осуществляются и другие операции. Вы легко сможете настроить эти операции, отредактировав соответствующий пакетный файл с модификацией (при необходимости) используемых утилит (заменив, например, ML.EXE на TASM32.EXE и т.п.).

Вторая программа, с которой я хочу познакомить читателя, это EAS.EXE (Easy Assembler Shell). Редактор, а точнее оболочка, позволяет создавать и транслировать довольно сложные проекты, состоящие из ASM-,OВJ-,RC-,RES-,DEF-файлов. Программа позволяет работать как с TASM, так и MASM, а также с другими утилитами (отладчиками, редакторами ресурсов и т.д.). Непосредственно в программе можно настроить компиляторы и редакторы связей на определенный режим работы путем задания ключей этих утилит.

Отладчики.

Отладчики позволяют исполнять программу в пошаговом режиме. В IV части книги мы более подробно будем рассматривать отладчики и дизассемблеры. Приведу несколько наиболее известных отладчиков14: CodeView (Микрософт), Turbo Debugger (Borland), Ice.

Дизассемблеры.

Дизассемблеры переводят исполняемый модуль в ассемблерный код. Примером простейшего дизассемблера является программа DUMPPE.EXE, работающая в строковом режиме. Пример работы программы DUMPPE.EXE представлен на Рис. 1.1.11. Здесь дизассемблируется программа, приведенная на Рис.1.1.5. Ну как, узнали нашу программу? Смысл обозначений будет ясен из дальнейшего изложения.

knia.exe                    (hex)                   (dec)
.EXE size (bytes)           490                1168
Minimum load size (bytes)   450                1104
Overlay number              0                  0
Initial CS:IP               0000:0000
Initial SS:SP               0000:OOB8          184
Minimum allocation (para)   0                  0
Maximum allocation (para)   FFFF               65535
Header size (para)          4                  4
Relocation table offset     40                 64
Relocation entries          0                  0

Portable Executable starts at    a8
Signature                   00004550 (РЕ)
Machine                     014C (Intel 386)
Sections               0001
Time Date Stamp        3AE6D1B1 Wed Apr 25 19:31:29 2001

Symbol Table           00000000
Number of Symbols      00000000
Optional header size   OOEO
Characteristics        O1OF
Relocation information stripped
Executable Image
Line numbers stripped
Local symbols stripped
32 bit word machine
Magic                       010B
Linker Version              5.12
Size of Code                00000200
Size of Initialized Data    00000000
Size of Uninitialized Data  00000000
Address of Entry Point      00001000
Base of Code                00001000
Base of Data                00002000
Image Base                  00400000
Section Alignment           00001000
File Alignment              00000200
Operating System Version    4.00
Image Version               0.00
Subsystem Version           4.00
Reserved                    00000000
Image Size                  00002000
Header Size                 00000200
Checksum                    00000000
Subsystem                   0002 (Windows)
DLL Characteristics         0000
Size Of Stack Reserve       00100000
Size Of Stack Commit        00001000
Size Of Heap Reserve        00100000
Size Of Heap Commit         00001000
Loader Flags                00000000
Number of Directories       00000010

Directory Name                       VirtAddr VirtSize
-----------------------------------  -------- --------
Export                               00000000 00000000
Import                               00000000 00000000
Resource                             00000000 00000000
Exception                            00000000 00000000
Security                             00000000 00000000
Base Relocation                      00000000 00000000
Debug                                00000000 00000000
Decription/Architecture              00000000 00000000
Machine Value (MIPS GP)              00000000 00000000
Thread Storage                       00000000 00000000
Load Configuration                   00000000 00000000
Bound Import                         00000000 00000000
Import Address Table                 00000000 00000000
Delay Import                         00000000 00000000
СОМ Runtime Descriptor               00000000 00000000
(reserved)                           00000000 00000000

Section Table
-------------
Virtual Address        0001000
Virtual Size           OOOOOE
Raw Data Offset        000200
Raw Data Size          0000200
Relocation Offset      000000
Relocation Count       000
Line Number Offset     0000000
Line Number Count      000
Characteristics        0000020
Code
Executable
Readable

Disassembly

00401000 start:
00401000 E803000000    call fn_00401008
00401005 C3            ret
00401006 CC            int 3
00401007 CC            int 3
00401008 fn_00401008:
00401008 B8E8030000    mov eax,3E8h
0040100D C3            ret

Рис. 1.1.11. Пример дизассемблированш программы с помощью DUMPPE.EXE.

Отмечу также дизассемблер W32Dasm, который подробно будет описан в последней части книги, и знаменитый дизассемблер IDA Pro. В части IV мы будем подробно рассматривать и сами дизассемблеры, и методику их использования.

Нех-редакторы

Нех-редакторы позволяют просматривать и редактировать загружаемые модули в шестнадцатеричном виде. Их великое множество, к тому же отладчики и дизассемблеры, как правило, имеют встроенные НЕХ-редакторы. Отмечу только, весьма популярную в хакерских кругах программу HIEW.EXE. Эта программа позволяет просматривать загружаемые модули как в шестнадцатеричном виде, так и в виде ассемблерного кода. И не только просматривать, но и редактировать.

Компиляторы ресурсов

В пакетах MASM32 и TASM32 есть компиляторы ресурсов, которые будут описаны ниже. Это программы RC.EXE и BRC32.EXE соответственно.

Редакторы ресурсов

Обычно я пользуюсь редактором ресурсов из пакета ВС5 (Borland C++ 5.0) Простые ресурсы можно создавать в обычном текстовом редакторе. Язык описания ресурсов будет подробно рассмотрен далее.

14 Программа DEBUGER.EXE все еще поставляется с операционной системой Windows, но этот отладчик не поддерживает новый формат исполняемых файлов.


Часть I. Глава 2. Основы программирования в операционной системе Windows

Глава 2. Основы программирования в операционной системе Windows

В данной главе я намерен рассмотреть два момента, которые крайне важны для начала программирования на ассемблере в среде Windows - это вызов системных функций (API-функций) и возможные структуры программ 15 для Windows. Я полагаю, что можно выделить три типа структуры программ, которые условно можно назвать как классическая16 структура, диалоговая (основное окно — диалоговое), консольная, или безоконная17, структура. В данной главе подробно описывается первая, классическая структура.

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

  1. Программирование в Windows основывается на использовании функций API (Application Program Interface, т.е. интерфейс программного приложения). Их количество достигает двух тысяч. Ваша программа в значительной степени будет состоять из таких вызовов. Все взаимодействие с внешними устройствами и ресурсами операционной системы будет происходить посредством таких функций.
  2. Список функций API и их описание лучше всего брать из файла WIN32.HLP, который поставляется, например, с пакетом Borland C++.
  3. Главным элементом программы в среде Windows является окно. Для каждого окна определяется своя процедура18 обработки сообщений (см. ниже).
  4. Окно может содержать элементы управления: кнопки, списки, окна редактирования и др. Эти элементы, по сути, также являются окнами, но обладающими особыми свойствами. События, происходящие с этими элементами (и самим окном), приводят к приходу сообщений в процедуру окна.
  5. Операционная система Windows использует линейную модель памяти. Другими словами, всю память можно рассматривать как один сегмент. Для программиста на языке ассемблера это означает, что адрес любой ячейки памяти будет определяться содержимым одного 32-битного регистра, например EBX.
  6. Следствием пункта 5 является то, что мы фактически не ограничены в объеме данных, кода или стека (объеме локальных переменных). Выделение в тексте программы сегмента кода и сегмента данных является теперь простой формальностью, улучшающей читаемость программы.
  7. Операционная система Windows является многозадачной средой. Каждая задача имеет свое адресное пространство и свою очередь сообщений. Более того, даже в рамках одной программы может быть осуществлена многозадачность - любая процедура может быть запущена как самостоятельная задача.

Итак, после теоретических положений самое время перейти к программным примерам.

15 Не путать со структурой загружаемых модулей.

16 Классификация автора.

17 Как потом станет ясно, консольное приложение вполне может иметь диалоговое окно.

18 Исходя из терминологии, принятой в MS DOS, такую процедуру следует назвать "процедурой прерывания". Для Windows же принята другая терминология. Подобные процедуры, вызываемые самой системой, называются процедурами обратного вызова (CALLBACK).

I

Начнем с того, как можно вызвать функции API. Обратимся к файлу помощи и выберем любую функцию API, например, MessageBox:

int MessageBox ( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType );

Данная функция выводит на экран окно с сообщением и кнопкой (или кнопками) выхода. Смысл параметров: hWnd -дескриптор окна, в котором будет появляться окно-сообщение, lpText - текст, который будет появляться в окне, lpCaption - текст в заголовке окна, uType - тип окна, в частности можно определить количество кнопок выхода. Теперь о типах параметров. Все они в действительности 32-битные целые числа: HWND — 32-битное целое, LPCTSTR — 32-битный указатель на строку, UINT — 32-битное целое. По причине, о которой будет сказано ниже, к имени функций нам придется добавлять суффикс "А", кроме того, при использовании MASM необходимо также в конце имени добавить @16. Таким образом, вызов указанной функции будет выглядеть так: CALL MessageBoxA@16. А как же быть с параметрами? Их следует аккуратно поместить в стек. Запомните правило: СЛЕВА НАПРАВО — СНИЗУ ВВЕРХ. Итак, пусть дескриптор окна расположен по адресу HW, строки — по адресам STR1 и STR2, а тип окна-сообщения — это константа. Самый простой тип имеет значение 0 и называется МВ_ОК. Имеем следующее:

МВ_ОК    equ 0
.
.
STR1     DB "Неверный ввод! ",0
STR2     DB "Сообщение об ошибке.",0
HW       DWORD ?
.
.
PUSH     МВ_ОК
PUSH     OFFSET STR1
PUSH     OFFSET STR2
PUSH     HW
CALL     MessageBoxA@16

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

Аналогичным образом в ассемблере легко воспроизвести те или иные Си-структуры. Рассмотрим, например, структуру, определяющую системное сообщение:

typedef struct tagMSG { // msg
     HWND hwnd;
     UINT message;
     WPARAM wParam;
     LPARAM lParam;
     DWORD time;
     POINT pt;
} MSG;

Это сообщение будет далее подробно прокомментировано в одном из примеров. На ассемблере эта структура будет иметь вид:

MSGSTRUCT STRUC
     MSHWND    DD ?
     MSMESSAGE DD ?
     MSWPARAM  DD ?
     MSLPARAM  DD ?
     MSTIME    DD ?
     MSPT      DD ?
MSGSTRUCT ENDS

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

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

  • Регистрация класса окон
  • Создание главного окна
  • Цикл обработки очереди сообщений
  • Процедура главного окна

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

Регистрация класса окон. Регистрация класса окон осуществляется с помощью функции RegisterClassA, единственным параметром которой является указатель на структуру WNDCLASS, содержащую информацию об окне (см. пример ниже).

Создание окна. На основе зарегистрированного класса с помощью функции CreateWindowExA (или CreateWindowA) можно создать экземпляр окна. Как можно заметить, это весьма напоминает объектную модель программирования.

Цикл обработки очереди сообщений. Вот как выглядит этот цикл на языке Си:

while (GetMessage (&msg, NULL, 0, 0))
{
// разрешить использование клавиатуры,
// путем трансляции сообщений о виртуальных клавишах
// в сообщения о алфавитно-цифровых клавишах
    TranslateMessage (&msg);
// вернуть управление Windows и передать сообщение дальше
// процедуре окна
    DispatchMessage(&msg);
}

Функция GetMessage() "отлавливает" очередное сообщение из ряда сообщений данного приложения и помещает его в структуру MSG.

Что касается функции TranslateMessage, то ее компетенция касается сообщений WM_KEYDOWN и WM_KEYUP, которые транслируются в WM_CHAR и WM_DEADCHAR, а также WM_SYSKEYDOWN и WM_SYSKEYUP, преобразующиеся в WM_SYSCHAR и WM_SYSDEADCHAR. Смысл трансляции заключается не в замене, а в отправке дополнительных сообщений. Так, например, при нажатии и отпускании алфавитно-цифровой клавиши в окно сначала придет сообщение WM_KEYDOWN, затем WM_KEYUP, а затем уже WM_CHAR.

Как можно видеть, выход из цикла ожиданий имеет место только в том случае, если функция GetMessage возвращает 0. Это происходит только при получении сообщения о выходе (сообщение WM_QUIT, см. ниже). Таким образом, цикл ожидания играет двоякую роль: определенным образом преобразуются сообщения, предназначенные для какого-либо окна, и ожидается сообщение о выходе из программы.

Процедура главного окна.

Вот прототип функции19 окна на языке С:

LRESULT CALLBACK WindowFunc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

Оставив в стороне тип возвращаемого функцией значения20, обратите внимание на передаваемые параметры. Вот смысл этих параметров: hwnd — идентификатор окна, message — идентификатор сообщения, wParam и lParam — параметры, уточняющие смысл сообщения (для каждого сообщения могут играть разные роли или не играть никаких). Все четыре параметра, как вы, наверное, уже догадались, имеют тип DWORD.

А теперь рассмотрим "скелет" этой функции на языке ассемблера.

WNDPROC PROC
     PUSH EBP
     MOV EBP, ESP ; теперь EBP указывает на вершину стека
     PUSH EBX
     PUSH ESI
     PUSH EDI
     PUSH DWORD PTR [EBP+14H]; LPARAM (lParam)
     PUSH DWORD PTR [EBP+10H]; WPARAM (wParam)
     PUSH DWORD PTR [EBP+0CH]; MES (message)
     PUSH DWORD PTR [EBP+08H]; HWND (hwnd)
     CALL DefWindowProcA@16
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP

Puc. 1.2.1. "Скелет" оконной процедуры.

Прокомментируем фрагмент на Рис. 1.2.1.

RET 16 — выход с освобождением стека от четырех параметров (16=4*4).
Доступ к параметрам осуществляется через регистр EBP:
DWORD PTR [EBP+14H]; LPARAM (lParam)
DWORD PTR [EBP+10H]; WPARAM (wParam)
DWORD PTR [EBP+0CH]; MES (message) — код сообщения
DWORD PTR [EBP+08H]; HWND (hwnd) — дескриптор окна.

Функция DefWindowProc вызывается для тех сообщений, которые не обрабатываются в функции окна. В данном примере, как Вы понимаете, не обрабатываются все приходящие в функцию окна сообщения.

19 В данной книге термины процедура и функция являются синонимами.

20 Нам это никогда не понадобится.

II

Приступим теперь к разбору конкретных примеров. На Рис. 1.2.2 представлена простая программа. Разберите ее внимательно — она будет фундаментом, на котором мы будем строить дальнейшее рассмотрение.

Прежде всего обратите внимание на директивы INCLUDELIB. В пакете MASM32 довольно много разных библиотек. Для данного примера нам понадобились две: user32.lib и kernel32.lib.

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

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

Мы отслеживаем четыре сообщения: WM_CREATE, WM_DESTROY, WM_LBUTTONDOWN, WM_RBUTTONDOWN. Сообщения WM_CREATE и WM_DESTROY в терминах объектного программирования играют роль конструктора и деструктора: они приходят в функцию окна при создании окна и при уничтожении окна. Если щелкнуть по крестику в правом углу окна, то в функцию окна придет сообщение WM_DESTROY. Далее будет выполнена функция PostQuitMessage и приложению будет послано сообщение WM_QUIT, которое вызовет выход из цикла ожидания и выполнение функции ExitProcess, что в свою очередь приведет к удалению приложения из памяти.

Обращаю Ваше внимание на метку _ERR — переход на нее происходит при возникновении ошибки, и здесь можно поместить соответствующее сообщение.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
; сообщение приходит при закрытии окна
WM_DESTROY        equ 2
; сообщение приходит при создании окна
WM_CREATE         equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN    equ 201h
; сообщение при щелчке правой кнопкой мыши в области окна
WM_RBUTTONDOWN    equ 204h
; свойства окна
CS_VREDRAW        equ 1h
CS_HREDRAW        equ 2h
CS_GLOBALCLASS    equ 4000h
WS_OVERLAPPEDWINDOW    equ 000CF0000H
style equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
; идентификатор стандартной иконки
IDI_APPLICATION   equ 32512
; идентификатор курсора
IDC_CROSS         equ 32515
; режим показа окна - нормальный
SW_SHOWNORMAL     equ 1
; прототипы внешних процедур
EXTERN   MessageBoxA@16: NEAR
EXTERN   CreateWindowExA@48:NEAR
EXTERN   DefWindowProcA@16:NEAR
EXTERN   DispatchMessageA@4:NEAR
EXTERN   ExitProcess@4:NEAR
EXTERN   GetMessageA@16:NEAR
EXTERN   GetModuleHandleA@4:NEAR
EXTERN   LoadCursorA@8:NEAR
EXTERN   LoadIconA@8:NEAR
EXTERN   PostQuitMessage@4:NEAR
EXTERN   RegisterClassA@4:NEAR
EXTERN   ShowWindow@8:NEAR
EXTERN   TranslateMessage@4:NEAR
EXTERN   UpdateWindow@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;--------------------------------------------------
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND        DD   ? ; идентификатор окна,
                          ; получающего сообщение
     MSMESSAGE     DD   ? ; идентификатор сообщения
     MSWPARAM      DD   ? ; доп. информация о сообщении
     MSLPARAM      DD   ? ; доп. информация о сообщении
     MSTIME        DD   ? ; время посылки сообщения
     MSPT          DD   ? ; положение курсора, во время посылки
                          ; сообщения
MSGSTRUCT ENDS
;---------
WNDCLASS STRUC
     CLSSTYLE      DD   ? ; стиль окна
     CLWNDPROC     DD   ? ; указатель на процедуру окна
     CLSCSEXTRA    DD   ? ; информация о доп. байтах для
                          ; данной структуры
     CLWNDEXTRA    DD   ? ; информация о доп. байтах для окна
     CLSHINSTANCE  DD   ? ; дескриптор приложения
     CLSHICON      DD   ? ; идентификатор иконы окна
     CLSHCURSOR    DD   ? ; идентификатор курсора окна
     CLBKGROUND    DD   ? ; идентификатор кисти окна
     CLMENUNAME    DD   ? ; имя-идентификатор меню
     CLNAME        DD   ? ; специфицирует имя класса окон
WNDCLASS ENDS

; сегмент данных
_DATA SEGMENT  DWORD PUBLIC USE32 'DATA'
     NEWHWND   DD 0
     MSG       MSGSTRUCT <?>
     WC        WNDCLASS  <?>
     HINST     DD 0 ; здесь хранится дескриптор приложения
     TITLENAME DB 'Простой пример 32-битного приложения',0
     CLASSNAME DB 'CLASS32',0
     CAP       DB 'Сообщение',0
     MES1      DB 'Вы нажали левую кнопку мыши',0
     MES2      DB 'Выход из программы. Пока!',0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH     0
     CALL     GetModuleHandleA@4
     MOV      [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
     MOV [WC.CLSSTYLE], style
; процедура обработки сообщений
     MOV [WC.CLWNDPROC], OFFSET WNDPROC
     MOV [WC.CLSCEXTRA], 0
     MOV [WC.CLWNDEXTRA], 0
     MOV EAX, [HINST]
     MOV [WC.CLSHINSTANCE], EAX
;--------- иконка окна
     PUSH IDI_APPLICATION
     PUSH 0
     CALL LoadIconA@8
     MOV  [WC.CLSHICON], EAX
;---------- курсор окна
     PUSH IDC_CROSS
     PUSH 0
     CALL LoadCursorA@8
     MOV  [WC.CLSHCURSOR], EAX
;---------
     MOV  [WC.CLBKGROUND], 17 ; цвет окна
     MOV DWORD PTR [WC.CLMENUNAME], 0
     MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
     PUSH OFFSET WC
     CALL RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH 0
     PUSH 400 ; DY — высота окна
     PUSH 400 ; DX - ширина окна
     PUSH 100 ; Y — координата левого верхнего угла
     PUSH 100 ; X — координата левого верхнего угла
     PUSH WS_OVERLAPPEDWINDOW
     PUSH OFFSET TITLENAME ; имя окна
     PUSH OFFSET CLASSNAME ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
; проверка на ошибку
     CMP EAX, 0
     JZ  _ERR
     MOV [NEWHWND], EAX ; дескриптор окна
; --------------------------------------------------
     PUSH SW_SHOWNORMAL
     PUSH [NEWHWND]
     CALL ShowWindow@8; показать созданное окно
; --------------------------------------------------
     PUSH [NEWHWND]
     CALL UpdateWindow@4  ; команда перерисовать видимую
                          ; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL GetMessageA@16
     CMP EAX, 0
     JE  END_LOOP
     PUSH OFFSET MSG
     CALL TranslateMessage@4
     PUSH OFFSET MSG
     CALL DispatchMessageA@4
     JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH [MSG.MSWPARAM]
     CALL ExitProcess@4
_ERR:
     JMP END_LOOP
; --------------------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014H] LPARAM
; [EBP+10H] WAPARAM
; [EBP+0CH] MES
; [EBP+8] HWND
WNDPROC  PROC
     PUSH EBP
     MOV EBP, ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH], WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH], WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH] ,WM_LBUTTONDOWN   ;левая кнопка
     JE  LBUTTON
     CMP DWORD PTR [EBP+0CH] ,WM_RBUTTONDOWN   ;правая кнопка
     JE  RBUTTON
     JMP DEFWNDPROC
; нажатие правой кнопки приводит к закрытию окна
RBUTTON:
     JMP WMDESTROY
; нажатие левой кнопки мыши
LBUTTON:
; выводим сообщение
     PUSH 0 ; МВ_ОК
     PUSH OFFSET CAP
     PUSH OFFSET MES1
     PUSH DWORD PTR [EBP+08H]
     CALL MessageBoxA@16
     MOV EAX, 0
     JMP FINISH
WMCREATE:
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA@16
     JMP FINISH
WMDESTROY:
     PUSH 0         ; МВ_ОК
     PUSH OFFSET CAP
     PUSH OFFSET MES2
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
     PUSH 0
     CALL PostQuitMessage@4 ; сообщение WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 1.2.2. Простой пример программы для Windows (MASM32).

III

После разбора программы на Рис. 1.2.2 возникает вопрос по поводу ее реализации на ассемблере TASM. В действительности здесь требуются минимальные изменения: вместо библиотек user32.lib и kernel32.lib надо подключить библиотеку import32.lib, удалить во всех именах библиотечных процедур @N и далее выполнить команды tasm32 /ml prog.asm и tlink32 -aa prog.obj.

.386P
; плоская модель
MODEL FLAT, stdcall
; константы
; сообщение приходит при закрытии окна
WM_DESTROY             equ 2
; сообщение приходит при создании окна
WM_CREATE              equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN         equ 201h
; сообщение при щелчке правой кнопкой мыши в области окна
WM_RBUTTONDOWN         equ 204h
; свойства окна
CS_VREDRAW             equ 1h
CS_HREDRAW             equ 2h
CS_GLOBALCLASS         equ 4000h
WS_OVERLAPPEDWINDOW    equ 000CF0000H
style equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
; идентификатор стандартной иконки
IDI_APPLICATION        equ 32512
; идентификатор курсора
IDC_CROSS              equ 32515
; режим показа окна — нормальный
SW_SHOWNORMAL          equ 1
; прототипы внешних процедур
EXTERN   MessageBoxA:NEAR
EXTERN   CreateWindowExA:NEAR
EXTERN   DefWindowProcA:NEAR
EXTERN   DispatchMessageA:NEAR
EXTERN   ExitProcess:NEAR
EXTERN   GetMessageA:NEAR
EXTERN   GetModuleHandleA:NEAR
EXTERN   LoadCursorA:NEAR
EXTERN   LoadIconA:NEAR
EXTERN   PostQuitMessage:NEAR
EXTERN   RegisterClassA:NEAR
EXTERN   ShowWindow:NEAR
EXTERN   TranslateMessage:NEAR
EXTERN   UpdateWindow:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\tasm32\lib\import32.lib
;--------------------------------------------------
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ? ; идентификатор окна,
                    ; получающего сообщение
     MSMESSAGE DD ? ; идентификатор сообщения
     MSWPARAM  DD ? ; доп. информация о сообщении
     MSLPARAM  DD ? ; доп. информация о сообщении
     MSTIME    DD ? ; время посылки сообщения
     MSPT      DD ? ; положение курсора, во время посылки сообщения
MSGSTRUCT ENDS
; ---------
WNDCLASS STRUC
     CLSSTYLE      DD ? ; стиль окна
     CLWNDPROC     DD ? ; указатель на процедуру окна
     CLSCEXTRA     DD ? ; информация о доп. байтах для
                        ; данной структуры
     CLWNDEXTRA    DD ? ; информация о доп. байтах для окна
     CLSHINSTANCE  DD ? ; дескриптор приложения
     CLSHICON      DD ? ; идентификатор иконы окна
     CLSHCURSOR    DD ? ; идентификатор курсора окна
     CLBKGROUND    DD ? ; идентификатор кисти окна
     CLMENUNAME    DD ? ; имя-идентификатор меню
     CLNAME        DD ? ; специфицирует имя класса окон
WNDCLASS ENDS

; сегмент данных
_DATA SEGMENT  DWORD PUBLIC USE32 'DATA'
     NEWHWND   DD 0
     MSG       MSGSTRUCT <?>
     WC        WNDCLASS  <?>
     HINST     DD 0 ; здесь хранится дескриптор приложения
     TITLENAME DB 'Простой пример 32-битного приложения',0
     CLASSNAME DB 'CLASS32',0
     CAP       DB 'Сообщение',0
     MES1      DB 'Вы нажали левую кнопку мыши',0
     MES2      DB 'Выход из программы. Пока!',0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH 0
     CALL GetModuleHandleA
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
     MOV  [WC.CLSSTYLE], style
; процедура обработки сообщений
     MOV  [WC.CLWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCSEXTRA], 0
     MOV  [WC.CLWNDEXTRA], 0
     MOV EAX, [HINST]
     MOV  [WC.CLSHINSTANCE], EAX
; ---------- иконка окна
     PUSH IDI_APPLICATION
     PUSH 0
     CALL LoadIconA
     MOV  [WC.CLSHICON], EAX
;----------- курсор окна
     PUSH IDC_CROSS
     PUSH 0
     CALL LoadCursorA
     MOV  [WC.CLSHCURSOR], EAX
; ----------
     MOV  [WC.CLBKGROUND], 17 ; цвет окна
     MOV DWORD PTR [WC.CLMENUNAME], 0
     MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
     PUSH OFFSET WC
     CALL RegisterClassA
; создать окно зарегистрированного класса
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH 0
     PUSH 400  ; DY - высота окна
     PUSH 400  ; DX - ширина окна
     PUSH 100  ; Y - координата левого верхнего угла
     PUSH 100  ; X - координата левого верхнего угла
     PUSH WS_OVERLAPPEDWINDOW
     PUSH OFFSET TITLENAME ; имя окна
     PUSH OFFSET CLASSNAME ; имя класса
     PUSH 0
     CALL CreateWindowExA
; проверка на ошибку
     CMP EAX, 0
     JZ  _ERR
     MOV  [NEWHWND], EAX
     PUSH SW_SHOWNORMAL
     PUSH [NEWHWND]
     CALL ShowWindow ; показать созданное окно
     PUSH [NEWHWND]
     CALL UpdateWindow ; команда перерисовать видимую
                       ; часть окна, сообщение WM_PAINT

; петля обработки сообщений
MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL GetMessageA
     CMP EAX, 0
     JE  END_LOOP
     PUSH OFFSET MSG
     CALL TranslateMessage
     PUSH OFFSET MSG
     CALL DispatchMessageA
     JMP MSG_LOOP
END_LOOP:

; выход из программы (закрыть процесс)
     PUSH [MSG.MSWPARAM]
     CALL ExitProcess
_ERR:
     JMP END_LOOP
;--------------------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  LPARAM
; [EBP+10H]   WAPARAM
; [EBP+0СН]   MES
; [EBP+8]     HWND
WNDPROC  PROC
     PUSH EBP
     MOV EBP, ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH], WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH], WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH], WM_LBUTTONDOWN   ; левая кнопка
     JE  LBUTTON
     CMP DWORD PTR [EBP+0CH], WM_RBUTTONDOWN   ; правая кнопка
     JE  RBUTTON
     JMP DEFWNDPROC
; нажатие правой кнопки приводит к закрытию окна
RBUTTON:
     JMP WMDESTROY
; нажатие левой кнопки мыши
LBUTTON:
; выводим сообщение
     PUSH 0  ; МВ_ОК
     PUSH OFFSET CAP
     PUSH OFFSET MES1
     PUSH DWORD PTR [EBP+08H]
     CALL MessageBoxA
     MOV EAX, 0
     JMP FINISH
WMCREATE:
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA
     JMP FINISH
WMDESTROY:
     PUSH 0         ; MB_OK
     PUSH OFFSET CAP
     PUSH OFFSET MES2
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA
     PUSH 0
     CALL PostQuitMessage ; сообщение WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 1.2.3. Простой пример программы для Windows (TASM32).

Кстати заметьте, что исполняемый модуль, транслируемый в TASM32, всегда несколько длиннее аналогичного модуля, транслируемого с помощью пакета MASM32.

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

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

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

Часть API-функций получила суффикс "А". Дело в том, что функции имеют два прототипа: с суффиксом "А" - поддерживают ANSI, а с суффиксом "W" - Unicode.

21 Собственно PostQuitMessage есть пример посылки сообщения своему приложению.

IV

Здесь мне хотелось бы рассмотреть подробнее вопрос о передачи параметров через стек.

Это не единственный способ передачи параметров, но именно через стек передаются параметры API-функциям, поэтому на это необходимо обратить внимание.

Состояния стека до и после вызова процедуры приводится на Рис. 1.2.4.

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

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

PUSH EBP
MOV EBP, ESP
SUB ESP, N   ; N - количество байт для локальных переменных.

Адрес первого параметра определяется как [EBP+8Н], что мы уже неоднократно использовали. Адрес первой локальной переменной, если она зарезервирована, определяется как [EBP-4] (имеется в виду переменная типа DWORD). На ассемблере не очень удобно использовать локальные переменные, и мы не будем резервировать для них место (смотрите, однако, Главу 2.5).

В конце процедуры идут команды:

MOV ESP, EBP
POP EBP
RET М

Здесь M - объем, взятый у стека для передачи параметров.

Такого же результата можно добиться, используя команду ENTER N,0 (PUSH EBP\MOV EBP,ESP\SUB ESP) в начале процедуры и LEAVE (MOV ESP,EBP\POP EBP) в конце процедуры.

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

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

Существуют два основных подхода (см. [1]). Условно первый подход можно назвать Си-подходом, а второй — Паскаль-подходом.

Первый подход предполагает, что процедура "не знает", сколько параметров находится в стеке. Естественно, в этом случае освобождение стека от параметров должно происходить после команды вызова процедуры, например, с помощью команды POP или команды ADD ESP,N (N - количество байт в параметрах).

Второй подход основан на том, что количество параметров фиксировано, а стек можно освободить в самой процедуре. Это достигается выполнением команды RET N (N - количество байт в параметрах). Как Вы уже, наверное, догадались, вызов функций API осуществяется по второй схеме. Впрочем, есть и исключения, о которых вы узнаете несколько позже (см. Гл. 2.1).

Рис. 1.2.4. Схема передачи параметров в процедуру


Часть I. Глава 3. Примеры простых программ на языке ассемблера

Глава 3. Примеры простых программ на языке ассемблера

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

I

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

  1. Свойства конкретного окна задаются при вызове функции CreateWindow определением параметра Style. Константы, определяющие свойства окна, содержатся в специальных файлах, которые подключаются при компиляции. Поскольку свойства фактически определяются наличием или отсутствием того или иного бита в константе, комбинация свойств — это просто сумма констант. В отличие от многих рекомендаций для разработчиков все константы здесь определяются непосредственно в программах.
  2. Окно создается на основе зарегистрированного класса. Окно может содержать элементы управления — кнопки, окна редактирования, списки, полосы прокрутки и т.д. Все эти элементы могут создаваться как окна с предопределенными классами (для кнопок "BUTTON", для окна редактирования "EDIT", для списка "LISTBOX" и т.д.).
  3. Система общается с окном, а следовательно, и с самим приложением посредством посылки сообщений. Эти сообщения должны обрабатываться процедурой окна. Программирование под Windows в значительной степени является программированием обработки таких сообщений. Сообщения генерируются системой также в случаях каких-либо визуальных событий, происходящих с окном или управляющими элементами 22; на нем. К таким событиям относятся передвижение окна или изменение его размеров, нажатие кнопки, выбор элемента в списке, передвижение курсора мыши и т.д. и т.п. Это и понятно, программа должна как-то реагировать на подобные события.
  4. Сообщение имеет код (будем обозначать его в программе MES) и два параметра (WPARAM и LPARAM). Для каждого кода сообщения придумано свое макроимя, хотя это всего лишь целое число. Например, сообщение WM_CREATE приходит один раз, когда создается окно, WM_PAINT посылается окну", если оно перерисовывается, сообщение WM_RBUTTONDOWN генерируется, если щелкнуть правой кнопкой мыши при расположении курсора мыши в области окна и т.д. Параметры сообщения могут не иметь никакого смысла либо играть уточняющую роль. Например, сообщение WM_COMMAND генерируется системой, когда что-то происходит с управляющими элементами окна. В этом случае по значению параметров можно определить, какой это элемент и что с ним произошло (LPARAM — дескриптор элемента, старшее слово WPARAM — событие, младшее слово WPARAM — обычно идентификатор ресурса см. Часть II). Можно сказать, что сообщение WM_COMMAND несет сообщение от элемента на окне.
  5. Сообщение может генерироваться не только системой, но и самой программой. Например, можно послать сообщение-команду какому-либо элементу управления (добавить элемент в список, послать строку в окно редактирования и т.п.). Иногда посылка сообщений используется как прием программирования. Например, можно придумать свои сообщения так, чтобы при их посылке программа выполнила те или иные действия. Естественно, это сообщение должно "отлавливаться" либо в процедуре какого-либо окна, либо в цикле обработки сообщений. Такой подход очень удобен, поскольку позволяет фактически осуществлять циклические алгоритмы так, чтобы возможные изменения с окном во время исполнения такого цикла сразу проявлялись на экране.

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

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

II

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

Самое важное здесь, как мы узнаем, что кнопка нажата. Все просто: в начале проверка сообщения WM_COMMAND, а затем проверяем LPARAM — здесь хранится дескриптор (уникальный номер) окна (кнопка создается как окно). В данном случае для кнопки этого уже достаточно, чтобы определить событие24.

Да, и обратите внимание на свойства кнопки, которую мы создаем как окно, — это наиболее типичное сочетание свойств, но не единственное. Например, если Вы хотите, чтобы кнопка содержала иконку, то необходимым условием для этого будет свойство BS_ICON (или BS_BITMAP).

24 Честно говоря, здесь я, читатель, немного грешен. Убедившись, что событие произошло именно с кнопкой, нам бы следовало определить, какое событие произошло, проверив старшее слово параметра WPARAM (событие BN_CLICKED=0). Не вдаваясь в подробности, замечу, что в большинстве примеров, которые мы разбираем, для кнопки этого делать не обязательно.

; файл button.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY             equ 2
; сообщение приходит при создании окна
WM_CREATE              equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN         equ 201h
WM_COMMAND             equ 111h
; свойства окна
CS_VREDRAW             equ 1h
CS_HREDRAW             equ 2h
CS_GLOBALCLASS         equ 4000h
WS_OVERLAPPEDWINDOW    equ 000CF0000H
STYLE equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
BS_DEFPUSHBUTTON       equ 1h
WS_VISIBLE             equ 10000000h
WS_CHILD               equ 40000000h
STYLBTN equ WS_CHILD + BS_DEFPUSHBUTTON + WS_VISIBLE
; идентификатор стандартной иконки
IDI_APPLICATION        equ 32512
; идентификатор курсора
IDC_ARROW              equ 32512
; режим показа окна — нормальный
SW_SHOWNORMAL          equ 1
; прототипы внешних процедур
EXTERN   MessageBoxA@16:NEAR
EXTERN   CreateWindowExA@48:NEAR
EXTERN   DefWindowProcA@16:NEAR
EXTERN   DispatchMessageA@4:NEAR
EXTERN   ExitProcess@4:NEAR
EXTERN   GetMessageA@16:NEAR
EXTERN   GetModuleHandleA@4:NEAR
EXTERN   LoadCursorA@8:NEAR
EXTERN   LoadIconA@8:NEAR
EXTERN   PostQuitMessage@4:NEAR
EXTERN   RegisterClassA@4:NEAR
EXTERN   ShowWindow@8:NEAR
EXTERN   TranslateMessage@4:NEAR
EXTERN   UpdateWindow@4:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND        DD ?
     MSMESSAGE     DD ?
     MSWPARAM      DD ?
     MSLPARAM      DD ?
     MSTIME        DD ?
     MSPT          DD ?
MSGSTRUCT ENDS

;----структура класса окон
WNDCLASS STRUC
     CLSSTYLE      DD ?
     CLWNDPROC     DD ?
     CLSCBCLSEX    DD ?
     CLSCBWNDEX    DD ?
     CLSHINST      DD ?
     CLSHICON      DD ?
     CLSHCURSOR    DD ?
     CLBKGROUND    DD ?
     CLMENNAME     DD ?
     CLNAME        DD ?
WNDCLASS ENDS

; файл button.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include button.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND       DD 0
     MSG           MSGSTRUCT <?>
     WC            WNDCLASS  <?>
     HINST         DD 0 ; дескриптор приложения
     TITLENAME     DB 'Пример - кнопка выхода',0
     CLASSNAME     DB 'CLASS32',0
     CPBUT         DB 'Выход',0 ; выход
     CLSBUTN       DB 'BUTTON',0
     HWNDBTN       DWORD 0
     CAP           DB 'Сообщение',0
     MES           DB 'Конец работы Программы',0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH 0
     CALL GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
     MOV  [WC.CLSSTYLE], STYLE
; процедура обработки сообщений
     MOV  [WC.CLWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEX], 0
     MOV  [WC.CLSCBWNDEX], 0
     MOV  EAX, [HINST]
     MOV  [WC.CLSHINST], EAX
;---------- иконка окна
     PUSH IDI_APPLICATION
     PUSH 0
     CALL LoadIconA@8
     MOV  [WC.CLSHICON], EAX
;---------- курсор окна
     PUSH IDC_ARROW
     PUSH 0
     CALL LoadCursorA@8
     MOV  [WC.CLSHCURSOR], EAX
;----------
     MOV  [WC.CLBKGROUND], 17 ; цвет окна
     MOV DWORD PTR [WC.CLMENNAME],0
     MOV DWORD PTR [WC.CLNAME],OFFSET CLASSNAME
     PUSH OFFSET WC
     CALL RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH 0
     PUSH 400  ; DY - высота окна
     PUSH 400  ; DX - ширина окна
     PUSH 100  ; Y - координата левого верхнего угла
     PUSH 100  ; X - координата левого верхнего угла
     PUSH WS_OVERLAPPEDWINDOW
     PUSH OFFSET TITLENAME ; имя окна
     PUSH OFFSET CLASSNAME ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
; проверка на ошибку
     CMP EAX, 0
     JZ  _ERR
     MOV  [NEWHWND], EAX ; дескриптор окна
;--------------------------------------------------
     PUSH SW_SHOWNORMAL
     PUSH [NEWHWND]
     CALL ShowWindow@8 ; показать созданное окно
;--------------------------------------------------
     PUSH [NEWHWND]
     CALL UpdateWindow@4 ; команда перерисовать видимую
                         ; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL GetMessageA@16
     CMP EAX, 0
     JE  END_LOOP
     PUSH OFFSET MSG
     CALL TranslateMessage@4
     PUSH OFFSET MSG
     CALL DispatchMessageA@4
     JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH [MSG.MSWPARAM]
     CALL ExitProcess@4
_ERR:
     JMP END_LOOP
;----------------------------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ;MES
; [EBP+8]     ;HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH], WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH], WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH], WM_COMMAND
     JE  WMCOMMND
     JMP DEFWNDPROC
WMCOMMND:
     MOV EAX, HWNDBTN
     CMP DWORD PTR [EBP+14H], EAX ; не кнопка ли нажата?
     JE  WMDESTROY
     MOV EAX, 0
     JMP FINISH
WMCREATE:
; создать окно-кнопку
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     PUSH 20   ; DY
     PUSH 60   ; DX
     PUSH 10   ; Y
     PUSH 10   ; X
     PUSH STYLBTN
     PUSH OFFSET CPBUT   ; имя окна
     PUSH OFFSET CLSBUTN ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
     MOV HWNDBTN, EAX ; запомнить дескриптор кнопки
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA@16
     JMP FINISH
WMDESTROY:
     PUSH 0         ; MB_OK
     PUSH OFFSET CAP
     PUSH OFFSET MES
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
     PUSH 0
     CALL PostQuitMessage@4 ; сообщение WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 1.3.1. Пример окна с кнопкой выхода.

III

Второй пример касается использования окна редактирования. Результат работы программы показан на Рис. 1.3.3, а сама программа — на Рис. 1.3.2. При нажатии кнопки "Выход" в начале появляется окно-сообщение с отредактированной строкой.

Обратите внимание на то, как осуществляется посылка сообщения окну (управляющему элементу). Для этого используют в основном две функции: SendMessage и PostMessage. Отличие их друг от друга заключается в том, что первая вызывает процедуру окна с соответствующими параметрами и ждет, когда та возвратит управление; вторая функция ставит сообщение в очередь и сразу возвращает управление.

; файл edit.inc
; константы
WM_SETFOCUS       equ 7h
; сообщение приходит при закрытии окна
WM_DESTROY        equ 2
; сообщение приходит при содании окна
WM_CREATE         equ 1
; сообщение, если что-то происходит с элементами на окне
WM_COMMAND        equ 111h
; сообщение, позволяющее послать элементу строку
WM_SETTEXT        equ 0Ch
; сообщение, позволяющее получить строку
WM_GETTEXT        equ 0Dh
; свойства окна
CS_VREDRAW        equ 1h
CS_HREDRAW        equ 2h
CS_GLOBALCLASS    equ 4000h
WS_TABSTOP        equ 10000h
WS_SYSMENU        equ 50000h
WS_OVERLAPPEDWINDOW equ 0+WS_TABSTOP+WS_SYSMENU
STYLE equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
CS_HREDRAW        equ 2h
BS_DEFPUSHBUTTON  equ 1h
WS_VISIBLE        equ 10000000h
WS_CHILD          equ 40000000h
WS_BORDER         equ 800000h
STYLBTN equ WS_CHILD+BS_DEFPUSHBUTTON+WS_VISIBLE+WS_TABSTOP
STYLEDT equ WS_CHILD+WS_VISIBLE+WS_BORDER+WS_TABSTOP
; идентификатор стандартной иконки
IDI_APPLICATION   equ 32512
; идентификатор курсора
IDC_ARROW         equ 32512
; режим показа окна — нормальный
SW_SHOWNORMAL     equ 1
; прототипы внешних процедур
EXTERN   SetFocus@4:NEAR
EXTERN   SendMessageA@16:NEAR
EXTERN   MessageBoxA@16:NEAR
EXTERN   CreateWindowExA@48:NEAR
EXTERN   DefWindowProcA@16:NEAR
EXTERN   DispatchMessageA@4:NEAR
EXTERN   ExitProcess@4:NEAR
EXTERN   GetMessageA@16:NEAR
EXTERN   GetModuleHandleA@4:NEAR
EXTERN   LoadCursorA@8:NEAR
EXTERN   LoadIconA@8:NEAR
EXTERN   PostQuitMessage@4:NEAR
EXTERN   RegisterClassA@4:NEAR
EXTERN   ShowWindow@8:NEAR
EXTERN   TranslateMessage@4:NEAR
EXTERN   UpdateWindow@4:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND        DD ?
     MSMESSAGE     DD ?
     MSWPARAM      DD ?
     MSLPARAM      DD ?
     MSTIME        DD ?
     MSPT          DD ?
MSGSTRUCT ENDS
;----структура класса окон
WNDCLASS STRUC
     CLSSTYLE      DD ?
     CLWNDPROC     DD ?
     CLSCBCLSEX    DD ?
     CLSCBWNDEX    DD ?
     CLSHINST      DD ?
     CLSHICON      DD ?
     CLSHCURSOR    DD ?
     CLBKGROUND    DD ?
     CLMENNAME     DD ?
     CLNAME        DD ?
WNDCLASS ENDS

; файл edit.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include edit.inc
; директивы копоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\iib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND  DD 0
     MSG      MSGSTRUCT <?>
     WC       WNDCLASS  <?>
     HINST         DD 0 ; дескриптор приложения
     TITLENAME DB 'Пример — окно редактирования',0
     CLASSNAME DB 'CLASS32',0
     CPBUT         DB 'Выход',0 ; выход
     CPEDT         DB ' ',0
     CLSBUTN  DB 'BUTTON',0
     CLSEDIT  DB 'EDIT',0
     HWNDBTN  DWORD 0
     HWNDEDT  DWORD 0
     CAP      DB 'Сообщение',0
     MES      DB 'Конец работы программы',0
     TEXT     DB 'Строка редактирования',0
     DB 50 DUP(0) ; продолжение буфера
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH 0
     CALL GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
     MOV [WC.CLSSTYLE], STYLE
; процедура обработки сообщений
     MOV [WC.CLWNDPROC], OFFSET WNDPROC
     MOV [WC.CLSCBCLSEX],0
     MOV [WC.CLSCBWNDEX],0
     MOV EAX, [HINST]
     MOV [WC.CLSHINST], EAX
;------—— иконка окна
     PUSH IDI_APPLICATION
     PUSH 0
     CALL LoadIconA@8
     MOV [WC.CLSHICON], EAX
;---------- курсор окна
     PUSH IDC_ARROW
     PUSH 0
     CALL LoadCursorA@8
     MOV [WC.CLSHCURSOR], EAX
;--------—
     MOV [WC.CLBKGROUND], 17  ; цвет окна
     MOV DWORD PTR [WC.CLMENNAME], 0
     MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
     PUSH OFFSET WC
     CALL RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH 0
     PUSH 150 ; DY - высота окна
     PUSH 400 ; DX - ширина окна
     PUSH 100 ; Y - координата левого верхнего угла
     PUSH 100 ; X - координата левого верхнего угла
     PUSH WS_OVERLAPPEDWINDOW
     PUSH OFFSET TITLENAME ; имя окна
     PUSH OFFSET CLASSNAME ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
; проверка на ошибку
     CMP EAX, 0
     JZ  _ERR
     MOV [NEWHWND], EAX ; дескриптор окна
     PUSH SW_SHOWNORMAL
     PUSH [NEWHWND]
     CALLShowWindow@8 ; показать созданное окно
;------------------------------------------------------------
     PUSH [NEWHWND]
     CALL UpdateWindow@4 ; команда перерисовать видимую
                         ; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL GetMessageA@16
     CMP EAX, 0
     JE  END_LOOP
     PUSH OFFSET MSG
     CALL TranslateMessage@4
     PUSH OFFSET MSG
     CALL DispatchMessageA@4
     JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH [MSG.MSWPARAM]
     CALL ExitProcess@4
_ERR:
     JMP END_LOOP
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC  PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH] ,WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH] ,WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH] ,WM_COMMAND
     JE  WMCOMMND
     JMP DEFWNDPROC
WMCOMMND:
     MOV EAX,HWNDBTN
     CMP DWORD PTR [EBP+14H],EAX
     JNE NODESTROY
; получить отредактированную строку
     PUSH OFFSET TEXT
     PUSH 150
     PUSH WM_GETTEXT
     PUSH HWNDEDT
     CALL SendMessageA@16 ; показать эту строку
     PUSH 0
     PUSH OFFSET CAP
     PUSH OFFSET TEXT
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
; на выход
     JMP WMDESTROY
NODESTROY:
     MOV EAX, 0
     JMP FINISH
WMCREATE:
; создать окно-кнопку
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     PUSH 20   ; DY
     PUSH 60   ; DX
     PUSH 10   ; Y
     PUSH 10   ; X
     PUSH STYLBTN
     PUSH OFFSET CPBUT    ; имя окна
     PUSH OFFSET CLSBUTN  ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
     MOV HWNDBTN,EAX ; запомнить дескриптор кнопки
; создать окно редактирования
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     PUSH 20   ; DY
     PUSH 350  ; DX
     PUSH 50   ; Y
     PUSH 10   ; X
     PUSH STYLEDT
     PUSH OFFSET CPEDT   ; имя окна
     PUSH OFFSET CLSEDIT ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
     MOV HWNDEDT,EAX
;--------- поместить строку в окно редактирования
     PUSH OFFSET TEXT
     PUSH 0
     PUSH WM_SETTEXT
     PUSH HWNDEDT
     CALL SendMessageA@16
;--------- установить фокус на окне редактирования
     PUSH HWNDEDT
     CALL SetFocus@4
;------------------------------------------------------------
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA@16
     JMP FINISH
WMDESTROY:
     PUSH 0         ; MB_OK
     PUSH OFFSET CAP
     PUSH OFFSET MES
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
     PUSH 0
     CALL PostQuitMessage@4 ; сообщение WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 1.3.2. Пример окна со строкой редактирования.

Рис. 1.3.3. Работа программы со строкой редактирования (Программа на Рис. 1.3.2).

IV

Наконец последний в этой главе пример — окно-список. При создании окна в список помещаются названия цветов. Если произвести двойной щелчок по цвету, то появится окно-сообщение с названием этого цвета.

Двойной щелчок по элементу списка определяется по следующей схеме: отслеживается событие, происходящее со списком, а далее по старшему слову параметра WPARAM определяется, какое событие имело место (параметр [EBP+10Н], а его старшая часть [EBP+12Н]).

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

; файл list.inc
; константы
WM_SETFOCUS       equ 7h
; сообщение приходит при закрытии окна
WM_DESTROY        equ 2
; сообщение приходит при содании окна
WM_CREATE         equ 1
; сообщение, если что-то происходит с элементами на окне
 WM_COMMAND        equ 111h
; сообщение, позволяющее послать элементу строку
WM_SETTEXT        equ 0Ch
; сообщение, позволяющее получить строку
WM_GETTEXT        equ 0Dh
; сообщение — команда добавить строку
LB_ADDSTRING      equ 180h
LB_GETTEXT        equ 189h
LB_GETCURSEL      equ 188h
LBN_DBLCLK        equ 2
; свойства окна
CS_VREDRAW        equ 1h
CS_HREDRAW        equ 2h
CS_GLOBALCLASS    equ 4000h
WS_TABSTOP        equ 10000h
WS_SYSMENU        equ 80000h
WS_THICKFRAME     equ 40000h
WS_OVERLAPPEDWINDOW    equ WS_TABSTOP + WS_SYSMENU
STYLE equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
CS_HREDRAW        equ 2h
BS_DEFPUSHBUTTON  equ 1h
WS_VISIBLE        equ 10000000h
WS_CHILD          equ 40000000h
WS_BORDER         equ 800000h
WS_VSCROLL        equ 200000h
LBS_NOTIFY        equ 1h
STYLBTN equ WS_CHILD + BS_DEFPUSHBUTTON + WS_VISIBLE + WS_TABSTOP
STYLLST equ WS_THICKFRAME + WS_CHILD + WS_VISIBLE + WS_BORDER + WS_TABSTOP + WS_VSCROLL + LBS_NOTIFY
; идентификатор стандартной иконки
IDI_APPLICATION    equ 32512
; идентификатор курсора
IDC_ARROW          equ 32512
; режим показа окна — нормальный
SW_SHOWNORMAL      equ 1
; прототипы внешних процедур
EXTERN   SetFocus@4:NEAR
EXTERN   SendMessageA@16:NEAR
EXTERN   MessageBoxA@16:NEAR
EXTERN   CreateWindowExA@48:NEAR
EXTERN   DefWindowProcA@16:NEAR
EXTERN   DispatchMessageA@4:NEAR
EXTERN   ExitProcess@4:NEAR
EXTERN   GetMessageA@16:NEAR
EXTERN   GetModuleHandleA@4:NEAR
EXTERN   LoadCursorA@8:NEAR
EXTERN   LoadIconA@8:NEAR
EXTERN   PostQuitMessage@4:NEAR
EXTERN   RegisterClassA@4:NEAR
EXTERN   ShowWindow@8:NEAR
EXTERN   TranslateMessage@4:NEAR
EXTERN   UpdateWindow@4:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND   DD ?
     MSMESSAGE DD ?
     MSWPARAM DD ?
     MSLPARAM DD ?
     MSTIME   DD ?
     MSPT     DD ?
MSGSTRUCT ENDS
;----структура класса окон
WNDCLASS STRUC
     CLSSTYLE DD ?
     CLWNDPROC DD ?
     CLSCBCLSEX    DD ?
     CLSCBWNDEX    DD ?
     CLSHINST DD ?
     CLSHICON DD ?
     CLSHCURSOR    DD ?
     CLBKGROUND    DD ?
     CLMENNAME DD ?
     CLNAME   DD ?
WNDCLASS ENDS

; файл list.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include list.inc
; директивы копоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND  DD 0
     MSG      MSGSTRUCT <?>
     WC       WNDCLASS  <?>
     HINST     DD 0 ; дескриптор приложения
     TITLENAME DB 'Пример - окно LISTBOX',0
     CLASSNAME DB 'CLASS32',0
     CPBUT    DB' Выход', 0 ; выход
     CPLST    DB ' ',0
     CLSBUTN  DB 'BUTTON',0
     CLSLIST  DB 'LISTBOX',0
     HWNDBTN  DWORD 0
     HWNDLST  DWORD 0
     CAP      DB 'Сообщение',0
     CAP1     DB 'Выбран',0
     MES      DB 'Конец работы Программы',0
; массив строк
     STR1 DB ' Красный',0
     STR2 DB 'Зеленый',0
     STR3 DB 'Синий',0
     STR4 DB 'Желтый',0
     STR5 DB 'Черный',0
     STR6 DB 'Белый',0
; указатели на строки
     PS  DWORD OFFSET STR1
         DWORD OFFSET STR2
         DWORD OFFSET STR3
         DWORD OFFSET STR4
         DWORD OFFSET STR5
         DWORD OFFSET STR6
     BUF DB 30 dup(0)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  О
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
     MOV  [WC.CLSSTYLE], STYLE
; процедура обработки сообщений
     MOV  [WC.CLWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEX], 0
     MOV  [WC.CLSCBWNDEX], 0
     MOV  EAX, [HINST]
     MOV  [WC.CLSHINST], EAX
;----——— иконка окна
     PUSH IDI_APPLICATION
     PUSH 0
     CALL LoadIconA@8
     MOV [WC.CLSHICON], EAX
;--——-— курсор окна
     PUSH IDC_ARROW
     PUSH 0
     CALL LoadCursorA@8
     MOV [WC.CLSHCURSOR] , EAX
;---------
     MOV [WC.CLBKGROUND], 17 ; цвет окна
     MOV DWORD PTR [WC.CLMENNAME], 0
     MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
     PUSH OFFSET WC
     CALL RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH 0
     PUSH 200 ; DY - высота окна
     PUSH 250 ; DX - ширина окна
     PUSH 100 ; Y - координата левого верхнего угла
     PUSH 100 ; X - координата левого верхнего угла
     PUSH WS_OVERLAPPEDWINDOW
     PUSH OFFSET TITLENAME ; имя окна
     PUSH OFFSET CLASSNAME ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
; проверка на ошибку
     CMP EAX, 0
     JZ  _ERR
     MOV [NEWHWND], EAX ; дескриптор окна
;------------------------------------------------------------
     PUSH SW_SHOWNORMAL
     PUSH [NEWHWND]
     CALL ShowWindow@8 ; показать созданное окно
;------------------------------------------------------------
     PUSH [NEWHWND]
     CALL UpdateWindow@4   ; команда перерисовать видимую
                           ; часть окна, сообщение WM PAINT
; петля обработки сообщений
MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL  GetMessageA@16
     CMP  EAX, 0
     JE   END_LOOP
     PUSH  OFFSET MSG
     CALL  TranslateMessage@4
     PUSH  OFFSET MSG
     CALL  DispatchMessageA@4
     JMP  MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH  [MSG.MSWPARAM]
     CALL  ExitProcess@4
_ERR:
     JMP   END_LOOP
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC  PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH] ,WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH] ,WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH] ,WM_COMMAND
     JE  WMCOMMND
     JMP DEFWNDPROC
WMCOMMND:
     MOV EAX,HWNDBTN
     CMP DWORD PTR [EBP+14H],EAX ; кнопка ?
; на выход ?
     JE  WMDESTROY
     MOV EAX, HWNDLST
     CMP DWORD PTR [EBP+14H],EAX ; список ?
     JNE NOLIST
; работаем со списком
     CMP WORD PTR [EBP+12H], LBN_DBLCLK
     JNE NOLIST
; двойной щелчок есть, теперь определить
; выбранную строку
; вначале индекс
     PUSH  0
     PUSH  0
     PUSH  LB_GETCURSEL
     PUSH  HWNDLST
     CALL  SendMessageA@16
; теперь сам текст
     PUSH  OFFSET BUF
     PUSH  EAX
     PUSH  LB_GETTEXT
     PUSH  HWNDLST
     CALL  SendMessageA@16
; сообщить, что выбрано
     PUSH  0
     PUSH  OFFSET CAP1
     PUSH  OFFSET BUF
     PUSH  DWORD PTR [EBP+08H]
     CALL  MessageBoxA@16
NOLIST:
     MOV  EAX, 0
     JMP  FINISH
WMCREATE:
; создать окно-кнопку
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  DWORD PTR [EBP+08H]
     PUSH  20 ; DY
     PUSH  60 ; DX
     PUSH  10 ; Y
     PUSH  10 ; X
     PUSH  STYLBTN
     PUSH  OFFSET CPBUT  ; имя окна
     PUSH  OFFSET CLSBUTN ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
     MOV  HWNDBTN, EAX ; запомнить дескриптор кнопки
;------------------------------------------------------------
; создать окно LISTBOX
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  DWORD PTR [EBP+08H]
     PUSH  90  ; DY
     PUSH  150 ; DX
     PUSH  50 ; Y
     PUSH  10 ; X
     PUSH  STYLLST
     PUSH  OFFSET CPLST  ; имя окна
     PUSH  OFFSET CLSLIST ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
     MOV  HWNDLST,EAX ; заполнить список
     PUSH  PS
     PUSH  0
     PUSH  LB_ADDSTRING
     PUSH  HWNDLST
     CALL  SendMessageA@16
     PUSH  PS+4
     PUSH  0
     PUSH  LB_ADDSTRING
     PUSH  HWNDLST
     CALL  SendMessageA@16
     PUSH  PS+8
     PUSH  0
     PUSH  LB_ADDSTRING
     PUSH  HWNDLST
     CALL  SendMessageA@16
     PUSH  PS+12
     PUSH  0
     PUSH  LB_ADDSTRING
     PUSH  HWNDLST
     CALL  SendMessageA@16
     PUSH  PS+16
     PUSH  0
     PUSH  LB_ADDSTRING
     PUSH  HWNDLST
     CALL  SendMessageA@16
     PUSH  PS+20
     PUSH  0
     PUSH LB_ADDSTRING
     PUSH HWNDLST
     CALL SendMessageA@16
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA@16
     JMP   FINISH
WMDESTROY:
     PUSH 0         ; MB_OK
     PUSH OFFSET CAP
     PUSH OFFSET MES
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
     PUSH 0
     CALL PostQuitMessage@4 ; сообщение WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Puc. 1.3.4. Пример окна с простым списком.

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

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

В таких ситуациях система посылает окну сообщение WM_PAINT. Такое же сообщение посылается окну при выполнении некоторых функций, связанных с перерисовкой окна, таких, например, как UpdateWindow.

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

Здесь, чтобы информация сохранялась в окне, нам не обойтись без обработки сообщения WM_PAINT.

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

Для того чтобы использовать их с TASM32, как и ранее, достаточно удалить из всех API-имен суффикс @N и подключить вместо USER32.LIB и KERNEL32.LIB библиотеку IMPORT32.LIB.


Часть I. Глава 4. Экскурс в 16-битное программирование

Глава 4. Экскурс в 16-битное программирование

Данная глава носит уже исторический характер. Обычно при изложении исходных данных по программированию на ассемблере под Windows начинают именно с 16-битного программирования. На мой взгляд, в данный момент оно уже настолько отошло в тень, что не может служить введением в программирование под Windows25. Поскольку у Вас вряд ли возникнет потребность написания 16-битного приложения, я ограничусь одним несложным примером. Всех интересующихся отсылаю к книгам [1,7].

Начнем с того, что рассмотрим, чем отличается 16-битное программирование на ассемблере от 32-битного программирования. Вы убедитесь, что программировать стало гораздо проще.

1. В отличие от памяти в Windows 9x, модель памяти в старой системе Windows была сегментной. Соответственно, и в программе данные, стек и код относятся к разным сегментам26. Как и в операционной системе MS DOS, регистр DS будет указывать на сегмент данных, a CS на сегмент кода.

2. В 16-битной модели адресация осуществляется по схеме сегмент/смещение. Соответственно, адрес определяется двумя 16-битными компонентами. Например, чтобы поместить в стек адрес переменной М, потребуется по крайней мере две команды PUSH DS/PUSH OFFSET M. При этом сегментный адрес должен быть помещен в ячейку с большим адресом, чем смещение (стек растет в сторону меньших адресов).

3. Для создания 16-битных приложений удобнее всего пользоваться пакетом MASM версии 6.1. Поскольку данная глава — всего лишь исторический экскурс, мы не будем останавливаться на том, как делать 16-битные приложения с помощью Турбо Ассемблера. Для компоновки приложений мы будем использовать библиотеку LIBW.LIB, которая поставлялась в пакете MASM 6.1. Особенностью вызова API-функций в данном случае будет обратный порядок помещения параметров в стек, по сравнению с тем, что было до сих пор. В данной главе, и только в ней, будет действовать принцип: СЛЕВА НАПРАВО - СВЕРХУ ВНИЗ.

4. После запуска программы, должна быть выполнена начальная инициализация. Для этого требуется выполнить три функции API: INITTASK, WAITEVENT, INITAPP. Ниже дано описание этих функций.
INITTASK — инициализирует регистры, командную строку и память. Входных параметров не требует. Вызывается первой. Возвращаемые значения регистров: AX = 1 (0 — ошибка), CX — размер стека, DI — уникальный номер для данной задачи, DX — параметр NCMDSHOW (см. ниже), ES — сегментный адрес (селектор) PSP, ES:BX — адрес командной строки, SI — уникальный номер для ранее запущенного того же приложения. В Windows 3.1 при запуске приложения несколько раз каждый раз в память загружается только часть сегментов, часть сегментов является общим ресурсом. Таким методом достигалась экономия памяти. В Windows 95 от этого отказались. Каждая запущенная задача является изолированной и независимой. В Windows 95 SI всегда будет содержать 0. Кроме того, данная процедура заполняет резервный заголовок сегмента данных. WAITEVENT — проверяет наличие событий для указанного приложения. Если событие есть, то оно удаляется из очереди. Вызов:

PUSH AX ; AX — номер приложения, если 0, то текущее.
CALL WAITEVENT

INITAPP — инициализирует очередь событий для данного приложения. Вызов:

PUSH DI ; уникальный номер задачи
CALL INITAPP

В случае ошибки данная функция возвращает 0, иначе ненулевое значение.

5. Некоторые параметры API-функций 16-битного приложения имеют размер 2 байта. В частности параметры WPARAM и HWND процедуры окна также имеют размер 2 байта. С четырехбайтными же параметрами приходится работать в два приема.

6. Наконец последнее отличие: сегмент данных в начале должен содержать резервный блок размером в 16 байт.

7. Интересно, что в 16-битном приложении мы можем пользоваться обычными прерываниями MS DOS, используя INT 21H. Надо только иметь в виду, какие функции прерывания имеют смысл для Windows.

25 Как Вы убедитесь, 16-битное программирование даже несколько сложнее 32-битного.

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

Для иллюстрации сказанного я привожу программу из моей книги [1], которую я незначительно изменил. Трансляция программы производится средствами MASM 6.1:

ML /c prog.asm
LINK prog,prog,,libw (вопрос о файле .def можно проигнорировать)

.286
.DOSSEG ; порядок сегментов согласно соглашению Microsoft
DGROUP GROUP DATA, STA
       ASSUME CS: CODE, DS: DGROUP

;прототипы внешних процедур
    EXTRN INITTASK:FAR
    EXTRN INITAPP:FAR
    EXTRN WAITEVENT:FAR
    EXTRN DOS3CALL:FAR
    EXTRN REGISTERCLASS:FAR
    EXTRN LOADCURSOR:FAR
    EXTRN GETSTOCKOBJECT:FAR
    EXTRN GETMESSAGE:FAR
    EXTRN TRANSLATEMESSAGE:FAR
    EXTRN DISPATCHMESSAGE:FAR
    EXTRN CREATEWINDOW:FAR
    EXTRN CREATEWINDOWEX:FAR
    EXTRN UPDATEWINDOW:FAR
    EXTRN SHOWWINDOW:FAR
    EXTRN POSTQUITMESSAGE:FAR
    EXTRN DEFWINDOWPROC:FAR

; шаблоны

WNDCL         STRUCT
    STYLE         DW 0 ; стиль класса окна
    LPFNWNDPROC   DD 0 ; указатель на процедуру обработки
    CBCLSEXTRA    DW 0
    CBWNDEXTRA    DW 0
    HINSTANCE     DW 0
    HICON         DW 0
    HCURSOR       DW 0
    HBRBACKGROUND DW 0
    LPSZMENUNAME  DD 0 ; указатель на строку
    LPSZCLASSNAME DD 0 ; указатель на строку
WNDCL         ENDS
;
MESSA         STRUCT
     HWND     DW ?
     MESSAGE  DW ?
     WPARAM   DW ?
     LPARAM   DD ?
     TIME     DW ?
     X        DW ?
     Y        DW ?
MESSA         ENDS

; сегмент стека
STA SEGMENT STACK 'STACK'
     DW 2000 DUP(?)
STA ENDS

; сегмент данных
DATA SEGMENT WORD 'DATA'
; в начале 16 байт — резерв, необходимый 16-ти битному
; приложению для правильной работы в среде Windows
     DWORD 0
     WORD 5
     WORD 5 DUP (0)
     HPREV    DW ?
     HINST    DW ?
     LPSZCMD  DD ?
     CMDSHOW  DW ?
; структура для создания класса
     WNDCLASS WNDCL <>
; структура сообщения
     MSG MESSA <>
;имя класса окна
     CLAS_NAME DB 'PRIVET',0
; заголовок окна
     APP_NAME DB '16-битная программа',0
; тип курсора
     CURSOR   EQU 00007F00H
; стиль окна
     STYLE    EQU 000CF0000H
; параметры окна
     XSTART   DW 100
     YSTART   DW 100
     DXCLIENT DW 300
     DYCLIENT DW 200
DATA ENDS

; сегмент кода
CODE SEGMENT WORD 'CODE'
_BEGIN:                        ; I. Начальный код
     CALL INITTASK             ; инициализировать задачу
     OR AX, AX                 ; CX - границы стека (!!! CX или AX ????)
     JZ _ERR
     MOV HPREV, SI             ; номер предыдущего прил.
     MOV HINST, DI             ; номер для новой задачи
     MOV WORD PTR LPSZCMD, BX  ; ES:BX - адрес
     MOV WORD PTR LPSZCMD+2,ES ; командной строки
     MOV CMDSHOW, DX           ; экранный параметр
     PUSH 0                    ; текущая задача
     CALL WAITEVENT            ; очистить очередь событий
     PUSH HINST
     CALL INITAPP              ; инициализировать приложения
     OR AX, AX
     JZ _ERR
     CALL MAIN                 ; запуск основной части
_TO_OS:
     MOV AH,4CH
     INT 21H                   ; выйти из программы
_ERR:
; здесь можно поставить сообщение об ошибке
     JMP SHORT _TO_OS
; основная процедура
;************************************************************
MAIN PROC
; II. Регистрация класса окна
; стиль окна NULL — стандартное окно
     MOV  WNDCLASS.STYLE, 0
; процедура обработки
     LEA  BX,WNDPROC
     MOV  WORD PTR WNDCLASS.LPFNWNDPROC, BX
     MOV  BX,CS
     MOV  WORD PTR WNDCLASS.LPFNWNDPROC+2, BX
;------------------------------------------------------------
; резервные байты в конце резервируемой структуры
     MOV  WNDCLASS.CBCLSEXTRA, 0
; резервные байты в конце структуры для каждого окна
     MOV  WNDCLASS.CBWNDEXTRA, 0
; иконка окна отсутствует
     MOV  WNDCLASS.HICON, 0
; номер запускаемой задачи
     MOV  AX, HINST
     MOV  WNDCLASS.HINSTANCE,AX
; определить номер стандартного курсора
     PUSH  0
     PUSH  DS
     PUSH  CURSOR
     CALL  LOADCURSOR
     MOV  WNDCLASS.HCURSOR, AX
; определить номер стандартного объекта
     PUSH  0    ; WHITE_BRUSH
     CALL  GETSTOCKOBJECT
; цвет фона
     MOV  WNDCLASS.HBRBACKGROUND, AX
; имя меню из файла ресурсов (отсутствует = NULL)
     MOV  WORD PTR WNDCLASS.LPSZMENUNAME, 0
     MOV  WORD PTR WNDCLASS.LPSZMENUNAME+2,0
; указатель на строку, содержащую имя класса
     LEA  BX,CLAS_NAME
     MOV  WORD PTR WNDCLASS.LPSZCLASSNAME,BX
     MOV  WORD PTR WNDCLASS.LPSZCLASSNAME+2,DS
; вызов процедуры регистрации
     PUSH DS ; указатель на
     LEA DI,WNDCLASS
     PUSH DI ; структуры WNDCLASS
     CALL REGISTERCLASS
     CMP AX,0
     JNZ _OK1
; ошибка регистрации
     RET    ; ошибка при регистрации
_OK1:
; III. Создание окна
; адрес строки-имени класса окна
     PUSH DS
     LEA BX,CLAS_NAME
     PUSH BX
; адрес строки-заголовка окна
     PUSH DS
     LEA BX,APP_NAME
     PUSH BX
; стиль окна
     MOV BX,HIGHWORD STYLE
     PUSH BX
     MOV BX,LOWWORD STYLE
     PUSH BX
; координата X левого верхнего угла
     PUSH XSTART
; координата Y левого верхнего угла
     PUSH YSTART
; ширина окна
     PUSH DXCLIENT
; высота окна
     PUSH DYCLIENT
; номер окна-родителя
     PUSH 0
; номер (идентификатор) меню окна
     PUSH 0 ; NULL
; номер задачи
     PUSH HINST
; адрес блока параметров окна (нет)
     PUSH 0
     PUSH 0
     CALL CREATEWINDOW
     CMP AX,0
     JNZ NO_NULL
; ошибка создания окна
     RET   ; ошибка при создании окна
; установка для окна состояния видимости
; (окно или пиктограмма)
; согласно параметру CMDSHOW
; и его отображение
NO_NULL:
     MOV SI,AX
     PUSH SI
     PUSH CMDSHOW
     CALL SHOWWINDOW
; посылка команды обновления области окна (команда WM_PAINT)
; сообщение посылается непосредственно окну
     PUSH SI
     CALL UPDATEWINDOW
; IV. Цикл ожидания
LOOP1:
; извлечение сообщения из очереди
     PUSH DS
     LEA BX,MSG ; указатель на структуру
     PUSH BX    ; сообщения
     PUSH 0
     PUSH 0
     PUSH 0
CALL GETMESSAGE
; проверка — не получено сообщение "выход"
     CMP AX,0
     JZ  NO_LOOP1
; перевод всех пришедших сообщений к стандарту ANSI
     PUSH DS
     LEA BX,MSG
     PUSH BX
     CALL TRANSLATEMESSAGE
; указать WINDOWS
; передать данное сообщение соответствующему окну
     PUSH DS
     LEA BX,MSG
     PUSH BX
     CALL DISPATCHMESSAGE
; замкнуть цикл (петлю)
     JMP SHORT LOOP1
NO_LOOP1:
     RET
MAIN ENDP

; процедура для заданного класса окон
; WINDOWS передает в эту процедуру параметры:
; HWND - дескриптор (номер) окна, тип WORD
; MES - номер сообщения, тип WORD
; WPARAM - дополнительная информация, тип WORD
; LPARAM - дополнительная информация, тип DWORD
WNDPROC  PROC
     PUSH BP
     MOV BP,SP
     MOV AX, [BP+0CH] ; MES - номер сообщения
     CMP AX, 2 ; не сообщение ли о закрытии WM_DESTROY
     JNZ NEXT
; передать сообщение о закрытии приложения,
; это сообщение будет принято в цикле ожидания,
; и т.о. приложение завершит свой путь
     PUSH 0
     CALL POSTQUITMESSAGE
     JMP _QUIT
NEXT:
; передать сообщение дальше WINDOWS
; своего рода правило вежливости — то,
; что не обработано процедурой обработки,
; предоставляется для обработки WINDOWS
     PUSH [BP+0EH]   ; HWND
     PUSH [BP+0CH]   ; MES - номер сообщения
     PUSH [BP+0AH]   ; WPARAM
     PUSH [BP+8]     ; HIGHWORD LPARAM
     PUSH [BP+6]     ; LOWWORD LPARAM
     CALL DEFWINDOWPROC
;************************************************************
_QUIT:
     POP BP
; вызов процедуры окна всегда дальний, поэтому RETF
     RETF 10      ; освобождаем стек от параметров
WNDPROC  ENDP
CODE ENDS
END _BEGIN

Рис. 1.4.1. Пример 16-битного приложения.

Вот и все. Сделаю еще замечание по поводу директивы .DOSSEG. Эта директива задает определенный порядок сегментов в ЕХЕ-файле. Этот порядок таков: в начале идет сегмент класса CODE, далее идут сегменты, отличные от класса CODE и не входящие в группу сегментов (GROUP), и наконец — сегменты, сгруппированные при помощи директивы GROUP. Причем в начале идут сегменты, отличные от класса BSS и класса STACK, далее — сегменты класса BSS (если есть) и последний сегмент класса STACK.

Попрощаемся с 16-битным программированием. Более в книге мы с ним не встретимся. Согласитесь теперь, что с точки зрения программирования, Windows 95 была несомненным шагом вперед. Программировать на ассемблере стало еще проще.


Часть I. Глава 5. Ассемблеры MASM и TASM

Глава 5. Ассемблеры MASM и TASM

В данной главе мы поговорим о двух конкурирующих продуктах MASM и TASM, их плюсах и минусах, общих чертах и различиях. Когда в конце 80-х я впервые "пересел" на "айбиэмки", первый вопрос, который я задал знающим людям, был об ассемблере. До этого я программировал на разных компьютерах, в основном имеющих весьма ограниченные ресурсы. Естественно, что основным языком на таких компьютерах был ассемблер 27. Мне дали MASM, кажется, это была вторая версия. Удивительно, но тогда на ассемблере я начал писать что-то типа баз данных. Проект мой не был закончен, но к ассемблеру я прикипел основательно. Потом мне попался Турбо Ассемблер версии 1.0. Он работал гораздо быстрее MASM. В дальнейшем мне приходилось использовать то один, то другой ассемблер. Как Вы, наверное, уже поняли, первая любовь оказалась сильнее, но теперь нам придется серьезно поговорить о том, что же все-таки предпочтительнее использовать при программировании в операционной системе Windows.

27 Одно время в образовании был широко распространен персональный компьютер "Ямаха", ОЗУ которого составляла всего 64 Кб (потом 128). Писать для такого компьютера, скажем, на языке Паскаль было, естественно, непозволительной роскошью.

Начнем со справочной информации о параметрах командной строки ML.EXE и TASM32.EXE. В начале рассмотрим транслятор ML.EXE.

Программа ML.EXE

Параметр | Комментарий
/? | Вывод помощи.
/AT | Создать файл в формате .СОМ. Для программирования в Windows этот ключ, естественно, бесполезен.
/Bl<linker> | Использовать альтернативный компоновщик. Предполагается автоматический запуск компоновщика.
/c | Компиляция без компоновки.
/Ср | Сохранение регистров пользовательских идентификаторов. Может использоваться для дополнительного контроля.
/Cu | Приведение всех пользовательских идентификаторов к верхнему регистру.
/Сх | Сохранение регистров пользовательских идентификаторов, объявленных PUBLIC и EXTERNAL.
/coff | Создание объектных файлов в стандарте coff. Применение обязательно.
/D<name>=[строка] | Задание текстового макроса. Очень удобен для отладки с использованием условной компиляции.
/ЕР | Листинг: текст программы с включаемыми файлами.
/F <hex> | Размер стека в байтах. Размер стека по умолчанию равен 1 Мб.
/Fe<file> | Имя исполняемого файла. Имеет смысл без параметра /с.
/Fl<file> | Создать файл листинга.
/Fm<file> | Создать map-файл. Имеет смысл без опции /с.
/Fo<file> | Задать имя объектного файла.
/Fpi | Включение кода эмулятора сопроцессора. Начиная с 486-ого микропроцессора, данный параметр потерял актуальность.
/Fr<file> | Включить ограниченную информацию браузера.
/FR<file> | Включить полную информацию браузера.
/G<c|d|z> | Использовать соглашение вызова Паскаль, Си, stdcall.
/H<number> | Установить максимальную длину внешних имен.
/I<name> | Добавить путь для inc-файлов. Допускается до 10 опций /I.
/link <opt> | Опции командной строки компоновщика. Имеет смысл без опции /с.
/nologo | Не показывать заголовочный текст компилятора.
/Sa | Листинг максимального формата.
/Sc | Включить в листинг синхронизацию.
/Sf | Листинг первого прохода.
/Sl<number> | Длина строки листинга.
/Sn | Не включать в листинг таблицу символов.
/Sp<number> | Высота страницы листинга.
/Ss<string> | Текст подзаголовка листинга.
/St<string> | Текст заголовка листинга.
/Sx | Включить в листинг фрагменты условной компиляции.
/Ta<file> | Для компилирования файлов, расширение которых не .asm.
/W<number> | Устанавливает перечень событий компиляции, трактуемые как предупреждения.
/WX | Трактовать предупреждения как ошибки.
/w | Тоже что /W0 /WX.
/X | Игнорировать путь, установленный переменной окружения INCLUDE.
/Zd | Отладочная информация состоит только из номеров строк.
/Zf | Объявить все имена PUBLIC.
/Zi | Включить полную отладочную информацию.
/Zm | Включить совместимость с MASM 5.01.
/Zp<n> | Установить выравнивание структур.
/Zs | Выполнять только проверку синтаксиса.

Программа TASM32.EXE

Параметр | Комментарий
/? или /h | Вывод помощи.
/a | Сегменты в объектном файле располагаются в алфавитном порядке.
/s | Сегменты в объектном файле расположены в порядке их описания.
/d<name>=[string] | Задание текстового макроса. Очень удобен для отладки с использованием условной компиляции.
/e | Включение кода эмуляции сопроцессора.
/r | Разрешение инструкций сопроцессора.
/i<string> | Добавить путь для inc-файлов. Синтаксис такой же, как у команды РАТН.
/j<dir> | Определяет директиву, которая будет транслироваться перед трансляцией.
/kh<number> | Задается максимальное количество идентификаторов. По умолчанию 16384.
/l | Создавать файл листинга.
/la | Показать в листинге код, вставляемый транслятором для организации интерфейса с языками высокого уровня.
/ml | Различать прописные и строчные буквы в идентификаторах.
/mx | Различать прописные и строчные буквы в идентификаторах PUBLIC и EXTERNAL.
/mu | Считать все символы в идентификаторах как прописные.
/mv<number> | Установить максимальную длину идентификатора.
/m<number> | Установка количества проходов транслятора. По умолчанию это число равно 1.
/n | Не выдавать в файле листинга таблицы идентификаторов.
/os, /o, /op, /oi | Тип объектного кода: стандартный, оверлейный, Phar Lap, IBM.
/p | Проверять наличие кода с побочными эффектами при работе в защищенном режиме.
/q | Удаление из объектного кода лишней информации.
/t | Подавление вывода всех сообщений при условном ассемблировании.
/w0, /w1, /w2 | Уровень полноты сообщений: сообщения не генерируются, сообщения генерируются.
/w-<xxx> /w+<xxx> | Генереция (+) или ее отсутствие (-) сообщений класса xxx.
/x | Включить в листинг блоки условного ассемблирования.
/z | Выводить не только сообщения об ошибках, но строку с ошибкой.
/zi | Включить в объектный код информацию для отладки.
/zd | Поместить в объектный код номера строк.
/zn | Не помещать в объектный код отладочной информации.

Программа LINK.EXE(32 bit).

В этой таблице объяснение опции помещено под строкой, содержащей эту опцию.

/ALIGN:number
Определяет выравнивание секций в линейной модели. По умолчанию 4096.

/BASE:{address|@filename,key}
Определяет базовый адрес (адрес загрузки). По умолчанию для ЕХЕ-программы адрес 0х400000, для DLL — 0х10000000.

/COMMENT:["]comment["]
Определяет комментарий, помещаемый в заголовок ЕХЕ- и DLL-файлов.

/DEBUG
Создает отладочную информацию для ЕХЕ- и DLL-файлов. Отладочная информация помещается в pdb-файл.

/DEBUGTYPE:{CV|COFF|BOTH}
CV — отладочная информация в формате Microsoft, COFF — отладочная информация в формате COFF (Common Object File Format), BOTH — создаются оба вида отладочной информации.

/DEF:filename
Определяет DEF-файл.

/DEFAULTLIB:library
Добавляет одну библиотеку к списку используемых библиотек.

/DLL
Создать DLL-файл.

/DRIVER[:{UPONLY|WDM}]
Используется для создания NT-драйвера (Kernel Mode Driver).

/ENTRY:symbol
Определяет стартовый адрес для ЕХЕ- и DLL-файлов.

/EXETYPE:DYNAMIC
Данная опция используется при создании VxD-драйвера.

/EXPORT:entryname[=internalname][,@ordinal[,NONAME]][,DATA]
Данная опция позволяет экспортировать функцию из вашей программы так, чтобы она была доступна для других программ. При этом создается import-библиотека.

/FIXED[:NO]
Данная опция фиксирует базовый адрес, определенный в опции /BASE.

/FORCE[:{MULTIPLE|UNRESOLVED}]
Позволяет создавать исполняемый файл, даже если не найдено внешнее имя или имеется несколько разных определений.

/GPSIZE:number
Определяет размер общих переменных для MIPS и Alpha платформ.

/HEAP:reserve[,commit]
Определяет размер кучи (HEAP) в байтах. По умолчанию этот размер равен одному мегабайту.

/IMPLIB:filename
Определяет имя import-библиотеки, если она создается.

/INCLUDE:symbol
Добавляет имя к таблице имен.

/INCREMENTAL:{YES|NO}
Если установлена опция /INCREMENTAL:YES, то в ЕХЕ добавляется дополнительная информация, позволяющая быстрее перекомпилировать этот файл. По умолчанию это информация не добавляется.

/LARGEADDRESSAWARE[:NO]
Указывает, что приложение оперирует адресами, большими 2 Гб.

/LIBPATH:dir
Определяет библиотеку, которая в первую очередь разыскивается компоновщиком.

/MACHINE: {ALPHA|ARM|IX86|MIPS|MIPS16|MIPSR41XX|PPC|SH3|SH4}
Определяет платформу. В большинстве случаев это делать не приходится.

/MAP[:filename]
Дает команду создания МАР-файла.

/MAPINFO:{EXPORTS|FIXUPS|LINES}
Указывает компоновщику включить соответствующую информацию в МАР-файл.

/MERGE:from=to
Объединить секцию "from" с секцией "to" и присвоить имя "to".

/NODEFAULTLIB[:library]
Игнорирует все или конкретную библиотеку.

/NOENTRY
Необходимо для создания DLL-файла.

/NOLOGO
Не выводить начальное сообщение компоновщика.

/OPT:{ICF[,iterations]|NOICF|NOREF|NOWIN98|REF|WIN98}
Определяет способ оптимизации, которую выполняет компоновщик.

/ORDER:@filename
Оптимизация программы путем вставки определенных инициализированных данных (COMDAT).

/OUT:filename
Определяет выходной файл.

/PDB: {filename|NONE}
Определить имя файла, содержащего информацию для отладки.

/PDBTYPE:{CON[SOLIDATE]|SEPT[YPES]}
Определяет тип РDВ-файла.

/PROFILE
Используется для работы с профайлером (анализатором работы программы).

/RELEASE
Помещает контрольную сумму в выходной файл.

/SECTION:name,[E][R][W][S][D][K][L][P][X]
Данная опция позволяет изменить атрибут секции.

/STACK:reserve[,commit]
Определяет размер выделяемого стека. Commit — определяет размер памяти, интерпретируемый операционной системой.

/STUB:filename
Определяет STUB-файл, запускающийся в системе MS DOS.

/SUBSYSTEM:{NATIVE|WINDOWS|CONSOLE|WINDOWSCE|POSIX}[,#[.##]]
Определяет, как запускать ЕХЕ-файл. CONSOLE — консольное приложение, WINDOWS — обычные WINDOWS-приложения, NATIVE — приложение для Windows NT, POSIX — создает приложение в POSIX-подсистеме WINDOWS NT.

/SWAPRUN:{CD|NET}
Сообщает операционной системе скопировать выходной файл в swap-файл (WINDOWS NT).

/VERBOSE[:LIB]
Заставляет выводить информацию о процессе компоновки.

/VERSION:#[.#]
Помещает информацию о версии в ЕХЕ-заголовок.

/VXD
Создать VXD-драйвер.

/WARN[:warninglevel]
Определяет количество возможных предупреждений, выдаваемых компоновщиком.

/WS:AGGRESSIVE
Несколько уменьшает скорость выполнения приложения (Windows NT). Операционная система удаляет данное приложение из памяти в случае его простоя.

Программа TLINK32.EXE.

В пакетах Borland С, начиная с 1997 года, сосуществовали две программы для компоновки объектных файлов: tlink32.exe и ilink32.exe. Опции этих программ практически совпадали, ilink32.exe - инкрементальный, или пошаговый, компоновщик. Он хранит информацию о предыдущих компоновках, что позволяет значительно ускорить весь процесс повторных трансляций. В последнее время программа tlink32.exe вообще исчезла из поставки. В дальнейшем мы не будем делать различий между этими двумя программами. Опции, помеченные (+), появились в новых версиях ilink32.exe, а опции со знаком (-), наоборот, исчезли. В новых версиях для выделения опции используется "/" вместо тире.

-m | Создать МАР-файл с информацией о сегментах и два листинга с PUBLIC-именами (в алфавитном и адресном порядке).
-s | Детальная информация о сегментах в МАР-файле.
-M | Поместить в МАР-файл сокращенные имена идентификаторов.
-c | Различать прописные и заглавные буквы в PUBLIC и EXTERNAL именах.
-Enn | Задает максимальное количество ошибок, приводящее к остановке компоновки.
-P- | Запретить паковать сегменты. Имеет смысл для 16-битных приложений (-P — разрешить).
-b:хххх | Задает базовый адрес. По умолчанию базовый адрес равен 400000Н. (+)
-B:хххх | Аналогично опции -b, но не создает таблицу настройки. Ключи —b и -B могут несколько ускорить работу программы.
-wxxx | Возможные предупреждения. Например, -w-stk игнорировать отсутствие стекового сегмента.
-Txx | -Tpx PE image(x: е=ЕХЕ, d=DLL) Тип выходного файла. Tpe - создать ЕХЕ-файл. Tpd - создать DLL-файл. Tpp - создать пакетный файл. (+)
-ax -ap -aa | -ар — создать консольное приложение, -аа — создать обычное Windows-приложение (GUI), -ad — создать 32-битный драйвер для Windows.(+)
-r | Заставляет компоновщик выдавать информацию о процессе компоновки.
-Vd.d | Помещает в ЕХЕ-заголовок ожидаемую версию Windows.
-Ud.d | Поместить в заголовок ЕХЕ-файла номер версии программы. (+)
-o | Импорт по номеру функции. (-)
-Ao:nnnn | Определяет величину выравнивания (кратно 2, минимально 16).
-Af:nnnn | Файл выравнивания.
-Sc:xxxx | Определить размер стека.
-S:xxxx | Определить размер резервного стека. Минимально 4К.
-Hc:хххх | Определить размер специальной "кучи".
-H:хххх | Определить размер резервной "кучи".
-n | Нет библиотеки "по умолчанию". (-)
-v | Поместить в исполняемый файл полную отладочную информацию. Можно использовать -v+ и -v- для селективного помещения отладочной информации в отдельные файлы.
-j | Определить путь поиска OBJ-файлов.
-L | Определить путь к LIB-библиотеке.
-x | Не создавать МАР-файл.
-Rr | Позволяет заменять или добавлять ресурсы. (+)
-d | Установить задержку загрузки библиотеки DLL. Она будет загружаться лишь при вызове входной процедуры. (+)
-Dxxxx | Поместить в PE-заголовок специальный дескриптор.
-GC | Поместить в заголовок РЕ строку (или строки). Например -GC"Hellow!". (+)
-GD | Генерировать Delphi-совместимый файл ресурсов. (+)
-GF | Установить флаг для загрузочного модуля: SWAPNET - указание для операционной системы поместить загрузочный модуль в swap-файл и загружать его оттуда, если он расположен на сетевом устройстве. SWAPCD - аналогично предыдущему флагу, но для съемных устройств. UNIPROCESSOR - приложение не должно запускаться в мультипроцессорной системе. LARGEADDRESSAWARE - приложение использует адреса больше 4 Gb. AGGRESSIVE - операционная система удаляет приложение из памяти в случае его простоя.(+)
-Gk | Заставляет компоновщик оставлять в случае ошибки те файлы, которые обычно в этом случае уничтожались.(+)
-Gl | Генерировать LIB-файл.(+)
-Gpr | Создавать пакет "времени исполнения".(+)
-Gpd | Создать пакет "времени создания".(+)
-Gn | Запретить пошаговую компиляцию.(+)
-GSstring | -GS:string=[ECIRWSDKP] Добавляет флаги к уже существующим флагам секций.(+)
-Gz | Помещает в РЕ-заголовок контрольную сумму загрузочного модуля.

На первый взгляд трудно выявить предпочтение между MASM32 и TASM32. Но все же:

  1. MASM32 несколько более богат возможностями. Я имею в виду опции командной строки.
  2. TASM32 перестал поддерживаться как отдельный продукт. В связи с этим MASM обошел конкурента по количеству содержащихся в пакете примеров, документации, библиотек и т.д.
  3. TASM32 осуществляет более сложный алгоритм вызова API-функций, а это приводит к тому, что исполняемые модули в TASM32 получаются несколько большими, чем в MASM32.

Завершая главу, приведу несколько простых примеров.

Включение в исполняемый файл отладочной информации. Если в исполняемый файл была включена отладочная информация, то фирменный отладчик позволяет работать одновременно и с текстом программы, и с дизассемблированным кодом. Это особенно удобно для языков высокого уровня, а для программ на ассемблере это весьма мощный инструмент отладки. Пусть текст программы содержится в файле PROG.ASM. Для того чтобы включить отладочную информацию в исполняемый модуль, используем при трансляции следующие ключи:

MASM:

ML /c /coff /Zd /Zi prog.asm
LINK /subsystem:windows /debug prog.obj

При этом кроме файла PROG.EXE на диске появится файл PROG.PDB, содержащий отладочные данные. Теперь для отладки следует запустить фирменный 32-битный отладчик фирмы Microsoft - Code View.

TASM:

TASM32 /ml /zi prog.asm
TLINK32 -aa -v prog.obj

В результате в модуль PROG.EXE будет включена отладочная информация. Для отладки такого модуля следует использовать 32-битный Turbo Debugger - TD32.EXE. На Рис. 1.5.1 представлено окно отладчика с отлаживаемой программой. Можно видеть, что на экране имеется и текст программы, и ее дизассемблированный код. Более подробно об отладчиках мы будем говорить в последней части книги.

Получение консольных и GUI приложений. О консольных приложениях речь еще впереди, здесь же я буду краток. Консольные приложения - это приложения, работающие с текстовым экраном, при этом они являются полнокровными 32- битными программами. О структуре консольных программ речь пойдет ниже, сейчас же заметим, что для получения консольного приложения с помощью TLINK32.EXE вместо ключа -aa следует использовать -ар. Что касается компоновщика из пакета MASM32, то здесь следует использовать ключ /subsystem:console вместо /subsystem:windows.

Рис. 1.5.1. Окно TD32.EXE с отлаживаемой программой.

Автоматическая компоновка. Транслятор ML.EXE обладает удобным свойством автоматического запуска компоновщика. Обычно мы игнорируем это свойство, используя ключ /c. Если не применять это ключ, то транслятор ML будет пытаться запустить программу LINK.EXE. Чтобы правильно провести всю трансляцию, необходимо еще указать опции компоновщика. Вот как будет выглядеть вся строка:

ML /coff prog.asm /LINK /subsystem:windows

He правда ли, удобно?


Часть II. Глава 1. Примеры простейших программ

Часть II. Более подробное описание программирования в среде Windows

Глава 1. Примеры простейших программ

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

I

Если вы только начинаете программировать под Windows, то в программе на Рис. 2.1.1 найдете много нового. Поэтому приступим к подробному разбору программы.

  1. В данной программе мы определяем цвет окна и текста через комбинацию трех цветов: красного, зеленого и синего. Цвет определяется одним 32-битным числом. В этом числе первый байт - интенсивность красного, второй байт - интенсивность зеленого, третий байт - интенсивность синего цвета. Последний байт равен нулю. Механизм получения этого числа продемонстрирован в определении константы RGBW.
  2. Цвет окна задается посредством определения кисти через функцию CreateSolidBrush.
  3. Поскольку при перерисовке окна системой посылается сообщение WM_PAINT, именно при получении этого сообщения и следует перерисовывать содержимое этого окна. В данном случае мы выводим всего лишь одну строку текста. Для того чтобы осуществить вывод информации в окно, необходимо сначала получить контекст окна (контекст устройства - Device Context). Для нас это — просто некоторое число, посредством которого осуществляется связь между приложением и окном. Обычно контекст устройства определяется посредством функции GetDC. При получении сообщения WM_PAINT контекст устройства получается посредством функции BeginPaint. Аргументом для этой функции является указатель на специальную структуру, которая у нас называется PAINTSTR и поля которой, впрочем, мы пока не используем.
  4. Текст, как Вы уже, надеюсь, поняли из текста программы, выводится посредством функции OutText. Предварительно, посредством функций SetBkColor и SetTextColor, мы определяем цвет фона и цвет букв. Цвет фона, соответственно, совпадает с цветом окна.
  5. Несколько слов о системе координат. Центр системы координат находится в левом верхнем углу, ось Y направлена вниз, ось Х - вправо. Впрочем, это общепринятый вариант для графических экранов.
  6. Еще один момент также связан с выводом текста в окно. Одним из параметров функции OutText является количество символов выводимой строки. И здесь начинается самое интересное. Определить длину строки (за минусом нулевого элемента) можно по-разному. Например, можно использовать операторы макроассемблера SIZEOF или LENGTHOF. Но вот беда, в Турбо Ассемблере этих операторов нет. Можно, конечно, решить эту проблему, поставив метку в конце строки или используя старые директивы LENGTH и SIZE. Но, как Вы, наверное, уже поняли, для того чтобы легко переходить от MASM32 к TASM32, следует как можно меньше использовать макросредства. Кроме того, раз уже мы употребляем определение строк, как это принято в Си, — естественно и определить функции для работы со строковыми переменными (см. замечание в конце главы). В данном примере мы определили функцию, которая возвращает длину строки. Не смущайтесь, что функция помещает результат в регистр EBX. Нам просто так удобнее. У функции, кроме того, есть одно очень важное преимущество перед макросредствами - она получает длину при выполнении программы, а не во время ее трансляции.

Теперь, чтобы добиться транслируемости программы на Турбо Ассемблере, нужно проделать те же манипуляции, которые мы производили раньше: убрать суффиксы @N и подключить библиотеку import32.lib.

; файл text1.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY        equ 2
; сообщение приходит при создании окна
WM_CREATE         equ 1
; сообщение приходит при перерисовке окна
WM_PAINT          equ 0FH
; свойства окна
CS_VREDRAW        equ 1h
CS_HREDRAW        equ 2h
CS_GLOBALCLASS    equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
stylcl        equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
DX0               equ 300
DY0               equ 200
; компоненты цветов
RED               equ 50
GREEN             equ 50
BLUE              equ 255
RGBW              equ (RED or (GREEN shl 8)) or (BLUE shl 16)
RGBT              equ 255 ; красный
; идентификатор стандартной иконки
IDI_APPLICATION   equ 32512
; идентификатор курсора
IDC_CROSS         equ 32515
; режим показа окна — нормальный
SW_SHOWNORMAL     equ 1
; прототипы внешних процедур
EXTERN   CreateWindowExA@48:NEAR
EXTERN   DefWindowProcA@16:NEAR
EXTERN   DispatchMessageA@4:NEAR
EXTERN   ExitProcess@4:NEAR
EXTERN   GetMessageA@16:NEAR
EXTERN   GetModuleHandleA@4:NEAR
EXTERN   LoadCursorA@8:NEAR
EXTERN   LoadIconA@8:NEAR
EXTERN   PostQuitMessage@4:NEAR
EXTERN   RegisterClassA@4:NEAR
EXTERN   ShowWindow@8:NEAR
EXTERN   TranslateMessage@4:NEAR
EXTERN   UpdateWindow@4:NEAR
EXTERN   BeginPaint@8:NEAR
EXTERN   EndPaint@8:NEAR
EXTERN   TextOutA@20:NEAR
EXTERN   GetStockObject@4:NEAR
EXTERN   CreateSolidBrush@4:NEAR
EXTERN   SetBkColor@8:NEAR
EXTERN   SetTextColor@8:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND        DD ?  ; идентификатор окна,
                         ; получающего сообщение
     MSMESSAGE     DD ?  ; идентификатор сообщения
     MSWPARAM      DD ?  ; доп. информация о сообщении
     MSLPARAM      DD ?  ; доп. информация о сообщении
     MSTIME        DD ?  ; время посылки сообщения
     MSPT          DD ?  ; положение курсора во время
                         ; посылки сообщения
MSGSTRUCT ENDS

WNDCLASS STRUC
     CLSSTYLE       DD ?  ; стиль окна
     CLSLPFNWNDPROC DD ?  ; указатель на процедуру окна
     CLSCBCLSEXTRA  DD ?  ; информация о доп. байтах для
                          ; данной структуры
     CLSCBWNDEXTRA  DD ?  ; информация о доп. байтах для окна
     CLSHINSTANCE   DD ?  ; дескриптор приложения
     CLSHICON       DD ?  ; идентификатор иконы окна
     CLSHCURSOR     DD ?  ; идентификатор курсора окна
     CLSHBRBACKGROUND  DD ? ; идентификатор кисти окна
     MENNAME           DD ? ; имя-идентификатор меню
     CLSNAME           DD ? ; специфицирует имя класса окон
WNDCLASS ENDS

PAINTSTR STRUC
     hdc    DWORD 0
     fErase DWORD 0
     left   DWORD 0
     top    DWORD 0
     right  DWORD 0
     bottom DWORD 0
     fRes   DWORD 0
     fIncUp DWORD 0
     Reserv DB 32 dup(0)
PAINTSTR ENDS

; файл text1.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
;------------------------------------------------------------
include text1.inc
; подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\gdi32.lib
;------------------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND  DD 0
     MSG      MSGSTRUCT <?>
     WC       WNDCLASS  <?>
     PNT      PAINTSTR  <?>
     HINST         DD 0
     TITLENAME     DB 'Текст в окне',0
     NAM           DB 'CLASS32',0
     XT       DWORD 30
     YT       DWORD 30
     TEXT     DB 'Текст в окне красный',0
_DATA    ENDS

; сегмент кода
_TEXT    SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
     MOV  [WC.CLSSTYLE] , stylcl
; процедура обработки сообщений
     MOV  [WC.CLSLPFNWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEXTRA],0
     MOV  [WC.CLSCBWNDEXTRA],0
     MOV  EAX, [HINST]
     MOV  [WC.CLSHINSTANCE],EAX
; иконка окна
     PUSH  IDI_APPLICATION
     PUSH  0
     CALL  LoadIconA@8
     MOV  [WC.CLSHICON], EAX
;----------курсор окна
     PUSH  IDC_CROSS
     PUSH  0
     CALL  LoadCursorA@8
     MOV  [WC.CLSHCURSOR],EAX
     PUSH  RGBW ; цвет кисти
     CALL  CreateSolidBrush@4 ; создать кисть
     MOV  [WC.CLSHBRBACKGROUND],EAX
     MOV  DWORD PTR [WC.MENNAME],0
     MOV  DWORD PTR [WC.CLSNAME], OFFSET NAM
     PUSH  OFFSET WC
     CALL  RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  0
     PUSH  DY0 ; DY0 - высота окна
     PUSH  DX0 ; DX0 - ширина окна
     PUSH  100 ; координата Y
     PUSH  100 ; координата X
     PUSH  WS_OVERLAPPEDWINDOW
     PUSH  OFFSET TITLENAME ; имя окна
     PUSH  OFFSET NAM       ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
; проверка на ошибку
     CMP  EAX, 0
     JZ   _ERR
     MOV  [NEWHWND], EAX ; дескриптор окна
;------------------------------------------------------------
     PUSH  SW_SHOWNORMAL
     PUSH  [NEWHWND]
     CALL  ShowWindow@8 ; показать созданное окно
; ————————————————————
     PUSH  [NEWHWND]
     CALL  UpdateWindow@4 ; перерисовать видимую часть окна
; петля обработки сообщений
MSG_LOOP:
     PUSH  0
     PUSH  0
     PUSH  0
     PUSH  OFFSET MSG
     CALL  GetMessageA@16
     CMP  AX, 0
     JE   END_LOOP
     PUSH  OFFSET MSG
     CALL  TranslateMessage@4
     PUSH  OFFSET MSG
     CALL  DispatchMessageA@4
     JMP  MSG_LOOP
END_LOOP: ; выход из программы (закрыть процесс)
     PUSH  [MSG.MSWPARAM]
     CALL  ExitProcess@4
_ERR:
     JMP  END_LOOP
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC    PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH], WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH], WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH], WM_PAINT
     JE  WMPAINT
     JMP DEFWNDPROC
WMPAINT:
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL BeginPaint@8
     PUSH EAX ; сохранить контекст (дескриптор)
;---------------- цвет фона = цвет окна
     PUSH RGBW
     PUSH EAX
     CALL SetBkColor@8
;---------------- контекст
     POP EAX
     PUSH EAX
;---------------- цвет текста (красный)
     PUSH RGBT
     PUSH EAX
     CALL SetTextColor@8
;---------------- контекст
     POP EAX
;---------------- вывести текст
     PUSH OFFSET TEXT
     CALL LENSTR
     PUSH EBX ; длина строки
     PUSH OFFSET TEXT ; адрес строки
     PUSH YT  ; Y
     PUSH XT  ; X
     PUSH EAX ; контекст окна
     CALL TextOutA@20
;---------------- закрыть
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL EndPaint@8
     MOV EAX, 0
     JMP FINISH
WMCREATE:
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA@16
     JMP FINISH
WMDESTROY:
     PUSH 0
     CALL PostQuitMessage@4 ; WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP

;----------- функция --------------------------
; длина строки
; [EBP+08H] - указатель на строку
LENSTR PROC
     PUSH EBP
     MOV EBP, ESP
     PUSH ESI
     MOV ESI, DWORD PTR [EBP+8]
     XOR EBX, EBX
LBL1:
     CMP BYTE PTR [ESI], 0
     JZ LBL2
     INC EBX
     INC ESI
     JMP LBL1
LBL2:
     POP ESI
     POP EBP
     RET 4
LENSTR ENDP
_TEXT ENDS
END START

Puc. 2.1.1. Пример простейшей программы с текстом.

Еще один пример с выводом текста в окно. Теперь мы усложняем свою задачу. Зададимся целью, чтобы текстовая строка все время, чтобы ни случилось с окном, была бы в его середине. Для этого необходимо знать длину строки в пикселях и размеры окна. Длина строки в пикселях определяется с помощью функции GetTextExtentPoint32, а размеры окна - с помощью функции GetWindowRect. При этом нам понадобятся структуры типа SIZET и RECT. Надеюсь, читатель понимает, как определить положение строки, если известна ее длина и размеры окна, добавлю только, что необходимо учесть высоту заголовка окна.

; файл text2.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY        equ 2
; сообщение приходит при создании окна
WM_CREATE         equ 1
; сообщение приходит при перерисовке окна
WM_PAINT          equ 0FH
; свойства окна
CS_VREDRAW        equ 1h
CS_HREDRAW        equ 2h
CS_GLOBALCLASS    equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
stylcl   equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
DX0                equ 300
DY0                equ 200
; компоненты цветов
RED               equ 80
GREEN             equ 80
BLUE              equ 255
RGBW              equ (RED or (GREEN shl 8)) or (BLUE shl 16)
RGBT equ 00FF00H ; зеленый
; идентификатор стандартной иконки
IDI_APPLICATION   equ 32512
; идентификатор курсора
IDC_CROSS         equ 32515
; режим показа окна - нормальный
SW_SHOWNORMAL     equ 1
; прототипы внешних процедур
EXTERN   CreateWindowExA@48:NEAR
EXTERN   DefWindowProcA@16:NEAR
EXTERN   DispatchMessageA@4:NEAR
EXTERN   ExitProcess@4:NEAR
EXTERN   GetMessageA@16:NEAR
EXTERN   GetModuleHandleA@4:NEAR
EXTERN   LoadCursorA@8:NEAR
EXTERN   LoadIconA@8:NEAR
EXTERN   PostQuitMessage@4:NEAR
EXTERN   RegisterClassA@4:NEAR
EXTERN   ShowWindow@8:NEAR
EXTERN   TranslateMessage@4:NEAR
EXTERN   UpdateWindow@4:NEAR
EXTERN   BeginPaint@8:NEAR
EXTERN   EndPaint@8:NEAR
EXTERN   TextOutA@20:NEAR
EXTERN   GetStockObject@4:NEAR
EXTERN   CreateSolidBrush@4:NEAR
EXTERN   SetBkColor@8:NEAR
EXTERN   SetTextColor@8:NEAR
EXTERN   GetTextExtentPoint32A@16:NEAR
EXTERN   GetWindowRect@8:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ? ; идентификатор окна, получающего сообщение
     MSMESSAGE DD ? ; идентификатор сообщения
     MSWPARAM  DD ? ; доп. информация о сообщении
     MSLPARAM  DD ? ; доп. информация о сообщении
     MSTIME    DD ? ; время посылки сообщения
     MSPT      DD ? ; положение курсора, во время
                    ; посылки сообщения
MSGSTRUCT ENDS

WNDCLASS STRUC
     CLSSTYLE       DD ? ; стиль окна
     CLSLPFNWNDPROC DD ? ; указатель на процедуру окна
     CLSCBCLSEXTRA  DD ? ; информация о доп. байтах для данной структуры
     CLSCBWNDEXTRA  DD ? ; информация о доп. байтах для окна
     CLSHINSTANCE   DD ? ; дескриптор приложения
     CLSHICON       DD ? ; идентификатор иконы окна
     CLSHCURSOR     DD ? ; идентификатор курсора окна
     CLSHBRBACKGROUND  DD ? ; идентификатор кисти окна
     MENNAME        DD ? ; имя-идентификатор меню
     CLSNAME        DD ? ; специфицирует имя класса окон
WNDCLASS ENDS

PAINTSTR STRUC
     hdc      DWORD 0
     fErase   DWORD 0
     left     DWORD 0
     top      DWORD 0
     right    DWORD 0
     bottom   DWORD 0
     fRes     DWORD 0
     fIncUp   DWORD 0
     Reserv   DB 32 dup (0)
PAINTSTR ENDS

SIZET STRUC
     X1   DWORD ?
     Y1   DWORD ?
SIZET ENDS

RECT STRUC
     L DWORD ? ; X - левого верхнего угла
     T DWORD ? ; Y - левого верхнего угла
     R DWORD ? ; X - правого нижнего угла
     B DWORD ? ; Y - правого нижнего угла
RECT ENDS

; файл text2.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
;------------------------------------------------------------
include text2.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\gdi32.lib

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND  DD 0
     MSG MSGSTRUCT <?>
     WC  WNDCLASS  <?>
     PNT PAINTSTR  <?>
     SZT SIZET     <?>
     RCT RECT      <?>
     HINST     DD 0
     TITLENAME DB 'Текст в окне',0
     NAM DB 'CLASS32',0
     XT  DWORD ?
     YT  DWORD ?
     TEXT DB 'Текст в окне зеленый',0
     CONT DWORD ?
_DATA    ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
     MOV  [WC.CLSSTYLE],stylcl
; процедура обработки сообщений
     MOV  [WC.CLSLPFNWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEXTRA], 0
     MOV  [WC.CLSCBWNDEXTRA], 0
     MOV  EAX, [HINST]
     MOV  [WC.CLSHINSTANCE], EAX
; ---------- иконка окна
     PUSH  IDI_APPLICATION
     PUSH  0
     CALL  LoadIconA@8
     MOV  [WC.CLSHICON], EAX
;---------- курсор окна
     PUSH  IDC_CROSS
     PUSH  0
     CALL  LoadCursorA@8
     MOV  [WC.CLSHCURSOR], EAX
     PUSH  RGBW ; цвет кисти
     CALL  CreateSolidBrush@4 ; создать кисть
     MOV  [WC.CLSHBRBACKGROUND], EAX
     MOV  DWORD PTR [WC.MENNAME],0
     MOV  DWORD PTR [WC.CLSNAME], OFFSET NAM
     PUSH  OFFSET WC
     CALL  RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  0
     PUSH  DY0 ; DY0 - высота окна
     PUSH  DX0 ; DX0 - ширина окна
     PUSH  100 ; координата Y
     PUSH  100 ; координата X
     PUSH  WS_OVERLAPPEDWINDOW
     PUSH  OFFSET TITLENAME ; имя окна
     PUSH  OFFSET NAM       ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
; проверка на ошибку
     CMP  EAX, 0
     JZ   _ERR
     MOV  [NEWHWND], EAX ; дескриптор окна
     PUSH  SW_SHOWNORMAL
     PUSH  [NEWHWND]
     CALL  ShowWindow@8 ; показать созданное окно
     PUSH  [NEWHWND]
     CALL  UpdateWindow@4 ; перерисовать видимую часть окна
; петля обработки сообщений
MSG_LOOP:
     PUSH  0
     PUSH  0
     PUSH  0
     PUSH  OFFSET MSG
     CALL  GetMessageA@16
     CMP  AX, 0
     JE   END_LOOP
     PUSH  OFFSET MSG
     CALL  TranslateMessage@4
     PUSH  OFFSET MSG
     CALL  DispatchMessageA@4
     JMP  MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH  [MSG.MSWPARAM]
     CALL  ExitProcess@4
_ERR:
     JMP  END_LOOP
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]   LPARAM
; [EBP+10H]    WAPARAM
; [EBP+0CH]    MES
; [EBP+8]      HWND
WNDPROC    PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH],WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH],WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH],WM_PAINT
     JE  WMPAINT
     JMP DEFWNDPROC
WMPAINT:
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL BeginPaint@8
     MOV CONT,EAX ; сохранить контекст (дескриптор)
;---------------- цвет фона = цвет окна
     PUSH RGBW
     PUSH EAX
     CALL SetBkColor@8
;---------------- цвет текста (красный)
     PUSH RGBT
     PUSH CONT
     CALL SetTextColor@8
;- вычислить длину текста в пикселях текста
     PUSH OFFSET TEXT
     CALL LENSTR
     PUSH EBX ; сохраним длину строки
     PUSH OFFSET SZT
     PUSH EBX
     PUSH OFFSET TEXT
     PUSH CONT
     CALL GetTextExtentPoint32A@16
;---------------- размер окна
     PUSH OFFSET RCT
     PUSH DWORD PTR [EBP+8]
     CALL GetWindowRect@8
;---------------- здесь вычисления координат
     MOV EAX, RCT.R
     SUB EAX, RCT.L
     SUB EAX, SZT.X1
     SHR EAX, 1 ; текст посередине
     MOV XT, EAX
     MOV EAX, RCT.B
     SUB EAX, RCT.T
     SHR EAX, 1
     SUB EAX, 25 ; учтем заголовочную часть окна
     MOV YT,EAX
; ---------------- вывести текст
; длина строки уже в стеке
     PUSH OFFSET TEXT
     PUSH YT
     PUSH XT
     PUSH CONT
     CALL TextOutA@20
;---------------- закрыть контекст
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL EndPaint@8
     MOV EAX, 0
     JMP FINISH
WMCREATE:
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH  DWORD PTR [EBP+14H]
     PUSH  DWORD PTR [EBP+10H]
     PUSH  DWORD PTR [EBP+0CH]
     PUSH  DWORD PTR [EBP+08H]
     CALL  DefWindowProcA@16
     JMP  FINISH
WMDESTROY:
     PUSH  0
     CALL  PostQuitMessage@4 ; WM_QUIT
     MOV  EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP

; длина строки
; указатель на строку в - [EBP+08H]
LENSTR PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH ESI
     MOV ESI, DWORD PTR [EBP+8]
     XOR EBX, EBX
LBL1:
     CMP BYTE PTR [ESI],0
     JZ LBL2
     INC EBX
     INC ESI
     JMP LBL1
LBL2:
     POP ESI
     POP EBP
     RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.1.2. Текстовая строка все время в середине окна.

Не могу не воспользоваться случаем и не восхититься теми возможностями, которые открывает перед программистом ассемблер. Можете передавать параметры через стек, а можете и через регистры. Хотите - сохраняйте регистры в начале процедуры, а хотите - не сохраняйте. Ассемблерный код можно совершенствовать и еще раз совершенствовать. Кстати, для любителей цепочечных (строковых) команд ниже привожу другую процедуру определения длины строки, основанную на команде микропроцессора SCAS, позволяющей осуществлять поиск нужного элемента (байта - SCASB, слова - SCASW, двойного слова - SCASD) в строке.

; длина строки - [EBP+08Н]
LENSTR PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EAX
     CLD
     MOV EDI, DWORD PTR [EBP+08H]
     MOV EBX, EDI
     MOV ECX,100 ; ограничить длину строки
     XOR AL,AL
REPNE SCASB ; найти символ 0
     SUB EDI, EBX ; длина строки, включая 0
     MOV EBX, EDI
     DEC EBX ; теперь здесь длина строки
     POP EAX
     POP EBP
     RET 4
LENSTR ENDP

II

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

LOGFONT STRUC
     LfHeight           DWORD ?
     LfWidth            DWORD ?
     LfEscapement       DWORD ?
     LfOrientation      DWORD ?
     LfWeight           DWORD ?
     Lfitalic           DB ?
     LfUnderline        DB ?
     LfStrikeOut        DB ?
     LfCharSet          DB ?
     LfOutPrecision     DB ?
     LfClipPrecision    DB ?
     LfQuality          DB ?
     LfPitchAndFamily   DB ?
     LfFaceName         DB 32 DUP(0)
LOGFONT ENDS

LfHeight - определяет высоту шрифта в логических единицах; если 0, то высота берется по умолчанию.
LfWidth - определяет ширину шрифта в логических единицах; если 0, то ширина по умолчанию.
LfEscapement - угол наклона текста в десятых долях градуса по отношению к горизонтальной оси в направлении против часовой стрелки.
LfOrientation - тоже, что и предыдущий параметр, но по отношению к отдельному символу (игнорируется в Windows 9х).
LfWeight - задает жирность шрифта (0-900).
Lfitalic - если 1, то курсив.
LfUnderline — если 1, то символы подчеркнуты.
LfStrikeOut — если 1, то символы перечеркнуты.
LfCharSet - задает множество символов шрифта, обычно определяется константой ANSI_CHARSET (=0).
LfOutPrecision - флаг точности шрифта; определяет, насколько точно созданный шрифт отвечает заданным параметрам. Возможные значения:

OUT_DEFAULT_PRECIS          = 0
OUT_STRING_PRECIS           = 1
OUT_CHARACTER_PRECIS        = 2
OUT_STROKE_PRECIS           = 3
OUT_TT_PRECIS               = 4
OUT_DEVICE__PRECIS          = 5
OUT_RASTER_PRECIS           = 6
OUT_TT_ONLY_PRECIS          = 7
OUT_OUTLINE_PRECIS          = 8
OUT_SCREEN_OUTLINE_PRECIS   = 9

LfClipPrecision - флаг точности прилегания шрифта; определяет, как будут отсекаться части шрифта, не попадающие в видимую область. Возможные значения:

CLIP_DEFAULT_PRECIS         = 0
CLIP_CHARACTER_PRECIS       = 1
CLIP_STROKE_PRECIS          = 2
CLIP_MASK                   = 0FH
CLIP_LH_ANGLES              = (1 SHL 4)
CLIP_TT_ALWAYS              = (2 SHL 4)
CLIP_EMBEDDED               = (8 SHL 4)

LfQuality - флаг качества шрифта; определяет соответствие логического шрифта и шрифта, допустимого данным устройством. Возможные значения:

DEFAULT_QUALITY             = 0
DRAFT_QUALITY               = 1
PROOF_QUALITY               = 2

LfPitchAndFamily - определяет тип и семейство шрифта. Возможные значения определяются комбинацией (ИЛИ) двух групп констант:

DEFAULT_PITCH               = 0
FIXED_PITCH                 = 1
VARIABLE_PITCH              = 2

и
FF_DONTCARE                 = 0
FF_ROMAN                    = (1 SHL 4)
FF_SWISS                    = (2 SHL 4)
FF_MODERN                   = (3 SHL 4)
FF_SCRIPT                   = (4 SHL 4)
FF_DECORATIVE               = (5 SHL 4)

LfFaceName - содержит название шрифта. Длина имени не может превосходить 32 символа.

Обратимся к примеру задания своего шрифта (результат работы программы - на Рис. 2.1.5). Однако поскольку большая часть программы будет совпадать с аналогичной частью предыдущих программ, я приведу здесь только необходимые фрагменты. Рассмотрим сначала фрагмент, выполняющийся при получении сообщения WM_PAINT (Рис.2.1.3).

WMPAINT:
;------- определить контекст
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL BeginPaint@8
     MOV CONT,EAX ; сохранить контекст (дескриптор)
;------- цвет фона = цвет окна
     PUSH RGBW
     PUSH EAX
     CALL SetBkColor@8
;------- цвет текста (красный)
     PUSH RGBT
     PUSH CONT
     CALL SetTextColor@8
;------ здесь определение координат
     MOV XT, 120
     MOV YT, 140
;------ задать (создать) шрифт
     MOV lg.LfHeight,12   ; высота фонта
     MOV lg.LfWidth, 9    ; ширина фонта
     MOV lg.LfEscapement,900 ; ориентация
     MOV lg.LfOrientation, 0 ; вертикальная
     MOV lg.LfWeight,400   ; толщина линий шрифта
     MOV lg.LfItalic, 0    ; курсив
     MOV lg.LfUnderline, 0 ; подчеркивание
     MOV lg.LfStrikeOut, 0 ; перечеркивание
     MOV lg.LfCharSet, 0   ; набор шрифтов
     MOV lg.LfOutPrecision, 0
     MOV lg.LfClipPrecision, 0
     MOV lg.LfQuality,2
     MOV lg.LfPitchAndFamily,0
     PUSH OFFSET lg
; задать название шрифта
     PUSH OFFSET NFONT
     PUSH OFFSET lg.LfFaceName
     CALL COPYSTR
     CALL CreateFontIndirectA@4
;------ выбрать созданный объект
     PUSH EAX
     PUSH CONT
     CALL SelectObject@8
     PUSH EAX
;------ вычислить длину текста в пикселях текста
     PUSH OFFSET TEXT
     CALL LENSTR
;---------- вывести текст ----------------
     PUSH EBX
     PUSH OFFSET TEXT
     PUSH YT
     PUSH XT
     PUSH CONT
     CALL TextOutA@20
; удалить объект "FONT"
; идентификатор уже в стеке
     CALL DeleteObject@4
;---------------- закрыть контекст
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL EndPaint@8
     MOV EAX, 0
     JMP FINISH

Рис. 2.1.3. Фрагмент программы, выводящей текст с заданным шрифтом (см. Рис. 2.1.5).

Как видно из фрагмента, создание шрифта производится по следующей схеме: надо задать шрифт при помощи функции CreateFontIndirect, выбрать шрифт функцией SelectObject, вывести текст заданным шрифтом, удалить созданный шрифт (объект). Поле LfFaceName структуры LOGFONT должно содержать название шрифта. Если такого шрифта нет, выводится шрифт по умолчанию. Название шрифта у нас задано в строке NFONT, и мы копируем его в поле LfFaceName при помощи функции COPYSTR, текст которой приводится на Рис. 2.1.4.

; процедура копирования одной строки в другую
; строка, куда копировать [EBP+08H]
; строка, что копировать [EBP+0CH]
COPYSTR PROC
     PUSH EBP
     MOV EBP,ESP
     MOV ESI, DWORD PTR [EBP+0CH]
     MOV EDI, DWORD PTR [EBP+08H]
L1:
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EDI],AL
     CMP AL,0
     JE L2
     INC ESI
     INC EDI
     JMP L1
L2:
     POP EBP
     RET 8
COPYSTR ENDP

Рис. 2.1.4. Процедура копирования одной строки в другую.

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

Рис. 2.1.5. Вывод текста под углом 90 градусов.

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

Если кто-то программировал для операционной системы MS DOS, то там подобная проблема также возникает. Решается она следующим образом: используется фоновая видеостраница, на которую выводится вся информация. Затем фоновая страница копируется на видимую страницу. При этом создается впечатление, что информация появляется на экране мгновенно. В качестве фоновой страницы используется как область ОЗУ, так и область видеопамяти.

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

1. При создании окна:

  1. Создается совместимый контекст устройства.
    Функция CreateCompatibleDC. Полученный контекст следует запомнить.
  2. Создается карта бит, совместимая с данным контекстом.
    Функция CreateCompatibleBitmap.
  3. Выбирается кисть цветом, совпадающим с цветом основного окна.
  4. Создается битовый шаблон путем выполнения растровой операции с использованием выбранной кисти. Функция PatBlt.

2. Вся информация выводится в виртуальное окно и дается команда перерисовки окна. Функция InvalidateRect.

3. При получении сообщения WM_PAINT содержимое виртуального окна копируется на реальное окно. Функция BitBlt. Изложенная теория будет применена на практике в следующем разделе.

III

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

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

2. Цвет рисования образуется тремя способами. При использовании функции SetPixel задается цвет данной точки. Для линий необходимо задать цвет пера. Для задания цвета замкнутых графических объектов следует задать цвет кисти.

3. Перо создается при помощи функции CreatePen, кисть - при помощи CreateSolidBrush (мы ее уже использовали). Для создания разноцветной картинки можно заранее создать несколько кистей и перьев, а затем в нужный момент выбирать при помощи функции SelectObject (мы также уже использовали эту функцию).

4. Для рисования можно использовать следующие функции API:

  1. SetPixel - установить заданный цвет пикселя.
  2. LineTo - провести линию от текущей точки до точки с заданными координатами, которая в свою очередь становится текущей.
  3. MoveToEx - сменить текущую точку.
  4. Arc - рисование дуги.
  5. Rectangle - нарисовать прямоугольник.
  6. RoundRect - нарисовать прямоугольник с округленными углами.
  7. Ellipse, Pie - рисование эллипсов и секторов эллипсов.

5. Если при рисовании замкнутой фигуры был установлен цвет кисти, отличный от цвета основного фона, то замкнутая фигура окрашивается этим цветом.

6. Для установки соотношения между логическими единицами и пикселями используется функция SetMapMode.

7. Можно установить область вывода при помощи функции SetViewportExtEx. С помощью функции SetViewportOrgEx можно задать начало области ввода.

После всего сказанного пора продемонстрировать программу. Программа достаточно проста, но в ней заложены основы работы с графикой. По щелчку левой кнопки мыши сначала появляется горизонтальная линия, по второму щелчку - наклонная линия, по третьему щелчку - заполненный прямоугольник. Программа показана на Рис. 2.1.6, результат ее работы - на Рис. 2.1.7.

; файл graph1.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY    equ 2
; сообщение приходит при создании окна
WM_CREATE     equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN   equ 201h
; сообщение приходит при перерисовке окна
WM_PAINT      equ 0FH
; свойства окна
CS_VREDRAW    equ 1h
CS_HREDRAW    equ 2h
CS_GLOBALCLASS   equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
stylcl equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
DX0   equ 600
DY0   equ 400
; компоненты цветов
RGBW   equ (50 or (50 shl 8)) or (255 shl 16) ; цвет окна
RGBR   equ 150 ; цвет региона
RGBL   equ 0 ; цвет линии
RGBP   equ 255 or (100 shl 8) ; цвет точки
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_CROSS     equ 32515
; режим показа окна - нормальный
SW_SHOWNORMAL   equ 1
; прототипы внешних процедур
EXTERN      CreateWindowExA@48:NEAR
EXTERN      DefWindowProcA@16:NEAR
EXTERN      DispatchMessageA@4:NEAR
EXTERN      ExitProcess@4:NEAR
EXTERN      GetMessageA@16:NEAR
EXTERN      GetModuleHandleA@4:NEAR
EXTERN      LoadCursorA@8:NEAR
EXTERN      LoadIconA@8:NEAR
EXTERN      PostQuitMessage@4:NEAR
EXTERN      RegisterClassA@4:NEAR
EXTERN      ShowWindow@8:NEAR
EXTERN      TranslateMessage@4:NEAR
EXTERN      UpdateWindow@4:NEAR
EXTERN      BeginPaint@8:NEAR
EXTERN      EndPaint@8:NEAR
EXTERN      GetStockObject@4:NEAR
EXTERN      CreateSolidBrush@4:NEAR
EXTERN      GetSystemMetrics@4:NEAR
EXTERN      GetDC@4:NEAR
EXTERN      CreateCompatibleDC@4:NEAR
EXTERN      SelectObject@8:NEAR
EXTERN      CreateCompatibleBitmap@12:NEAR
EXTERN      PatBlt@24:NEAR
EXTERN      BitBlt@36:NEAR
EXTERN      ReleaseDC@8:NEAR
EXTERN      DeleteObject@4:NEAR
EXTERN      InvalidateRect@12:NEAR
EXTERN      GetStockObject@4:NEAR
EXTERN      DeleteDC@4:NEAR
EXTERN      CreatePen@12:NEAR
EXTERN      SetPixel@16:NEAR
EXTERN      LineTo@12:NEAR
EXTERN      MoveToEx@16:NEAR
EXTERN      Rectangle@20:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND      DD ? ; идентификатор окна, получающего сообщение
     MSMESSAGE   DD ? ; идентификатор сообщения
     MSWPARAM    DD ? ; доп. информация о сообщении
     MSLPARAM    DD ? ; доп. информация о сообщении
     MSTIME      DD ? ; время посылки сообщения
     MSPT        DD ? ; положение курсора во время
                      ; посылки сообщения
MSGSTRUCT ENDS

WNDCLASS STRUC
     CLSSTYLE       DD ? ; стиль окна
     CLSLPFNWNDPROC DD ? ; указатель на процедуру окна
     CLSCBCLSEXTRA  DD ? ; информация о доп. байтах для
                         ; данной структуры

     CLSCBWNDEXTRA  DD ? ; информация о доп. байтах для окна
     CLSHINSTANCE   DD ? ; дескриптор приложения
     CLSHICON       DD ? ; идентификатор иконы окна
     CLSHCURSOR     DD ? ; идентификатор курсора окна
     CLSHBRBACKGROUND DD ? ; идентификатор кисти окна
     MENNAME        DD ? ; имя-идентификатор меню
     CLSNAME        DD ? ; специфицирует имя класса окон
WNDCLASS ENDS

PAINTSTR STRUC
     hdc    DD 0
     fErase DD 0
     left   DD 0
     top    DD 0
     right  DD 0
     bottom DD 0
     fRes   DD 0
     fIncUp DD 0
     Reserv DB 32 dup(0)
PAINTSTR ENDS

RECT STRUC
     L   DD ? ; Х-левого верхнего угла
     T   DD ? ; Y-левого верхнего угла
     R   DD ? ; Х- правого нижнего угла
     B   DD ? ; Y- правого нижнего угла
RECT ENDS

; файл graph.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
;------------------------------------------------------------------
include graph1.inc
; подключения библиотек
includelib c:\masm32\iib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\gdi32.lib
;------------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND DWORD 0
     MSG   MSGSTRUCT <?>
     WC    WNDCLASS  <?>
     PNT   PAINTSTR  <?>
     HINST  DWORD 0
     TITLENAME BYTE 'Графика в окне',0
     NAM   BYTE 'CLASS32',0
     XT    DWORD 30
     YT    DWORD 30
     XM    DWORD ?
     YM    DWORD ?
     HDC   DWORD ?
     MEMDC  DWORD ?
     HPEN   DWORD ?
     HBRUSH DWORD ?
     P      DWORD 0 ; признак вывода
     XP     DWORD ?
     YP     DWORD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
     MOV  [WC.CLSSTYLE],stylcl
; процедура обработки сообщений
     MOV  [WC.CLSLPFNWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEXTRA],0
     MOV  [WC.CLSCBWNDEXTRA], 0
     MOV  EAX, [HINST]
     MOV  [WC.CLSHINSTANCE], EAX
; ----------иконка окна
     PUSH  IDI_APPLICATION
     PUSH  0
     CALL  LoadIconA@8
     MOV  [WC.CLSHICON], EAX
; ----------курсор окна
     PUSH  IDC_CROSS
     PUSH  0
     CALL  LoadCursorA@8
     MOV  [WC.CLSHCURSOR], EAX
;-----------
     PUSH  RGBW ; цвет кисти
     CALL  CreateSolidBrush@4 ; создать кисть
     MOV  [WC.CLSHBRBACKGROUND], EAX
     MOV  DWORD PTR [WC.MENNAME],0
     MOV  DWORD PTR [WC.CLSNAME], OFFSET NAM
     PUSH  OFFSET WC
     CALL  RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  0
     PUSH  DY0 ; DYO - высота окна
     PUSH  DX0 ; DXO - ширина окна
     PUSH  100 ; координата Y
     PUSH  100 ; координата X
     PUSH  WS_OVERLAPPEDWINDOW
     PUSH  OFFSET TITLENAME ; имя окна
     PUSH  OFFSET NAM    ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
; проверка на ошибку
     CMP  EAX, 0
     JZ   _ERR
     MOV  [NEWHWND], EAX ; дескриптор окна
;------------------------------------------------------------
     PUSH  SW_SHOWNORMAL
     PUSH  [NEWHWND]
     CALL  ShowWindow@8 ; показать созданное окно
;------------------------------------------------------------
     PUSH  [NEWHWND]
     CALL  UpdateWindow@4 ; перерисовать видимую часть окна
; петля обработки сообщений
MSG_LOOP:
     PUSH  0
     PUSH  0
     PUSH  0
     PUSH  OFFSET MSG
     CALL  GetMessageA@16
     CMP  AX, 0
     JE   END_LOOP
     PUSH  OFFSET MSG
     CALL  TranslateMessage@4
     PUSH  OFFSET MSG
     CALL  DispatchMessageA@4
     JMP  MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH  [MSG.MSWPARAM]
     CALL  ExitProcess@4
_ERR:
     JMP  END_LOOP
;------------------------------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC    PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH],WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH],WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH],WM_PAINT
     JE  WMPAINT
     CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN
     JE  LBUTTON
     JMP DEFWNDPROC
LBUTTON:
     CMP P,0
     JNE F1
; линия точками (горизонтальная)
     MOV YP,50 ; Y
     MOV XP,10 ; X
     MOV ECX,200
LL:
     PUSH ECX
     PUSH RGBP
     PUSH YP
     PUSH XP
     PUSH MEMDC
     CALL SetPixel@16
     INC XP
     POP ECX
     LOOP LL
     INC P
     JMP F3
F1:
     CMP P,1
     JNE F2
; вначале установим текущие координаты на конец
; предыдущей линии
     PUSH 0
     PUSH YP
     PUSH XP
     PUSH MEMDC
     CALL MoveToEx@16
; линия пером
     PUSH 300
     PUSH 550
     PUSH MEMDC
     CALL LineTo@12
     INC P
     JMP F3
F2:
     CMP P,2
     JNE FIN
; замкнутая фигура - прямоугольник
; вначале выбрать кисть для заполнения области
     PUSH HBRUSH
     PUSH MEMDC
     CALL SelectObject@8
; теперь рисуем заполненный прямоугольник если не выбирать
; кисть, то будет нарисован незаполненный прямоугольник
     PUSH 350
     PUSH 400
     PUSH 200
     PUSH 200
     PUSH MEMDC
     CALL Rectangle@20
     INC P
F3:
; дать команду перерисовать окно
     PUSH 0
     PUSH OFFSET RECT
     PUSH DWORD PTR [EBP+08H]
     CALL InvalidateRect@12
FIN:
     MOV EAX, 0
     JMP FINISH
WMPAINT:
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL BeginPaint@8
     MOV HDC,EAX ; сохранить контекст (дескриптор)
; скопировать виртуальное окно на реальное
     PUSH 0CC0020h ; SRCCOPY=изображение как есть
     PUSH 0  ; у-источника
     PUSH 0  ; х-источника
     PUSH MEMDC ; контекст источника
     PUSH YM  ; высота - куда
     PUSH XM  ; ширина - куда
     PUSH 0   ; у - куда
     PUSH 0   ; х-куда
     PUSH HDC ; контекст - куда
     CALL BitBlt@36
;---------------- закрыть контекст окна
     PUSH OFFSET PNT
     PUSH DWORD PTR [EBP+08H]
     CALL EndPaint@8
     MOV EAX, 0
     JMP FINISH
WMCREATE:
; размеры экрана
     PUSH 0 ; X
     CALL GetSystemMetrics@4
     MOV XM, EAX
     PUSH 1 ; Y
     CALL GetSystemMetrics@4
     MOV YM, EAX
; открыть контекст окна
     PUSH DWORD PTR [EBP+08H]
     CALL GetDC@4
     MOV HDC,EAX
;  создать совместимый с данным окном контекст
     PUSH EAX
     CALL CreateCompatibleDC@4
     MOV MEMDC, EAX
; создать в памяти растровое изображение, совместимое с hdc
     PUSH YM
     PUSH XM
     PUSH HDC
     CALL CreateCompatibleBitmap@12
; выбрать растровое изображение в данном контексте
     PUSH EAX
     PUSH MEMDC
     CALL SelectObject@8
; цвет кисти
     PUSH RGBW
     CALL CreateSolidBrush@4  ; создать кисть
; выбрать кисть в данном контексте
     PUSH EAX
     PUSH MEMDC
     CALL SelectObject@8
; заполнить данную прямоугольную область
     PUSH 0F00021h ; РАТСОРУ=заполнить данным цветом
     PUSH YM
     PUSH XM
     PUSH 0
     PUSH 0
     PUSH MEMDC
     CALL PatBlt@24
; создать кисть и перо для рисования
; цвет кисти
     PUSH  RGBR
     CALL  CreateSolidBrush@4 ; создать кисть
     MOV  HBRUSH,EAX
; задать перо
     PUSH  RGBR ; цвет
     PUSH  0  ; толщина=1
     PUSH  0  ; сплошная линия
     CALL CreatePen@12
     MOV HPEN, EAX
; удалить контекст
     PUSH HDC
     PUSH DWORD PTR [EBP+08H]
     CALL ReleaseDC@8
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA@16
     JMP FINISH
WMDESTROY:
; удалить перо
     PUSH HPEN
     CALL DeleteDC@4
; удалить кисть
     PUSH HBRUSH
     CALL DeleteDC@4
; удалить виртуальное окно
     PUSH MEMDC
     CALL DeleteDC@4
; выход
     PUSH 0
     CALL PostQuitMessage@4 ; WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Puc. 2.1.6. Простая программа для демонстрации графики.

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

  1. Загрузить растровое изображение и запомнить его дескриптор.
  2. Получить контекст устройства для области памяти, где будет храниться изображение.
  3. Выбрать изображение в данном контексте.
  4. Скопировать изображение на экран (BitBlt).

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

Рис. 2.1.7. Результат работы программы на Рис. 2.1.6.

Замечание. Специалисту в Windows-программировании, возможно, показалось странным, что мы пишем свои собственные строковые функции, вместо того чтобы воспользоваться существующими в Windows соответствующими API-функциями. Да-да, такие функции существуют, дорогой читатель. Причина моего пренебрежения этими функциями проста. Во-первых, я рассчитываю не только на "продвинутых" читателей, но и на людей, обучающихся программированию на ассемблере. Во-вторых, как я уже говорил в предисловии, данная книга - это попытка создания некоторого симбиоза ассемблера и программирования в Windows. Следуя этому принципу, мы не всегда будем решать задачи только средствами API-функций. Однако, понимая всю важность строковых API-функций, в свое время я приведу примеры их использования. Кроме того, в приложении будет дано их полное описание.


Часть II. Глава 2. Консольные приложения

Глава 2. Консольные приложения

I

Консольные программы - что это? О, это для тех, кто любит работать с командной строкой. Самая знаменитая консольная программа - это Far. А на первый взгляд кажется, что работаешь с DOS-программой, не правда ли? Но дело ведь не только в любви к текстовому режиму. Часто нет необходимости и времени для создания графического интерфейса, а программа должна что-то делать, например, обрабатывать большие объемы информации. И вот тут на помощь приходят консольные приложения. Ниже Вы увидите, что консольные приложения очень компактны не только в откомпилированном виде, но и в текстовом варианте. Но главное, консольное приложение имеет такие же возможности обращаться к ресурсам Windows посредством API-функций, как и обычное графическое приложение.

Надо сказать, что в книге автора "Assembler. Учебный курс" [1] излагается несколько экзотический способ трансляции консольных приложений, но связано это было с отсутствием у меня в то время нового инструментария. В данной книге мы используем MASM32 6.14 и TASM32 5.0. Здесь все достаточно просто.

Для MASM:

ml /с /coff cons1.asm

link /subsystem:console cons1.obj

Для TASM32:

TASM32 /ml cons1.asm

tlink32 /ap cons1.obj

Как и раньше, мы предполагаем, что библиотеки будут указываться при помощи директивы includelib. Ниже на Рис. 2.2.1 и Рис. 2.2.2 представлено простое консольное приложение для MASM и TASM соответственно. Для вывода текстовой информации используется функция API WriteConsoleA, параметры которой (слева направо) имеют следующий смысл.

  • 1-й параметр - дескриптор буфера вывода консоли, который может быть получен при помощи функции GetStdHandle.
  • 2-й параметр - указатель на буфер, где находится выводимый текст.
  • 3-й параметр - количество выводимых символов.
  • 4-й параметр - указывает на переменную DWORD, куда будет помещено количество действительно выведенных символов.
  • 5-й параметр - резервный параметр, должен быть равен нулю.

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

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
; прототипы внешних процедур
EXTERN  GetStdHandle@4:NEAR
EXTERN  WriteConsoleA@20:NEAR
EXTERN  ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
; строка в DOS-овской кодировке
     STR1 DB "Консольное приложение",0
     LENS DD ? ; количество выведенных символов
     RES  DD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
; длина строки
     PUSH OFFSET STR1
     CALL LENSTR
; вывести строку
     PUSH OFFSET RES  ; резерв
     PUSH OFFSET LENS ; выведено символов
     PUSH EBX         ; длина строки
     PUSH OFFSET STR1 ; адрес строки
     PUSH EAX         ; HANDLE вывода
     CALL WriteConsoleA@20
     PUSH 0
     CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EAX
;------------------------------
     CLD
     MOV EDI, DWORD PTR [EBP+08H]
     MOV EBX,EDI
     MOV ECX,100 ; ограничить длину строки
     XOR AL,AL
     REPNE SCASB ; найти символ 0
     SUB EDI,EBX ; длина строки, включая 0
     MOV EBX,EDI
     DEC EBX
;------------------------------
     POP EAX
     POP EBP
     RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.1. Простое консольное приложение для MASM32.

.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
STD_OUTPUT_HANDLE equ -11

; прототипы внешних процедур
EXTERN  GetStdHandle:NEAR
EXTERN  WriteConsoleA:NEAR
EXTERN  ExitProcess:NEAR

; директивы компоновщику для подключения библиотек
includelib c:\tasm32\lib\import32.lib

;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
; строка в DOS-овской кодировке
     STR1 DB "Консольное приложение",0
     LENS DD ?
; количество выведенных символов
     RES DD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle
; длина строки
     PUSH OFFSET STR1
     CALL LENSTR
; вывести строку
     PUSH OFFSET RES  ; резерв
     PUSH OFFSET LENS ; выведено символов
     PUSH EBX         ; длина строки
     PUSH OFFSET STR1 ; адрес строки
     PUSH EAX         ; HANDLE вывода
     CALL WriteConsoleA
     PUSH 0
     CALL ExitProcess
; строка - [EBP+08H]
; длина в EBX

LENSTR PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EAX
; ----------
     CLD
     MOV EDI, DWORD PTR [EBP+08H]
     MOV EBX,EDI
     MOV ECX,100 ; ограничить длину строки
     XOR AL,AL
     REPNE SCASB  ; найти символ 0
     SUB EDI, EBX ; длина строки, включая 0
     MOV EBX,EDI
     DEC EBX
; ----------
     POP EAX
     POP EBP
     RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.2. Простое консольное приложение для TASM32.

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

Прокомментируем теперь приведенные выше программы. При запуске их из командной строки, например из Far'a, в строку выводится сообщение "Консольное приложение". При запуске программы как Windows-приложения консольное окно появляется лишь на секунду. В чем тут дело? Дело в том, что консольные приложения могут создать свою консоль. В этом случае весь ввод-вывод будет производиться в эту консоль. Если же приложение консоль не создает, то здесь может возникнуть двоякая ситуация: либо наследуется консоль, в которой программа была запущена, либо Windows создает для приложения свою консоль.

II

Рассмотрим несколько простых консольных функций и их применение. Во-первых, работать с чужой консолью не всегда удобно. А для того чтобы создать свою консоль, используется функция AllocConsole. По завершении программы все выделенные консоли автоматически освобождаются. Однако это можно сделать и принудительно, используя функцию FreeConsole. Для того чтобы получить дескриптор консоли, используется уже знакомая Вам функция GetStdHandle, аргументом которой может являться следующая из трех констант:

STD_INPUT_HANDLE equ -10 ; для ввода
STD_OUTPUT_HANDLE equ -11 ; для вывода
STD_ERROR_HANDLE equ -12 ; для сообщения об ошибке

Следует отметить, что один процесс может иметь только одну консоль, поэтому выполнение в начале программы FreeConsole обязательно. При запуске программы в "чужой" консоли она наследует эту консоль, поэтому, пока мы не выполним функцию FreeConsole, новой консоли не создать - чужой консоли эта функция закрыть не может.

Для чтения из буфера консоли используется функция ReadConsole. Значения параметров этой функции (слева-направо)28 следующие:

  • 1-й, дескриптор входного буфера.
  • 2-й, адрес буфера, куда будет помещена вводимая информация.
  • 3-й, длина этого буфера.
  • 4-й, количество фактически прочитанных символов.
  • 5-й, зарезервировано.

Установить позицию курсора в консоли можно при помощи функции SetConsoleCursorPosition со следующими параметрами:

  • 1-й, дескриптор входного буфера консоли.
  • 2-й, структура COORD:

COORD STRUC
     Х WORD ?
     Y WORD ?
COORD ENDS

Хочу лишний раз подчеркнуть, что вторым параметром является не указатель на структуру (что обычно бывает), а именно структура. На самом деле для ассемблера это просто двойное слово (DWORD), у которого младшее слово - координата X, а старшее слово — координата Y.

Установить цвет выводимых букв можно с помощью функции SetConsoleTextAttribute. Первым параметром этой функции является дескриптор выходного буфера консоли, а вторым - цвет букв и фона. Цвет получается путем комбинации (сумма или операция "ИЛИ") двух или более из представленных ниже констант. Причем возможна "смесь" не только цвета и интенсивности, но и цветов (см. программа ниже).

FOREGROUND_BLUE equ 1h ; синий цвет букв
FOREGROUND_GREEN equ 2h ; зеленый цвет букв
FOREGROUND_RED equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE equ 10h ; синий свет фона
BACKGROUND_GREEN equ 20h ; зеленый цвет фона
BACKGROUND_RED equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность

Для определения заголовка окна консоли используется функция SetConsoleTitle, единственным параметром которой является адрес строки с нулем на конце. Здесь следует оговорить следующее: если для вывода в само окно консоли требовалась DOS-кодировка, то для установки заголовка требуется Windows-кодировка. Чтобы покончить с этой проблемой раз и навсегда, посмотрим, как это можно решить средствами Windows.

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

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

Ну что же, пора приступать к разбору следующих примеров.

.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE  equ -10

; атрибуты цветов
FOREGROUND_BLUE   equ 1h ; синий цвет букв
FOREGROUND_GREEN  equ 2h ; зеленый цвет букв
FOREGROUND_RED    equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE   equ 10h ; синий свет фона
BACKGROUND_GREEN  equ 20h ; зеленый цвет фона
BACKGROUND_RED    equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность

COL1 = 2h+8h ; цвет выводимого текста
COL2 = 1h+2h+8h ; цвет выводимого текста 2

; прототипы внешних процедур
EXTERN  GetStdHandle@4:NEAR
EXTERN  WriteConsoleA@20:NEAR
EXTERN  SetConsoleCursorPosition@8:NEAR
EXTERN  SetConsoleTitleA@4:NEAR
EXTERN  FreeConsole@0:NEAR
EXTERN  AllocConsole@0:NEAR
EXTERN  CharToOemA@8:NEAR
EXTERN  SetConsoleCursorPosition@8:NEAR
EXTERN  SetConsoleTextAttribute@8:NEAR
EXTERN  ReadConsoleA@20:NEAR
EXTERN  SetConsoleScreenBufferSize@8:NEAR
EXTERN  ExitProcess@4:NEAR

; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib

;------------------------------------------------------------
COOR  STRUC
     X  WORD ?
     Y  WORD ?
COOR  ENDS

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HANDL DWORD ?
     HANDL1 DWORD ?
     STR1 DB "Введите строку: ",13,10,0
     STR2 DB "Простой пример работы консоли",0
     BUF  DB 200 dup (?)
     LENS DWORD ? ; количество выведенных символов
     CRD  COOR <?>
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; перекодируем строку
     PUSH OFFSET STR1
     PUSH OFFSET STR1
     CALL CharToOemA@8
; образовать консоль
; вначале освободить уже существующую
     CALL FreeConsole@0
     CALL AllocConsole@0
; получить HANDL1 ввода
     PUSH STD_INPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL1, EAX
; получить HANDL вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL, EAX
; установить новый размер окна консоли
     MOV CRD.X, 100
     MOV CRD.Y, 25
     PUSH CRD
     PUSH EAX
     CALL SetConsoleScreenBufferSize@8
; задать заголовок окна консоли
     PUSH OFFSET STR2
     CALL SetConsoleTitleA@4
; установить позицию курсора
     MOV CRD.X,0
     MOV CRD.Y,10
     PUSH CRD
     PUSH HANDL
     CALL SetConsoleCursorPosition@8
; задать цветовые атрибуты выводимого текста
     PUSH COL1
     PUSH HANDL
     CALL SetConsoleTextAttribute@8
; вывести строку
     PUSH OFFSET STR1
     CALL LENSTR ; в EBX длина строки
     PUSH 0
     PUSH OFFSET LENS
     PUSH EBX
     PUSH OFFSET STR1
     PUSH HANDL
     CALL WriteConsoleA@20
; ждать ввод строки
     PUSH 0
     PUSH OFFSET LENS
     PUSH 200
     PUSH OFFSET BUF
     PUSH HANDL1
     CALL ReadConsoleA@20
; вывести полученную строку
; вначале задать цветовые атрибуты выводимого текста
     PUSH COL2
     PUSH HANDL
     CALL SetConsoleTextAttribute@8
;------------------------------------------------------------
     PUSH 0
     PUSH OFFSET LENS
     PUSH [LENS] ; длина вводимой строки
     PUSH OFFSET BUF
     PUSH HANDL
     CALL WriteConsoleA@20
; небольшая задержка
     MOV ECX,01FFFFFFFH
L1:
     LOOP L1
; закрыть консоль
     CALL FreeConsole@0
     CALL ExitProcess@4

; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
     ENTER 0,0
     PUSH EAX
;--------------
     CLD
     MOV EDI, DWORD PTR [EBP+08H]
     MOV EBX, EDI
     MOV ECX, 100 ; ограничить длину строки
     XOR AL,AL
     REPNE SCASB ; найти символ 0
     SUB EDI, EBX ; длина строки, включая 0
     MOV EBX, EDI
     DEC EBX
;--------------
     POP EAX
     LEAVE
     RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.3. Пример создания собственной консоли.

В программе на Рис. 2.2.3, кроме уже описанных функций, появились еще две SetConsoleCursorPosition - установить позицию курсора, и здесь все довольно ясно. Функция SetConsoleScreenBufferSize менее понятна. Она устанавливает размер буфера окна консоли. Этот размер не может уменьшить уже существующий буфер (существующее окно), а может только его увеличить.

Заметим, кстати, что в функции LENSTR мы теперь используем пару команд ENTER-LEAVE (см. Гл. 1.2) вместо обычных сочетаний. Честно говоря, никаких особых преимуществ такое использование не дает. Просто пора расширять свой командный запас.

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

III

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

Прежде, однако, мы рассмотрим одну весьма необычную, но чрезвычайно полезную API-функцию. Эта функция wsprintfA. Я подчеркиваю, что это именно API-функция, которая предоставляется системой приложению. Эта функция является неким аналогом библиотечной Си-функции - sprintf. Первым параметром функции является указатель на буфер, куда помещается результат форматирования. Второй - указатель на форматную строку, например: "Числа: %lu, %lu". Далее идут указатели на параметры (либо сами параметры, если это числа, см. ниже), число которых определено только содержимым форматной строки. А теперь - самое главное. Поскольку количество параметров не определено, то стек придется освобождать нам. Пример использования этой функции будет дан ниже. Заметим также, что прототипом этой функции для библиотеки import32.lib (TASM32) будет не wsprintfA, a _wsprintfA (!). Наконец отметим, что если функция выполнена успешно, то в EAX будет возвращена длина скопированной строки.

В основе получения информации о клавиатуре и мыши в консольном режиме является функция ReadConsoleInput. Параметры этой функции:

  • 1-й, дескриптор входного буфера консоли.
  • 2-й, указатель на структуру (или массив структур), в которой содержится информация о событиях, происшедших с консолью. Ниже мы подробно рассмотрим эту структуру.
  • 3-й, количество получаемых информационных записей (структур).
  • 4-й, указатель на двойное слово, содержащее количество реально полученных записей.

А теперь подробно разберемся со структурой, в которой содержится информация о консольном событии. Прежде всего замечу, что в Си эта структура записывается с помощью типа данных union (о типах данных см. Гл. 6 данной части). На мой взгляд, частое использование этого слова притупляет понимание того, что же за этим стоит. И при описании этой структуры мы обойдемся без STRUCT и UNION. Замечу также, что в начале этого блока данных идет двойное слово, младшее слово которого определяет тип события. В зависимости от значения этого слова последующие байты (максимум 18) будут трактоваться так или иначе. Те, кто уже знаком с различными структурами, используемыми в Си и Макроассемблере, теперь должны понять, почему UNION здесь весьма подходит.

Но вернемся к типу события. Всего системой зарезервировано пять типов событий:

KEY_EVENT equ 1h ; клавиатурное событие
MOUSE_EVENT equ 2h ; событие с мышью
WINDOW_BUFFER_SIZE_EVENT equ 4h ; изменился размер окна
MENU_EVENT equ 8h ; зарезервировано
FOCUS_EVENT equ 10h; зарезервировано

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

Событие KEY_EVENT

Смещение | Длина | Значение
+4 | 4 | При нажатии клавиши значение поля больше нуля.
+8 | 2 | Количество повторов при удержании клавиши.
+10 | 2 | Виртуальный код клавиши.
+12 | 2 | Скан-код клавиши.
+14 | 2 | Для функции ReadConsoleInputA-младший байт равен ASCII-коду клавиши. Для функции ReadConsoleInputW слово содержит код клавиши в двухбайтной кодировке (Unicode).
+16 | 4 | Содержится состояния управляющих клавиш. Может являться суммой следующих констант: RIGHT_ALT_PRESSED equ 1h LEFT_ALT_PRESSED equ 2h RIGHT_CTRL_PRESSED equ 4h LEFT_CTRL_PRESSED equ 8h SHIFT_PRESSED equ 10h NUMLOCK_ON equ 20h SCROLLLOCK_ON equ 40h CAPSLOCK_ON equ 80h ENHANCED_KEY equ 100h Смысл констант очевиден.

Событие MOUSE_EVENT

Смещение | Длина | Значение
+4 | 4 | Младшее слово - Х-координата курсора мыши, старшее слово - Y-координата мыши.
+8 | 4 | Описывает состояние кнопок мыши. Первый бит - левая кнопка, второй бит - правая кнопка, третий бит - средняя кнопка. Бит установлен - кнопка нажата.
+12 | 4 | Состояние управляющих клавиш. Аналогично предыдущей таблице.
+16 | 4 | Может содержать следующие значения: MOUSE_MOV equ 1h; было движение мыши DOUBLE_CL equ 2h; был двойной щелчок

Событие WINDOW_BUFFER_SIZE_EVENT

По смещению +4 находится двойное слово, содержащее новый размер консольного окна. Младшее слово - это размер по X, старшее слово - размер по Y. Да, когда речь идет о консольном окне, все размеры и координаты даются в "символьных" единицах.

Что касается последних двух событий, то там также значимым является двойное слово по смещению +4, Ниже на Рис. 2.2.4 дана простая программа обработки консольных событий.

.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE  equ -10

; тип события
KEY_EV      equ 1h
MOUSE_EV    equ 2h

; константы - состояния клавиатуры
RIGHT_ALT_PRESSED  equ 1h
LEFT_ALT_PRESSED   equ 2h
RIGHT_CTRL_PRESSED equ 4h
LEFT_CTRL_PRESSED  equ 8h
SHIFT_PRESSED     equ 10h
NUMLOCK_ON        equ 20h
SCROLLLOCK_ON     equ 40h
CAPSLOCK_ON       equ 80h
ENHANCED_KEY     equ 100h

; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTitleA@4:NEAR
EXTERN FreeConsole@0:NEAR
EXTERN AllocConsole@0:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN SetConsoleTextAttribute@8:NEAR
EXTERN ReadConsoleInputA@16:NEAR
EXTERN ExitProcess@4:NEAR

; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; структура для определения событий
COOR STRUC
     Х WORD ?
     Y WORD ?
COOR ENDS

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HANDL  DWORD ?
     HANDL1 DWORD ?
     TITL DB "Обработка событий мыши",0
     BUF  DB 200 dup (?)
     LENS DWORD ? ; количество выведенных символов
     C0   DWORD ?
     FORM DB "Координаты: %u %u "
     CRD  COOR <?>
     STR1 DB "Для выхода нажмите ESC",0
     MOUS_KEY WORD 9 dup (?)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; образовать консоль
; вначале освободить уже существующую
     CALL FreeConsole@0
     CALL AllocConsole@0
; получить HANDL1 ввода
     PUSH STD_INPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL1,EAX
; получить HANDL вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; задать заголовок окна консоли
     PUSH OFFSET TITL
     CALL SetConsoleTitleA@4
;**********************************
; перекодировка строки
     PUSH OFFSET STR1
     PUSH OFFSET STR1
     CALL CharToOemA@8
; длина строки
     PUSH OFFSET STR1
     CALL LENSTR
; вывести строку
     PUSH 0
     PUSH OFFSET LENS
     PUSH EBX
     PUSH OFFSET STR1
     PUSH HANDL
     CALL WriteConsoleA@20
; цикл ожиданий: движение мыши или двойной щелчок
L00:
; координаты курсора
     MOV CRD.X,0
     MOV CRD.Y,10
     PUSH CRD
     PUSH HANDL
     CALL SetConsoleCursorPosition@8
; прочитать одну запись о событии
     PUSH OFFSET C0
     PUSH 1
     PUSH OFFSET MOUS_KEY
     PUSH HANDL1
     CALL ReadConsoleInputA@16
; проверим, не с мышью ли что?
     CMP WORD PTR MOUS_KEY, MOUSE_EV
     JNE L001
; здесь преобразуем координаты мыши в строку
     MOV AX, WORD PTR MOUS_KEY+6 ; Y-мышь
; копирование с обнулением старших битов
     MOVZX EAX,AX
     PUSH EAX
     MOV AX, WORD PTR MOUS_KEY+4 ; Х-мышь
; копирование с обнулением старших битов
     MOVZX EAX,AX
     PUSH EAX
     PUSH OFFSET FORM
     PUSH OFFSET BUF
     CALL wsprintfA
; восстановить стек
     ADD ESP,16
; перекодировать строку для вывода
     PUSH OFFSET BUF
     PUSH OFFSET BUF
     CALL CharToOemA@8
; длина строки
     PUSH OFFSET BUF
     CALL LENSTR
; вывести на экран координаты курсора
     PUSH 0
     PUSH OFFSET LENS
     PUSH EBX
     PUSH OFFSET BUF
     PUSH HANDL
     CALL WriteConsoleA@20
     JMP L00 ; к началу цикла
L001:
; нет ли события от клавиатуры?
     CMP WORD PTR MOUS_KEY,KEY_EV
     JNE L00
; есть, какое?
     CMP BYTE PTR MOUS_KEY+14,27
     JNE L00
;********************************
; закрыть консоль
     CALL FreeConsole@0
     PUSH 0
     CALL ExitProcess@4
     RET
; процедура определения длины строки
; строка - [EBP+08Н]
; длина в EBX
LENSTR PROC
     ENTER 0,0
     PUSH EAX
     CLD
     MOV EDI, DWORD PTR [EBP+08Н]
     MOV EBX, EDI
     MOV ECX, 100 ; ограничить длину строки
     XOR AL,AL
     REPNE SCASB ; найти символ 0
     SUB EDI, EBX ; длина строки, включая 0
     MOV EBX, EDI
     DEC EBX
     POP EAX
     LEAVE
     RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.4. Пример обработки событий от мыши и клавиатуры для консольного приложения.

После того как вы познакомились с программой на Рис. 2.2.4, давайте ее подробнее обсудим.

Начнем с функции wsprintfA. Как я уже заметил, функция необычная.

  1. Она имеет переменное число параметров. Первые два параметра обязательны. Вначале идет указатель на буфер, куда будет скопирована результирующая строка. Вторым идет указатель на форматную строку. Форматная строка может содержать текст, а также формат выводимых параметров. Поля, содержащие информацию о параметре, начинаются с символа "%". Формат этих полей в точности соответствует формату полей, используемых в стандартных Си-функциях printf, sprintf и др. Исключением является отсутствие в формате для функции wsprintf вещественных чисел. Нет нужды излагать этот формат, заметим только, что каждое поле в форматной строке соответствует параметру (начиная с третьего). В нашем случае форматная строка была равна: "Координаты: %u %u". Это означало, что далее в стек будет отправлено два числовых параметра типа WORD. Конечно, в стек мы отправили два двойных слова, позаботившись лишь о том, чтобы старшие слова были обнулены. Для такой операции очень удобна команда микропроцессора MOVZX, которая копирует второй операнд в первый так, чтобы биты старшего слова были заполнены нулями. Если бы параметры были двойными словами, то вместо поля %u мы бы поставили %lu. В случае, если поле форматной строки определяет строку-параметр, например "%S", в стек следует отправлять указатель на строку (что естественно).29
  2. Поскольку функция "не знает", сколько параметров может быть в нее отправлено, разработчики не стали усложнять текст этой функции, и оставили нам проблему освобождения стека30. Это производится командой ADD ESP,N. Здесь N - это количество освобождаемых байтов.

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

По обыкновению отмечу, как откомпилировать данную программу в TASM32. Как обычно, удаляем все значки @N, указываем библиотеку import32.lib и наконец wsprintfA меняем на _wsprintfA.

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

30 Компилятор Си, естественно, делает это за нас.

IV

В последнем разделе главы мы рассмотрим довольно редко освещаемый в литературе вопрос - таймеры в консольном приложении. Надо сказать, что мы несколько опережаем события и рассматриваем таймер в консольном приложении раньше, чем в приложении GUI (Graphic Universal Interface - так называются обычные оконные приложения).

Основным способом создания таймера является использование функции SetTimer. Позднее мы будем подробно о ней говорить. Таймер может быть установлен в двух режимах. Первый режим - это когда последний параметр равен нулю. В этом случае на текущее окно (его функцию) через равные промежутки времени, определяемые третьим параметром, будет приходить сообщение WM_TIMER. Во втором режиме последний параметр указывает на функцию, которая будет вызываться опять через равные промежутки времени. Однако для консольного приложения эта функция не подходит, так как сообщение WM_TIMER пересылается окну функцией DispatchMessage, которая используется в петле обработки сообщений. Но использование этой функции для консольных приложений проблематично.

Для консольных приложений следует использовать функцию timeSetEvent. Вот параметры этой функции:

  • 1-й параметр - время задержки таймера, для нас это время совпадает со временем между двумя вызовами таймера.
  • 2-й параметр - точность работы таймера (приоритет посылки сообщения).
  • 3-й параметр - адрес вызываемой процедуры.
  • 4-й параметр - параметр, посылаемый в процедуру.
  • 5-й параметр - тип вызова - одиночный или периодический.

Если функция завершилась удачно, то в EAX возвращается идентификатор таймера.

Сама вызываемая процедура получает также 5 параметров:

  • 1-й параметр - идентификатор таймера.
  • 2-й параметр - не используется.
  • 3-й параметр - параметр Dat (см. timeSetEvent).
  • 4 и 5-й параметры - не используются.

Для удаления таймера используется функция timeKillEvent, параметром которой является идентификатор таймера.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE  equ -10
TIME_PERIODIC     equ 1 ; тип вызова таймера

; атрибуты цветов
FOREGROUND_BLUE        equ 1h ; синий цвет букв
FOREGROUND_GREEN       equ 2h ; зеленый цвет букв
FOREGROUND_RED         equ 4h ; красный цвет букв
FOREGROUND_INTENSITY   equ 8h ; повышенная интенсивность
BACKGROUND_BLUE        equ 10h ; синий свет фона
BACKGROUND_GREEN       equ 20h ; зеленый цвет фона
BACKGROUND_RED         equ 40h ; красный цвет фона
BACKGROUND_INTENSITY   equ 80h ; повышенная интенсивность

COL1 = 2h+8h ; цвет выводимого текста

; прототипы внешних процедур
EXTERN  wsprintfA:NEAR
EXTERN  GetStdHandle@4:NEAR
EXTERN  WriteConsoleA@20:NEAR
EXTERN  SetConsoleCursorPosition@8:NEAR
EXTERN  SetConsoleTitleA@4:NEAR
EXTERN  FreeConsole@0:NEAR
EXTERN  AllocConsole@0:NEAR
EXTERN  CharToOemA@8:NEAR
EXTERN  SetConsoleCursorPosition@8:NEAR
EXTERN  SetConsoleTextAttribute@8:NEAR
EXTERN  ReadConsoleA@20:NEAR
EXTERN  timeSetEvent@20:NEAR
EXTERN  timeKillEvent@4:NEAR
EXTERN  ExitProcess@4:NEAR

; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\winmm.lib
;------------------------------------------------------------

COOR  STRUC
     X  WORD ?
     Y  WORD ?
COOR  ENDS

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HANDL  DWORD ?
     HANDL1 DWORD ?
     STR2 DB "Пример таймера в консольном приложении",0
     STR3 DB 100 dup (0)
     FORM DB "Число вызовов таймера: %lu",0
     BUF  DB 200 dup (?)
     NUM  DWORD 0
     LENS DWORD ? ; количество выведенных символов
     CRD  COOR <?>
     ID  DWORD ? ; идентификатор таймера
     HWND DWORD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; образовать консоль
; вначале освободить уже существующую
     CALL FreeConsole@0
     CALL AllocConsole@0
; получить HANDL1 ввода
     PUSH STD_INPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL1,EAX
; получить HANDL вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; задать заголовок окна консоли
     PUSH OFFSET STR2
     CALL SetConsoleTitleA@4
; задать цветовые атрибуты выводимого текста
     PUSH COL1
     PUSH HANDL
     CALL SetConsoleTextAttribute@8
; установить таймер
     PUSH TIME_PERIODIC ; периодический вызов
     PUSH 0
     PUSH OFFSET TIME ; вызываемая таймером процедура
     PUSH 0  ; точность вызова таймера
     PUSH 1000 ; вызов через одну секунду
     CALL timeSetEvent@20
     MOV ID, EAX
; ждать ввод строки
     PUSH 0
     PUSH OFFSET LENS
     PUSH 200
     PUSH OFFSET BUF
     PUSH HANDL1
     CALL ReadConsoleA@20
; закрыть таймер
     PUSH ID
     CALL timeKillEvent@4
; закрыть консоль
     CALL FreeConsole@0
     PUSH 0
     CALL ExitProcess@4

; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
     ENTER 0,0
     PUSH EAX
;--------------------
     CLD
     MOV EDI, DWORD PTR [EBP+08H]
     MOV EBX,EDI
     MOV ECX,100 ; ограничить длину строки
     XOR AL,AL
     REPNE SCASB ; найти символ 0
     SUB EDI,EBX ; длина строки, включая 0
     MOV EBX,EDI
     DEC EBX
;--------------------
     POP EAX
     LEAVE
     RET 4
LENSTR ENDP

; процедура вызывается таймером
TIME  PROC
     PUSHA ; сохранить все регистры
; установить позицию курсора
     MOV CRD.X,0
     MOV CRD.Y,10
     PUSH CRD
     PUSH HANDL
     CALL SetConsoleCursorPosition@8
; заполнить строку STR3
     PUSH NUM
     PUSH OFFSET FORM
     PUSH OFFSET STR3
     CALL wsprintfA
     ADD ESP,12 ; восстановить стек
; перекодировать строку STR3
     PUSH OFFSET STR3
     PUSH OFFSET STR3
     CALL CharToOemA@8
; вывести строку с номером вызова таймера
     PUSH OFFSET STR3
     CALL LENSTR
     PUSH 0
     PUSH OFFSET LENS
     PUSH EBX
     PUSH OFFSET STR3
     PUSH HANDL
     CALL WriteConsoleA@20
     INC NUM
     POPA
     RET 20 ; выход с освобождением стека
TIME ENDP
_TEXT ENDS
END START

Рис. 2.2.5. Таймер в консольном режиме.

Программа на Рис. 2.2.5 будет выводить в окно значение счетчика, которое будет каждую секунду увеличиваться на единицу.

Я начал данную главу с рассуждения о командной строке, но до сих пор не объявил, как работать с командной строкой. О, здесь все очень просто. Есть API-функция GetCommandLine, которая возвращает указатель на командную строку. Эта функция одинаково работает как для консольных приложений, так и для приложений GUI. Ниже представлена программа, печатающая параметры командной строки. Надеюсь, вы понимаете, что первым параметром является полное имя программы.

; программа вывода параметров командной строки
.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
STD_OUTPUT_HANDLE equ -11

; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR

; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     BUF  DB 100 dup (0)
     LENS DWORD ? ; количество выведенных символов
     NUM  DWORD ?
     CNT  DWORD ?
     HANDL DWORD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; получить количество параметров
     CALL NUMPAR
     MOV NUM,EAX
     MOV CNT,0
;-------------------------------------
; вывести параметры командной строки
LL1:
     MOV EDI,CNT
     CMP NUM,EDI
     JE  LL2
; номер параметра
     INC EDI
     MOV CNT, EDI
; получить параметр номером EDI
     LEA EBX,BUF
     CALL GETPAR
; получить длину параметра
     PUSH OFFSET BUF
     CALL LENSTR
; в конце - перевод строки
     MOV BYTE PTR [BUF+EBX],13
     MOV BYTE PTR [BUF+EBX+1],10
     MOV BYTE PTR [BUF+EBX+2],0
     ADD EBX,2
; вывод строки
     PUSH 0
     PUSH OFFSET LENS
     PUSH EBX
     PUSH OFFSET BUF
     PUSH HANDL
     CALL WriteConsoleA@20
     JMP LL1
LL2:
     PUSH 0
     CALL ExitProcess@4

; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EAX
;--------------------
     CLD
     MOV EDI, DWORD PTR [EBP+08H]
     MOV EBX,EDI
     MOV ECX,100 ; ограничить длину строки
     XOR AL,AL
     REPNE SCASB ; найти символ 0
     SUB EDI,EBX ; длина строки, включая 0
     MOV EBX,EDI
     DEC EBX
;--------------------
     POP EAX
     POP EBP
     RET 4
LENSTR ENDP

; определить количество параметров (->EAX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1 ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX, 0
     JMP L2
L3:
     OR EDX, 1
L2:
     INC ESI
     JMP L1
L4:
     MOV EAX,ECX
     RET
NUMPAR ENDP

; получить параметр
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI, EAX ; указатель на строку
     XOR ECX, ECX ; счетчик
     MOV EDX,1    ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI] ,32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX, 0
     JMP L2
L3:
     OR EDX, 1
L2:
     CMP ECX, EDI
     JNE L5
     MOV AL, BYTE PTR [ESI]
     MOV BYTE PTR [EBX],AL
     INC EBX
L5:
     INC ESI
     JMP L1
L4:
     MOV BYTE PTR [EBX],0
     RET
GETPAR ENDP
_TEXT ENDS
END START

Рис. 2.2.6. Пример работы с параметрами командной строки.

Рекомендую читателю разобраться в алгоритме работы процедур NUMPAR и GETPAR.

Следует отметить, что для трансляции программы на Рис. 2.2.6 в TASM, кроме обычных, уже известных Вам изменений, для совпадающих меток следует в начале имени поставить "@@" - признак локальности, а в начале программы поставить директиву LOCALS. Транслятор MASM метки, стоящие в процедуре, считает локальными автоматически. Подробнее о локальных метках будет сказано в Главах 2.5 и 2.6.


Часть II. Глава 3. Понятие ресурса. Редакторы и трансляторы ресурсов

Глава 3. Понятие ресурса. Редакторы и трансляторы ресурсов

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

Использование ресурсов дает две вполне определенные выгоды:

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

Описание ресурсов хранится отдельно от программы в текстовом файле (*.rc) и компилируется (*.res) специальным транслятором ресурсов. В исполняемый файл ресурсы включаются компоновщиком. Транслятором ресурсов в пакете MASM32 является RC.EXE, в пакете TASM32 - BRCC32.EXE.

I

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

Начнем с перечисления наиболее употребляемых ресурсов.

  1. Иконки.
  2. Курсоры.
  3. Битовая картинка.
  4. Строка.
  5. Диалоговое окно.
  6. Меню.
  7. Акселераторы.

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

1. Иконки. Могут быть описаны в самом файле ресурсов, либо храниться в отдельном файле *.ico. Рассмотрим последний случай. Вот файл ресурсов resu.rc:

#define IDI_ICON1 1

IDI_ICON1 ICON "Cdrom01.ico"

Как видите, файл содержит всего две значимых строки. Одна строка определяет идентификатор иконки, вторая - ассоциирует идентификатор с файлом "Cdrom01.ico". Оператор define является Си-оператором препроцессора. Как вы увидите в дальнейшем, язык ресурсов очень напоминает язык Си. Откомпилируем текстовый файл resu.rc: RC resu.rc. На диске появляется объектный файл resu.res. При компоновке укажем этот файл в командной строке:

LINK /subsystem:windows resu.obj resu.res

У читателя возникает вопрос: как использовать данный ресурс в программе? Здесь все просто: предположим, что мы хотим установить новую иконку для окна. Вот фрагмент программы, который устанавливает стандартную иконку для главного окна.

     PUSH IDI_APPLICATION
     PUSH 0
     CALL LoadIconA@8
     MOV [WC.CLSHICON], EAX

А вот фрагмент программы для установки иконки, указанной в файле ресурсов:

     PUSH 1   ; идентификатор иконки (см. файл resu.rc)
     PUSH [HINST] ; идентификатор процесса
     CALL LoadIconA@8
     MOV [WC.CLSHICON], EAX

Компилятор ресурсов brcc32.exe (из пакета TASM32) допускает включение иконки в текст проекта. В этом случае проект будет иметь следующий вид (Рис. 2.3.1):

#define IDI_ICON1 1

IDI_ICON1 ICON
{
'00 00 01 00 02 00 20 20 10 00 00 00 00 00 E8 02'
'00 00 26 00 00 00 10 10 10 00 00 00 00 00 28 01'
'00 00 0E 03 00 00 28 00 00 00 20 00 00 00 40 00'
'00 00 01 00 04 00 00 00 00 00 80 02 00 00 00 00'
'00 00 00 00 00 00 10 00 00 00 00 00 00 00 00 00'
'00 00 00 00 BF 00 00 BF 00 00 00 BF BF 00 BF 00'
'00 00 BF 00 BF 00 BF BF 00 00 C0 C0 C0 00 80 80'
'80 00 00 00 FF 00 00 FF 00 00 00 FF FF 00 FF 00'
'00 00 FF 00 FF 00 FF FF 00 00 FF FF FF 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 77 78 33 AA 00 00 00 00 00 00 00 00'
'00 00 07 7F 77 78 33 AA 77 80 00 00 00 00 00 00'
'00 0F F7 F7 77 78 33 AA 77 C8 60 00 00 00 00 00'
'00 FF FF 7F 77 78 33 AA 78 C6 66 00 00 00 00 00'
'0F FF FF F7 77 78 38 A7 7C 86 66 60 00 00 00 00'
'77 FF FF 7F 77 78 37 A7 8C 66 66 77 00 00 00 07'
'87 7F FF F7 F7 78 37 A7 C8 66 67 77 70 00 00 08'
'78 77 FF FF 77 78 3A A8 C6 66 77 77 E0 00 00 87'
'87 87 7F FF F7 78 3A AC 86 67 77 EE EE 00 00 78'
'78 78 77 FF 7F 78 3A 8C 66 77 EE EE BB 00 07 87'
'87 87 87 7F F7 78 3A C8 67 7E EB BB BA A0 08 78'
'78 78 78 77 F8 88 88 C6 7E BB BB AA AA A0 07 87'
'87 87 87 87 88 00 00 88 BB BA AA A3 33 30 08 78'
'78 78 78 78 80 8F F8 08 33 33 33 DD DD D0 08 88'
'88 88 88 88 80 FF FF 08 5D 5D 5D 5D 5D 50 05 D5'
'D5 D5 D5 D5 80 FF FF 08 88 88 88 88 88 80 0D DD'
'DD 33 33 33 80 8F F8 08 87 87 87 87 87 80 03 33'
'3A AA AB BB 88 00 00 88 78 78 78 78 78 70 0A AA'
'AA BB BB E7 6C 88 88 8F 77 87 87 87 87 80 0A AB'
'BB BE E7 76 8C A3 87 7F F7 78 78 78 78 70 00 BB'
'EE EE 77 66 C8 A3 87 F7 FF 77 87 87 87 00 00 EE'
'EE 77 76 68 CA A3 87 7F FF F7 78 78 78 00 00 0E'
'77 77 66 6C 8A A3 87 77 FF FF 77 87 80 00 00 07'
'77 76 66 8C 7A 73 87 7F 7F FF F7 78 70 00 00 00'
'77 66 66 C8 7A 73 87 77 F7 FF FF 77 00 00 00 00'
'06 66 68 C7 7A 83 87 77 7F FF FF F0 00 00 00 00'
'00 66 6C 87 AA 33 87 77 F7 FF FF 00 00 00 00 00'
'00 06 8C 77 AA 33 87 77 7F 7F F0 00 00 00 00 00'
'00 00 08 77 AA 33 87 77 F7 70 00 00 00 00 00 00'
'00 00 00 00 AA 33 87 77 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF F0'
'0F FF FF 80 01 FF FE 00 00 7F FC 00 00 3F F8 00'
'00 1F F0 00 00 0F E0 00 00 07 C0 00 00 03 C0 00'
'00 03 80 00 00 01 80 00 00 01 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 80 00 00 01 80 00 00 01 C0 00 00 03 C0 00'
'00 03 E0 00 00 07 F0 00 00 0F F8 00 00 1F FC 00'
'00 3F FE 00 00 7F FF 80 01 FF FF F0 0F FF 28 00'
'00 00 10 00 00 00 20 00 00 00 01 00 04 00 00 00'
'00 00 C0 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80'
'00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80'
'00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF'
'00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF'
'00 00 FF FF FF 00 00 00 00 00 00 00 00 00 00 00'
'08 87 3A 80 00 00 00 0F F8 87 32 CC 60 00 00 08'
'F8 87 32 C6 68 00 00 87 8F 87 2C 66 86 00 08 78'
'78 87 2C 68 AA A0 07 87 87 70 08 2A A2 20 08 78'
'78 0F F0 II 15 50 05 51 II 0F F0 87 87 80 02 2A'
'A2 80 08 78 78 70 0A AA 86 C2 78 87 87 80 00 68'
'66 C2 78 F8 78 00 00 86 6C 23 78 8F 88 00 00 06'
'CC 23 78 8F F0 00 00 00 08 A3 78 80 00 00 00 00'
'00 00 00 00 00 00 F8 IF 00 00 E0 07 00 00 C0 03'
'00 00 80 01 00 00 80 01 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 80 01 00 00 80 01 00 00 C0 03 00 00 E0 07'
'00 00 F8 1F 00 00'
}

Рис. 2.3.1. Пример файла ресурсов с включенным туда кодом иконки.

2. Курсоры. Подход здесь полностью идентичен. Привожу ниже файл ресурсов, где определен и курсор, и иконка.

#define IDI_ICON1 1
#define IDI_CUR1 2

IDI_ICON1 ICON "Cdrom01.ico"
IDI_CUR1 CURSOR "4way01.cur"

А вот фрагмент программы, вызывающей иконку и курсор.

     ;----------иконка окна
     PUSH 1 ; идентификатор иконки
     PUSH [HINST]
     CALL LoadIconA@8
     MOV [WC.CLSHICON], EAX
     ; ----------курсор окна
     PUSH 2 ; идентификатор курсора
     PUSH [HINST]
     CALL LoadCursorA@8
     MOV [WC.CLSHCURSOR], EAX

Как и для иконки, программа brcc32.exe обрабатывает определение курсора в тексте файла ресурсов.

3. Битовые картинки (*.BMP). Здесь ситуация аналогична двум предыдущим. Вот пример файла ресурсов с битовой картинкой.

#define ВIТ1 1

BIT1 BITMAP "PIR2.BMP"

4. Строки. Чтобы задать строку или несколько строк используется ключевое слово STRINGTABLE. Ниже представлен текст ресурса, задающий две строки. Для загрузки строки в программу используется функция LoadString (см. ниже). Строки, задаваемые в файле ресурсов, могут играть роль констант.

#define STR1 1
#define STR2 2

STRINGTABLE
{
STR1, "Сообщение"
STR2, "Версия 1.01"
}

5. Диалоговые окна. Диалоговые окна являются наиболее сложными элементами ресурсов. В отличие от ресурсов, которые мы до сих пор рассматривали, для диалога не задается идентификатор. Обращение к диалогу происходит по его имени (строке).

#define WS_SYSMENU       0x00080000L
#define WS_MINIMIZEBOX   0x00020000L
#define WS_MAXIMIZEBOX   0x00010000L

DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}

Как видим, определение диалога начинается со строки, содержащей ключевое слово DIALOG. В этой же строке далее указывается положение и размер диалогового окна. Далее идут строки, содержащие другие свойства окна. Наконец идут фигурные скобки. В данном случае они пусты. Это означает, что на окне нет никаких управляющих элементов. Тип окна, а также других элементов определяется константами, которые мы поместили в начале файла. Эти константы стандартны, и для языка Си хранятся в файле RESOURCE.H. Мы, как и раньше, все константы будем определять непосредственно в файле ресурсов. Обращаю ваше внимание, что константы определяются согласно нотации языка Си.

Прежде чем разбирать пример на Рис. 2.3.2, рассмотрим особенности работы с диалоговыми окнами. Диалоговое окно очень похоже на обычное окно. Так же как обычное окно, оно имеет свою процедуру. Процедура диалогового окна имеет те же параметры, что и процедура обычного окна. Сообщений, которые приходят на процедуру диалогового окна, гораздо меньше. Но те, которые у диалогового окна имеются, в основном совпадают с аналогичными сообщениями для обычного окна. Только вместо сообщения WM_CREATE приходит сообщение WM_INITDIALOG. Процедура диалогового окна может возвращать либо нулевое, либо ненулевое значение. Ненулевое значение должно возвращаться в том случае, если процедура обрабатывает (берет на себя обработку) данное сообщение, и ноль - если предоставляет обработку системе.

Отличия в поведении диалогового окна от обычного окна легко объяснить. Действительно, если Вы создаете обычное окно, то все его свойства определяются тремя факторами: свойствами класса, свойствами, определяемыми при создании окна, реакцией процедуры окна на определенные сообщения. При создании диалогового окна все свойства заданы в ресурсах. Часть этих свойств задается, когда при вызове функции создания диалогового окна (DialogBox, DialogBoxParam и др.) неявно вызывается функция CreateWindow. Остальная же часть свойств определяется поведением внутренней функции, которую создает система при создании диалогового окна. Если с диалоговым окном что-то происходит, то сообщение сначала приходит на внутреннюю процедуру, а затем вызывается процедура диалогового окна, которую мы создаем в программе. Если процедура возвращает 0, то внутренняя процедура продолжает обработку данного сообщения, если же возвращается ненулевое значение, внутренняя процедура не обрабатывает сообщение. Вот, вкратце, как работают механизмы, регулирующие работу диалогового окна. Рассмотрите теперь программу на Рис. 2.3.2, ниже будет дано ее разъяснение.

// файл dial.rc
// определение констант
#define WS_SYSMENU       0x00080000L
#define WS_MINIMIZEBOX   0x00020000L
#define WS_MAXIMIZEBOX   0x00010000L

// идентификаторы
#define STR1 1
#define STR2 2
#define IDI_ICON1 3

// определили иконку
IDI_ICON1 ICON "ico1.ico"

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}

// определение строк
STRINGTABLE
{
STR1, "Сообщение"
STR2, "Версия программы 1.00"
}

; файл dial.inc
; константы

; сообщение приходит при закрытии окна
WM_CLOSE      equ 10h
WM_INITDIALOG equ 110h
WM_SETICON    equ 80h
; прототипы внешних процедур
EXTERN      MessageBoxA@16:NEAR
EXTERN      ExitProcess@4:NEAR
EXTERN      GetModuleHandleA@4:NEAR
EXTERN      DialogBoxParamA@20:NEAR
EXTERN      EndDialog@8:NEAR
EXTERN      LoadStringA@16:NEAR
EXTERN      LoadIconA@8:NEAR
EXTERN      SendMessageA@16:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND      DD ?
     MSMESSAGE   DD ?
     MSWPARAM    DD ?
     MSLPARAM    DD ?
     MSTIME      DD ?
     MSPT        DD ?
MSGSTRUCT ENDS

;файл dial.asm
.386P

; плоская модель
.MODEL FLAT, stdcall
include dial.inc

; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;--------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG       MSGSTRUCT <?>
     HINST     DD 0 ; дескриптор приложения
     PA        DB "DIAL1",0
     BUF1      DB 40 dup(0)
     BUF2      DB 40 dup(0)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST],EAX
;------
; загрузить строку
     PUSH 40
     PUSH OFFSET BUF1
     PUSH 1
     PUSH [HINST]
     CALL LoadStringA@16
; загрузить строку
     PUSH 40
     PUSH OFFSET BUF2
     PUSH 2
     PUSH [HINST]
     CALL LoadStringA@16
;------------------------------------------------------------
     PUSH 0         ; MB_OK
     PUSH OFFSET BUF1
     PUSH OFFSET BUF2
     PUSH 0
     CALL MessageBoxA@16
; создать диалоговое окно
     PUSH 0
     PUSH OFFSET WNDPROC ; процедура окна
     PUSH 0
     PUSH OFFSET PA   ; название ресурса (DIAL1)
     PUSH [HINST]
     CALL DialogBoxParamA@20
     CMP EAX,-1
     JNE KOL
KOL:
     PUSH 0
     CALL ExitProcess@4
;--------------------------

; процедура диалогового окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CН]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;-----
     CMP DWORD PTR [EBP+ОСН], WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+ОСН], WM_INITDIALOG
     JNE FINISH
; загрузить иконку
     PUSH 3 ; идентификатор иконки
     PUSH [HINST] ; идентификатор процесса
     CALL LoadIconA@8
; установить иконку
     PUSH EAX
     PUSH 0 ; тип иконки (маленькая)
     PUSH WM_SETICON
     PUSH DWORD PTR [EBP+08H]
     CALL SendMessageA@16
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX, 0
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. 2.3.2. Демонстрация использования простых ресурсов.

Рассмотрим теперь, как работает эта программа.

  1. Файл ресурсов должен быть Вам понятен, так как все используемые там ресурсы были подробно рассмотрены ранее. Замечу только, что файл ресурсов содержит сразу несколько элементов. При этом все ресурсы, кроме диалогового окна, должны иметь идентификатор. Для диалогового окна определяющим является его название, в нашем случае это DIAL1.
  2. Перед тем как вызвать диалоговое окно, демонстрируется то, как нужно работать с таким ресурсом, как строка. Как видите, это достаточно просто. При помощи функции LoadString строка загружается в буфер, после чего с ней можно работать, как с обычной строкой.
  3. Вызов диалогового окна достаточно очевиден, так что перейдем сразу к процедуре диалогового окна. Начнем с сообщения WM_INITDIALOG. Это сообщение, как и сообщение WM_CREATE для обычного окна, приходит один раз при создании окна. Это весьма удобно для проведения какой-то начальной инициализации. Мы используем это для определения иконки диалогового окна. В начале загружаем иконку, а далее посылаем сообщение установить иконку для данного окна (WM_SETICON). Вторым сообщением, которое мы обрабатываем, является WM_CLOSE. Это сообщение приходит, когда происходит щелчок мышью по крестику в правом верхнем углу экрана. По получении этого сообщения выполняется функция EndDialog, что приводит к удалению диалогового окна из памяти, выходу из функции DialogBoxParamA и в конечном итоге - к выходу из программы.

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

31 Лично я предпочитаю использовать редактор ресурсов из пакета Borland C++ 5.00, либо простой текстовый редактор.

II

6. Меню. Меню также может быть задано в файле ресурсов. Как и диалоговое окно, в программе оно определяется по имени (строке). Меню можно задать и в обычном окне, и в диалоговом окне. Для обычного окна при регистрации класса следует просто заменить строку

     MOV DWORD PTR [WC.CLMENNAME],0

на

     MOV DWORD PTR [WC.CLMENNAME], OFFSET MENS

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

А теперь обо всем подробнее. Рассмотрим структуру файла ресурсов, содержащего определение меню. Ниже представлен текст файла, содержащего определение меню.

Далее представлена программа, демонстрирующая меню на диалоговом окне.

MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2
POPUP "Подмен&ю"
{
MENUITEM "Десятый пунк&т",6
}
}
POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый",4
}
MENUITEM "Вы&ход",5
}

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

//файл menu.rc
//определение констант
#define WS_SYSMENU     0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
#define WS_POPUP       0x80000000L
#define WS_CAPTION     0x00C00000L

MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2
}
POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый",4

POPUP "Еще подмен&ю"
{

MENUITEM "Десятый пунк&т", 6
}
}
MENUITEM "Вы&ход", 5
}

// идентификаторы
#define IDI_ICON1 100

; определили иконку
IDI_ICON1 ICON "ico1.ico"

//определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120

STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX

CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}

; файл menu.inc
; константы
; сообщение приходит при закрытии окна

WM_CLOSE      equ 10h
WM_INITDIALOG equ 110h
WM_SETICON    equ 80h
WM_COMMAND    equ 111h

; прототипы внешних процедур
EXTERN      MessageBoxA@16:NEAR
EXTERN      ExitProcess@4:NEAR
EXTERN      GetModuleHandleA@4:NEAR
EXTERN      DialogBoxParamA@20:NEAR
EXTERN      EndDialog@8:NEAR
EXTERN      LoadStringA@16:NEAR
EXTERN      LoadIconA@8:NEAR
EXTERN      LoadMenuA@8:NEAR
EXTERN      SendMessageA@16:NEAR
EXTERN      SetMenu@8:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DD ?
     MSMESSAGE  DD ?
     MSWPARAM   DD ?
     MSLPARAM   DD ?
     MSTIME     DD ?
     MSPT       DD ?
MSGSTRUCT ENDS

; файл menu.asm
.386P

; плоская модель
.MODEL FLAT, stdcall

include menu. inc

; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG       MSGSTRUCT <?>
     HINST     DD 0 ; дескриптор приложения
     PA        DB "DIAL1",0
     PMENU     DB "MENUP",0
     STR1      DB "Выход из программы",0
     STR2      DB "Сообщение",0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;---------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
KOL:
     PUSH  0
     CALL  ExitProcess@4

; процедура окна
; расположение параметров в стеке

; [EBP+014Н]  LPARAM
; [EBP+10H]   WAPARAM
; [EBP+0CH]   MES
; [EBP+8]     HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;-----------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; закрыть диалоговое окно
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
; загрузить иконку
     PUSH  100    ; идентификатор иконки
     PUSH [HINST] ; идентификатор процесса
     CALL LoadIconA@8
; установить иконку
     PUSH EAX
     PUSH 0  ; тип иконки (маленькая)
     PUSH WM_SETICON
     PUSH DWORD PTR [EBP+08H]
     CALL SendMessageA@16
; загрузить меню
     PUSH OFFSET PMENU
     PUSH [HINST]
     CALL LoadMenuA@8
; установить меню
     PUSH EAX
     PUSH DWORD PTR [EBP+08H]
     CALL SetMenu@8
     JMP FINISH
L2:
; проверяем, не случилось ли чего с управляющими элементами
; на диалоговом окне, в нашем случае имеется единственный
; управляющий элемент - это меню
     CMP DWORD PTR [EBP+0CH], WM_COMMAND
     JNE FINISH
; здесь определяем идентификатор, в данном случае
; это идентификатор пункта меню
     CMP WORD PTR [EBP+10Н],5
     JNE FINISH
; сообщение
     PUSH 0         ; МВ_ОК
     PUSH OFFSET STR2
     PUSH OFFSET STR1
     PUSH 0
     CALL MessageBoxA@16
; закрыть диалоговое окно
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
FINISH:
     MOV EAX, 0
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. 2.3.3. Пример программы с меню.

Дадим небольшой комментарий к программе на Рис. 2.3.3. Прежде всего обращаю ваше внимание на довольно прозрачную аналогию между диалоговым окном и меню. Действительно, и в том и в другом случае ресурс определяется не по идентификатору, а по имени. Далее, и диалоговое окно, и меню содержат в себе элементы, определяющиеся по их идентификаторам, которые мы задали в файле ресурсов и которые помещаются в младшее слово параметра WPARAM. В программе на Рис. 2.3.3 мы программно загружаем меню. Можно поступить и по другому: указать меню в опциях определения диалогового окна следующим образом.

//определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120

STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
MENU MENUP

CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
}

Этого достаточно, чтобы меню загрузилось и отобразилось автоматически.

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

Как видите, идентифицировать элемент, расположенный на диалоговом окне, можно и по дескриптору, и по идентификатору ресурса.

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

Вот эти свойства, понимаемые компилятором ресурсов:
CHECKED - пункт отмечен "птичкой".
GRAYED - элемент недоступен (имеет серый цвет).
HELP - элемент может быть связан с помощью. Редакторы ресурсов дополнительно создают ресурс - строку. При этом идентификатор строки совпадает с идентификатором пункта меню.
MENUBARBREAK - для горизонтального пункта это означает, что начиная с него горизонтальные пункты располагаются в новой строке. Для вертикального пункта - то, что начиная с него пункты расположены в новом столбце. При этом проводится разделительная линия.
MENUBREAK - аналогично предыдущему, но разделительная линия не проводится.
INACTIVE - пункт не срабатывает.
SEPARATOR - создает в меню разделитель. При этом идентификатор не ставится.

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

III

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

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

Вот пример такой таблицы. Определяется один акселератор на пункт меню MENUP, имеющий идентификатор 4.

MENUP ACCELERATORS
{
VK_F5, 4, VIRTKEY
}

А вот общий вид таблицы акселераторов.

Имя ACCELERATORS
{
Клавиша 1, Идентификатор пункта меню (1) [,тип] [,параметр]
Клавиша 2, Идентификатор пункта меню (2) [,тип] [,параметр]
Клавиша 3, Идентификатор пункта меню (3) [,тип] [,параметр]
...
Клавиша N, Идентификатор пункта меню (N) [,тип] [,параметр]
}

Рассмотрим представленную схему. Клавиша - это либо символ в кавычках, либо код ASCII символа, либо виртуальная клавиша. Если вначале стоит код символа, то тип задается как ASCII. Если используется виртуальная клавиша, то тип определяется как VIRTUAL. Все названия (макроимена) виртуальных клавиш можно найти в include- файлах (windows.h). Мы, как обычно, будем определять все макроимена непосредственно в программе.

Параметр может принимать одно из следующих значений: NOINVERT, ALT, CONTROL, SHIFT. Значение NOINVERT означает, что не подсвечивается выбранный при помощи акселератора пункт меню. Значения ALT, SHIFT, CONTROL означают, что, кроме клавиши, определенной в акселераторе, должна быть нажата одна из управляющих клавиш. Кроме этого, если клавиша определяется в кавычках, то нажатие при этом клавиши CONTROL определяется знаком "^": "^А".

А теперь поговорим о механизме работы акселераторов. Для того чтобы акселераторы работали, необходимо выполнить два условия:

  1. Должна быть загружена таблица акселераторов. Для этого используется функция LoadAccelerators.
  2. Сообщения, пришедшие от акселератора, следует преобразовать в сообщение WM_COMMAND. Здесь нам пригодится функция TranslateAccelerator.

Остановимся подробнее на втором пункте. Функция TranslateAccelerator преобразует сообщения WM_KEYDOWN и WM_SYSKEYDOWN в сообщения WM_COMMAND и WM_SYSCOMMAND соответственно. При этом в старшем слове параметра WPARAM помещается 1, как отличие для акселератора. В младшем слове, как Вы помните, содержится идентификатор пункта меню. Возникает вопрос: для чего необходимы два сообщения WM_COMMAND и WM_SYSCOMMAND? Здесь все закономерно: сообщение WM_SYSCOMMAND генерируется для пунктов системного меню или меню окна (см. Рис. 2.3.4).

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

MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL GetMessageA@16
     CMP EAX, 0
     JE  END_LOOP
     PUSH OFFSET MSG
     PUSH [ACC]
     PUSH [NEWHWND]
     CALL TranslateAcceleratorA@12
     CMP EAX ,0
     JNE MSG_LOOP
     PUSH OFFSET MSG
     CALL TranslateMessage@4
     PUSH OFFSET MSG
     CALL DispatchMessageA@4
     JMP MSG_LOOP
END_LOOP:

Рис. 2.3.4. Меню окна.

Фрагмент Вам знаком, но в него вставлена функция TranslateAccelerator. Первым параметром этой функции идет дескриптор приложения, вторым параметром идет дескриптор таблицы акселераторов ([ACC]), получаемый при загрузке таблицы с помощью функции LoadAccelerators. Третий параметр - адрес, где содержится сообщение, полученное функцией GetMessage.

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

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

  1. Немодальный диалог создается при помощи функции CreateDialog.
  2. Уничтожается немодальный диалог функцией DestroyWindow.
  3. Для того чтобы немодальный диалог появился на экране, нужно либо указать у него свойство WS_VISIBLE, либо после создания диалога выполнить команду ShowWindow.

Ниже (Рис. 2.3.5) представлена программа, демонстрирующая немодальный диалог с меню и обработкой сообщений акселератора.

// файл menu1.rc
// определение констант
#define WS_SYSMENU     0x00080000L
#define WS_MINIMIZEBOX   0x00020000L
#define WS_MAXIMIZEBOX   0x00010000L
#define WS_POPUP      0x80000000L
#define VK_F5       0х74
#define st WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX

MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2,HELP
MENUITEM "Что-то?",8
}

POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый",4
MENUITEM SEPARATOR
POPUP "Еще подмен&ю"
{
MENUITEM "Десятый пунк&т",6
}
}
MENUITEM "Вы&ход",5
}

// идентификаторы
#define IDI_ICON1 100

// определили иконку
IDI_ICON1 ICON "ico1.ico"

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_POPUP | st
CAPTION "Пример немодального диалогового окна"
FONT 8, "Arial"
{
}

MENUP ACCELERATORS
{
  VK_F5, 4, VIRTKEY, ALT
}

; файл menu1.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE       equ 10h
WM_INITDIALOG  equ 110h
WM_SETICON     equ 80h
WM_COMMAND     equ 111h

; прототипы внешних процедур
EXTERN      ShowWindow@8:NEAR
EXTERN      MessageBoxA@16:NEAR
EXTERN      ExitProcess@4:NEAR
EXTERN      GetModuleHandleA@4:NEAR
EXTERN      LoadIconA@8:NEAR
EXTERN      LoadMenuA@8:NEAR
EXTERN      SendMessageA@16:NEAR
EXTERN      SetMenu@8:NEAR
EXTERN      LoadAcceleratorsA@8:NEAR
EXTERN      TranslateAcceleratorA@12:NEAR
EXTERN      GetMessageA@16:NEAR
EXTERN      DispatchMessageA@4:NEAR
EXTERN      PostQuitMessage@4:NEAR
EXTERN      CreateDialogParamA@20:NEAR
EXTERN      DestroyWindow@4:NEAR
EXTERN      TranslateMessage@4:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ?
     MSMESSAGE DD ?
     MSWPARAM  DD ?
     MSLPARAM  DD ?
     MSTIME    DD ?
     MSPT      DD ?
MSGSTRUCT ENDS

;файл menu1.asm
.386P

; плоская модель
.MODEL FLAT, stdcall
include menu1.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND  DD 0
     MSG      MSGSTRUCT <?>
     HINST    DD 0 ; дескриптор приложения
     PA       DB "DIAL1",0
     PMENU    DB "MENUP",0
     STR1     DB "Выход из программы",0
     STR2     DB "Сообщение",0
     STR3     DB "Выбран четвертый", О
     АСС      DWORD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH 0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
; загрузить акселераторы
     PUSH OFFSET PMENU
     PUSH [HINST]
     CALL LoadAcceleratorsA@8
     MOV АСС, EAX ; запомнить дескриптор таблицы
; создать немодальный диалог
     PUSH 0
     PUSH  OFFSET WNDPROC
     PUSH 0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  CreateDialogParamA@20
; визуализировать немодальный диалог
     MOV  NEWHWND, EAX
     PUSH 1 ; SW_SHOWNORMAL
     PUSH [NEWHWND]
     CALL ShowWindow@8 ; показать созданное окно
; кольцо обработки сообщений
MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL GetMessageA@l6
     CMP EAX, 0
     JE  END_LOOP
; транслировать сообщение акселератора
     PUSH OFFSET MSG
     PUSH [АСС]
     PUSH [NEWHWND]
     CALL TranslateAcceleratorA@12
     CMP EAX,0
     JNE MSG_LOOP
     PUSH OFFSET MSG
     CALL TranslateMessage@4
     PUSH OFFSET MSG
     CALL DispatchMessageA@4
     JMP MSG_LOOP
END_LOOP:
     PUSH  0
     CALL  ExitProcess@4

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  LPARAM
; [EBP+10H]   WAPARAM
; [EBP+0CH]   MES
; [EBP+8]    HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;-------------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; закрыть диалоговое окно
     JMP L5
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L3
; загрузить иконку
     PUSH  100    ; идентификатор иконки
     PUSH [HINST] ; идентификатор процесса
     CALL LoadIconA@8
; установить иконку
     PUSH EAX
     PUSH 0  ; тип иконки (маленькая)
     PUSH WM_SETICON
     PUSH DWORD PTR [EBP+08H]
     CALL SendMessageA@16
; загрузить меню
     PUSH OFFSET PMENU
     PUSH [HINST]
     CALL LoadMenuA@8
; установить меню
     PUSH EAX
     PUSH DWORD PTR [EBP+08H]
     CALL SetMenu@8
;-----------------------------
     MOV EAX, 1 ; возвратить не нулевое значение
     JMP FIN
; проверяем, не случилось ли чего с управляющими
; элементами на диалоговом окне
L3:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JE L6
     JMP FINISH
; здесь определяем идентификатор, в данном случае
; это идентификатор пункта меню сообщение
L6:
     CMP WORD PTR [EBP+10H], 4
     JNE L4
     PUSH 0         ; MB_OK
     PUSH OFFSET STR2
     PUSH OFFSET STR3
     PUSH 0
     CALL MessageBoxA@16
     JMP FINISH
L4:
     CMP WORD PTR [EBP+10H], 5
     JNE FINISH
; сообщение
     PUSH 0         ; MB_OK
     PUSH OFFSET STR2
     PUSH OFFSET STR1
     PUSH 0
     CALL MessageBoxA@16
; закрыть диалоговое немодальное окно
L5:
     PUSH DWORD PTR [EBP+08H]
     CALL DestroyWindow@4
; послать сообщение для выхода из кольца
; обработки сообщений
     PUSH 0
     CALL PostQuitMessage@4 ; сообщение WM_QUIT
FINISH:
     MOV EAX, 0
FIN:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. 2.3.5. Пример диалогового немодального окна сменю и обработкой сообщений акселераторов.

Несколько комментариев по поводу программы на Рис. 2.3.5.

  1. Немодальный диалог задается в файле ресурсов так же, как и модальный. Поскольку в свойствах окна нами не было указано свойство WS_VISIBLE, в программе, для того чтобы окно было видимым, мы используем функцию ShowWindow.
  2. Выход из программы в нашем случае предполагает не только удаление из памяти диалогового окна, что достигается посредством функции DestroyWindow, но и выход из цикла обработки сообщений. Последнее осуществляется через вызов функции PostQuitMessage.

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

1. Ресурс, содержащий неструктурированные данные.

имя RCDATA
BEGIN
   raw-data
   . . .
END

2. Ресурс VERSIONINFO.

ID VERSIONINFO
BEGIN
   block-statement
   . . .
END

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

Трансляция при помощи пакета TASM32

При работе с ресурсами в пакете TASM32 следует учитывать некоторые особенности. И дело здесь не только в том, что компиляторы ресурсов могут иметь свои конструкции языка, которые не понимает другой компилятор. Об этом мы уже говорили и больше касаться этого не будем. Есть и другое отличие. Пусть программа называется DIAL.ASM, а файл ресурсов DIAL.RC. Тогда полная трансляция в пакете TASM32 будет выглядеть следующим образом.

TASM32 /ml DIAL.ASM
BRCC32 DIAL.RC
TLINK32 DIAL.OBJ,,,,, DIAL.RES

В результате получится программа DIAL.EXE. Если программа представляет на экран диалоговое окно (именно диалоговое, а не обычное), то, скорее всего (возможно и нет). Вы обнаружите, что стиль его соответствует стилю окон Windows 3.1, чего не было в случае трансляции в MASM32. Проблема разрешится, если добавить в стиль окна константу DS_3DLOOK, равную 0x0004L. В файле помощи можно найти утверждение, что стиль DS_3DLOOK должен автоматически устанавливаться у диалоговых окон. Возможно, суть здесь заключается в особенности работы TLINK32.EXE. Других существенных отличий при работе с ресурсами в пакетах MASM32 и TASM32 я не усматриваю.


Часть II. Глава 4. Примеры программ использующих ресурсы

Глава 4. Примеры программ использующих ресурсы

Вопрос использования ресурсов при программировании в Windows весьма важен, поэтому я посвящаю ему еще одну главу. Здесь будет приведено три более сложных примера на использование ресурсов и подробное их разъяснение.

I

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

Программа открывает окно с кнопкой и меню. При нажатии кнопки текущее меню заменяется другим. Если нажать еще раз, то меню исчезает. Следующее нажатие приводит к появлению первого меню и так далее, по кругу. Кроме того, в первом меню имеется пункт, который приводит к такому же результату, что и нажатие кнопки. Наконец, для этого пункта установлена акселераторная клавиша - F5. При передвижении по меню название пунктов меню и заголовков выпадающих (POPUP) подменю отображается в заголовке окна. Вот, вкратце, как работает программа. Механизмы работы программы будут подробно разобраны ниже.

// файл menu2.rc

// виртуальная клавиша F5
#define VK_F5   0х74

// ************** MENUP **************

MENUP MENU
{
 POPUP "&Первый пункт"
 {
 MENUITEM "&Первый",1
 MENUITEM "В&торой",2
 }

 POPUP "&Второй пункт"
 {
 MENUITEM "Трети&й",3
 MENUITEM "Четверт&ый\tF5",4
 MENUITEM SEPARATOR
 POPUP "Еще подмен&ю"
 {
 MENUITEM "Дополнительный пу&нкт",6
 }

 }

 MENUITEM "Вы&ход",5
}

//**************** MENUC ********************
MENUC MENU
{
 POPUP "Набор первый"
 {
 MENUITEM "Белый",101
 MENUITEM "Серый",102
 MENUITEM "Черный",103
 }
 POPUP "Набор второй"
 {
 MENUITEM "Красный",104
 MENUITEM "Синий",105
 MENUITEM "Зеленый",106
 }
}

// таблица акселераторов
// определен один акселератор для вызова
// пункта из меню MENUP
MENUP ACCELERATORS
{
 VK_F5, 4, VIRTKEY, NOINVERT
}

; файл menu2.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY             equ 2
; сообщение приходит при создании окна
WM_CREATE              equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_COMMAND             equ 111h
WM_MENUSELECT          equ 11Fh
WM_SETTEXT             equ 0Ch
MIIM_TYPE              equ 10h
MF_STRING              equ 0h
MF_POPUP               equ 10h

; свойства окна
CS_VREDRAW             equ 1h
CS_HREDRAW             equ 2h
CS_GLOBALCLASS         equ 4000h
WS_OVERLAPPEDWINDOW    equ 000CF0000H
STYLE equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
BS_DEFPUSHBUTTON       equ 1h
WS_VISIBLE             equ 10000000h
WS_CHILD               equ 40000000h
STYLBTN equ WS_CHILD+BS_DEFPUSHBUTTON+WS_VISIBLE
; идентификатор стандартной иконки
IDI_APPLICATION        equ 32512
; идентификатор курсора
IDC_ARROW              equ 32512
; режим показа окна - нормальный
SW_SHOWNORMAL          equ 1
SW_HIDE                equ 0
SW_SHOWMINIMIZED       equ 2

; прототипы внешних процедур
EXTERN   wsprintfA:NEAR
EXTERN   GetMenuItemInfoA@16:NEAR
EXTERN   LoadMenuA@8:NEAR
EXTERN   SendMessageA@16:NEAR
EXTERN   MessageBoxA@16:NEAR
EXTERN   CreateWindowExA@48:NEAR
EXTERN   DefWindowProcA@16:NEAR
EXTERN   DispatchMessageA@4:NEAR
EXTERN   ExitProcess@4:NEAR
EXTERN   GetMessageA@16:NEAR
EXTERN   GetModuleHandleA@4:NEAR
EXTERN   LoadCursorA@8:NEAR
EXTERN   LoadIconA@8:NEAR
EXTERN   PostQuitMessage@4:NEAR
EXTERN   RegisterClassA@4:NEAR
EXTERN   ShowWindow@8:NEAR
EXTERN   TranslateMessage@4:NEAR
EXTERN   UpdateWindow@4:NEAR
EXTERN   TranslateAcceleratorA@12:NEAR
EXTERN   LoadAcceleratorsA@8:NEAR
EXTERN   GetMenu@4:NEAR
EXTERN   DestroyMenu@4:NEAR
EXTERN   SetMenu@8:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ?
     MSMESSAGE DD ?
     MSWPARAM  DD ?
     MSLPARAM  DD ?
     MSTIME    DD ?
     MSPT      DD ?
MSGSTRUCT ENDS

;----структура класса окон
WNDCLASS STRUC
     CLSSTYLE   DD ?
     CLWNDPROC  DD ?
     CLSCBCLSEX DD ?
     CLSCBWNDEX DD ?
     CLSHINST   DD ?
     CLSHICON   DD ?
     CLSHCURSOR DD ?
     CLBKGROUND DD ?
     CLMENNAME  DD ?
     CLNAME     DD ?
WNDCLASS ENDS

MENINFO STRUCT
     cbSize        DD ?
     fMask         DD ?
     fType         DD ?
     fState        DD ?
     wID           DD ?
     hSubMenu      DD ?
     hbmpChecked   DD ?
     hbmpUnchecked DD ?
     dwItemData    DD ?
     dwTypeData    DD ?
     cch           DD ?
MENINFO ENDS

; файл menu2.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include menu2.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
; ------------------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     SPACE     DB 30 dup(32),0
     MENI      MENINFO  <0>
     NEWHWND   DD 0
     MSG       MSGSTRUCT <?>
     WC        WNDCLASS  <?>
     HINST     DD 0  ; дескриптор приложения
     CLASSNAME DB 'CLASS32',0
     CPBUT     DB 'Кнопка',0 ; выход
     CLSBUTN   DB 'BUTTON',0
     HWNDBTN   DD 0
     CAP       DB 'Сообщение',0
     MES       DB 'Конец работы программы',0
     MEN       DB 'MENUP',0
     MENC      DB 'MENUC',0
     ACC       DD ?
     HMENU     DD ?
     PRIZN     DD ?
     BUFER     DB 100 DUP(0)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; инициализировать счетчик
     MOV  PRIZN, 2
; получить дескриптор приложения
     PUSH 0
     CALL GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
     MOV  [WC.CLSSTYLE], STYLE
; процедура обработки сообщений
     MOV  [WC.CLWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEX], 0
     MOV  [WC.CLSCBWNDEX], 0
     MOV EAX, [HINST]
     MOV  [WC.CLSHINST], EAX
; ----------иконка окна
     PUSH IDI_APPLICATION
     PUSH 0
     CALL LoadIconA@8
     MOV  [WC.CLSHICON], EAX
; ----------курсор окна
     PUSH IDC_ARROW
     PUSH 0
     CALL LoadCursorA@8
     MOV  [WC.CLSHCURSOR], EAX
; ——————----
     MOV  [WC.CLBKGROUND], 17 ; цвет окна
     MOV DWORD PTR [WC.CLMENNAME], OFFSET MEN
     MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
     PUSH OFFSET WC
     CALL RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH 0
     PUSH 400  ; DY - высота окна
     PUSH 400  ; DX - ширина окна
     PUSH 100  ; Y
     PUSH 100  ; X
     PUSH WS_OVERLAPPEDWINDOW
     PUSH OFFSET SPACE ; имя окна
     PUSH OFFSET CLASSNAME ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
; проверка на ошибку
     CMP EAX, 0
     JZ  _ERR
     MOV  [NEWHWND], EAX ; дескриптор окна
; определить идентификатор меню
     PUSH EAX
     CALL GetMenu@4
     MOV HMENU, EAX
; загрузить акселераторы
     PUSH OFFSET MEN
     PUSH [HINST]
     CALL LoadAcceleratorsA@8
     MOV ACC, EAX
; --———————————————————-
     PUSH SW_SHOWNORMAL
     PUSH [NEWHWND]
     CALL ShowWindow@8 ; показать созданное окно
; -------------------------
     PUSH [NEWHWND]
     CALL UpdateWindow@4 ; команда перерисовать видимую
                         ; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET MSG
     CALL GetMessageA@16
     CMP EAX, 0
     JE  END_LOOP
     PUSH OFFSET MSG
     PUSH [ACC]
     PUSH [NEWHWND]
     CALL TranslateAcceleratorA@12
     CMP EAX, 0
     JNE MSG_LOOP
     PUSH OFFSET MSG
     CALL TranslateMessage@4
     PUSH OFFSET MSG
     CALL DispatchMessageA@4
     JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH [MSG.MSWPARAM]
     CALL ExitProcess@4
_ERR:
     JMP END_LOOP

;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC    PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
; сообщение WM_DESTROY - при закрытии окна
     CMP DWORD PTR [EBP+0CH], WM__DESTROY
     JE  WMDESTROY
; сообщение WM CREATE - при создании окна
     CMP DWORD PTR [EBP+0CH], WM_CREATE
     JE  WMCREATE
; сообщение WM COMMAND - при событиях с элементами на окне
     CMP DWORD PTR [EBP+0CH], WM_COMMAND
     JE  WMCOMMND
; сообщение WM_MENUSELECT - события, связанные с меню
     CMP DWORD PTR [EBP+0CH], WM_MENUSELECT
     JE WMMENUSELECT
; остальные события возвращаем обратно
     JMP DEFWNDPROC
WMMENUSELECT:
; пропускаем первое сообщение при обращении к меню
     CMP WORD PTR [EBP+14Н],0
     JE  FINISH
; проверяем, что активизировано - пункт меню
;или заголовок выпадающего меню
     MOV EDX, 0
     TEST WORD PTR [EBP+12H],MF_POPUP
     SETNE DL
; заполнение структуры для вызова функции
; GetMenuItemInfo
     MOVZX EAX,WORD PTR [EBP+10H] ; идентификатор
     MOV MENI.cbSize,48
     MOV MENI.fMask, MIIM_TYPE
     MOV MENI.fType, MF_STRING
     MOV EBX, DWORD PTR [EBP+14H]
     MOV MENI.hSubMenu, EBX
     MOV MENI.dwTypeData, OFFSET BUFER
     MOV MENI.cch, 100
; получить информацию о выбранном пункте меню
     PUSH OFFSET MENI
     PUSH EDX
     PUSH EAX
     PUSH DWORD PTR [EBP+14H]
     CALL GetMenuItemInfoA@16
; проверить результат выполнения функции
     CMP EAX, 0
     JE FINISH
; вывести название пункта меню
     PUSH MENI.dwTypeData
     PUSH 0
     PUSH WM_SETTEXT
     PUSH DWORD PTR [EBP+0BH]
     CALL SendMessageA@16
     MOV EAX, 0
     JMP FINISH
WMCOMMND:
     MOV EAX, HWNDBTN
; проверить, не нажата ли кнопка
     CMP DWORD PTR [EBP+14Н], EAX
     JE  YES_BUT
; проверить, не выбран ли пункт меню MENUC - Выход
     CMP WORD PTR [EBP+10Н],5
     JE  WMDESTROY
; проверить, не выбран ли пункт меню с идентификатором 5
     CMP WORD PTR [EBP+10Н], 4
     JNE L00
     JMP YES_BUT
L00:
     MOV EAX, 0
     JMP FINISH
YES_BUT:
; здесь обработка нажатия кнопки
; вначале стереть надпись в заголовке
     PUSH OFFSET SPACE
     PUSH 0
     PUSH WM_SETTEXT
     PUSH DWORD PTR [EBP+08H]
     CALL SendMessageA@16
; проверить загружено или нет меню
     CMP PRIZN, 0
     JE  L1
     CMP PRIZN, 1
     JE  L2
; загрузить меню MENC
     PUSH OFFSET MENC
     PUSH [HINST]
     CALL LoadMenuA@8
; установить меню
     MOV HMENU, EAX
     PUSH EAX
     PUSH DWORD PTR [EBP+08H]
     CALL SetMenu@8
; установить признак
     MOV PRIZN,0
     MOV EAX,0
     JMP FINISH
L2:
; загрузить меню MENUP
     PUSH OFFSET MEN
     PUSH [HINST]
     CALL LoadMenuA@8
; установить меню
     MOV  HMENU, EAX
     PUSH EAX
     PUSH DWORD PTR [EBP+08H]
     CALL SetMenu@8
; установить признак
     MOV PRIZN, 2
     MOV EAX,0
     JMP FINISH
L1:
; удалить меню
     PUSH HMENU
     CALL DestroyMenu@4
; обновить содержимое окна
     PUSH SW_SHOWMINIMIZED
     PUSH DWORD PTR [EBP+08H]
     CALL ShowWindow@8
     PUSH SW_SHOWNORMAL
     PUSH DWORD PTR [EBP+08H]
     CALL ShowWindow@8
     MOV PRIZN,1
     MOV EAX, 0
     JMP FINISH
WMCREATE:
; создать окно-кнопку
     PUSH 0
     PUSH [HINST]
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     PUSH 20   ; DY
     PUSH 60   ; DX
     PUSH 10   ; Y
     PUSH 10   ; X
     PUSH STYLBTN
; имя окна (надпись на кнопке)
     PUSH OFFSET CPBUT
     PUSH OFFSET CLSBUTN ; имя класса
     PUSH 0
     CALL CreateWindowExA@48
     MOV HWNDBTN, EAX ; запомнить дескриптор кнопки
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH DWORD PTR [EBP+14H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     CALL DefWindowProcA@16
     JMP  FINISH
WMDESTROY:
     PUSH 0         ; MB_OK
     PUSH OFFSET CAP
     PUSH OFFSET MES
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
     PUSH 0
     CALL PostQuitMessage@4 ; сообщение WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 2.4.1. Пример манипуляции с меню.

Программа на Рис. 2.4.1 имеет ряд механизмов, к обсуждению которых я намерен сейчас приступить. Для начала замечу, что в программе используются три ресурса: два меню и таблица акселераторов (см. файл ресурсов).

1. Первое, на что хочу обратить Ваше внимание, - это переменная PRIZN. В ней хранится состояние меню: 2 - загружено меню MENUP, 1 - меню отсутствует, 0 - загружено меню MENUC. Начальное состояние обеспечивается заданием меню при регистрации класса окна:

MOV DWORD PTR [WC.CLMENNAME], OFFSET MEN

2. Второе - это кнопка. Механизм распознавания нажатия кнопки мы уже разбирали, так что больше на этом останавливаться не будем. Одно из событий, которое может произойти при нажатии кнопки - это удаление меню. Удаляется меню при помощи функции DestroyMenu. После удаления необходимо обновить содержимое окна. Это достигается последовательностью двух команд ShowWindow.

3. Еще одно событие, которое происходит при нажатии кнопки, это смена меню. Интересно, что смена меню происходит автоматически, если мы загрузим и установим новое меню.

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

5. Интересная ситуация возникает с акселератором. Акселераторная клавиша у нас F5. При ее нажатии генерируется такое же сообщение, как при выборе пункта "Четвертый" меню MENUP. Важно то, что такое же сообщение будет генерироваться и тогда, когда загружается меню MENUC и когда меню не будет. А поскольку наша процедура обрабатывает сообщение в любом случае, клавиша F5 будет срабатывать всегда.

6. Рассмотрим теперь то, как производится определение названия выбранного пункта меню. Центральную роль в этом механизме играет сообщение WM_MENUSELECT. Это сообщение приходит всегда, когда выбирается пункт меню. Тут важно отметить, что когда мы активизируем меню, то в начале приходит сообщение WM_MENUSELECT со значением LPARAM, которое определяет идентификатор меню равным нулю. Этим целям служат строки:

CMP WORD PTR [EBP+14H], 0
JE  FINISH

7. По получении сообщения WM_MENUSELECT в младшем слове параметра WPARAM может содержаться либо идентификатор пункта меню, либо номер заголовка выпадающего меню. Это ключевой момент. Нам важно это знать, так как строка заголовка выпадающего меню и строка пункта меню получаются по-разному. Определить, что выбрано, можно по старшему слову WPARAM. Мы используем для этого константу MF_POPUP: TEST WORD PTR [EBP+12H], MF_POPUP. Обратите внимание, как удобна и как кстати здесь команда SETNE.

8. Далее, для получения строки-названия используется функция GetMenuItemInfo. Третьим параметром этой функции как раз и может быть либо ноль, либо единица. Если ноль, то второй параметр - это идентификатор пункта меню, если единица, то второй параметр - номер заголовка выпадающего меню. Четвертым параметром является указатель на структуру, которая и будет заполняться в результате выполнения функции. Некоторые поля этой структуры должны быть, однако, заполнены заранее. Обращаю внимание на поле dwTypeData, которое должно содержать указатель на буфер, получающий необходимую нам строку. При этом поле cch должно содержать длину этого буфера. Но для того чтобы поля dwTypeData и cch трактовались функцией именно как указатель на буфер и его длину, поля fMask и fType должны быть правильно заполнены (см. программу). Наконец, поле cbSize должно содержать длину всей структуры.

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

II

Итак, продолжим рассматривать ресурсы. Хочется рассказать о весьма интересном приеме, который можно использовать при работе с окнами редактирования. Наверное, Вы работали с визуальными языками типа Visual Basic, Delphi и пр. и обратили внимание, что окна редактирования можно так запрограммировать, а точнее, задать их свойства, что они позволят вводить только вполне определенные символы. В Delphi это свойство называется EditMask. Я думаю. Вам хотелось бы понять, как подобное реализовать только API-средствами. Но обо всем по порядку.

Обычное окно при нажатии клавиши (если в нем находится фокус) получает сообщения WM_KEYDOWN, WM_KEYUP и их квинтэссенцию WM_CHAR. Но в данном случае мы имеем дело не с обычным окном, а с диалоговым. Диалоговое окно таких сообщений не получает. Остается надеяться на сообщения, посылаемые на события, происходящие с самим элементом "окном редактирования". Но, увы, и здесь нас ждут разочарования. Данный элемент получает лишь два сообщения из тех, которые нас хоть как-то могут заинтересовать. Это сообщение EN_UPDATE и сообщение EN_CHANGE. Оба сообщения приходят, когда уже произведено изменение в окне редактирования. Но сообщение EN_UPDATE приходит, когда изменения на экране еще не произведены, а EN_CHANGE - после таких изменений. Нам придется сначала получить содержимое окна редактирования, определить, какой символ туда поступил последним, и если он недопустим, удалить его из строки и послать строку в окно снова. Добавьте сюда еще проблему, связанную с положением курсора и вторичным приходом сообщения EN_UPDATE. Лично я по такому пути бы не пошел.

Есть другой более изящный и короткий путь: использовать понятие горячей клавиши (HOTKEY). Мы ограничимся лишь программными свойствами горячих клавиш, то есть свойствами, которые необходимо знать программисту, чтобы использовать горячие клавиши в своих программах.

Горячая клавиша может быть определена для любой виртуальной клавиши, клавиши, определяемой через макроконстанты с префиксом VK. Для обычных алфавитно-цифровых клавиш значение этих констант просто совпадает с кодами ASCII. Возможны также сочетания с управляющими клавишами Alt, Control, Shift. После того как для данного окна определена горячая клавиша, при ее нажатии на функцию окна приходит сообщение WM_HOTKEY. По параметрам можно определить, какая именно горячая клавиша была нажата. Существенно, что понятие горячей клавиши глобально, т.е. она будет срабатывать, если будут активны другие окна и даже окна других приложений. Это требует от программиста весьма аккуратной работы, так как вы можете заблокировать нормальную работу других приложений. Т.е. необходимо отслеживать, когда данное окно активно, а когда нет. Этому весьма могут помочь сообщения WM_ACTIVATE и WM_ACTIVATEAPP. Первое сообщение всегда приходит на функцию окна тогда, когда окно активизируется или деактивизируется. Первый раз сообщение приходит при создании окна. Вот при получении этого сообщения и есть резон зарегистрировать горячие клавиши. Второе сообщение всегда приходит на функцию окна, когда окно теряет "фокус" - активизируется другое окно. Соответственно, при получении этого сообщения и следует отменить регистрацию этих клавиш.

Для работы с горячими клавишами используют в основном две функции: RegisterHotKey и UnregisterHotKey. Функция RegisterHotKey имеет следующие параметры:

  • первый - дескриптор окна;
  • второй - идентификатор горячей клавиши;
  • третий - модификатор, определяющий, не нажата ли управляющая клавиша;
  • четвертый - виртуальный код клавиши.

Функция UnregisterHotKey имеет всего два параметра:

  • первый - дескриптор окна;
  • второй - идентификатор.

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

Рассмотрим простой пример диалогового окна, на котором расположены два окна редактирования и кнопка выхода. Поставим перед собой такую цель. Первое окно редактирования должно пропускать только цифры от 0 до 9. Во второе окно можно вводить все символы. Выше рассматривался возможный механизм использования горячих клавиш с сообщениями WM_ACTIVATE и WM_ ACTIVATEAPP. Ясно, что эти события в данном случае нам ничем не помогут. Здесь дело тоньше, надо использовать сообщения, относящиеся к одному окну редактирования. Это сообщения EN_SETFOCUS и EN_KILLFOCUS, передаваемые, естественно, через сообщение WM_COMMAND. Ниже представлена программа, демонстрирующая этот механизм, и комментарий к ней. Сообщение EN_SETFOCUS говорит о том, что окно редактирования приобрело фокус (стало активным), а сообщение EN_KILLFOCUS - что окно редактирования потеряло фокус.

// файл dial1.rc
// определение констант
// стили окна
#define WS_SYSMENU       0x00080000L
#define WS_MINIMIZEBOX   0x00020000L
#define WS_MAXIMIZEBOX   0x00010000L

// текст в окне редактирования прижат к левому краю
#define ES_LEFT      0x0000L
// стиль всех элементов на окне
#define WS_CHILD     0x40000000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE   0x10000000L
// бордюр вокруг элемента
#define WS_BORDER    0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP   0x00010000L
// прижать строку к левому краю отведенного поля
#define SS_LEFT      0x00000000L
// стиль кнопка
#define BS_PUSHBUTTON  0x00000000L
// центрировать текст на кнопке
#define BS_CENTER     0x00000300L
#define DS_LOCALEDIT  0x20L

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
// поле редактирования, идентификатор 1
CONTROL "", 1, "edit", ES_LEFT | WS_CHILD
 | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 24, 20, 128, 12
// еще одно поле редактирования, идентификатор 2
CONTROL "", 2, "edit", ES_LEFT | WS_CHILD
 | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 24, 52, 127, 12
// текст, идентификатор 3
CONTROL "Строка 1", 3, "static", SS_LEFT
 | WS_CHILD | WS_VISIBLE, 164, 22, 60, 8
// еще текст, идентификатор 4
CONTROL "Строка 2", 4, "static", SS_LEFT
 | WS_CHILD | WS_VISIBLE, 163, 54, 60, 8
// кнопка, идентификатор 5
CONTROL "Выход", 5, "button", BS_PUSHBUTTON
 | BS_CENTER | WS_CHILD | WS_VISlBLE | WS_TABSTOP,
 180, 76, 50, 14
}

;файл dial1.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE      equ  10h
WM_INITDIALOG equ 110h
WM_COMMAND    equ 111h
WM_SETTEXT    equ  0Ch
WM_HOTKEY     equ 312h
EN_SETFOCUS   equ 100h
EN_KILLFOCUS  equ 200h

; прототипы внешних процедур
EXTERN      UnregisterHotKey@8:NEAR
EXTERN      RegisterHotKey@16:NEAR
EXTERN      MessageBoxA@16:NEAR
EXTERN      ExitProcess@4:NEAR
EXTERN      GetModuleHandleA@4:NEAR
EXTERN      DialogBoxParamA@20:NEAR
EXTERN      EndDialog@8:NEAR
EXTERN      SendMessageA@16:NEAR
EXTERN      GetDlgItem@8:NEAR
EXTERN      MessageBoxA@16:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DWORD ?
     MSMESSAGE  DWORD ?
     MSWPARAM   DWORD ?
     MSLPARAM   DWORD ?
     MSTIME     DWORD ?
     MSPT       DWORD ?
MSGSTRUCT ENDS

;файл dial.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dial1.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
; ------------------------------------------------------------
; сегмент данных DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG   MSGSTRUCT <?>
     HINST DD 0    ; дескриптор приложения
     PA    DB "DIAL1",0
     STR1  DB "Неправильный символ !",0
     STR2  DB "Ошибка !",0
; таблица для создания горячих клавиш
TAB  DB 32,33,34,35,36,37,38,39,40
     DB 41,42,43,44,45,46,47,58,59,60
     DB 61,62,63,64,65,66,67,68,69,70
     DB 71,72,73,74,75,76,77,78,79,80
     DB 81,82,83,84,85,86,87,88,89,90
     DB 91,92,93,94,95,96,97,98,99,100
     DB 101,102,103,104,105,106,107,108,109,110
     DB 111,112,113,114,115,116,117,118,119,120
     DB 121,122,123,124,125,126,127,128,129,130
     DB 131,132,133,134,135,136,137,138,139,140
     DB 141,142,143,144,145,146,147,148,149,150
     DB 151,152,153,154,155,156,157,158,159,160
     DB 161,162,163,164,165,166,167,168,169,170
     DB 171,172,173,174,175,176,177,178,179,180
     DB 181,182,183,184,185,186,187,188,189,190
     DB 191,192,193,194,195,196,197,198,199,200
     DB 201,202,203,204,205,206,207,208,209,210
     DB 211,212,213,214,215,216,217,218,219,220
     DB 221,222,223,224,225,226,227,228,229,230
     DB 231,232,233,234,235,236,237,238,239,240
     DB 241,242,243,244,245,246,247,248,249,250
     DB 251,252,253,254,255
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH 0
     CALL GetModuleHandleA@4
     MOV [HINST], EAX
;----------------------------------------
     PUSH 0
     PUSH OFFSET WNDPROC
     PUSH 0
     PUSH OFFSET PA
     PUSH [HINST]
     CALL DialogBoxParamA@20
     CMP EAX,-1
     JNE KOL
KOL:
;----------------------------------------
     PUSH 0 CALL
     ExitProcess@4
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;----------------------------------------
     CMP DWORD PTR [EBP+0CH], WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     MOV EAX, 1
     JMP FIN
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
; здесь заполнить окна редактирования, если надо
;
;
     MOV EAX, 1
     JMP FIN
L2:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JNE L5
; кнопка выхода ?
     CMP WORD PTR [EBP+10H], 5
     JNE L3
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     MOV EAX, 1
     JMP FIN
L3:
     CMP WORD PTR [EBP+10H], 1
     JNE FINISH
; блок обработки сообщений первого окна редактирования
     CMP WORD PTR [EBP+12H], EN_KILLFOCUS
     JNE L4
; окно редактирования с идентификатором 1 теряет фокус
     MOV EBX, 0
; снимаем все горячие клавиши
L33:
     MOVZX EAX,BYTE PTR [ТАВ+EBX]
     PUSH EAX
     PUSH DWORD PTR [EBP+08Н]
     CALL UnregisterHotKey@8
     INC EBX
     CMP EBX, 214
     JNE L33
     MOV EAX, 1
     JMP FIN
L4:
     CMP WORD PTR [EBP+12H],EN_SETFOCUS
     JNE FINISH
; окно редактирования с идентификатором 1 получает фокус
     MOV EBX, 0
; устанавливаем горячие клавиши
L44:
     MOVZX EAX,BYTE PTR [ТАВ+EBX]
     PUSH EAX
     PUSH 0
     PUSH EAX
     PUSH DWORD PTR [EBP+08Н]
     CALL RegisterHotKey@16
     INC EBX
     CMP EBX, 214
     JNE L44
     MOV EAX, 1
     JMP FIN
L5:
     CMP DWORD PTR [EBP+0CH],WM_HOTKEY
     JNE FINISH
; здесь реакция на неправильно введенный символ
     PUSH 0         ; МВ_ОК
     PUSH OFFSET STR2
     PUSH OFFSET STR1
     PUSH DWORD PTR [EBP+08Н] ; дескриптор окна
     CALL MessageBoxA@16
FINISH:
     MOV EAX, 0
FIN:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 2.4.2. Пример использования горячих клавиш с диалоговым окном.

Комментарий к программе на Рис. 2.4.2.

1. Самое главное: разберитесь с тем, как мы определяем, когда первое окно редактирования теряет, когда приобретает фокус. В начале определяется, что сообщение пришло от окна редактирования с идентификатором 1, а затем - какое сообщение пришло: EN_SETFOCUS или EN_KILLFOCUS. В первом случае мы устанавливаем горячие клавиши, а во втором снимаем горячие клавиши.

2. В области данных задаем таблицу горячих клавиш. Функция RegisterHotKey имеет следующие параметры:

  • 1-й параметр - идентификатор окна.
  • 2-й параметр - идентификатор горячей клавиши.
  • 3-й параметр - флаг нажатия управляющих клавиш.
  • 4-й параметр - виртуальный код клавиши.

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

III

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

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

32 Впрочем, это можно сказать о любых элементах на диалоговом окне. Не правда ли, это весьма похоже на методы и свойства в объектном программировании. Но мы то с Вами знаем, что если углубиться еще дальше, то мы обнаружим, что значительная часть свойств опять сведется к обработке сообщений (см. комментарий к программе на Рис. 2.4.3).

// файл diallst.rc

// определение констант
#define WS_SYSMENU     0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
#define WS_VISIBLE     0x10000000L
#define WS_TABSTOP     0x00010000L
#define WS_VSCROLL     0x00200000L
#define WS_THICKFRAME  0x00040000L
#define LBS_NOTIFY     0x0001L
#define LBS_SORT       0x0002L
#define LBS_WANTKEYBOARDINPUT 0x0400L

// идентификаторы
#define LIST1 101
#define LIST2 102
#define IDI_ICON1 3

// определили иконку
IDI_ICON1 ICON "ico1.ico"

// определение диалогового окна
DIAL1 DIALOG 0, 0, 210, 110
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
 CONTROL "ListBox1",LIST1, "listbox", WS_VISIBLE |
 WS_TABSTOP | WS_VSCROLL | WS_THICKFRAME | LBS_NOTIFY |
 LBS_WANTKEYBOARDINPUT,
 16, 16, 70, 75
 CONTROL "ListBox2", LIST2, "listbox", WS_VISIBLE |
 WS_TABSTOP | WS_VSCROLL | WS_THICKFRAME | LBS_NOTIFY | LBS_SORT,
 116, 16, 70, 75
}

; файл diallst.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE       equ  10h
WM_INITDIALOG  equ 110h
WM_SETICON     equ  80h
WM_COMMAND     equ 111h
WM_VKEYTOITEM  equ  2Eh
LB_ADDSTRING   equ 180h
LBN_DBLCLK     equ   2
LB_GETCURSEL   equ 188h
LB_GETTEXT     equ 189h
LB_FINDSTRING  equ 18Fh
VK_INSERT      equ  2Dh

; прототипы внешних процедур
EXTERN      ExitProcess@4:NEAR
EXTERN      GetModuleHandleA@4:NEAR
EXTERN      DialogBoxParamA@20:NEAR
EXTERN      EndDialog@8:NEAR
EXTERN      LoadIconA@8:NEAR
EXTERN      SendMessageA@16:NEAR
EXTERN      SendDlgItemMessageA@20:NEAR
EXTERN      MessageBoxA@16:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DD ?
     MSMESSAGE  DD ?
     MSWPARAM   DD ?
     MSLPARAM   DD ?
     MSTIME     DD ?
     MSPT       DD ?
MSGSTRUCT ENDS

; файл diallst.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dial.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG       MSGSTRUCT <?>
     HINST     DD 0 ; дескриптор приложения
     PA        DB "DIAL1",0
     BUFER     DB 100 DUP (0)
     STR1      DB "Первый",0
     STR2      DB "Второй",0
     STR3      DB "Третий",0
     STR4      DB "Четвертый",0
     STR5      DB "Пятый ",0
     STR6      DB "Шестой",0
     STR7      DB "Седьмой",0
     STR8      DB "Восьмой",0
     STR9      DB "Девятый",0
     STR10     DB "Десятый",0
     STR11     DB "Одиннадцатый",0
     STR12     DB "Двенадцатый",0
     STR13     DB "Тринадцатый",0
     STR14     DB "Четырнадцатый",0
     STR15     DB "Пятнадцатый",0
     INDEX     DD OFFSET STR1
               DD OFFSET STR2
               DD OFFSET STR3
               DD OFFSET STR4
               DD OFFSET STR5
               DD OFFSET STR6
               DD OFFSET STR7
               DD OFFSET STR8
               DD OFFSET STR9
               DD OFFSET STR10
               DD OFFSET STR11
               DD OFFSET STR12
               DD OFFSET STR13
               DD OFFSET STR14
               DD OFFSET STR15
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;----------------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
; сообщение об ошибке
KOL:
;----------------------------------------
     PUSH  0
     CALL  ExitProcess@4
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP, ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;----------------------------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH], WM_INITDIALOG
     JNE L2
; загрузить иконку
     PUSH  3  ; идентификатор иконки
     PUSH [HINST] ; идентификатор процесса
     CALL LoadIconA@8
; установить иконку
     PUSH EAX
     PUSH 0  ; тип иконки (маленькая)
     PUSH WM_SETICON
     PUSH DWORD PTR [EBP+08H]
     CALL SendMessageA@16
; заполнить левый список
     MOV ECX, 15
     MOV ESI, 0
L01:
     PUSH ECX ; сохранить параметр цикла
     PUSH INDEX[ESI]
     PUSH 0
     PUSH LB_ADDSTRING
     PUSH 101
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA@20
     ADD ESI, 4
     POP ECX
     LOOP L01
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JNE L3
; не сообщение ли от левого списка?
     CMP WORD PTR [EBP+10Н],101
     JNE FINISH
; не было ли двойного щелчка?
     CMP WORD PTR [EBP+12H], LBN_DBLCLK
     JNE FINISH
; был двойной щелчок, теперь определим элемент
; получить индекс выбранного элемента
L4:
      PUSH 0
      PUSH 0
      PUSH LB_GETCURSEL
      PUSH 101
      PUSH DWORD PTR [EBP+08H]
      CALL SendDlgItemMessageA@20
; скопировать элемент списка в буфер
      PUSH OFFSET BUFER
      PUSH EAX ; индекс записи
      PUSH LB_GETTEXT
      PUSH 101
      PUSH DWORD PTR [EBP+08H]
      CALL SendDlgItemMessageA@20
; определить, нет ли элемента в правом списке
      PUSH OFFSET BUFER
      PUSH -1 ; искать во всем списке
      PUSH LB_FINDSTRING
      PUSH 102
      PUSH DWORD PTR [EBP+08H]
      CALL SendDlgItemMessageA@20
      CMP EAX,-1
      JNE FINISH ; элемент нашли
; не нашли, можно добавлять
      PUSH OFFSET BUFER
      PUSH 0
      PUSH LB_ADDSTRING
      PUSH 102
      PUSH DWORD PTR [EBP+08H]
      CALL SendDlgItemMessageA@20
      MOV EAX,-1
      JMP FIN
L3:
; здесь проверка, не нажата ли клавиша
      CMP DWORD PTR [EBP+0CH],WM_VKEYTOITEM
      JNE FINISH
      CMP WORD PTR [EBP+10H],VK_INSERT
      JE L4
      MOV EAX,-1
      JMP FIN
FINISH:
      MOV EAX, 0
FIN:
      POP EDI
      POP ESI
      POP EBX
      POP EBP
      RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 2.4.3. Пример программы с двумя списками. Перебросить запись из левого списка в правый можно двойным щелчком мыши или клавишей INSERT.

Комментарий к программе на Рис. 2.4.3.

  1. В первую очередь обратите внимание на функцию SendDIgItemMessage. Для посылки сообщения элементам диалогового окна эта функция более удобна, чем SendMessage, так как элемент в ней идентифицируется не дескриптором (который еще надо узнать), а номером, определенным в файле ресурсов.
  2. Взглянув на файл ресурсов. Вы увидите, что второму (правому) списку присвоено свойство LBS_SORT. Если такое свойство присвоено списку, то при добавлении в него элемента (сообщение LB_ADDSTRING) этот элемент помещается в список так, что список остается упорядоченным. Свойство LBS_SORT стоит системе Windows довольно большой работы. Посредством сообщения WM_COMPAREITEM она определяет нужное положение нового элемента в списке, а затем вставляет его при помощи сообщения LB_INSERTSTRING.
  3. Хотелось бы также обратить внимание на цикл заполнения левого списка. Нам приходится хранить регистр ECX в стеке. Вы скажете - дело обыкновенное при организации цикла при помощи команды LOOP. А я Вам скажу, что это совсем не очевидно. К сожалению, в документации по функциям API и сообщениям не указывается, какие регистры микропроцессора сохраняются, а какие нет. Все это придется устанавливать экспериментально. Что и было сделано мною в данном примере.
  4. Сообщение WM_VKEYTOITEM приходит при нажатии какой-либо клавиши, при наличии фокуса на списке. При этом список должен иметь свойство LBS_WANTKEYBOARDINPUT. Именно потому, что данное свойство установлено только у левого списка, у нас нет необходимости проверять, от какого списка пришло сообщение.


Часть II. Глава 5. Управление файлами

Глава 5. Управление файлами

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

I

32-битная FAT. Характеристики файлов

Давая описание характеристикам файлов, я буду основываться на характеристиках, которыми манипулируют функции API. О типах и структуре файловых систем речь пойдет далее.

Атрибут файла. Размер - DWORD.
FILE_ATTRIBUTE_READONLY equ 1h
Атрибут - "только чтение". Приложения могут лишь читать данный файл. Соответственно, попытка открыть файл для записи вызовет ошибку.
FILE_ATTRIBUTE_HIDDEN equ 2h
Атрибут - "скрытый файл". "Невиден" при обычном просмотре каталога (см. ниже, поиск файлов).
FILE_ATTRIBUTE_SYSTEM equ 4h
Атрибут - "системный файл". Говорит о том, что данный файл принадлежит операционной системе.
FILE_ATTRIBUTE_DIRECTORY equ 10h
Атрибут - "директорий", С файлами с таким атрибутом операционная система обращается особым образом, считая его каталогом, т.е. считая его списком файлов, состоящим из записей по 32 байта.
FILE_ATTRIBUTE_ARCHIVE equ 20h
Со времен MS DOS таким атрибутом отмечались файлы, над которыми не произведена операция BACKUP или XCOPY. Для целей программирования данный атрибут эквивалентен нулевому значению атрибута.
FILE_ATTRIBUTE_NORMAL equ 80h
Данный атрибут означает, что у файла не установлены другие атрибуты.
FILE_ATTRIBUTE_TEMPORARY equ 100h
Атрибут означает, что данный файл предназначен для временного хранения. После закрытия файла система должна его удалить.
FILE_ATTRIBUTE_COMPRESSED equ 800h
Для файла это означает, что он сжат системой; для директория - что вновь создаваемый файл по умолчанию должен быть сжат.
FILE_ATTRIBUTE_OFFLINE equ 1000h
Данный атрибут означает, что данные файла не доступны в данный момент.

Смену атрибута можно осуществить функцией SetFileAttributes, получить значение атрибута функцией GetFileAttributes. Следует заметить, что если операционная система не накладывает никаких ограничений на возможности изменения атрибутов файлов, то фактически обесценивается смысл самих атрибутов - всегда можно снять атрибут "только чтение" и делать с файлом, что заблагорассудится.

Файл имеет три временные характеристики: время создания, время последней модификации, время последнего доступа. Время отсчитывается в наносекундных интервалах начиная с 12.00 по полудни 1 января 1600 года и хранится в двух 32-битных величинах. Надо сказать, что время хранится в так называемых универсальных координатах и должно еще быть преобразовано в локальное время (функция FileTimeToLocalFileTime). Получить значение всех трех времен можно функцией GetFileTime.

Длина файла в байтах хранится обычно в двух 32-битных величинах либо в одной 64-битной величине. Если 32-битные величины обозначить как l1 (младшая часть) и l2 (старшая часть), то 64-битная величина выразится формулой l2*0FFFFH+l1. Paзмер файла можно получить функцией GetFileSize.

Кроме указанных характеристик, файл, разумеется, имеет имя. При этом мы будем различать длинное и короткое имя. Точно также будем различать полный путь (со всеми длинными именами) и укороченный путь (все длинные имена заменены укороченными). Необходимость использования укороченного имени и пути диктуется, прежде всего, тем, что некоторые программы получают путь или имя на стандартный вход и трактуют пробелы как разделители для параметров. Преобразование длинного имени в короткое можно осуществить функцией GetShortPathName, которая работает и для имени, и для пути. Обратное преобразование можно осуществить функиией GetFullPathName.

В данной книге мы не рассматриваем вопроса о прямом доступе к диску. Но вопрос о структуре записей каталога может у читателя все же возникнуть. Это и понятно, ведь с переходом к FAT3233, во-первых, появилась возможность хранения файлов с длинным именем, во-вторых, у файла, кроме времени и даты модификации, появились еще время и дата создания и доступа. Где же все это хранится?

Для того чтобы ответить на поставленный вопрос, вспомним, что каталог в файловых системах FAT34 делится на записи длиной 32 байта. Ниже приводится структура записи для FAT32. Пустыми записями считаются записи, содержащие нулевые байты, либо начинающиеся с кода E5H (для удаленных записей). На файл с обычным именем (восемь байт на имя и 3-на расширение) отводится 32 байта. В байте со смещением +11 содержится атрибут файла. Если атрибут файла равен 0FH, то система считает, что здесь содержится длинное имя. Длинное имя кодируется в UNICODE и записывается в обратном порядке. За одной или несколькими записями с длинным именем должна следовать запись с обычным именем, содержащим знак "~" - тильда. Здесь содержится также остальная информация о файле. Как видите, алгоритм просмотра каталога с выявлением информации о файле весьма прост. Обратимся теперь к структуре записи каталога. В старой операционной системе MS DOS байты с 12 по 21 никак не использовались системой (см. [1]). Новой системе они пригодились. Ниже в таблице дана новая структура записи каталога.

Смещение | Размер | Содержимое
(+0) | 8 | Имя файла или каталога, выровненное на левую границу и дополненное пробелами.
(+8) | 3 | Расширение имени файла, выровненное на левую границу и дополненное пробелами.
(+11) | 1 | Атрибут файла.
(+12) | 2 | Время доступа.
(+14) | 2 | Время создания.
(+16) | 2 | Дата создания.
(+18) | 2 | Дата доступа.
(+20) | 2 | Два старших байта номера первого кластера файла.
(+22) | 2 | Время модификации файла.
(+24) | 2 | Дата модификации файла.
(+26) | 2 | Два младших байта номера первого кластера файла.
(+28) | 4 | Размер файла в байтах.

Как видите, все байты 32-байтной записи каталога теперь заняты. Лишний раз убеждаешься в первоначальной непродуманности файловой системы MS DOS, Это касается, в частности, длины файла. Как можно заметить, на длину файла отводится всего 4 байта. А как найти длину файла, если на нее требуется более 4 байт? Разумеется, в этом случае следует считать, что в каталоге хранятся младшие байты длины, а полную длину легко определить, обратившись к таблице размещения файлов. Но, согласитесь, что это уже явная недоработка. Странно также выглядит функция GetFileSize, которая возвращает четыре младших байта длины файла, старшие же байты возвращаются во втором параметре функции.

Иное дело в файловой системе NTFS, поддерживаемой Windows NT, изначально планируемой для работы с файлами больших размеров. Здесь для индексации кластеров используются 64-битные поля.

33 В начале Windows 95 работала с 16-битной FAT, но длинные имена уже поддерживала.

34 FAT (File Allocation Table) - один из элементов, на котором базируются файловые системы MS DOS и Windows 9х. По этой причине часто такие файловые системы называют FAT системами.

II

Поиск файлов. Для поиска файлов в Windows существуют две функции FindFirstFile и FindNextFile, очень похожие на аналогичные функции MS DOS и, как и там, работающие в паре. При успешном поиске первая функция возвращает некое число или идентификатор, который затем используется второй функцией для продолжения поиска.

Первым параметром функции FindFirstFile является указатель на строку для поиска файлов, второй параметр - указатель на структуру, которая получает информацию о найденных файлах. Функция FindNextFile первым своим параметром имеет идентификатор, полученный первой функцией, а вторым параметром - указатель на структуру. Эту структуру можно изучить по программе на Рис. 2.5.1.

Основным отличием этих функций от соответствующих функций MS DOS является то, что поиск ограничивается только маской поиска (*.*, *.ЕХЕ и т.п.). Если файл найден, то тогда по возвращаемой структуре, где содержится вся информация о нем, Вы уже можете решать, подходит файл или нет.

На Рис. 2.5.1 представлена программа, осуществляющая поиск файлов в указанном каталоге. Программа может иметь один или два параметра, или не иметь их вовсе. Если имеются два параметра, то первый параметр трактуется как каталог для поиска, причем программа учитывает, есть ли на конце косая черта или нет (допустимо c:, c:\, c:\windows\, c:\windows\system и т.п.). Второй параметр (в программе он третий, так как первым считается командная строка), если он есть, представляет собой маску поиска. Если его нет, то маска поиска берется в виде "*.*". Наконец, если параметров нет вообще, то поиск осуществляется в текущем каталоге по маске "*.*". Эту программу легко развить и сделать из нее полезную утилиту. Предоставляю это Вам, дорогой читатель. Ниже будет дан комментарий к означенной программе.

; файл FILES.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE  equ -10
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN lstrcatA@8:NEAR
EXTERN FindFirstFileA@8:NEAR
EXTERN FindNextFileA@8:NEAR
EXTERN FindClose@4:NEAR
;-----------------------------
; структура, используемая для поиска файла
; при помощи функций FindFirstFile и FindNextFile
_FIND STRUC
; атрибут файла
     ATR      DWORD ?
; время создания файла
     CRTIME   DWORD ?
              DWORD ?
; время доступа к файлу
     ACTIME   DWORD ?
              DWORD ?
; время модификации файла
     WRTIME   DWORD ?
              DWORD ?
; размер файла
     SIZEH    DWORD ?
     SIZEL    DWORD ?
; резерв
              DWORD ?
              DWORD ?
; длинное имя файла
     NAM      DB 260 DUP (0)
; короткое имя файла
     ANAM     DB 14 DUP(0)
_FIND ENDS
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     BUF    DB 0
            DB 100 dup(0)
     LENS   DWORD ? ; количество выведенных символов
     HANDL  DWORD ?
     HANDL1 DWORD ?
     MASKA  DB "*.*",0
     АР     DB "\",0
     FIN _FIND <0>
     TEXT   DB "Для продолжения нажмите клавишу ENTER",13,10,0
     BUFIN  DB 10 DUP(0)
     FINDH  DWORD ?
     NUM    DB 0
     NUMF   DWORD 0 ; счетчик файлов
     NUMD   DWORD 0 ; счетчик каталогов
     FORM   DB "Число найденных файлов: %lu",0
     FORM1  DB "Число найденных каталогов: %lu",0
     BUFER  DB 100 DUP (?)
     DIR    DB "<DIR>",0
     PAR    DB 0 ; количество параметров
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; получить HANDL1 ввода
     PUSH STD_INPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL1,EAX
; преобразовать строки для вывода
     PUSH OFFSET TEXT
     PUSH OFFSET TEXT
     CALL CharToOemA@8
     PUSH OFFSET FORM
     PUSH OFFSET FORM CALL
     CharToOemA@8
     PUSH OFFSET FORM1
     PUSH OFFSET FORM1
     CALL CharToOemA@8
; получить количество параметров
     CALL NUMPAR
     MOV PAR,EAX
; если параметр один, то искать в текущем каталоге
     CMP EAX, 1
     JE NO_PAR
;-------------------------------------------------
; получить параметр номером EDI
     MOV EDI, 2
     LEA EBX,BUF
     CALL GETPAR
     PUSH OFFSET BUF
     CALL LENSTR
; если в конце нет "\" - добавим
     CMP BYTE PTR [BUF+EBX-1],"\"
     JE NO_PAR
     PUSH OFFSET AP
     PUSH OFFSET BUF
     CALL lstrcatA@8
; нет ли еще параметра, где задана маска поиска
     CMP PAR,3
     JB NO_PAR
; получить параметр - маску поиска
     MOV EDI,3
     LEA EBX,MASKA
     CALL GETPAR
NO_PAR:
;-------------------------------------------------
     CALL FIND
; вывести количество файлов
     PUSH NUMF
     PUSH OFFSET FORM
     PUSH OFFSET BUFER
     CALL wsprintfA
     LEA EAX, BUFER
     MOV EDX,1
     CALL WRITE
; вывести количество каталогов
     PUSH NUMD
     PUSH OFFSET FORM1
     PUSH OFFSET BUFER
     CALL wsprintfA
     LEA EAX, BUFER
     MOV EDX, 1
     CALL WRITE
_END:
     PUSH 0
     CALL ExitProcess@4
;************************
; область процедур
;************************
; вывести строку (в конце перевод строки)
; EAX - на начало строки
; EDX - с переводом строки или без
WRITE PROC
;  получить длину параметра
     PUSH EAX
     CALL LENSTR
     MOV ESI,EAX
     CMP EDX,1
     JNE NO_ENT
; в конце - перевод строки
     MOV BYTE PTR [EBX+ESI],13
     MOV BYTE PTR [EBX+ESI+1],10
     MOV BYTE PTR [EBX+ESI+2],0
     ADD EBX,2
NO_ENT:
; вывод строки
     PUSH 0
     PUSH OFFSET LENS
     PUSH EBX
     PUSH EAX
     PUSH HANDL
     CALL WriteConsoleA@20
     RET
WRITE ENDP

; процедура определения длины строки
; строка - [EBP+08Н]
; длина в EBX
LENSTR PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EAX
;----------------------
     CLD
     MOV EDI, DWORD PTR [EBP+08Н]
     MOV EBX, EDI
     MOV ECX,100 ; ограничить длину строки
     XOR AL,AL
     REPNE SCASB ; найти символ 0
     SUB EDI, EBX ;-длина строки, включая 0
     MOV EBX, EDI
     DEC EBX
;----------------------
     POP EAX
     POP EBP
     RET 4
LENSTR ENDP

; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX, 0
     JMP L2
L3:
     OR EDX,1
L2:
     INC ESI
     JMP L1
L4:
     MOV EAX,ECX
     RET
NUMPAR ENDP

; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI, EAX ; указатель на строку
     XOR ECX, ECX ; счетчик
     MOV EDX, 1   ; признак
L1:
     CMP BYTE PTR [ESI], 0
     JE L4
     CMP BYTE PTR [ESI], 32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     CMP ECX,EDI
     JNE L5
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EBX],AL
     INC EBX
L5:
     INC ESI
     JMP L1
L4:
     MOV BYTE PTR [EBX], 0
     RET
GETPAR ENDP

; поиск в каталоге файлов и их вывод
; имя каталога в BUF
FIND  PROC
; путь с маской
     PUSH OFFSET MASKA
     PUSH OFFSET BUF
     CALL lstrcatA@8
; здесь начало поиска
     PUSH OFFSET FIN
     PUSH OFFSET BUF
     CALL FindFirstFileA@8
     CMP EAX,-1
     JE _ERR
; сохранить дескриптор поиска
     MOV FINDH,EAX
LF:
; исключить "файлы" "." и ".."
     CMP BYTE PTR FIN.NAM,"."
     JE _NO
; не каталог ли?
     TEST BYTE PTR FIN.ATR,10H
     JE NO_DIR
     PUSH OFFSET DIR
     PUSH OFFSET FIN.NAM
     CALL lstrcatA@8
     INC NUMD
     DEC NUMF
NO_DIR:
; преобразовать строку
     PUSH OFFSET FIN.NAM
     PUSH OFFSET FIN.NAM
     CALL CharToOemA@8
; здесь вывод результата
     LEA EAX, FIN.NAM
     MOV EDX,1
     CALL WRITE
; увеличить счетчики
     INC NUMF
     INC NUM
; конец страницы?
     CMP NUM, 22
     JNE _NO
     MOV NUM, 0
; ждать ввод строки
     MOV EDX,0
     LEA EAX, TEXT
     CALL WRITE
     PUSH 0
     PUSH OFFSET LENS
     PUSH 10
     PUSH OFFSET BUFIN
     PUSH HANDL1
     CALL ReadConsoleA@20
_NO:
; продолжение поиска
     PUSH OFFSET FIN
     PUSH FINDH
     CALL FindNextFileA@8
     CMP EAX,0
     JNE LF
; закрыть поиск
     PUSH FINDH
     CALL FindClose@4
_ERR:
     RET
FIND ENDP
_TEXT ENDS
END START

Рис. 2.5.1 Пример простой программы, которая осуществляет поиск файлов и выводит их название на экран.

Программа на Рис. 2.5.1 довольно проста. Из нового здесь Вы обнаружите лишь то, как обращаться с функциями FindFirstFile и FindNextFile. Процедуры, которые используются для работы с параметрами командной строки, Вы уже встречали ранее. Вывод информации осуществляется в текущую консоль, с чем Вы тоже знакомы. Для получения дескриптора консоли используется функция GetStdHandle. Процедура WRITE позволила несколько упростить те участки программы, которые отвечают за вывод информации на экран. Ранее я обещал, что мы не обойдем вниманием строковые API-функции. В данной программе это обещание выполнено, и наряду со строковыми процедурами "собственного изготовления" используется строковая функция lstrcat, которая осуществляет сложение (конкатенацию) строк. По поводу параметра в командной строке замечу, что при наличии в имени каталога пробела Вам придется задавать имя в укороченном виде. Так, например, вместо C:\Program Files придется написать C:\Progra~1. Это должно быть понятно - пробелы отделяют параметры. Чтобы корректно решать проблему, необходимо ввести специальный разделитель для параметров, например "-" или "/".

Данная программа осуществляет поиск в указанном или текущем каталоге. Если бы программа была написана на языке высокого уровня, например Си, ее легко можно было бы видоизменить так, чтобы она осуществляла поиск по дереву каталогов. Собственно, небольшая модификация потребовалась бы только для процедуры FIND, которая должна была бы вызываться рекурсивно. Можно видеть, что эта легкость произрастает из наличия в языках высокого уровня такого элемента, как локальная переменная. Попробуем осуществить это, основываясь на материале Главы 1.2. А можно осуществить это без использования локальных переменных?

Программа на Рис. 2.5.2 немного похожа на предыдущую программу. Но поиск она осуществляет по дереву каталогов, начиная с заданного каталога. Эта программа - одна из самых сложных в книге, поэтому советую читателю скрупулезно в ней разобраться. Может быть, Вам удастся ее усовершенствовать. Я могу дать и направление, в котором возможно такое усовершенствование. Дело в том, что вторым параметром командной строки можно указать маску поиска. Если, например, указать маску "*.ЕХЕ", по этой маске будет осуществляться поиск не только файлов, но и каталогов. Этот недостаток и следовало бы устранить в первую очередь.

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

В данной программе я, ради простоты, отказался от процедуры LENSTR и использую функцию API lstrlen. Кроме того, я усовершенствовал вывод так, чтобы на экран выводилось полное имя файла.

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

; файл FILES.ASM

.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE  equ -10

; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN lstrcatA@8:NEAR
EXTERN lstrcpyA@8:NEAR
EXTERN lstrlenA@4:NEAR
EXTERN FindFirstFileA@8:NEAR
EXTERN FindNextFileA@8:NEAR
EXTERN FindClose@4:NEAR
;----------------------------

; структура, используемая для поиска файла
; при помощи функций FindFirstFile и FindNextFile

_FIND STRUC
; атрибут файла
     ATR      DWORD ?
; время создания файла
     CRTIME   DWORD ?
              DWORD ?
; время доступа к файлу
     ACTIME   DWORD ?
              DWORD ?
; время модификации файла
     WRTIME   DWORD ?
              DWORD ?
; размер файла
     SIZEH    DWORD ?
     SIZEL    DWORD ?
; резерв
              DWORD ?
              DWORD ?
; длинное имя файла
     NAM      DB 260 DUP (0)
; короткое имя файла
     ANAM     DB 14 DUP (0)
_FIND ENDS
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     BUF    DB  0
            DB  100 dup (0)
     LENS   DWORD ? ; количество выведенных символов
     HANDL  DWORD ?
     HANDL1 DWORD ?
     MASKA  DB "*.*"
            DB 50 DUP (0)
     AP     DB "\",0
     FIN    _FIND <0>
     TEXT  DB "Нажмите клавишу ENTER",13, 10, 0
     BUFIN DB 10 DUP (0) ; буфер ввода
     NUM   DB 0
     NUMF  DWORD 0 ; счетчик файлов
     NUMD  DWORD 0 ; счетчик каталогов
     FORM  DB "Число найденных файлов: %lu",0
     FORM1 DB "Число найденных каталогов: %lu",0
     DIRN  DB " <DIR>",0
     PAR   DWORD 0
     PRIZN DB 0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV  HANDL,EAX
; получить HANDL1 ввода
     PUSH STD_INPUT_HANDLE
     CALL GetStdHandle@4
     MOV  HANDL1,EAX
; преобразовать строки для вывода
     PUSH OFFSET TEXT
     PUSH OFFSET TEXT
     CALL CharToOemA@8
     PUSH OFFSET FORM
     PUSH OFFSET FORM
     CALL CharToOemA@8
     PUSH OFFSET FORM1
     PUSH OFFSET FORM1
     CALL CharToOemA@8
; получить количество параметров
     CALL NUMPAR
     MOV PAR,EAX
; если параметр один, то искать в текущем каталоге
     CMP EAX, 1
     JE  NO_PAR
;----------------------------------
; получить параметр номером EDI
     MOV EDI,2
     LEA EBX,BUF
     CALL GETPAR
     CMP PAR,3
     JB  NO_PAR
; получить параметр - маску поиска
     MOV EDI,3
     LEA EBX, MASKA
     CALL GETPAR
NO_PAR:
;----------------------------------
     PUSH OFFSET BUF
     CALL FIND
; вывести количество файлов
     PUSH NUMF
     PUSH OFFSET FORM
     PUSH OFFSET BUF
     CALL wsprintfA
     LEA EAX, BUF
     MOV EDX,1
     CALL WRITE
;+++++++++++++++++
; вывести количество каталогов
     PUSH NUMD
     PUSH OFFSET FORM1
     PUSH OFFSET BUF
     CALL wsprintfA
     LEA EAX, BUF
     MOV EDX, 1
     CALL WRITE
_END:
     PUSH 0
     CALL ExitProcess@4
; область процедур
;*************************************
; вывести строку (в конце перевод строки)
; EAX - на начало строки
; EDX - с переводом строки или без
WRITE PROC
; получить длину параметра
     PUSH EAX
     PUSH EAX
     CALL lstrlenA@4
     MOV ESI,EAX
     POP EBX
     CMP EDX, 1
     JNE NO_ENT
; в конце - перевод строки
     MOV BYTE PTR [EBX+ESI],13
     MOV BYTE PTR [EBX+ESI+1],10
     MOV BYTE PTR [EBX+ESI+2],0
     ADD EAX,2
NO_ENT:
; вывод строки
     PUSH 0
     PUSH OFFSET LENS
     PUSH EAX
     PUSH EBX
     PUSH HANDL
     CALL WriteConsoleA@20
     RET
WRITE ENDP

; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX, 1
L2:
     INC ESI
     JMP L1
L4:
     MOV EAX,ECX
     RET
NUMPAR ENDP

; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     CMP ECX,EDI
     JNE L5
     MOV AL, BYTE PTR [ESI]
     MOV BYTE PTR [EBX], AL
     INC EBX
L5:
     INC ESI
     JMP L1
L4:
     MOV BYTE PTR [EBX], 0
     RET
GETPAR ENDP
;-----------------------------------
; поиск в каталоге файлов и их вывод
; локальные переменные
FINDH EQU [EBP-4]   ; дескриптор поиска
DIRS  EQU [EBP-304] ; полное имя файла
DIRSS EQU [EBP-604] ; для хранения каталога
DIRV  EQU [EBP-904] ; для временного хранения
DIR   EQU [EBP+8]   ; параметр - имя каталога
FIND  PROC
     PUSH EBP
     MOV EBP,ESP
     SUB ESP,904
; инициализация локальных переменных
     MOV ECX,300
     MOV AL,0
     MOV EDI,0
CLR:
     MOV BYTE PTR DIRS+[EDI],AL
     MOV BYTE PTR DIRSS+[EDI],AL
     MOV BYTE PTR DIRV+[EDI],AL
     INC EDI
     LOOP CLR
; определить длину пути
     PUSH DIR
     CALL lstrlenA@4
     MOV EBX,EAX
     MOV EDI, DIR
     CMP BYTE PTR [EDI],0
     JE  _OK
;если в конце нет "\" - добавим
     CMP BYTE PTR [EDI+EBX-1],"\"
     JE  _OK
     PUSH OFFSET AP
     PUSH DWORD PTR DIR
     CALL lstrcatA@8
_OK:
; запомним каталог
     PUSH DWORD PTR DIR
     LEA EAX,DIRSS
     PUSH EAX
     CALL lstrcpyA@8
; путь с маской
     PUSH OFFSET MASKA
     PUSH DWORD PTR DIR
     CALL lstrcatA@8
; здесь начало поиска
     PUSH OFFSET FIN
     PUSH DWORD PTR DIR
     CALL FindFirstFileA@8
     CMP EAX,-1
     JE _ERR
; сохранить дескриптор поиска
     MOV FINDH,EAX
LF:
; исключить "файлы" "." и ".."
    CMP BYTE PTR FIN.NAM,"."
    JE _FF
;---------------------
    LEA EAX,DIRSS
    PUSH EAX
    LEA EAX,DIRS
    PUSH EAX
    CALL lstrcpyA@8
;---------------------
    PUSH OFFSET FIN.NAM
    LEA EAX, DIRS
    PUSH EAX
    CALL lstrcatA@8
; не каталог ли?
    TEST BYTE PTR FIN.ATR, 10H
    JE NO_DIR
; добавить в строку <DIR>
    PUSH OFFSET DIRN
    LEA EAX, DIRS
    PUSH EAX
    CALL lstrcatA@8
; увеличим счетчики
    INC NUMD
    DEC NUMF
; установим признак каталога
    MOV PRIZN,1
; вывести имя каталога
    LEA EAX, DIRS
    PUSH EAX
    CALL OUTF
    JMP _NO
NO_DIR:
; вывести имя файла
    LEA EAX, DIRS
    PUSH EAX
    CALL OUTF
; признак файла (не каталога)
    MOV PRIZN,0
_NO:
    CMP PRIZN,0
    JZ _F
; каталог, готовимся в рекурсивному вызову
     LEA EAX,DIRSS
     PUSH EAX
     LEA EAX, DIRV
     PUSH EAX
     CALL lstrcpyA@8
     PUSH OFFSET FIN.NAM
     LEA EAX,DIRV
     PUSH EAX
     CALL lstrcatA@8
; осуществляем вызов
     LEA EAX, DIRV
     PUSH EAX
     CALL FIND
; продолжение поиска
_F:
     INC NUMF
_FF:
     PUSH OFFSET FIN
     PUSH DWORD PTR FINDH
     CALL FindNextFileA@8
     CMP EAX,0
     JNE LF
; закрыть дескриптор поиска
     PUSH DWORD PTR FINDH
     CALL FindClose@4
_ERR:
     MOV ESP, EBP
     POP EBP
     RET 4
FIND  ENDP
;----------------------------------
; страничный вывод имен найденных файлов
STRN EQU [EBP+8]
OUTF PROC
     PUSH EBP
     MOV EBP,ESP
; преобразовать строку
     PUSH DWORD PTR STRN
     PUSH DWORD PTR STRN
     CALL CharToOemA@8
; здесь вывод результата
     MOV EAX, STRN
     MOV EDX, 1
     CALL WRITE
     INC NUM
; конец страницы?
     CMP NUM, 22
     JNE NO
     MOV NUM, 0
; ждать ввод строки
     MOV EDX, 0
     LEA EAX, TEXT
     CALL WRITE
     PUSH 0
     PUSH OFFSET LENS
     PUSH 10
     PUSH OFFSET BUFIN
     PUSH HANDL1
     CALL ReadConsoleA@20
NO:
     POP EBP
     RET 4
OUTF ENDP
_TEXT ENDS
END START

Рис. 2.5.2. Пример программы, которая осуществляет рекурсивный поиск по дереву каталогов.

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

Аналогичную роль играет переменная DIRSS. В ней хранится текущий каталог. Это важно, т.к. с помощью этой переменной формируется полное имя файла.

Переменные DIRS и DIRV играют вспомогательную роль. В принципе, вместо них можно было бы использовать и глобальные переменные. Хотя, с точки зрения эффективности рекурсивных алгоритмов, чем меньше объем локальных переменных - тем лучше.

Еще один вопрос я хочу здесь обсудить. Для передачи имени каталога при вызове процедуры используется переменная DIRV. Почему же для этой цели нельзя использовать переменную DIRSS? Причина вот в чем. В процедуру передается не само значение, а указатель (адрес). Следовательно, любые изменения с параметром DIR приведет к аналогичным изменениям с переменной DIRSS на нижнем уровне рекурсии, В чем мы, разумеется, не заинтересованы.

Трансляция программы в TASM. Основная проблема при трансляции программ на Рис. 2.5.1 и Рис. 2.5.2 возникнет с локальными метками. Локальная метка - это метка, которая действует в пределах некоторого блока программы. В нашем случае таким блоком программы является процедура. Транслятор MASM автоматически различает метки, находящиеся в пределах процедуры, и считает их локальными. Поэтому не возникает проблемы, когда в разных процедурах встречаются метки с одинаковым именем. В TASM несколько иной подход: по умолчанию метки считаются глобальными. Локальные метки должны иметь перед именем обозначение "@@". Кроме того, в начале программы следует поставить директиву LOCALS. Сделав нужные метки локальными и поставив директиву LOCALS, Вы без труда, уже известными действиями, приведете программу к виду, приемлемому для TASM. Не забудьте о преобразовании wsprintfA -> _wsprintfA.

III

Приемы работы с двоичными файлами*. Манипуляция внешними файлами36 основывается на нескольких функциях API, главной и наиболее сложной из которых является функция CreateFile.

В связи с ограниченностью объема книги, мы не можем подробно остановиться на свойствах функции CreateFile. Однако замечу, что с помощью этой функции можно не только создавать или открывать файл, но и такие объекты как каналы (PIPE), консоли, устройства жесткого диска (disk device), коммуникационный ресурс и др. Функция различает устройство по структуре имени. К примеру, "C:\config.sys" определяет файл, a "CONOUT$" - буфер вывода текущей консоли.

Сейчас я представлю две простые, но весьма важные программы (Рис. 2.5.3(1) и Рис. 2.5.3(2)). Обе программы выводят содержимое текстового файла37, имя которого указано в командной строке, в текущую консоль. В первом случае мы получаем дескриптор текущей консоли стандартным способом. Во втором случае - открываем консоль как файл и, соответственно, выводим туда информацию, как в файл. Хочу обратить Ваше внимание на роль буфера, в который читается содержимое файла. Поэкспериментируйте с размером буфера, взяв для пробы большой текстовый файл. Интересно, что в указанных программах никак не учитывается структура текстового файла. Для такого ввода-вывода это ни к чему. Ниже мы поговорим и о структуре текстового файла.

36 Имеется в виду файлами, расположенными на внешнем устройстве.

37 Точнее любого файла, но смысл выводить файл на консоль именно таким образом имеется только для текстового файла.

* Кажется, автор имеет ввиду текстовые файлы, а не бинарные, как это следует из дальнейшего повествования :)

;файл FILES1.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
STD_OUTPUT_HANDLE equ -11
GENERIC_READ    equ 80000000h
GENERIC_WRITE   equ 40000000h
GEN = GENERIC_READ or GENERIC_WRITE
SHARE = 0
OPEN_EXISTING   equ 3

; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN  ExitProcess@4:NEAR
EXTERN  GetCommandLineA@0:NEAR
EXTERN  CreateFileA@28:NEAR
EXTERN  CloseHandle@4:NEAR
EXTERN  ReadFile@20:NEAR

;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HANDL DWORD ?
     HFILE DWORD ?
     BUF   DB 100 DUP (0)
     BUFER DB 300 DUP (0)
     NUMB  DWORD ?
     NUMW  DWORD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; получить количество параметров
     CALL NUMPAR
     CMP EAX,1
     JE NO_PAR
;------------------------------
; получить параметр номером EDI
     MOV EDI,2
     LEA EBX, BUF
     CALL GETPAR
; открыть файл
     PUSH 0             ; должен быть равен 0
     PUSH 0             ; атрибут файла (если создаем)
     PUSH OPEN_EXISTING ; как открывать
     PUSH 0             ; указатель на security attr
     PUSH 0             ; режим общего доступа
     PUSH GEN           ; режим доступа
     PUSH OFFSET BUF    ; имя файла
     CALL CreateFileA@28
     CMP EAX,-1
     JE  NO_PAR
     MOV HFILE, EAX
L00:
; прочесть в буфер
     PUSH 0
     PUSH OFFSET NUMB
     PUSH 300
     PUSH OFFSET BUFER
     PUSH HFILE
     CALL ReadFile@20
; вывести содержимое буфера на консоль
     PUSH 0
     PUSH OFFSET NUMW
     PUSH NUMB
     PUSH OFFSET BUFER
     PUSH HANDL
     CALL WriteConsoleA@20
; проверить, не последние ли байты прочитаны
     CMP NUMB, 300
     JE L00
; закрыть файл
     PUSH HFILE
     CALL CloseHandle@4
; конец работы программы
NO_PAR:
     PUSH 0
     CALL ExitProcess@4

; область процедур
; процедура определения количества параметров в строке
; определить количество параметров (->ЕАX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX   ; указатель на строку
     XOR ECX,ECX   ; счетчик
     MOV EDX,1     ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX.EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     INC ESI
     JMP L1
L4:
     MOV EAX, ECX
     RET
NUMPAR ENDP

; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI], 0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     CMP ECX,EDI
     JNE L5
     MOV AL, BYTE PTR [ESI]
     MOV BYTE PTR [EBX], AL
     INC EBX
L5:
     INC ESI
     JMP L1
L4:
     MOV BYTE PTR [EBX], 0
     RET
GETPAR ENDP
_TEXT ENDS
END START

Рис. 2.5.3(1). Вывод на консоль содержимого текстового файла. Первый способ.

; файл FILES2.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
STD_OUTPUT_HANDLE equ -11
GENERIC_READ    equ 80000000h
GENERIC_WRITE   equ 40000000h
GEN = GENERIC_READ or GENERIC_WRITE
SHARE = 0
OPEN_EXISTING   equ 3

; прототипы внешних процедур
EXTERN  ExitProcess@4:NEAR
EXTERN  GetCommandLineA@0:NEAR
EXTERN  CreateFileA@28:NEAR
EXTERN  CloseHandle@4:NEAR
EXTERN  ReadFile@20:NEAR
EXTERN  WriteFile@20:NEAR
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
; includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HANDL  DWORD ?
     HFILE  DWORD ?
     BUF    DB 100 DUP (0)
     BUFER  DB 300 DUP (0)
     NUMB   DWORD ?
     NUMW   DWORD ?
     NAMEOUT DB "CONOUT$"
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:

; получить HANDLE вывода (консоли) как файла
     PUSH 0
     PUSH 0
     PUSH OPEN_EXISTING
     PUSH 0
     PUSH 0
     PUSH GEN
     PUSH OFFSET NAMEOUT
     CALL CreateFileA@28
     MOV HANDL,EAX
; получить количество параметров
     CALL NUMPAR
     CMP EAX, 1
     JE NO_PAR
;---------------------------------------------
; получить параметр номером EDI
     MOV EDI,2
     LEA EBX,BUF
     CALL GETPAR
; открыть файл
     PUSH 0
     PUSH 0
     PUSH OPEN_EXISTING
     PUSH 0
     PUSH 0
     PUSH GEN
     PUSH OFFSET BUF
     CALL CreateFileA@28
     CMP EAX,-1
     JE  NO_PAR
     MOV HFILE,EAX
L00:
; прочесть в буфер
     PUSH 0
     PUSH OFFSET NUMB
     PUSH 300
     PUSH OFFSET BUFER
     PUSH HFILE
     CALL ReadFile@20
; вывести на консоль как в файл
     PUSH 0
     PUSH OFFSET NUMW
     PUSH NUMB
     PUSH OFFSET BUFER
     PUSH HANDL
     CALL WriteFile@20
     CMP NUMB, 300
     JE L00
; закрыть файл
     PUSH HFILE
     CALL CloseHandle@4
; конец работы программы
NO_PAR:
     PUSH 0
     CALL ExitProcess@4

; область процедур
; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     INC ESI
     JMP L1
L4:
     MOV EAX,ECX
     RET
NUMPAR ENDP

; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     CMP ECX,EDI
     JNE L5
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EBX], AL
     INC EBX
L5:
     INC ESI
     JMP L1
L4:
     MOV BYTE PTR [EBX], 0
     RET
GETPAR ENDP
_TEXT ENDS
END START

Рис. 2.5.3(2). Вывод на консоль содержимого текстового фаша. Второй способ.

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

Основным признаком текстового файла является то, что он состоит из строк разной длины. Строки отделены друг от друга разделителями. Чаще всего это последовательность двух кодов - 13 и 10. Возможны и другие варианты, например, некоторые DOS-редакторы отделяли строки только одним кодом 13.

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

  1. Побайтное чтение из файла. Как только достигаем символа-разделителя, производим действие над считанной строкой и переходим к чтению следующей строки. При этом, разумеется, следует учесть, что на конце файла может не быть символа-разделителя. Если кто-то решит, что это слишком медленный способ, то замечу, что Windows неплохо кэширует диск, так что все выглядит не так уж плохо.
  2. Чтение в небольшой буфер, но так чтобы туда входила, по крайней мере, одна строка. Прочитав, находим в буфере конец строки и производим над ней какое-либо действие. Далее следует обратиться к файлу и передвинуть указатель так, чтобы он был в файле на начале следующей строки и, разумеется, повторить действие.
  3. Чтение в произвольный буфер. После чтения производится поиск всех строк, попавших в буфер, и совершение над ними действий. При этом с большой вероятностью должна возникнуть ситуация, когда одна строка неполностью умещается в буфере. Мы обязаны учесть такую возможность.
  4. Чтение в буфер, в который помещается весь файл. Это частный случай третьего подхода, и наиболее простой с точки зрения программирования.

В программе на Рис. 2.5.4 реализуется третий подход.

; файл FILES2.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
GENERIC_READ   equ 80000000h
GENERIC_WRITE  equ 40000000h
GEN = GENERIC_READ or GENERIC_WRITE
SHARE = 0
OPEN_EXISTING   equ 3
; прототипы внешних процедур
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN CreateFileA@28:NEAR
EXTERN CloseHandle@4:NEAR
EXTERN ReadFile@20:NEAR
EXTERN WriteFile@20:NEAR
EXTERN CharToOemA@8:NEAR
;-------------------------------------------------
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HANDL   DWORD ? ; дескриптор консоли
     HFILE   DWORD ? ; дескриптор файла
     BUF     DB 100 DUP (0) ; буфер для параметров
     BUFER   DB 1000 DUP (0) ; буфер для файла
     NAMEOUT DB "CONOUT$"
     INDS    DD 0 ; номер символа в строке
     INDB    DD 0 ; номер символа в буфере
     NUMB    DD ?
     NUMC    DD ?
     PRIZN   DD 0
     STROKA  DB 300 DUP (0)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода (консоли) как файла
     PUSH 0
     PUSH 0
     PUSH OPEN_EXISTING
     PUSH 0
     PUSH 0
     PUSH GEN
     PUSH OFFSET NAMEOUT
     CALL CreateFileA@28
     MOV HANDL,EAX
; получить количество параметров
     CALL NUMPAR
     CMP EAX, 1
     JE NO_PAR
;---------------------------------------------------
; получить параметр номером EDI
     MOV EDI,2
     LEA EBX,BUF
     CALL GET PAR
; открыть файл
     PUSH 0
     PUSH 0
     PUSH OPEN_EXISTING
     PUSH 0
     PUSH 0
     PUSH GEN
     PUSH OFFSET BUF
     CALL CreateFileA@28
     CMP EAX,-1
     JE  NO_PAR
     MOV HFILE, EAX
;++++++++++++++++++++++++++++
L00:
; читать 1000 байт
     PUSH 0
     PUSH OFFSET NUMB
     PUSH 1000
     PUSH OFFSET BUFER
     PUSH HFILE
     CALL ReadFile@20
     MOV INDB, 0
; проверим, есть ли в буфере байты
     CMP NUMB, 0
     JZ  _CLOSE
; заполняем строку
L001:
     MOV EDI,INDS
     MOV ESI,INDB
     MOV AL,BYTE PTR BUFER[ESI]
     CMP AL,13 ; проверка на конец строки
     JE  _ENDSTR
     MOV BYTE PTR STROKA[EDI],AL
     INC ESI
     INC EDI
     MOV INDS,EDI
     MOV INDB,ESI
     CMP NUMB, ESI ; проверка на конец буфера
     JNBE L001
; закончился буфер
     MOV INDS,EDI
     MOV INDB,ESI
     JMP L00
_ENDSTR:
; делаем что-то со строкой
     CALL OUTST
; обнулить строку
     MOV INDS,0
; перейти к следующей строке в буфере
     ADD INDB,2
; не закончился ли буфер?
     MOV ESI,INDB
     CMP NUMB, ESI
     JAE L001
     JMP L00
;++++++++++++++++++++++++++++++
_CLOSE:
; проверим, не пустая ли строка
     CMP INDS,0
     JZ  CONT
; делаем что-то со строкой
     CALL OUTST
CONT:
; закрыть файлы
     PUSH HFILE
     CALL CloseHandle@4
; конец работы программы
NO_PAR:
     PUSH 0
     CALL ExitProcess@4

; область процедур
; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     INC ESI
     JMP L1
L4:
     MOV EAX,ECX
     RET
NUMPAR ENDP

; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
L1:
     CMP BYTE PTR [ESI],0
     JE L4
     CMP BYTE PTR [ESI],32
     JE L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP L2
L3:
     OR EDX,1
L2:
     CMP ECX,EDI
     JNE L5
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EBX],AL
     INC EBX
L5:
     INC ESI
     JMP L1
L4:
     MOV BYTE PTR [EBX],0
     RET
GETPAR ENDP

; вывести строку в консоль с разделителем
OUTST PROC
     MOV EBX,INDS
     MOV BYTE PTR STROKA[EBX],0
     PUSH OFFSET STROKA
     PUSH OFFSET STROKA
     CALL CharToOemA@8
; в конце строки - разделитель
     MOV BYTE PTR STROKA[EBX],6
     INC INDS
; вывести строку
     PUSH 0
     PUSH OFFSET NUMC
     PUSH INDS
     PUSH OFFSET STROKA
     PUSH HANDL
     CALL WriteFile@20
     RET
OUTST ENDP
_TEXT ENDS
END START

Рис. 2.5.4. Пример обработки текстового файла.

Программа на Рис. 2.5.4 демонстрирует один из возможных алгоритмов обработки текстового файла - построчное чтение текстового файла. Часть программы, занимающаяся чтением и анализом текстового файла, сосредоточена между метками L00 и CONT. Детально разберитесь в алгоритме и проникнитесь тем, что язык высокого уровня никогда не будет стимулировать написание таких алгоритмов, а значит, язык ассемблера делает нас интеллектуально богаче.


Часть II. Глава 6. Макросредства ассемблера и программирование в Windows

Глава 6. Макросредства ассемблера и программирование в Windows

I

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

Метки

1. Метка с двоеточием после имени определяет адрес следующей за меткой команды.

2. Директива LABEL позволяет определить явно тип метки. Значение же определенной таким образом метки равно адресу команды или данных, стоящих далее. Например,
LABEL L1 DWORD.

3. Выражение ИМЯ PROC определяет метку, переход на которую обычно происходит по команде CALL. Блок кода, начинающийся с такой метки, называют процедурой. Впрочем, переход на такую метку можно осуществлять и с помощью JMP, как, впрочем, и команду CALL можно использовать для перехода на обычную метку. В этом, несомненно, состоит сила и гибкость ассемблера.

4. В строке за меткой может стоять директива резервирования данных, например: ERR DB 'Ошибка' или NUM DWORD 0. С точки зрения языка высокого уровня таким образом мы определяем глобальную переменную. С точки же зрения ассемблера нет никакой разницы между командой и данными, поэтому между меткой, определяющей команду, и меткой, определяющей данные, нет никакой разницы. Раз уж речь пошла о данных, перечислю их типы:
BYTE (DB) - байт,
WORD (DW) - 2 байта,
DWORD (DD) - 4 байта,
FWORD (DF) - 6 байт,
QWORD (DQ) - 8 байт,
TBYTE (DT) - 10 байт.

5. С помощью директивы EQU в терминах языков высокого уровня определяются константы. Например - MES EQU "ERROR!", LAB EQU 145Н. С помощью EQU значение данной метке может быть присвоено только один раз. С правой стороны от EQU может стоять выражение с использованием арифметических, логических и битовых операций. Вот эти операции: "+", "-", "*", "/", "MOD"-ocтaток от деления, "AND", "OR", "NOT", "XOR", "SHR", "SHL". Используются также операции сравнения: EQ, GE, GT, LE, LT, NE. Выражение с операцией сравнения считается логическим и принимает значение 0, если условие не выполняется, и 1 - если выполняется. С помощью директивы "=" можно присваивать только целые значения, но зато производить переприсваивание. Заметим, что выражение может являться операндом команды: MOV EAX,16*16-1.

6. Метка "$" всегда определяет текущий адрес.

7. В MASM метки, стоящие в процедуре, автоматически считаются локальными и, следовательно, имена меток в процедурах могут дублироваться. В TASM все метки по умолчанию считаются глобальными. Чтобы сделать метки, стоящие в процедуре локальными, они должны иметь префикс @@, а в начале программы следует указать директиву LOCALS (см. предыдущую главу).

Структура

1. Директива STRUC позволяет объединить несколько разнородных данных в одно целое. Эти данные называются полями. Вначале при помощи STRUC определяется шаблон структуры, затем с помощью директивы < > можно определить любое количество структур. Рассмотрим пример:

STRUC COMPLEX
     RE   DD ?
     IM   DD ?
STRUC ENDS

     ...
     ;в сегменте данных
     COMP1 COMPLEX <?>
     COMP2 COMPLEX <?>

Доступ к полям структуры осуществляется посредством точки: COMP1.RE.

2. Объединение. Объединение определяется при помощи ключевого слова UNION. От структуры объединение отличается только тем, что все поля располагаются в структуре с нулевым смещением, т.е. накладываются друг на друга.

Условное ассемблирование

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

а)
     IF выражение
     ...
     ENDIF

б)
     IF выражение
     ...
     ELSE
     ...
     ENDIF
в)
     IF выражение 1
     ...
     ELSEIF выражение 2
     ...
     ELSEIF выражение 3
     ...
     ELSE
     ...
     ENDIF
Условие считается не выполненным, если выражение принимает значение 0 и выполненным, если выражение отлично от нуля.

2. Ассемблеры MASM и TASM поддерживают также несколько условных специальных директив, назовем некоторые из них.

а)

     IFE выражение
     ...
     ELSEIFE
     ...
     ENDIFE

б) Операторы IF1 и IF2 проверяют первый и второй проход при ассемблировании.

в) Оператор IFDEF - проверяет, определено ли в программе символическое имя, IFDEFN - обратный оператор. И другие IF операторы. Они имеются в любом справочнике по ассемблеру.

г) Имеется целый набор директив, начинающихся с .ERR. Например, .ERRE выражение - вызовет прекращение трансляции и сообщение об ошибке, если выражение станет равным 0.

Условное ассемблирование понадобится нам в конце главы для написания программы, транслируемой как в MASM, так и TASM.

Вызов процедур

1. С упрощенным вызовом процедур в MASM Вы уже познакомились. Это директива INVOKE. Процедура должна быть заранее определена с использованием ключевого слова PROTO. Например:

MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD
...
invoke MessageBox, h, ADDR TheMsg, ADDR TitleW, MB_OK
Здесь h - дескриптор окна, откуда вызывается сообщение, TheMsg - строка сообщения, TitleW - заголовок окна, MB_OK - тип сообщения. ADDR в данном случае синоним OFFSET.

2. Оказывается, в синтаксисе TASM тоже имеется свой упрощенный вызов.

EXTERN MESSAGEBOX:PROC
...
call MessageBox PASCAL,h,ADDR TheMsg,ADDR TitleW, MB_OK
PASCAL - тип вызова, точнее порядок следования параметров. Можно поставить параметр C, тогда порядок будет обратным.

Макроповторения

1. Повторение, заданное опеделенное число раз. Используется макродиректива REPT. Например:

   A EQU 10
   REPT 100
   DB A
   ENDM
Будет сгенерировано 100 директив DB 10. С этой директивой удобно использовать оператор "=", который позволяет изменять значение переменной многократно, т.е. использовать выражение типа А = А + 5.

2. Директива IRP.

   IRP параметр,<список>
   ...
   ENDM
Блок будет вызываться столько раз, сколько параметров в списке. Например:
   IRP REG, <EAX,EBX,ECX,EDX,ESI,EDI>
       PUSH REG
   ENDM
Приведет к генерации следующих строк:
   PUSH EAX
   PUSH EBX
   PUSH ECX
   PUSH EDX
   PUSH ESI
   PUSH EDI

3. Директива IRPC.

   IRPC параметр, строка
   Операторы
   ENDM

Пример:

   IRPC CHAR,azklg
   CMP AL,'&CHAR&'
   JZ EndC
   ENDM
   EndC:

Данный фрагмент эквивалентен следующей последовательности:
   CMP AL,'a'
   JZ EndC
   CMP AL,'z'
   JZ EndC
   CMP AL,'k'
   JZ EndC
   CMP AL,'l'
   JZ EndC
   CMP AL,'g'
   JZ EndC
   EndC:

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

Макроопределения

Общий вид макроопределения.

   Имя MACRO параетры
   ...
   ENDM
Определив блок один раз, можно использовать его в программе многократно. Причем в зависимости от значений параметров заменяемый участок может иметь разные значения. Если заданный участок предполагается многократно использовать, например в цикле, макроопределение имеет несомненные преимущества перед процедурой, т.к. несколько убыстряет выполнение кода. Пример:
   ЕХС MACRO par1,par2
     PUSH par1
     POP  par2
   ENDM
Данное макроопределение приводит к обмену содержимым между параметрами.

ЕХС EAX,EBX эквивалентно PUSH EAX\POP EAX; ЕХС MEM1,ESI - PUSH MEM1\POP ESI и т. д. Заметим, что если первый параметр будет непосредственно числом, то это приведет к загрузке данного числа во второй операнд.

Важным вопросом в связи с макроопределениями является проблема меток. Действительно, если мы будем применять в макроопределении обычные метки, то при использовании его более чем один раз возникнет коллизия. Коллизия эта разрешается при помощи объявления локальных меток. Для этого используется ключевое слово LOCAL. Например,

   ЕХС MACRO par1,par2
      LOCAL EXI
      CMP par1,par2
      JE EXI
      PUSH par1
      POP par2
   EXI:
   ENDM

Данное макроопределение можно использовать сколь угодно много раз - при каждой подстановке ассемблер будет генерировать уникальную метку. Для выхода из макроопределения (т.е. для прекращения генерации макроопределения) применяется директива EXITM. Она может понадобиться, если в макроопределении Вы используете условные конструкции типа IF..ENDIF.

Некоторые другие директивы транслятора ассемблера

1. Кроме объявлений с использованием директив PUBLIC и EXTERN, возможно объявление при помощи директивы GLOBAL, которая действует, как PUBLIC и EXTERN одновременно.

2. PURGE имя макроса. Отменяет загрузку макроса. Используется при работе с библиотекой макросов, чтобы не перегружать память38.

3. LENGTHOF - определяет число элементов данных. SIZEOF - определяет размер данных (отсутствуют в TASM).

4. Директивы задания набора команд.
.8086 - разрешены только команды микропроцессора 8086. Данная директива работает по умолчанию.
.186 - разрешены команды 186.
.286 и .286Р - разрешены команды 286-ого микропроцессора. Добавка "P" здесь и далее означает разрешение команд защищенного режима.
.386 и .386P - разрешение команд 386-ого микропроцессора.
.486 и .486Р - разрешение команд 486-ого процессора.
.586 и .586Р - разрешены команды Р5 (Pentium).
.686 и .686Р - разрешены команды Pб (Pentium Pro, Pentium II).
.8087 - разрешены команды арифметического сопроцессора 8087.
.287 - разрешены команды арифметического сопроцессора 287.
.387 - разрешены команды арифметического сопроцессора 387.
.MMX - разрешены команды расширения ММХ.

5. Директивы управления листингом.
NAME - задать имя модуля.
TITLE - определяет заголовок листинга.
По умолчанию и имя модуля, и заголовок листинга совпадают с именем файла.
SUBTTL - определяет подзаголовок листинга.
PAGE - определяет размеры страницы листинга: длина, ширина. Директива PAGE без аргументов начинает новую страницу листинга.
.LIST - выдавать листинг.
.XLIST - запретить выдачу листинга.
.SALL - подавить печать макроопределений.
.SFCOND - подвить печать условных блоков с ложными условиями.
.LFCOND - печатать условные блоки с ложными условиями.
.CREF - разрешить листинг перекрестных ссылок.
.XCREF - запретить листинг перекрестных ссылок.

38 В операционной системе MS DOS это было существенно.

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

1. Условные конструкции.

а)
     .IF условие
     ...
     .ENDIF
б)
     .IF условие
     ...
     .ELSE
     ...
     .ENDIF
в)
     .IF условие 1
     ...
     .ELSEIF условие 2
     ...
     .ELSEIF условие 3
     ...
     ...
     .ELSE
     ...
     .ENDIF

Рассмотрим следующий фрагмент, содержащий условную конструкцию и соответствующий ей ассемблерный код.

     .IF EAX==12H
          MOV EAX,10H
     .ELSE
          MOV EAX,15Н
     .ENDIF
эквивалентен следующему ассемблерному коду:
        CMP EAX,12Н
        JNE NO_EQ
        MOV EAX,10H
        JMP EX_BLOK
     NO_EQ:
        MOV EAX,15Н
     EX_BLOK:
Весьма удобная штука, но не увлекайтесь: на мой взгляд, это сильно расслабляет.

2. Цикл "пока".

     .WHILE условие
       ...
     .ENDW

Пример.

     WHILE EAX<64H
     ADD EAX,10
     ENDW

Для MASM:

     JMP L2
     L1:
     ADD EAX,10Н
     L2:
     CMP EAX,64Н
     JB L1

Для TASM:

     L1:
     CМР EAX,64Н
     JNB EXI
     ADD EAX,10Н
     JMP L1
     EXI:

Есть некоторое отличие в том, как два ассемблера транслируют директивы .IF и .WHILE. Транслятор TASM32 производит автоматически оптимизацию на предмет выравнивания по границе учетверенного слова, добавляя дополнительно команды NOP. Это несколько ускоряет выполнение программы, но увеличивает ее объем. Мне ближе позиция MASM.

II

Сейчас мы рассмотрим вопрос о написании программ, которые одинаково транслировались бы и в MASM, и в TASM. Для этого прекрасно подходит условное ассемблирование. Удобнее всего использовать IFDEF и возможности трансляторов задавать символьную константу, все равно - TASM или MASM. И в ML, и в TASM32 определен ключ /D, позволяющий задавать такую константу.

На Рис. 2.6.1 представлена программа, транслируемая и в MASM, и TASM. Программа весьма проста, но рассмотрения ее вполне достаточно для создания более сложных подобных совместимых программ.

.386P
; плоская модель
.MODEL FLAT, STDCALL
; проверить, определена символьная константа MASM или нет
IFDEF MASM
; работаем в MASM
     EXTERN      ExitProcess@4:NEAR
     EXTERN      MessageBoxA@16:NEAR
     includelib с:\masm32\lib\kernel32.lib
     includelib c:\masm32\lib\user32.lib
ELSE
; работаем в TASM
     EXTERN      ExitProcess:NEAR
     EXTERN      MessageBoxA:NEAR
     includelib c:\tasm32\lib\import32.lib
     ExitProcess@4 = ExitProcess
     MessageBoxA@16 = MessageBoxA
ENDIF
;-----------------------------------------------------------

;сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG DB "Простая программа",0
     TIT DB "Заголовок",0
_DATA ENDS

;сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
    PUSH 0
    PUSH OFFSET TIT
    PUSH OFFSET MSG
    PUSH 0 ; дескриптор экрана
    CALL MessageBoxA@16
;-----------------------------------
    PUSH 0
    CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 2.6.1. Пример использования условного ассемблирования для написания совместимой программы.

Трансляция в MASM:

ML /с /coff /DMASM PROG.ASM
LINK /SUBSYSTEM:WINDOWS PROG.OBJ

Трансляция в TASM:

TASM32 /ml PROG.ASM
TLINK32 -aa PROG.OBJ

Как видите, все сводится к проверке, определена символьная константа MASM или нет (ключ /DMASM). Еще одна сложность - добавка в конце имени @N. Эту проблему мы обходим, используя оператор "=", с помощью которого переопределяем имена (см. секцию "работаем в TASM").


Часть III. Глава 1. Примеры программ, использующих таймер

Часть III. Более сложные примеры программирования в Windows

Глава 1. Примеры программ, использующих таймер

Таймер является одним из мощных инструментов, предоставляемых операционной системой и позволяющих решать самые разнообразные задачи. С таймером Вы познакомились, когда занимались консольными приложениями. Там мы пользовались функциями timeSetEvent и timeKillEvent. Для консольных приложений это очень удобные функции. В оконных приложениях чаще используют функции SetTimer и KillTimer. Особенность таймера, создаваемого функцией SetTimer, заключается в том, что сообщение WM_TIMER, которое начинает посылать система приложению после выполнения функции SetTimer, приходит со всеми другими сообщениями наравне, на общих основаниях. Следовательно, интервал между двумя приходами сообщения WM_TIMER может несколько варьироваться. В большинстве случаев это не существенно.

У сообщения таймера есть еще одна особенность. Если система посылает сообщение приложению, а предыдущее сообщение еще стоит в очереди, то система объединяет эти два сообщения. Таким образом, "вынужденный простой" не приводит к приходу на приложение подряд нескольких сообщений таймера.

Вот те задачи, которые можно решить с помощью таймера.

  1. Отслеживание времени: секундомер, часы и т.д. Нарушение периодичности не имеет значения, так как по приходе сообщения время можно отследить, вызвав функцию получения системного времени.
  2. Таймер - один из способов осуществления многозадачности. Можно установить сразу несколько таймеров на разные функции, в результате периодически будет исполняться то одна, то другая функция. Более подробно о многозадачности будет сказано в следующей главе.
  3. Периодический вывод на экран обновленной информации.
  4. Автосохранение - функция особенно полезная для редакторов.
  5. Задание темпа изменения каких-либо объектов на экране.
  6. Мультипликация - по приходе сообщения от таймера обновляется графическое содержимое экрана или окна, так что возникает эффект мультипликации.

Рассмотрим, как нужно обращаться с функцией SetTimer. Вот параметры этой функции.

  • 1-й параметр - дескриптор окна, с которым ассоциируется таймер. Если этот параметр сделать равным NULL (0), то будет проигнорирован и второй параметр.
  • 2-й параметр - определяет идентификатор таймера.
  • 3-й параметр - определяет интервал посылки сообщения WM_TIMER.
  • 4-й параметр - определяет адрес функции, на которую будет приходить сообщение WM_TIMER. Если параметр равен NULL, то сообщение будет приходить на функцию окна.

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

Из сказанного следует, что функция может быть вызвана тремя способами:

  1. Задан дескриптор окна, а четвертый параметр задается равным нулю.
  2. Задан дескриптор окна, а четвертый параметр определяет функцию, на которую будет приходить сообщение WM_TIMER.
  3. Дескриптор окна равен NULL, а четвертый параметр определяет функцию, на которую будет приходить сообщение WM_TIMER. Идентификатор таймера в этом случае будет определяться по возвращаемому функцией значению.

Функция, на которую приходит сообщение WM_TIMER, имеет следующие параметры:

  • 1-й параметр - дескриптор окна, с которым ассоциирован таймер.
  • 2-й параметр - сообщение WM_T1MER.
  • 3-й параметр - идентификатор таймера.
  • 4-й параметр - время в миллисекундах, которое прошло с момента запуска Windows.

Функция KillTimer удаляет созданный параметр и имеет следующие параметры:

  • 1-й параметр - дескриптор окна.
  • 2-й параметр - идентификатор таймера.

I

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

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

// файл timer.rc
// определение констант
#define WS_SYSMENU       0x00080000L
#define WS_MINIMIZEBOX   0x00020000L
#define WS_MAXIMIZEBOX   0x00010000L

// стиль - кнопка
#define BS_PUSHBUTTON    0x00000000L
// кнопка в окне должна быть видимой
#define WS_VISIBLE       0x10000000L
// центрировать текст на кнопке
#define BS_CENTER       0x00000300L
// стиль кнопки
#define WS_CHILD         0х40000000L
// возможность фокусировать элемент
// при помощи клавиши TAB
#define WS_TABSTOP      0x00010000L
#define DS_3DLOOK       0x0004L
//определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | DS_3DLOOK
CAPTION "Пример диалогового окна с таймером"
FONT 8, "Arial"
{
// кнопка, идентификатор 5
  CONTROL "Выход", 5, "button", BS_PUSHBUTTON
  | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
  180, 76, 50, 14
}

; файл timer.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE        equ 10h
WM_INITDIALOG   equ 110h
WM_COMMAND      equ 111h
WM_TIMER        equ 113h

; прототипы внешних процедур
IFDEF MASM
    EXTERN      ReleaseDC@8:NEAR
    EXTERN      GetDC@4:NEAR
    EXTERN      TextOutA@20:NEAR
    EXTERN      MessageBoxA@16:NEAR
    EXTERN      ExitProcess@4:NEAR
    EXTERN      GetModuleHandleA@4:NEAR
    EXTERN      DialogBoxParamA@20:NEAR
    EXTERN      EndDialog@8:NEAR
    EXTERN      SendMessageA@16:NEAR
    EXTERN      SetTimer@16:NEAR
    EXTERN      KillTimer@8:NEAR
ELSE
    EXTERN      ReleaseDC:NEAR
    EXTERN      GetDC:NEAR
    EXTERN      TextOutA:NEAR
    EXTERN      MessageBoxA:NEAR
    EXTERN      ExitProcess:NEAR
    EXTERN      GetModuleHandleA:NEAR
    EXTERN      DialogBoxParamA:NEAR
    EXTERN      EndDialog:NEAR
    EXTERN      SendMessageA:NEAR
    EXTERN      SetTimer:NEAR
    EXTERN      KillTimer:NEAR
    ReleaseDC@8 = ReleaseDC
    GetDC@4     = GetDC
    TextOutA@20 = TextOutA
    MessageBoxA@16 = MessageBoxA
    ExitProcess@4  = ExitProcess
    GetModuleHandleA@4 = GetModuleHandleA
    DialogBoxParamA@20 = DialogBoxParamA
    EndDialog@8        = EndDialog
    SendMessageA@16    = SendMessageA
    SetTimer@16        = SetTimer
    KillTimer@8 = KillTimer
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND      DD ?
     MSMESSAGE   DD ?
     MSWPARAM    DD ?
     MSLPARAM    DD ?
     MSTIME      DD ?
     MSPT        DD ?
MSGSTRUCT ENDS

; файл timer.asm

.386P
; плоская модель
.MODEL FLAT, stdcall
include timer.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
; для компоновщика LINK.EXE
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
   includelib c:\masm32\lib\gdi32.lib
ELSE
; для компоновщика TLINK32.EXE
   includelib c:\tasm32\lib\import32.lib
ENDIF
;-------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG      MSGSTRUCT <?>
     HINST    DD 0 ; дескриптор приложения
     PA       DB "DIAL1",0
     COUNT    DD 0
     TEXT     DB 0
     CAP DB   'Сообщение',0
     MES      DB 'Выход по таймеру',0
_DATA ENDS

; сегмент кода

_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;----------------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
KOL:
;----------------------------------------
     PUSH  0
     CALL  ExitProcess@4
;----------------------------------------

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;---------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; здесь реакция на закрытие окна
L3:
; удалить таймер
     PUSH 1 ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL KillTimer@8
; закрыть диалог
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH], WM_INITDIALOG
     JNE L5
; здесь начальная инициализация
; установить таймер
     PUSH 0    ; параметр = NULL
     PUSH 1000 ; интервал 1 с.
     PUSH 1    ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL SetTimer@16
     JMP FINISH
L5:
     CMP DWORD PTR [EBP+0CH], WM_COMMAND
     JNE L2
; кнопка выхода?
     CMP WORD PTR [EBP+10H],5
     JNE FINISH
     JMP L3
L2:
     CMP DWORD PTR [EBP+0CH],WM_TIMER
     JNE FINISH
     ; не пора ли заканчивать ?
     CMP COUNT,9
; выход без предупреждения
     JA  L3
; выход через сообщение
     JE  L4
; пришло сообщение таймера
; подготовить текст
     MOV EAX, COUNT
     ADD EAX,49
     MOV TEXT, AL
; получить контекст
     PUSH DWORD PTR [EBP+08H]
     CALL GetDC@4
; запомнить контекст
     PUSH EAX
; вывести значение счетчика
     PUSH 1
     PUSH OFFSET TEXT
     PUSH 10
     PUSH 10
     PUSH EAX
     CALL TextOutA@20
; удалить контекст
     POP EAX
     PUSH EAX
     PUSH DWORD PTR [EBP+08H]
     CALL ReleaseDC@8
; увеличить счетчик
     INC COUNT
     JMP FINISH
L4:
     INC COUNT
; сообщение о выходе по таймеру
     PUSH 0
     PUSH OFFSET CAP
     PUSH OFFSET MES
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
     JMP L3
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX,0
     RET 16
WNDPROC    ENDP
;-------------------------------------------------
_TEXT ENDS
END START

Рис. 3.1.1 Пример реализации простейшего таймера.

Прокомментируем программу на Рис. 3.1.1. Организация таймера здесь проста и очевидна. Единственное, что может вызвать трудность понимания, это то, как удается оставить MessageBox и одновременно закрыть диалоговое окно. Но здесь тоже все достаточно просто: сообщение появляется при COUNT=9, а когда приходит следующее сообщение, то COUNT уже больше 9, и выполняется ветка закрытия диалогового окна.

Трансляция программы.
Пакет MASM32.

   ML /c /coff /DMASM timer.asm
   RC timer.rc
   LINK /SUBSYSTEM:WINDOWS timer.obj timer.res
Пакет TASM32.
   TASM32 /ml timer.asm
   BRCC32 timer.rc
   TLINK32 -aa timer.obj,,,,,timer.res

II

Следующая программа несколько сложнее предыдущей. Здесь действуют два таймера. Можно считать, что запускаются одновременно две задачи39. Одна задача с периодичностью 0.5 сек. получает системное время и формирует строку для вывода (STRCOPY). Эта задача имеет свою собственную функцию, на которую приходит сообщение WM_TIMER. Вторая задача работает в рамках функции окна. Эта задача с периодичностью 1 сек. выводит время и дату в окно редактирования, расположенное на диалоговом окне. Таким образом, две задачи взаимодействуют друг с другом посредством глобальной переменной STRCOPY.

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

// файл timer2.rc
// определение констант
#define WS_SYSMENU     0x00080000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE     0x10000000L
// бордюр вокруг элемента
#define WS_BORDER      0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP     0x00010000L
// текст в окне редактирования прижат к левому краю
#define ES_LEFT        0x0000L
// стиль всех элементов на окне
#define WS_CHILD       0x40000000L
// запрещается ввод с клавиатуры
#define ES_READONLY    0x0800L

#define DS_3DLOOK      0x0004L

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 100
STYLE WS_SYSMENU | DS_3DLOOK
CAPTION "Диалоговое окно с часами и датой"
FONT 8, "Arial"
{
  CONTROL "", 1, "edit", ES_LEFT | WS_CHILD
  | WS_VISIBLE | WS_BORDER
  | WS_TABSTOP | ES_READONLY, 100, 5, 130, 12
}

; файл timer2.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE       equ 10h
;сообщение приходит при создании окна
WM_INITDIALOG  equ 110h
;сообщение приходит при событии с элементом на окне
WM_COMMAND     equ 111h
;сообщение от таймера
WM_TIMER       equ 113h
; сообщение посылки текста элементу
WM_SETTEXT     equ 0Ch

; прототипы внешних процедур
IFDEF MASM
    EXTERN      SendDlgItemMessageA@20:NEAR
    EXTERN      wsprintfA:NEAR
    EXTERN      GetLocalTime@4:NEAR
    EXTERN      ExitProcess@4:NEAR
    EXTERN      GetModuleHandleA@4:NEAR
    EXTERN      DialogBoxParamA@20:NEAR
    EXTERN      EndDialog@8:NEAR
    EXTERN      SetTimer@16:NEAR
    EXTERN      KillTimer@8:NEAR
ELSE
    EXTERN      SendDlgItemMessageA:NEAR
    EXTERN      _wsprintfA:NEAR
    EXTERN      GetLocalTime:NEAR
    EXTERN      ExitProcess:NEAR
    EXTERN      GetModuleHandleA:NEAR
    EXTERN      DialogBoxParamA:NEAR
    EXTERN      EndDialog:NEAR
    EXTERN      SetTimer:NEAR
    EXTERN      KillTimer:NEAR
    SendDlgItemMessageA@20 = SendDlgItemMessageA
    wsprintfA              = _wsprintfA
    GetLocalTime@4         = GetLocalTime
    ExitProcess@4          = ExitProcess
    GetModuleHandleA@4     = GetModuleHandleA
    DialogBoxParamA@20     = DialogBoxParamA
    EndDialog@8            = EndDialog
    SetTimer@16            = SetTimer
    KillTimer@8            = KillTimer
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DD ?
     MSMESSAGE  DD ?
     MSWPARAM   DD ?
     MSLPARAM   DD ?
     MSTIME     DD ?
     MSPT       DD ?
MSGSTRUCT ENDS

; структура данных дата-время
DAT STRUC
     year    DW ?
     month   DW ?
     dayweek DW ?
     day     DW ?
     hour    DW ?
     min     DW ?
     sec     DW ?
     msec    DW ?
DAT ENDS

; файл timer2.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include timer2.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
; для компоновщика LINK.EXE
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
   includelib c:\masm32\lib\gdi32.lib
ELSE
; для компоновщика TLINK32.EXE
   includelib c:\tasm32\lib\import32.lib
ENDIF
;------------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG       MSGSTRUCT <?>
     HINST     DD 0 ; дескриптор приложения
     PA        DB "DIAL1",0
     TIM       DB "Дата %u/%u/%u Время %u:%u:%u",0
     STRCOPY   DB 50 DUP (?)
     DATA      DAT <0>
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
; создать диалоговое окно
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
; сообщение об ошибке
KOL:
;--------------------------------------
     PUSH  0
     CALL  ExitProcess@4
;--------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;-----------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; здесь реакция на закрытие окна
; удалить таймер 1
     PUSH 1 ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL KillTimer@8
; удалить таймер 2
     PUSH 2 ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL KillTimer@8
; закрыть диалог
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH], WM_INITDIALOG
     JNE L2
; здесь начальная инициализация
; установить таймер 1
     PUSH 0    ; параметр = NULL
     PUSH 1000 ; интервал 1 с.
     PUSH 1    ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL SetTimer@16
; установить таймер 2
     PUSH OFFSET TIMPROC ; параметр = NULL
     PUSH 500  ; интервал 0.5 с.
     PUSH 2    ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL SetTimer@16
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_TIMER
     JNE FINISH
; отправить строку в окно
     PUSH OFFSET STRCOPY
     PUSH 0
     PUSH WM_SETTEXT
     PUSH 1 ; идентификатор элемента
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA@20
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX,0
     RET 16
WNDPROC    ENDP
;--------------------------------

;процедура таймера
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM - промежуток запуска Windows
; [EBP+10Н]   ; WAPARAM - идентификатор таймера
; [EBP+0CH]   ; WM_TIMER
; [EBP+8]     ; HWND
TIMPROC     PROC
     PUSH EBP
     MOV EBP,ESP
; получить локальное время
     PUSH OFFSET DATA
     CALL GetLocalTime@4
; получить строку для вывода даты и времени
     MOVZX EAX,DATA.sec
     PUSH EAX
     MOVZX EAX,DATA.min
     PUSH EAX
     MOVZX EAX,DATA.hour
     PUSH EAX
     MOVZX EAX,DATA.year
     PUSH EAX
     MOVZX EAX,DATA.month
     PUSH EAX
     MOVZX EAX,DATA.day
     PUSH EAX
     PUSH OFFSET TIM
     PUSH OFFSET STRCOPY
     CALL wsprintfA
; восстановить стек
     ADD ESP,32
     POP EBP
     RET 16
TIMPROC    ENDP
_TEXT ENDS
END START

Puc. 3.1.2. Пример использования двух таймеров.

Трансляция программы.
Пакет MASM32.

   ML /c /coff /DMASM timer2.asm
   RC timer2.rc
   LINK /SUBSYSTEM:WINDOWS timer2.obj timer2.res
Пакет TASM32.
   TASM32 /ml timer2.asm
   BRCC32 timer2.rc
   TLINK32 -aa timer2.obj,,,,,timer2.res

Puc. 3.1.3. Результат работы программы на Рис. 3.1.2.

Обращаю Ваше внимание на весьма полезную функцию GetLocalTime. Информация, полученная с помощью этой функции (см. структуру DAT), легко может быть использована для самых разных целей, в том числе и для вывода на экран. Аналогично, с помощью функции SetLocalTime, Вы сможете установить текущее время. Для получения времени по Гринвичу используется функция GetSystemTime, которая, соответственно, с помощью SetSystemTime используется для установки времени в Гринвичском выражении. Аргументом во всех этих функциях является уже упомянутая выше структура (точнее указатель на нее).

39 Вообще говоря, три, так как само диалоговое окно также работает независимо.

III

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

1. Прежде всего заметим, что всплывающая подсказка - это всего лишь окно с определенными свойствами. Вот эти свойства: DS_3DLOOK, WS_POPUP, WS_VISIBLE, WS_BORDER. В принципе можно экспериментировать - добавлять или удалять свойства. Но без одного свойства Вы никак не обойдетесь - это WS_POPUP. Собственно POPUP можно перевести как поплавок. Кроме того, определение всплывающего окна в файле ресурсов не должно содержать опции CAPTION.

2. Появление подсказки не должно менять ситуацию в диалоговом окне. Это значит - вызов подсказки должен быть немодальным, при помощи функции CreateDialogIndirect. Кроме того, следует предусмотреть переустановку фокуса на диалоговое окно. Для этого достаточно в нужном месте (см. Рис. 3.1.3) вызвать функцию SetFocus.

3. Итак, подсказка - это диалоговое окно, и, следовательно, оно должно иметь свою функцию. Что должна содержать эта функция? По крайней мере, обработку трех событий: WM_INITDIALOG, WM_PA1NT, WM_TIMER. По получении сообщения WM_INITDIALOG следует определить размер и положение подсказки. Кроме того, если мы предполагаем, что подсказка должна спустя некоторое время исчезать, следует установить таймер. По получении сообщения WM_PAINT следует вывести в окно подсказки текст. Если определять размер окна подсказки точно по строке выводимого текста, то цвет фона подсказки будет полностью определяться цветом выводимого текста. Наконец по приходе сообщения WM_TIMER мы закрываем подсказку.

4. С самой подсказкой более или менее ясно. Определимся теперь, как и где будет вызываться эта подсказка. Мне более импонирует такой подход: в основном диалоговом окне определяем таймер, в функции которого и будет проверяться положение курсора. В зависимости от этого положения и будет вызываться или удаляться подсказка. В функции таймера должно предусмотреть:

  1. Проверку положения курсора. Если курсор оказался на данном элементе, то вызывать подсказку. При этом желательно, чтобы подсказка появлялась бы с некоторой задержкой. Последнее можно обеспечить введением счетчика - вызывать подсказку, если счетчик превысил некоторое значение.
  2. Необходимо обеспечить удаление подсказки, если курсор покидает данный элемент.

На Рис. 3.1.3 представлена программа, которая демонстрирует описанный выше подход. На Рис. 3.1.4 представлено диалоговое окно с подсказками. В принципе, означенный подход не является единственным, и, разобравшись в данном подходе, вы сможете пофантазировать и придумать свои способы создания подсказок.

// файл HINT.RC
// определение констант
#define WS_SYSMENU     0x00080000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE     0x10000000L
// бордюр вокруг элемента
#define WS_BORDER      0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP     0x00010000L
// текст в окне редактирования прижат к левому краю
#define ES_LEFT        0x0000L
// стиль всех элементов на окне
#define WS_CHILD       0x40000000L
// стиль - кнопка
#define BS_PUSHBUTTON  0x00000000L
// центрировать текст на кнопке
#define BS_CENTER      0x00000300L
// тип окна - "поплавок"
#define WS_POPUP       0x80000000L
// стиль - диалоговое окно Windows 95
#define DS_3DLOOK      0x0004L

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 100
STYLE WS_SYSMENU | DS_3DLOOK
CAPTION "Окно с всплывающими подсказками"
FONT 8, "Arial"
{
// окно редактирования, идентификатор 1
  CONTROL "", 1, "edit", ES_LEFT | WS_CHILD
  | WS_VISIBLE | WS_BORDER
  | WS_TABSTOP , 100, 5, 130, 12
// кнопка, идентификатор 2
  CONTROL "Выход", 2, "button", BS_PUSHBUTTON
  | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
  180, 76, 50, 14
}

// диалоговое окно подсказки
HINTW DIALOG 0, 0, 240, 8
STYLE DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_BORDER
FONT 8, "MS Sans Serif"
{
}

; файл HINT.INC
;константы
;цвет фона окна подсказки
RED    = 255
GREEN  = 255
BLUE   = 150
RGBB   equ (RED or (GREEN shl 8)) or (BLUE shl 16)

;цвет текста окна подсказки
RED   = 20
GREEN = 20
BLUE  = 20
RGBT   equ (RED or (GREEN shl 8)) or (BLUE shl 16)

;сообщение приходит при закрытии окна
WM_CLOSE       equ 10h
WM_INITDIALOG  equ 110h
WM_COMMAND     equ 111h
WM_TIMER       equ 113h
WM_SETTEXT     equ 0Ch
WM_COMMAND     equ 111h
WM_PAINT       equ 0Fh

;прототипы внешних процедур
IFDEF MASM
   EXTERN      CreateDialogParamA@20:NEAR
   EXTERN      SetFocus@4:NEAR
   EXTERN      lstrcpyA@8:NEAR
   EXTERN      DestroyWindow@4:NEAR
   EXTERN      lstrlenA@4:NEAR
   EXTERN      GetDlgItem@8:NEAR
   EXTERN      GetCursorPos@4:NEAR
   EXTERN      TextOutA@20:NEAR
   EXTERN      SetBkColor@8:NEAR
   EXTERN      SetTextColor@8:NEAR
   EXTERN      BeginPaint@8:NEAR
   EXTERN      EndPaint@8:NEAR
   EXTERN      GetTextExtentPoint32A@16:NEAR
   EXTERN      MoveWindow@24:NEAR
   EXTERN      GetWindowRect@8:NEAR
   EXTERN      ReleaseDC@8:NEAR
   EXTERN      GetDC@4:NEAR
   EXTERN      SendDlgItemMessageA@20:NEAR
   EXTERN      ExitProcess@4:NEAR
   EXTERN      GetModuleHandleA@4:NEAR
   EXTERN      DialogBoxParamA@20:NEAR
   EXTERN      EndDialog@8:NEAR
   EXTERN      SetTimer@16:NEAR
   EXTERN      KillTimer@8:NEAR
ELSE
   EXTERN      CreateDialogParamA:NEAR
   EXTERN      SetFocus:NEAR
   EXTERN      lstrcpyA:NEAR
   EXTERN      DestroyWindow:NEAR
   EXTERN      lstrlenA:NEAR
   EXTERN      GetDlgItem:NEAR
   EXTERN      GetCursorPos:NEAR
   EXTERN      TextOutA:NEAR
   EXTERN      SetBkColor:NEAR
   EXTERN      SetTextColor:NEAR
   EXTERN      BeginPaint:NEAR
   EXTERN      EndPaint:NEAR
   EXTERN      GetTextExtentPoint32A:NEAR
   EXTERN      MoveWindow:NEAR
   EXTERN      GetWindowRect:NEAR
   EXTERN      ReleaseDC:NEAR
   EXTERN      GetDC:NEAR
   EXTERN      SendDlgItemMessageA:NEAR
   EXTERN      ExitProcess:NEAR
   EXTERN      GetModuleHandleA:NEAR
   EXTERN      DialogBoxParamA:NEAR
   EXTERN      EndDialog:NEAR
   EXTERN      SetTimer:NEAR
   EXTERN      KillTimer:NEAR
   CreateDialogParamA@20 = CreateDialogParamA
   SetFocus@4 = SetFocus
   lstrcpyA@8 = IstrcpyA
   DestroyWindow@4 = DestroyWindow
   lstrlenA@4 = IstrlenA
   GetDlgItem@8 = GetDlgItem
   GetCursorPos@4 = GetCursorPos
   TextOutA@20 = TextOutA
   SetBkColor@8 = SetBkColor
   SetTextColor@8 = SetTextColor
   BeginPaint@8 = BeginPaint
   EndPaint@8 = EndPaint
   GetTextExtentPoint32A@16 = GetTextExtentPoint32A
   MoveWindow@24 = MoveWindow
   GetWindowRect@8 = GetWindowRect
   ReleaseDC@8 = ReleaseDC
   GetDC@4 = GetDC
   SendDlgItemMessageA@20=SendDlgItemMessageA
   ExitProcess@4 = ExitProcess
   GetModuleHandleA@4 = GetModuleHandleA
   DialogBoxParamA@20 = DialogBoxParamA
   EndDialog@8 = EndDialog
   SetTimer@16 = SetTimer
   KillTimer@8 = KillTimer
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DD ?
     MSMESSAGE  DD ?
     MSWPARAM   DD ?
     MSLPARAM   DD ?
     MSTIME     DD ?
     MSPT       DD ?
MSGSTRUCT ENDS

; структура размера окна
RECT  STRUC
     L  DD ?
     T  DD ?
     R  DD ?
     B  DD ?
RECT  ENDS

;структура размер
SIZ  STRUC
    X DD ?
    Y DD ?
SIZ  ENDS

; структура для BeginPaint
PAINTSTR STRUC
     hdc    DWORD 0
     fErase DWORD 0
     left   DWORD 0
     top    DWORD 0
     right  DWORD 0
     bottom DWORD 0
     fRes   DWORD 0
     fIncUp DWORD 0
     Reserv DB 32 dup (0)
PAINTSTR ENDS

; структура для получения позиции курсора
POINT  STRUC
     X DD ?
     Y DD ?
POINT  ENDS

; файл HINT.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
include hint.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
; для компоновщика LINK.EXE
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
   includelib c:\masm32\lib\gdi32.lib
ELSE
; для компоновщика TLINK32.EXE
   includelib c:\tasm32\lib\import32.lib
ENDIF
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG        MSGSTRUCT <?>
     HINST      DD 0 ; дескриптор приложения
     PA         DB "DIAL1",0
     HIN        DB "HINTW",0
     XX       DD ?
     YY       DD ?
;-------------------------------
     R1       RECT <?>
     R2       RECT <?>
     S        SIZ  <?>
     PS       PAINTSTR <?>
     PT       POINT <?>
; дескрипторы окон-подсказок, для первого и второго элемента
     H1       DD 0
     H2       DD 0
; строка-подсказка
     HINTS     DB 60 DUP (?)
; перечень подсказок
     HINT1     DB "Редактирование строки",0
     HINT2     DB "Кнопка выхода",0
; для временного хранения контекста устройства
     DC       DD ?
; счетчик
     P1       DD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;----------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
KOL:
;-----------------------------
     PUSH  0
     CALL  ExitProcess@4
;-----------------------------

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;------------------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; здесь реакция на закрытие окна
; удалить таймер
L4:
     PUSH 2 ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL KillTimer@8
; закрыть диалог
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH], WM_INITDIALOG
     JNE L2
; здесь начальная инициализация
; установить таймер
     PUSH OFFSET TIMPROC
     PUSH 500  ; интервал 0.5 с.
     PUSH 2    ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL SetTimer@16
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JNE L3
; кнопка выхода?
     CMP WORD PTR [EBP+10H],2
     JNE L3
     JMP L4
L3:
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX,0
     RET 16
WNDPROC    ENDP

;---------------------------------------------
; процедура таймера
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM - промежуток запуска Windows
; [EBP+10Н]   ; WAPARAM - идентификатор таймера
; [EBP+0CH]   ; WM_TIMER
; [EBP+8]     ; HWND
TIMPROC     PROC
     PUSH EBP
     MOV EBP,ESP
; получить положение курсора
     PUSH OFFSET PT
     CALL GetCursorPos@4
; запомнить координаты
     MOV EAX,PT.X
     MOV XX,EAX
     MOV EAX,PT.Y
     MOV YY,EAX
; получить положение элементов
; окно редактирования
     PUSH 1
     PUSH DWORD PTR [EBP+08H]
     CALL GetDlgItem@8
     PUSH OFFSET R1
     PUSH EAX
     CALL GetWindowRect@8
; кнопка выхода
     PUSH 2
     PUSH DWORD PTR [EBP+08H]
     CALL GetDlgItem@8
     PUSH OFFSET R2
     PUSH EAX
     CALL GetWindowRect@8
; увеличить счетчик
     INC P1
     MOV ECX,XX
     MOV EDX,YY
; проверка условий
.IF H1==0 && P1>5
  .IF EDX<=R1.B && EDX>=R1.T && ECX>=R1.L && ECX<=R1.R
; подготовить строку
     PUSH OFFSET HINT1
     PUSH OFFSET HINTS
     CALL lstrcpyA@8
; создать диалоговое окно - подсказку
     PUSH  0
     PUSH  OFFSET HINT
     PUSH  DWORD PTR [EBP+08H]
     PUSH  OFFSET HIN
     PUSH  [HINST]
     CALL  CreateDialogParamA@20
     MOV  H1,EAX
; установить фокус
     PUSH  DWORD PTR [EBP+08H]
     CALL SetFocus@4
; обнулить счетчик
     MOV P1,0
     JMP _END
  .ENDIF
.ENDIF
.IF H1!=0
  .IF (EDX>R1.B || EDX<R1.T) || (ECX<R1.L || ECX>R1.R)
; удаление подсказки в связи с перемещением курсора
     PUSH H1
     CALL DestroyWindow@4
     MOV H1,0
     JMP _END
  .ENDIF
.ENDIF
.IF H2==0 && P1>5
  .IF EDX<=R2.B && EDX>=R2.T && ECX>=R2.L && ECX<=R2.R
; подготовить строку
     PUSH OFFSET HINT2
     PUSH OFFSET HINTS
     CALL lstrcpyA@8
; создать диалоговое окно - подсказку
     PUSH  0
     PUSH  OFFSET HINT
     PUSH  DWORD PTR [EBP+08H]
     PUSH  OFFSET HIN
     PUSH  [HINST]
     CALL  CreateDialogParamA@20
     MOV  H2,EAX
; установить фокус
     PUSH  DWORD PTR [EBP+08H]
     CALL SetFocus@4
; обнулить счетчик
     MOV P1,0
     JMP _END
  .ENDIF
.ENDIF
.IF H2!=0
  .IF (EDX>R2.B || EDX<R2.T) || (ECX<R2.L || ECX>R2.R)
;удаление подсказки в связи с перемещением курсора
      PUSH H2
      CALL DestroyWindow@4
      MOV H2,0
      JMP _END
  .ENDIF
.ENDIF
; восстановить стек
_END:
     POP EBP
     RET 16
TIMPROC    ENDP

; процедура окна всплывающей подсказки
HINT PROC
     PUSH EBP
     MOV EBP,ESP
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE NO_INIT
; инициализация
; получить контекст
     PUSH DWORD PTR [EBP+08H]
     CALL GetDC@4
     MOV DC,EAX
; получить длину строки
     PUSH OFFSET HINTS
     CALL lstrlenA@4
; получить длину и ширину строки
     PUSH OFFSET S
     PUSH EAX
     PUSH OFFSET HINTS
     PUSH DC
     CALL GetTextExtentPoint32A@16
; установить положение и размер окна-подсказки
     PUSH 0
     PUSH S.Y
     ADD S.X,2
     PUSH S.X
     SUB YY,20
     PUSH YY
     ADD XX,10
     PUSH XX
     PUSH DWORD PTR [EBP+08H]
     CALL MoveWindow@24
; закрыть контекст
     PUSH DC
     PUSH DWORD PTR [EBP+08H]
     CALL ReleaseDC@8
; установить таймер
     PUSH 0
     PUSH 6000 ; интервал 6 с.
     PUSH 3    ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL SetTimer@16
     JMP FIN
NO_INIT:
     CMP DWORD PTR [EBP+0CH],WM_PAINT
     JNE NO_PAINT
; перерисовка окна
; получить контекст
     PUSH OFFSET PS
     PUSH DWORD PTR [EBP+08H]
     CALL BeginPaint@8
     MOV DC,EAX
; установить цвета фона и текста подсказки
     PUSH RGBB
     PUSH EAX
     CALL SetBkColor@8
     PUSH RGBT
     PUSH DC
     CALL SetTextColor@8
; вывести текст
     PUSH OFFSET HINTS
     CALL lstrlenA@4
     PUSH EAX
     PUSH OFFSET HINTS
     PUSH 0
     PUSH 0
     PUSH DC
     CALL TextOutA@20
; закрыть контекст
     PUSH OFFSET PS
     PUSH DWORD PTR [EBP+08H]
     CALL EndPaint@8
     JMP FIN
NO_PAINT:
     CMP DWORD PTR [EBP+0CH],WM_TIMER
     JNE FIN
; обработка события таймера
; удалить таймер и удалить диалоговое окно
; подсказка удаляется в связи с истечением срока 6 с.
     PUSH 3
     PUSH DWORD PTR [EBP+08H]
     CALL KillTimer@8
     PUSH DWORD PTR [EBP+08H]
     CALL DestroyWindow@4
FIN:
     POP EBP
     RET 16
HINT ENDP
_TEXT ENDS
END START

Puc. 3.1.3. Пример диалогового окна с всплывающими подсказками.

Рис. 3.1.4. Диалоговое окно с всплывающими подсказками.

Комментарий к программе на Рис. 3.1.3.

1. Прежде всего обращу ваше внимание, что в данной программе мы используем условные конструкции времени выполнения. Данный шаг вполне закономерен и обусловлен только необходимостью несколько сократить объем, а также упростить читаемость программы. Вложенность условных конструкций, а также расстановка скобок вызваны желанием сократить длину строки и одновременно необходимостью транслировать программу как с помощью MASM32, так и с помощью TASM32. Я уже имел возможность сказать, что два ассемблера весьма сильно отличаются, когда речь идет о макросредствах.

2. Трансляция программы.
Пакет MASM32.

   ML /c /coff /DMASM hint.asm
   RC hint.rc
   LINK /SUBSYSTEM:WINDOWS hint.obj hint.res
Пакет TASM32.
   TASM32 /ml hint.asm
   BRCC32 hint.rc
   TLINK32 -aa hint.obj,,,,,hint.res

3. Как Вы, наверное, уже поняли, процедура таймера проверяет каждые 0.5 секунды положение курсора. Если курсор находится на элементе (окне редактирования или кнопке) и подсказка еще не вызвана (H1 или Н2 отлична от нуля), то вызывается подсказка. При этом учитывается еще величина счетчика (P1), чтобы подсказка появлялась с некоторой задержкой. Если при очередном вызове процедуры окажется, что курсор находится уже вне элемента, а подсказка еще на экране, то она удаляется. Данный механизм не учитывает тот случай, когда курсор быстро перейдет от одного элемента к другому. В этом случае вероятна ситуация, когда на экране окажется две подсказки. Впрочем, первая подсказка должна тут же исчезнуть.

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

5. Функция GetCursorPos получает положение курсора в абсолютных координатах относительно экрана. Здесь не возникает проблем, т.к. функция GetWindowRect также получает положение элемента окна в абсолютных координатах. Предварительно нам приходится определять дескриптор окна при помощи функции GetDlgItem.


Часть III. Глава 2. Многозадачное программирование

Глава 2. Многозадачное программирование

В предыдущей главе нами были рассмотрены возможности использования таймеров в прикладной задаче. Задав один или несколько таймеров, мы принуждаем систему вызывать одну или несколько процедур в автоматическом режиме. Посредством таймеров мы тем самым можем реализовать многозадачный режим в рамках одного процесса. Более того, было показано, что такие подзадачи могут взаимодействовать друг с другом. Это и понятно, ведь все эти подзадачи разделяют одно адресное пространство и, следовательно, информация от одной подзадачи к другой может передаваться через глобальные переменные. Это весьма интересный и сложный вопрос о том, как задачи и подзадачи могут взаимодействовать друг с другом. Мы рассмотрим его несколько позднее. Сейчас же рассмотрим многозадачность в операционной системе Windows с самого начала.

I

Под процессом будем понимать объект, создаваемый операционной системой Windows обычно при загрузке исполняемого модуля и получающий в единоличное пользование:

  1. Виртуальную память, выделяемую для него операционной системой.
  2. Дескрипторы открываемых им файлов.
  3. Список загруженных им в его собственную память динамических модулей (DLL).
  4. Созданные им подпроцессы или потоки, исполняемые независимо друг от друга, в собственной памяти процесса.

Думаю, данное определение весьма ясно раскрывает суть понятия процесс. Но для большинства рассматриваемых в данной главе проблем, достаточно было бы дать и более простое определение. Например, такое: всякий исполняемый модуль (.ЕХЕ), запущенный в операционной системе Windows, становится процессом.

Теперь что касается подпроцесса. Смысл его достаточно прост: каждый процесс в отведенном для него адресном пространстве может порождать еще процессы. Эти процессы выполняются независимо друг от друга и от порождающего их процесса. Однако порождающий процесс может при желании "убить" любой из порожденных им процессов. Такие процессы называют еще потоками, а также цепочками или нитями40. Теперь мы понимаем, что в предыдущей главе при помощи таймера мы создавали потоки. Однако в операционной системе Windows для создания потоков есть и специальные средства, речь о которых пойдет ниже.

Теперь немного поговорим о типах многозадачности. В старой 16-битной Windows переключение между задачами происходило только тогда, когда задача отдавала управление операционной системе. Такая многозадачность называется невытесняющей. В определенном смысле это было даже хуже, чем в операционной системе MS DOS. Там элементы многозадачности осуществлялись при помощи так называемых TSR-программ (см. [1]). Такие программы назывались еще резидентными. Они перехватывали прерывание от таймера, клавиатуры или другого устройства и имели возможность время от времени получать управление.

Положение, существовавшее в старой операционной системе Windows, требовало от программиста выполнения джентльменского правила - не захватывать надолго время микропроцессора. Некоторым решением проблемы являлось использование таймеров (в чем мы уже убедились), а также использование функции PeekMessage вместо GetMessage. Функция PeekMessage, в отличие от GetMessage, возвращает управление сразу, даже если в очереди нет ни одного сообщения.

В 32-битных операционных системах Windows (Windows 9x, Windows NT, Windows 2000) реализована вытесняющая схема многозадачности, в которой переключением между процессами и потоками занимается операционная система. Если процесс слишком долго выполняет некоторую операцию, то курсор над окном процесса преобразуется в песочные часы. При этом другие процессы будут по-прежнему выполняться, и Вы сможете переключаться на них. А вот доступ к окну данного процесса может оказаться затруднительным. Решить данную проблему можно уже упомянутым способом, заменив в цикле ожидания GetMessage на PeekMessage. Однако более правильным решением будет разбиение процесса на некоторое количество потоков.

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

  • 1-й параметр - указывает на имя запускаемой программы. Имя может содержать полный путь к программе.
  • 2-й параметр - его значение зависит от того, является первый параметр NULL (0) или нет. Если первый параметр указывает на строку, то данный параметр трактуется как командная строка запуска (без имени программы). Если первый параметр равен NULL, то данный параметр рассматривается как командная строка, первый элемент которой представляет собой имя программы. Если путь к программе не указан, то функция CreateProcess осуществляет поиск программы по определенному алгоритму:
    1. Поиск в каталоге, откуда была запущена программа.
    2. Поиск в текущем каталоге.
    3. Поиск в системном каталоге (можно получить через GetSystemDirectory). Обычно системным каталогом является C:\WINDOWS\SYSTEM.
    4. Поиск в каталоге Windows (можно получить через GetWindowsDirectory). Обычно этим каталогом является C:\WINDOWS.
    5. Поиск в каталогах, перечисленных в параметре PATH окружения.
  • 3-й и 4-й параметры. Используются для задания атрибутов доступа порождаемого процесса. Обычно полагают равным 0.
  • 5-й параметр. Если этот параметр 0, то порождаемый процесс не наследует дескрипторы порождающего процесса, в противном случае порождаемый процесс наследует дескрипторы.
  • 6-й параметр. Может менять свойства порождаемого процесса. Если параметр равен нулю, то свойства задаются по умолчанию. В силу большого количества значений этого параметра, мы отсылаем желающих к соответствующим справочным руководствам.
  • 7-й параметр. Является указателем на буфер, содержащий параметры среды. Если параметр равен 0, то порождаемый процесс наследует параметры среды порождающего процесса.
  • 8-й параметр. Задает текущее устройство и каталог для порождаемого процесса. Если параметр равен NULL, порождаемый процесс наследует текущее устройство и каталог порождающего процесса.
  • 9-й параметр. Представляет указатель на структуру, которая содержит информацию об окне создаваемого процесса. Ниже будут рассмотрены поля этой структуры.
  • 10-й параметр. Указывает на структуру, заполняемую при выполнении запуск приложения. Вот эта структура:

PROCINF STRUC
     hProcess DD ? ; дескриптор созданного процесса.
     hThread  DD ? ; дескриптор главного потока нового процесса.
     Idproc   DD ? ; идентификатор созданного процесса.
     idThr    DD ? ; идентификатор главного потока нового процесса.
PROCINF ENDS

Основное отличие дескриптора от идентификатора заключается в том, что дескриптор уникален лишь в пределах данного процесса, идентификатор же является глобальной величиной. Посредством идентификатора может быть найдена база данных текущего процесса. У читателя, я думаю, сразу возникнет вопрос: а чем дескриптор приложения, который мы получаем при помощи функции GetModuleHandle, от только что упомянутых величин? Дескриптор приложения, или дескриптор модуля есть величина локальная, т.е. действующая в пределах данного процесса и, как правило, равная адресу загрузки модуля в виртуальное адресное пространство. Дескриптор модуля имеется у любого модуля, загруженного в память, в том числе и у подчиненных DLL-библиотек.

Рассмотрим теперь структуру, на которую указывает 9-й параметр функции CreateProcess. Вот эта структура:

STARTUP STRUC
     cb               DD 0
     lpReserved       DD 0
     lpDesktop        DD 0
     lpTitle          DD 0
     dwX              DD 0
     dwY              DD 0
     dwXSize          DD 0
     dwYSize          DD 0
     dwXCountChars    DD 0
     dwYCountChars    DD 0
     dwFillAttribute  DD 0
     dwFlags          DD 0
     wShowWindow      DW 0
     cbReserved2      DW 0
     lpReserved2      DD 0
     hStdInput        DD 0
     hStdOutput       DD 0
     hStdError        DD 0
STARTUP ENDS

Итак, разберем смысл полей этой структуры.
cb - размер данной структуры в байтах. Заполняется обязательно.
lpReserved - резерв, должно быть равно нулю.
lpDesktop - имя рабочего стола (и рабочей станции). Имеет смысл только для Windows NT.
lpTitle - название окна для консольных приложений, создающих свое окно. Для остальных приложений должно быть равно 0.
dwX - координата X левого верхнего угла окна.
dwY - координата Y левого верхнего угла окна.
dwXSize - размер окна по X.
dwYSize - размер окна по Y.
dwXCountChars - размер буфера консоли по X.
dwYCountChars - размер буфера консоли по Y.
dwFillAttribute - начальный цвет текста. Имеет значение только для консольных приложений.
dwFlags - флаг значения полей. Вот значение этого флага.

Макро-значение флага | Значение константы | Смысл значения
STARTF_USESHOWWINDOW | 1h | Разрешить поле dwShowWindow
STARTF_USESIZE | 2h | Разрешить dwXSize и dwYSize
STARTF_USEPOSITI0N | 4h | Разрешить dwX и dwY
STARTF_USECOUNTCHARS | 8h | Разрешить dwXCountChars и dwYCountChars
STARTF_USEFILLATTR1BUTE | 10h | Разрешить dwFillAttribute
STARTF_FORCEONFEEDBACK | 40h | Включить возврат курсора
STARTF_FORCEOFFFEEDBACK | 80h | Выключить возврат курсора
STARTF_USESTDHANDLES | 100h | Разрешить hStdInput

wShowWindow - определяет способ отображения окна.
cbReserved2 - резерв, должно быть равно 0.
hStdInput - дескриптор ввода (для консоли).
hStdOutput - дескриптор вывода (для консоли).
hStdError - дескриптор вывода сообщения об ошибке (для консоли).

Следующий пример (Рис. 3.2.1) представляет собой простейший пример создания процесса. В качестве программы, порождающей процесс, взят редактор WINWORD.EXE. Для проверки правильности работы примера Вам придется указать путь к WINWORD на Вашем компьютере (переменная PATH). Обратите внимание на то, что приложение появляется на экране в свернутом виде и как это достигается. Как видите, функция CreateProcess совсем не так уж страшна, как кажется на первый взгляд.

// файл proces.rc

// определение констант
#define WS_SYSMENU    0x00080000L
#define WS_POPUP      0x80000000L
#define DS_3DLOOK     0x0004L

//ресурс - меню
MENUP MENU
{
  POPUP "&Запуск Word"
  {
  MENUITEM "&3апустить", 1
  MENUITEM "&Удалить", 2
  MENUITEM "Выход из &программы", 3
  }
}

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_POPUP | WS_SYSMENU | DS_3DLOOK
CAPTION "Пример немодального диалогового окна"
FONT 8, "Arial"
{
}

; файл proces.inc

; константы
STARTF_USESHOWWINDOW equ 1h
SW_SHOWMINIMIZED     equ 2h

; сообщение приходит при закрытии окна
WM_CLOSE       equ 10h
WM_INITDIALOG  equ 110h
WM_COMMAND     equ 111h

; прототипы внешних процедур
IFDEF MASM
; для MASM
     EXTERN TerminateProcess@8:NEAR
     EXTERN CreateProcessA@40:NEAR
     EXTERN DialogBoxParamA@20:NEAR
     EXTERN EndDialog@8:NEAR
     EXTERN MessageBoxA@16:NEAR
     EXTERN ExitProcess@4:NEAR
     EXTERN GetModuleHandleA@4:NEAR
     EXTERN LoadMenuA@8:NEAR
     EXTERN SetMenu@8:NEAR
     EXTERN TranslateMessage@4:NEAR
ELSE
; для TASM
     EXTERN TerminateProcess:NEAR
     EXTERN CreateProcessA:NEAR
     EXTERN DialogBoxParamA:NEAR
     EXTERN EndDialog:NEAR
     EXTERN MessageBoxA:NEAR
     EXTERN ExitProcess:NEAR
     EXTERN GetModuleHandleA:NEAR
     EXTERN LoadMenuA:NEAR
     EXTERN SetMenu:NEAR
     EXTERN TranslateMessage:NEAR
     TerminateProcess@8 = TerminateProcess
     CreateProcessA@40 = CreateProcessA
     DialogBoxParamA@20 = DialogBoxParamA
     EndDialog@8 = EndDialog
     MessageBoxA@16 = MessageBoxA
     ExitProcess@4 = ExitProcess
     GetModuleHandleA@4 = GetModuleHandleA
     LoadMenuA@8 = LoadMenuA
     SetMenu@8 = SetMenu
     TranslateMessage@4 = TranslateMessage
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DD ?
     MSMESSAGE  DD ?
     MSWPARAM   DD ?
     MSLPARAM   DD ?
     MSTIME     DD ?
     MSPT       DD ?
MSGSTRUCT ENDS

; структура для CreateProcess
STARTUP STRUC
     cb                DD 0
     lpReserved        DD 0
     lpDesktop         DD 0
     lpTitle           DD 0
     dwX               DD 0
     dwY               DD 0
     dwXSize           DD 0
     dwYSize           DD 0
     dwXCountChars     DD 0
     dwYCountChars     DD 0
     dwFillAttribute   DD 0
     dwFlags           DD 0
     wShowWindow       DW 0
     cbReserved2       DW 0
     lpReserved2       DD 0
     hStdInput         DD 0
     hStdOutput        DD 0
     hStdError         DD 0
STARTUP ENDS

; структура - информация о процессе
PROCINF STRUC
     hProcess DD ?
     hThread  DD ?
     Idproc   DD ?
     idThr    DD ?
PROCINF ENDS

; файл proces.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include proces.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
; директивы MASM
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
ELSE
; директивы TASM
   includelib c:\tasm32\lib\import32.lib
ENDIF

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND   DD 0
     MSG       MSGSTRUCT <?>
     STRUP     STARTUP <?>
     INF       PROCINF <?>
     HINST  DD 0 ; дескриптор приложения
     PA     DB "DIAL1",0
     PMENU  DB "MENUP",0
     PATH   DB "C:\Program Files\Office\WINWORD.EXE",0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
; создать модальный диалог
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     PUSH  0
     CALL  ExitProcess@4
;-----------------------------------

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
; сообщение при закрытии окна
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; закрыть диалоговое окно
     JMP L5
L1:
; сообщение при инициализации окна
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L3
; загрузить меню
     PUSH OFFSET PMENU
     PUSH [HINST]
     CALL LoadMenuA@8
; установить меню
     PUSH EAX
     PUSH DWORD PTR [EBP+08H]
     CALL SetMenu@8
    JMP FINISH
; проверяем, не случилось ли чего с пунктами
; меню в диалоговом окне
L3:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JNE FINISH
     CMP WORD PTR [EBP+10H],3
     JE  L5
     CMP WORD PTR [EBP+10H],2
     JE  L7
     CMP WORD PTR [EBP+10H],1
     JE  L6
     JMP FINISH
; закрыть диалоговое окно
L5:
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
; запустить программу WORD
L6:
; заполняем структуру для запуска
; окно должно появляться в свернутом виде
     MOV STRUP.cb,68
     MOV STRUP.lpReserved,0
     MOV STRUP.lpDesktop,0
     MOV STRUP.lpTitle,0
     MOV STRUP.dwFlags, STARTF_USESHOWWINDOW
     MOV STRUP.cbReserved2,0
     MOV STRUP.lpReserved2,0
     MOV STRUP.wShowWindow,SW_SHOWMINIMIZED
; запуск приложения Winword
     PUSH OFFSET INF
     PUSH OFFSET STRUP
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH OFFSET PATH
     PUSH 0
     CALL CreateProcessA@40
     JMP FINISH
; удалить из памяти процесс
L7:
     PUSH 0 ; код выхода
     PUSH INF.hProcess
     CALL TerminateProcess@8
FINISH:
     MOV EAX,0
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. 3.2.1. Пример создания процесса.

Трансляция программы PROCES.ASM на Рис. 3.2.1.
Пакет MASM32.

   ML /c /coff /DMASM proces.asm
   RC proces.rc
   LINK /SUBSYSTEM:WINDOWS proces.obj proces.res
Пакет TASM32.
   TASM32 /ml proces.asm
   BRCC32 proces.rc
   TLINK32 -aa proces.obj,,,,,proces.res

Думаю, что другой комментарий для программы на Рис. 3.2.1 не требуется.

40 В английской терминологии поток называется "THREAD", что буквально переводится как нить.

II

Теперь пришла пора вплотную заняться потоками. Вначале я намерен решить задачу из предыдущей главы (Рис. 3.1.2) при помощи потока.

Поток может быть создан при помощи функции CreateThread. Рассмотрим параметры этой функции.

  • 1-й параметр. Указатель на структуру атрибутов доступа. Имеет значение только для Windows NT. Обычно полагается NULL.
  • 2-й параметр. Размер стека потока. Если параметр равен нулю, то берется размер стека по умолчанию, равный размеру стека родительского потока.
  • 3-й параметр. Указатель на потоковую функцию, с вызова которой начинается исполнение потока.
  • 4-й параметр. Параметр для потоковой функции.
  • 5-й параметр. Флаг, определяющий состояние потока. Если флаг равен 0, то выполнение потока начинается немедленно. Если значение флага потока равно CREATE_SUSPENDED (4H), то поток находится в состояние ожидания и запускается по выполнению функции ResumeThread.
  • 6-й параметр. Указатель на переменную, куда будет помещен дескриптор потока.

Как уже было сказано, выполнение потока начинается с потоковой функции. Окончание работы этой функции приводит к естественному окончанию работы потока. Поток также может закончить свою работу, выполнив функцию ExitThread с указанием кода выхода. Наконец, порождающий поток может закончить работу порожденного потока при помощи функции TerminateThread. В нашем примере на Рис. 3.2.2 запускаемый процесс не может сам закончить свою работу и прекращает свою работу вместе с приложением по команде TerminateThread. Надо сказать, что такое завершение, вообще говоря, является аварийным и не рекомендуется к обычному употреблению. Связано это с тем, что при таком завершении не выполняется никаких действий по освобождению занятых ресурсов (блоки памяти, открытые файлы и т.п.). Поэтому стройте свои приложения так, что бы поток завершался по выходу из потоковой процедуры. Таким образом, в некотором смысле приведенный ниже пример является демонстрацией того, чего не рекомендуется делать.

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

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

// файл thread.rc
// определение констант
#define WS_SYSMENU     0x00080000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE     0x10000000L
// бордюр вокруг элемента
#define WS_BORDER      0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP     0x00010000L
// текст в окне редактирования прижат к левому краю
#define ES_LEFT        0x000OL
// стиль всех элементов на окне
#define WS_CHILD       0x40000000L
// запрещается ввод с клавиатуры
#define ES_READONLY    0x0800L
// стиль - кнопка
#define BS_PUSHBUTTON  0x00000000L
// центрировать текст на кнопке
#define BS_CENTER      0x00000300L

#define DS_3DLOOK      0x0004L

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 100
STYLE WS_SYSMENU | DS_3DLOOK
CAPTION "Пример использования потока"
FONT 8, "Arial"
{
 // окно редактирования, идентификатор 1
  CONTROL "", 1, "edit", ES_LEFT | WS_CHILD
  | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_READONLY,
  100, 5, 130, 12
 // кнопка, идентификатор 2
  CONTROL "Выход", 2, "button", BS_PUSHBUTTON
  | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
  180, 76, 50, 14
}

; файл thread.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE       equ 10h
; сообщение приходит при создании окна
WM_INITDIALOG  equ 110h
; сообщение приходит при событии с элементом на окне
WM_COMMAND     equ 111h
; сообщение посылки текста элементу
WM_SETTEXT     equ 0Ch

; прототипы внешних процедур
IFDEF MASM
; для MASM
     EXTERN      SendMessageA@16:NEAR
     EXTERN      GetDlgItem@8:NEAR
     EXTERN      Sleep@4:NEAR
     EXTERN      TerminateThread@8:NEAR
     EXTERN      CreateThread@24:NEAR
     EXTERN      wsprintfA:NEAR
     EXTERN      GetLocalTime@4:NEAR
     EXTERN      ExitProcess@4:NEAR
     EXTERN      GetModuleHandleA@4:NEAR
     EXTERN      DialogBoxParamA@20:NEAR
     EXTERN      EndDialog@8:NEAR
ELSE
; для TASM
     EXTERN      SendMessageA:NEAR
     EXTERN      GetDlgItem:NEAR
     EXTERN      Sleep:NEAR
     EXTERN      TerminateThread:NEAR
     EXTERN      CreateThread:NEAR
     EXTERN      _wsprintfA:NEAR
     EXTERN      GetLocalTime:NEAR
     EXTERN      ExitProcess:NEAR
     EXTERN      GetModuleHandleA:NEAR
     EXTERN      DialogBoxParamA:NEAR
     EXTERN      EndDialog:NEAR
     SendMessageA@16 = SendMessageA
     GetDlgItem@8 = GetDlgItem
     Sleep@4 = Sleep
     TerminateThread@8 = TerminateThread
     CreateThread@24 = CreateThread
     wsprintfA =  _wsprintfA
     GetLocalTime@4 = GetLocalTime
     ExitProcess@4 = ExitProcess
     GetModuleHandleA@4 = GetModuleHandleA
     DialogBoxParamA@20 = DialogBoxParamA
     EndDialog@8 = EndDialog
ENDIF
; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DD ?
     MSMESSAGE  DD ?
     MSWPARAM   DD ?
     MSLPARAM   DD ?
     MSTIME     DD ?
     MSPT       DD ?
MSGSTRUCT ENDS

; структура данных дата-время
DAT STRUC
     year    DW ?
     month   DW ?
     dayweek DW ?
     day     DW ?
     hour    DW ?
     min     DW ?
     sec     DW ?
     msec    DW ?
DAT ENDS

; файл thread.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include thread.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
; для компоновщика LINK.EXE
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
   includelib c:\masm32\lib\gdi32.lib
ELSE
; для компоновщика TLINK32.EXE
   includelib c:\tasm32\lib\import32.lib
ENDIF
;----------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG     MSGSTRUCT <?>
     HINST   DD 0 ; дескриптор приложения
     PA      DB "DIAL1",0
     TIM     DB "Дата %u/%u/%u   Время %u:%u:%u",0
     STRCOPY DB 50 DUP (?)
     DATA    DAT <0>
     HTHR    DD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
; создать диалоговое окно
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
; сообщение об ошибке
KOL:
;------------------------------------
     PUSH  0
     CALL  ExitProcess@4
;------------------------------------

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;-------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
L3:
; здесь реакция на закрытие окна
; удалить поток
     PUSH 0
     PUSH HTHR
     CALL TerminateThread@8
; закрыть диалог
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
; здесь начальная инициализация
; получить дескриптор окна редактирования
     PUSH 1
     PUSH DWORD PTR [EBP+08H]
     CALL GetDlgItem@8
; создать поток
     PUSH OFFSET HTHR    ; сюда дескриптор потока
     PUSH 0
     PUSH EAX            ; параметр
     PUSH OFFSET GETTIME ; адрес процедуры
     PUSH 0
     PUSH 0
     CALL CreateThread@24
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JNE FINISH
; кнопка выхода?
     CMP WORD PTR [EBP+10H],2
     JE  L3
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX,0
     RET 16
WNDPROC    ENDP

; потоковая функция
; [EBP+8]  параметр=дескриптор окна редактирования
GETTIME PROC
     PUSH EBP
     MOV EBP,ESP
L0:
; задержка в 1 секунду
     PUSH 1000
     CALL Sleep@4
; получить локальное время
     PUSH OFFSET DATA
     CALL GetLocalTime@4
; получить строку для вывода даты и времени
     MOVZX EAX,DATA.sec
     PUSH EAX
     MOVZX EAX,DATA.min
     PUSH EAX
     MOVZX EAX,DATA.hour
     PUSH EAX
     MOVZX EAX,DATA.year
     PUSH EAX
     MOVZX EAX,DATA.month
     PUSH EAX
     MOVZX EAX,DATA.day
     PUSH EAX
     PUSH OFFSET TIM
     PUSH OFFSET STRCOPY
     CALL wsprintfA
; отправить строку в окно редактирования
     PUSH OFFSET STRCOPY
     PUSH 0
     PUSH WM_SETTEXT
     PUSH DWORD PTR [EBP+08H]
     CALL SendMessageA@16
     JMP L0 ; бесконечный цикл
     POP EBP
     RET 4
GETTIME ENDP
_TEXT ENDS
END START

Рис. 3.2.2. Пример создания потока.

Комментарий к программе на Рис. 3.2.2.

Трансляция программы THREAD.ASM на Рис. 3.2.2.
Пакет MASM32.

   ML /c /coff /DMASM thread.asm
   RC thread.rc
   LINK /SUBSYSTEM:WINDOWS thread.obj thread.res
Пакет TASM32.
   TASM32 /ml thread.asm
   BRCC32 thread.rc
   TLINK32 -aa thread.obj,,,,,thread.res

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

III

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

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

Часто случается, что данные невозможно получать периодически. Они могут, например, зависеть от деятельности третьего процесса. Как же второй процесс узнает, что данные уже готовы для передачи? На первый взгляд проблема решается введением дополнительной переменной, назовем ее FLAG. Примем, что при FLAG=0 данные не готовы, а при FLAG=1 данные готовы. Далее действует весьма простая схема.

NO_DAT:
     CMP FLAG,1
     JNE NO_DAT
     ...   ;передача данных
     ...
     MOV FLAG,0
     ...

Это фрагмент, как Вы понимаете, для второго потока. Первый же поток также должен проверять переменную FLAG и, если FLAG=0, поместить новые данные и установить значение переменной FLAG, равное единице. Данная схема совсем не плоха, например, когда один процесс ждет окончания работы другого процесса. Другими словами, данные являются результатом всей работы этого процесса. Например, запущен компилятор, а другой поток ждет окончания его работы, дабы вывести результаты этой работы на некое устройство. Эта ситуация весьма распространена, но все же случай этот частный.

А что будет, если процесс передачи данных должен производится многократно? Легко видеть, что данная схема будет работать и в более сложном случае. Важно, однако, чтобы второй процесс менял содержимое FLAG только после того, как он возьмет данные, а первый процесс - после того, как положит данные. Если нарушить это правило, то может возникнуть коллизия, когда, например, второй процесс еще не взял данные, а они уже изменились.

Такой подход можно осуществить и в более общем случае, когда два потока (или - два процесса) должны поочередно получать доступ к одному ресурсу. Как легко видеть, данный подход предполагает, что ресурс открыт либо для одного, либо для другого процесса. Если бы поставить задачу несколько иным образом - процесс либо открыт, либо закрыт для доступа, то возникло бы некоторое затруднение. Действительно, вероятна такая ситуация, когда оба потока ожидают открытия ресурса. Другими словами, они непрерывно осуществляют проверку переменной FLAG (CMP FLAG,1). Может статься, что они оба почти одновременно обратятся к ресурсу. Совершенно ясно, что здесь возникает необходимость в "третьей силе", которая бы занималась распределением доступа к ресурсу. Например, посылала бы сообщение вначале одному потоку и, если он ожидает доступа, давала доступ именно ему, а затем подобный процесс повторяется со вторым потоком.

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

Оставшаяся часть данного раздела будет всецело посвящена средствам синхронизации операционной системы Windows.

Семафоры. Семафор представляет собой глобальный объект, позволяющий синхронизировать работу двух или нескольких процессов или потоков. Для программиста семафор - это просто счетчик. Если счетчик равен N, это означает, что к ресурсу имеют доступ N процессов. Рассмотрим функции для работы с семафорами.

CreateSemaphore - создает глобальный объект-семафор. Возвращает дескриптор семафора. Параметры функции:

  • 1-й параметр. Указатель на структуру, определяющую атрибуты доступа. Может иметь значение для Windows NT. Обычно данный параметр полагается равным NULL.
  • 2-й параметр. Начальное значение счетчика семафора. Определяет, сколько задач имеют доступ к ресурсу вначале.
  • 3-й параметр. Количество задач, которые имеют одновременный доступ к ресурсу.
  • 4-й параметр. Указатель на строку, содержащую имя семафора.

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

  • 1-й параметр. Определяет желаемый уровень доступа к семафору. Возможные значения:
    SEMAPHORE_MODIFY_STATE = 2Н, разрешить использование функции ReleaseSemaphore,
    SYNCHRONIZE = 100000Н, разрешить использование любой функции ожидания, только для Windows NT,
    SEMAPHORE_ALL_ACCESS = 0F0000h + SYNCHRONIZE + 3H, специфицирует все возможные флаги доступа к семафору.
  • (в книге пропущен) 2-й параметр. Указывает, может ли наследоваться дескриптор семафора, возвращаемый данной функцией, процессом, созаваемым функцией CreateProcess; 0 - не может.
  • (также в книге пропущен) 3-й параметр. Указатель на ASCIIZ-строку, содержащую имя семафора.

WaitForSingleObject - ожидать открытие семафора. При успешном завершении, т.е. открытии доступа к объекту, функция возвращает 0. Значение 102h будет означать, что закончился заданный период ожидания. Параметры функции:

  • 1-й параметр. Дескриптор семафора.
  • 2-й параметр. Время ожидания в миллисекундах. Если параметр равен INFINITE = 0FFFFFFFFh, то время ожидания не ограничено.

ReleaseSemaphore - освободить семафор и позволить получить доступ к ресурсу другим процессам. Параметры функции:

  • 1-й параметр. Дескриптор семафора.
  • 2-й параметр. Определяет, какое значение должно быть добавлено к счетчику семафора. Чаще всего этот параметр равен единице.
  • 3-й параметр. Указатель на переменную, куда должно быть помещено предыдущее значение счетчика.

Рассмотрим алгоритм работы с семафором. Сначала при помощи функции CreateSemaphore создается семафор и его дескриптор присваивается глобальной переменной. Перед попыткой обращения к ресурсам, доступ к которым необходимо ограничить, поток должен вызвать функцию WaitForSingleObject. При открытии доступа функция возвращает 0. По окончании работы с ресурсом следует вызвать функцию ReleaseSemaphore. Тем самым увеличивается счетчик доступа на 1. С помощью семафора можно регулировать количество потоков, которые одновременно могут иметь доступ к ресурсу. Максимальное значение счетчика как раз и определяет, сколько потоков могут получить доступ к ресурсу одновременно. Но обычно, как я уже говорил, максимальное значение полагают равным 1.

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

CreateEvent - создает объект-событие. Параметры функции.
1-й параметр. Имеет тот же смысл, что и первый параметр функции CreateSemaphore. Обычно полагается равным NULL.
2-параметр. Если параметр не равен нулю, то событие может быть сброшено при помощи функции ResetEvent. Иначе событие сбрасывается при доступе к нему какого либо процесса.
3-й параметр. Если параметр равен 0, то событие инициализируется как сброшенное, в противном случае сразу же подается сигнал о наступлении соответствующей ситуации.
4-й параметр. Указатель на строку, которая содержит имя события.

Ожидание события осуществляется, как и в случае с семафором, функцией WaitForSingleObject.

Функция OpenEvent аналогична функции OpenSemaphore, и на ней мы останавливаться не будем.

SetEvent - подать сигнал о наступлении события. Параметры функции.
1-й параметр. Дескриптор события.

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

InitializeCriticalSection - данная функция создает объект под названием критическая секция. Параметры функции.

  • 1-й параметр. Указатель на структуру, указанную ниже. Поля данной структуры используются только внутренними процедурами, и смысл их безразличен.
    CRITICAL_SECTION STRUCT
     DebugInfo      DWORD ?
     LockCount      LONG  ?
     RecursionCount LONG ?
     OwningThread   HANDLE ?
     LockSemaphore  HANDLE ?
     SpinCount      DWORD ?
CRITICAL_SECTION ENDS

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

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

DeleteCriticalSection - удалить объект "критическая секция". Параметр аналогичен предыдущим.

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

Теперь рассмотрим пример использования критической секции. Примеры использования семафоров и событий Вы сможете найти в книге [4]. Изложим вкратце идею, положенную в основу примера на Рис. 3.2.3.

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

Часть процедуры, выводящей очередной символ, сделана критической, поэтому доступ к выводу в окно в данный момент времени имеет только один поток.

; файл thread2.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY     equ 2
; сообщение приходит при создании окна
WM_CREATE     equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN   equ 201h
; сообщение при щелчке правой кнопкой мыши в области окна
WM_RBUTTONDOWN   equ 204h
; свойства окна
CS_VREDRAW     equ 1h
CS_HREDRAW     equ 2h
CS_GLOBALCLASS   equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
stylcl  equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
DX0   equ 300
DY0   equ 200
; компоненты цветов
RED   equ 50
GREEN  equ 50
BLUE   equ 255
RGBW   equ (RED or (GREEN shl 8)) or (BLUE shl 16)
RGBT   equ 255 ; красный
; идентификатор стандартной иконки
IDI_APPLICATION  equ 32512
; идентификатор курсора
IDC_CROSS     equ 32515
; режим показа окна - нормальный
SW_SHOWNORMAL   equ 1

; прототипы внешних процедур
IFDEF MASM
     EXTERN Sleep@4:NEAR
     EXTERN CreateThread@24:NEAR
     EXTERN InitializeCriticalSection@4:NEAR
     EXTERN EnterCriticalSection@4:NEAR
     EXTERN LeaveCriticalSection@4:NEAR
     EXTERN DeleteCriticalSection@4:NEAR
     EXTERN GetTextExtentPoint32A@16:NEAR
     EXTERN CreateWindowExA@48:NEAR
     EXTERN DefWindowProcA@16:NEAR
     EXTERN DispatchMessageA@4:NEAR
     EXTERN ExitProcess@4:NEAR
     EXTERN GetMessageA@16:NEAR
     EXTERN GetModuleHandleA@4:NEAR
     EXTERN LoadCursorA@8:NEAR
     EXTERN LoadIconA@8:NEAR
     EXTERN PostQuitMessage@4:NEAR
     EXTERN RegisterClassA@4:NEAR
     EXTERN ShowWindow@8:NEAR
     EXTERN TranslateMessage@4:NEAR
     EXTERN UpdateWindow@4:NEAR
     EXTERN TextOutA@20:NEAR
     EXTERN CreateSolidBrush@4:NEAR
     EXTERN SetBkColor@8:NEAR
     EXTERN SetTextColor@8:NEAR
     EXTERN GetDC@4:NEAR
     EXTERN DeleteDC@4:NEAR
ELSE
     EXTERN Sleep:NEAR
     EXTERN CreateThread:NEAR
     EXTERN InitializeCriticalSection:NEAR
     EXTERN EnterCriticalSection:NEAR
     EXTERN LeaveCriticalSection:NEAR
     EXTERN DeleteCriticaiSection:NEAR
     EXTERN GetTextExtentPoint32A:NEAR
     EXTERN CreateWindowExA:NEAR
     EXTERN DefWindowProcA:NEAR
     EXTERN DispatchMessageA:NEAR
     EXTERN ExitProcess:NEAR
     EXTERN GetMessageA:NEAR
     EXTERN GetModuleHandleA:NEAR
     EXTERN LoadCursorA:NEAR
     EXTERN LoadIconA:NEAR
     EXTERN PostQuitMessage:NEAR
     EXTERN RegisterClassA:NEAR
     EXTERN ShowWindow:NEAR
     EXTERN TranslateMessage:NEAR
     EXTERN UpdateWindow:NEAR
     EXTERN TextOutA:NEAR
     EXTERN CreateSolidBrush:NEAR
     EXTERN SetBkColor:NEAR
     EXTERN SetTextColor:NEAR
     EXTERN GetDC:NEAR
     EXTERN DeleteDC:NEAR

  Sleep@4 = Sleep
  CreateThread@24 = CreateThread
  InitializeCriticalSection@4 = InitializeCriticalSection
  EnterCriticalSection@4 = EnterCriticalSection
  LeaveCriticalSection@4 = LeaveCriticalSection
  DeleteCriticalSection@4 = DeleteCriticaiSection
  GetTextExtentPoint32A@16 = GetTextExtentPoint32A
  CreateWindowExA@48 = CreateWindowExA
  DefWindowProcA@16 = DefWindowProcA
  DispatchMessageA@4 = DispatchMessageA
  ExitProcess@4 = ExitProcess
  GetMessageA@16 = GetMessageA
  GetModuleHandleA@4 = GetModuleHandleA
  LoadCursorA@8 = LoadCursorA
  LoadIconA@8 = LoadIconA
  PostQuitMessage@4 = PostQuitMessage
  RegisterClassA@4 = RegisterClassA
  ShowWindow@8 = ShowWindow
  TranslateMessage@4 = TranslateMessage
  UpdateWindow@4 = UpdateWindow
  TextOutA@20 = TextOutA
  CreateSolidBrush@4 = CreateSolidBrush
  SetBkColor@8 = SetBkColor
  SetTextColor@8 = SetTextColor
  GetDC@4 = GetDC
  DeleteDC@4 = DeleteDC
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND        DD ?
     MSMESSAGE     DD ?
     MSWPARAM      DD ?
     MSLPARAM      DD ?
     MSTIME        DD ?
     MSPT          DD ?
MSGSTRUCT ENDS
;-------------

WNDCLASS STRUC
     CLSSTYLE       DD ?
     CLSLPFNWNDPROC DD ?
     CLSCBCLSEXTRA  DD ?
     CLSCBWNDEXTRA  DD ?
     CLSHINSTANCE   DD ?
     CLSHICON       DD ?
     CLSHCURSOR     DD ?
     CLSHBRBACKGROUND DD ?
     MENNAME        DD ?
     CLSNAME        DD ?
WNDCLASS ENDS

; структура для работы с критической секцией
CRIT STRUC
     DD ?
     DD ?
     DD ?
     DD ?
     DD ?
     DD ?
CRIT ENDS

; структура для определения длины текста
SIZET STRUC
     X1  DWORD ?
     Y1  DWORD ?
SIZET ENDS

; файл thread2.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
;------------------------------------------------
include thread2.inc
; подключения библиотек
IFDEF MASM
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
   includelib c:\masm32\lib\gdi32.lib
ELSE
   includelib c:\tasm32\lib\import32.lib
ENDIF
;------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND DD 0
     MSG     MSGSTRUCT <?>
     WC      WNDCLASS  <?>
     SZT     SIZET     <?>
     HINST   DD 0
     TITLENAME DB 'Вывод в окно двумя потоками',0
     NAM     DB 'CLASS32',0
     XT      DWORD 30
     YT      DWORD 30
     HW      DD ?
     DC      DD ?
     TEXT    DB 'Текст в окне красный',0
     SPA     DB '            '
             DB '            ',0
     IND     DD 0
     SK      CRIT <?>
     THR1    DD ?
     THR2    DD ?
     FLAG1   DD 0
     FLAG2   DD 0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
     MOV  [WC.CLSSTYLE],stylcl
; процедура обработки сообщений
     MOV  [WC.CLSLPFNWNDPROC],OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEXTRA],0
     MOV  [WC.CLSCBWNDEXTRA],0
     MOV  EAX,[HINST]
     MOV  [WC.CLSHINSTANCE],EAX
;----------иконка окна
     PUSH  IDI_APPLICATION
     PUSH  0
     CALL  LoadIconA@8
     MOV  [WC.CLSHICON], EAX
;----------курсор окна
     PUSH  IDC_CROSS
     PUSH  0
     CALL  LoadCursorA@8
     MOV  [WC.CLSHCURSOR], EAX
;----------
     PUSH  RGBW ; цвет кисти
     CALL  CreateSolidBrush@4 ; создать кисть
     MOV  [WC.CLSHBRBACKGROUND],EAX
     MOV  DWORD PTR [WC.MENNAME],0
     MOV  DWORD PTR [WC.CLSNAME],OFFSET NAM
     PUSH  OFFSET WC
     CALL  RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  0
     PUSH  DY0 ; DY0 - высота окна
     PUSH  DX0 ; DX0 - ширина окна
     PUSH  100 ; координата Y
     PUSH  100 ; координата X
     PUSH  WS_OVERLAPPEDWINDOW
     PUSH  OFFSET TITLENAME ; имя окна
     PUSH  OFFSET NAM       ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
; проверка на ошибку
     CMP  EAX,0
     JZ   _ERR
     MOV  [NEWHWND], EAX ; дескриптор окна
;------------------------------------
     PUSH  SW_SHOWNORMAL
     PUSH  [NEWHWND]
     CALL  ShowWindow@8 ; показать созданное окно
;------------------------------------
     PUSH  [NEWHWND]
     CALL  UpdateWindow@4 ;перерисовать видимую часть окна

; петля обработки сообщений
MSG_LOOP:
     PUSH  0
     PUSH  0
     PUSH  0
     PUSH  OFFSET MSG
     CALL  GetMessageA@16
     CMP  AX, 0
     JE   END_LOOP
     PUSH  OFFSET MSG
     CALL  TranslateMessage@4
     PUSH  OFFSET MSG
     CALL  DispatchMessageA@4
     JMP  MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH  [MSG.MSWPARAM]
     CALL  ExitProcess@4
_ERR:
     JMP  END_LOOP
;-------------------------------------

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC    PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH],WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH],WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN
     JNE CONTIN
; проверить флаг запуска
     CMP FLAG1,0
     JNE DEFWNDPROC
     MOV FLAG1,1
; инициализировать указатели
     LEA EAX,TEXT
     MOV IND,EAX
     MOV XT,30
; запуск первого потока
     PUSH OFFSET THR1
     PUSH 0
     PUSH EAX
     PUSH OFFSET THREAD1
     PUSH 0
     PUSH 0
     CALL CreateThread@24
; запуск второго потока
     PUSH OFFSET THR2
     PUSH 0
     PUSH EAX
     PUSH OFFSET THREAD2
     PUSH 0
     PUSH 0
     CALL CreateThread@24
     JMP DEFWNDPROC
CONTIN:
     CMP DWORD PTR [EBP+0CH],WM_RBUTTONDOWN
     JNE DEFWNDPROC
; проверить флаг запуска
     CMP FLAG2,0
     JNE DEFWNDPROC
     MOV FLAG2,1
; инициализировать указатели
     LEA EAX,SPA
     MOV IND,EAX
     MOV XT,30
; запуск первого потока
     PUSH OFFSET THR1
     PUSH 0
     PUSH EAX
     PUSH OFFSET THREAD1
     PUSH 0
     PUSH 0
     CALL CreateThread@24
; запуск второго потока
     PUSH OFFSET THR2
     PUSH 0
     PUSH EAX
     PUSH OFFSET THREAD2
     PUSH 0
     PUSH 0
     CALL CreateThread@24
     JMP DEFWNDPROC
WMCREATE:
     MOV EAX,DWORD PTR [EBP+08H]
; запомнить дескриптор окна в глобальной переменной
     MOV HW,EAX
; инициализировать критическую секцию
     PUSH OFFSET SK
     CALL InitializeCriticalSection@4
     MOV EAX, 0
     JMP FINISH
DEFWNDPROC:
     PUSH  DWORD PTR [EBP+14H]
     PUSH  DWORD PTR [EBP+10H]
     PUSH  DWORD PTR [EBP+0CH]
     PUSH  DWORD PTR [EBP+08H]
     CALL  DefWindowProcA@16
     JMP  FINISH
WMDESTROY:
; удалить критическую секцию
     PUSH OFFSET SK
     CALL DeleteCriticalSection@4
     PUSH  0
     CALL  PostQuitMessage@4 ; WM_QUIT
     MOV  EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP

; вывод
OUTSTR PROC
; проверяем, не закончился ли текст
     MOV EBX,IND
     CMP BYTE PTR [EBX],0
     JNE NO_0
     RET
NO_0:
; вход в критическую секцию
     PUSH OFFSET SK
     CALL EnterCriticalSection@4
;-----------------
     PUSH HW
     CALL GetDC@4
     MOV DC,EAX
;---------------- цвет фона = цвет окна
     PUSH RGBW
     PUSH EAX
     CALL SetBkColor@8
;---------------- цвет текста (красный)
     PUSH RGBT
     PUSH DC
     CALL SetTextColor@8
;---------------- вывести текст
     PUSH 1
     PUSH IND
     PUSH YT
     PUSH XT
     PUSH DC
     CALL TextOutA@20
;- вычислить длину текста в пикселях текста
     PUSH OFFSET SZT
     PUSH 1
     PUSH IND
     PUSH DC
     CALL GetTextExtentPoint32A@16
; увеличить указатели
     MOV EAX,SZT.X1
     ADD XT,EAX
     INC IND
;---------------- закрыть контекст
     PUSH DC
     CALL DeleteDC@4
; выход из критической секции
     PUSH OFFSET SK
     CALL LeaveCriticalSection@4
     RET
OUTSTR ENDP

; первый поток
THREAD1 PROC
L01:
; проверить, не конец ли текста
     MOV EBX,IND
     CMP BYTE PTR [EBX],0
     JE  _END1
; вывод очередного символа
     CALL OUTSTR
; задержка
     PUSH 1000
     CALL Sleep@4
     JMP L01
_END1:
     RET 4
THREAD1 ENDP

; второй поток
THREAD2 PROC
L02:
; проверить, не конец ли текста
     MOV EBX,IND
     CMP BYTE PTR [EBX],0
     JE _END2
; вывод очередного символа
     CALL OUTSTR
; задержка
     PUSH 1000
     CALL Sleep@4
     JMP L02
_END2:
     RET 4
THREAD2 ENDP
_TEXT ENDS
END START

Рис. 3.2.3. Пример синхронизации двух потоков посредством критической секции.

Комментарий к программе.
Трансляция программы THREAD2.ASM на Рис. 3.2.3.

Пакет MASM32.

   ML /c /coff /DMASM thread2.asm
   LINK /SUBSYSTEM:WINDOWS thread2.obj
Пакет TASM32.
   TASM32 /ml thread2.asm
   TLINK32 -aa thread2.obj

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

Для того чтобы несколько замедлить вывод текста, мы вводим задержку (Sleep) в цикл вызова процедуры OUTSTR в каждом потоке.

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

После в обоих потоках срабатывает задержка (функция Sleep).

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

Взаимоисключения. Мы не упомянули еще один способ синхронизации, отложив его подробное описание на следующие главы. Здесь же отметим, что этот способ называется "взаимоисключением" или мьютексом (Mutex). Данный способ синхронизации не удобен для работы с потоками, он более пригоден для процессов. Данный объект создается при помощи функции CreateMutex. Все процессы, пытающиеся создать уже созданный объект, получают дескриптор уже существующего, созданного другим процессом объекта "взаимоисключение". Особенность данного объекта прежде всего в том, что им может владеть только один процесс. В документации фирмы Microsoft рекомендуется использовать данный объект для определения, запущено уже данное приложение или нет. Но об этом речь пойдет ниже.

41 К слову сказать, в однозадачной операционной системе MS DOS проблема совместного функционирования резидентных программ стояла весьма остро. Не смотря на то, что программисты, их писавшие, добивались весьма больших успехов, все же одновременная работа нескольких резидентных программ часто приводила к весьма заметным конфликтам.


Часть III. Глава 3. Создание динамических библиотек

Глава 3. Создание динамических библиотек

I

Использование динамических библиотек (по-другому - библиотек динамической компоновки) - это способ осуществления модульности в период выполнения программы. Динамическая библиотека (Dynamic Link Library - DLL) позволяет упростить и саму разработку программного обеспечения. Вместо того чтобы каждый раз перекомпилировать огромные ЕХЕ-программы, достаточно перекомпилировать лишь отдельный динамический модуль. Кроме того, доступ к динамической библиотеке возможен сразу из нескольких исполняемых модулей, что делает многозадачность более гибкой. Структуре исполняемых модулей будет посвящена отдельная глава, но уже сейчас могу сказать, что структура DLL-модуля практически такая же, как и ЕХЕ-модуля. Тот, кто программировал под MS DOS, должен быть знаком с понятием оверлея. По своей функциональности динамическая библиотека очень похожа на оверлей, но название "динамическая библиотека" более удачно42.

При написании ЕХЕ-модулей Вы уже познакомились с тем, как определять импортируемые функции. Достаточно объявить эти функции как EXTERN. При создании динамической библиотеки Вам придется указывать и импортируемые, и экспортируемые функции.

Для того чтобы двигаться дальше, введу такое понятие, как связывание. Собственно, я уже ввел это понятие, когда рассматривал работу редактора связей. Во время трансляции связываются имена, указанные в программе как внешние, (EXTERN) с соответствующими именами из библиотек, которые указываются при помощи директивы IMPORTLIB. Такое связывание называется ранним (или статическим). Напротив, в случае с динамической библиотекой связывание происходит во время выполнения модуля. Такое связывание называется поздним (или динамическим). При этом позднее связывание может происходить в автоматическом режиме в начале запуска программы и при помощи специальных API-функций (см. ниже), по желанию программиста. При этом говорят о явном и неявном связывании. Сказанное иллюстрирует Рис. 3.3.1. Заметим также, что использование динамической библиотеки экономит дисковое пространство, т.к. представленная в библиотеке процедура содержится лишь один раз, в отличие от процедур, помещаемых в модули из статических библиотек43.

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

Рис. 3.3.1. Иллюстрация понятия связывания в ассемблере.

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

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

  • 1-й параметр. Идентификатор DLL-модуля.
  • 2-й параметр. Причина вызова (см. ниже).
  • 3-й параметр. Резерв.

Рассмотрим подробнее второй параметр процедуры входа. Вот четыре возможных значения этого параметра:

DLL_PROCESS_DETACH    equ 0
DLL_PROCESS_ATTACH    equ 1
DLL_THREAD_ATTACH     equ 2
DLL_THREAD_DETACH     equ 3

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

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

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

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

42 Оверлей (Overlay) в переводе означает перекрытие - указание на то, что в оверлейную область памяти могут загружаться по очереди разные части оверлея, перекрывая друг друга.

43 Вообще говоря, библиотеки, используемые нами для программирования в Windows, такие как import32.lib, user32.lib и т.п., правильнее называть не статическими библиотеками, а библиотеками импорта. В них нет программного кода, а лишь информация, используема для трансляции.

II

Перейдем теперь к разбору программных примеров динамических библиотек. На Рис. 3.3.2 приводится пример простейшей динамической библиотеки. Данная динамическая библиотека, по сути, ничего не делает. Просто при загрузке библиотеки, ее выгрузки, а также вызове процедуры DLLP1 будет вызвано обычное Windows-сообщение. Обратите внимание, как определяется процесс загрузки и выгрузки библиотеки. Заметим также, что процедура входа должна возвращать ненулевое значение. Процедура DLLP1 обрабатывает также один параметр, передаваемый через стек обычным способом.

.386P
; плоская модель
IFDEF MASM
  .MODEL FLAT, stdcall
ELSE
  .MODEL FLAT
ENDIF

PUBLIC DLLP1

; константы
; сообщения, приходящие при открытии
; динамической библиотеки

DLL_PROCESS_DETACH    equ 0
DLL_PROCESS_ATTACH    equ 1
DLL_THREAD_ATTACH     equ 2
DLL_THREAD_DETACH     equ 3

IFDEF MASM
; MASM
; прототипы внешних процедур

   EXTERN      MessageBoxA@16:NEAR

; директивы компоновщику для подключения библиотек
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
   EXTERN      MessageBoxA:NEAR
   MessageBoxA@16 = MessageBoxA
   includelib c:\tasm32\lib\import32.lib
ENDIF
;--------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     TEXT1 DB 'Вход в библиотеку',0
     TEXT2 DB 'Выход из библиотеки',0
     MS    DB 'Сообщение из библиотеки',0
     TEXT  DB 'Вызов процедуры из DLL',0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; [EBP+10H]   ; резервный параметр
; [EBP+0CH]   ; причина вызова
; [EBP+8]     ; идентификатор DLL-модуля
DLLENTRY:
     MOV EAX,DWORD PTR [EBP+0CH]
     CMP EAX,0
     JNE D1
; закрытие библиотеки
     PUSH  0
     PUSH  OFFSET MS
     PUSH  OFFSET TEXT2
     PUSH  0
     CALL  MessageBoxA@16
     JMP _EXIT
D1:
     CMP EAX,1
     JNE _EXIT
; открытие библиотеки
     PUSH  0
     PUSH  OFFSET MS
     PUSH  OFFSET TEXT1
     PUSH  0
     CALL  MessageBoxA@16
_EXIT:
     MOV EAX,1
     RET 12

;———————————————————
; [EBP+8]    ; параметр процедуры
DLLP1 PROC EXPORT
     PUSH EBP
     MOV EBP,ESP
     CMP DWORD PTR [EBP+8],1
     JNE _EX
     PUSH 0
     PUSH OFFSET MS
     PUSH OFFSET TEXT
     PUSH 0
     CALL MessageBoxA@16
_EX:
     POP EBP
     RET 4
DLLP1 ENDP
_TEXT ENDS
END DLLENTRY

Puc. 3.3.2. Простейшая DLL-библиотека.

Программа на Рис. 3.3.2 может быть оттранслирована как с помощью MASM32, так и TASM32 (см. ниже). На этом стоит остановиться более подробно. Прежде всего обратите внимание, что за процедурой, вызываемой из другого модуля, мы указали ключевое слово EXPORT. Это слово необходимо для правильной трансляции в MASM. Для TASM этого не нужно, но, к счастью, этот транслятор просто не замечает наличия какого-либо слова после PROC. Зато для TASM процедура DLLP1 должна быть определена как PUBLIC, кроме того, для трансляции в пакете TASM необходимо подготовить DEF-файл и указать его в командной строке TLINK32. Для создания динамических библиотек в строке link следует указать ключ /DLL, а в строке tlink32 -Tpd (no умолчанию работает ключ -Tpe). Ключ /ENTRY:DLLENTRY в строке link можно опустить, так как точка входа определяется из директивы END DLLENTRY.

Трансляция динамической библиотеки на Рис. 3.3.2.
MASM32.

   ml /c /coff /DMASM dll1.asm
   link /subsystem:windows /DLL /ENTRY:DLLENTRY dll1.obj
TASM32.
   tasm32 /ml dll1.asm
   tlink32 -aa -Tpd dll1.obj,,,,dll1.def
Содержимое dll1.def:
   EXPORTS DLLP1

Ниже на Рис. 3.3.3 представлена программа, которая загружает динамическую библиотеку, показанную на Рис. 3.3.2. Это пример позднего связывания. Библиотека должна быть вначале загружена при помощи функции LoadLibrary. Затем определяется адрес процедуры с помощью функции GetProcAddress, после чего можно осуществлять вызов. Как и следовало ожидать, MASM помещает в динамическую библиотеку вместо DLLP1 имя _DLLP1@0, тогда как TASM помещает имя без искажения. Это мы учитываем в нашей программе. Мы учитываем также возможность ошибки при вызове функций LoadLibrary и GetProcAddress. В этой связи укажем, как (в какой последовательности) ищет библиотеку функция LoadLibrary:

  1. Поиск в каталоге, откуда была запущена программа.
  2. Поиск в текущем каталоге.
  3. В системном директории (GetSystemDirectory).
  4. В директории Windows (GetWindowsDirectory).
  5. В каталогах, указанных в окружении (PATH).

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

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
; прототипы внешних процедур
IFDEF MASM
;MASM
   EXTERN      GetProcAddress@8:NEAR
   EXTERN      LoadLibraryA@4:NEAR
   EXTERN      FreeLibrary@4:NEAR
   EXTERN      ExitProcess@4:NEAR
   EXTERN      MessageBoxA@16:NEAR
; директивы компоновщику для подключения библиотек
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
   EXTERN      GetProcAddress:NEAR
   EXTERN      LoadLibraryA:NEAR
   EXTERN      FreeLibrary:NEAR
   EXTERN      ExitProcess:NEAR
   EXTERN      MessageBoxA:NEAR
; директивы компоновщику для подключения библиотек
   includelib c:\tasm32\lib\import32.lib
   GetProcAddress@8 = GetProcAddress
   LoadLibraryA@4 = LoadLibraryA
   FreeLibrary@4 = FreeLibrary
   ExitProcess@4 = ExitProcess
   MessageBoxA@16 = MessageBoxA
ENDIF
;-----------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     TXT    DB 'Ошибка динамической библиотеки',0
     MS     DB 'Сообщение',0
     LIBR   DB 'DLL1.DLL',0
     HLIB   DD ?
IFDEF MASM
     NAMEPROC DB '_DLLP1@0',0
ELSE
     NAMEPROC DB 'DLLP1',0
ENDIF
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; загрузить библиотеку
     PUSH OFFSET LIBR
     CALL LoadLibraryA@4
     CMP EAX,0
     JE _ERR
     MOV HLIB,EAX
; получить адрес процедуры
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL GetProcAddress@8
     CMP EAX,0
     JNE YES_NAME
; сообщение об ошибке
_ERR:
     PUSH  0
     PUSH  OFFSET MS
     PUSH  OFFSET TXT
     PUSH  0
     CALL  MessageBoxA@16
     JMP  _EXIT
YES_NAME:
     PUSH 1 ; параметр
     CALL EAX
; закрыть библиотеку
     PUSH HLIB
     CALL FreeLibrary@4
; библиотека автоматически закрывается также
; при выходе из программы
; выход
_EXIT:
     PUSH 0
     CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 3.3.3. Вызов динамической библиотеки. Явное связывание.

Трансляция программы на Рис. 3.3.3 ничем не отличается от трансляции обычных программ.

MASM32.

   ml /c /coff /DMASM dllex.asm
   link /subsystem:windows dllex.obj
TASM32.
   tasm32 /ml dllex.asm
   tlink32 -aa dllex.obj

III

Хотя я указал, что, на мой взгляд, неявное связывание менее гибко, я нашел необходимым привести пример неявного связывания. Мы здесь рассмотрим только вызывающую программу, так как вызываемая программа, естественно не меняется. Как видите, текст программы стал несколько проще. Здесь важно заметить, что, во-первых, необходимо объявить вызываемую из динамической библиотеки процедуру как внешнюю, а, во-вторых, подключить статическую библиотеку DLLP1.LIB.

.386P
; плоская модель
IFDEF MASM
  .MODEL FLAT, stdcall
ELSE
  .MODEL FLAT
ENDIF

; константы
; прототипы внешних процедур
includelib dll1.lib
IFDEF MASM
; MASM
   EXTERN      DLLP1@0:NEAR
   EXTERN      ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; директивы компоновщику для подключения библиотек
  includelib c:\tasm32\lib\import32.lib
  EXTERN      DLLP1:NEAR
  EXTERN      ExitProcess:NEAR
  DLLP1@0 = DLLP1
  ExitProcess@4 = ExitProcess
ENDIF

;-----------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
     PUSH 1 ; параметр
     CALL DLLP1@0
; выход
_EXIT:
     PUSH 0
     CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 3.3.4. Вызов динамической библиотеки. Неявное связывание.

У читателя, скорее всего, возникнет вопрос, откуда появляется библиотека DLLP1.LIB. Здесь все достаточно просто. Транслятор MASM создает библиотеку автоматически, а в трансляторе TASM есть утилита IMPLIB, которая создает статическую библиотеку непосредственно из динамической библиотеки.

Трансляция программы на Рис. 3.3.4.

MASM32.

  ml /c /coff /DMASM dllex.asm
  link /subsystem:windows dllex.obj
TASM32.
  tasm32 /ml dllex.asm
  implib dll1.lib dll1.dll
  tlink32 -aa dllex.obj

Ранее было сказано, что возможным механизмом связывания является определения адреса процедуры через порядковый номер. Изложим схему того, как это можно сделать. Сначала Вы должны сопоставить процедуре, которая будет вызываться из динамической библиотеки, некое двухбайтное число. Это делается посредством строки в DEF-файле: EXPORTS DLLP1@1. Здесь процедуре DLLP1 сопоставляется номер 1. DEF-файлы мы уже использовали при трансляции в TASM. Точно также их можно использовать и в MASM (ключ /DEF:имя для программы link). После этого производится трансляция, и динамическая библиотека готова. Теперь при вызове функции GetProcAddress вторым параметром следует указать порядковый номер, точнее двойное слово, младшее слово которого есть порядковый номер, а старшее слово равно нулю. И все будет работать точно также, как и раньше. Лично я не вижу особой необходимости использовать такой подход.

IV

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

; динамическая библиотека DLL2.ASM
.386P
; плоская модель
IFDEF MASM
  .MODEL FLAT, stdcall
ELSE
  .MODEL FLAT
ENDIF

PUBLIC DLLP1

; константы
; сообщения, приходящие при открытии
; динамической библиотеки
DLL_PROCESS_DETACH    equ 0
DLL_PROCESS_ATTACH    equ 1
DLL_THREAD_ATTACH     equ 2
DLL_THREAD_DETACH     equ 3

IFDEF MASM
; MASM
; прототипы внешних процедур
  EXTERN      MessageBoxA@16:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
  EXTERN      MessageBoxA:NEAR
  MessageBoxA@16 = MessageBoxA
  includelib c:\tasm32\lib\import32.lib
ENDIF
;--------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     TEXT DB "Строка в динамической библиотеке",0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; [EBP+10H]   ; резервный параметр
; [EBP+0CH]   ; причина вызова
; [EBP+8]     ; идентификатор DLL-модуля
DLLENTRY:
     MOV EAX,DWORD PTR [EBP+0CH]
     CMP EAX,0
     JNE D1
; закрытие библиотеки
     JMP _EXIT
D1:
     CMP EAX,1
     JNE _EXIT
; открытие библиотеки
_EXIT:
     MOV EAX,1
     RET 12
;---------------------
; адреса параметров
; [EBP+8]
; [EBP+0CH]
DLLP1 PROC EXPORT
     PUSH  EBP
     MOV  EBP,ESP
     PUSH  0
     PUSH  DWORD PTR [EBP+0CH]
     PUSH  DWORD PTR [EBP+8]
     PUSH  0
     CALL  MessageBoxA@16
     POP  EBP
     LEA  EAX,TEXT
     RET 8
DLLP1 ENDP
_TEXT ENDS
END DLLENTRY

; основной модуль DLLEX2.ASM, вызывающий
; процедуру из динамической библиотеки
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
; прототипы внешних процедур
IFDEF MASM
; MASM
  EXTERN      GetProcAddress@8:NEAR
  EXTERN      LoadLibraryA@4:NEAR
  EXTERN      FreeLibrary@4:NEAR
  EXTERN      ExitProcess@4:NEAR
  EXTERN      MessageBoxA@16:NEAR
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
  includelib c:\tasm32\lib\import32.lib
  EXTERN      GetProcAddress:NEAR
  EXTERN      LoadLibraryA:NEAR
  EXTERN      FreeLibrary:NEAR
  EXTERN      ExitProcess:NEAR
  EXTERN      MessageBoxA:NEAR
  GetProcAddress@8 = GetProcAddress
  LoadLibraryA@4 = LoadLibraryA
  FreeLibrary@4 = FreeLibrary
  ExitProcess@4 = ExitProcess
  MessageBoxA@16 = MessageBoxA
ENDIF
;----------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     TXT  DB 'Ошибка динамической библиотеки',0
     MS   DB 'Сообщение',0
     LIBR DB 'DLL2.DLL',0
     HLIB DD ?
     MS1  DB 'Сообщение из библиотеки',0
     TEXT DB 'Строка содержится в основном модуле',0
IFDEF MASM
   NAMEPROC DB '_DLLP1@0',0
ELSE
   NAMEPROC DB 'DLLP1',0
ENDIF
_DATA ENDS

; сегмент кода

_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; [EBP+10H]   ; резервный параметр
; [EBP+0CH]   ; причина вызова
; [EBP+8]     ; идентификатор DLL-модуля
START:
; загрузить библиотеку
     PUSH OFFSET LIBR
     CALL LoadLibraryA@4
     CMP EAX,0
     JE _ERR
     MOV HLIB,EAX
; получить адрес
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL GetProcAddress@8
     CMP EAX,0
     JNE YES_NAME
; сообщение об ошибке
_ERR:
     PUSH 0
     PUSH OFFSET MS
     PUSH OFFSET TXT
     PUSH 0
     CALL MessageBoxA@16
     JMP _EXIT
YES_NAME:
     PUSH OFFSET MS1
     PUSH OFFSET TEXT
     CALL EAX
     PUSH 0
     PUSH OFFSET MS
     PUSH EAX
     PUSH 0
     CALL MessageBoxA@16
; закрыть библиотеку
     PUSH HLIB
     CALL FreeLibrary@4
; библиотека автоматически закрывается также
; при выходе из программы
; выход
_EXIT:
     PUSH 0
     CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 3.3.5. Основной модуль и динамическая библиотека. Передача параметров.

Трансляция программы на Рис. 3.3.5.
MASM32.

  ml /c /coff /DMASM dllex2.asm
  link /subsystem:windows dllex2.obj
TASM32.
  tasm32 /ml dllex2.asm
  tlink32 -aa -Tpd dllex2.obj

Ниже мы рассмотрим весьма интересный пример (Рис. 3.3.6). Основной процесс использует ресурсы загруженной им динамической библиотеки. Я уже говорил, что файлы шрифтов, по сути, являются динамическими библиотеками. Не правда ли удобно: ресурсы можно поместить отдельно от основной программы в динамическую библиотеку, загружая их по мере необходимости? Наша программа вначале загружает иконку из ресурсов динамической библиотеки и устанавливает ее на окно. Если Вы будете щелкать правой кнопкой мыши, направив курсор на окно, то будет вызываться процедура из динамической библиотеки, которая будет поочередно устанавливать то один, то другой значок на окно.

// файл dll3.rc
// идентификаторы

#define IDI_ICON1  3
#define IDI_ICON2 10

// определили иконку
IDI_ICON1 ICON "ico1.ico"
IDI_ICON2 ICON "ico2.ico"

; динамическая библиотека DLL3.ASM
.386P
PUBLIC SETIC
; плоская модель
IFDEF MASM
  .MODEL FLAT, stdcall
ELSE
  .MODEL FLAT
ENDIF

; константы
WM_SETICON     equ 80h

IFDEF MASM
; MASM
; прототипы внешних процедур
  EXTERN      LoadIconA@8:NEAR
  EXTERN      PostMessageA@16:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
; прототипы внешних процедур
  EXTERN      LoadIconA:NEAR
  EXTERN      PostMessageA:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\tasm32\lib\import32.lib
  LoadIconA@8 = LoadIconA
  PostMessageA@16 = PostMessageA
ENDIF
;-------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     PRIZ DB 0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
DLLENTRY:
     MOV EAX,1
     RET 12
; [EBP+8]
; [EBP+0CH]
SETIC PROC EXPORT
     PUSH EBP
     MOV EBP,ESP
; выбрать, какую иконку устанавливать
     CMP PRIZ,0
     JZ  IC_1
     MOV PRIZ,0
     PUSH 3
     JMP CONT
IC_1:
     MOV PRIZ,1
     PUSH 10
CONT:
; загрузить иконку из ресурсов библиотеки
     PUSH DWORD PTR [EBP+0CH]    ; идентификатор
                                 ; динамической
                                 ; библиотеки
     CALL LoadIconA@8
; установить значок окна
     PUSH EAX
     PUSH 0
     PUSH WM_SETICON
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL PostMessageA@16
     POP EBP
     RET 8
SETIC ENDP
_TEXT ENDS
END DLLENTRY

// файл dllex3.rc
// определение констант

#define WS_SYSMENU       0x00080000L
#define WS_MINIMIZEBOX   0x00020000L
#define WS_MAXIMIZEBOX   0x00010000L
#define DS_3DLOOK        0x0004L

// определение диалогового окна

DIAL1 DIALOG 0, 0, 340, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | DS_3DLOOK
CAPTION "Диалоговое окно с иконкой из динамической библиотеки"
FONT 8, "Arial"
{
}

; основной модуль DLLEX3.ASM, вызывающий
; процедуру из динамической библиотеки

.386P
; плоская модель
IFDEF MASM
  .MODEL FLAT, stdcall
ELSE
  .MODEL FLAT
ENDIF

; константы
; сообщение приходит при закрытии окна
WM_CLOSE        equ  10h
WM_INITDIALOG   equ 110h
WM_SETICON      equ  80h
WM_LBUTTONDOWN  equ 201h

; прототипы внешних процедур
IFDEF MASM
; MASM
  EXTERN      PostMessageA@16:NEAR
  EXTERN      GetProcAddress@8:NEAR
  EXTERN      LoadLibraryA@4:NEAR
  EXTERN      FreeLibrary@4:NEAR
  EXTERN      ExitProcess@4:NEAR
  EXTERN      GetModuleHandleA@4:NEAR
  EXTERN      DialogBoxParamA@20:NEAR
  EXTERN      EndDialog@8:NEAR
  EXTERN      LoadIconA@8:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; директивы компоновщику для подключения библиотек
  includelib c:\tasm32\lib\import32.lib
  EXTERN      PostMessageA:NEAR
  EXTERN      GetProcAddress:NEAR
  EXTERN      LoadLibraryA:NEAR
  EXTERN      FreeLibrary:NEAR
  EXTERN      ExitProcess:NEAR
  EXTERN      GetModuleHandleA:NEAR
  EXTERN      DialogBoxParamA:NEAR
  EXTERN      EndDialog:NEAR
  EXTERN      LoadIconA:NEAR
  PostMessageA@16 = PostMessageA
  LoadIconA@8 = LoadIconA
  EndDialog@8 = EndDialog
  GetModuleHandleA@4 = GetModuleHandleA
  DialogBoxParamA@20 = DialogBoxParamA
  GetProcAddress@8 = GetProcAddress
  LoadLibraryA@4 = LoadLibraryA
  FreeLibrary@4 = FreeLibrary
  ExitProcess@4 = ExitProcess
ENDIF
;------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     LIBR  DB 'DLL3.DLL',0
     HLIB  DD ?
     HINST DD ?
     PA    DB "DIAL1",0
IFDEF MASM
     NAMEPROC DB "_SETIC@0",0
ELSE
     NAMEPROC DB "SETIC",0
ENDIF
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
; создать диалог
     MOV  [HINST], EAX
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
; выход
_EXIT:
     PUSH 0
     CALL ExitProcess@4
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;-------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; закрыть библиотеку
; библиотека автоматически закрывается также
; при выходе из программы
     PUSH HLIB
     CALL FreeLibrary@4
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
; загрузить библиотеку
     PUSH OFFSET LIBR
     CALL LoadLibraryA@4
     MOV HLIB,EAX
; загрузить иконку
     PUSH 3           ; идентификатор иконки
     PUSH [HLIB]      ; идентификатор процесса
     CALL LoadIconA@8
; установить иконку
     PUSH EAX
     PUSH 0 ; тип иконки (маленькая)
     PUSH WM_SETICON
     PUSH DWORD PTR [EBP+08H]
     CALL PostMessageA@16
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN
     JNE FINISH
; получить адрес процедуры из динамической библиотеки
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL GetProcAddress@8
; вызвать процедуру с двумя параметрами
     PUSH [HLIB]
     PUSH DWORD PTR [EBP+08H]
     CALL EAX
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX,0
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. З.З.6. Пример загрузки ресурса из динамической библиотеки.

Трансляция программ на Рис. 3.3.6.
MASM32:

  ml /c /coff /DMASM dllex3.asm
  rc dllex3.rc
  link /subsystem:windows dllex3.obj dllex3.res

  ml /c /coff /DMASM dll3.asm
  rc dll3.rc
  link /subsystem:windows /DLL /ENTRY:DLLENTRY dll3.obj dll3.res
TASM32:
  tasm32 /ml dllex3.asm
  brcc32 dllex3.rc
  tlink32 -aa -Tpe dllex3.obj,,,,,dllex3.res

  tasm32 /ml dll3.asm
  brcc32 dll3.rc
  tlink32 -aa -Tpd dll3.obj,,,,dll3.def,dll3.res

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

EXPORTS SETIC

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

V

Рассмотрим теперь вопрос о том, как используют динамическую библиотеку различные экземпляры приложения или разные процессы. Если Вы немного знакомы с принципом функционирования операционной системы Windows, то, возможно, такая постановка вопроса у Вас вызовет недоумение. "У каждого приложения свое адресное пространство, куда загружается динамическая библиотека", - скажете Вы. Конечно, это не совсем рационально, но зато безопасно. О памяти мы еще подробно будем говорить в гл. 3.6, здесь же заметим, что, вообще говоря, приложение может инициализировать так называемую разделяемую память. Мы вернемся к этому вопросу еще неоднократно, сейчас же рассмотрим этот вопрос чисто технически, применительно к динамическим библиотекам. Рассмотрим конкретную ситуацию. Запускаемое приложение загружает динамическую библиотеку и вызывает процедуру из динамической библиотеки, которая меняет данные, расположенные опять же в динамической библиотеке. Запустим теперь второй экземпляр приложения. Оно загружает еще один экземпляр динамической библиотеки. Могут быть ситуации, когда желательно, чтобы второе запущенное приложение "знало", что по команде первого приложения данные уже изменились. Ясно, что в этом случае данные, которыми оперирует динамическая библиотека, должны быть общими. Технически это делается очень просто. У редактора связей LINK есть опция /section: имя, атрибуты, которая позволяет объявить явно свойства данной секции. Мы будем говорить о секциях далее, здесь же достаточно сказать, секция - это просто сегмент в старом понимании. В редакторе связей TLINK32 то же действие можно осуществить с помощью файла .DEF.

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

; динамическая библиотека DLL4.ASM
.386P
; плоская модель
IFDEF MASM
  .MODEL FLAT, stdcall
ELSE
.MODEL FLAT
ENDIF

PUBLIC DLLP1

IFDEF MASM
; MASM
; прототипы внешних процедур
  EXTERN      MessageBoxA@16:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
  EXTERN      MessageBoxA:NEAR
  MessageBoxA@16 = MessageBoxA
  includelib c:\tasm32\lib\import32.lib
ENDIF
;--------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     TEXT DB "В динамической библиотеке",0
     MS   DB "Сообщение",0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; [EBP+10H]   ; резервный параметр
; [EBP+0CH]   ; причина вызова
; [EBP+8]     ; идентификатор DLL-модуля
DLLENTRY:
     MOV EAX,1
     RET 12
;------------------
;адреса параметров
DLLP1 PROC EXPORT
     PUSH  EBP
     MOV  EBP,ESP
     PUSH  0
     PUSH  OFFSET MS
     PUSH  OFFSET TEXT
     PUSH  0
     CALL  MessageBoxA@16
; изменим строку, расположенную в разделяемой памяти
     MOV  TEXT,'И'
     MOV  TEXT+1,'з'
     POP  EBP
     RET
DLLP1 ENDP
_TEXT ENDS
END DLLENTRY

; основной модуль DLLEX4.ASM, вызывающий
; процедуру из динамической библиотеки
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
; прототипы внешних процедур
IFDEF MASM
; MASM
     EXTERN      GetProcAddress@8:NEAR
     EXTERN      LoadLibraryA@4:NEAR
     EXTERN      FreeLibrary@4:NEAR
     EXTERN      ExitProcess@4:NEAR
     EXTERN      MessageBoxA@16:NEAR
; директивы компоновщику для подключения библиотек
     includelib c:\masm32\lib\user32.lib
     includelib c:\masm32\lib\kernel32.lib
ELSE
; директивы копоновщику для подключения библиотек
     includelib c:\tasm32\lib\import32.lib
     EXTERN      GetProcAddress:NEAR
     EXTERN      LoadLibraryA:NEAR
     EXTERN      FreeLibrary:NEAR
     EXTERN      ExitProcess:NEAR
     EXTERN      MessageBoxA:NEAR
     GetProcAddress@8 = GetProcAddress
     LoadLibraryA@4 = LoadLibraryA
     FreeLibrary@4 = FreeLibrary
     ExitProcess@4 = ExitProcess
     MessageBoxA@16 = MessageBoxA
ENDIF
;----------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     TXT  DB 'Ошибка динамической библиотеки',0
     MS   DB 'Сообщение',0
     LIBR DB 'DLL4.DLL',0
     HLIB DD ?
IFDEF MASM
     NAMEPROC DB '_DLLP1@0',0
ELSE
     NAMEPROC DB 'DLLP1',0
ENDIF
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; [EBP+10H]   ; резервный параметр
; [EBP+0CH]   ; причина вызова
; [EBP+8]     ; идентификатор DLL-модуля
START:
; загрузить библиотеку
     PUSH OFFSET LIBR
     CALL LoadLibraryA@4
     CMP EAX,0
     JE _ERR
     MOV HLIB,EAX
; получить адрес
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL GetProcAddress@8
     CMP EAX,0
     JNE YES_NAME
; сообщение об ошибке
_ERR:
     PUSH  0
     PUSH OFFSET MS
     PUSH OFFSET TXT
     PUSH 0
     CALL MessageBoxA@16
     JMP _EXIT
YES_NAME:
     CALL EAX
     PUSH 0
     PUSH OFFSET MS
     PUSH OFFSET MS
     PUSH 0
     CALL MessageBoxA@16
; закрыть библиотеку
; библиотека автоматически закрывается также
; при выходе из программы
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL FreeLibrary@4
; выход
_EXIT:
     PUSH 0
     CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 3.3.7. Пример использования разделяемой памяти в динамической библиотеке.

Трансляция программ на Рис. 3.3.7.
MASM32.

  ml /c /coff /DMASM dll4.asm
  link /subsystem:windows /DLL /section:.data,SRW dll4.obj
  ml /c /coff /DMASM dllex4.asm
  link /subsystem:windows dllex4.obj

Атрибуты опции SECTION: S-SHARED, R-READ, W-WRITE.

TASM32.

  tasm32 /ml dll4.asm
  tlink32 -aa -Tpd dll4.obj,,,,dll4.def
  tasm32 /ml dllex4.asm
  tlink32 -aa dllex4.obj

Содержимое DEF-файла:

SECTIONS .DATA SHARED
EXPORTS DLLP1


Часть III. Глава 4. Взаимодействие с ресурсами локальной сети

Глава 4. Взаимодействие с ресурсами локальной сети

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

I

В прикладном программировании часто возникает вопрос определения сетевых устройств. В принципе, вопрос можно поставить более широко: как определить тип того или иного устройства. Тот, кто программировал в MS DOS, помнит, что там правильное определение типа устройства было не простой задачей. Операционная система Windows и здесь облегчает нам задачу. Здесь имеется очень полезная функция GetDriveType, единственным аргументом которой является строка корневого каталога искомого устройства, например "А:\" или "D:\". По возвращаемому функцией значению мы и определяем тип устройства (см. файл driv.inc на Рис. 3.4.1). Результат работы программы представлен на Рис. 3.4.2.

// файл driv.rc
// определение констант

#define WS_SYSMENU        0x00080000L
#define WS_MINIMIZEBOX    0x00020000L
#define WS_MAXIMIZEBOX    0x00010000L
#define WS_VISIBLE        0x10000000L
#define WS_TABSTOP        0x00010000L
#define WS_VSCROLL        0x00200000L
#define DS_3DLOOK             0x0004L
#define LBS_NOTIFY            0x0001L
#define LBS_SORT              0x0002L
#define LBS_WANTKEYBOARDINPUT 0x0400L

// идентификаторы
#define LIST1 101

//определение диалогового окна
DIAL1 DIALOG 0, 0, 180, 110
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | DS_3DLOOK
CAPTION "Определение типов устройств"
FONT 8, "Arial"
{
  CONTROL "ListBox1",LIST1,"listbox", WS_VISIBLE
  | WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_WANTKEYBOARDINPUT,
  16, 16, 100, 75
}

; файл driv.inc
; константы
; значения, возвращаемые функцией GetDriveType
; значения 0 и 1 можно считать признаком отсутствия устройства

DRIVE_REMOVABLE equ 2 ; накопитель на гибком диске
DRIVE_FIXED     equ 3 ; устройство жесткого диска
DRIVE_REMOTE    equ 4 ; сетевой диск
DRIVE_CDROM     equ 5 ; накопитель на лазерном диске
DRIVE_RAMDISK   equ 6 ; электронный диск

; сообщение приходит при закрытии окна
WM_CLOSE        equ  10h
WM_INITDIALOG   equ 110h
WM_COMMAND      equ 111h
LB_ADDSTRING    equ 180h
LB_RESETCONTENT  equ 184h
WM_LBUTTONDOWN   equ 201h

; прототипы внешних процедур
IFDEF MASM
  EXTERN lstrcpy@8:NEAR
  EXTERN lstrcat@8:NEAR
  EXTERN GetDriveTypeA@4:NEAR
  EXTERN ExitProcess@4:NEAR
  EXTERN GetModuleHandleA@4:NEAR
  EXTERN DialogBoxParamA@20:NEAR
  EXTERN EndDialog@8:NEAR
  EXTERN SendDlgItemMessageA@20:NEAR
ELSE
  EXTERN lstrcpy:NEAR
  EXTERN lstrcat:NEAR
  EXTERN GetDriveTypeA:NEAR
  EXTERN ExitProcess:NEAR
  EXTERN GetModuleHandleA:NEAR
  EXTERN DialogBoxParamA:NEAR
  EXTERN EndDialog:NEAR
  EXTERN SendDlgItemMessageA:NEAR
  lstrcpy@8 = lstrcpy
  lstrcat@8 = lstrcat
  GetDriveTypeA@4 = GetDriveTypeA
  ExitProcess@4 = ExitProcess
  GetModuleHandleA@4 = GetModuleHandleA
  DialogBoxParamA@20 = DialogBoxParamA
  EndDialog@8 = EndDialog
  SendDlgItemMessageA@20 = SendDlgItemMessageA
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND      DD ?
     MSMESSAGE   DD ?
     MSWPARAM    DD ?
     MSLPARAM    DD ?
     MSTIME      DD ?
     MSPT        DD ?
MSGSTRUCT ENDS

; файл driv.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include driv.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
  includelib c:\tasm32\lib\import32.lib
ENDIF
;-------------------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     PRIZ   DB 0
     MSG    MSGSTRUCT <?>
     HINST  DD 0 ; дескриптор приложения
     PA     DB "DIAL1",0
     ROO    DB "?:\", 0
     BUFER  DB 40 DUP (0)
     TYP0   DB " Нет устройства",0
     TYP1   DB " Нет устройства",0
     TYP2   DB " Гибкий диск",0
     TYP3   DB " Жесткий диск",0
     TYP4   DB " Сетевой диск",0
     TYP5   DB " Лазерный диск",0
     TYP6   DB " Электронный диск",0
     INDEX  DD OFFSET TYP0
            DD OFFSET TYP1
            DD OFFSET TYP2
            DD OFFSET TYP3
            DD OFFSET TYP4
            DD OFFSET TYP5
            DD OFFSET TYP6
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;--------------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
; сообщение об ошибке
KOL:
;--------------------------------------
     PUSH  0
     CALL  ExitProcess@4
;--------------------------------------
; процедура окна
; расположение параметров в стеке
; [BP+014Н]  ; LPARAM
; [BP+10Н]   ; WAPARAM
; [BP+0CН]   ; MES
; [BP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;--------------------------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
L4:
; здесь анализ устройств и заполнение списка
     MOV ECX,65
L00:
     PUSH ECX
     MOV ROO, CL
; определить тип устройства
     PUSH OFFSET ROO
     CALL GetDriveTypeA@4
; полный список
     CMP PRIZ,0
     JZ  _ALL
     CMP EAX,2
     JB  L3
_ALL:
; получить индекс
     SHL EAX,2
     PUSH EAX
; создать строку для списка
     PUSH OFFSET ROO
     PUSH OFFSET BUFER
     CALL lstrcpy@8
     POP EBX
     PUSH INDEX[EBX]
     PUSH OFFSET BUFER
     CALL lstrcat@8
; отправить строку в список
     PUSH OFFSET BUFER
     PUSH 0
     PUSH LB_ADDSTRING
     PUSH 101
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA@20
L3:
; проверить, не достигнута ли граница цикла
     POP ECX
     INC ECX
     CMP ECX,91
     JNE L00
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN
     JNE FINISH
     PUSH 0
     PUSH 0
     PUSH LB_RESETCONTENT
     PUSH 101
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA@20
     CMP PRIZ,0
     JE YES_0
     MOV PRIZ,0
     JMP L4
YES_0:
     MOV PRIZ,1
     JMP L4
FINISH:
     MOV EAX,0
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. 3.4.1. Простой пример определения типа устройств.

Трансляция программы на Рис. 3.4.1.
MASM32:

  ml /c /coff /DMASM driv.asm
  rc driv.rc
  link /subsystem:windows driv.obj driv.res
TASM32:
  tasm32 /ml driv.asm
  brcc32 driv.rc
  tlink32 -aa driv.obj,,,,,driv.res

Рис. 3.4.2. Результат работы программы на Рис.3.4.1

После того как Вы определили, что данное устройство является сетевым, может возникнуть вторая задача: определить статус устройства. Под статусом в данном случае я понимаю три возможных ситуации: устройство открыто только для чтения и записи, устройство открыто только для чтения, устройство недоступно. Лично я поступаю следующим образом. Для проверки статуса использую две функции CreateFile и GetDiskFreeSpace. С первой функцией Вы уже знакомы, с помощью второй функции можно определить объем свободного места на диске. Если данное устройство позволяет создать файл (файл лучше создавать с атрибутом "удалить после закрытия", и операционная система сама выполнит процедуру удаления) и прочесть данные об устройстве, следовательно, оно открыто для чтения и записи. Если устройство позволяет прочитать данные о нем, но не позволяет создать файл, следовательно, устройство открыто только для чтения. Наконец, если не разрешено ни то, ни другое, следовательно, устройство недоступно. Такой бесхитростный подход, однако, работает весьма эффективно.

II

В данном разделе мы рассмотрим вопрос о доступе к ресурсам локальной сети. При этом следует выделить две проблемы: поиск ресурсов в локальной сети и подключение к ресурсам. Начну с того, что перечислю основные функции для работы с сетевыми ресурсами. Это не все функции, но их вполне достаточно, чтобы Ваша программа самостоятельно искала сетевые ресурсы и подключалась к ним. Конечно, я предполагаю, что вы умеете работать в сети, знаете, что такое сетевое устройство, сетевой компьютер и т.п.

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

NETRESOURCE STRUC
     dwScope       DWORD ?
     dwType        DWORD ?
     dwDisplayType DWORD ?
     dwUsage       DWORD ?
     lpLocalName   DWORD ?
     lpRemoteName  DWORD ?
     lpComment     DWORD ?
     lpProvider    DWORD ?
NETRESOURCE ENDS

dwScope - может принимать одно из трех значений:

  • RESOURCE_CONNECTED - ресурс подсоединен в настоящее время.
  • RESOURCE_REMEMBERED - ресурс, запоминаемый системой, чтобы при запуске автоматически подсоединяться к нему.
  • RESOURCE_GLOBALNET- глобальный сетевой ресурс. Скорее всего, Вам понадобится только последнее значение.

dwType - тип ресурса. Возможны следующие значения:

  • RESOURCETYPE_ANY - любой ресурс.
  • RESOURCETYPE_DISK - диск.
  • RESOURCETYPE_PRINT - сетевой принтер.

dwDisplayType - как данный ресурс должен быть представлен сетевым браузером. Типов довольно много. Например, для сетевого компьютера определен тип RESOURCEDISPLAYTYPE_SERVER, для группы - RESOURCEDISPLAYTYPE_GROUP и т.д.

dwUsage - чаще всего полагают равным 0.

lpLocalName - локальное имя устройства, например Е:, LPT1: и т.п.

lpRemoteName - сетевое имя, например \\SUPER, \\NDI\EPSON и т.д.

lpComment - комментарий к сетевому ресурсу.

lpProvider - имя провайдера. В настоящее время имя может принимать одно из двух значений: Microsoft Network и NetWare, но возможны и другие имена.

WNetAddConnection2 - можно подсоединить к Вашему компьютеру сетевой ресурс (диск или принтер).

  • 1-й параметр. Адрес структуры NETRESOURCE, значение полей которой было разобрано выше. Должны быть заполнены следующие поля: dwType, lpLocalName, lpRemoteName, lpProvider (обычно NULL). Ниже будет приведен пример заполнения.
  • 2-й параметр. Пароль, необходимый для соединения с ресурсом. В случае пустой сроки - соединение беспарольное, в случае NULL - берется пароль, ассоциированный с именем (см. ниже).
  • 3-й параметр. Имя пользователя. Если значение NULL, то берется имя по умолчанию.
  • 4-й параметр. Данный параметр определяет, будет ли система потом автоматически подсоединяться к данному ресурсу. В случае значения 0, такого подсоединения не происходит.
При успешном завершении функция возвращает 0 (NO_ERROR). Это касается и всех остальных рассмотренных ниже функций.

WNetCancelConnection2 - отсоединить ресурс.

  • 1-й параметр. Содержит указатель на строку, содержащую имя ресурса. Причем если имя локальное, то разрывается данное локальное соединение. Если это имя удаленного ресурса, то разрываются все соединения с данным ресурсом.
  • 2-й параметр. Определяет, будет ли система и далее подсоединяться к данному ресурсу. Если 0, то подсоединение (если было) будет возобновляться при следующем запуске системы.
  • 3-й параметр. Если значение не нулевое, то отсоединение произойдет, даже если на сетевом диске имеются открытые файлы или сетевой принтер выполняет задание с данного компьютера.

WNetOpenEnum - открыть поиск сетевых ресурсов. Вообще говоря, сетевые ресурсы представляют собой структуру в виде дерева. Об этом мы будем говорить ниже, поэтому поиск сетевых ресурсов весьма напоминает поиск файлов по дереву каталогов.

  • 1-й параметр. dwScope (см. структуру NETRESOURCE) - обычно полагают RESOURCE_GLOBALNET.
  • 2-й параметр. dwType - для поиска всяких ресурсов следует положить равным RESOURCETYPE_ANY.
  • 3-й параметр. dwUsage - обычно следует положить равным нулю.
  • 4-й параметр. Адрес структур NETRESOURCE. Если адрес равен 0 (NULL), то поиск будет начинаться с самого нижнего уровня (корня), в противном случае поиск начнется с уровня, определяемого полями lpRemoteName и lpProvider.
  • 5-й параметр. Указатель на переменную, которая должна получить дескриптор для дальнейшего поиска.

WNetCloseEnum - закрыть поиск. Единственным параметром этой функции является дескриптор, полученный при выполнении функции WNetOpenEnum.

WNetEnumResource - функция, осуществляющая непосредственный поиск сетевых ресурсов.

  • 1-й параметр. Дескриптор поиска.
  • 2-й параметр. Указатель на переменную, которая должна содержать: какое максимальное количество ресурсов должно быть найдено за один раз. Обычно переменную полагают равной 0FFFFFFFFH - для поиска всех возможных ресурсов. При успешном завершении данной функции переменная будет содержать количество реально найденных ресурсов.
  • 3-й параметр. Указатель на массив, каждым элементом которого является структура NETRESOURCE. Ясно, что данный массив должен быть достаточно большим, чтобы вместить столько данных о ресурсе, сколько Вам нужно. Поскольку размер структуры составляет 32 байта, то в случае большой сети размер массива должен составлять не менее 32000 байт.
  • 4-й параметр. Адрес переменной, содержащей объем массива. Если объем окажется мал, то переменная будет содержать реально требуемый объем.

Лишний раз подчеркну, что данная функция осуществляет поиск не всех ресурсов, а лишь ресурсов данного иерархического уровня. Так что без рекурсии не обойтись.

WNetGetConnection - с помощью данной функции можно получить информацию о данном соединении.

  • 1-й параметр. Адрес локального имени (A:,C:,LPT2 и т.п.).
  • 2-й параметр. Адрес буфера, куда будет помещено удаленное имя.
  • 3-й параметр. Указатель на переменную, содержащую размер буфера.

Завершая краткий обзор сетевых функций и переходя к программированию, замечу, что нами описана лишь часть наиболее важных сетевых функций. Кроме того, в Windows NT имеются еще и свои функции для работы с сетевыми ресурсами, которые отсутствуют в операционной системе Windows 9x. Кстати, будьте готовы к тому, что поведение описанных функций может несколько отличаться в Windows 9x, Windows NT и Windows 2000. О некоторых таких тонкостях мы скажем ниже.

Ниже представлена программа, позволяющая подключаться к сетевым дискам. Командная строка программы: NET \\SERVER\CC Z:. Первый параметр - имя подключаемого устройства, включающее имя сетевого сервера. Второй параметр - локальный диск, на который будет спроецирован сетевой диск.

; программа NET, осуществляющая подсоединение к
; сетевому ресурсу: NET \\SUPER\\D Z:

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
RESOURCETYPE_DISK equ  1h
; прототипы внешних процедур
IFDEF MASM
  EXTERN lstrcat@8:NEAR
  EXTERN lstrlen@4:NEAR
  EXTERN GetStdHandle@4:NEAR
  EXTERN WriteConsoleA@20:NEAR
  EXTERN ExitProcess@4:NEAR
  EXTERN GetCommandLineA@0:NEAR
  EXTERN WNetAddConnection2A@16:NEAR
ELSE
  LOCALS
  EXTERN lstrcat:NEAR
  EXTERN lstrlen:NEAR
  EXTERN GetStdHandle:NEAR
  EXTERN WriteConsoleA:NEAR
  EXTERN ExitProcess:NEAR
  EXTERN GetCommandLineA:NEAR
  EXTERN WNetAddConnection2A:NEAR
  lstrcat@8 = lstrcat
  lstrlen@4 = lstrlen
  GetStdHandle@4 = GetStdHandle
  WriteConsoleA@20 = WriteConsoleA
  ExitProcess@4 = ExitProcess
  GetCommandLineA@0 = GetCommandLineA
  WNetAddConnection2A@16 = WNetAddConnection2A
ENDIF

; структуры
NETRESOURCE STRUC
     dwScope       DWORD ?
     dwType        DWORD ?
     dwDisplayType DWORD ?
     dwUsage       DWORD ?
     lpLocalName   DWORD ?
     lpRemoteName  DWORD ?
     lpComment     DWORD ?
     lpProvider    DWORD ?
NETRESOURCE ENDS

; директивы компоновщику для подключения библиотек
IFDEF MASM
   includelib c:\masm32\lib\user32.lib
   includelib c:\masm32\lib\kernel32.lib
   includelib c:\masm32\lib\mpr.lib
ELSE
   includelib c:\tasm32\lib\import32.lib
ENDIF
;--------------------------------------------------
;сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     BUF1  DB  100 dup (0)
     BUF2  DB  100 dup (0)
     LENS  DWORD ? ; количество выведенных символов
     HANDL DWORD ?
     NR    NETRESOURCE <0>
     PUSTO DB 0
     ERR2  DB "Ошибка!",0
     ERR1  DB "Мало параметров!",0
     ST1   DB "->",0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор выхода вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; получить количество параметров
     CALL NUMPAR
     CMP EAX,3
     JNB PAR_OK
     LEA EBX,ERR1
     CALL SETMSG
     JMP _END
PAR_OK:
; получить параметры
     MOV EDI,2
     LEA EBX,BUF1
     CALL GETPAR
     MOV EDI,3
     LEA EBX,BUF2
     CALL GETPAR
; пытаемся произвести подсоединение
; вначале заполняем структуру NETRESOURCE
; для Windows NT NR.dwType = 0
     MOV NR.dwType, RESOURCETYPE_DISK
     LEA EAX,BUF2
     MOV NR.lpLocalName,EAX
     LEA EAX,BUF1
     MOV NR.lpRemoteName,EAX
     MOV NR.lpProvider,0
; вызов функции, осуществляющей соединение
     PUSH 0
     PUSH OFFSET PUSTO
     PUSH OFFSET PUSTO
     PUSH OFFSET NR
     CALL WNetAddConnection2A@16
     CMP EAX,0
     JE _OK
; сообщение об ошибке
     LEA EBX,ERR2
     CALL SETMSG
     JMP _END
_OK:
; сообщение об успешном соединении
     PUSH OFFSET ST1
     PUSH OFFSET BUF1
     CALL lstrcat@8
     PUSH OFFSET BUF2
     PUSH OFFSET BUF1
     CALL lstrcat@8
     LEA EBX,BUF1
     CALL SETMSG
_END:
     PUSH 0
     CALL ExitProcess@4

; определить количество параметров (->EAX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
@@L1:
     CMP BYTE PTR [ESI],0
     JE @@L4
     CMP BYTE PTR [ESI],32
     JE @@L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP @@L2
@@L3:
     OR EDX,1
@@L2:
     INC ESI
     JMP @@L1
@@L4 :
     MOV EAX,ECX
     RET
NUMPAR ENDP

; получить параметр
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
@@L1:
     CMP BYTE PTR [ESI],0
     JE @@L4
     CMP BYTE PTR [ESI],32
     JE @@L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP @@L2
@@L3:
     OR EDX,1
@@L2:
     CMP ECX,EDI
     JNE @@L5
     MOV AL,BYTE PTR [ESI]
     CMP AL,32
     JE @@L5
     MOV BYTE PTR [EBX],AL
     INC EBX
@@L5:
     INC ESI
     JMP @@L1
@@L4:
     MOV BYTE PTR [EBX],0
     RET
GETPAR ENDP

; вывод сообщения
; EBX -> строка
SETMSG PROC
     PUSH EBX
     CALL lstrlen@4
     PUSH 0
     PUSH OFFSET LENS
     PUSH EAX
     PUSH EBX
     PUSH HANDL
     CALL WriteConsoleA@20
     RET
SETMSG ENDP
_TEXT ENDS
END START

Рис. 3.4.3. Программа, осуществляющая соединение с сетевым ресурсам.

Трансляция программы на Рис. 3.4.3.
MASM32:

  ml /c /coff /DMASM net.asm
  link /subsystem:console net.obj
TASM32:
  tasm32 /ml net.asm
  tlink32 -ap net.obj

  1. Рассматривая программу на Рис. 3.4.3, прежде всего обратите внимание на использование локальных меток. MASM распознает локальные метки автоматически. TASM требует, чтобы локальная метка имела в начале два символа "@@". Кроме того, в начале программы требуется директива LOCALS.
  2. Отметим одну существенную особенность функции WNetAddConnection2A@16. При заполнении структуры NETRESOURCE поле dwType для операционной системы Windows NT должно заполняться нулем, а не RESOURCETYPE_DISK. К сожалению, имеются и другие подобные нюансы при работе с ресурсами локальной сети. Если Вы всерьез займетесь программированием в локальной сети, Вам придется проверять свою программу на разных компьютерах и разных сетях.
  3. Данная программа далека от совершенства. Попробуйте над ней поработать. В частности, не мешало бы проверять, является ли локальное устройство сетевым или нет, и если является, спросить разрешение на переподключение данного устройства. В этом случае необходимо вначале отключить устройство от старого сетевого ресурса при помощи известной Вам функции WNetCancelConnection2.
  4. Если необходимо подключать сетевой принтер, то поле dwType должно быть равно RESOURCETYPE_PRINT.

Следующая программа (Рис. 3.4.4) осуществляет рекурсивный поиск сетевых ресурсов. Работая в консольном режиме, она выдает на экран название провайдера и удаленное имя ресурса. Данная программа должна правильно работать и в сетях Microsoft, и в сетях Novel.

;программа NET1, осуществляющая поиск сетевых ресурсов
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE  equ -11
RESOURCETYPE_DISK  equ 1h
RESOURCE_GLOBALNET equ 2h
RESOURCETYPE_ANY   equ 0h

; прототипы внешних процедур
IFDEF MASM
  EXTERN CharToOemA@8:NEAR
  EXTERN RtlMoveMemory@12:NEAR
  EXTERN WNetCloseEnum@4:NEAR
  EXTERN WNetEnumResourceA@16:NEAR
  EXTERN WNetOpenEnumA@20:NEAR
  EXTERN lstrcpy@8:NEAR
  EXTERN lstrcat@8:NEAR
  EXTERN lstrlen@4:NEAR
  EXTERN GetStdHandle@4:NEAR
  EXTERN WriteConsoleA@20:NEAR
  EXTERN ExitProcess@4:NEAR
  EXTERN GetCommandLineA@0:NEAR
ELSE
  EXTERN  CharToOemA:NEAR
  EXTERN  RtlMoveMemory:NEAR
  EXTERN  WNetCloseEnum:NEAR
  EXTERN  WNetEnumResourceA:NEAR
  EXTERN  WNetOpenEnumA:NEAR
  EXTERN  lstrcpy:NEAR
  EXTERN  lstrcat:NEAR
  EXTERN  lstrlen:NEAR
  EXTERN  GetStdHandle:NEAR
  EXTERN  WriteConsoleA:NEAR
  EXTERN  ExitProcess:NEAR
  EXTERN  GetCommandLineA:NEAR
  CharToOemA@8 = CharToOemA
  RtlMoveMemory@12 = RtlMoveMemory
  WNetCloseEnum@4 = WNetCloseEnum
  WNetEnumResourceA@16 = WNetEnumResourceA
  lstrcpy@8 = lstrcpy
  WNetOpenEnumA@20 = WNetOpenEnumA
  lstrcat@8 = lstrcat
  lstrlen@4 = lstrlen
  GetStdHandle@4 = GetStdHandle
  WriteConsoleA@20 = WriteConsoleA
  ExitProcess@4 = ExitProcess
  GetCommandLineA@0 = GetCommandLineA
ENDIF

; структуры
NETRESOURCE STRUC
     dwScope       DWORD ?
     dwType        DWORD ?
     dwDisplayType DWORD ?
     dwUsage       DWORD ?
     lpLocalName   DWORD ?
     lpRemoteName  DWORD ?
     lpComment     DWORD ?
     lpProvider    DWORD ?
NETRESOURCE ENDS

; директивы компоновщику для подключения библиотек
IFDEF MASM
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
  includelib c:\masm32\lib\mpr.lib
ELSE
  includelib c:\tasm32\lib\import32.lib
ENDIF

;———————————————————————————————————————————————

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     LENS  DWORD ? ; количество выведенных символов
     HANDL DWORD ?
     NR    NETRESOURCE <0>
     ENT   DB 13,10,0
     BUF   DB 100 dup (0)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор выхода вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; запустить процедуру поиска сетевых ресурсов
     PUSH 0
     PUSH OFFSET NR
     CALL POISK
_END:
     PUSH 0
     CALL ExitProcess@4
; процедура поиска
PAR1 EQU [EBP+8]   ; указатель на структуру
PAR2 EQU [EBP+0CH] ; признак
; локальные переменные
HANDLP EQU [EBP-4] ; дескриптор поиска
CC     EQU [EBP-8]
NB     EQU [EBP-12]
NR1    EQU [EBP-44] ; структура
BUFER  EQU [EBP-144] ; буфер
RS     EQU [EBP-32144] ; массив структур
POISK PROC
     PUSH EBP
     MOV EBP,ESP
     SUB ESP,32144
     CMP DWORD PTR PAR2,0
     JNE SECOND
; при первом запуске NULL
     XOR EBX,EBX
     JMP FIRST
SECOND:
; запуск при рекурсивном вызове
; вначале скопировать структуру в локальную
; переменную, хотя для данной программы это излишне
     PUSH 32
     PUSH DWORD PTR PAR1
     LEA EAX,DWORD PTR NR1
     PUSH EAX
     CALL RtlMoveMemory@12
; при вторичном поиске указатель на структуру
     LEA EBX,DWORD PTR NR1
FIRST:
; запуск при первом вызове
     LEA EAX,HANDLP
     PUSH EAX
     PUSH EBX
     PUSH 0
     PUSH RESOURCETYPE_ANY
     PUSH RESOURCE_GLOBALNET
     CALL WNetOpenEnumA@20
     CMP EAX,0
     JNE _EN
; здесь осуществляется основной поиск
REPI:
; запуск функции WNetEnumResource
; объем массива структур NETRESOURCE
     MOV DWORD PTR NB,32000
     LEA EAX,NB
     PUSH EAX
     LEA EAX,RS
     PUSH EAX
; искать максимальное количество объектов
     MOV DWORD PTR CC,0FFFFFFFFH
     LEA EAX,CC
     PUSH EAX
     PUSH DWORD PTR HANDLP
     CALL WNetEnumResourceA@16
     CMP EAX,0
     JNE _CLOSE
; цикл по полученному массиву
     MOV ESI,CC
     SHL ESI,5 ; умножаем на 32
     MOV EDI,0
L00:
     CMP EDI,ESI
     JE REPI
; вывод информации
; провайдер
     MOV EBX,DWORD PTR RS[EDI]+28
     CALL SETMSG
; удаленное имя
     MOV EBX, DWORD PTR RS[EDI]+20
     CALL SETMSG
; сохранить нужные регистры
     PUSH ESI
     PUSH EDI
; теперь рекурсивный вызов
     PUSH 1
     LEA EAX,DWORD PTR RS[EDI]
     PUSH EAX
     CALL POISK
; восстановить регистры
     POP EDI
     POP ESI
     ADD EDI,32
     JMP L00
;-----------------------------------
     JMP REPI
;-----------------------------------
_CLOSE:
     PUSH DWORD PTR HANDLP
     CALL WNetCloseEnum@4
_EN:
     MOV ESP,EBP
     POP EBP
     RET 8
POISK ENDP

; вывод сообщения
; EBX -> строка
SETMSG PROC
; скопировать текст в отдельный буфер
     PUSH EBX
     PUSH OFFSET BUF
     CALL lstrcpy@8
     LEA EBX,BUF
; перекодировать для консоли
     PUSH EBX
     PUSH EBX
     CALL CharToOemA@8
; добавить перевод строки
     PUSH OFFSET ENT
     PUSH EBX
     CALL lstrcat@8
; определить длину строки
     PUSH EBX
     CALL lstrlen@4
; вывести строку
     PUSH 0
     PUSH OFFSET LENS
     PUSH EAX
     PUSH EBX
     PUSH HANDL
     CALL WriteConsoleA@20
     RET
SETMSG ENDP
_TEXT ENDS
END START

Рис. 3.4.4. Рекурсивный поиск сетевых ресурсов в локальной сети.

Трансляция программы на Рис. 3.4.4.
MASM32:

  ml /c /coff /DMASM net1.asm
  link /subsystem:console /STACK:1000000,1000000 net1.obj
TASM32:
  tasm32 /ml net1.asm
  tlink32 -ap -S:1000000 -Sc:1000000 net1.obj

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

  1. Нам уже приходилось сталкиваться с локальными переменными, когда мы рассматривали поиск файлов по дереву каталогов. Данная задача весьма похожа, но есть и отличие. В данном случае мы используем слишком большой объем для локальных переменных. По этой причине мы явно указываем (заказываем) большой объем стека (опции STACK и S,Sc). По умолчанию компоновщик устанавливает всего 8 Кб, что явно не достаточно.
  2. Функция WNetEnumResource требует указать своим параметром массив структур NETRESOURCE. Объем одной структуры 32 байта. Мы резервируем тысячу таких структур. Не много ли, скажете Вы? Честно говоря, я не встречал локальной сети с тысячью сетевых компьютеров. Однако я встречал локальную сеть, где на одном из серверов было создано около восьмисот сетевых каталогов. Если говорить начистоту, то здесь я все же демонстрирую не лучший стиль программирования. Более корректный путь заключается в том, что функция WNetEnumResource вначале вызывается с указанием объема буфера меньше, чем 32 байта, - в этом случае в переменную, содержащую объем буфера, будет возвращен необходимый объем. Зная необходимый объем, программа должна запросить у системы нужный и вторично запустить WNetEnumResource. Данный подход более корректен, но более сложен.
  3. При рекурсивном вызове процедуры POISK первым параметром является указатель на элемент массива структур NETRESOURCE. Мы копируем элемент массива в локальную переменную NR1. В принципе, в данной программе можно этого не делать, а сразу воспользоваться полученным указателем. В более сложных программах, однако, скорее всего, придется этот делать.
  4. Обратите внимание, что в процедуре вывода информации мы копируем строку в буфер, который потом используем для вывода. Это не прихоть, а необходимость. Дело в том, что перед выводом мы добавляем в конец строки коды 13 и 10. Поскольку мы выводим строки, которые потом используем для дальнейшего поиска, нам приходится использовать для вывода дополнительный буфер.

Что осталось за бортом?

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

  1. Сетевое программирование на сервере Windows NT.
  2. Изучение сетевых протоколов как низкого (IPX, TCP/IP, NetBios и т.д.), так и высокого уровня (HTTP, FTP и т.д.).
  3. Программирование сокетов.

44 Сейчас часто под термином сетевое программирование понимают программирование в глобальной сети Интернет, либо использование подходов и средств Интернет в локальной сети (Интранет). Мы, в данном случае, ведем речь о достаточно узкой области задач, касающейся проблем только локальной сети.


Часть III. Глава 5. Разрешение некоторых проблем программирования в Windows

Глава 5. Разрешение некоторых проблем программирования в Windows

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

I

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

Эта проблема решается с использованием системной функции Shell_NotifyIcon. Причем решение это столь просто и прозрачно, что приходится удивляться тому, что большинство для этих целей привлекают какие-то библиотеки. Вот параметры этой функции:

  • 1-й параметр. Действие, которое необходимо произвести:
    • NIM_ADD equ 0h ; добавить иконку на системную панель
    • NIM_MODIFY equ 1h ; удалить иконку
    • NIM_DELETE equ 2h ; модифицировать иконку
  • 2-й параметр. Указатель на структуру, где содержится информация, необходимая для реализации указанного действия.
    NOTI_ICON STRUC
  cbSize           DWORD ?
  hWnd             DWORD ?
  uID              DWORD ?
  uFlags           DWORD ?
  uCallbackMessage DWORD ?
  hIcon            DWORD ?
  szTip            DB 64 DUP (?)
NOTI_ICON ENDS
    • cbSize - размер структуры.
    • hWnd — дескриптор окна, куда будет посылаться сообщение (см. ниже)
    • uID - идентификатор иконки
    • uFlags - комбинация следующих флагов
      NIF_MESSAGE equ 1h; использовать поле hIcon
      NIF_ICON equ 2h; использовать поле uCallbackMessage
      NIF_TIP equ 4h; использовать поле szTip
    • uCallbackMessage - сообщение, которое приходит на окно, определяемое дескриптором
    • hWnd, в случае возникновения некого события вблизи иконки на системной панели. Значение сообщения должно быть больше, чем 1024. При приходе этого сообщения WPARAM содержит идентификатор иконки, a LPARAM - событие, т.е. то, что произошло с иконкой.
    • hIcon - дескриптор иконки.
    • szTip - текст всплывающей подсказки.

Давайте рассмотрим, как работает весь механизм. В случае минимизации окна на функцию окна приходит сообщение WM_SIZE. Причем wParam должен содержать значение SIZE_MINIMIZED. Вот тогда то и следует воспользоваться функцией Shell_NotifyIcon, которая поместит иконку на системную панель. Любые же события с мышью, когда ее курсор находится на иконке, вызовут приход на функцию окна сообщения uCallbackMessage, которое, естественно, мы сами и определи. При этом младшее слово wParam будет содержать идентификатор иконки, а по младшему слову lParam можно определить, что же приключилось (см. ниже). Ниже (Рис. 3.5.1) Вы увидите, как все это работает.

// файл tray.rc
// определение констант
#define WS_SYSMENU     0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x0001OOOOL
#define DS_3DLOOK      0x0004L

// идентификаторы
#define IDI_ICON1 1

// определили иконку
IDI_ICON1 ICON "ico1.ico"

//определение диалогового окна
DIAL1 DIALOG 0, 0, 250, 110
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | DS_3DLOOK
CAPTION "Поместить иконку на системную панель"
FONT 8, "Arial"
{
}

; файл tray.inc
; константы
NIM_ADD    equ 0h ; добавить иконку на системную панель
NIM_MODIFY equ 1h ; удалить иконку
NIM_DELETE equ 2h ; модифицировать иконку
NIF_MESSAGE equ 1h ; использовать поле hIcon
NIF_ICON    equ 2h ; использовать поле uCallbackMessage
NIF_TIP     equ 4h ; использовать поле szTip
FLAG    equ NIF_MESSAGE or NIF_ICON or NIF_TIP

SIZE_MINIMIZED equ 1h
SW_HIDE        equ 0
SW_SHOWNORMAL  equ 1

; сообщение приходит при закрытии окна
WM_CLOSE        equ 10h
WM_INITDIALOG   equ 110h
WM_SIZE         equ 5h
WM_LBUTTONDBLCLK  equ 203h
WM_LBUTTONDOWN    equ 201h

; прототипы внешних процедур
IFDEF MASM
  EXTERN ShowWindow@8:NEAR
  EXTERN LoadIconA@8:NEAR
  EXTERN lstrcpy@8:NEAR
  EXTERN Shell_NotifyIconA@8:NEAR
  EXTERN ExitProcess@4:NEAR
  EXTERN GetModuleHandleA@4:NEAR
  EXTERN DialogBoxParamA@20:NEAR
  EXTERN EndDialog@8:NEAR
  EXTERN SendDlgItemMessageA@20:NEAR
ELSE
  EXTERN ShowWindow:NEAR
  EXTERN LoadIconA:NEAR
  EXTERN lstrcpy:NEAR
  EXTERN Shell_NotifyIconA:NEAR
  EXTERN ExitProcess:NEAR
  EXTERN GetModuleHandleA:NEAR
  EXTERN DialogBoxParamA:NEAR
  EXTERN EndDialog:NEAR
  EXTERN SendDlgItemMessageA:NEAR
  ShowWindow@8 = ShowWindow
  LoadIconA@8 = LoadIconA
  lstrcpy@8 = lstrcpy
  Shell_NotifyIconA@8 = Shell_NotifyIconA
  ExitProcess@4 = ExitProcess
  GetModuleHandleA@4 = GetModuleHandleA
  DialogBoxParamA@20 = DialogBoxParamA
  EndDialog@8 = EndDialog
  SendDlgItemMessageA@20 = SendDlgItemMessageA
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ?
     MSMESSAGE DD ?
     MSWPARAM  DD ?
     MSLPARAM  DD ?
     MSTIME    DD ?
     MSPT      DD ?
MSGSTRUCT ENDS

; структура для функции Shell_NotifyIcon
NOTI_ICON STRUC
     cbSize DWORD ?
     hWnd   DWORD ?
     uID    DWORD ?
     uFlags DWORD ?
     uCallbackMessage DWORD ?
     hIcon  DWORD ?
     szTip  DB 64 DUP (?)
NOTI_ICON ENDS

; файл tray.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include tray.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
  includelib c:\masm32\lib\shell32.lib
ELSE
  includelib c:\tasm32\lib\import32.lib
ENDIF
;————————-----------------------------------------

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG    MSGSTRUCT <?>
     HINST  DD 0 ; дескриптор приложения
     PA     DB "DIAL1",0
     NOTI   NOTI_ICON <0>
     TIP    DB "Пример использования функции"
            DB "Shell_NotifyIcon",0
_DATA ENDS

;сегмент кода

_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;--------------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
; сообщение об ошибке
KOL:
;--------------------------------------
     PUSH  0
     CALL  ExitProcess@4
;--------------------------------------

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;---------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_SIZE
     JNE L3
     CMP DWORD PTR [EBP+10H],SIZE_MINIMIZED
     JNE L3
; здесь работа по установке иконки на системную панель
     MOV NOTI.cbSize,88
     MOV EAX,DWORD PTR [EBP+8H]
     MOV NOTI.hWnd,EAX
     MOV NOTI.uFlags,FLAG
     MOV NOTI.uID,12 ; идентификатор иконки
     MOV NOTI.uCallbackMessage,2000 ; сообщение
; загрузить иконку из ресурсов
     PUSH 1
     PUSH [HINST]
     CALL LoadIconA@8
; скопировать текст всплывающего сообщения
     MOV NOTI.hIcon,EAX
     PUSH OFFSET TIP
     PUSH OFFSET NOTI.szTip
     CALL lstrcpy@8
; поместить иконку
     PUSH OFFSET NOTI
     PUSH NIM_ADD
     CALL Shell_NotifyIconA@8
; спрятать минимизированное окно
     PUSH SW_HIDE
     PUSH DWORD PTR [EBP+08H]
     CALL ShowWindow@8
     JMP FINISH
L3:
; сообщение от иконки на системной панели?
     CMP DWORD PTR [EBP+0CH],2000
     JNE FINISH
; идентификатор нашей иконки?
     CMP WORD PTR [EBP+10H],12
     JNE FINISH
; что произошло? двойной щелчок?
     CMP WORD PTR [EBP+14H],WM_LBUTTONDBLCLK
     JNE  FINISH
; заполнить структуру
     MOV NOTI.cbSize,88
     MOV EAX,DWORD PTR [EBP+8H]
     MOV NOTI.hWnd,EAX
     MOV NOTI.uFlags,NIF_ICON
     MOV NOTI.uID,12
; идентификатор иконки
     MOV NOTI.uCallbackMessage,1000 ; сообщение
; удалить иконку
     PUSH OFFSET NOTI
     PUSH NIM_DELETE
     CALL Shell_NotifyIconA@8
; восстановить окно
     PUSH SW_SHOWNORMAL
     PUSH DWORD PTR [EBP+08H]
     CALL ShowWindow@8
FINISH:
     MOV EAX,0
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. 3.5.1. Демонстрация процедуры помещения иконки на системную панель.

Трансляция программы на Рис. 3.5.1.
MASM32:

  ml /c /coff /DMASM tray.asm
  rc tray.rc
  link /subsystem:windows tray.obj tray.res
TASM32:
  tasm32 /ml tray.asm
  brcc32 tray.rc
  tlink32 -aa tray.obj,,,,,tray.res

В связи с программой на Рис. 3.5.1 хочу особо акцентировать Ваше внимание на сообщении WM_SIZE. Весьма полезное сообщение, я Вам скажу. Представьте, что у себя в окне Вы расположили какую-то информацию. Если окно допускает изменение размеров, то Вам придется решать проблему размещения информации в случае, если размер окна изменился. Так вот, аккуратно все перерисовать и отмасштабировать можно как раз, если использовать данное сообщение. Подчеркну, что сообщение посылается, когда размер окна уже изменился. При этом WPARAM содержит признак того, что произошло с окном, a LPARAM - новый размер окна (младшее слово - ширина, старшее - высота).

II

В. Есть ли дополнительные средства, упрощающие файловую обработку?

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

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

Работу с файлами, отображаемыми в память, производят по следующему алгоритму:

  1. Открыть (или создать) файл с помощью обычной функции CreateFile. Функция, как известно, возвращает дескриптор открытого файла.
  2. Отобразить файл45 с помощью функции CreateFileMapping. Именно эта функция определяет размер отображения. Эта функция, как и предыдущая, возвращает дескриптор, только не обычного, а отображенного файла.
  3. Скопировать файл (или его часть) в созданную область при помощи функции MapViewOfFile. Эта функция возвращает указатель (вот оно!) на начало области, где будет расположен файл. После этого руки у Вас развязаны, и Вы можете делать с отображенным файлом все, что Вам заблагорассудится.
  4. При желании можно записать область памяти в файл при помощи функции FlushViewOfFile. Разумеется, сбрасываться на диск будет та область памяти, которую мы заказали при помощи функции CreateFileMapping. Записывать на диск можно, разумеется, и с помощью обычной функции WriteFile.
  5. Перед тем как закрывать отображенный файл, следует вначале сделать указатель недействительным. Это делается с помощью функции UnmapViewOfFile.
  6. Закрыть следует оба дескриптора. Вначале дескриптор, возвращенный функцией CreateFileMapping, а затем дескриптор, созданный функцией CreateFile.

Как видите, алгоритм работы с отображаемыми файлами весьма прост. Рассмотрим теперь новые для Вас функции.

Функция CreateFileMapping. Возвращает дескриптор отображаемого файла.

  • 1-й параметр. Дескриптор открытого файла.
  • 2-й параметр. Атрибут доступа, обычно полагают равным нулю.
  • 3-й параметр. Может принимать одно из следующих значений, и должен быть совместим с режимом разделения файла: PAGE_READONLY, PAGE_WRITECOPY, PAGE_READWRITE. Как Вы понимаете, этот атрибут определяет защиту отображаемого файла и не должен противоречить атрибуту файла, открытого с помощью CreateFile.
  • 4-й параметр. Старшая часть (32 бита) размера отображаемого файла.
  • 5-й параметр. Младшая часть размера отображаемого файла (как Вы понимаете, размер может и не совпадать с размером файла). Если оба параметра равны нулю, то размер полагается равным размеру открытого файла (1-й параметр).
  • 6-й параметр. Имя отображаемого файла. Необходимо только в том случае, если предполагается, что отображаемый файл будет использоваться несколькими процессами. В этом случае повторный вызов функции CreateFileMapping другими процессами с тем же именем приведет не к созданию нового отображаемого файла, а к возвращению уже созданного дескриптора.

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

  • 1-й параметр. Дескриптор, возвращенный функцией CreateFileMapping.
  • 2-й параметр. Определяет операцию, которую мы будем делать. Например, FILE_MAP_READ означает - только чтение, a FILE_MAP_WRITE - чтение и запись.
  • 3-й параметр. Старшая часть (32-байта) смещения в файле, откуда начинается копирование в память.
  • 4-й параметр. Младшая часть смещения в файле, откуда начинается копирование.
  • 5-й параметр. Определяет количество копируемых байт. Если вы хотите скопировать весь файл, то положите три последних параметра равными 0.

Функция FlushViewOfFile.

  • 1-й параметр. Указывает на область, записываемую в файл.
  • 2-й параметр. Определяет количество записываемых байт.

Функция UnmapViewOfFile.

  • 1-й параметр. Дескриптор отображаемого файла.

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

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

III

В.Можно ли контролировать ввод информации в окне редактирования?

Да, в главе 2.4 мы видели, как можно это корректно делать при помощи горячих клавиш. Однако с помощью горячих клавиш легко блокировать приход в окно редактирования некоторых символов. Речь же в некоторых случаях может идти о том, чтобы преобразовывать вводимую информацию, что называется, "налету". Таким механизмом может быть использование подклассов. Для демонстрации его мы возьмем программу на Рис. 1.3.2, видоизменим ее так, чтобы можно было контролировать ввод информации.

На самом деле в этом механизме нет ничего нового. Еще в операционной системе MS DOS можно было перехватывать прерывание и встраивать туда свою процедуру. В результате при вызове данного прерывания вызывалась вначале Ваша процедура, а потом уже та, которая была установлена ранее. Впрочем, можно было поступать и по-другому: вначале вызывается процедура, существовавшая ранее, а потом, в последнюю очередь, вызывается Ваша процедура (см. [1]). Поскольку перехватывать прерывание можно было многократно, в результате могла образоваться целая цепочка процедур, выполняющихся одна за другой. Мне уже приходилось сравнивать вызов процедур окна с вызовом прерываний. Это очень похоже, не правда ли? С точки зрения объектного программирования это является ни чем иным как созданием класса-родителя.

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

  • 1-й параметр. Дескриптор окна, которое было создано текущим процессом.
  • 2-й параметр. Величина, определяющая, какой атрибут следует изменить. Вообще говоря, это всего лишь смещение в некоторой области памяти. Нас будет интересовать только величина, определяющая адрес процедуры окна. Эта величина определяется константой DWL_DLGPROC = 4 для диалогового окна и GWL_WNDPROC = -4 для обычного окна. Отсюда, кстати, следует, что данное действо можно производить не только над простыми, но и над диалоговыми окнами.
  • 3-й параметр. Новое значение атрибута окна.

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

Вопрос следующий: как вызвать старую процедуру окна. Обычная команда CALL не подходит. Нужно использовать специальную функцию CallWindowProc. Параметры этой функции следующие:

  • 1-й параметр. Адрес вызываемой процедуры.
  • 2-й параметр. Дескриптор окна.
  • 3-й параметр. Код сообщения.
  • 4-й параметр. Параметр WPARAM.
  • 5-й параметр. Параметр LPARAM.

; файл edit.inc
; константы
WM_CHAR      equ 102h
WM_SETFOCUS  equ 7h
; сообщение приходит при закрытии окна
WM_DESTROY   equ 2
; сообщение приходит при создании окна
WM_CREATE    equ 1
; сообщение, если что-то происходит с элементами на окне
WM_COMMAND   equ 111h
; сообщение, позволяющее получить строку
WM_GETTEXT   equ 0Dh
; константа для функции SetWindowLong
GWL_WNDPROC  equ -4
; свойства окна
CS_VREDRAW     equ 1h
CS_HREDRAW     equ 2h
CS_GLOBALCLASS equ 4000h
WS_TABSTOP     equ 10000h
WS_SYSMENU     equ 80000h
WS_OVERLAPPEDWINDOW equ 0 + WS_TABSTOP + WS_SYSMENU
STYLE equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS

;CS_HREDRAW equ 2h
BS_DEFPUSHBUTTON equ 1h
WS_VISIBLE       equ 10000000h
WS_CHILD         equ 40000000h
WS_BORDER        equ 800000h
STYLBTN equ WS_CHILD + BS_DEFPUSHBUTTON + WS_VISIBLE + WS_TABSTOP
STYLEDT equ WS_CHILD + WS_VISIBLE + WS_BORDER + WS_TABSTOP
; идентификатор стандартной иконки
IDI_APPLICATION  equ 32512
; идентификатор курсора
IDC_ARROW        equ 32512
; режим показа окна - нормальный
SW_SHOWNORMAL   equ 1
; прототипы внешних процедур
IFDEF MASM
  EXTERN      CallWindowProcA@20:NEAR
  EXTERN      SetWindowLongA@12:NEAR
  EXTERN      SetFocus@4:NEAR
  EXTERN      SendMessageA@16:NEAR
  EXTERN      MessageBoxA@16:NEAR
  EXTERN      CreateWindowExA@48:NEAR
  EXTERN      DefWindowProcA@16:NEAR
  EXTERN      DispatchMessageA@4:NEAR
  EXTERN      ExitProcess@4:NEAR
  EXTERN      GetMessageA@16:NEAR
  EXTERN      GetModuleHandleA@4:NEAR
  EXTERN      LoadCursorA@8:NEAR
  EXTERN      LoadIconA@8:NEAR
  EXTERN      PostQuitMessage@4:NEAR
  EXTERN      RegisterClassA@4:NEAR
  EXTERN      ShowWindow@8:NEAR
  EXTERN      TranslateMessage@4:NEAR
  EXTERN      UpdateWindow@4:NEAR
ELSE
  EXTERN      CallWindowProcA:NEAR
  EXTERN      SetWindowLongA:NEAR
  EXTERN      SetFocus:NEAR
  EXTERN      SendMessageA:NEAR
  EXTERN      MessageBoxA:NEAR
  EXTERN      CreateWindowExA:NEAR
  EXTERN      DefWindowProcA:NEAR
  EXTERN      DispatchMessageA:NEAR
  EXTERN      ExitProcess:NEAR
  EXTERN      GetMessageA:NEAR
  EXTERN      GetModuleHandleA:NEAR
  EXTERN      LoadCursorA:NEAR
  EXTERN      LoadIconA:NEAR
  EXTERN      PostQuitMessage:NEAR
  EXTERN      RegisterClassA:NEAR
  EXTERN      ShowWindow:NEAR
  EXTERN      TranslateMessage:NEAR
  EXTERN      UpdateWindow:NEAR
  CallWindowProcA@20 = CallWindowProcA
  SetWindowLongA@12 = SetWindowLongA
  SetFocus@4 = SetFocus
  SendMessageA@16 = SendMessageA
  MessageBoxA@16 = MessageBoxA
  CreateWindowExA@48 = CreateWindowExA
  DefWindowProcA@16 = DefWindowProcA
  DispatchMessageA@4 = DispatchMessageA
  ExitProcess@4 = ExitProcess
  GetMessageA@16 = GetMessageA
  GetModuleHandleA@4 = GetModuleHandleA
  LoadCursorA@8 = LoadCursorA
  LoadIconA@8 = LoadIconA
  PostQuitMessage@4 = PostQuitMessage
  RegisterClassA@4 = RegisterClassA
  ShowWindow@8 = ShowWindow
  TranslateMessage@4 = TranslateMessage
  UpdateWindow@4 = UpdateWindow
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ?
     MSMESSAGE DD ?
     MSWPARAM  DD ?
     MSLPARAM  DD ?
     MSTIME    DD ?
     MSPT      DD ?
MSGSTRUCT ENDS

;----структура класса окон
WNDCLASS STRUC
     CLSSTYLE    DD ?
     CLWNDPROC   DD ?
     CLSCBCLSEX  DD ?
     CLSCBWNDEX  DD ?
     CLSHINST    DD ?
     CLSHICON    DD ?
     CLSHCURSOR  DD ?
     CLBKGROUND  DD ?
     CLMENNAME   DD ?
     CLNAME      DD ?
WNDCLASS ENDS

; файл edit.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include edit.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
  includelib c:\tasm32\lib\import32.lib
ENDIF

;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     NEWHWND  DD 0
     MSG      MSGSTRUCT <?>
     WC       WNDCLASS  <?>
     HINST    DD 0 ; дескриптор приложения
     TITLENAME  DB 'Контроль окна редактирования',0
     CLASSNAME  DB 'CLASS32',0
     CPBUT      DB 'Выход',0   ; выход
     CPEDT      DB ' ',0
     CLSBUTN    DB 'BUTTON',0
     CLSEDIT    DB 'EDIT',0
     HWNDBTN    DWORD 0
     HWNDEDT    DWORD 0
     CAP        BYTE 'Сообщение',0
     MES        BYTE 'Конец работы программы',0
     TEXT       DB 100 DUP (0)
     OLDWND     DD 0
     CHAR       DD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
     MOV  [WC.CLSSTYLE],STYLE
; процедура обработки сообщений
     MOV  [WC.CLWNDPROC], OFFSET WNDPROC
     MOV  [WC.CLSCBCLSEX],0
     MOV  [WC.CLSCBWNDEX],0
     MOV  EAX, [HINST]
     MOV  [WC.CLSHINST], EAX
;----------иконка окна
     PUSH  IDI_APPLICATION
     PUSH  0
     CALL  LoadIconA@8
     MOV  [WC.CLSHICON], EAX
;----------курсор окна
     PUSH  IDC_ARROW
     PUSH  0
     CALL  LoadCursorA@8
     MOV  [WC.CLSHCURSOR], EAX
;——————————
     MOV  [WC.CLBKGROUND], 17 ; цвет окна
     MOV  DWORD PTR [WC.CLMENNAME],0
     MOV  DWORD PTR [WC.CLNAME],OFFSET CLASSNAME
     PUSH  OFFSET WC
     CALL  RegisterClassA@4
; создать окно зарегистрированного класса
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  0
     PUSH  150 ; DY - высота окна
     PUSH  400 ; DX - ширина окна
     PUSH  100 ; Y - координата левого верхнего угла
     PUSH  100 ; X - координата левого верхнего угла
     PUSH  WS_OVERLAPPEDWINDOW
     PUSH  OFFSET TITLENAME ; имя окна
     PUSH  OFFSET CLASSNAME ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
; проверка на ошибку
     CMP  EAX,0
     JZ   _ERR
     MOV  [NEWHWND], EAX ; дескриптор окна
;-----------------------------------------
     PUSH  SW_SHOWNORMAL
     PUSH  [NEWHWND]
     CALL  ShowWindow@8  ; показать созданное окно
;-----------------------------------------
     PUSH  [NEWHWND]
     CALL  UpdateWindow@4 ; команда перерисовать видимую
                          ; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
     PUSH  0
     PUSH  0
     PUSH  0
     PUSH  OFFSET MSG
     CALL  GetMessageA@16
     CMP  AX, 0
     JE   END_LOOP
     PUSH  OFFSET MSG
     CALL  TranslateMessage@4
     PUSH  OFFSET MSG
     CALL  DispatchMessageA@4
     JMP  MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
     PUSH  [MSG.MSWPARAM]
     CALL  ExitProcess@4
_ERR:
     JMP  END_LOOP
;-----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10H]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
     CMP DWORD PTR [EBP+0CH],WM_DESTROY
     JE  WMDESTROY
     CMP DWORD PTR [EBP+0CH],WM_CREATE
     JE  WMCREATE
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JE  WMCOMMND
     JMP DEFWNDPROC
WMCOMMND:
     MOV EAX,HWNDBTN
     CMP DWORD PTR [EBP+14H],EAX
     JNE NODESTROY
     JMP WMDESTROY
NODESTROY:
     MOV EAX, 0
     JMP FINISH
WMCREATE:
; создать окно-кнопку
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  DWORD PTR [EBP+08H]
     PUSH  20 ; DY
     PUSH  60 ; DX
     PUSH  10 ; Y
     PUSH  10 ; X
     PUSH  STYLBTN
     PUSH  OFFSET CPBUT   ; имя окна
     PUSH  OFFSET CLSBUTN ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
     MOV  HWNDBTN,EAX     ; запомнить дескриптор кнопки
                          ; создать окно редактирования
     PUSH  0
     PUSH  [HINST]
     PUSH  0
     PUSH  DWORD PTR [EBP+08H]
     PUSH  20 ; DY
     PUSH  350; DX
     PUSH  50 ; Y
     PUSH  10 ; X
     PUSH  STYLEDT
     PUSH  OFFSET CPEDT  ; имя окна
     PUSH  OFFSET CLSEDIT ; имя класса
     PUSH  0
     CALL  CreateWindowExA@48
     MOV  HWNDEDT,EAX
; установить фокус на окне редактирования
     PUSH  HWNDEDT
     CALL  SetFocus@4
; установить свою собственную процедуру обработки
     PUSH  OFFSET WNDEDIT
     PUSH  GWL_WNDPROC
     PUSH  [HWNDEDT]
     CALL  SetWindowLongA@12
     MOV   OLDWND,EAX
     MOV  EAX, 0
     JMP  FINISH
DEFWNDPROC:
     PUSH  DWORD PTR [EBP+14H]
     PUSH  DWORD PTR [EBP+10H]
     PUSH  DWORD PTR [EBP+0CH]
     PUSH  DWORD PTR [EBP+08H]
     CALL  DefWindowProcA@16
     JMP  FINISH
WMDESTROY:
; получить отредактированную строку
     PUSH  OFFSET TEXT
     PUSH  150
     PUSH  WM_GETTEXT
     PUSH  HWNDEDT
     CALL  SendMessageA@16
; показать эту строку
     PUSH 0
     PUSH OFFSET CAP
     PUSH OFFSET TEXT
     PUSH DWORD PTR [EBP+08H] ; дескриптор окна
     CALL MessageBoxA@16
; на выход
     PUSH 0
     CALL PostQuitMessage@4 ; сообщение WM_QUIT
     MOV EAX, 0
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
;-----------------------------------------------
; новая процедура обработки сообщений окну редактирования
WNDEDIT PROC
     PUSH EBP
     MOV EBP,ESP
     MOV EAX,DWORD PTR [EBP+10H]
     MOV CHAR,EAX
     CMP DWORD PTR [EBP+0CH],WM_CHAR
     JNE _OLD
; проверка вводимого символа
     CMP AL,13
     JNE _OLD
; послать сообщение о закрытии основного окна
     PUSH 0
     PUSH 0
     PUSH WM_DESTROY
     PUSH [NEWHWND]
     CALL SendMessageA@16
_OLD:
; вызвать старую процедуру
     PUSH DWORD PTR [EBP+014H]
     PUSH DWORD PTR [EBP+10H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+8H]
     PUSH [OLDWND]
     CALL CallWindowProcA@20
FIN:
     POP EBP
     RET 16
WNDEDIT ENDP
_TEXT ENDS
END START

Рис. 3.5.2. Пример использования подклассов.

Трансляция программы на Рис. 3.5.2.
MASM32:

  ml /c /coff /DMASM edit.asm
  link /subsystem:windows edit.obj
TASM32:
  tasm32 /ml edit.asm
  tlink32 -aa edit.obj

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

IV

В. Возможны ли какие-либо способы взаимодействия и обмен информацией между запущенными приложениями?

Мы уже говорили о различных способах синхронизации, о разделяемой памяти. Есть еще один интересный подход, реализованный в Windows, - это анонимные каналы (pipes)46. Этот подход наиболее эффективен для обмена информацией с консольным процессом, порождаемым данным процессом. Представьте себе, что Вам необходимо, чтобы запускаемый Вами из приложения консольный процесс (например, какой-нибудь строковый компилятор) выводил информацию не в консоль, а в окно редактирования основного процесса. Пример такого приложения представлен на Рис. 3.5.3.

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

// файл pipe.rc
// определение констант
#define WS_SYSMENU     0x00080000L
#define WS_VISIBLE     0x10000000L
#define WS_TABSTOP     0x00010000L
#define DS_3DLOOK      0x0004L
#define ES_LEFT        0x0000L
#define WS_CHILD       0x40000000L
#define WS_BORDER      0x00800000L
#define ES_MULTILINE   0x0004L
#define WS_VSCROLL     0x00200000L
#define WS_HSCROLL     0x00100000L

MENUP MENU
{
  POPUP "&Запуск программы"
  {
  MENUITEM "&Запустить", 200
  MENUITEM "Выход из &программы", 300
  }
}

// определение диалогового окна
DIAL1 DIALOG 0, 0, 200, 140
STYLE WS_SYSMENU | DS_3DLOOK
CAPTION "Пример использования PIPE"
FONT 8, "Arial"
{
  CONTROL "", 101, "edit", ES_LEFT | ES_MULTILINE
  | WS_VISIBLE | WS_BORDER | WS_VSCROLL | WS_HSCROLL, 24, 20, 128, 70
}

; файл pipe.inc
; константы

SW_HIDE              equ 0
SW_SHOWNORMAL        equ 1
STARTF_USESHOWWINDOW equ 1h
STARTF_USESTDHANDLES equ 100h
STARTF_ADD = STARTF_USESHOWWINDOW + STARTF_USESTDHANDLES

; сообщение приходит при закрытии окна
WM_CLOSE       equ 10h
WM_INITDIALOG  equ 110h
WM_COMMAND     equ 111h
EM_REPLACESEL  equ 0C2h

; прототипы внешних процедур
IFDEF MASM
  EXTERN ReadFile@20:NEAR
  EXTERN CloseHandle@4:NEAR
  EXTERN CreatePipe@16:NEAR
  EXTERN SetMenu@8:NEAR
  EXTERN LoadMenuA@8:NEAR
  EXTERN CreateProcessA@40:NEAR
  EXTERN ExitProcess@4:NEAR
  EXTERN GetModuleHandleA@4:NEAR
  EXTERN DialogBoxParamA@20:NEAR
  EXTERN EndDialog@8:NEAR
  EXTERN SendDlgItemMessageA@20:NEAR
ELSE
  EXTERN ReadFile:NEAR
  EXTERN CloseHandle:NEAR
  EXTERN CreatePipe:NEAR
  EXTERN TerminateProcess:NEAR
  EXTERN WaitForSingieObject:NEAR
  EXTERN SetMenu:NEAR
  EXTERN LoadMenuA:NEAR
  EXTERN CreateProcessA:NEAR
  EXTERN ExitProcess:NEAR
  EXTERN GetModuleHandleA:NEAR
  EXTERN DialogBoxParamA:NEAR
  EXTERN EndDialog:NEAR
  EXTERN SendDlgItemMessageA:NEAR

  ReadFile@20 = ReadFile
  CloseHandle@4 = CloseHandle
  CreatePipe@16 = CreatePipe
  TerminateProcess@8 = TerminateProcess
  WaitForSingleObject@8 = WaitForSingleObject
  SetMenu@8 = SetMenu
  LoadMenuA@8 = LoadMenuA
  CreateProcessA@40 = CreateProcessA
  ExitProcess@4 = ExitProcess
  GetModuleHandleA@4 = GetModuleHandleA
  DialogBoxParamA@20 = DialogBoxParamA
  EndDialog@8 = EndDialog
  SendDlgItemMessageA@20 = SendDlgItemMessageA
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DD ?
     MSMESSAGE  DD ?
     MSWPARAM   DD ?
     MSLPARAM   DD ?
     MSTIME     DD ?
     MSPT       DD ?
MSGSTRUCT ENDS

; структура для CreateProcess
STARTUP STRUC
     cb              DD 0
     lpReserved      DD 0
     lpDesktop       DD 0
     lpTitle         DD 0
     dwX             DD 0
     dwY             DD 0
     dwXSize         DD 0
     dwYSize         DD 0
     dwXCountChars   DD 0
     dwYCountChars   DD 0
     dwFillAttribute DD 0
     dwFlags         DD 0
     wShowWindow     DW 0
     cbReserved2     DW 0
     lpReserved2     DD 0
     hStdInput       DD 0
     hStdOutput      DD 0
     hStdError       DD 0
STARTUP ENDS

; структура - информация о процессе
PROCINF STRUC
     hProcess DD ?
     hThread  DD ?
     Idproc   DD ?
     idThr    DD ?
PROCINF ENDS

; файл pipe.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include pipe.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
  includelib c:\tasm32\lib\import32.lib
ENDIF
;----------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     STRUP  STARTUP <?>
     INF    PROCINF <?>
     MSG    MSGSTRUCT <?>
     HINST  DD 0 ; дескриптор приложения
     PA     DB "DIAL1",0
     CMD    DB "c:\tasm32\bin\tlink32.exe",0
     PMENU  DB "MENUP",0
     HW     DD ?
     HR     DD ?
     BUFER  DB 3000 DUP (0)
     BYT    DD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;-------------------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
; сообщение об ошибке
KOL:
;---------------------------------
     PUSH  0
     CALL  ExitProcess@4
;---------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10Н]  ; WAPARAM
; [EBP+0CH]  ; MES
; [EBP+8]    ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;------------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
L3:
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
; загрузить меню
     PUSH OFFSET PMENU
     PUSH [HINST]
     CALL LoadMenuA@8
; установить меню
     PUSH EAX
     PUSH DWORD PTR [EBP+08H]
     CALL SetMenu@8
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JNE FINISH
     CMP WORD PTR [EBP+10H],300
     JE  L3
     CMP WORD PTR [EBP+10H],200
     JNE FINISH
; здесь запуск
; в начале PIPE
     PUSH 0
     PUSH 0
     PUSH OFFSET HW
     PUSH OFFSET HR
     CALL CreatePipe@16
     MOV EAX,HW
; здесь запуск консольного приложения
     MOV STRUP.cb,68
     MOV STRUP.lpReserved,0
     MOV STRUP.lpDesktop,0
     MOV STRUP.lpTitle,0
     MOV STRUP.dwFlags,STARTF_ADD
     MOV STRUP.cbReserved2,0
     MOV STRUP.lpReserved2,0
     MOV STRUP.wShowWindow,SW_HIDE ; окно процесса невидимо
     MOV STRUP.hStdOutput,EAX
     MOV STRUP.hStdError,EAX
;----------------------------------------------------------
     PUSH OFFSET INF
     PUSH OFFSET STRUP
     PUSH 0
     PUSH 0
     PUSH 0
     PUSH 1 ; наследует дескрипторы
     PUSH 0
     PUSH 0
     PUSH OFFSET CMD
     PUSH 0
     CALL CreateProcessA@40
; здесь чтение информации
     PUSH 0
     PUSH OFFSET BYT
     PUSH 3000
     PUSH OFFSET BUFER
     PUSH HR
     CALL ReadFile@20
     PUSH OFFSET BUFER
     PUSH 0
     PUSH EM_REPLACESEL
     PUSH 101
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA@20
; закрыть HANDLE на запись
     PUSH HW
     CALL CloseHandle@4
; закрыть HANDLE на чтение
     PUSH HR
     CALL CloseHandle@4
FINISH:
     MOV EAX,0
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

Рис. 3.5.3. Пример взаимодействия с консольным процессом через PIPE.

Трансляция программы на Рис. 3.5.3.
MASM32:

  ml /c /coff /DMASM pipe.asm
  rc pipe.rc
  link /subsystem:windows pipe.obj pipe.res
TASM32:
  tasm32 /ml pipe.asm
  brcc32 pipe.rc
  tlink32 -aa pipe.obj,,,,,pipe.res

Комментарий к программе на Рис. 3.5.3.

Вообще, запуск консольного приложения - дело довольно запутанное. Мы не будем вдаваться в детали. В нашей программе этот запуск почти не отличается от запуска программы Word.exe в главе 3.2. Отмечу новое для Вас в этой программе. Обратите внимание, что управляющий элемент EditBox выступает в несколько новой ипостаси. По сути, этот элемент играет роль консоли вывода. Для этого мы указали свойство ES_MULTILINE, что дает возможность помещать в окно целый текст, который отправляется в окно при помощи сообщения EM_REPLACESEL. Для чтения информации мы используем довольно большой буфер. В принципе, как и в случае с файлами, можно читать несколькими порциями, проверяя количество считанных байт.

46 Так называемые именованные каналы реализованы в Windows NT.

V

В. Можно ли не допустить многократный запуск одного и того же приложения?

Да. Наиболее часто употребляемым для этого средством является создание объекта Mutex. Этот объект как раз и предназначен для того, чтобы координировать разные процессы. Создается данный объект при помощи функции CreateMutex. Рассмотрим параметры этой функции.

  1. 1-й параметр. Указатель на структуру, определяющую атрибут доступа. Обычно NULL (0).
  2. 2-й параметр. Флаг. В случае ненулевого значения процесс требует немедленного владения объектом (!).
  3. 3-й параметр. Указатель на имя объекта.

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

К тому же результату можно прийти, используя семафор или файл, отображаемый в память. В данном случае все достаточно тривиально.

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

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

VI

В. Имеет ли операционная система Windows средства, упрощающие операции над группами файлов и каталогами?

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

SH STRUCT
     hwnd          DWORD ?
     wFunc         DWORD ?
     pFrom         DWORD ?
     pTo           DWORD ?
     fFlags        DWORD ?
     fAnyOperationsAborted DWORD ?
     hNameMappings         DWORD ?
     lpszProgressTitle     DWORD ?
SH ENDS

Рассмотрим значение этих полей.

  • hwnd - дескриптор окна, куда будет выводиться статус операции.
  • wFunc - код операции. Может принимать следующие значения: FO_COPY, FO_DELETE, FO_MOVE, FO_RENAME. Смысл этих значений, я думаю, Вам понятен.
  • pFrom - название файла, каталога или группы файлов или каталогов, над которыми будет производиться операция. Если несколько объектов, то имена отделяются символами с кодом 0. Можно выделять списки, которые отделяются друг от друга двумя нулевыми символами.
  • pTo - имя или группа имен - результат операции, например копирование.
  • fFlags - флаг, определяет характер операции. Может являться комбинацией следующих констант:
    • FOF_ALLOWUNDO - сохранить, если возможно, информацию для возвращения в исходное состояние.
    • FOF_CONFIRMMOUSE - данное значение не реализовано.
    • FOF_FILESONLY - выполнять только над файлами, если определен шаблон.
    • FOF_MULTIDESTFILES - указывает, что pTo содержит несколько результирующих файлов или каталогов. Например, можно копировать сразу в несколько каталогов. Если pFrom состоит из нескольких файлов, то каждый файл будет копироваться в свой каталог.
    • FOF_NOCONFIRMATION - отвечать утвердительно на все запросы.
    • FOF_NOCONFIRMMKDIR- не подтверждать создание каталога, если это требуется.
    • FOF_RENAMEONCOLLISION - давать файлам новые имена, если файлы с такими именами уже существуют.
    • FOF_SILENT - не показывать окно-статус.
    • FOF_SIMPLEPROGRESS - показывать окно-статус, но не показывать имена файлов.
    • FOF_WANTMAPPINGHANDLE - заполнять отображаемый файл (см. ниже).
  • fAnyOperationsAborted - переменная, по которой после операции можно определить, была ли прервана операция (<>0) или нет (0).
  • hNameMappings - дескриптор отображаемого в памяти файла, содержащего массив, состоящий из новых и старых имен файлов, участвующих в операции.
  • lpszProgressTitle - указывает на строку-заголовок для диалогового окна-статуса.

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

VII

В. Как отправить данные на печатающее устройство?

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

  1. Печать очень схожа с выводом на экран. При этом используются те же функции вывода, например TextOut или Ellipse. Как и в случае с экраном, для этого необходимо знать контекст принтера.
  2. Контекст принтера создается, а не получается, как в случае с экраном. Для этого используется функция CreateDC, у которой второй параметр - это указатель на строку, содержащую имя принтера. Все остальные три параметра, как правило, равны NULL.
  3. Функция PrintDlg позволяет выбрать название принтера в диалоге. При этом она возвращает тот контекст принтера.
  4. Если Вы все же решили использовать функцию CreateDC, Вам крайне необходимо узнать имена принтеров, которые есть в системе. Для этого Вам как раз подойдет функция EnumPrinters. Это весьма мощное средство может определить не только локальные, но и сетевые принтеры.
  5. Начало печати документа осуществляется функцией StartDoc. Конец печати - EndDoc. Эти две функции обрамляют блок, осуществляющий печать. В пределах этого блока можно выделять также страницы при помощи функций StartPage и EndPage.
  6. По окончании печати следует удалить контекст при помощи функции DeleteDC.

VIII

В. Может ли приложение узнать, какие программы в настоящее время запущены?

Да. В основу метода положено использование функции EnumWindows (т.е. пересчитать окна). Вот параметры этой функции.

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

Сама вызываемая процедура получает два параметра: дескриптор найденного окна и определенный выше параметр. По известному дескриптору с помощью функции GetWindowThreadProcessId можно определить уникальный идентификатор процесса или потока, который владеет данным окном. Имея же идентификатор, мы можем, в частности, удалить данный процесс из памяти с помощью функции TerminateProcess (впрочем, будьте осторожнее).

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

// файл proc.rc
// определение констант

#define WS_SYSMENU      0x00080000L
#define WS_MINIMIZEBOX  0x00020000L
#define WS_VISIBLE      0x10000000L
#define WS_TABSTOP      0x00010000L
#define WS_VSCROLL      0x00200000L
#define WS_HSCROLL      0x00100000L
#define DS_3DLOOK       0x0004L
#define LBS_NOTIFY      0x000lL
#define LBS_SORT        0x0002L

// идентификаторы
#define LIST1 101

// определение диалогового окна
DIAL1 DIALOG 0, 0, 220, 110
STYLE WS_SYSMENU | WS_MINIMIZEBOX | DS_3DLOOK
CAPTION "Поиск процессов"
FONT 8, "Arial"
{
  CONTROL "ListBox1", LIST1, "listbox", WS_VISIBLE |
  WS_TABSTOP | WS_VSCROLL | WS_HSCROLL | LBS_NOTIFY,
  16, 16, 190, 75
}

; файл proc.inc
; константы
; значения, возвращаемые функцией GetDriveType
; значения 0 и 1 можно считать признаком отсутствия устройства

DRIVE_REMOVABLE equ 2 ; накопитель на гибком диске
DRIVE_FIXED     equ 3 ; устройство жесткого диска
DRIVE_REMOTE    equ 4 ; сетевой диск
DRIVE_CDROM     equ 5 ; накопитель на лазерном диске
DRIVE_RAMDISK   equ 6 ; электронный диск

; сообщение приходит при закрытии окна
WM_CLOSE         equ 10h
WM_INITDIALOG    equ 110h
WM_COMMAND       equ 111h
LB_ADDSTRING     equ 180h
LB_RESETCONTENT  equ 184h
WM_LBUTTONDOWN   equ 201h

; прототипы внешних процедур
IFDEF MASM
  EXTERN wsprintfA:NEAR
  EXTERN GetWindowThreadProcessId@8:NEAR
  EXTERN GetWindowTextA@12:NEAR
  EXTERN EnumWindows@8:NEAR
  EXTERN lstrcat@8:NEAR
  EXTERN ExitProcess@4:NEAR
  EXTERN GetModuleHandleA@4:NEAR
  EXTERN DialogBoxParamA@20:NEAR
  EXTERN EndDialog@8:NEAR
  EXTERN SendDlgItemMessageA@20:NEAR
ELSE
  EXTERN _wsprintfA:NEAR
  EXTERN GetWindowThreadProcessId:NEAR
  EXTERN GetWindowTextA@12:NEAR
  EXTERN EnumWindows:NEAR
  EXTERN lstrcat:NEAR
  EXTERN ExitProcess:NEAR
  EXTERN GetModuleHandleA:NEAR
  EXTERN DialogBoxParamA:NEAR
  EXTERN EndDialog:NEAR
  EXTERN SendDlgItemMessageA:NEAR

  wsprintfA = _wsprintfA
  GetWindowThreadProcessId@8 = GetWindowThreadProcessId
  GetWindowTextA@12 = GetWindowTextA
  EnumWindows@8 = EnumWindows
  lstrcat@8 = lstrcat
  ExitProcess@4 = ExitProcess
  GetModuleHandleA@4 = GetModuleHandleA
  DialogBoxParamA@20 = DialogBoxParamA
  EndDialog@8 = EndDialog
  SendDlgItemMessageA@20 = SendDlgItemMessageA
ENDIF

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ?
     MSMESSAGE DD ?
     MSWPARAM  DD ?
     MSLPARAM  DD ?
     MSTIME    DD ?
     MSPT      DD ?
MSGSTRUCT ENDS

; файл proc.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include proc.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
  includelib c:\tasm32\lib\import32.lib
ENDIF
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG   MSGSTRUCT <?>
     HINST DD 0 ; дескриптор приложения
     PA    DB "DIAL1",0
     BUFER DB 200 DUP (0)
     BUF   DB 20 DUP (0)
     FORM  DB ";%lu",0
     IDP   DD ?
     HWN   DD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA@4
     MOV  [HINST], EAX
;---------------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
     CMP  EAX,-1
     JNE  KOL
; сообщение об ошибке
KOL:
;---------------------------------------
     PUSH  0
     CALL  ExitProcess@4
;---------------------------------------

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ;LPARAM
; [EBP+10Н]   ;WAPARAM
; [EBP+0CH]   ;MES
; [EBP+8]     ;HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;-------------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE FINISH
; запомним дескриптор окна
     MOV EAX,DWORD PTR [EBP+08H]
     MOV HWN,EAX
; вызвать функцию EnumWindows
     PUSH 1 ; неиспользуемый параметр
     PUSH OFFSET PENUM
     CALL EnumWindows@8
FINISH:
     MOV EAX,0
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     RET 16
WNDPROC    ENDP

; процедура обратного вызова, вызываемая при поиске окон
; [EBP+0CH]   ; параметр
; [EBP+8]     ; дескриптор окна
PENUM PROC
     PUSH EBP
     MOV EBP,ESP
; получить заголовок окна
     PUSH 200
     PUSH OFFSET BUFER
     PUSH DWORD PTR [EBP+8]
     CALL GetWindowTextA@12
; получить идентификатор процесса или потока,
; владеющего окном
     PUSH OFFSET IDP
     PUSH DWORD PTR [EBP+8]
     CALL GetWindowThreadProcessId@8
; сформировать строку для списка
     PUSH OFFSET IDP
     PUSH OFFSET FORM
     PUSH OFFSET BUF
     CALL wsprintfA
     ADD ESP,12
     PUSH OFFSET BUF
     PUSH OFFSET BUFER
     CALL lstrcat@8
; добавить в список
     PUSH OFFSET BUFER
     PUSH 0
     PUSH LB_ADDSTRING
     PUSH 101
     PUSH [HWN]
     CALL SendDlgItemMessageA@20
     POP EBP
     MOV EAX,1
     RET 8
PENUM ENDP
_TEXT ENDS
END START

Рис. 3.5.4. Программа поиска процессов.

Трансляция программы на Рис. 3.5.4.
MASM32:

  ml /c /coff /DMASM PROC.asm
  rc proc.rc
  link /subsystem:windows PROC.obj proc.res
TASM32:
  tasm32 /ml proc.asm
  brcc32 proc.rc
  tlink32 -aa proc.obj,,,,,proc.res

Рис. 3.5.5. Пример работы программы на Рис. 3.5.4.


Часть III. Глава 6. Некоторые вопросы системного программирования в Windows

Глава 6. Некоторые вопросы системного программирования в Windows

Большая часть материала этой главы будет посвящена управлению памятью Windows 9x. Данный материал требует от читателя некоторой подготовки в области так называемого защищенного режима микропроцессоров Intel. Более подробно о защищенном режиме можно узнать в книгах [1,5].

I

О страничной и сегментной адресации. Начну изложение с некоторого исторического экскурса. Семейство микропроцессоров Intel47 ведет свое начало с микропроцессора Intel 8086. В настоящее время во всю работает уже седьмое поколение. Каждое новое поколение отличалось от предыдущего в программном отношении, главным образом, расширением набора команд. Но были в этой восходящей лестнице и две ступени, сыгравшие огромную роль в развитии компьютеров на базе микропроцессоров Intel. Это микропроцессор 80286 (защищенный режим) и микропроцессор 80386 (страничная адресация).

До появления микропроцессора 80286 микропроцессоры использовались в так называемом реальном режиме адресации. Кратко изложу, в чем заключался этот режим. Для программирования использовался так называемый логический адрес, состоящий из двух 16-битных компонент: сегмента и смещения. Сегментный адрес мог храниться в одном из трех сегментных регистров CS,DS, SS,ES. Смещение хранилось в одном из индексных регистров DI, SI,BX,BP,SP 48. При обращении к памяти логический адрес подвергался преобразованию, заключающемуся в том, что к смещению прибавлялся сегментный адрес, сдвинутый на четыре бита влево. В результате получался 20-битный адрес, который, как легко заметить, мог охватывать всего около 1 Мб памяти49. Операционная система MS DOS и была изначально рассчитана для работы в таком адресном пространстве. Получаемый 20-битный адрес назывался линейным, и при этом фактически совпадал с физическим адресом ячейки памяти. Разумеется, с точки зрения развития операционных систем это был тупик. Должна быть, по крайней мере, возможность расширять память, и не просто расширять, а сделать все адресное пространство равноправным. Выход был найден с введением так называемого защищенного режима.

Гениальность подхода заключалась в том, что на первый взгляд ничего не изменилось. По-прежнему логический адрес формировался при помощи сегментных регистров и регистров, где хранилось смещение. Однако сегментные регистры хранили теперь не сегментный адрес, а так называемый селектор, часть которого (13 бит) представляла собой индекс в некоторой таблице, называемой дескрипторной. Индекс указывал на дескриптор, в котором хранилась полная информация о сегменте. Размер дескриптора был достаточен для адресации уже гораздо большего объема памяти.

Рис. 3.6.1. Схема преобразования логического адреса в линейный адрес.

На Рис. 3.6.1 схематически представлен алгоритм преобразования логического адреса в линейный адрес. Правда, за основу мы взяли уже 32-битный микропроцессор. Таблица дескрипторов или таблица базовых адресов могла быть двух типов: глобальная (GDT) и локальная (LDT). Тип таблицы определялся вторым битом содержимого сегментного регистра. На расположение глобальной таблицы и ее размер указывал регистр GDTR. Предполагалось, что содержимое этого регистра после его загрузки не должно меняться. В глобальной дескрипторной таблице должны храниться дескрипторы сегментов, занятых операционной системой. Адрес локальной таблицы дескрипторов хранился в регистре LDTR. Предполагалось, что локальных дескрипторных таблиц может быть несколько — одна для каждой запущенной задачи. Тем самым уже на уровне микропроцессора закладывалась поддержка многозадачности. Размер регистра GDTR составляет 48 бит. 32 бита - адрес глобальной таблицы, 16 бит - размер.

Кроме глобальной дескрипторной таблицы, предусматривалась еще одна общесистемная таблица - дескрипторная таблица прерываний (IDT). Она содержит дескрипторы специальных системных объектов, которые называются шлюзами и определяют точки входа процедур обработки прерываний и особых случаев. Положение дескрипторной таблицы прерываний определяется содержимым регистра IDTR, структура которого аналогична регистру GDTR.

Размер регистра LDTR составляет всего 10 байт50. Первые 2 байта адресуют локальную дескрипторную таблицу не напрямую, а посредством глобальной дескрип-торной таблицы, т.е. играют роль селектора для каждой вновь создаваемой задачи. Т.о. в глобальную дескрипторную таблицу должен быть добавлен элемент, определяющий сегмент, где будет храниться локальная дескрипторная таблица данной задачи. Переключение же между задачами может происходить всего лишь сменой содержимого регистра LDTR. Отсюда, кстати, вытекает то, что, если задача одна собирается работать в защищенном режиме, ей незачем использовать локальные дескрипторные таблицы и регистр LDTR.

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

Подытожим, что же давал нам защищенный режим.

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

Обратимся опять к Рис. 3.6.1. Из схемы видно, что результатом преобразования является линейный адрес. Но если для микропроцессора 80286 линейный адрес можно отождествить с физическим адресом, для микропроцессора 80386 это уже не так.

Начиная с микропроцессора 80386 появился еще один механизм преобразования адресов - это страничная адресация. Чтобы механизм страничной адресации заработал, старший бит системного регистра CR0 должен быть равен 1.

Обратимся к Рис. 3.6.2. Линейный адрес, получаемый путем дескрипторного преобразования, делится на три части. Старшие 10 бит адреса используются как индекс в таблице, которая называется каталог таблиц страниц. Расположение каталога страниц определяется содержимым регистра CR3. Каталог состоит из дескрипторов. Максимальное количество дескрипторов 1024. Самих же каталогов может быть бесчисленное множество, но в данный момент работает каталог, на который указывает регистр CR3.

Рис. 3.6.2. Преобразование линейного адреса в физический адрес.

Средние 10 бит линейного адреса предназначены для индексации таблицы страниц, которая содержит 1024 дескриптора страниц, которые, в свою очередь, определяют физический адрес страниц. Размер страницы составляет 4 Кб. Легко сосчитать, какое адресное пространство может быть охвачено одним каталогом таблиц страниц. Это составляет 1024*1024*1024*4 байт, т.е. порядка четырех гигабайт.

Младшие 12 бит определяют смещение внутри страницы. Как легко заметить, это как раз составляет 4 Кб (4095 байта)51. Конечно, читатель уже догадался, что для каждого процесса должен существовать свой каталог таблиц страниц. Переключение же между процессами можно осуществлять посредством изменения содержимого регистра CR3. Однако это не совсем рационально, так как требует большого объема памяти. В реальной ситуации для переключения между процессами производится изменение каталога таблиц страниц.

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

  • Биты 12-31 - адрес страницы, который в дальнейшем складывается со смещением, предварительно сдвигаясь на двенадцать бит.
  • Биты 9-11 - для использования операционной системой.
  • Биты 7-8 - зарезервированы и должны быть равны нулю.
  • Бит 6 - устанавливается, если была осуществлена запись в каталог или страницу.
  • Бит 5 - устанавливается перед чтением и записью на страницу.
  • Бит 4 - запрещение кэширования.
  • Бит 3 - бит сквозной записи.
  • Бит 2 - если значение этого бита равно 0, то страница относится к супервизору, если 1, то страница относится к рабочему процессу. Этим устанавливается два уровня доступа.
  • Бит 1 - если бит установлен, то запись на страницу разрешена.
  • Бит 0. Если бит установлен, то страница присутствует в памяти. Страницы, содержащие данные сбрасываются на диск и считываются, когда происходит обращение к ним. Страницы, содержащие код, на диск не сбрасываются, но могут подкачиваться из соответствующих модулей на диске. Поэтому память, занятая этими страницами, также может рационально использоваться.

47 Я имею в виду и микропроцессоры совместимые с Intel, выпускаемые другими фирмами.

48 В узком смысле слова индексными регистрами называются DI и SI.

49 Когда-то казалось, что один мегабайт памяти это много.

50 В старых моделях регистр содержал всего 2 байта.

51 Размер страницы в операционной системе Windows NT может отличаться от 4 Кб, что, впрочем, почти никогда не сказывается на программировании.

II

Адресное пространство процесса. В предыдущем разделе мы говорили о страничной и сегментной адресации. Как же эти две адресации уживаются в Windows? Оказывается, все очень просто. В сегментные регистры загружаются селекторы, базовые адреса которых равны нулю, а размер сегмента составляет 4 гигабайта. После этого о существовании сегментов и селекторов можно забыть, хотя для микропроцессора этот механизм по-прежнему работает. Основным же механизмом формирования адреса становятся страничные преобразования. Такая модель памяти и называется плоской (FLAT). Логическая адресация в такой модели определяется всего одним 32-битным смещением. До сих пор все наши программы писались именно в плоской модели памяти. При этом мы представляли, что вся область памяти, адресуемая 32-битным адресом, находится в нашем распоряжении. Разумеется, мы были правы, только адрес этот является логическим адресом, который, в свою очередь, подвергается страничному преобразованию, а вот в какую физическую ячейку памяти он попадает, ответить уже весьма затруднительно.

На Рис. 3.6.3 представлено логическое адресное пространство процесса. Особо обратите внимание на разделенные области памяти. Что это значит? А значит это только одно: эти области памяти проецируются на одно и то же физическое пространство.

Самая нижняя область адресного пространства отводится под образ операционной системы MS DOS, которая как видите, еще вполне зримо присутствует в операционной системе Windows. Кроме того, эта область используется для выделения динамической памяти 16-битным процессам.

Следующая область адресного пространства, между 4 Мб и 2 Гб, является адресным пространством процесса. Процесс занимает эту область пространства под код, данные, а также специфичные для него динамические библиотеки. Это не разделяемая область. Есть, однако, исключения, с которым мы уже встречались. Можно определить отдельные разделяемые секции. Это значит, что некоторые страницы из этого логического пространства будут отображаться в одну физическую область у разных процессов. Следующая область содержит в себе файлы, отображаемые в память, системные динамические библиотеки, а также динамическую память для 16-битных приложений. Последняя часть адресного пространства отведена под системные компоненты. Удивительно, но в Windows 9х эта область не защищена от доступа обычных программ.

Рис. 3.6.3. Адресное пространство процесса. Области 1,3,4 являются разделяемыми.

III

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

Начнем с функции GlobalAlloc. Другая функция, LocalAlloc, полностью эквивалентна первой и сохранена только для совместимости со старыми приложениями. Функция имеет два аргумента. Первым аргументом является флаг, о значении которого будем говорить ниже. Вторым аргументом является число необходимых байтов. Если функция выполнена успешно, то она возвращает адрес начала блока, который можно использовать в дальнейших операциях. Если же система не может выделить достаточно памяти, то функция возвращает 0.

Обычно значение флага принимают равным константе GMEM_FIXED, которая равна нулю. Это означает, что блок памяти неперемещаем. Неперемещаемость следует понимать в том смысле, что не будет меняться виртуальный адрес блока, тогда как физическая память, куда проецируется данный блок, может, разумеется, меняться системой. Комбинация данного флага с флагом GMEM_ZEROINIT приводит к автоматическому заполнению выделенного блока нулями, что часто бывает весьма удобно. Изменить размер выделенного блока можно при помощи функции GlobalReAlloc. Первым аргументом данной функции является указатель на изменяемый блок, второй аргумент - размер нового блока, третий аргумент - флаг. Заметим, что данная функция может изменить свойства блока памяти, т.е., например, сделать его перемещаемым.

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

Возможен и еще более экзотический подход с использованием флага GMEM_DISCARDABLE. Этот флаг используется совместно с GMEM_MOVEABLE. В этом случае блок может быть удален из памяти системой, если только вы его предварительно не зафиксировали. Если блок был удален системой, то функция GlobalLock возвратит 0, и Вам придется снова выделять блок и загружать, если необходимо, данные.

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

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

MEM STRUC
     dwLength        DW ?
     dwMemoryLoad    DW ?
     dwTotalPhys     DW ?
     dwAvailPhys     DW ?
     dwTotalPageFile DW ?
     dwAvailPageFile DW ?
     dwTotalVirtual  DW ?
     dwAvailVirtual  DW ?
MEM ENDS

dwLength - размер структуры в байтах.
dwMemoryLoad - процент использованной памяти.
dwTotalPhys - полный объем физической памяти в байтах.
dwAvailPhys - объем доступной физической памяти в байтах.
dwTotalPageFile - количество сохраненных байт физической памяти на диске.
dwAvailPageFile - количество доступных байт памяти, сохраненных на диске.
dwTotalVirtual - объем виртуальной памяти.
dwAvailVirtual - объем доступной виртуальной памяти.

Ниже на Рис. 3.6.4 показано простейшее применение функции GlobalAlloc.

; файл MEM.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall

; константы
; для вывода в консоль
STD_OUTPUT_HANDLE equ -11
GENERIC_READ   equ 80000000h
OPEN_EXISTING  equ 3
IFDEF MASM
; MASM
; прототипы внешних процедур
  EXTERN GlobalFree@4:NEAR
  EXTERN GlobalAlloc@8:NEAR
  EXTERN GetFileSize@8:NEAR
  EXTERN CloseHandle@4:NEAR
  EXTERN CreateFileA@28:NEAR
  EXTERN ReadFile@20:NEAR
  EXTERN GetStdHandle@4:NEAR
  EXTERN WriteConsoleA@20:NEAR
  EXTERN ExitProcess@4:NEAR
  EXTERN GetCommandLineA@0:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
LOCALS
; прототипы внешних процедур
  EXTERN  GlobalFree:NEAR
  EXTERN  GlobalAlloc:NEAR
  EXTERN  GetFileSize:NEAR
  EXTERN  CloseHandle:NEAR
  EXTERN  CreateFileA:NEAR
  EXTERN  ReadFile:NEAR
  EXTERN  GetStdHandle:NEAR
  EXTERN  WriteConsoleA:NEAR
  EXTERN  ExitProcess:NEAR
  EXTERN  GetCommandLineA:NEAR

  GlobalFree@4 = GlobalFree
  GlobalAlloc@8 = GlobalAlloc
  GetFileSize@8 = GetFileSize
  CloseHandle@4 = CloseHandle
  CreateFileA@28 = CreateFileA
  ReadFile@20 = ReadFile
  GetStdHandle@4 = GetStdHandle
  WriteConsoleA@20 = WriteConsoleA
  ExitProcess@4 = ExitProcess
  GetCommandLineA@0 = GetCommandLineA
; директивы компоновщику для подключения библиотек
  includelib c:\tasm32\lib\import32.lib
ENDIF
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     LENS  DWORD ?
     HANDL DWORD ? ; дескриптор консоли
     HF    DWORD ? ; дескриптор файла
     SIZEH DWORD ? ; старшая часть длины файла
     SIZEL DWORD ? ; младшая часть длины файла
     GH    DWORD ? ; указатель на блок памяти
     NUMB  DWORD ?
     BUF   DB 10 DUP (0)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     MOV HANDL,EAX
; получить количество параметров
     CALL NUMPAR
     CMP EAX,2
     JB _EXIT
;-------------------------------------------------
; получить параметр номером EDI
     MOV EDI,2
     LEA EBX,BUF
     CALL GETPAR
; теперь работаем с файлом
; открыть только для чтения
     PUSH 0
     PUSH 0
     PUSH OPEN_EXISTING
     PUSH 0
     PUSH 0
     PUSH GENERIC_READ
     PUSH OFFSET BUF
     CALL CreateFileA@28
     CMP EAX,-1
     JE  _EXIT
; запомнить дескриптор файла
     MOV HF,EAX
; определить размер файла
     PUSH OFFSET SIZEH
     PUSH EAX
     CALL GetFileSize@8
; запомнить размер, предполагаем, что размер не превосходит 4 Гб
     MOV SIZEL,EAX
; запросить память для считывания туда файла
     PUSH EAX
     PUSH 0
     CALL GlobalAlloc@8
     CMP EAX,0
     JE  _CLOSE
; запомнить адрес выделенного блока
     MOV GH,EAX
; читать файл в выделенную память
     PUSH 0
     PUSH OFFSET NUMB
     PUSH SIZEL
     PUSH GH
     PUSH HF
     CALL ReadFile@20
     CMP EAX,0
     JE  _FREE
; вывести прочитанное
     PUSH 0
     PUSH OFFSET LENS
     PUSH SIZEL
     PUSH GH
     PUSH HANDL
     CALL WriteConsoleA@20
_FREE:
; освободить память
     PUSH GH
     CALL GlobalFree@4
; закрыть файлы
_CLOSE:
     PUSH HF
     CALL CloseHandle@4
_EXIT:
; конец работы программы
     PUSH 0
     CALL ExitProcess@4
;---------------------------------------------
; область процедур
; процедура определения количества параметров в строке
; определить количество параметров (->EAX)
NUMPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
@@L1:
     CMP BYTE PTR [ESI],0
     JE @@L4
     CMP BYTE PTR [ESI],32
     JE @@L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP @@L2
@@L3:
     OR EDX,1
@@L2:
     INC ESI
     JMP @@L1
@@L4:
     MOV EAX,ECX
     RET
NUMPAR ENDP

; получить параметр из командной строки
; EBX - указывает на буфер, куда будет помещен параметр
; в буфер помещается строка с нулем на конце
; EDI - номер параметра
GETPAR PROC
     CALL GetCommandLineA@0
     MOV ESI,EAX ; указатель на строку
     XOR ECX,ECX ; счетчик
     MOV EDX,1   ; признак
@@L1:
     CMP BYTE PTR [ESI],0
     JE @@L4
     CMP BYTE PTR [ESI],32
     JE @@L3
     ADD ECX,EDX ; номер параметра
     MOV EDX,0
     JMP @@L2
@@L3:
     OR EDX,1
@@L2:
     CMP ECX,EDI
     JNE @@L5
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EBX],AL
     INC EBX
@@L5:
     INC ESI
     JMP @@L1
@@L4:
     MOV BYTE PTR [EBX],0
     RET
GETPAR ENDP
_TEXT ENDS
END START

Puc. 3.6.4. Пример программы с выделением динамической памяти.

Трансляция программы на Рис. 3.6.4.
MASM32:

  ML /С /coff /DMASM MEM.ASM
  LINK /SUBSYSTEM:CONSOLE MEM.OBJ
TASM32:
  TASM32 /ml MEM.ASM
  TLINK32 -ap MEM.OBJ

Операционная система Windows предоставляет также группу функций, осуществляющих управление виртуальной памятью. Основной функцией этой группы является функция VirtualAlloc. Вот параметры этой функции:

  • 1-й параметр. Адрес блока памяти для резервирования или передачи ему физической памяти.
  • 2-й параметр. Размер блока.
  • 3-й параметр. Может быть равен MEM_RESERVE - для резервирования блока, или MEM_COMMIT - для резервирования и передачи ему физической памяти.
  • 4-й параметр. Определяет уровень защиты блока. Он может быть, например, равен PAGE_READONLY или PAGE_READWRITE, или другой константе, определенной в документации Windows.
  • Возвращает функция виртуальный адрес блока памяти.

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

Другая функция, VirtualFree, может освобождать блоки, задействованные функцией VirtualAlloc. Первым параметром этой функции является адрес блока. Вторым параметром функции является размер освобождаемого блока. Третий параметр функции может принимать значение МЕМ_DЕСОММIТ либо значение MEM_RELEASE. В первом случае блок (или его часть) перестает быть отображаемым. Во втором случае весь блок перестает быть зарезервированным. При этом значении второй параметр обязательно должен быть равен нулю.

IV

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

Рассмотрим некоторые средства для работы с фильтрами. Ниже перечислены основные типы фильтров или сообщения.

  • WH_CALLWNDPROC - фильтр срабатывает, когда вызывается функция SendMessage.
  • WH_CALLWNDPROCRET - фильтр срабатывает, когда функция SendMessage возвращает управление.
  • WH_CBT - сообщение приходит, когда что-то происходит с окном.
  • WH_DEBUG - данное сообщение посылается перед тем, как послать сообщение какому-либо другому фильтру.
  • WH_GETMESSAGE - данный фильтр срабатывает, когда функция GetMessage принимает какое-либо сообщение из очереди.
  • WH_JOURNALRECORD - данное сообщение приходит на процедуру фильтра, когда система удаляет из очереди какое-либо сообщение.
  • WH_JOURNALPLAYBACK - вызывается за предыдущим вызовом (WH_JOURNALRECORD).
  • WH_KEYBOARD - сообщение приходит, когда происходят клавиатурные события.
  • WH_MOUSE - аналогично предыдущему, но относится к событиям с мышью.
  • WH_MSGFILTER - вызывается в случае событий ввода, которые произошли с диалоговым окном, меню, полосой прокрутки, но до того, как эти события были обработаны в пределах данного процесса.
  • WH_SHELL - данный фильтр срабатывает, когда что-то происходит с Windows-оболочкой.
  • WH_SYSMSGFILTER - аналогично сообщению WH_MSGFILTER, но относится ко всей системе.

Фильтр устанавливается при помощи функции SetWindowsHookEx. Рассмотрим параметры этой функции.

  • 1-й параметр. Тип фильтра, из тех, что мы перечислили выше.
  • 2-й параметр. Адрес процедуры фильтра. Если Вы создаете для всей системы, то эта процедура должна находиться в динамической библиотеке. Исключение составляют лишь два типа фильтра: WH_JOURNALRECORD и WH_JOURNALPLAYBACK.
  • 3-й параметр. Дескриптор динамической библиотеки, если фильтр предназначен для всей системы. Исключение составляют два, уже упомянутых типа фильтра.
  • 4-й параметр. Идентификатор потока, если Вы хотите следить за одним из потоков. Если значение этого параметра равно нулю, то создается фильтр для всей системы. Вообще говоря, поток может относиться и к Вашему, и к "чужому" процессу.
  • Функция SetWindowsHookEx возвращает дескриптор фильтра.

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

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

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

  • 1-й параметр. Дескриптор Вашего фильтра.
  • 2,3,4-й параметры в точности соответствуют трем параметрам, переданным Вашей процедуре фильтра.

Ниже на Рис. 3.6.5 приводится пример простого фильтра, который отлавливает все произошедшие в системе нажатия клавиши "пробел". Обратите внимание, что поскольку устанавливаемый нами фильтр является глобальным, мы помещаем процедуру фильтра в динамическую библиотеку.

// файл dial.rc для программы DLLEX.ASM
// определение констант

#define WS_SYSMENU     0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример программы с фильтром"
FONT 8, "Arial"
{
}

; основной модуль DLLEX.ASM,
; устанавливающий фильтр в динамической библиотеке
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
; сообщение приходит при закрытии окна
WM_CLOSE       equ 10h
WM_INITDIALOG  equ 110h
WH_KEYBOARD    equ 2

; структура сообщения
MSGSTRUCT STRUC
     MSHWND    DD ?
     MSMESSAGE DD ?
     MSWPARAM  DD ?
     MSLPARAM  DD ?
     MSTIME    DD ?
     MSPT      DD ?
MSGSTRUCT ENDS

; прототипы внешних процедур
IFDEF MASM
; MASM
 EXTERN      UnhookWindowsHookEx@4:NEAR
 EXTERN      SetWindowsHookExA@16:NEAR
 EXTERN      EndDialog@8:NEAR
 EXTERN      DialogBoxParamA@20:NEAR
 EXTERN      GetProcAddress@8:NEAR
 EXTERN      LoadLibraryA@4:NEAR
 EXTERN      FreeLibrary@4:NEAR
 EXTERN      ExitProcess@4:NEAR
 EXTERN      MessageBoxA@16:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
  EXTERN      UnhookWindowsHookEx:NEAR
  EXTERN      SetWindowsHookExA:NEAER
  EXTERN      EndDialog:NEAR
  EXTERN      DialogBoxParamA:NEAR
  EXTERN      GetProcAddress:NEAR
  EXTERN      LoadLibraryA:NEAR
  EXTERN      FreeLibrary:NEAR
  EXTERN      ExitProcess:NEAR
  EXTERN      MessageBoxA:NEAR

 UnhookWindowsHookEx@4 = UnhookWindowsHookEx
 SetWindowsHookExA@16 = SetWindowsHookExA
 EndDialog@8 = EndDialog
 DialogBoxParamA@20 = DialogBoxParamA
 GetProcAddress@8 = GetProcAddress
 LoadLibraryA@4 = LoadLibraryA
 FreeLibrary@4 = FreeLibrary
 ExitProcess@4 = ExitProcess
 MessageBoxA@16 = MessageBoxA
; директивы компоновщику для подключения библиотек
  includelib c:\tasm32\lib\import32.lib
ENDIF

; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG    MSGSTRUCT <?>
     HINST  DD 0 ; дескриптор приложения
     PA     DB "DIAL1",0
     LIBR   DB 'DLL2.DLL',0
     HLIB   DD ?
     APROC  DD ?
     HH     DD ?
     ATOH   DD ?
IFDEF MASM
     NAMEPROC  DB '_HOOK@0',0
     NAMEPROC1 DB '_TOH@0',0
ELSE
     NAMEPROC1 DB '_TOH',0
     NAMEPROC  DB 'HOOK',0
ENDIF
_DATA ENDS

; сегмент кода

_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; загрузить библиотеку
     PUSH OFFSET LIBR
     CALL LoadLibraryA@4
     CMP EAX,0
     JE _EXIT
     MOV HLIB,EAX
; получить адрес процедуры-фильтра
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL GetProcAddress@8
     CMP EAX,0
     JE _EXIT
     MOV APROC,EAX
; получить адрес вспомогательной процедуры
     PUSH OFFSET NAMEPROC1
     PUSH HLIB
     CALL GetProcAddress@8
     CMP EAX,0
     JE _EXIT
     MOV ATOH,EAX
; здесь установить HOOK
     PUSH 0
     PUSH HLIB
     PUSH APROC
     PUSH WH_KEYBOARD
     CALL SetWindowsHookExA@16
     MOV HH,EAX
; запомним и передадим в библиотеку
     MOV EAX,ATOH
     PUSH HH
     CALL ATOH
; открыть диалоговое окно
     PUSH 0
     PUSH OFFSET WNDPROC
     PUSH 0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA@20
; удалить HOOK
     PUSH HH
     CALL UnhookWindowsHookEx@4
; закрыть библиотеку
; библиотека автоматически закрывается также
; при выходе из программы
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL FreeLibrary@4
; выход
_EXIT:
     PUSH 0
     CALL ExitProcess@4

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog@8
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE FINISH
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX,0
     RET 16
WNDPROC    ENDP
_TEXT ENDS
END START

; динамическая библиотека DLL2.ASM
; содержащая процедуру-фильтр
.386P
; плоская модель
IFDEF MASM
  .MODEL FLAT, stdcall
ELSE
  .MODEL FLAT
ENDIF

PUBLIC HOOK, TOH

; константы
; сообщения, приходящие при открытии
; динамической библиотеки

DLL_PROCESS_DETACH    equ 0
DLL_PROCESS_ATTACH    equ 1
DLL_THREAD_ATTACH     equ 2
DLL_THREAD_DETACH     equ 3

IFDEF MASM
; MASM
; прототипы внешних процедур
  EXTERN      CallNextHookEx@16:NEAR
  EXTERN      MessageBoxA@16:NEAR
; директивы компоновщику для подключения библиотек
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
; TASM
  EXTERN      CallNextHookEx:NEAR
  EXTERN      MessageBoxA:NEAR
  CallNextHookEx@16 = CallNextHookEx
  MessageBoxA@16 = MessageBoxA
  includelib c:\tasm32\lib\import32.lib
ENDIF

;--------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HDL DD ?
     HHOOK DD ?
     CAP DB "Сообщение фильтра",0
     MES DB "Нажат пробел",0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; [EBP+10H]   ; резервный параметр
; [EBP+0CH]   ; причина вызова
; [EBP+8]     ; идентификатор DLL-модуля
DLLENTRY:
     MOV EAX,DWORD PTR [EBP+0CH]
     CMP EAX,0
     JNE D1
; закрытие библиотеки
     JMP _ЕХIТ
D1:
     CMP EAX,1
     JNE _EXIT
; открытие библиотеки
; запомнить идентификатор динамической библиотеки
     MOV EDX,DWORD PTR [EBP+08H]
     MOV HDL,EDX
_ЕХIТ:
     MOV EAX,1
     RET 12
;----------------
TOH PROC EXPORT
     PUSH  EBP
     MOV  EBP,ESP
     MOV  EAX,DWORD PTR [EBP+08H]
     MOV  HHOOK,EAX
     POP  EBP
     RET
TOH ENDP

; процедура фильтра
HOOK PROC EXPORT
     PUSH  EBP
     MOV  EBP,ESP
; отправить сообщение по цепочке
     PUSH DWORD PTR [EBP+010H]
     PUSH DWORD PTR [EBP+0CH]
     PUSH DWORD PTR [EBP+08H]
     PUSH HHOOK
     CALL CallNextHookEx@16
; проверить, не нажат ли пробел
     CMP DWORD PTR [EBP+0CH],32
     JNE _EX
; нажат - выводим сообщение
     PUSH 0         ; МВ_ОК
     PUSH OFFSET CAP
     PUSH OFFSET MES
     PUSH 0 ; в окне экрана
     CALL MessageBoxA@16
_EX:
     POP EBP
     RET
HOOK ENDP
_TEXT ENDS
END DLLENTRY

Puc. 3.6.5. Простой пример построения глобального фильтра.

Трансляция программ на Рис. 3.6.5.
MASM32:
Динамическая библиотека

   ml /c /coff /DMASM dll2.asm
   link /subsystem:windows /DLL dll2.obj
Основная программа.
   ml /c /coff /DMASM dllex.asm
   rc dial.rc
   link /subsystem:windows dllex.obj dial.res
TASM32:
Динамическая библиотека
   TASM32 /ml dll2.asm
   tlink32 /subsystem:windows -aa -Tpd dll2.obj
Основная программа.
   TASM32 /ml dllex.asm
   brcc32 dial.rc
   tlink32 -aa dllex.obj,,,,,dial.res

При разборе программ на Рис. 3.6.5 обратите внимание на роль, которую играет процедура TOH. Заметьте также, что второй и третий параметр процедуры фильтра в точности соответствует значению аналогичных параметров сообщения WM_KEYDOWN. Кстати, надеюсь, Вы понимаете, почему при нажатии клавиши пробел появляются два сообщения - по одному на нажатие и отпускание.

52 Hook можно перевести как ловушка, да и по смыслу это ближе к понятию ловушка.


Часть III. Глава 7. Использование ассемблера с языками высокого уровня

Глава 7. Использование ассемблера с языками высокого уровня

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

Вообще, стыковка ассемблера с языками высокого уровня зиждется на трех китах: согласование имен, согласование параметров, согласование вызовов. Остановимся сначала на последнем "ките", т.е. на согласовании вызовов.

Согласование вызовов. В операционной системе MS DOS вызываемая процедура могла находиться либо в том же сегменте, что и команда вызова, тогда вызов назывался близким (NEAR) или внутрисегментным, либо в другом сегменте, тогда вызов назывался дальним (FAR) или межсегментным. Разница заключалась в том, что адрес в первом случае формировался из двух байт, а во втором - из четырех байт. Соответственно, возврат из процедуры мог быть либо близким (RETN), т.е. адрес возврата формировался на основе двух байт, взятых из стека, либо дальним (RETF), и в этом случае адрес формировался на основе четырех байт, взятых опять же из стека. Ясно, что вызов и возврат должны быть согласованы друг с другом. В рамках единой программы это, как правило, не вызывало больших проблем. Но вот когда необходимо было подключить или какую-то библиотеку, или объектный модуль, здесь могли возникнуть трудности. Если в объектном модуле возврат осуществлялся по RETN, вы должны были компоновать объектные модули так, чтобы сегмент, где находится процедура, был объединен с сегментом, откуда осуществляется вызов. Вызов в этом случае, разумеется, должен быть близким (см. [1]). Если же возврат из процедуры осуществлялся по команде RETF, то и вызов этой процедуры должен быть дальним. При этом при компоновке вызов и сама процедура должны были попасть в разные сегменты. Данная проблема усугублялась еще и тем, что ошибка обнаруживалась не при компоновке, а при исполнении программы. С этим были связаны и так называемые модели памяти в языке Си, что также было головной болью многих начинающих программистов. Если, кстати, Вы посмотрите на каталог библиотек Си для DOS, то обнаружите, что для каждой модели памяти там существовала своя библиотека. Сегментация памяти приводила в Си еще к одной проблеме - проблеме указателей, но это уже совсем другая история. В Турбо Паскале пошли по другому пути. Там приняли, что в программе должен существовать один сегмент данных и несколько сегментов кода. Если же Вам не хватало одного сегмента для хранения данных, то предлагалось использовать динамическую память. При переходе к Windows мы получили значительный подарок в виде плоской модели памяти. Теперь все вызовы по типу являются близкими, т.е. осуществляющимися в рамках одного огромного сегмента. Тем самым была снята проблема согласования вызовов, и мы более к этой проблеме обращаться не будем.

Согласование имен. Согласование вызовов, как мы убедились, снято с повестки дня, а вот согласование имен год от года только усложнялось. Кое-что Вы уже знаете. Транслятор MASM, как известно, добавляет в конце имени @N, где N количество передаваемых в стек параметров. То же делает и компилятор Visual C++. Т.о. трудности возникают уже при согласовании двух ассемблерных модулей. В этом смысле TASM является более гибким компилятором, т.к. при желании в конце любого имени можно добавить @N, тем самым согласовав имена.

Другая проблема - подчеркивание перед именем. Транслятор MASM генерирует подчеркивание автоматически, если в начале программы устанавливается тип вызова stdcall (Standard Call, т.е. стандартный вызов). Транслятор TASM этого не делает, следовательно, при необходимости это нужно делать прямо в тексте программы, что, на мой взгляд, является положительным моментом. Интересно, что между фирмами Borland и Microsoft здесь полное разночтение.

Еще одна проблема - согласование заглавных и прописных букв. Как Вы помните, при трансляции с помощью TASM мы используем ключ /ml как раз для того, чтобы различать буквы прописные и заглавные. Транслятор MASM делает это автоматически. Как известно, и в стандарте языка Си с самого начала предполагалось различие между заглавными и прописными буквами. В Паскале же прописные и заглавные буквы не различаются. В этом есть своя логика: Турбо Паскаль и Delphi не создают стандартных объектных модулей, зато могут подключать их. При создании же динамических библиотек туда помещается имя так, как оно указано в заголовке процедуры.

Наконец последняя проблема, связанная с согласованием имен, - это уточняющие имена в Си++. Дело в том, что в Си++ возможна так называемая перегрузка. Это значит, что одно и тоже имя может относиться к разным функциям. В тексте программы эти функции отличаются друг от друга по количеству и типу параметров и типу возвращаемого значения. Поэтому компилятор Си++ автоматически делает в конце имени добавку - так, чтобы разные по смыслу функции различались при компоновке. Разумеется, фирмы Borland и Microsoft и тут не пожелали согласовать свои позиции и делают в конце имени совершенно разные добавки. Обойти эту проблему не так сложно, нужно использовать модификатор EXTERN "С" (см. примеры далее).

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

Таблица, представляющая соглашения о вызовах
Соглашение | Параметры | Очистка стека | Регистры
Pascal (конвенция языка Паскаль) | Слева направо | Процедура | Нет
Register (быстрый или регистровый вызов) | Слева направо | Процедура | Задействованы три регистра (EAX,EDX,ECX), далее стек
Cdecl (конвенция С) | Справа налево | Вызывающая программа | Нет
Stdcall (стандартный вызов) | Справа налево | Процедура | Нет

Таблица довольно ясно объясняет соглашения о передаче параметров, и здесь более добавить нечего.

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

I

В данном разделе рассматривается простой модуль на языке ассемблера, содержащий процедуру, копирующую одну строку в другую. Мы подсоединяем этот модуль к программам, написанным на языке Си и Паскаль, с использованием трех трансляторов: Borland C++ 5.02, Visual C++ 6.0, Delphi 5.0.

1) Borland C++ 5.0

Функцию, вызываемую из модуля, написанного на языке ассемблера, мы объявляем при помощи модификаторов extern "С" и stdcall. Поскольку модуль на языке ассемблера транслируется с помощью транслятора TASM, проблемы с подчеркиванием не возникает. Тип вызова stdcall предполагает, что стек освобождается в вызываемой процедуре. В ассемблерном модуле вызываемая процедура должна быть дополнительно объявлена при помощи директивы PUBLIC.

// файл copyc.cpp
#include <windows.h>
#include <stdio.h>
extern "C" _stdcall COPYSTR(char *, char *);
void main()
{
     char s1[100];
     char *s2="Privet!";
     printf("%s\n",(char *)COPYSTR(s1, s2));
     ExitProcess(0);
}

; файл copy.asm
.386P
; эта процедура будет вызываться из внешнего модуля
PUBLIC COPYSTR
; плоская модель
.MODEL FLAT, stdcall
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; процедура копирования одной строки в другую
; строка, куда копировать [EBP+08Н]
; строка, что копировать [EBP+0CН]
; не учитывает длину строки, куда производится копирование
COPYSTR PROC
     PUSH EBP
     MOV EBP,ESP
     MOV ESI,DWORD PTR [EBP+0CH]
     MOV EDI,DWORD PTR [EBP+08H]
L1:
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EDI],AL
     CMP AL,0
     JE L2
     INC ESI
     INC EDI
     JMP L1
L2:
     MOV EAX,DWORD PTR [EBP+08H]
     POP EBP
     RET 8
COPYSTR ENDP
_TEXT ENDS
END

Рис. 3.7.1. Пример использования процедуры из внешнего модуля. Используется транслятор BORLAND C++ 5.0.

Трансляция модулей на 3.7.1.
1-й способ.
В начале TASM32 /ml copy.asm
Затем в проект Borland C++ включить файл copy.obj.

2-й способ.
В проект включить сразу файл copy.asm, тогда транслятор автоматически вызовет TASM32.EXE.

3-й способ. Трансляция из командной строки. Предварительно готовим командный файл copy. Содержимое файла:

copyc.cpp
copy.obj
Далее вызываем bcc32 @сору.

4-й способ. В командном файле вместо copy.obj помещаем файл copy.asm.

2) Visual C++ 6.0

Текст на языке C++ не изменится, а вот ассемблерный текст придется изменить. Дело здесь в том, что транслятор Си автоматически добавит в конец вызываемой функции @8. В этом случае пришлось отказаться от нашей обычной детализации программы и положится на транслятор MASM. Для этого потребовалось явно указать параметры при объявлении процедуры. Как только это было сделано, транслятор взял часть работы на себя, кроме того, к имени COPYSTR он, естественно, добавил @8. Лишний раз подчеркну, что при таком объявлении не нужно явно устанавливать регистр EBP и освобождать стек - за нас все делает транслятор.

; файл proc.asm
.386P
.MODEL FLAT, stdcall
PUBLIC COPYSTR
; плоская модель
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; процедура копирования одной строки в другую
; строка, куда копировать [EBP+08H]
; строка, что копировать [EBP+0CH]
; не учитывает длину строки, куда производится копирование
; явное указывание параметров
COPYSTR PROC str1:DWORD, str2:DWORD
     MOV ESI,str2  ; DWORD PTR [EBP+0CH]
     MOV EDI,str1  ; DWORD PTR [EBP+08H]
L1:
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EDI],AL
     CMP AL,0
     JE L2
     INC ESI
     INC EDI
     JMP L1
L2:
     MOV EAX,DWORD PTR [EBP+08H]
     RET
COPYSTR ENDP
_TEXT ENDS
END

Puc. 3.7.2. Модуль на языке ассемблера для компоновки с помощью пакета Visual C++ 6.0.

Комментарий к программе на Рис. 3.7.2.

Явное указание параметров в заголовке процедуры приводит:

  1. К имени процедуры в объектном модуле автоматически добавляется @8 (а не @0).
  2. Ассемблер автоматически управляется со стеком.

Трансляция.

  1. В проект Visual C++ включается уже объектный модуль, откомпилированный первоначально с помощью ml.exe.
  2. Если Вы хотите включить в проект ассемблерный текст. Вам придется для него указать способ трансляции, т.е. командную строку для программы ML.EXE.

3) Delphi 5.0

Транслятор Delphi также вносит незначительные нюансы в данную проблему. Для сегмента кода нам придется взять название CODE. Из-за того, что строки Паскаль понимает несколько иначе, чем, скажем, Си, в качестве строк мне пришлось взять символьный массив. Впрочем, оператор writeln оказался довольно интеллектуальным и понял все с полуслова. Обратите внимание, что директива stdcall используется и здесь.

{ программа COPYD.PAS, использующая ассемблер
  путем подключения объектного модуля }
program Project2;
uses
     SysUtils;
{$APPTYPE CONSOLE}
{$L 'copy.OBJ'}
procedure COPYSTR(s1,s2 : PChar); stdcall; EXTERNAL;
var
     s1,s2 : array[1..30] of char;
begin
     s2[1] ='P';
     s2[2] ='r';
     s2[3] ='i';
     s2[4] ='v';
     s2[5] ='e';
     s2[6] ='t';
     s2[7] = char(0);
     COPYSTR(addr(s1[1]),addr(s2[1])) ;
     Writeln(s1);
end.

; файл proc.asm
.386P
.MODEL FLAT, stdcall
PUBLIC COPYSTR
; плоская модель
CODE SEGMENT DWORD PUBLIC USE32 'CODE'
; процедура копирования одной строки в другую
; строка, куда копировать [EBP+08Н]
; строка, что копировать [EBP+0CH]
; не учитывает длину строки, куда производится копирование
COPYSTR PROC
     PUSH EBP
     MOV EBP,ESP
     MOV ESI,DWORD PTR [EBP+0CH]
     MOV EDI,DWORD PTR [EBP+08H]
L1:
     MOV AL,BYTE PTR [ESI]
     MOV BYTE PTR [EDI],AL
     CMP AL,0
     JE L2
     INC ESI
     INC EDI
     JMP L1
L2:
     MOV EAX,DWORD PTR [EBP+08H]
     POP EBP
     RET 8
COPYSTR ENDP
CODE ENDS
END

Puc. 3.7.3. Пример подключения объектного модуля к программе на Delphi.

Трансляция.
Вам достаточно только подготовить объектный модуль (с помощью транслятора TASM32) и указать его в директиве {$L copy.obj} - далее транслятор сделает все сам.

II

В этом разделе используется другой тип вызова - быстрый, или регистровый. В соответствии с таблицей в начале главы, этот тип вызова предполагает, что три первых параметра будут передаваться в регистрах (EAX,EDX,ECX), а остальные в стеке, как и в случае соглашения stdcall. При этом если стек был задействован, освобождение его возлагается на вызываемую процедуру. Есть еще один нюанс. В случае быстрого вызова транслятор Си добавляет к имени значок @ спереди, что мы естественно учитываем в ассемблерном модуле.

#include <windows.h>
#include <stdio.h>
// файл ADDC.cpp
// объявляется внешняя функция сложения четырех целых чисел
extern "C" _fastcall ADDD(DWORD, DWORD, DWORD, DWORD);
void main()
{
  DWORD a,b,c,d;
  a=1; b=2; c=3; d=4;
  printf("%lu\n",(DWORD *)ADDD(a,b,c,d));
  ExitProcess(0);
}

; файл add.asm
.386P
.MODEL FLAT, stdcall
PUBLIC @ADDD
; плоская модель
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; процедура возвращает сумму четырех параметров
; передача параметров регистровая
; первые три параметра в регистрах EAX,EDX,ECX
; четвертый параметр в стеке, т.е. [ЕРВ+08Н]
@ADDD PROC
     PUSH EBP
     MOV EBP,ESP
     ADD EAX,ECX
     ADD EAX,EDX
     ADD EAX,DWORD PTR [EBP+08H]
     POP EBP
     RET 4
@ADDD ENDP
_TEXT ENDS
END

Puc. 3.7.4. Пример регистрового соглашения вызова процедуры.

Трансляция модулей на Рис. 3.7.4.

tasm32 /ml add.asm
Включаем add.obj в проект addc (Borland C++ 5.0)

III

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

#include <windows.h>
#include <stdio.h>
// объявляется внешняя функция
extern "C" _stdcall DIAL1();
void main()
{
  DIAL1();
  ExitProcess(0);
}

Рис. 3.7.5. Консольная программа на C++ вызывает процедуру (Рис. 3.7.6), определенную в ассемблерном модуле, которая, в свою очередь, работает в GUI-режиме.

// файл dialforc.rc
// определение констант
#define WS_SYSMENU     0x00080000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE     0x10000000L
// бордюр вокруг элемента
#define WS_BORDER      0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP     0x00010000L
// текст в окне редактирования прижат к левому краю
#define ES_LEFT        0x0000L
// стиль всех элементов на окне
#define WS_CHILD       0x40000000L
// запрещается ввод с клавиатуры
#define ES_READONLY    0x0800L
#define DS_3DLOOK      0x0004L

// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 100
STYLE WS_SYSMENU | DS_3DLOOK
CAPTION "Диалоговое окно с часами и датой"
FONT 8, "Arial"
{
  CONTROL "", 1, "edit", ES_LEFT | WS_CHILD
  | WS_VISIBLE | WS_BORDER
  | WS_TABSTOP | ES_READONLY, 100, 5, 130, 12
}

; файл dialforc.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE        equ 10h
; сообщение приходит при создании окна
WM_INITDIALOG   equ 110h
; сообщение приходит при событии с элементом на окне
WM_COMMAND      equ 111h
; сообщение от таймера
WM_TIMER        equ 113h
; сообщение посылки текста элементу
WM_SETTEXT      equ 0CH

; прототипы внешних процедур
  EXTERN      SendDlgItemMessageA:NEAR
  EXTERN      _wsprintfA:NEAR
  EXTERN      GetLocalTime:NEAR
  EXTERN      ExitProcess:NEAR
  EXTERN      GetModuleHandleA:NEAR
  EXTERN      DialogBoxParamA:NEAR
  EXTERN      EndDialog:NEAR
  EXTERN      SetTimer:NEAR
  EXTERN      KillTimer:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND      DD ?
     MSMESSAGE   DD ?
     MSWPARAM    DD ?
     MSLPARAM    DD ?
     MSTIME      DD ?
     MSPT        DD ?
MSGSTRUCT ENDS

; структура данных дата-время
DAT STRUC
     year    DW ?
     month   DW ?
     dayweek DW ?
     day     DW ?
     hour    DW ?
     min     DW ?
     sec     DW ?
     msec    DW ?
DAT ENDS

; файл dialforc.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dialforc.inc
PUBLIC DIAL1
;---------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG     MSGSTRUCT <?>
     HINST   DD 0 ; дескриптор приложения
     PA      DB "DIAL1",0
     TIM    DB "Дата %u/%u/%u   Время %u:%u:%u",0
     STRCOPY  DB 50 DUP (?)
     DATA      DAT <0>
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
DIAL1 PROC
     PUSH  EBP
     MOV  EBP,ESP
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA
     MOV  [HINST], EAX
; создать диалоговое окно
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA
     CMP  EAX,-1
;------------------------------
     POP  EBP
     RET
DIAL1 ENDP
;------------------------------

; процедура окна
; расположение параметров в стеке
; [EВР+014Н]  ;LPARAM
; [EВР+10Н]   ;WAPARAM
; [EВР+0CH]   ;MES
; [EВР+8]     ;HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
     PUSH EBX
     PUSH ESI
     PUSH EDI
;--------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
; здесь реакция на закрытие окна
; удалить таймер 1
     PUSH 1 ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL KillTimer
; удалить таймер 2
     PUSH 2 ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL KillTimer ; закрыть диалог
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
; здесь начальная инициализация
; установить таймер 1
     PUSH 0    ; параметр = NULL
     PUSH 1000 ; интервал 1 с.
     PUSH 1    ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL SetTimer
; установить таймер 2
     PUSH OFFSET TIMPROC ; параметр = NULL
     PUSH 500 ; интервал 0.5 с.
     PUSH 2 ; идентификатор таймера
     PUSH DWORD PTR [EBP+08H]
     CALL SetTimer
     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_TIMER
     JNE FINISH
; отправить строку в окно
     PUSH OFFSET STRCOPY
     PUSH 0
     PUSH WM_SETTEXT
     PUSH 1 ; идентификатор элемента
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
FINISH:
     POP EDI
     POP ESI
     POP EBX
     POP EBP
     MOV EAX,0
     RET 16
WNDPROC    ENDP
;--------------------------------------------

; процедура таймера
; расположение параметров в стеке
; [EВР+014Н]  ; LPARAM - промежуток запуска Windows
; [EВР+10Н]   ; WAPARAM - идентификатор таймера
; [EВР+0CH]   ; WM_TIMER
; [EВР+8]     ; HWND
TIMPROC     PROC
     PUSH EBP
     MOV EBP,ESP
; получить локальное время
     PUSH OFFSET DATA
     CALL GetLocalTime
; получить строку для вывода даты и времени
     MOVZX EAX,DATA.sec
     PUSH EAX
     MOVZX EAX,DATA.min
     PUSH EAX
     MOVZX EAX,DATA.hour
     PUSH EAX
     MOVZX EAX, DATA.year
     PUSH EAX
     MOVZX EAX,DATA.month
     PUSH EAX
     MOVZX EAX,DATA.day
     PUSH EAX
     PUSH OFFSET TIM
     PUSH OFFSET STRCOPY
     CALL _wsprintfA
; восстановить стек
     ADD ESP,32
     POP EBP
     RET 16
TIMPROC    ENDP
_TEXT ENDS
END

Puc. 3.7.6. Пример использования в ассемблерном модуле API-функций и ресурсов.

Трансляция.

1-й способ.

  tasm32 /ml dialforc.asm
  brcc32 dialforc.rc
Далее включаем в проект для программы на Рис. 3.7.5 файлы dialforc.obj и dialforc.res.

2-й способ.
Отличается от предыдущего тем, что в проект включается файл dialforc.rc, а не dialforc.res. Разумеется, определения констант оттуда следует убрать, т.к. они имеются в подключаемых Си head-файлах.

IV

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

// calcc.срр
#include <windows.h>
#include <stdio.h>

// вызов главной ассемолернои процедуры
extern "С" _stdcall MAIN1();
// сложить
extern "С" _stdcall void sum(char *, char *, char *);
// вычесть
extern "C" _stdcall void su(char *, char *, char *);
// умножить
extern "C" _stdcall void mu(char *, char *, char *);
// разделить
extern "C" _stdcall void dii(char *, char *, char *);

int WINAPI WinMain (HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR lpszArgs, int nWinMode)
{
     MAIN1();
     return 0;
}

extern "C" _stdcall void sum(char * s1, char * s2, char * s)
{
     float f1,f2,f;
     f1=atof(s1);
     f2=atof(s2);
     f=f1+f2;
     sprintf(s,"%f",f);
     strcat(s," +");
     return;
}

extern "C" _stdcall void su(char * s1, char * s2, char * s)
{
     float f1,f2,f;
     f1=atof(s1); f2=atof(s2);
     f=f1-f2;
     sprintf(s,"%f",f);
     strcat(s," -");
     return;
}

extern "C" _stdcall void mu(char * s1, char * s2, char * s)
{
     float f1,f2,f;
     f1=atof(s1); f2=atof(s2);
     f=f1*f2;
     sprintf(s,"%f",f);
     strcat(s," *");
     return;
}

extern "C" __stdcall void dii(char * s1, char * s2, char * s)
{
     float f1,f2,f;
     f1=atof(s1); f2=atof(s2);
     if(f2!=0)
     {
       f=f1/f2;
       sprintf(s,"%f",f);
       strcat(s," /");
     } else strcpy(s,"Ошибка деления");
     return;
}

Рис. 3.7.7. Си-модуль для программы простейшего калькулятора, компонуемый с ассемблерным модулем на Рис. 3.8.8.

// calc.rc
// определение констант
// стили окна
#define WS_SYSMENU       0x00080000L
#define WS_MINIMIZEBOX   0x00020000L
#define DS_3DLOOK        0x0004L
#define ES_LEFT          0x0000L
#define WS_CHILD         0x40000000L
#define WS_VISIBLE       0x10000000L
#define WS_BORDER        0x00800000L
#define WS_TABSTOP       0x00010000L
#define SS_LEFT          0x00000000L
#define BS_PUSHBUTTON    0x00000000L
#define BS_CENTER        0x00000300L
#define DS_LOCALEDIT     0x20L
#define ES_READONLY      0x0800L

// идентификаторы кнопок
#define IDC_BUTTON1    101
#define IDC_BUTTON2    102
#define IDC_BUTTON3    103
#define IDC_BUTTON4    104
DIAL1 DIALOG 0, 0, 170, 110
STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_3DLOOK
CAPTION "Пример простейшего калькулятора"
FONT 8, "Arial"
{
  CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE
  | WS_BORDER | WS_TABSTOP, 9, 8, 128, 12
  CONTROL "", 2, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE
  | WS_BORDER | WS_TABSTOP, 9, 27, 128, 12
  CONTROL "", 3, "edit", ES_LEFT | WS_CHILD | ES_READONLY
  | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 9, 76, 127, 12
  CONTROL "+", IDC_BUTTON1, "button", BS_PUSHBUTTON
  | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 11, 48, 15, 14
  CONTROL "-", IDC_BUTTON2, "button", BS_PUSHBUTTON
  | BS_CENTER | WS_CHILD | WS_VISIBLE |  WS_TABSTOP, 34, 48, 15, 14
  CONTROL "*", IDC_BUTTON3, "button", BS_PUSHBUTTON
  | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 56, 48, 15, 14
  CONTROL "/", IDC_BUTTON4, "button", BS_PUSHBUTTON
  | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 80, 48, 15, 14
}

; calc. inc
; константы
; сообшение приходит при закрытии окна
WM_CLOSE        equ 10h
WM_INITDIALOG   equ 110h
WM_COMMAND      equ 111h
WM_GETTEXT      equ 0Dh
WM_SETTEXT     equ 0Ch

; прототипы внешних процедур
EXTERN      ExitProcess:NEAR
EXTERN      GetModuleHandleA:NEAR
EXTERN      DialogBoxParamA:NEAR
EXTERN      EndDialog:NEAR
EXTERN      SendDlgItemMessageA:NEAR

; структуры
; структура сообщения
MSGSTRUCT STRUC
     MSHWND     DWORD ?
     MSMESSAGE  DWORD ?
     MSWPARAM   DWORD ?
     MSLPARAM   DWORD ?
     MSTIME     DWORD ?
     MSPT       DWORD ?
MSGSTRUCT ENDS

; модуль calc.asm
.386P
; поская модель
.MODEL FLAT, stdcall
include calc.inc
EXTERN sum:NEAR
EXTERN su:NEAR
EXTERN mu:NEAR
EXTERN dii:NEAR
PUBLIC MAIN1

; директивы компоновщику для подключения библиотек
includelib c:\tasm32\lib\import32.lib
;------------------------------------------------

;сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     MSG   MSGSTRUCT <?>
     HINST DD 0    ; дескриптор приложения
     PA    DB "DIAL1",0
     S1    DB 50 DUP (0)
     S2    DB 50 DUP (0)
     S     DB 50 DUP (0)
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; процедура, вызываемая из Си-модуля
MAIN1 PROC
; получить дескриптор приложения
     PUSH  0
     CALL  GetModuleHandleA
     MOV  [HINST], EAX
;-------------------------------------------
     PUSH  0
     PUSH  OFFSET WNDPROC
     PUSH  0
     PUSH  OFFSET PA
     PUSH  [HINST]
     CALL  DialogBoxParamA
;-------------------------------------------
     RET
MAIN1 ENDP

; процедура окна
; расположение параметров в стеке
; [EBP+014Н]  ; LPARAM
; [EBP+10Н]   ; WAPARAM
; [EBP+0CH]   ; MES
; [EBP+8]     ; HWND
WNDPROC     PROC
     PUSH EBP
     MOV EBP,ESP
;-------------------------------------------
     CMP DWORD PTR [EBP+0CH],WM_CLOSE
     JNE L1
     PUSH 0
     PUSH DWORD PTR [EBP+08H]
     CALL EndDialog
     MOV EAX,1
     JMP FINISH
L1:
     CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
     JNE L2
; здесь заполнить окна редактирования, если надо

     JMP FINISH
L2:
     CMP DWORD PTR [EBP+0CH],WM_COMMAND
     JNE FINISH
     CMP WORD PTR [EBP+10H],101
     JNE NO_SUM
; первое слагаемое
     PUSH OFFSET S1
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 1
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; второе слагаемое
     PUSH OFFSET S2
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 2
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; сумма
     PUSH OFFSET S
     PUSH OFFSET S2
     PUSH OFFSET S1
     CALL sum
; вывести сумму
     PUSH OFFSET S
     PUSH 50
     PUSH WM_SETTEXT
     PUSH 3
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
     JMP FINISH
NO_SUM:
     CMP WORD PTR [EBP+10H],102
     JNE NO_SUB
; уменьшаемое
     PUSH OFFSET SI
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 1
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; вычитаемое
     PUSH OFFSET S2
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 2
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; разность
     PUSH OFFSET S
     PUSH OFFSET S2
     PUSH OFFSET S1
     CALL su
; вычислить разность
     PUSH OFFSET S
     PUSH 50
     PUSH WM_SETTEXT
     PUSH 3
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
     JMP FINISH
;
NO_SUB:
     CMP WORD PTR [EBP+10H],103
     JNE NO_MULT
; первый множитель
     PUSH OFFSET S1
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 1
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; второй множитель
     PUSH OFFSET S2
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 2
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; произведение
     PUSH OFFSET S
     PUSH OFFSET S2
     PUSH OFFSET S1
     CALL mu
; вывести произведение
     PUSH OFFSET S
     PUSH 50
     PUSH WM_SETTEXT
     PUSH 3
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
     JMP FINISH
;
NO_MULT:
     CMP WORD PTR [EBP+10H],104
     JNE FINISH
; делимое
     PUSH OFFSET S1
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 1
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; делитель
     PUSH OFFSET S2
     PUSH 50
     PUSH WM_GETTEXT
     PUSH 2
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
; деление
     PUSH OFFSET S
     PUSH OFFSET S2
     PUSH OFFSET S1
     CALL dii
; вывести результат деления
     PUSH OFFSET S
     PUSH 50
     PUSH WM_SETTEXT
     PUSH 3
     PUSH DWORD PTR [EBP+08H]
     CALL SendDlgItemMessageA
     JNE FINISH

FINISH:
     MOV EAX,0
     POP EBP
     RET 16
WNDPROC ENDP
_TEXT ENDS
END

Рис. 3.7.8. Ассемблерный модуль для программы простейшего калькулятора, компонуемый с Си-модулем на Рис. 3.7.7.

Трансляция.

  tasm32 /ml calc.asm
  brcc32 calc.rc
Затем в проект к программе calcc.cpp (на Рис. 3.8.7) добавим файлы calc.obj и calc.res.

Рис. 3.7.9. Пример работы программы-калькулятора (Рис. 3.7.7-3.7.8).

V

Теперь поговорим о встроенном ассемблере. Это весьма мощное средство. Надо только иметь в виду, что встроенные ассемблеры часто несколько отстают от обычных ассемблеров в части поддержки новых команд микропроцессоров. Это вполне объяснимо, так как разработка новой версии пакета, скажем C++ Builder, требует гораздо больше времени, чем пакета TASM. В примерах на Рис. 3.7.10 и Рис. 3.7.11 мы используем команды арифметического сопроцессора.

program Project2;
{$APPTYPE CONSOLE}
uses
    SysUtils;
var
    d:double;
function soproc(f:double): double;
var res:double;
begin
  asm
     FLD f
     FSIN
     FSTP res
  end;
  soproc:=res;
end;

begin
    d:=-pi;
    while (d<=pi) do
    begin
      writeln(d:10:2,'-',soproc(d):10:2);
      d:=d+0.1;
    end;
end.

Рис. 3.7.10. Пример использования директивы ASM и команд сопроцессора в программе на языке Паскаль (Delphi 5.0).

#include <windows.h>
#include <stdio.h>
double soproc(double f);

void main()
{
     double w=-3.14;
     while(w<=3.14)
     {
          printf("%f- %f\n", w, soproc(w));
          w=w+0.1;
     }
     ExitProcess(0);
}

double soproc(double f)
{
     double d;
     asm {
         FLD f
         FSIN
         FSTP d
     }
     return d;
}

Рис. 3.7.11. Пример использования директивы ASM и команд сопроцессора в программе на языке Си (Borland C++ 5.0).

VI

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

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

library lnk;

uses
    SysUtils,
    Classes,
    Windows,
    ShlObj, ActiveX, ComObj, Registry, syncobjs;

procedure setup(prog:PChar; uns: PChar; jar:PChar; menu:PChar); stdcall;
var
  MyObject : IUnknown;
  MySLink  : IShellLink;
  MyPFile  : IPersistFile;
  FileName : String;
  Directory : String;
  WFileName : WideString;
  MyReg : TRegIniFile;
  ps1,ps2,sn,path,s :string;
  nb:dword;
  handle : integer;
  l:DWORD;
  f: text;
  begin
     MyObject := CreateComObject(CLSID_ShellLink);
     MySLink := MyObject as IShellLink;
     MyPFile := MyObject as IPersistFile;
     FileName := prog;
     path := ExtractFilePath(FileName);
  with MySLink do
  begin
    SetArguments('');
    SetPath(PChar(FileName));
    SetWorkingDirectory(PChar(ExtractFilePath(FileName)));
  end;

  // вначале ярлык на экране
  MyReg : = TRegIniFile.Create('Software\MicroSoft\Windows\CurrentVersion\Explorer');
  Directory := MyReg.ReadString('Shell Folders','Desktop','');
  WFileName := Directory+'\';
  WFileName := WFileName+jar;
  WFileName := WFileName+'.lnk';
  MyPFile.Save(PWChar(WFileName),False);
  ps1:=string(WFileName);

  // создание ярлыка в главном меню
  Directory :=MyReg.ReadString('Shell Folders','Programs','')+'\';
  Directory:=Directory+menu;
  WFileName := Directory+'\';
  WFileName := WFileName+jar;
  WFileName := WFileName+'.lnk';
  CreateDir(Directory);
  ps2:=Directory+'\';
  MyPFile.Save(PWChar(WFileName),False);
  //************************************
  MyObject:= CreateComObject(CLSID_ShellLink);
  MySLink:= MyObject as IShellLink;
  MyPFile := MyObject as IPersistFile;
  FileName := uns;
  path := ExtractFilePath(FileName);
  with MySLink do
  begin
    SetArguments('');
    SetPath(PChar(FileName));
    SetWorkingDirectory(PChar(ExtractFilePath(FileName)));
  end;
  WFileName := Directory+'\';
  WFileName := WFileName+'UNFILES.lnk';
  MyPFile.Save(PWChar(WFileName),False);
  MyReg.Free;
  // создать файл, куда будет помещена
  // нужная для дальнейшей инсталляции информация
  sn:=path+'perebros.lnk';
  AssignFile(f,sn);
  rewrite(f);
  writeln(f,ps1);
  writeln(f,ps2);
  close(f);
end;
//*********************
Procedure DLLMain(r:DWORD);
begin
end;
exports setup;
begin
    DLLProc:=@DLLMain;
    DLLMain(dll_Process_Attach);
end.

Рис. 3.7.12. Пример динамической библиотеки, написанной на Delphi.

; файл setup.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
; прототипы внешних процедур
IFDEF MASM
; MASM
  EXTERN      GetProcAddress@8:NEAR
  EXTERN      LoadLibraryA@4:NEAR
  EXTERN      FreeLibrary@4:NEAR
  EXTERN      ExitProcess@4:NEAR
  EXTERN      MessageBoxA@16:NEAR
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\kernel32.lib
ELSE
  includelib c:\tasm32\lib\import32.lib
  EXTERN      GetProcAddress:NEAR
  EXTERN      LoadLibraryA:NEAR
  EXTERN      FreeLibrary:NEAR
  EXTERN      ExitProcess:NEAR
  EXTERN      MessageBoxA:NEAR
  GetProcAddress@8 = GetProcAddress
  LoadLibraryA@4 = LoadLibraryA
  FreeLibrary@4 = FreeLibrary
  ExitProcess@4 = ExitProcess
  MessageBoxA@16 = MessageBoxA
ENDIF
;--------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     TXT    DB 'Ошибка динамической библиотеки',0
     MS     DB 'Сообщение',0
     LIBR   DB 'LNK.DLL',0
     HLIB   DD ?
     PAR1   DB "C:\PROG\FILES.EXE",0
     PAR2   DB "C:\PROG\UNINST.EXE",0
     PAR3   DB "Универсальный поиск",0
     PAR4   DB "Программа универсального поиска",0
     NAMEPROC DB 'setup',0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
; [EBP+10Н]   ; резервный параметр
; [EBP+0CH]   ; причина вызова
; [EBP+8]     ; идентификатор DLL-модуля
START:
;загрузить библиотеку
     PUSH OFFSET LIBR
     CALL LoadLibraryA@4
     CMP EAX,0
     JE _ERR
     MOV HLIB,EAX
;получить адрес
     PUSH OFFSET NAMEPROC
     PUSH HLIB
     CALL GetProcAddress@8
     CMP EAX,0
     JNE YES_NAME
; сообщение об ошибке
_ERR:
     PUSH  0
     PUSH OFFSET MS
     PUSH OFFSET TXT
     PUSH 0
     CALL MessageBoxA@16
     JMP _EXIT
YES_NAME:
     PUSH OFFSET PAR4
     PUSH OFFSET PAR3
     PUSH OFFSET PAR2
     PUSH OFFSET PAR1
     CALL EAX
; закрыть библиотеку
; библиотека автоматически закрывается также
; при выходе из программы
     PUSH HLIB
     CALL FreeLibrary@4
; выход
_EXIT:
     PUSH 0
     CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 3.7.13. Пример программы на языке ассемблера, осуществляющей вызов динамической библиотеки на Рис. 3.8.12.

Трансляция.
MASM32:

  ml /c /coff /DMASM setup.asm
  link /subsystem:windows setup.obj
TASM32:
  tasm32 /ml setup.asm
  tlink32 -aa setup.obj

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


Часть IV. Глава 1. Структура исполняемых модулей

Часть IV. Отладка, анализ кода программ, драйверы

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

Глава 1. Структура исполняемых модулей

Исполняемым форматом в Windows является формат PE. Сокращение PE означает Portable Executable, т.е. переносимый исполняемый формат. Этот формат имеют как ЕХЕ-файлы, так и динамические библиотеки. Важно, что сейчас фирма Microsoft ввела "новый" формат и для объектных модулей - это COFF-формат (COFF - Common Object File Format), который, однако, на поверку оказался, в сущности, все тем же PE-форматом. Заметим в этой связи, что фирма Borland по-прежнему работает с объектными файлами, имеющими структуру OMF (Object Module Format). Старый NE-формат (NE - New Executable), используемый старой операционной системой Windows и рассчитанный на сегментную структуру памяти, ушел в небытие. Кроме того, есть еще формат VXD-драйверов - LE-формат (Linear Executable, т.е. линейный исполняемый). Этого формата мы коснемся, когда будем говорить о драйверах в операционной системе Windows. Таким образом, данная глава будет посвящена разбору структуры исполняемых РЕ-модулей. Поскольку в состав исполняемого РЕ-модуля входит и DOS-программа (STUB), мы начнем наше рассмотрение со структуры DOS-программ. Наше рассмотрение будет кратким, и мы воспользуемся таблицей из книги [1].

Структура ЕХЕ-программ для MS DOS

Смещение | Длина | Название | Комментарий
+0 | 2 | MZ | подпись, признак ЕХЕ-программы
+2 | 2 | PartPag | длина неполной последней страницы
+4 | 2 | PageCnt | длина в страницах (512 б), включая заголовок и последнюю страницу
+6 | 2 | ReloCnt | число элементов в таблице перемещения
+8 | 2 | HdrSize | длина заголовка в параграфах
+0AН | 2 | MinMem | минимум требуемой памяти за концом программы
+0CH | 2 | MaxMem | максимум требуемой памяти за концом программы
+0EН | 2 | ReloSS | сегментный адрес стека
+10H | 2 | EXESp | значение регистра SP
+12Н | 2 | ChkSum | контрольная сумма
+14Н | 2 | ExeIP | значение регистра IP
+16H | 2 | ReloCS | сегментный адрес кодового сегмента
+18H | 2 | TablOff | смещение в файле первого элемента таблицы перемещения
+1АН | 2 | Overlay | номер оверлея, 0 для главного модуля
* Конец форматированной порции заголовка ** +1СН
** Начало таблицы перемещения (возможно с 1СН) ** +? 4*? смещ. сегмент ... смещ. сегмент

Рис. 4.1.1. Структура ЕХЕ-программы для MS DOS.

Более подробно разбор заголовка DOS-программы можно найти в [1]. Добавлю только, что сразу за таблицей перемещения начинается исполняемая часть модуля. Таблица же перемещения используется для того, чтобы при загрузке настроить адреса. Но это лишь в том случае, если в программе используются адреса сегментов. В противном случае таблица перемещения не содержит элементов, и исполняемый код начинается сразу за форматированной частью заголовка. Перейдем теперь к общей структуре РЕ-модуля.

I

Общая структура РЕ-модуля. Начало заголовка ЕХЕ-файлов в WIN32 представляет собой небольшую DOS-программу, основное предназначение которой заключается в том, чтобы при запуске в операционной системе MS DOS сделать сообщение о том, что данный модуль не предназначен для работы в MS DOS. Программа LINK.EXE (TLINK32.EXE) устанавливает свой вариант DOS-программы. Однако при желании Вы всегда можете поставить свою программу-заглушку (stub в переводе заглушка).

Рассмотрим общую структуру РЕ-заголовка.
Смещение | Комментарий
00H | Обычный DOS-заголовок.
1CH | Четыре байта до выравнивания до 20Н байт, т.е. выравнивание до двух параграфов.
20H | Информация о программе, обычно отсутствующая.
3CH | Смещение 32-битного РЕ-заголовка.
40H | Таблица перемещения для программы-заглушки. У стандартных заглушек таблица, разумеется, пуста. Не смотря на это TablOff должен показывать именно сюда.
40H+?? | Здесь начинается тело самой заглушки, которая начинается за таблицей перемещения. Естественно, в стандартных заглушках нет ничего, кроме сообщения о невозможности запуска программы. Однако заглушка может иметь и весьма разрушительные свойства.
?? | Здесь начинается собственно PE-заголовок. Сюда показывает содержимое четырех байт по адресу 3CH. Начало должно быть выровнено по 8-байтной границе.
?? | Таблица описаний секций файлов (Object Table).
?? | Остальная информация: coff-символы, отладочная информация, таблица импорта и таблица экспорта, ресурсы и т.д. Данный раздел называется Image Pages, т.е. страницы образов.

Рис. 4.1.2. Общая структура РЕ-заголовка.

00000: 4D 5A 0A 00 02 00 00 00 | 04 00 0F 00 FF FF 00 00 MZ
00010: C0 00 00 00 00 00 00 00 | 40 00 00 00 00 00 00 00
00020: 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
00030: 00 00 00 00 00 00 00 00 | 00 00 00 00 80 00 00 00
00040: B4 09 BA 10 00 OE IF CD | 21 B8 01 4C CD 21 90 90
00050: 54 68 69 73 20 69 73 20 | 61 20 57 69 6E 33 32 20
00060: 70 72 6F 67 72 61 6D 2E | 0D OA 24 00 00 00 00 00
00070: 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
00080: 50 45 00 00 4C 01 03 00 | 39 30 00 00 00 00 00 00 PE
00090: 00 00 00 00 E0 00 OE 03 | 0B 01 02 34 00 30 00 00
000A0: 00 10 00 00 00 60 00 00 | 30 96 00 00 00 70 00 00
000B0: 00 A0 00 00 00 00 40 00 | 00 10 00 00 00 02 00 00

Рис. 4.1.3. Фрагмент РЕ-заголовка.

На рис. 4.1.3 показан фрагмент PE-заголовка. Обратите внимание, что по смещению 3CH действительно находится адрес начала основного заголовка (символы PE).

II

В таблице, представленной ниже мы даем описание заголовка РЕ.
Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Signature Bytes | Сигнатура. Первые два байта "PE" 4550Н. Еще два байта обязательно должны быть равны нулю.
04h | WORD | CPU Type | Данное поле указывает на процессор, который следует предпочесть при запуске программы. Вот возможное значение этого поля: 0000h - неизвестный процессор. 014Ch - i386 014Dh - i486 014Eh - i586 0162h - MIPS Mark I (R2000, R3000) 0163h - MIPS Mark II (R6000) 0166h - MIPS Mark III (R4000) Чаще всего данное поле указывает на процессор 386.
06h | WORD | Num of Objects | Поле указывает на число реальных входов в Object Table (см. таб. ниже).
08h | DWORD | Time/Date Stamp | Дата и время, которые устанавливаются при компоновке программы.
0Ch | DWORD | Pointer to COFF table | Дополнительный указатель, определяющий местонахождение отладочной COFF-таблицы в файлах. Это поле используется только в OBJ-файлах и РЕ-файлах, содержащих отладочную COFF-информацию.
10h | DWORD | COFF table size | Количество символов в COFF-таблице.
14h | WORD | NT Header Size | Размер заголовка РЕ-файла, начиная с поля Magic - таким образом, общий размер заголовка РЕ-файла составляет NT Header Size + 18h.
16h | WORD | Flags | Указывает на предназначение программы. Значение флагов: 0000h - это программа; 0001h - файл не содержит перемещений и таблицы перемещаемых элементов; 0002h - образ в файле можно запускать. Если этот бит не установлен, то это обычно указывает на ошибку, обнаруженную на этапе линковки, или же на то, что код был инкрементально отлинкован (инкрементальная линковка - частичная линковка кода при изменении участка программы, а не тотальная перекомпиляция проекта); 0200h - загружать в память фиксированно. Указывает на то, что программу можно грузить только по адресу, записанному в Image Base, если это невозможно, то такой файл лучше вообще не запускать. 2000h - это библиотека.
18h | WORD | Magic | Слово-сигнатура, определяющее состояние отображенного файла. Определены следующие значения: 107Н - отображение ПЗУ. 10BH - нормально исполняемое отображение.
1Ah | BYTE | Link Major | Старший номер версии использовавшегося при создании модуля компоновщика. Десятичный вид.
1Bh | BYTE | Link Minor | Младший номер версии использовавшегося при создании модуля компоновщика. Десятичный вид.
1Ch | DWORD | Size of Code | Размер именно программного кода в файле. KERNEL использует это значение для фактического отведения памяти под загружаемую программу. Установка этого значения слишком маленьким приведет к выдаче сообщения о нехватке памяти. Обычно большинство модулей имеют только одну программную секцию .text.
20h | DWORD | Size of Init Data | Размер секции инициализированных данных, очевидно, не используется в Windows 95, но используется в Windows NT. Назначение аналогично приведенному выше.
24h | DWORD | Size of UnInit Data | Размер секции неинициализированных данных. Неинициализированные данные обычно содержатся в секции .bss. Данная секция не занимает на диске никакого места, но при загрузке модуля в память загрузчик отводит под нее память.
28h | DWORD | Entry point RVA | Адрес относительно Image Base, no которому передается управление при запуске программы или адрес инициализации/завершения библиотеки.
2Ch | DWORD | Base of Code | Адрес секции относительно базового адреса (40000Н), содержащей программный код. Этот адрес обычно равен 1000Н для компоновщика Microsoft и 10000H для компоновщика Borland.
30h | DWORD | Base of Data | Адрес относительно базового (40000H), с которого начинаются секции данных файла. Секции данных обычно идут последними в памяти, после заголовка РЕ и программных секций.
34h | DWORD | Image Base | При создании компоновщик помещает сюда адрес, куда будет отображен исполняемый файл в памяти. Если загрузчик отобразит файл именно по этому адресу, то дополнительной настройки не потребуется.
38h | DWORD | Object align | Выравнивание программных секций. После отображения в память каждая секция будет обязательно начинаться с виртуального адреса, кратного данной величине.
3Ch | DWORD | File align | В случае РЕ-файла исходные данные, которые входят в состав каждой секции, будут обязательно начинаться с адреса, кратного данной величине. Значение по умолчанию составляет 200Н.
40h | WORD | OS Major | Старший номер версии операционной системы, необходимый для запуска программы.
42h | WORD | OS Minor | Младший номер версии операционной системы.
44h | WORD | USER Major | Пользовательский номер версии, задается пользователем при линковке программы. Старшая часть.
46h | WORD | USER Minor | Пользовательский номер версии, младшая часть.
48h | WORD | SubSys Major | Старший номер версии подсистемы.
4Ah | WORD | SubSys Minor | Младший номер версии подсистемы. Типичное значение версии 4.0, что означает Windows 95.
4Ch | DWORD | Reserved | Зарезервировано.
50h | DWORD | Image Size | Представляет общий размер всех частей отображения, находящихся под контролем загрузчика. Эта величина равна размеру области памяти, начиная с базового адреса отображения и заканчивая адресом конца последней секции. Адрес конца секции выровнен на ближайшую верхнюю границу секции.
54h | DWORD | Header Size | Общий размер всех заголовков: DOS Stub + РЕ Header + Object Table
58h | DWORD | File CheckSum | Контрольная сумма всего файла. Как и в операционной системе MS DOS, ее никто не контролирует, а программа редактирования связей устанавливает ее в 0. Предполагалось ее рассчитывать как инверсию суммы всех байтов файла.
5Ch | WORD | Subsystem | Операционная подсистема, необходимая для запуска данного файла. Вот значения этого поля: 1 - подсистема не требуется (NATIVE). 2 - запускается в подсистеме Windows GUI. 3 - запускается в подсистеме Windows character (терминальное или консольное приложение). 5 - запускается в подсистеме OS/2. 7 - запускается в подсистеме Posix.
5Eh | WORD | DLL Flags | Указывает на специальные потребности при загрузке, начиная с операционной системы NT 3.5. Устарел и не используется.
60h | DWORD | Stack Reserve Size | Память, требуемая для стека приложения. Память резервируется, но выделяется только Stack Commit Size байтов. Следующая страница является охранной. Когда приложение достигает этой страницы, то она становится доступной, а следующая страница - охранной, и так до достижения нижней границы, после чего Windows 95 убивает программу.
64h | DWORD | Stack Commit Size | Объем памяти, отводимой для стека немедленно после загрузки.
68h | DWORD | Heap Reserve Size | Максимально возможный размер локальной кучи.
6Ch | DWORD | Heap Comit Size | Отводимый размер кучи при загрузке.
70h | DWORD | Loader Flags | Начиная с Windows NT 3.5 объявлено неиспользуемым, назначение неясно, но в целом связано с поддержкой отладки.
74h | DWORD | Num of RVA and Sizes | Указывает размер массива VA/Size, который следует ниже, данное поле зарезервирована под будущие расширения формата. В данный момент его значение всегда равно 10h.
78h | DWORD | Export Table RVA | Относительный адрес (относительно базового адреса) таблицы экспорта.
7Ch | DWORD | Export Data Size | Размер таблицы экспорта.
80h | DWORD | Import Table RVA | Относительный адрес (относительно базового адреса) таблицы импорта.
84h | DWORD | Import Data Size | Размер таблицы импорта.
88h | DWORD | Resource Table RVA | Относительный адрес (относительно базового адреса) таблицы ресурсов.
8Ch | DWORD | Resource Data Size | Размер таблицы ресурсов.
90h | DWORD | Exception Table RVA | Относительный адрес таблицы исключений.
94h | DWORD | Exception Data Size | Размер таблицы исключений.
98h | DWORD | Security Table RVA | Адрес таблицы безопасности. По-видимому, не используется.
9Ch | DWORD | Security Data Size | Размер таблицы безопасности.
A0h | DWORD | Fix Up's Table RVA | Относительный адрес таблицы настроек.
A4h | DWORD | Fix Up's Data Size | Размер таблицы настроек.
A8h | DWORD | Debug Table RVA | Относительный адрес таблицы отладочной информации.
ACh | DWORD | Debug Data Size | Размер таблицы отладочной информации.
B0h | DWORD | Image Description RVA | Относительный адрес строки описания модуля.
B4h | DWORD | Description Data Size | Размер строки описания модуля.
B8h | DWORD | Machine Specific RVA | Адрес таблицы значений, специфичных для микропроцессора.
BCh | DWORD | Machine Data Size | Размер таблицы значений, специфичных для микропроцессора.
C0h | DWORD | TLS RVA | Указатель на локальную область данных цепочек.
C4h | DWORD | TLS Data Size | Размер области данных цепочек.
C8h | DWORD | Load Config RVA | Предназначение неизвестно.
CCh | DWORD | Load Config Data Size | Предназначение неизвестно.
D0h | 08h | Reserved | Зарезервировано.
D8h | DWORD | IAT RVA | Используется в NT. В Windows 95, судя по всему, нет.
DCh | DWORD | IAT Data Size | Размер описанного поля.
E0h | 08h | Reserved | Зарезервировано.
E8h | 08h | Reserved | Зарезервировано.
F0h | 08h | Reserved | Зарезервировано.

Между заголовком РЕ и данными для секций расположена таблица секций. Вот элемент этой таблицы.

Элемент таблицы секций содержит полную базу данных об одной секции.

Смещение | Длина поля | Название поля | Описание поля
00h 08h Object Name Имя объекта, остаток заполнен нулями. Если имя объекта имеет длину 8 символов, то заключительного 0 нет. Вот несколько возможных имен:
.text - исполняемый код общего назначения.
CODE - исполняемый код, помещаемый компоновщиками фирмы BORLAND.
.icode - переходники (jump'ы), помещаемые сюда старой версией TLINK32.
.data - инициализированные данные, помещаются компоновщиком фирмы Microsoft.
DATA - инициализированные данные, помещаемые сюда компоновщиком TLINK32.
.bss - неинициализированные глобальные и статические переменные.
.CRT - еще одна секция для хранения инициализированных данных.
.rsrc - секция для хранения ресурсов.
.idata - секция импорта.
.edata - секция экспорта.
.reloc - секция настроек. Данная информация может понадобиться загрузчику, если он не сможет загрузить модуль по базовому адресу.
.tls - данные для запуска цепочек.
.rdata - данная секция в основном содержит отладочную информацию.
.debug$s и .debug$t - данные секции есть только в COFF-объектных файлах. Они содержат информацию о символах Code View и их типах.
.drective - в данной секции содержится текст программ для компоновки. Данная секция есть только в объектных файлах. Секции, содержащие символ $. Такие секции обрабатываются особым образом. Компоновщик объединяет все секции, имеющие одинаковые символы в имени до символа $. Именем получившейся секции считается то, что стоит перед указанным символом.
08h | DWORD | Virtual Size | Виртуальный размер секции - именно столько памяти будет отведено под секцию. Если Virtual Size превышает Physical Size, то разница заполняется нулями, так определяются секции неинициализированных данных (Physical Size = 0).
0Ch | DWORD | Section RVA | Размещение секции в памяти, ее виртуальный адрес относительно Image Base. Позиция каждой секции выровнена на границу Object align (степень 2 от 512 до 256М включительно, по умолчанию 64К), секции упакованы вплотную друг к другу, впрочем, можно это не соблюдать. Для объектных файлов поле не имеет смысла.
10h | DWORD | Physical Size | Размер секции (ее инициализированной части) в файле кратно полю File align в заголовке РЕ Header, должно быть меньше или равно Virtual Size. Для объектных файлов это поле содержит точный размер секции, сгенерированный компилятором или ассемблером. Другими словами, для объектных файлов оно эквивалентно полю Virtual Size.
14h | DWORD | Physical Offset | Физическое смещение относительно начала ЕХЕ-файла, выровнено на границу File align поля заголовка РЕ Header. Смещение используется загрузчиком для поиска.
18h | DWORD | Pointer to Linenumber | Файловое смещение таблицы номеров строк. Используется для объектных файлов.
1Ch | WORD | Number of Relocations | Количество перемещений в таблице поправок. Используется только для объектных файлов.
1Eh | WORD | Number of Linenumbers | Количество номеров строк в таблице номеров строк для данной секции. Используется для объектных файлов.
20h | 08h | Reserved | Зарезервировано для объектных файлов.
28h DWORD Object Flags Битовые флаги секции:
00000004h - используется для кода с 16-битными смещениями.
00000020h - секция кода.
00000040h - секция инициализированных данных.
00000080h - секция неинициализированных данных.
00000200h - комментарии или любой другой тип информации.
00000400h - оверлейная секция.
00000800h - не будет являться частью образа программы.
00001000h - общие данные.
00500000h - выравнивание по умолчанию, если не указано иное.
02000000h - может быть выгружен из памяти.
04000000h - не кэшируется.
08000000h - не подвергается страничному преобразованию.
10000000h - разделяемый.
20000000h - выполнимый.
40000000h - можно читать.
80000000h - можно писать.

Страницы образов секций. Здесь мы изучим некоторые секции.

Секция экспорта (.edata). Общая структура

1 | Таблица собственно экспорта | Export Directory Table
2 | Адресная таблица | Export Address Table
3 | Таблица указателей на имена | Export Name Table Pointers
4 | Таблица номеров | Export Ordinal Table
5 | Таблица самих имен | Export Name Table

1. Таблица экспорта

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Flags | Зарезервировано, должно быть равно нулю.
04h | DWORD | Time/Date Stamp | Время и дата создания экспортных данных.
08h | WORD | Major Version | Старший номер версии таблицы экспорта. Не используется.
0Ah | DWORD | Minor Version | Младший номер версии таблицы экспорта, также не используется.
0Ch | DWORD | Name RVA | Относительный адрес строки, указывающей на имя нашей библиотеки.
10h | DWORD | Ordinal Base | Начальный номер экспорта, для функций, экспортируемых данным модулем.
14h | DWORD | Num of Functions | Количество функций экспортируемых данным модулем, является числом элементов массива Address Table (см.ниже).
18h | DWORD | Num of Name Pointers | Число указателей на имена, обычно равно числу функций, но это не так, если у нас есть функции, экспортируемые только по номеру.
1Ch | DWORD | Address Table RVA | Указатель на таблицу относительных адресов экспортируемых функций.
20h | DWORD | Name Pointers RVA | Указатель на таблицу указателей на имена экспортируемых функций данного модуля.
24h | DWORD | Ordinal Table RVA | Указатель на таблицу номеров экспорта, данный массив по индексам параллелен Name Pointers, элементами являются слова.

2. Таблица адресов экспорта. Эта структура данных содержит адреса экспортируемых функций (их точки входа) в формате DWORD (по 4 байта на элемент). Для доступа к данным используется номер функции с коррекцией на базу номеров (Ordinal Base).

3. Таблица указателей на имена. Данная структура содержит указатели на имена экспортируемых функций, указатели отсортированы в лексическом порядке для обеспечения возможности бинарного поиска. Каждый указатель занимает 4 байта. Имена функций обычно лежат в секции экспорта.

4. Таблица номеров. Данная структура совместно с Name Table Pointers формирует два параллельных массива, разделенных для облегчения к ним доступа индексированием на родные для процессора данные (слова, двойные слова, но не сложные структуры). Данный массив содержит номера экспорта, которые в общем случае являются индексами в Address Table экспорта (за вычетом базы Ordinal Base). Элементами данного массива являются слова (2 байта).

5. Таблица имен экспорта. Эта таблица содержит необязательные (по мнению Microsoft) имена экспортируемых функций. Данный массив используется совместно с Name Table Pointers и Ordinal Table для обеспечения связывания загрузчиком импорта/экспорта по имени. Механизм описывался выше. Каждый элемент являет собой ASCIIZ строку с именем экспортируемой функции. Никто не говорит, что они должны в файле идти друг за другом последовательно, хотя так построено большинство файлов. Надо отметить, что имена экспорта чувствительны к регистру. Отметим особенность загрузчика - при связывании, если адрес функции находится в секции экспорта, на самом деле по указанному адресу лежит строка, переадресующая к другой библиотеке, экспортирующей данную функцию (с указанием библиотеки и самой функции). Это называется - передача экспорта.

Секция импорта (.idata)

Рис. 4.1.4. Вызов импортируемой функции.

Схема вызова импортируемых функций из РЕ-модуля изображена на Рис. 4.1.4, который с некоторыми изменениями взят из книги [2]. Смысл данного рисунка заключается в следующем. При компоновке все вызовы API-функций преобразуются к вызову типа CALL Адрес1. При этом адрес, также как и вызов, находится в секции кода (.text). По адресу же стоит команда JMP DWORD PTR [Адрес2]. [Адрес2] находится в секции .idata (импорта) и содержит двойное слово — адрес функции в динамической библиотеке. Современные компиляторы содержат директивы, позволяющие вместо двух вызовов (CALL и JMP) генерировать один CALL [Адрес2].

Таблица директория импорта
Каталог импорта | Import Directory Table
Таблица ссылок на имена | LookUp Table
Таблица имен | Hint-Name Table
Таблица адресов импорта | Import Address Table

Формат входа в каталог импорта

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Import LookUp | Содержит ссылку на таблицу относительных адресов (относительно базового адреса), указывающих на соответствующие имена импортируемой функции, или непосредственно номер импортируемого входа.
04h | DWORD | Time/Date Stamp | Отметка о времени создания, часто содержит ноль.
08h | DWORD | Forward Chain | Связано с возможностью передачи экспорта в другие библиотеки. Обычно равно 0FFFFFFFFh.
0Ch | DWORD | Name RVA | Ссылка на библиотеку импорта в виде ASCII строки с нулем на конце. Например, KERNEL32.DLL или USER32.DLL.
10h | DWORD | Addres Table RVA | Ссылка на таблицу адресов импорта, заполняется системой при связывании.

Таблица просмотра импорта или таблица имен сервисов. Ссылка из поля Import LookUp на массив, содержащий ссылки на таблицу просмотра импорта. При импортировании по номеру старший бит элемента массива равен 1.

Тип | Содержимое
Word | Номер функции
Hint | ASCII имя функции

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

Локальная область данных цепочек (потоков)

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

Таблица разделов цепочек | TLS Directory Table
Данные цепочек | TLS Data
Индексные переменные | Index Variables
Адреса обратных вызовов | CallBack

Таблица разделов потоков

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Start Data Block VA | Виртуальный адрес начала блока данных цепочки.
04h | DWORD | End Data Block VA | Виртуальный адрес конца блока данных цепочки.
08h | DWORD | Index VA | Виртуальный адрес индексной переменной, используемой для доступа к локальному блоку данных цепочки.
0Ch | DWORD | CallBack Table VA | Виртуальный адрес таблицы обратных вызовов. Локальные обратные вызовы - массив виртуальных адресов функций, которые будут вызваны загрузчиком после создания цепочки (нити) и после ее завершения. Последний вход имеет нулевое значение и указывает на конец таблицы.

Секция ресурсов (.rdata)

Ресурсы представляют собой многоуровневое двоичное дерево. Их структура позволяет содержать до 2^31 уровней, однако реально используется только 3: самый верхний есть Type, затем Nam и затем Language (тип, имя, язык). Перемещения по иерархии каталогов ресурсов похожи на перемещения по каталогам жесткого диска. Типичное представление ресурсного участка в файлах:

Каталог ресурсов | Resources Directory Table
Данные ресурсов | Resources Data

Каталог ресурсов (Resource Directory Table)

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Flags | Пока не используются, должны быть сброшены в ноль.
04h | DWORD | Time/Date Stamp | Дата и время создания ресурсов от ресурсного компилятора.
08h | WORD | Major Version | Старшая часть версии ресурсов. Обычно равно нулю.
0Ah | WORD | Minor Version | Младшая часть версии ресурсов. Обычно равно нулю.
0Ch | WORD | Name Entry | Количество входов в таблицу имен (элементов массива) ресурсов. Таблица располагается в самом начале массива входов и содержит строковые имена, ассоциируемые с ресурсами.
0Eh | WORD | ID_Num Entry | Количество элементов массива, использующих целые ID.

За каталогом ресурсов сразу следует массив переменной длины, содержащий ресурсные входы. Name Entry содержит число ресурсных входов, имеющих имена (связанные с каждым входом). Имена нечувствительны к регистру и расположены в порядке возрастания. ID_Num Entry определяет число входов имеющих в качестве имени 32-битовый идентификатор. Эти входы так же отсортированы по возрастанию. Данная структура позволяет получать быстрый доступ к ресурсам по имени или по идентификатору, но для отдельно взятого ресурса поддерживается только одна из форм поиска. Что согласуется с синтаксисом .RC и .RES файлов. Каждый вход в таблицу ресурсов имеет следующий формат.

Вход в таблицу ресурсов (Resource Entry Item)

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Name RVA or Res ID | Поле содержит либо идентификатор ресурса, либо указатель на его имя в таблице имен ресурсов.
04h | DWORD | Data Entry RVA or SubDirectory RVA | Указывает либо на данные, либо на еще одну таблицу входов ресурсов. Старший бит поля, сброшенный в ноль, говорит, что поле указывает на данные.

Каждый пункт данных (Resource Entry Item) имеет следующий формат:

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Data RVA | Указатель на реально расположенные данные относительно Image Base.
04h | DWORD | Size | Размер ресурсных данных.
08h | DWORD | CodePage | Кодовая страница.
0Ch | DWORD | Reserved | He используется и устанавливается в ноль.

Таблица настроек адресов

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

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

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Page RVA | Относительный адрес страницы применения.
04h | DWORD | Block Size | Размер блока настроек (с заголовком). Эта величина используется для вычисления количества настроек.
08h | WORD | TypeOffset Record | Массив записей настроек, их переменное количество.

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

Структура элемента массива настроек

15...12
Type 		11...0
Offset 		Биты слова, Type указывает на тип настройки, a Offset на ее смещение внутри 4-килобайтной страницы.

Перечислим возможные типы поправок.

0h | Адрес абсолютный и никаких изменений производить не требуется.
1h | Добавить старшие 16 битов "Дельты" к 16-битовому полю, находящемуся по смещению Offset. 16-битовое поле представляет старшие биты 32-битового слова.
2h | Добавить младшие 16 битов "Дельты" по смещению Offset. 16-битовое поле представляет младшую половину 32-битового слова. Данная запись настройки присутствует только на RISC машине, когда Object align не является по умолчанию 64К.
3h | Прибавляет 32-битовое "Дельта" к 32-битовому значению.
4h | Настройка требует полного 32-битового значения. Старшие 16-бит берутся по адресу Offset, а младшие в следующем элементе TypeOffset. Все это объединяется в знаковую переменную, затем добавляется 32-битовое "Дельта" и DWORD 8000h. Старшие 16 бит получившегося значения сохраняются по адресу Offset в 16-битовом поле.
5h | ?

Отладочная информация (.debug$S, .debug$T)

Здесь помещается структура отладочного каталога, создаваемого любыми компоновщиками. Другая отладочная информация зависит от транслятора. Структуру COFF-отладочной информации можно посмотреть в книге [2].

Отладочный каталог

Смещение | Длина поля | Название поля | Описание поля
00h | DWORD | Debug Flags | Флаги, по-видимому, не используются и устанавливаются в нулевое значение.
04h | DWORD | Time/Date Stamp | Дата и время создания отладочной информации.
08h | WORD | Major Version | Старший номер версии отладочной информации.
0Ah | WORD | Minor Version | Младший номер версии отладочной информации.
0Ch DWORD Debug Type Тип информации для отладчика. Вот эти типы:
0000h - UNKNOWN/BORLAND;
0001h - COFF таблица символов;
0002h - Code View таблица символов;
0003h - FPO таблица символов;
0004h - MISC;
0005h - EXCEPTION;
0006h - FIXUP.
10h | DWORD | Data Size | Размер в байтах данных для отладки без размера заголовка.
14h | DWORD | Data RVA | Относительный адрес расположения отладочных данных в памяти.
18h | DWORD | Data Seek | Смещение к отладочным данным в файле.


Часть IV. Глава 2. Обзор отладчиков и дизассемблеров

Глава 2. Обзор отладчиков и дизассемблеров

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

I

Утилиты фирмы Microsoft

EDITBIN.EXE

Название программы многообещающе, но в действительности программу нельзя назвать редактором. Основное ее предназначение - конвертировать OMF-формат объектных файлов в COFF-формат. Кроме того, данная утилита позволяет менять некоторые другие атрибуты исполняемых и объектных модулей. Если в командной строке данной программы указать имя объектного модуля, то, в случае если модуль будет в OMF-формате, он будет преобразован в COFF-формат. Рассмотрим ключи данной программы, которые можно применять как к исполняемым, так и к объектным модулям.

/BIND - позволяет указать пути к динамическим библиотекам, которые используют данный исполняемый модуль. Например,
EDITBIN /BIND:PATH=c:\edit;d:\dll EDIT.EXE.

/HEAP - изменяет размер кучи в байтах. Например,
EDITBIN /HEAP:100000,100000 (см. Опции программы LINK.EXE).

/LARGEADDRESSAWARE - указывает, что приложение оперирует адресами, большими 2 гигабайт.

/NOLOGO - подавляет вывод информации о программе.

/REBASE - устанавливает базовый адрес модуля. По умолчанию для исполняемого модуля базовый адрес равен 400000Н, для динамической библиотеки - 10000000H.

/RELEASE - устанавливает контрольную сумму в заголовке исполняемого модуля.

/SECTION - изменяет атрибуты секций исполняемого модуля. Полный формат опции /SECTION:name[=newname][,attributes][,alignment]
Значение атрибутов
Атрибут | Значение
c | code
d | discardable
е | executable
i | initialized data
k | cached virtual memory
m | link remove
o | link info
p | paged virtual memory
r | read
s | shared
u | uninitialized data
w | write

Значение опции выравнивания
1 | 1
2 | 2
4 | 4
8 | 8
p | 16
t | 32
s | 64
x | no alignment

/STACK - изменяет значение требуемого для загружаемого модуля стека.
Например: EDITBIN /STACK:10000,10000 EDIT.EXE

/SUBSYSTEM - переопределяет подсистему, в которой работает данная программа.
Например, если программа оттранслирована с опцией /SUBSYSTEM:WINDOWS, можно изменить установку, не перекомпилируя ее. EDITBIN /SUBSYSTEM:CONSOLE EDIT.EXE.

/SWAPRUN - устанавливает для исполняемого модуля атрибут "помещать модуль в SWAP-файл".

/VERSION - устанавливает версию для исполняемого модуля.

/WS (/WS:AGGRESSIVE) - ycтaнaвливает атрибут AGGRESSIVE, который используется операционной системой Windows NT и Windows 2000.

Утилита весьма полезна для быстрого изменения атрибутов исполняемых и объектных модулей.

DUMPBIN.EXE

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

/ALL - выводит всю доступную информацию о модуле, кроме ассемблерного кода.

/ARCH - выводит содержимое секции .arch заголовка модуля.

/ARCHIVEMEMBERS - выводит минимальную информацию о элементах объектной библиотеки.

/DEPENDENTS - выводит имена динамических библиотек, откуда данным модулем импортируются функции.

/DIRECTIVES - выводит содержимое секции .drective, создаваемой компилятором (только для объектных модулей).

/DISASM - дизассемблирует содержимое секций модуля с использованием и символьной информации, если она присутствует там.

/EXPORTS - выдает все экспортируемые модулем имена.

/HEADER - выдает заголовки модуля и всех его секций. В случае объектной библиотеки выдает заголовки всех составляющих ее объектных модулей.

/IMPORTS - выдает все имена, импортируемые данным модулем.

/LINENUMBERS - выдает номера строк объектного модуля, если таковые имеются.

/LINKERMEMBER[:{1|2}] - выдает все имена в объектной библиотеке, определяемые как public.

/LINKERMEMBER:1 - в порядке следования объектных модулей в библиотеке.

/LINKERMEMBER:2 - вначале выдает смещение и индекс объектных модулей, а затем список имен в алфавитном порядке для каждого модуля.

/LINKERMEMBER - сочетание ключей 1 и 2.

/OUT - определяет, что вывод осуществляется не в консоль, а в файл (/OUT:ED.TXT).

/PDATA - выдает содержимое таблиц исключения.

/RAWDATA - выдает дамп каждой секции файла. Разновидности данного ключа: /RAWDATA:BYTE, /RAWDATA:SHORTS, /RAWDATA:LONGS, /RAWDATA:NONE, /RAWDATA:,number. Здесь number определяет ширину строк.

/SUMMARY - выдает минимальную информацию о секциях.

/SYMBOLS - выдает таблицу символов COFF-файла.

Рассматриваемая программа является весьма мощным средством дизассемблирования. Пусть программа называется prog.asm. Выполним трансляцию программы следующим образом.

  ml /с /coff /Zi /Zd prog.asm
  link /debug /subsystem:windows prog.obj53

При этом, кроме исполняемого модуля prog.exe, появляется еще и файл prog.pdb, содержащий отладочную информацию.

Выполним теперь команду DUMPBIN /DISASM /OUT:PROG.TXT PROG.EXE. В результате получим практически исходный ассемблерный код. Часть этого кода представлена на Рис. 4.2.1.

_START:
0040101С: 6A00     push 0
0040101E: E843020000 call _GetModuleHandleA@4
00401023: A344404000 mov [00404044],eax
00401028: C7051C404000 mov dword ptr ds:[40401Ch],4003h
     03400000
00401032: C70520404000 mov dword ptr ds:[404020h],offset
@ILT+0(_WNDPROC@0)
     05104000
0040103C: C70524404000 mov dword ptr ds:[404024h],0
     00000000
00401046: C70528404000 mov dword ptr ds:[404028h],0
     00000000
00401050: A144404000 mov eax,[00404044]
00401055: A32C404000 mov [0040402C],eax
0040105A: 68007F0000 push 7F00h
0040105F: 6A00     push 0
00401061: E8D6010000 call _LoadIconA@8
00401066: A330404000 mov [00404030],eax
0040106B: 68037F0000 push 7F03h
00401070: 6A00     push 0
00401072: E8BF010000 call _LoadCursorA@8

Рис. 4.2.1. Часть дизассемблированиого кода.

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

     _LoadIconA@8:
     0040123С: FF2510514000 jmp dword ptr [_imp_LoadIconA@8]

В действительности, когда мы вызываем, например, функцию _LoadIconA@8, то на самом деле происходит переход на адрес, где стоит команда

     jmp dword ptr [_imp_LoadIconA@8].

По этой причине в своих программах вместо объявления LoadIcon@8, можно объявлять _imp_LoadIconA@8, что несколько увеличит производительность Вашей программы. Ранее мы не говорили о такой возможности по двум причинам:

  1. Повышение производительности здесь минимально.
  2. Транслятор TASM32 делает все несколько иначе.

Более подробно данный вопрос рассматривается в Главе 4.1.

53 Здесь, разумеется, мы рассматриваем только средства MASM.

II

Утилиты других производителей

DUMPPE.EXE

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

HIEW.EXE

Данная программа широко известна в среде программистов, скажем так, хакерского направления. Название программы происходит от фразы "Hacker's View". Основная задача, которую выполняет данная программа - просматривать и редактировать загружаемые модули. Причем просмотр и редактирование допускается в трех вариантах: двоичный, текстовый и ассемблерный. Хороших дизассемблирующих программ создано довольно много, а вот программ, подобных данной, можно по пальцам перечесть.

Интерфейс программы весьма напоминает интерфейс редакторов таких программ, как FAR или Norton Commander (см. Рис. 4.2.2.). Все команды осуществляются при помощи функциональных клавиш с использованием клавиш "Alt" и "Ctrl". Например, нажимая клавишу F4, вы получаете возможность выбрать способ представления двоичного файла: текстовый, ассемблерный или двоичный. Нажимая клавишу F3 (при условии, если Вы находитесь в двоичном или ассемблерном просмотре), Вы получаете возможность редактировать файл. Если же, находясь в ассемблерном просмотре, Вы после F3 нажмете еще и F2, то сможете редактировать машинную команду в символьном виде. Мы не будем далее останавливаться на командах данной программы, поскольку они просты, очевидны и могут быть получены просто по F1, а перейдем сразу к простому примеру использования данной программы, хотя пример тематически и относится к материалу Гл. 4.6. Чтобы слишком не загромождать рассмотрение возьмем простую консольную программу.

Рис. 4.2.2. Внешний вид программы HIEW.EXE

Ниже (Рис. 4.2.3) представлена простая консольная программа, выводящая на экран текстовую строку.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
INVALID_HANDLE_VALUE equ -1
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     BUF   DB "Строка для вывода",0
     LENS  DWORD ? ; количество выведенных символов
     HANDL DWORD ?
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
     PUSH STD_OUTPUT_HANDLE
     CALL GetStdHandle@4
     CMP EAX,INVALID_HANDLE_VALUE
     JNE _EX
     MOV HANDL,EAX
; вывод строки
     PUSH 0
     PUSH OFFSET LENS
     PUSH 17
     PUSH OFFSET BUF
     PUSH HANDL
     CALL WriteConsoleA@20
_EX:
     PUSH 0
     CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 4.2.3. Консольная программа.

Программа на Рис. 4.2.3 проста и корректна. Представьте теперь, что при отладке Вы случайно изменили одну команду: вместо JE поставили JNE. В результате поспе трансляции программа перестала работать. Можно исправить ее, не прибегая к ассемблерному тексту? Конечно. Для этого в начале ее следует дизассемблировать, найти ошибку, а потом воспользоваться программой HIEW.EXE. Вообще говоря, можно ограничиться только программой HIEW, так как она вполне корректно дизассемблирует. Однако мы нарочно проведем исправление в два этапа.

Дизассемблируем модуль при помощи программы DUMPBIN.EXE. Вот дизассемблированный текст программы (Рис. 4.2.4).

Dump of file cons1.exe

File Type: EXECUTABLE IMAGE

  00401000: 6A F5              push        0F5h
  00401002: E8 2B 00 00 00     call        00401032
  00401007: 83 F8 FF           cmp         eax,0FFFFFFFFh
  0040100A: 75 1E              jne         0040102A
  0040100C: A3 16 30 40 00     mov         [00403016],eax
  00401011: 6A 00              push        0
  00401013: 68 12 30 40 00     push        403012h
  00401018: 6A 11              push        11h
  0040101A: 68 00 30 40 00     push        403000h
  0040101F: FF 35 16 30 40 00  push        dword ptr ds:[00403016h]
  00401025: E8 0E 00 00 00     call        00401038
  0040102A: 6A 00              push        0
  0040102C: E8 0D 00 00 00     call        0040103E
  00401031: CC                 int         3
  00401032: FF 25 08 20 40 00  jmp         dword ptr ds:[00402008h]
  00401038: FF 25 00 20 40 00  jmp         dword ptr ds:[00402000h]
  0040103E: FF 25 04 20 40 00  jmp         dword ptr ds:[00402004h]

Рис. 4.2.4. Дизассемблированный код программы на Рис. 4.2.3.

По дизассемблированному коду легко обнаружить ошибку. Кстати, команду cmp eax,0FFh надо, естественно, понимать как cmp eax,0FFFFFFFFh. Запомним нужный код 83F8FF. Запускаем программу HIEW.EXE, нажимаем клавишу F7 и ищем нужное сочетание. Далее клавиша F3, затем F2 и далее - заменяем команду JNE на JE. Клавиша F9 закрепляет изменение. В результате мы исправили программу, не прибегая к ее перетрансляции.

DEWIN.EXE

Программа работает в командном режиме, но по сравнению, например, с DUMPBIN.EXE обладает рядом достоинств. Главное из этих достоинств - это распознавание языков высокого уровня. Кроме того, Вы сами можете писать скрипт-процедуры на предлагаемом макроязыке.

IDA PRO

Один из самых мощных дизассемблеров. Возможности настолько велики, что многие программисты считают его всемогущим. Работая над текстом дизассемблируемой программы. Вы можете называть своими именами метки и процедуры, давать свои комментарии так, что дизассемблированный текст становится в конце концов ясным и понятным. Все сохраняется в специальную базу и при последующем запуске, естественно, восстанавливаются. Внешний вид дизассемблера IDA PRO показан на Рис. 4.2.4. Мы дизассемблировали одну из наших старых программ. Обратите, кстати, внимание на ссылку offset WNDPROC. Название WNDPROC дано уже нами в процессе анализа кода программы. Но рассмотрим все по порядку.

Рис. 4.2.4. Пример дизассемблирования программы с помощью самого мощного дизассемблера IDA PRO (под Windows).

Рассмотрим некоторые возможности этого дизассемблера.

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

2. Распознавание библиотечных и API-функций (см. Рис. 4.2.4). Дизассемблер не просто распознает эти функции, но и комментирует параметры этих функций.

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

4. При помощи Shift+Ins, Ins, а так же пунктов меню "EDIT" в любом месте программы можно записать комментарий. Комментарий, как и введенные названия меток, запоминается в базе данных программы. Но это еще не самое приятное. В комментарии может присутствовать адрес строки программы или имя метки. Если сделать двойной щелчок по адресу или метке, то мы как раз и очутимся на этом месте.

5. Сворачивание и разворачивание процедур. При помощи клавиши "-" на дополнительной клавиатуре можно свернуть процедуру, а при помощи "+" развернуть процедуру. Представление процедур в свернутом виде позволяет представить программу в более компактном и более понятном виде.

6. IDA PRO весьма аккуратно распознает не только код, но и данные. На Рис. 4.2.5 показана дизассемблированная часть нашей программы, содержащей данные.

7. Создание и выполнение командных файлов. Язык командных файлов очень близок к языку Си. У меня нет намерения рассказывать о языке, который использует IDA PRO, приведу только один такой командный файл, содержащийся в пакете IDA PRO.

Рис. 4.2.5. Часть программы, содержащей данные, дизассемблированная при помощи IDA PRO.

//
// This example shows how to get list of functions.
//

#include <idc.idc>
static main() {
auto ea,x;

for (ea=NextFunction(0); ea != BADADDR; ea=NextFunction(ea))
{
  Message("Function at %081X:%s",ea,GetFunctionName(ea));
  x = GetFunctionFlags(ea);
  if ( x & FUNC_NORET ) Message(" Noret");
  if ( x & FUNC_FAR ) Message(" Far");
  Message("\n") ;
}
ea = ChooseFunction("Please choose a function");
Message("The user chose function at %081X\n",ea);
}

Рис. 4.2.6. Пример командного файла IDA PRO.

Прокомментируем программу на Рис. 4.2.6. Как легко догадаться, организация цикла и условные конструкции имеют в точности тот же синтаксис, что и в языке Си. Главное здесь - разобрать смысл используемых библиотечных функций. Легко видеть, что функция Message просто выводит строку в окно сообщений, которое находится под основным окном. Функция ChooseFunction вызывает окно, которое вызывается также из меню: "Jump to Function". Функция GetFunctionFlags возвращает информацию об указанной функции. Наконец функция NextFunction осуществляет переход на следующую функцию. Она возвращает адрес функции. Аргументом же ее является адрес функции, от которой осуществляется переход на следующую функцию. Оставляю Вам изучение командного языка, поддерживаемого IDA PRO, который представлен в помощи программы.

Теоретически можно написать любую сколь угодно сложную программу по анализу дизассемблированного кода.

8. Программа IDA PRO осуществляет дизассемблирование модулей самых различных форматов: .OBJ, .EXE, .DLL, VXD, .ZIP, .NLM и др.

9. Функциональность IDA PRO может быть значительно усилена посредством подключаемых модулей - plugin'ов. Подключаемые модули пишутся на языке C++ и имеют структуру РЕ-модулей. Подключение модулей осуществляется через горячие клавиши или через пункты меню Edit\Plugins. Подключаемые модули находятся в специальном каталоге Plugins, где находится и файл конфигурации, где указаны эти модули.

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


Часть IV. Глава 3. Введение в Turbo Debugger

Глава 3. Введение в Turbo Debugger

I

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

Рассмотрим, например, программу на Рис. 3.4.1. Эта программа выводит в окно типы устройств (список доступных дисков). При трансляции с помощью TASM32 добавим также ключ /zi, а при компоновке (TLINK.32.EXE) ключ -v. В этом случае в исполняемом модуле будет сохранена информация, необходимая для символьной отладки.

Тут важно иметь в виду, что для символьной отладки, т.е. с использованием текста программы, необходим не только сам исполняемый модуль и отладочная информация в нем, но и сам программный текст. Дело в том, что в исполняемом модуле хранится информация, позволяющая сопоставлять машинные коды программ и текст программы. На Рис. 4.3.1 представлено окно отладчика с загруженной отладочной информацией программы DRIV.ASM (Рис. 3.4.1). Отладчик позволяет выполнять программу в пошаговом режиме (подробнее о режимах выполнения программы см. далее в главе), видя одновременно текст программы и дизассемблированный текст, и передвигаясь по нему, видеть результат каждого шага.

На Рис. 4.3.1 представлены три наиболее часто используемых окна отладчика: окно с текстом программы (модульное окно), окно CPU, где хранится дизассемблированный текст, а также текущая информация о регистрах и флагах, и окно сообщений, которое очень важно при отладке оконных GUI-приложений.

Рассмотрим команды отладчика и возможности отладчика.

Запуск отлаживаемого приложения. Выполняется по клавише F9 или по команде RUN из меню RUN. Программа выполняется в полном объеме, если только не указана точка останова (breakpoint) на строку в окне с текстом программы или окне CPU. Точка останова устанавливается при помощи клавиши F2. Открыв окно Watch (View/ Watches), Вы можете установить там слежение за переменными, которые Вам интересны. В этом случае точки останова окажутся очень полезными.

Выполнить программу до строки, где находится курсор. Команда выполняется при помощи клавиши F4 или посредством меню. Смысл данной команды заключается в том, что, заинтересовавшись каким-либо местом программы, Вы можете выполнить программу до данной строки и посмотреть результат выполнения. Фактически это та же точка останова, но работает более оперативно. Кроме того, можно заставить выполняться программу до данного адреса (Alt+F9).

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

Рис. 4.3.1. Окна Turbo Debugger.

Выполнение команды с переходом через процедуру. Данная команда выполняется по нажатию клавиши F8. Отличается от предыдущей команды тем, что не происходит вход в процедуру, но все инструкции в процедуре выполняются автоматически.

Выполнить процедуру. Если курсор находится на команде вызова процедуры, то можно выполнить лишь одну эту процедуру командой Alt+F8.

Выполнение с задержкой. Можно заставить отладчик выполнять программу с задержкой после каждой команды (Animation) - команда Animate меню Run.

При выполнении любой из перечисленных команд также могут быть заданы опции командной строки - Run/Arguments...

Рассмотрим теперь окна, которые используются при отладке в данном отладчике. С некоторыми из них мы уже познакомились, но все равно повторимся. Показать на экране то или иное окно можно посредством меню View. Мы опишем часть наиболее важных окон.

Текст программы можно увидеть на экране, обратившись к пункту меню Module. При этом программа должна быть оттранслирована с опциями, обеспечивающими сохранение в исполняемом модуле отладочной информации. К сожалению, речь идет только о продуктах фирмы Borland: C++, Delphi, Assembler. Отладочная информация других фирм Turbo Debugger не распознает. В дальнейшем мы рассмотрим пример отладки программы, написанной на Borland C++.

Если отладочную информацию в исполняемом модуле Turbo Debugger не распознает, Вам при отладке придется использовать окно CPU. Окно разделено на пять частей (см. Рис. 4.3.2 против часовой стрелки):
1. Инструкции микропроцессора.
2. Сегмент данных.
3. Сегмент стека.
4. Состояние регистров.
5. Основные флаги.

Рис. 4.3.2. Окно CPU отладчика Turbo Debugger.

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

Окно стека. Показывает текущее состояние стека. Причем первая вызванная функция будет находиться на дне стека.

Окно точек останова. Здесь содержится информация обо всех установленных в программе точках останова. Здесь же можно добавить или установить точки останова.

Окно регистрации (Log). В нем хранится информация о происходящих в отладчике событиях.

Окно переменных. Отображает все переменные, доступные в данном месте.

Окно файлов. В этом окне можно просмотреть двоичный файл и исправить его при необходимости.

Окно отображения памяти. Дает возможность построчного просмотра памяти.

Окно сопроцессора. Данное окно отображает текущее состояние сопроцессора.

Окно истории выполнения (Execution History). Окно содержит историю выполнения программы.

Окно иерархии (Hierarchy). Выводит на дисплей иерархическое дерево для всех объектов и всех типах классов, используемых в текущем модуле. Пример окна, отображающего иерархию классов объектов, см. на Рис. 4.3.3.

Рис. 4.3.3. Пример окна Turbo Debugger, отображающего иерархию классов.

Окно потоков. Данное окно содержит информацию обо всех потоках, работающих в данный момент в программе.

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

Рис. 4.3.4. Пример содержимого окна сообщений

Окно буфера обмена. С помощью данного окна можно следить за содержимым буфера обмена при выполнении программы.

II

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

Puc. 4.3.5. Программа, расположенная на Рис. 4.3.6 в окне отладчика.

#include <windows.h>
#include <stdio.h>
void sort(DWORD a[], DWORD );
void main()
{
    DWORD a[10];
    DWORD j,p,k,r;
    randomize();
    for(j=0; j<10; j++) a[j]=random(10) ;
// сортировка
    for(k=9; k>l; k--)
    {
        r=0;
        for(j=0; j<k; j++)
        {
            if(a[j]>a[j+1]){ p=a[j+1] ;
            a[j+1]=a[j] ;
            a[j]=p; r=1;
            };
        }
        if(r==0) break;
    }
    for(j=0; j<10; j++) printf("%lu\n",a[j]);
    ExitProcess(0);
}

Рис. 4.3.6. Пример простой консольной программы.

На Рис. 4.3.5 показано окно отладчика Turbo Debugger с данной программой. В нижней части рисунка расположено окно Watches. В этом окне отслеживается значение элементов массива а[]. Обратившись к окну CPU, можно увидеть, как алгоритм пузырьковой сортировки, реализованный на языке Си, преобразуется в ассемблерный код.

III

Основной целью отладчика Turbo Debugger является отладка программы, имеющей отладочную информацию. Дело в том, что хотя отладчик и дизассемблирует программу, и мы можем видеть дизассемблированный код в окне CPU, такие дизассемблеры, как W32Dasm и IDA Pro намного превосходят в этой части Turbo Debugger. Отладчик же Ice, работая в нулевом кольце, также более удобен для анализа исполняемых модулей. Отладчик Turbo Debugger занимает свою нишу в отладке, но здесь он незаменим и очень удобен. В данном разделе мы намерены рассмотреть некоторые вопросы методики отладки.

Вообще процесс отладки можно разделить на четыре этапа:

  1. Обнаружение ошибки. Обнаружение ошибок в программе связано с тестированием программы и ее эксплуатацией.
  2. Поиск ее местонахождения. Данный этап часто оказывается самым трудоемким, и именно здесь может пригодиться использование отладчика. Профессиональные программисты знают, что в сложных алгоритмах найти ошибку путем простого анализа текста программы бывает очень сложно. Можно, конечно, выводить промежуточные результаты, но для этого может понадобиться слишком много таких выводов. Хороший отладчик в этом случае незаменим, поскольку может позволить отслеживать значение переменных на каждом шаге.
  3. Определение причины ошибки. Отбрасывая тот случай, когда причиной ошибки является неправильный алгоритм, рассмотрим типичные ассемблерные ошибки.
    1. Ошибка в порядке следования операндов. Например, MOV EAX,EBX вместо MOV EBX,EAX.
    2. При использовании рекурсивных алгоритмов или слишком большой вложенности вызовов процедур может быть исчерпан стек.
    3. При вызове процедур может быть испорчено содержимое того или иного регистра.
    4. Неосвобождение стека при выходе из процедуры.
    5. Неправильное использование условных переходов - JA вместо JNA и т.п.
    6. Часто при организации циклических алгоритмов программисты ошибаются относительно последних значений переменных.
    7. Неправильная установка флага направления.
    8. Ошибка при определении границ переменных и массивов. Эти ошибки часто приводят к тому, что портится содержимое и других переменных.
    9. Неправильное преобразование из одного типа операнда к другому. Например, после загрузки MOV AL,BL используется EAX и забывается об обнулении старших байтов регистра EAX.
  4. Исправление ошибки. Если найденная ошибка проста и очевидна для Вас, то исправить ее не составит большого труда. Кстати, как Вы, наверное, уже поняли, Turbo Debugger не позволяет исправлять исполняемые модули. Но случаются и ситуации, когда очевидно, что данный участок программы (или процедура) выдает ошибочное значение, а на то, чтобы найти ошибку, у Вас не хватает времени - участок достаточно сложен. Иногда неправильное значение выходной информации возникает, лишь при редком сочетании входных параметров. В этом случае может быть применен простой прием: между указанным участком и остальной частью программы вставляется несколько строк, проверяющих выходную информацию и исправляющих ее, если нужно. Такой прием часто оказывается незаменим при доработке чужой программы очень большого объема, когда понять логику чужого алгоритма (да еще содержащего ошибку) бывает просто невозможно.


Часть IV. Глава 4. Описание работы с дизассемблером W32Dasm и отладчиком ICE

Глава 4. Описание работы с дизассемблером W32Dasm и отладчиком ICE

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

I

Программа W32Dasm (Windows Disassembler) представляет собой симбиоз довольно мощного дизассемблера и отладчика. Версия 8.93 программы, наиболее распространенная в настоящее время, может работать не только с РЕ-модулями, но и DOS-, NE-, -LE- модулями. Я намерен довольно полно описать работу с этой программой.

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

Рис. 4.4.1. Внешний вид программы W32Dasm.

Как уже было сказано, программа является дизассемблером и отладчиком в одном лице. Это отражено также в двух пунктах меню: "Disassembler" и "Debugger". Соответственно, имеются отдельные настройки для дизассемблера и отладчика. Для дизассемблера имеется всего три опции, касающиеся анализа перекрестных ссылок в условных переходах, безусловных переходах и вызовах процедур. По умолчанию все три опции установлены. Отмена этих опций нежелательна, т.к. снижает информативность дизассемблированного текста. В принципе, это может понадобиться при дизассемблировании очень большой программы, так это несколько ускоряет процесс анализа.

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

Рис. 4.4.2. Опции отлпдчика

Для начала работы с исполняемым модулем достаточно выбрать нужный файл в меню "Disassembler\0pen File...". После этого программа производит анализ модуля и выдает дизассемблированный текст, а также весьма полную информацию о секциях модуля54. W32Dasm весьма корректно распознает API-функции и комментирует их (см. Рис. 4.4.3).

После работы с модулем можно создать проект работы при помощи пункта "Disassembler\Save Disassembler...". По умолчанию проект сохраняется в подкаталог "wpjfiles", который расположен в рабочем каталоге W32Dasm и состоит из двух файлов: с расширением "alf" - дизассемблированный текст, с расширением "wpj" - собственно сам проект. При повторном запуске можно открывать уже не модуль, а проект с помощью пункта "Project\0pen...".

* Possible Reference to String Resource ID=00001: "! >>1I5=85"
                                  |
:00401013 6A01                    push 00000001
:00401015 FF3518304000            push dword ptr [00403018]

* Reference To: USER32.LoadStringA, Ord:01A8h
                                  |
:0040101B E8AE000000              Call 004010CE

Puc. 4.4.3. Фрагмент дизассемблированного текста.

Передвижение по дизассемблированному тексту. При передвижении по тексту текущая строка подсвечивается другим цветом, при этом особо выделяются переходы и вызовы процедур. Передвижение облегчается также с помощью пункта меню "Goto":
Goto Code Start - переход на начало листинга;
Goto Program Entry Point - переход на точку входа программы, наиболее важный пункт меню;
Goto Page - переход на страницу с заданным номером, по умолчанию число строк на странице составляет пятьдесят;
Goto Code Location - переход по заданному адресу, в случае отсутствия адреса учитывается диапазон и близость к другим адресам.

Другим способом передвижения по дизассемблированному тексту является пункт "Search" - поиск. Здесь нет никаких отличий от подобных команд других программ.

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

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

Данные. Есть несколько вариантов работы с данными.

Во-первых, имеется пункт меню "HexData\Hex Display of Data...", где можно просмотреть содержимое сегментов данных в шестнадцатеричном и строковом варианте. Кроме того, сам код программы также можно просматривать в шестнадцатеричном виде. Для этого используется пункт "HexData\Hex Display of Code...".

Во-вторых, имеется пункт меню "Refs\String Data References". Это весьма мощное и полезное средство. При выборе этого пункта появляется список строк, на которые имеются ссылки в тексте программы. Во всяком случае, это то, что сумел определить дизассемблер при анализе программы. Выбрав нужную строку, можно двойным щелчком перенестись в соответствующее место программы. Если ссылок на данную строку несколько, то, продолжая делать двойные щелчки, мы будем переходить во все нужные места программы. На Рис. 4.4.4 изображено окно ссылок на строковые типы данных.

Рис. 4.4.4. Окно ссылок на строки.

Как видно из рисунка, можно скопировать в буфер выбранную строку или все строки.

Импортированные и экспортированные функции. Список импортированных функций и модулей находится в начале дизассемблированного текста (см. Рис. 4.4.5). Кроме того, список импортированных функций можно получить из меню "Functions\Imports". Выбрав нужную функцию в списке, двойным щелчком можно получить все места программы, где вызывается эта функция. Экспортированные функции также можно получить в соответствующем окне, выбрав пункт "Functions\Exports".

Ресурсы. В начале дизассемблированного текста также описаны и ресурсы, точнее два основных ресурса-меню и диалоговое окно. Со списком этих ресурсов можно работать и в специальных окнах, получаемых с помощью пунктов меню программы "Refs\Menu References" и "Refs\Dialog references". Строковые ресурсы можно увидеть в уже упомянутом окне просмотра перечня строковых ссылок (см. Рис. 4.4.4). Остальные ресурсы данной версии программы, к сожалению, не выделяются.

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

Загрузка программ для отладки. Загрузить модуль для отладки можно двумя способами. С помощью пункта "Debug\Load Process" загружается для отладки уже дизассемблированный модуль. Пункт же "Debug\Attach to an Active Process" позволяет "подсоединяться" и отлаживать процесс, находящийся в памяти. После загрузки отладчика на экране появляются два окна. Первое окно информационное (Рис. 4.4.6), в документации оно называется "нижним левым окном отладчика". Второе окно управляющее (Рис. 4.4.7), называемое в документации "нижним правым окном отладчика".

+++++++++++++++++++ IMPORTED FUNCTIONS ++++++++++++++++++
Number of Imported Modules =    7 (decimal)

   Import Module 001: ADVAPI32.dll
   Import Module 002: KERNEL32.dll
   Import Module 003: MPR.dll
   Import Module 004: COMCTL32.dll
   Import Module 005: GDI32.dll
   Import Module 006: SHELL32.dll
   Import Module 007: USER32.dll

+++++++++++++++++++ IMPORT MODULE DETAILS +++++++++++++++

   Import Module 001: ADVAPI32.dll
 Addr:000D9660 hint(0000) Name: RegCloseKey
 Addr:000D966B hint(0000) Name: RegOpenKeyExA
 Addr:000D967B hint(0000) Name: KegQueryValueExA
 Addr:000D9692 hint(0000) Name: RegSetValueBxA

   Import Module 002: KERNEL32.dll

Рис. 4.4.5. Фрагмент списка импортированных модулей и функций.

Информационное окно содержит несколько окон-списков: содержимое регистров микропроцессора, значение флагов микропроцессора, точки останова, содержимое сегментных регистров, история трассировок, история событий, базовые адреса, два дисплея данных. Ниже мы объясним также значения кнопок этого окна.

Обратимся теперь к управляющему окну. Кнопка "Run" запускает загруженную в отладчик программу, кнопка "Pause" приостанавливает работу программы, кнопка "Terminate" останавливает выполнение программы и выгружает ее из отладчика. Кнопки "Step Over" и "Step Into" используются для пошагового исполнения программы. Первая кнопка, выполняя инструкции, "перескакивает" код процедур и цепочечные команды с повторением, вторая кнопка выполняет все инструкции последовательно. Кроме того, имеются кнопки "AutoStep Over" и "AutoStep Into" для автоматического пошагового выполнения программы. В случае API-функций даже использование "Step Into" не приведет к пошаговому выполнению кода функции в силу того, что код функции не доступен для пользовательских программ. Очень удобно, что при пошаговом выполнении происходит передвижение не только в окне отладчика, но и в окне дизассемблера.

Рис. 4.4.6. Первое информационное окно отладчика.

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

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

Точки останова. В дизассемблированном тексте можно установить точки останова. Для этого следует перейти к нужной строке и воспользоваться клавишей F2 или использовать левую кнопку мыши при нажатой клавише Ctrl. Установка точки останова в окне дизассемблера тут же отражается в информационном окне и в управляющем окне - у отмеченной команды появляется префикс BP*. Удалить точку останова можно тем же способом, что и при установке. Точку останова можно сделать также неактивной. Для этого нужно обратиться к информационному окну и списку точек останова. Выбрав нужный адрес, щелкните по нему правой кнопкой. При этом "звездочка" у данной точки останова исчезнет, а строка в окне дизассемблера из желтой станет зеленой.

Рис. 4.4.7. Второе управляющее окно отладчика.

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

Модификация кода, данных и регистров. Отладчик позволяет модифицировать загруженный в него код. Сделать это можно, обратившись к кнопке "Patch Code" в управляющем окне (см. Рис. 4.4.8). Важно отметить, что модификации подвергается только код, загруженный в отладчик, а не дизассемблированный текст. Найдя нужное место в отлаживаемом коде, и модифицировав его, вы можете тут же проверить результат модификации, запустив программу. Если модификация оказалась правильной, можно приступать уже к модификации самого модуля.

Для модификации регистров и ячеек памяти исполняемого процесса, существует специальная кнопка "Modify Data" на панели информационного окна. Окно расположено на Рис. 4.4.9. Окно несколько загромождено элементами, но, присмотревшись, Вы поймете, что все элементы на своем месте. В верхней части окна расположены текущие значения основных флагов микропроцессора, которые Вы можете изменить. Для того чтобы модифицировать содержимое регистра или ячейку памяти следует вначале установить модифицирующую величину - "Enter Value". Далее следует выбрать нужный регистр и нажать кнопку слева от нее. Чтобы установить старое значение, следует нажать кнопку "R" справа от регистра. Чтобы изменить содержимое ячейки памяти, следует вначале записать адрес ячейки в поле "Mem Lock", а затем воспользоваться кнопкой "Mem". Другие операции, предоставляемые данным окном, также достаточно очевидны.

Рис. 4.4.8. Окно модификации отлаживаемого кода.

Отладчик позволяет выдавать дополнительную информацию о выполняемых API-функциях. Чтобы воспользоваться этим, необходимо сделать следующее. В управляющем окне установите флаги: "Enable Documented API Detail", "Stop Auto On API". Далее запустите программу на выполнение клавишей F5. При прохождении API-функции будет производиться остановка и на экране будет появляться окно с информацией о данной функции,

Поиск нужного места в программе. Часто требуется найти в дизассемблированном коде место, соответствующее месту исполняемой программы. Наиболее эффективно это можно сделать следующим образом. Загружаем в отладчик данный модуль. Запускаем его, доходим до нужного места и нажимаем кнопку "Terminate". В результате подсвеченная строка в дизассемблированном коде окажется как раз в нужном месте. Нужно только иметь в виду, что некоторые программы делают изменения, которые потом продолжают действовать. К таковым относятся, в частности, горячие клавиши.

Рис. 4.4.9. Окно модификации регистров и ячеек памяти. Дополнительные возможности для работы с API.

К использованию программы W32Dasm мы еще вернемся в последующих главах.

54 Хотя W32Dasm работает с разного типа модулями, мы рассматриваем только модули формата РЕ.

II

Отладчик SoftIce (версия 4.05) или просто Ice рассчитан для работы в Windows 9x и Windows NT. 55. Отладчик состоит из собственно отладчика (в английском варианте это "kernel-mode debugger", что можно перевести как "отладчик на уровне ядра"), кроме этого, в пакет SoftIce входит еще символьный загрузчик для загрузки в отладчик исполняемых модулей. Загрузчик позволяет прочитать отладочную информацию для продуктов фирмы Microsoft и Borland.

Итак, что дает отладка при помощи SoftIce?

  • символьная и обычная отладка 32-битных приложений;
  • отладка драйверов для Windows NT и для Windows 9x, отладка 16-битных приложений для MS DOS и Windows, отладка внутренних программ операционной системы;
  • установка обычных точек останова на команду, стоящую по определенному адресу;
  • установка точек останова на операции чтения/записи в память, чтения/записи в порты ввода-вывода;
  • установка точек останова на сообщения Windows;
  • установка условных точек останова, т.е. точек останова, срабатывающих при выполнении определенного условия;
  • получение внутренней информации операционной системы;
  • возможность использования отладчика на удаленной машине и др.

Отладчик SoftIce имеет разные исполнения для Windows 9x и для Windows NT. В первом случае он представляет собой VXD-драйвер и запускается из autoexec.bat (программа WINICE.EXE). В Windows NT он представляет собой драйвер уровня ядра - NTICE.SYS.

Начальная установка. Установка пакета достаточно проста. Основной вопрос, который возникает при установке - выбор видеоадаптера и мыши. Что касается видеоадаптера, то обычно рекомендуют выбрать стандартный VGA-адаптер, и с видеосистемой не будет никаких проблем. Отладчик SoftIce предполагает использование мыши для управления. Однако мышь является второстепенным средством управления, и можно обойтись без нее.

После установки и перезапуска Вам, скорее всего, понадобится еще некоторая настройка файла WINICE.DAT. Вот основные рекомендации.

  1. Строка PHYSMB=32 определяет реальный объем физической памяти в мегабайтах. Укажите реальный объем памяти на Вашем компьютере.
  2. Строка INIT= - определяет оконные установки. Лично мне нравятся установки, определяемые строкой:INIT="SET FONT 1; SET ORIGIN 30 30; LINES 65; WIDTH 90; WR; WF; WD 4; WL; WC 30; X;". Впрочем, Вы сами сможете подобрать параметры по Вашему вкусу. Но главное заключается в том, что, открыв окно отладчика, можно оперативно изменить существующие установки.
  3. В конце файла установок стоят строки типа ;EXP=c:\windows\system\kernel32.dll - снимите с них комментарий. Это необходимо сделать, чтобы отладчик распознавал импортируемые приложением функции стандартных динамических библиотек.
  4. В Windows 9x SoftIce при инсталляции помещает в конце файла AUTOEXEC.BAT строку типа C:\ICE\WINICE.EXE. При желании Вы всегда можете поставить комментарий на эту строку.

Загрузка программы для отладки. Для загрузки приложения в отладчик в пакете SoftIce имеется специальная программа LOADER32.EXE. Она используется для загрузки 32-битных приложений. Для загрузки 16-битных приложений используются утилиты, расположенные в подкаталоге Util16.

Итак, вернемся к программе LOADER32.EXE. Вид загрузчика показан на Рис. 4.4.10. Последовательность действий для загрузки программы в отладчик следующая: открыть модуль, транслировать модуль - преобразование отладочной информации в NMS-файл, загрузить модуль. Если загрузчик разобрался с отладочной информацией, то в окне отладчика появится программный текст, в противном случае Вам придется работать с обычным дизассемблированным текстом. После загрузки модуля, вы можете настроить запуск модуля в отладчике, воспользовавшись пунктом меню Module\Setting (Рис. 4.4.11). Настройка достаточно очевидна, поэтому мы не будем разъяснять смысл пунктов данного окна.

Рис. 4.4.10. Вид загрузчика SoftIce (LOADER32.EXE).

Рис. 4.4.11. Окно настройки запуска модуля в отладчике Softice.

Рис. 4.4.12. Окно отладчика SoftIce.

Обзор команд отладчика

  1. Вызов отладчика. Вызов отладчика осуществляется клавишами Ctrl+D, повторное нажатие этих клавиш приводит к закрытию отладчика. Закрытие окна отладчика можно осуществить и клавишей F5.
  2. Помощь. Для получения списка всех команд наберите в командном окне команду "h". Для получения информации по конкретной команде наберите "h <команда>". Нижняя часть командного окна - панель помощи. В ней появляются подсказки. Например, если Вы введете букву "w" (на эту букву начинаются оконные команды) на панели появится список команд, начинающихся на "w".
  3. Работа с окнами. При вызове отладчика на экране появляется его окно (см. Рис. 4.4.12.), разделенное на несколько окон, каждое из которых несет на себе определенную информационную нагрузку. Количество и размер этих окон определяется начальной установкой (см. начало), но может меняться по нашему желанию. Для ввода команд существует командное окно. Обычно курсор находится именно в командном окне. Каждое окно имеет свое мнемоническое обозначение, например "c" - окно кода, "d" - окно данных, "r" - окно регистров, "f" - окно сопроцессора, "w" - окно наблюдения за переменными, "s" - окно стека, "l" - окно локальных переменных, "x" - окно регистров Pentium III. В часть окон можно перейти командой Alt+"мнемоническое обозначение окна". Перейти можно в каждое такое окно, выяснить это, как Вы понимаете, достаточно просто. Обратный переход осуществляется той же командой. Для создания соответствующего окна, если оно отсутствует, используются команды, в которых первым идет символ "w", вторым символом является символ, обозначающий окно. Например, если у Вас нет на экране окна данных, то создать его можно командой wd, та же команда удалит окно данных из перечня показываемых окон. Высота окна определяется параметром данной команды, например wc 20. Запомните еще несколько полезных команд. Ctrl+"стрелка вверх-вниз" - скроллинг окна кода, Alt+"стрелка вверх-вниз" - скроллинг окна данных, Alt+Ctrl "стрелка вверх-вниз, вправо-влево" - передвижение самого окна отладчика.
  4. Просмотр кода и данных. Один способ передвижения, а значит и просмотра, Вы уже знаете: скроллинг окна кода осуществляется клавишами Ctrl+<стрелка вниз, стрелка вверх> (Alt для окна данных). Наиболее удобной является команда "U". Общий формат этой команды: U [address [length]] | [name]. Address - адрес непосредственный или определяемый через регистр, length - число выводимых байт, name - осуществлять скроллинг, пока не встретится данное имя. Например, u ebx -20 - вывести инструкции, начиная с адреса за 20 байт до адреса CS:EBX. Пустая команда u выводит коды, начиная с CS:EIP. Если вторым параметром идет l, то вывод осуществляется в командное окно. Аналогично для окна данных работает команда D, формат которой имеет вид: D [address], [length]. Например, d 100, 100 или d eax. Положим, видя команду типа MOV EAX,[EBX-10], мы можем посмотреть область данных, откуда берется значение EAX: d ebx-10.
  5. Просмотр - модификация регистров. Переход к окну регистров, как уже было сказано, может быть осуществлено командой Alt+r. Находясь в окне. Вы можете вместе с тем менять содержимое регистров. К аналогичному результату можно прийти, просто выполнив в командной строке команду "R". Возможна также команда вида r reg=знач. Например, r еах=10. Ключ -d позволяет выводить содержимое регистров в командное окно. Команда r есх переводит курсор прямо к нужному регистру, a r fl - прямо к регистру флагов. Команда вида r fl=o+a-p — устанавливает флаги "o" и "a" и сбрасывает флаг "p".
  6. Трассировка кода. Горячие клавиши: F8 - выполнение инструкции с заходом в процедуру, F10 - выполнение инструкции с обходом процедуры, F7 - выполнить инструкции до текущей команды (если Вы находитесь в окне кода). Нажатие клавиши F7 эквивалентно команде HERE. Клавиша F12 - выполнять код, пока не встретится команда RET (команда P с параметром RET). F11 - вернуться к последней выполненной команде CALL. В командной строке Вы можете использовать команду T, которая имеет следующий формат: Т [=address] [count]. Здесь address - адрес, с которого начинается выполнение, count - количество шагов.
  7. Точки останова. Самое мощное и удобное средство отладки программы SoftIce.
    1. Обычные точки останова. Список точек останова можно просмотреть при помощи команды BL. Каждой точке останова ставится в соответствие номер, который Вы увидите в списке. Удалить точку останова из списка можно командой ВС <номер или список>. Все точки останова можно удалить командой ВС *. Находясь в окне кода, Вы можете поставить точку останова, нажав клавишу F9, строка при этом будет подсвечена. Повторное нажатие клавиши F9 снимает точку останова с текущей строки. Команда BD снимает точку останова, но не удаляет ее из списка, команда BE вновь активизирует точку останова. Поставить точку останова можно также командой bpx addess.
    2. Точки останова на функции API. Например, bpx MessageBoxA поставит точку останова на любой вызов данной функции. Для того чтобы проверить, распознает отладчик функции данной группы, выполните команду exp MessageBox, при этом в командное окно будет выведен соответствующий список.
    3. Точки останова с условием. Общий формат команды bpx имеет вид: BPX [address] [if expression] [do "command1; command2.."]. Таким образом, точка останова сработает только при выполнении условия. При этом будет выполнена последовательность команд отладчика. Например, возможно такое выражение: BPX eip if eax=10 do "db bx".
    4. Точки останова на сообщения. Точку останова на сообщения можно поставить при помощи команды bmsg. При этом надо знать дескриптор окна. Но на это есть специальная команда hwnd, которая дает список окон, их дескрипторов и адресов их процедур. Например, BMSG 0b0f wm_destroywindow означает установить точку останова на сообщение WM_DESTROYWTNDOW, приходящее на окно с дескриптором 0b0fh. Поскольку сообщения представляют собой определенные целые числа, можно через пробел указывать диапазон сообщений. Кроме того, точка останова может быть дополнена условием (if) и набором команда (do).
  8. Другие команды. Команд в SoftIce неисчислимое количество, точнее около 200. Вы можете посмотреть описание их в документации, которая прилагается к пакету, либо выполнив в командной строке команду h(F1). Поражает обилие команд для получения системной информации. Думаю, что во всех этих возможностях Вы разберетесь сами.

Примеры использования программ W32Dasm и SoftIce вы найдете в последующих главах.

55 Автору не известно о возможностях использования отладчика в Windows 2000 либо новых версиях, обладающих такими возможностями.


Часть IV. Глава 5. Основы анализа кода программ

Глава 5. Основы анализа кода программ

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

I

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

Для примера я взял простую консольную программу, написанную на Borland C++. В текстовом варианте программа занимает полтора десятка строк, тогда как ЕХЕ-файл имеет размер более 50 Кб. Впрочем, размер исполняемых файлов давно уже никого не удивляет. Интересно другое: корректно справился с задачей, т.е. корректно выявил точку входа - метку _main, только один дизассемблер - IDA PRO. Т.е., конечно, реально работающий участок программы дизассемблировали все, но выявить, как происходит переход на участок, смог только упомянутый мной дизассемблер. Приятно также и то, что аккуратно была распознана функция _printf. На Рис. 4.5.1 показан фрагмент дизассемблированной программы, соответствующей основной процедуре main. С другой стороны, в данном случае нет никаких видимых возможностей быстрого поиска данного фрагмента в отладчике. Осюда наглядно можно понять полезность совместного использования отладчика и дизассемблера.

CODE 00401108 _main proc near ; DATA XREF: DATA:0040B044
CODE 00401108
CODE 00401108 argc      = dword ptr 8
CODE 00401108 argv      = dword ptr 0Ch
CODE 00401108 envp      = dword ptr 10h
CODE 00401108
CODE 00401108        push ebp
CODE 00401109        mov ebp, esp
CODE 0040110B        push ebx
CODE 0040110C        mov edx, offset unk_40D42C
CODE 00401111        xor eax, eax
CODE 00401113
CODE 00401113 loc_401113: ; CODE XREF: _main+22
CODE 00401113        mov ecx, 1Fh
CODE 00401118        sub ecx, eax
CODE 0040111A        mov ebx, ds:off_40B074
CODE 00401120        mov cl, [ebx+ecx]
CODE 00401123        mov [edx+eax], cl
CODE 00401126        inc eax
CODE 00401127        cmp eax, 21h
CODE 0040112A        jl   short loc_401113
CODE 0040112C        mov byte ptr [edx+20h], 0
CODE 00401130        push edx      ; char
CODE 00401131        push offset aS ;_va_args
CODE 00401136        call _printf
CODE 0040113B        add esp, 8
CODE 0040113E        pop ebx
CODE 0040113F        pop ebp
CODE 00401140        retn
CODE 00401140 _main     endp

Рис. 4.5.1. Функция main консольного приложения. Дизассемблер с программы на Си, IDA PRO.

Попытаемся теперь понять, какая программа на Си была исходным источником данного фрагмента. Начнем с рассмотрения стандартных структур. Собственно, налицо только одна структура - цикл. Команда

CODE:0040112A jl short loc_401113
и является ключевой в организации цикла. Команда же inc eax, очевидно, инкрементирует параметр цикла. Т.о. в EAX хранится некая переменная, играющая роль параметра цикла. Назовем эту переменную i. Вышесказанное подтверждается и наличием команды xor eax,eax перед началом цикла. Команда эта, разумеется, эквивалентна просто i=0. Команда inc eax означает просто i++. Попытаемся выявить еще переменные. Обратим внимание на команду
CODE:0040110C mov edx, offset unk_40D42C
Команда весьма примечательна, т.к. в регистр EDX помещается некий адрес, адрес чего-то. Проследим далее, как используется регистр EDX. Команда
CODE:00401123 mov [edx+eax], cl
а также отсутствие в цикле других команд с регистром EDX убеждает нас, что EDX играет роль указателя на строку, массив или запись. Это нам предстоит сейчас выяснить. Тут примечательны две команды:
CODE:0040112C mov byte ptr [edx+20h],0
и
CODE:00401130        push edx     ; char

Первая команда убеждает нас, что EDX указывает на строку, т.к. именно строка должна содержать в конце символ 0. Вторая команда является посылкой второго для функции printf. Исходя из этого, а также комментария IDA PRO (она раньше нас поняла, что это такое), заключаем, что EDX являет собой указатель на некую строку. Заметьте, что мы не обратились к просмотру блока данных, что, несомненно, ускорило бы заключение. Обозначим этот указатель как s1. В этой связи выражение [edx+eax] можно трактовать как s1[i] или как *(s1+i). Рассмотрим теперь команду

CODE:0040111A    mov ebx, ds:off_40B074

несомненно, означающую, что и регистр EBX также представляет собой указатель на строку (будем обозначать ее s2), подтверждением этому также служат последующие строки, означающие переброску символов из строки s2 в строку s1. Вот об этом следует поговорить подробнее. Что означает последовательность команд

CODE:00401113      mov ecx, 1Fh
CODE:00401118      sub ecx, eax

Значить она может только одно: на каждом шаге цикла в ECX будут находиться числа от 1FH (31) до 0 (последним значением регистра EAX, то бишь переменной i, участвующим в команде sub ecx,eax будет 1FH). Поскольку в формировании содержимого регистра ECX участвует еще команда mov cx,1FH, логично предположить, что в регистре ECX перед операцией перекачки символов из одной строки в другую всегда будет находиться число 1FH-i или 31-i. Выражение [ebx+ecx] тогда будет эквивалентно s2[31-i] или *(s2+31-i). В результате имеем, что строки

CODE:00401120        mov cl, [ebx+ecx]
CODE:00401123        mov [edx+eax], cl
по сути, можно заменить на s1[i]=s2[31-i]. Думаю, что теперь мы готовы рассмотреть целый фрагмент:
CODE 00401111        xor eax, eax
CODE 00401113
CODE 00401113 loc_401113: ; CODE XREF: _main+22
CODE 00401113        mov ecx, 1Fh
CODE 00401118        sub ecx, eax
CODE 0040111A        mov ebx, ds:off_40B074
CODE 00401120        mov cl, [ebx+ecx]
CODE 00401123        mov [edx+eax], cl
CODE 00401126        inc eax
CODE 00401127        cmp eax, 21h
CODE 0040112A        jl   short loc_401113

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

i=0;
do {
  s1[i]=s2[31-i] ;
  i++;
} while(i>0x20)

Все бы хорошо, но не понятным остается наличие в цикле строки

CODE:0040111A  mov ebx, ds:off_40B074

Почему бы ни поместить, ее, например, перед циклом. Ну что ж, оставим это на совести компилятора. Что касается того, что цикл этот должен быть обязательно циклом DO, то это совсем не обязательно. В данном случае количество шагов цикла задано явно, и можно сделать один условный переход в конце цикла. Другими словами, приведенная выше структура могла быть результатом оптимизационного преобразования цикла WHILE.

Имеется и еще один вопрос: где хранятся строки s1 и s2? Здесь можно разобраться достаточно быстро.

main является самой обычной процедурой, и если бы строки являлись локальными переменными, то для них была бы зарезервирована область в стеке процедуры путем вставки команды SUB ESP,N (или ADD ESP,-N).

Таким образом, строковые переменные являются глобальными.

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

Итак, окончательный результат наших изысканий может быть представлен на Рис. 4.5.2.

char s1[32];
char * s2="абвгдежзийклмнопрстуфхцчшщыьъэюя";
void main()
{
  int i;
  i=0;
  do {
    s1[i]=s2[31-i];
    i++;
  } while(i>32);
  s1[32]=0;
  printf("%s\n",s1);
}

Рис. 4.5.2. Окончательный вариант программы на Си.

Ну, уж чтобы совсем покончить с рассматриваемым примером, отмечу, что переменная s1 является неинициализированной, а переменная s2 - инициализированной. Инициализированные переменные хранятся в секции DATA, неинициализированные - в BSS (компилятор Borland C++).

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

II

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

1. Условные конструкции.

Неполная условная конструкция.

if (простое условие)
    {
         ...
         ...
         ...
    }
если условие простое, то оно, разумеется, заменяется следующей возможной последовательностью
    CMP EAX,1
    JNZ L1
    ...
    ...
    ...
L1:

Полная условная конструкция.

if (простое условие)
    {
         ...
         ...
         ...
    } else
    {
         ...
         ...
         ...
    }

    CMP EAX,1
    JNZ L1
    ...
    ...
    ...
    JMP L2
L1:
    ...
    ...
    ...
L2:

Вложенные условные конструкции.

Здесь все достаточно очевидно.

    CMP EAX,1
    JNZ L1
    CMP EBX,2
    JNZ L1
    ...
    ...
    ...
L1:

Что, конечно, равносильно одному составному условию, связанному союзом "И". Союз "ИЛИ", как известно, заменяется проверкой условий в блоке "ELSE".

2. Оператор switch или оператор выбора.

Оператор switch весьма часто употребляется в функциях окон. Хорошее знание его ассемблерной структуры поможет Вам легче отыскивать эти функции в море ассемблерного кода.

  switch(i)
    {
      case 1:
      ...
      ...
      ...
      break;
      case 3:
      ...
      ...
      ...
      break;
      case 5:
      ...
      ...
      ...
      break;
    }

А вот соответствующий данной структуре ассемблерный код.

  DEC EAX
  JZ L1
  SUB EAX,2
  JZ L2
  SUB EAX,2
  JZ L3
  JMP L4
L1:
  ...
  ...
  ...
  JMP L4
L2:
  ...
  ...
  ...
  JMP L4
L3:
  ...
  ...
  ...
L4:

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

  CMP EAX,10
  JE L1
  CMP EAX,5
  JE L2
  CMP EAX,11
  JE L3
  ...

3. Циклы.

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

1-я структура.

L1:
   ...
   ...
   ...
   CMP EAX,10
   JL L1

2-я структура.

L2:
  CMP EAX,10
  JA L1
  ...
  ...
  ...
  JMP L2
L1:

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

    JMP L2
L1:
    ...
    ...
    ...
    CMP EAX,10
    JL L1
L2:

Как по мановению волшебной палочки, в последней структуре цикл "ДО" превращается в цикл "ПОКА".

В своем рассмотрении я не упомянул цикл "FOR" - этого "монстра" языка Си, но, в принципе, это все тот же цикл "ПОКА".

4. Локальные переменные.

Чаще всего нам приходится работать с локальными переменными. С глобальными переменными мы уже встречались, они хранятся во вполне определенных секциях. Хороший дизассемблер их легко локализует. С локальными переменными часто разбираться гораздо сложнее. Особенно это касается записей или массивов. Хороший дизассемблер значительно упростит задачу. Рассмотрим, например, следующий фрагмент, взятый из IDA PRO.

CODE 00401108 _main proc near ; DATA XREF: DATA:0040B044
CODE 00401108
CODE 00401108 var_54     = dword ptr -54h
CODE 00401108 var_28     = byte ptr -28h
CODE 00401108 argc      = dword ptr 8
CODE 00401108 argv      = dword ptr 0Ch
CODE 00401108 envp      = dword ptr 10h
CODE 00401108
CODE 00401108        push ebp
CODE 00401109        mov ebp, esp
CODE 0040110B        add esp, 0FFFFFFACh
CODE 0040110E        push ebx
CODE 0040110F        xor ebx, ebx

Рис. 4.5.3. Пример задания двух локальных массивов. Взят из отладчика IDA PRO.

Взгляните внимательно на Рис. 4.5.3. Как видите, отладчик нам все прописал. Две переменные var_54 и var_28 являются, несомненно, массивами типа DWORD. Причем если на первый отводится 28h байт, т.е. 40 байт или 10 элементов массива, то на второй 54h-28h=2CH=44 или 11 элементов массива. И всего, следовательно, под локальные переменные зарезервировано 84 байта. А что означает команда add esp,0FFFFFFACH ? Но нас не обманешь! 0-0FFFFFFACH = 54h, что в десятичном исчислении и есть 84. В связи с массивами хотелось бы упомянуть, что в Си изначально практиковалось два способа доступа к элементам массива: посредством индексов и посредством указателей. Другими словами, можно было написать: a[i]=10 и *(a+i)=10. Причем вторая запись оказывалась более эффективной (см. по этому поводу книгу [1], гл. 15.). Разумеется, делать это можно и сейчас, но обе записи теперь в Borland C++ и Microsoft C++ 6.0 приводят к совершенно одинаковым ассемблерным эквивалентам. Что весьма отрадно - значит, компиляторы действительно развиваются. Кстати, весьма поучительным было бы сравнение ассемблерного кода, производимого разными компиляторами. Мы не будем этим заниматься, замечу только, что мое весьма поверхностное сравнение компиляторов Borland C++ 5.0 и Visual C++ 6.0 привело к выводу, что средства, разработанные фирмой Microsoft, несколько более эффективно оптимизируют транслируемый код.

Но вернемся опять к фрагменту на Рис. 4.5.3. Посмотрим, как выглядит начало функции main в дизассемблере W32Dasm.

:00401108 55           push ebp
:00401109 8ВЕС          mov ebp, esp
:0040110B 83C4AC         add esp, FFFFFFAC
:0040110E 53           push ebx
:0040110F 33DB          xor ebx, ebx

Рис. 4.5.4. Как выглядит фрагмент программы, представленный на Рис. 4.5.3 в окне дизассемблера W32Dasm.

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

5. Функции и процедуры.

Функции и процедуры идентифицируются достаточно просто. Структуру вызова и внутреннюю часть процедур Вы уже хорошо знаете. Остается только напомнить некоторые положения. Вызов процедуры:

   PUSH par1
   PUSH par2
   PUSH par3
   CALL 232343

Здесь все достаточно просто. Главное - распознать параметры и понять порядок помещения их в стек. Надо также иметь в виду, что существует протокол передачи параметров через регистры (см. главу 3.7). После вызова процедуры может стоять команда очистки стека ADD ESP,N.

Внутренняя часть процедуры также нами неоднократно разбиралась (см. Гл. 1.2, 3.7). Думаю, что она достаточно Вами узнаваема, и мы не будем здесь на этом останавливаться.

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

6. Оптимизация кода.

Выше мы пытались восстановить исходный текст программы по ассемблерному коду. Насколько это удалось, можно выяснить, сравнив получившийся текст с исходным. Следует заметить, что мы ошиблись: в исходном тексте стоит цикл while, а в нашем do. Однако, если откомпилировать получившуюся программу, она будет работать, так же как и исходная. Причина отличия двух текстов программ (весьма простых, кстати) заключается в том, что транслятор, кроме всего прочего, производит еще оптимизацию кода. Результат оптимизации - принципиальная невозможность восстановить исходный текст по исполняемому коду. Можно, однако, получить программный текст, правильно описывающий алгоритм исполнения. Можно просто понять то, что делает данный код, чем мы в данной главе и занимаемся.

Рассмотрим небольшую и весьма тривиальную программу на Си.

void main ()
{
    int i;
    char a[10];
    char b[11];
    for(i=0; i<10; i++)
    {
      a[i]='a';
      *(b+i)='a';
      printf("%c %c\n",a[i],b[i]);
    }
    ExitProcess(0);
}

Рис. 4.5.5.

Вот как выглядит ассемблерный код, полученный с помощью транслятора Borland C++ 5.0.

.text 00401108 _main proc near ; DATA XREF: .data:0040A0B8
.text 00401108
.text 00401108 var_18     = byte ptr -18h
.text 00401108 var_C     = byte ptr -0Ch
.text 00401108 argc      = dword ptr 8
.text 00401108 argv      = dword ptr 0Ch
.text 00401108 envp      = dword ptr 10h
.text 00401108
.text 00401108        push ebp
.text 00401109        mov ebp, esp
.text 0040110B        add esp, 0FFFFFFE8h
.text 0040110E        push ebx
.text 0040110F        xor ebx, ebx
.text 00401111
.text 00401111 loc_401111: ; CODE XREF: _main+30
.text 00401111        mov [ebp+ebx+var_C], 61h
.text 00401116        mov [ebp+ebx+var_18], 61h
.text 0040111B        movsx eax, [ebp+ebx+var_18]
.text 00401120        push eax
.text 00401121        movsx edx, [ebp+ebx+var_C]
.text 00401126        push edx      ; char
.text 00401127        push offset aCC ; _va_args
.text 0040112C        call _printf
.text 00401131        add esp, 0Ch
.text 00401134        inc ebx
.text 00401135        cmp ebx, 0Ah
.text 00401138        jl   short loc_401111
.text 0040113A        push 0 ; uExitCode
.text 0040113C        call ExitProcess
.text 00401141        pop ebx
.text 00401142        mov esp, ebp
.text 00401144        pop ebp
.text 00401145        retn
.text 00401145 _main     endp

Рис. 4.5.6. Дизассемблированный текст программы (Рис. 4.5.5). Транслятор Borland C++ 5.0.

Кстати, функция ExitProcess(0) введена в текст программы для быстрого поиска нужного фрагмента в отладчике или дизассемблированном коде. Сразу видно, что оптимизацией здесь и не пахло. По такому тексту достаточно просто восстановить исходную Си-программу. А вот код, оптимизированный транслятором Visual C++ 6.0 (Рис. 4.5.7).

.text 00401000 sub_401000 proc near ; CODE XREF: start+AF
.text 00401000        push esi
.text 00401001        mov esi, 0Ah
.text 00401006
.text 00401006 loc_401006: ; CODE XREF: sub_401000+18
.text 00401006        push 61h
.text 00401008        push 61h
.text 0040100A        push offset aCC ; "%c %c\n"
.text 0040100F        call sub_401030 ;printf
.text 00401014        add esp, 0Ch
.text 00401017        dec esi
.text 00401018        jnz short loc_401006
.text 0040101A        push 0 ; uExitCode
.text 0040101C        call ds:ExitProcess
.text 00401022        pop esi
.text 00401023        retn
.text 00401023 sub_401000   endp

Рис. 4.5.7. Дизассемблированный текст программы (Рис. 4.5.5). Транслятор Visual C++ 6.0.

Я думаю, что текст на Рис. 4.5.7 удивит Вас. Однако, что же здесь удивительного? Взгляните на текст Си-программы. Заданные нами два символьных массива - абсолютно ни к чему. Транслятор Visual C++ очень точно это подметил и изменил код так, что, как ни старайся, текст исходной программы восстановить не удасться. Конечно, такая оптимизация оказалась возможной только потому, что наши массивы используются в ограниченной области.

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

Скорость или объем. Это весьма важный вопрос, когда дело касается микропроцессора.

Например, команда MOV EAX,EBX выполняется быстрее команды XCHG EAX,EBX, но длиннее ее. Зная такую особенность, можно либо сокращать объем, либо увеличивать скорость программы. Особенно часто используется замена такой операции, как MUL, другими командами, в частности SHL. Это может значительно увеличить скорость выполнения программы и увеличить ее объем. Интересно, что арифметические действия можно производить и с помощью команды LEA (см. Приложение 2), и современные трансляторы уже взяли это на вооружение. Так что команда MUL не так часто может встретиться в оттранслированном коде, как об этом можно подумать, исходя из текста программы. Вообще, свойство команд надо исследовать, и иногда узнаешь довольно интересные вещи. Например, нахождение остатка от деления числа без знака на 4 производится следующим образом: AND EAX,0003h - не правда ли, оригинально?

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

   ...
   CMP EAX, 100
   JB L1
   Фрагмент 1
   JMP L2
L1:
   Фрагмент 2
L2:

Мы знаем, что содержимое EAX чаще всего оказывается меньше 100. Следовательно, фрагмент можно заменить на следующий.

   ...
   CMP EAX,100
   JNB L1
   Фрагмент 2
   JMP L2
L1:
   Фрагмент 1
L2:

Оптимизация вызовов процедур. Рассмотрим следующий фрагмент.

P1 PROC
...
...
...
CALL P2
RET
P1 ENDP
...
...
...
P2 PROC
...
...
...
RET
P2 ENDP

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

Р1 PROC
...
...
...
JMP P2
Р1 ENDP
...
...
...
P2 PROC
...
...
...
RET
P2 ENDP

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

6. Объектное программирование. Объектное программирование в значительной степени усложняет получаемый исполняемый код. Мы рассмотрим один простой пример. На Рис. 4.5.7 представлена программа на C++.

#include <windows.h>
#include <stdio.h>
class stroka {
  public:
    char с[200];
    char g[100];
    stroka() { strcpy(c,"Privet"); strcpy(g,"Privet");}
    int strcp();
};

stroka::stri()
{
    strcpy(c,g);
    return 0;
}

main()
{
    stroka * s = new stroka;
    s->strcp();
    printf("%s\n",s->c);
    delete s;
}

Рис. 4.5.7. Программа на Си++, использующая объекты.

Ниже на Рис. 4.5.8. представлен дизассемблированный код процедуры main.

CODE 00401122 _main proc near ; DATA XREF: DATA:0040C044
CODE 00401122
CODE 00401122 var_28     = dword ptr -28h
CODE 00401122 var_18     = word ptr -18h
CODE 00401122 dest      = dword ptr -4
CODE 00401122 argc      = dword ptr 8
CODE 00401122 argv      = dword ptr 0Ch
CODE 00401122 envp      = dword ptr 10h
CODE 00401122
CODE 00401122        push ebp
CODE 00401123        mov ebp, esp
CODE 00401125        add esp, 0FFFFFFD8h
CODE 00401128        push ebx
CODE 00401129        mov eax, offset stru_40C084
CODE 0040112E        call @_InitExceptBlockLDTC
CODE 00401133        push 12Ch
CODE 00401138        call unknown_libname_8
CODE 0040113D        pop ecx
CODE 0040113E        mov [ebp+dest], eax
CODE 00401141        test eax, eax
CODE 00401143        jz   short loc_40117D
CODE 00401145        mov [ebp+var_18 ], 14h
CODE 0040114B        push offset aPrivet ; src
CODE 00401150        push [ebp+dest]   ; dest
CODE 00401153        call _strcpy
CODE 00401158        add esp, 8
CODE 0040115B        push offset aPrivet_0 ; src
CODE 00401160        mov edx, [ebp+dest]
CODE 00401163        add edx, 0C8h
CODE 00401169        push edx      ; dest
CODE 0040116A        call _strcpy
CODE 0040116F        add esp, 8
CODE 00401172        mov [ebp+var_18], 8
CODE 00401178        mov ebx, [ebp+dest]
CODE 0040117B        jmp short loc_401180
CODE 0040117D ; ------------------------------------
CODE 0040117D
CODE 0040117D loc_40117D:   ; CODE XREF: _main+21
CODE 0040117D        mov ebx, [ebp+dest]
CODE 00401180
CODE 00401180 loc_401180: ; CODE XREF: _main+59
CODE 00401180        push ebx      ; dest
CODE 00401181        call sub_401108
CODE 00401186        pop ecx
CODE 00401187        push ebx      ; char
CODE 00401188        push offset aS ; _va_args
CODE 0040118D        call _printf
CODE 00401192        add esp, 8
CODE 00401195        push ebx      ; block
CODE 00401196 call @$bdele$qpv ; operator delete(void *)
CODE 0040119B        pop ecx
CODE 0040119C        mov eax, [ebp+var_28]
CODE 0040119F        mov large fs:0, eax
CODE 004011A5        xor eax, eax
CODE 004011A7        pop ebx
CODE 004011A8        mov esp, ebp
CODE 004011AA        pop ebp
CODE 004011AB        retn
CODE 004011AB _main     endp

Рис. 4.5.8. Дизассемблированный код функции main с Рис. 4.5.7. Дизассемблер IDA PRO.

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

  1. Оператор new сводится к выполнению библиотечной процедуры: unknown_libname_8. Последняя процедура выделяет память для свойств экземпляра объекта (300 байт).
  2. Конструктор хранится и выполняется непосредственно в теле программы. Это связано с тем, что и сам конструктор определен в тексте класса. Для эксперимента попробуйте вынести текст конструктора в отдельную функцию, и Вы увидите, что конструктор будет вызываться из main, как обычная функция.
  3. Процедуру @_InitExceptBlockLDTC вставляет транслятор для поддержки так называемых исключений (Exception). Вы можете удалить информацию для использования исключений, несколько сократив получившийся код, но тогда Вы не сможете использовать операторы исключений, такие как try или catch.
  4. При вызове какого-либо метода в стек всегда помещается, по крайней мере, один параметр - указатель на экземпляр объекта.

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

.text 00401122 _main proc near ; DATA XREF: .data:0040B0C8
.text 00401122
.text 00401122 argc      = dword ptr 8
.text 00401122 argv      = dword ptr 0Ch
.text 00401122 envp      = dword ptr 10h
.text 00401122
.text 00401122        push ebp
.text 00401123        mov ebp, esp
.text 00401125        push ebx
.text 00401126        push 12Ch
.text 0040112B        call unknown_libname_8
.text 00401130        pop ecx
.text 00401131        mov ebx, eax
.text 00401133        test eax, eax
.text 00401135        jz   short loc_40115D
.text 00401137        push offset aPrivet ; src
.text 0040113C        push ebx      ; dest
.text 0040113D        call _strcpy
.text 00401142        add esp, 8
.text 00401145        push offset aPrivet_0 ; src
.text 0040114A        lea edx, [ebx+0C8h]
.text 00401150        push edx      ; dest
.text 00401151        call _strcpy
.text 00401156        add esp, 8
.text 00401159        mov ecx, ebx
.text 0040115B        jmp short loc_40115F
.text 0040115D ;-----------------———-—----—-—---———
.text 0040115D
.text 0040115D loc_40115D: ; CODE XREF: _main+13
.text 0040115D        mov ecx, ebx
.text 0040115F
.text 0040115F loc_40115F: ; CODE XREF: _main+39
.text 0040115F       mov ebx, ecx
.text 00401161        push ebx      ; dest
.text 00401162        call sub_401108
.text 00401167        pop ecx
.text 00401168        push ebx      ; char
.text 00401169        push offset aS ; _va_args
.text 0040116E        call _printf
.text 00401173        add esp, 8
.text 00401176        push ebx    ; handle
.text 00401177        call _rtl_close
.text 0040117C        pop ecx
.text 0040117D        xor eax, eax
.text 0040117F        pop ebx
.text 00401180        pop ebp
.text 00401181        retn
.text 00401181 _main     endp

Рис. 4.5.8. Дизассемблированный код функции main с Рис. 4.5.7, при трансляции использована опция -x (исключить исключения, Borland C++). Дизассемблер IDA PRO.


Часть IV. Глава 6. Исправление исполняемых модулей

Глава 6. Исправление исполняемых модулей

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

I

Сейчас мы рассмотрим простой57 пример, демонстрирующий некоторые приемы такого типа работы. Задача, которую ставим перед собой, не так сложна, и решить ее можно, воспользовавшись только дизассемблером W32Dasm.

Данная программа (Allscreen - программа, с помощью которой можно "снимать" окна и отдельные части экрана) попала ко мне как Shareware Release. Программа написана на Delphi, но мы увидим, что решить поставленную задачу можно, и не зная, на чем написана программа. При запуске ее на экране появляется окно, изображенное на Рис. 4.6.1. Ближе познакомившись с предметом, Вы убедитесь, что чаще всего приходится искать место в программе, соответствующее какому-либо визуальному эффекту: открытие окна, закрытие окна, вывод текста и т.п.

Рис. 4.6.1. Окно, появляющееся при запуске программы Allscreen.

При выборе кнопки "Accept" возникает задержка секунд в шесть (см. Рис. 4.6.2.). Далее программа работает нормально.

Рис. 4.6.2. Окно задержки.

После 15-ти запусков появляется окно Рис. 4.6.3 и происходит выход из программы.

Рис. 4.6.3. Сообщение об истечении времени работы программы.

Таким образом, следует решить две проблемы:

  1. Устранить весьма раздражающую задержку.
  2. Сделать так, чтобы программа работала при любом количестве запусков.

Рис. 4.6.2 являет собой явный "прокол" авторов программы. Дело в том, что окно и все его содержимое можно спрятать в ресурсы. Но когда на том же окне появляется новая запись - это уже программный код. Итак, запускаем W32Dasm и считываем туда программу All Screen. Запускаем окно SDR (String Data Reference), ищем строку Shareware Delay, дважды щелкаем по ней и, закрыв его, оказываемся в нужном месте программы. Вот этот фрагмент (Рис. 4.6.4.).

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:004420BC(C)
|
:00442123 33D2      xor edx, edx
:00442125 8B83B0010000 mov eax, dword ptr [ebx+000001B0]
:0044212B E8541DFDFF   call 00413E84
:00442130 33D2      xor edx, edx
:00442132 8B83B4010000 mov eax, dword ptr [ebx+000001B4]
:00442138 E8471DFDFF   call 00413E84
:0044213D 33D2      xor edx, edx
:0044213F 8B83B8010000 mov eax, dword ptr [ebx+000001B8]
:00442145 E83A1DFDFF   call 00413E84
:0044214A BA50000000   mov edx, 00000050
:0044214F 8B83BC010000 mov eax, dword ptr [ebx+000001BC]
:00442155 E8D618FDFF   call 00413A30

* Possible StringData Ref from Code Obj ->"Shareware Delay"

:0044215A BAA8214400   mov edx, 004421A8
:0044215F 8B83BC010000 mov eax, dword ptr [ebx+000001BC]
:00442165 E8EE1DFDFF   call 00413F58
:0044216A 33D2      xor edx, edx
:0044216C 8B83C0010000 mov eax, dword ptr [ebx+000001C0]
:00442172 E80D1DFDFF   call 00413E84
:00442177 33D2      xor edx, edx
:00442179 8B83C4010000 mov eax, dword ptr [ebx+000001C4]
:0044217F E8001DFDFF   call 00413E84
:00442184 33D2      xor edx, edx
:00442186 8B83C8010000 mov eax, dword ptr [ebx+000001C8]
:0044218C E8F31CFDFF   call 00413E84
:00442191 8B83CC010000 mov eax, dword ptr [ebx+000001CC]
:00442197 E8E8D4FFFF   call 0043F684
:0044219C 5B       pop ebx
:0044219D C3       ret

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

Я сразу взял несколько больше кода, захватив и несколько верхних строк. По сути дела, перед нами вся процедура задержки. Нет смысла пытаться понять, что означает та или иная команда call, хотя легко сообразить, что, например, call 00413E84 убирает строку с экрана.

Для того чтобы решить проблему задержки, достаточно "выключить" этот фрагмент из программы. Проще всего это можно сделать, поставив в начало команды pop ebx / ret, используя такой редактор, как HIEW. В результате задержка исчезает.

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

:00443326 8ВС0      mov eax, eax
:00443328 53       push ebx
:00443329 8BD8      mov ebx, eax
:0044332B 803DEC56440001 cmp byte ptr [004456EC], 01
:00443332 7546      jne 0044337A
:00443334 A124564400   mov eax, dword ptr [00445624]
:00443339 E84E2CFEFF   call 00425F8C
:0044333E A1D8564400   mov eax, dword ptr [004456D8]
:00443343 E87816FEFF   call 004249CO
:00443348 FF05F0564400 inc dword ptr [004456F0]
:0044334E C605EC56440000 mov byte ptr [004456EC], 00
:00443355 833DF05644000F cmp dword ptr [004456FO], 0000000F
:0044335C 7E1C      jle 0044337A
:0044335E 6AOO      push 00000000
:00443360 668BODB0334400 mov cx, word ptr [004433B0]
:00443367 B202      mov dl, 02

* Possible StringData Ref from Code Obj ->"This Software Has Been Used Over"
                      |
:00443369 B8BC334400   mov eax, 004433BC
:0044336E E8BDAEFEFF   call 0042E230
:00443373 8BC3      mov eax, ebx
:00443375 E84214FEFF   call 004247BC

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
|:00443332(C), :0044335C(C)
:0044337A 33D2      xor edx, edx
:0044337C 8B83F4010000 mov eax, dword ptr [ebx+000001F4]
:00443382 E8A52DFFFF   call 0043612C
:00443387 33D2      xor edx, edx
:00443389 8B83F8010000 mov eax, dword ptr [ebx+000001F8]
:0044338F E8982DFFFF   call 0043612C
:00443394 33D2      xor edx, edx
:00443396 8B83FC010000 mov eax, dword ptr [ebx+000001FC]
:0044339C E88B2DFFFF   call 0043612C
:004433A1 33D2      xor edx, edx
:004433A3 8B8314020000 mov eax, dword ptr [ebx+00000214]
:004433A9 E87E2DFFFF   call 0043612C
:004433AE 5B       pop ebx
:004433AF C3       ret

Рис. 4.6.5. Фрагмент кода проверки количества запусков.

Опять мы представляем весь необходимый фрагмент. Найденной выше ссылкой на искомую строку легко обнаруживаем "подозрительные" команды:

     cmp dword ptr [004456F0], 0000000F
    jle 0044337A

Вспомним, что программа перестает работать как раз после пятнадцати запусков. Проще всего исправить ситуацию, "забив" фрагмент программы с 0044335E по 00443375 командами NOP (90), используя редактор HIEW.

II

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

Для примера я взял программу FILES.EXE. Эта поисковая программа очень удобна для поиска информации в локальной сети. Выберем, например, одно из диалоговых окон данной программы и поставим цель найти функцию этого диалогового окна. Сделаем это двумя способами: с помощью отладочных средств программы W32Dasm и с помощью отладчика SoftIce. На Рис. 4.6.6. показаны несколько открытых диалоговых окон программы. Последнее диалоговое окно с заголовком "Название сетевого компьютера" и будет объектом нашего внимания.

Рис. 4.6.6. Будем искать функцию последнего диалогового окна.

И так обратимся вначале к программе W32Dasm. Имеется последовательность шагов поиска:

  1. Загрузим туда программу FILES.EXE.
  2. Затем обратимся к пункту Debug\Load Process и выходим в отладчик.
  3. Запускаем программу при помощи клавиши F9 и выходим на нужное окно.
  4. Теперь решающий момент. Нажимаем клавишу "Terminate Process" и снова оказываемся в окне дизассемблера.
  5. Глянув чуть выше, Вы обнаружите вызов API-функции DialogBoxParam. На Рис. 4.6.7 изображен нужный фрагмент.

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:004156BE(C)
|
:004156CA FF7508        push [ebp+08]
:004156CD E8369EFFFF      call 0040F508
:004156D2 59          pop ecx
:004156D3 6A00         push 00000000
:004156D5 68019C4000      push 00409C01
:004156DA FF7508        push [ebp+08]

* Possible StringData Ref from Data Obj ->"DIAL3000"
                |
:004156DD 6896904300      push 00439096
:004156E2 FF3548CA4300     push dword ptr [0043CA48]

* Reference To: USER32.DialogBoxParamA, Ord:0000h
                |
:004156E8 E85FOD0200      call 0043644C
:004156ED 8A153C324D00     mov dl, byte ptr [004D323C]

Рис. 4.6.7. Фрагмент с вызовом функции DialogBoxParam.

Вспомним параметры функции DialogBoxParam. Четвертый параметр как раз представляет собой адрес процедуры окна. Итак, получаем адрес 00409C01. Обратимся к этому адресу (см. Рис. 4.6.8).

:00409C01 55           push ebp
:00409C02 8BEC          mov ebp, esp
:00409C04 8B450C         mov eax, dword ptr [ebp+0C]
:00409C07 2D10010000       sub eax, 00000110
:00409COC 7413          je 00409C21
:00409COE 48           dec eax
:00409COF OF84EB000000      je 00409D00
:00409C15 2D01020000       sub eax, 00000201
:00409C1A 7444          je 00409C60
:00409C1C E969010000       jmp 00409D8A

Рис. 4.6.7. Начало функции окна.

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

Поставим теперь себе цель, найти ту же самую функцию, но отладчиком SoftIce.

  1. Загружаем загрузчиком LOADER32.EXE программу FILES.EXE в отладчик SoftIce.
  2. Далее Ctrl-D, и мы имеем запущенную программу.
  3. Запускаем диалоговые окна, кроме последнего диалогового окна.
  4. Перед тем как вызвать последнее диалоговое окно, снова войдем в отладчик и установим точку останова на функцию DialogBoxParamA:
         ВРХ DialogBoxParamA
  5. Снова входим в программу, запускаем нужное диалоговое окно, при этом происходит автоматический выход в отладчик: сработала точка останова.
  6. Далее нажмем клавишу F11, при этом окажемся в точке вызова либо сразу, либо после закрытия диалогового окна (опять происходит выход в отладчик).
  7. Посмотрев на вызов функции DialogBoxParam, мы легко определим адрес процедуры окна. Разумеется, он совпадает с тем, который мы нашли ранее, с помощью отладчика программы W32Dasm (см. Рис. 4.6.7).

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

   ВРХ EIP IF(EBP+8==N)
здесь N - найденный ранее дескриптор окна. После этого остается только проделать что-то с окном, чтобы на функцию его пришло какое-нибудь сообщение.
57 Простой с точки зрения возможных проблем, возникающих при исправлении исполняемых модулей.


Часть IV. Глава 7. Структура и написание драйверов .VXD

Глава 7. Структура и написание драйверов .VXD

Последнюю главу нашей книги я посящаю написанию виртуальных драйверов. Сокращение VxD следует понимать как Virtual Device Driver, "x" относится к слову Device (имеется в виду любое устройство). Вопрос несколько устаревает, так как в Windows 98 принята на вооружение несколько иная концепция драйверов устройств, а Windows NT никогда не поддерживала VxD-драйверы, а использует модель, которая называется kernel mode (режим ядра). Все же программирование VxD остается актуальным по сей день, и знать основные положения должен каждый программирующий на языке ассемблера. В данной главе, отходя от нашей обычной практики, мы будем интенсивно использовать макроопределения, содержащиеся во включаемых (inc) файлах пакета DDK. Тем самым нам удастся поместить весь материал в одну главу. Кроме того, данная глава ориентирована на работу с MASM32.

I

Для программирования VxD-драйверов нам понадобятся файлы VMM.INC, SHELL.INC, VCOND.INC и др., которые можно найти в пакете DDK (Device Development Kit), свободно распространяемом фирмой Microsoft.

При загрузке Windows программа WIN.COM загружает драйвер VMM32.VXD, называемый Менеджером Виртуальной Машины (Virtual Machine Manager), который инициализирует другие VxD-драйверы. Затем VMM переключает процессор в защищенный режим и инициализирует системную виртуальную машину. Кроме уже сказанного, VMM обеспечивает сервис для других драйверов VXD. При запуске DOS-приложения для него выделяется виртуальная машина, так что приложению "кажется", что оно работает с настоящей машиной. При запуске обычных 32-битных приложений они работают в системной виртуальной машине. Приложения, работающие в разных виртуальных машинах, не подозревают о существовании других виртуальных машин. Одним из главных назначений виртуальных драйверов является обеспечение бесконфликтного коллективного доступа к физической аппаратуре для всех одновременно работающих виртуальных машин. Еще одна задача, которую приходится решать виртуальным драйверам, - это осуществление взаимодействия системной виртуальной машиной с другими виртуальными машинами.

Следует отметить, что в Windows существуют и так называемые обычные драйверы, имеющие расширение DRV и структуру DLL-библиотеки, которые экспортируют API-функции для работы с некоторыми внешними устройствами (например, видеоадаптером). Эти драйверы получают доступ к внешним устройствам не напрямую, а посредством VxD-драйверов. Поскольку драйверы VxD работают в нулевом кольце защиты, они имеют доступ к любому участку памяти и напрямую, посредством портов ввод-вывода, обращаются к внешним устройствам.

Все виртуальные драйверы делятся на два класса - статические и динамические. Статические драйверы загружаются во время загрузки системы и остаются в памяти до выхода из системы. Статические драйверы существовали еще в Windows 3.x. Динамические драйверы могут загружаться и выгружаться системой по мере необходимости. Они в основном используются для обслуживания устройств "Plug and Play" и загружаются менеджером конфигурации. Загрузить динамический виртуальный драйвер можно и из обычного приложения при помощи обычных функций работы с файлами.

Имеется три механизма взаимодействия виртуальных драйверов друг с другом.

  1. Управляющие сообщения. Эти сообщения посылает виртуальным драйверам менеджер виртуальной машины. Драйверы также могут обмениваться информацией посредством таких сообщений. Это очень напоминает сообщения Windows и взаимодействия приложений друг с другом и операционной системой через эти сообщения.
  2. Функции обратного вызова. Виртуальный драйвер может позволить вызов функции обратного вызова другому драйверу.
  3. Виртуальные драйверы и менеджер виртуальной машины могут экспортировать определенные функции для вызова из других виртуальных драйверов. Для вызова функции необходимо знать уникальный номер виртуального драйвера, который экспортирует данную функцию и номер этой функции.

Формат виртуальных драйверов называется LE-форматом, сокращенно от "linear executable". Данный формат поддерживает наличие как 16-битного, так и 32-битного кода. Это актуально для статических виртуальных драйверов, которые часть работы (инициализация) выполняют в реальном (незащищенном) режиме. В Windows NT драйверы грузятся в защищенном режиме, по этой причине данный формат в этой операционной системе не используется.

Код и данные в файле LE-формата располагаются в сегментах. Ниже мы опишем возможные классы сегментов.

LCODE - код или данные, заключенные в этом коде, не могут сбрасываться системой на диск (paging).

PCODE - код может временно помещаться на диск.

PDATE - аналогично предыдущему, но здесь хранятся данные.

ICODE - здесь располагается код инициализации, после инициализации сегмент удаляется из памяти.

DBOCODE - используется при запуске драйвера "под отладчиком".

SCODE - статические код и данные. Всегда остается в памяти, даже если драйвер выгружается.

RCODE — содержит 16-битный код для инициализации в реальном режиме.

16ICODE - 16-битный код инициализации в защищенном режиме.

MCODE - содержит строки сообщений.

Перечисленные классы сегментов не задаются непосредственно в тексте программы. Сегменты и классы объявляются в DEF-файле. Файл vmm.inc содержит огромное количество макросов, и нам не избежать пользоваться ими. Это позволит материал, который я хочу изложить, вместить в одну главу.

II

Начнем с содержимого DEF-файла. Содержимое показано на Рис. 4.7. Здесь перечислены сегменты на все случаи жизни. Вам нет необходимости использовать все определенные здесь сегменты. Т.о. образом данный файл можно использовать при создании любого виртуального драйвера. Сегменты, принадлежащие к одному классу, после компоновки объединяются в один сегмент. Менять следует только первую строку, где задается имя драйвера. Отметим, что имя драйвера следует задавать заглавными буквами. Кроме того, в первой строке можно задать тип драйвера. По умолчанию этот тип статический. Если бы мы записали строку VXD VXD1 DYNAMIC, то компоновщик создал бы динамический виртуальный драйвер.

VXD VXD1
SEGMENTS
_LPTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LTEXT  CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LDATA  CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TEXT   CLASS 'LCODE' PRELOAD NONDISCARDABLE
_DATA   CLASS 'LCODE' PRELOAD NONDISCARDABLE
CONST   CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TLS    CLASS 'LCODE' PRELOAD NONDISCARDABLE
_BSS    CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LMGTABLE CLASS 'MCODE' PRELOAD NONDISCARDABLE IOPL
_LMSGDATA CLASS 'MCODE' PRELOAD NONDISCARDABLE IOPL
_IMSGTABLE CLASS 'MCODE' PRELOAD DISCARDABLE IOPL
_IMSGDATA  CLASS 'MCODE' PRELOAD DISCARDABLE IOPL
_ITEXT   CLASS 'ICODE' DISCARDABLE
_IDATA   CLASS 'ICODE' DISCARDABLE
_PTEXT   CLASS 'PCODE' NONDISCARDABLE
_PMSGTABLE CLASS 'MCODE' NONDISCARDABLE IOPL
_PMSGDATA  CLASS 'MCODE' NONDISCARDABLE IOPL
_PDATA CLASS 'PDATA' NONDISCARDABLE SHARED
_STEXT CLASS 'SCODE' RESIDENT
_SDATA CLASS 'SCODE' RESIDENT
_DBOSTART CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBOCODE  CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBODATA  CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_16ICODE  CLASS '16ICODE' PRELOAD DISCARDABLE
_RCODE    CLASS 'RCODE'
EXPORTS
VXD1_DDB @1

Рис. 4.7.1. Файл VXD.DEF используемый для компоновки виртуального драйвера.

В конце файла указывается единственная экспортируемая переменная — блок описания устройства. DDB - Device Descriptor Block. В этот блок, состоящий из 22 полей, который определен в файле vmm.inc, содержит информацию о виртуальном драйвере (см. ниже).

В файле vmm.inc определены макроимена для всех записанных выше сегментов. Например, для сегмента _LTEXT задано имя VxD_LOCKED_CODE SEG, а для _RCODE имя VxD_REAL_INIT_SEG. Мы будем использовать эти имена в своих программах.

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

   ml /coff /c /Сх /DMASM6 /DBLD_COFF /DIS_32 vxd1.asm
   link /vxd /def:vxd.def vxd1.obj

Константы MASM6, BLD_COFF, IS_32 используются операторами условной трансляции, которые заданы в файлах vmm.inc и vcond.inc. Отметим, что если Вы используете DEF-файл в том виде, как мы его определили на Рис. 4.7.1, то при компоновке будут появляться сообщения об отсутствии той или иной секции, что смело можно проигнорировать.

Для того чтобы выполнить какие-либо действия, нам часто придется использовать макроопределения из файла vmm.inc. Познакомимся с некоторыми из них.

Во-первых, конечно, это Declare_Virtual_Device. Этот макрос заполняет структуру DDB, несколько облегчая нашу задачу. Вот его полный формат (саму структуру макроса, довольно сложную, можно изучить в файле vmm.inc):

   Declare_Virtual_Device Name, MajorVer, MinorVer, CtrlProc,
   DeviceID, InitOrder, V86Proc, PMProc, RefData

Разберем параметры.
Name - имя виртуального драйвера. Должно совпадать с именем, определенным в DEF-файле.
MajorVer, MinorVer - младшая и старшая части версии драйвера.
CtrlProc - имя управляющей процедуры драйвера. Эта процедура принимает и обрабатывает сообщения, приходящие на драйвер. Принято формировать имя процедуры из двух частей: имя драйвера и добавка Control. Например, если имя нашего драйвера будет VXD1, то имя процедуры следует взять VXD1_Control.
DeviceID - 16-битный уникальный идентификатор виртуального драйвера. Необходимо указывать в том случае, когда виртуальный драйвер будет предоставлять свой сервис другим драйверам. Кроме того, идентификатор может понадобиться, если ваш драйвер собирается работать в реальном режиме.
InitOrder - порядок загрузки драйвера. Это всего лишь номер. Драйверы, имеющие меньший номер, загружаются в первую очередь. Имеет смысл для статических драйверов.
V86Proc, PMProc - специфицируют адреса функций, которые будет экспортировать драйвер для DOS- и обычных приложений. Если экспортирование не предполагается, параметры следует опустить.
Ref_Data - ссылка на данные, используемые супервизором ввода-вывода. Обычно опускается.

Макросы Begin_control_dispatch и End_control_dispatch используются для определения управляющей процедуры. Выглядит это следующим образом.

  Begin control_dispatch VXD1

  Control_Dispatch message, function

  End_control_dispatch VXD1

Макрос Control_Dispatch определяет, какие сообщения какими функциями будут обрабатываться. Например:

  Begin_control_dispatch VXD1

  Control_Dispatch INIT_COMPLETE, INIT

  End_control_dispatch VXD1

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

.386P
include vmm.inc
include vcond.inc
DECLARE_VIRTUAL_DEVICE VXD1,1,0, VXD1_Control, \
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch VXD1
Control_Dispatch INIT_COMPLETE, INIT
End_control_dispatch VXD1

VxD_LOCKED_CODE_SEG
BeginProc INIT
EndProc INIT
VxD_LOCKED_CODE_ENDS

end

Рис. 4.7.2. "Скелет" виртуального драйвера.

Оттранслируем драйвер на Рис. 4.7.2 согласно ранее указанному алгоритму. Только при компоновке добавьте ключ /MAP, в результате, кроме драйвера VXD1.VXD, в каталоге появится файл VXD1.MAP. Содержимое этого файла мы поместили на Рис. 4.7.3.

VXD1

 Timestamp is 3bb5ad7a (Sat Sep 29 17:16:10 2001)

 Preferred load address is 00400000

 Start         Length     Name                   Class
 0001:00000000 00000050H _LDATA                  CODE
 0001:00000050 00000007H _LTEXT                  CODE

  Address         Publics by Value              Rva+Base     Lib:Object

 0001:00000000       VXD1_DDB                   00401000     vxd1.obj
 0001:00000050       VXD1_Control               00401050 f   vxd1.obj
 0001:00000057       INIT                       00401057 f   vxd1.obj

 entry point at        0000:00000000

 Static symbols

Рис. 4.7.3. Содержимое файла VXD1.MAP.

Просматривая MAP-файл, обратите внимание, что в объектном файле определень два сегмента: _LDATA и _LTEXT, относящиеся к одному классу.

В заключение отмечу, что вместо стандартного name proc/name endp мы пользуемся макросами BeginProc и EndProc, определение которых также можно обнаружить в VMM.INC.

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

int 20h
DD 00110002H

При этом 11Н - идентификатор виртуального драйвера, 02Н - номер сервиса (индекс в таблице сервисов). Мы не будем непосредственно писать вызов в таком виде, а будем пользоваться макросами VMMCall и VxDCall. Первый - для вызова сервисов VMM, а второй - для сервисов других виртуальных драйверов.

III

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

.386P

include vmm.inc
include shell.inc
include vcond.inc

; заполнить структуру DDB
DECLARE_VIRTUAL_DEVICE VXD2,1,0, VXD2_Control, \
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

; объявить принимаемые сообщения и процедуры,
; которые их обрабатывают
Begin_control_dispatch VXD2
  Control_Dispatch Create_VM, OnVMCreate
  Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch VXD2

; сегмент для хранения сообщений
VxD_PAGEABLE_DATA_SEG
  MsgTitle    db "Сообщение драйвера VXD",0
  VMCreated   db "Создается виртуальная машина",0
  VMDestroyed db "Уничтожается виртуальная машина",0
  VMFocus     db "Смена фокуса виртуальной машины"
VxD_PAGEABLE_DATA_ENDS

; сегмент, содержащий код
VxD_PAGEABLE_CODE_SEG

; процедура - реакция на создание виртуальной машины
BeginProc OnVMCreate
; здесь можно вставить код - реакцию на
; открытие виртуальной машины

  MOV ECX,OFFSET VMCreated
  CALL MES
  RET
EndProc OnVMCreate

; процедура - реакция на закрытие виртуальной машины
BeginProc OnVMClose
; здесь можно вставить код - реакцию на
; закрытие виртуальной машины

  MOV ECX,OFFSET VMDestroyed
  CALL MES
  RET
EndProc OnVMClose

; процедура, выводящая сообщение
MES PROC
; получить дескриптор системной виртуальной машины
  VMMCall Get_sys_vm_handle
; дескриптор возвращается в EBX
; в EAX - флаг сообщения
  MOV EAX,MB_OK ; адрес заголовка сообщения
  MOV EDI, OFFSET MsgTitle
; адрес CallBack функции, в данном случае NULL
  XOR ESI,ESI
; ссылка на данные для CallBack-функции
  XOR EDX,EDX
; сервисная функция VXD - окно сообщения
  VxDCall SHELL_Message
MES ENDP
VxD_PAGEABLE_CODE_ENDS

end

Рис. 4.7.3. Пример простого статического виртуального драйвера.

Как мы уже указывали, статический драйвер загружается при загрузке системы и остается в памяти до завершения работы операционной системы. Удобнее всего загрузить драйвер, указав строку device=имя_драйвера в секции [386enh] файла SYSTEM.INI. Можно также использовать системный реестр: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD\key\StaticVxD=pathname.

Первое, однако, более удобно, т.к. в случае ошибки в VXD-драйвере можно исключить его запуск, отредактировав файл SYSTEM.INI в MS DOS.

При установке виртуальных драйверов VMM посылает драйверам следующие три сообщения в порядке списка.

SysCriticalInit - посылается при переключении в защищенный режим, но до разрешения прерываний.

Device_Init - посылается после разрешения прерываний. Именно это сообщение чаще всего используется виртуальными драйверами для проведения начальной инициализации.

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

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

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

System_Exit2 - сообщение посылается перед выгрузкой системы. Микропроцессор находится еще в защищенном режиме.

Sys_Critical_Exit2 - следующее сообщение перед выгрузкой системы.

Device_Reboot_Notify2 - сообщают виртуальным драйверам, что система "собирается" выгружаться. Прерывания еще доступны.

Crit_Reboot_Notify2 - аналогичное предыдущему сообщению, но прерывания недоступны.

Обратимся теперь к программе на Рис. 4.7.3. Этот драйвер выводит сообщение при активизации виртуальной машины (например, создание консоли или просто запуск DOS-приложения) и при ее деактивизации. В драйвере мы использовали две сервисные функции: получить дескриптор системной виртуальной машины и вывести сообщение. Рассмотрим эти функции:

Get_sys_vm_handle - получить дескриптор системной виртуальной машины. Причем дескриптор возвращается в регистре EBX.

SHELL_Message — вывести сообщение. Параметры хранятся в регистрах:

  • EBX — дескриптор виртуальной машины.
  • EAX - флаг сообщения, например MB_OK.
  • ECX - 32-битный адрес строки-сообщения.
  • EDI - 32-битный адрес строки-заголовка.
  • ESI - адрес функции - реакции на действие пользователя. Если функции нет, тогда 0.
  • EDX - адрес данных, которые будут посылаться функции.

И еще, при выходе драйвер должен очищать флаг переноса. В нашем случае очистка флага переноса обусловлена правильным выполнением функции SHELL_Message.

IV

В последнем разделе главы мы рассмотрим динамические виртуальные драйверы. Существует три способа загрузки виртуальных динамических драйверов.

  1. Поместить драйвер в каталог \SYSTEM\IOSUBSYS. Драйверы из этого директория загружаются супервизором ввода- вывода.
  2. Использовать сервис VxDLDR. Эту сервисную функцию можно вызывать только из виртуальных драйверов.
  3. Использовать функцию CreateFile.

Именно последним способом загрузки динамических драйверов мы сейчас и займемся. Схема использования динамических виртуальных драйверов следующая:

  1. Открываем драйвер с помощью функции CreateFile. При удачном открытии функция возвращает идентификатор, который затем используется при вызове функций экспортируемых данным драйвером.
  2. Используем функции динамического драйвера посредством вызова функции API DeviceIoControl.
  3. Закрываем драйвер с помощью CloseHandle, при этом он автоматически выгружается из памяти.

Перейдем теперь к программе загрузки динамического драйвера. Программа показана на Рис. 4.7.4. Она загружает драйвер msg.vxd и вызывает сервис 3 драйвера.

; файл FILES1.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_INPUT_HANDLE equ -10
FILE_FLAG_DELETE_ON_CLOSE   equ 4000000h

; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN CreateFileA@28:NEAR
EXTERN CloseHandle@4:NEAR
EXTERN  MessageBoxA@16:NEAR
EXTERN  ReadConsoleA@20:NEAR
EXTERN  DeviceIoControl@32:NEAR
;
;------------------------------------------------
;директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------
;
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
     HANDL DWORD ?
     HFILE DWORD ?
     BUF  DB "\\.\msg.vxd",0
     CAP  DB "Окно сообщения",0
     MES  DB "Ошибка загрузки драйвера",0
     BUFER DB 20 DUP (0)
     LENS  DWORD ?  ; количество выведенных символов
     MES1  DB "Вызов сервиса OK!",0
_DATA ENDS

; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE ввода
      PUSH STD_INPUT_HANDLE
      CALL GetStdHandle@4
      MOV HANDL,EAX
; открыть файл
      PUSH 0
      PUSH FILE_FLAG_DELETE_ON_CLOSE
      PUSH 0
      PUSH 0
      PUSH 0
      PUSH 0
      PUSH OFFSET BUF
      CALL CreateFileA@28
      CMP EAX,-1
      JE  _ERR
      MOV HFILE,EAX
; вызов сервиса VXD
      PUSH 0
      PUSH 0
      PUSH 0
      PUSH 0
      PUSH 18
      PUSH OFFSET MES1
      PUSH 3 ; номер сервиса
      PUSH HFILE
      CALL DeviceIoControl@32
; ждать клавиши ENTER
      PUSH 0
      PUSH OFFSET LENS
      PUSH 200
      PUSH OFFSET BUFER
      PUSH HANDL
      CALL ReadConsoleA@20
; закрыть и выгрузить драйвер
      PUSH HFILE
      CALL CloseHandle@4
_EXIT:
; конец работы программы
      PUSH 0
      CALL ExitProcess@4
_ERR:
      PUSH 0         ; MB_OK
      PUSH OFFSET CAP
      PUSH OFFSET MES
      PUSH 0 ; дескриптор окна
      CALL MessageBoxA@16
      JMP _EXIT
_TEXT ENDS
END START

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

Прокомментируем программу на Рис. 4.7.4. Главное здесь - разобрать работу функции DeviceIoControl. Вот параметры этой функции:

  • 1-й параметр, дескриптор драйвера, полученный через функцию CreateFile.
  • 2-й параметр, номер необходимой Вам операции.
  • 3-й параметр, адрес данных для драйвера.
  • 4-й параметр, длина данных.
  • 5-й параметр, буфер, куда драйвер поместит свои данные.
  • 6-й параметр, длина буфера.
  • 7-й параметр, адрес переменной, куда будет занесено количество байтов, помещенных в буфер драйвером.
  • 8-й параметр, адрес OVERLAPPED-структуры.

Как видите, при вызове мы передаем указатель на строку MES1.

Теперь, я думаю. Вы легко сможете разобраться в программе загрузки. Переходим к самому драйверу. Драйвер выполняет весьма простую функцию. При вызове его сервиса он выводит на экран сообщение. Причем текст сообщения передается вызывающей программой. При вызове функции DeviceIoControl с дескриптором драйвера, на драйвер приходит сообщение w32_deviceIoControl. При этом регистр EBX содержит дескриптор виртуальной машины, ESI указывает на структуру, содержимое которой мы сейчас разберем. При этом надо иметь в виду, что при загрузке драйвера на него тоже приходит это же сообщение, и мы его должны также обработать. Разберем теперь структуру, на которую указывает регистр ESI.

DIOCParams        STRUC
     Internal1         DD  ?
     VMHandle          DD  ?
     Internal2         DD  ?
     dwIoControlCode   DD  ?
     lpvInBuffer       DD  ?
     cbInBuffer        DD  ?
     lpvOutBuffer      DD  ?
     cbOutBuffer       DD  ?
     lpcbBytesReturned DD  ?
     lpoOverlapped     DD  ?
     hDevice           DD  ?
     tagProcess        DD  ?
DIOCParams         ENDS

Опишем поле структуры:
Internal1 - указатель на структуру Client_Reg_Struc, определяющую регистры вызывающего приложения (см. Рис. 4.7.6. и пояснения к нему).
VMHandle - дескриптор виртуальной машины.
Internal2 - указатель на блок DDB.
dwIoControlCode - номер необходимой операции.
lpvInBuffer - указатель на буфер, содержащий информацию вызывающей программы.
cbInBuffer - количество байт, пересылаемых в буфере.
lpvOutBuffer - указатель на буфер, в который драйвер может поместить информацию для вызывающей программы.
cbOutBuffer - количество байт в буфере.
lpcbBytesReturned - количество возвращаемых байт.
lpoOverlapped - указатель на OverLapped-структуру.
hDevice - дескриптор драйвера, возвращаемый функцией CreateFile.
tagProcess - признак процесса.

.386P
include vmm.inc
include vcond.inc
include vwin32.inc
include shell.inc

DECLARE_VIRTUAL_DEVICE MSG,1,0, MSG_Control,\
  UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch MSG
; будем обрабатывать сообщение w32_Device!oControl
; процедурой PROC1
    Control_Dispatch w32_DeviceIoControl, PROC1
End_control_dispatch MSG

; сегмент данных
VxD_PAGEABLE_DATA_SEG
    CAP1 DB "Окно сообщения",0
    MES1 DB 50 DUP (0)
VxD_PAGEABLE_DATA_ENDS

; сегмент кода
VxD_PAGEABLE_CODE_SEG
BeginProc PROC1
    CMP DWORD PTR [ESI]+12,DIOC_Open
    JNE L1
    XOR EAX,EAX
    JMP _EXIT
L1:
    CMP DWORD PTR [ESI]+12,3
    JNZ _EXIT
; длина строки
    MOV EDI,DWORD PTR [ESI]+16
    VMMCall _lstrlen, <EDI>
; копировать в буфер
    INC EAX ; длина
    VMMCall _lstrcpyn,<OFFSET MES1,EDI,EAX>
; вызвать функцию SHELL_Message
    MOV ECX,OFFSET MES1 ; DWORD PTR [ESI]+14
    MOV EDI,OFFSET CAP1
    MOV EAX,MB_OK + MB_ICONEXCLAMATION
    VMMCall Get_Sys_VM_Handle
; адрес CaliBack функции, в данном случае NULL
    XOR ESI,ESI
; ссылка на данные для CallBack-функции
    XOR EDX,EDX
    VxDCall SHELL_Message
    XOR EAX,EAX
_EXIT:
    RET
EndProc PROC1
VxD_PAGEABLE_CODE_ENDS

end

Рис. 4.7.5. Пример динамического драйвера.

Прокомментируем программу на Рис. 4.7.5.

  1. Как я уже говорил, при загрузке драйвера на него приходит сообщение w32_DeviceIoControl, при этом на структуру указывает регистр ESI. При этом поле dwIoControlCode будет содержать число DIOC_Open, в действительности равное просто 0. Поле dwIoControlCode находится по смещению ESI+12. Убедившись, что там содержится 0, мы возвращаем управление, обнулив предварительно EAX (это необходимо).
  2. При вызове драйвера из программы мы используем номер 3. Убедившись, что в поле dwIoControlCode содержится 3, мы таким образом должны сделать то, что ждет от нас программа.
  3. Собственно задача драйвера - вывести сообщение со строкой, полученной от программы. Известен адрес строки и ее длина. Чтобы продемонстрировать некоторые функции VXD-сервиса, мы еще раз определяем длину строки и копируем ее в буфер, подготовленный в теле драйвера.
  4. Наконец вывод сообщения и возвращение управления с обнулением содержимого EAX.

Сделаем теперь некоторые пояснения к структуре Client_Reg_Struc.

Client_Reg_Struc    STRUC
    Client_EDI DD ?
    Client_ESI DD ?
    Client_EBP DD ?
    Client_res0 DD ?
    Client_EBX DD ?
    Client_EDX DD ?
    Client_ECX DD ?
    Client_EAX DD ?
    Client_Error DD ?
    Client_EIP DD ?
    Client_CS DW ?
    Client_res1 DW ?
    Client_EFlags DD ?
    Client_ESP DD ?
    Client_SS DW ?
    Client_res2 DW ?
    Client_ES DW ?
    Client_res3 DW ?
    Client_DS DW ?
    Client_res4 DW ?
    Client_FS DW ?
    Client_res5 DW ?
    Client_GS DW ?
    Client_res6 DW ?
    Client_Alt_EIP DD ?
    Client_Alt_CS DW ?
    Client_res7 DW ?
    Client_Alt_EFlags DD ?
    Client_Alt_ESP DD ?
    Client_Alt_SS DW ?
    Client_res8 DW ?
    Client_Alt_ES DW ?
    Client_res9 DW ?
    Client_Alt_DS DW ?
    Client_res10 DW ?
    Client_Alt_FS DW ?
    Client_res11 DW ?
    Client_Alt_GS DW ?
    Client_res12 DW ?
Client_Reg_Struc ENDS

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

Пояснение к Рис. 4.7.6. Структура содержит три типа полей:
Client_resX - зарезервированные поля.
Client_XXX - регистры программы, запускаемой в виртуальной машине.
Client_Alt_XXX - регистры 32-битной программы, запускаемой в системной виртуальной машине.

На этом мы заканчиваем рассмотрение виртуальных драйверов58.

58 При написании главы были использованы материалы с сайта http://win32asm.cjb.net


Приложения: Приложение 1. Справочник API-функций и сообщений Windows

Приложения

Приложение 1. Справочник API-функций и сообщений Windows.

В силу ограниченного объема книги мы не можем дать полный список API-функций — их насчитывается более двух тысяч. Мы даем список API-функций, которые содержатся в данной книге с кратким комментарием и указанием глав, где они были использованы или хотя бы упомянуты. Вторая таблица посвящена сообщениям Windows.

Таблица 1. Функции API.

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

 | Назначение функции | Где существенным образом упоминается
AllocConsole | Создать консоль | Глава 2.2. Консольные приложения.
Arc | Рисовать дугу | Упоминание в Главе 2.1.
BeginPaint | Получить контекст при получении сообщения WM_PAINT | Глава 2.1. См. программу на Рис. 2.1.1.
BitBlt | Скопировать виртуальную прямоугольную область в окно | Глава 2.1 и программа на Рис. 2.1.6.
CallNextHookEx | Продолжить выполнение других фильтров. | Глава 3.7, раздел "Фильтры".
CallWindowProc | Вызвать процедуру окна. | Глава 3.5. Использование подклассов.
CharToOem | Функция перекодировки строки. | Глава 2.2. Программа на Рис. 2.2.3.
CloseHandle | Закрыть объект: файл, консоль, коммуникационный канал, созданный функцией CreateFile, CreatePipe и т.п. | Глава 2.5.
CreateCompatibleBitmap | Создать карту бит, совместимую с заданным контекстом. | Глава 2.1, программа на Рис. 2.1.6.
CreateCompatibleDC | Создать контекст, совместимый с данным окном. | Глава 2.1, программа на Рис. 2.1.6.
CreateDialogParam | Создать немодальное диалоговое окно. | Глава 2.3, программа на Рис. 2.3.5.
CreateEvent | Создать событие. | Глава 3.2. События.
CreateFile | Создать или открыть файл, консоль, коммуникационный канал и т.п. | Глава 2.5.
CreateFileMapping | Создать отображаемый файл. | Глава 3.5, раздел II.
CreateFont | Задать параметры шрифта. | Упоминается в главе 2.1.
CreateFontIndirect | Задать параметры шрифта. | Глава 2.1.
CreateMutex | Создать объект синхронизации "взаимоисключение" | Глава 3.2. Взаимоисключения.
CreatePen | Создать перо. | Глава 2.1, раздел III.
CreatePipe | Создать канал обмена информацией. | Глава 3.5, раздел IV.
CreateProcess | Создать новый процесс. | Глава 3.2. программа 3.2.1.
CreateSemaphore | Создать семафор. | Глава 3.2. Семафоры.
CreateSolidBrush | Определить кисть. | Глава 2.1, программа на Рис. 2.1.1.
CreateThread | Создать поток. | Глава 3.2, программа на Рис. 3.2.2.
CreateWindow | Создать окно. | Глава 1.2. Начала программирования в Windows.
CreateWindowEx | Расширенное создание окна. | Глава 1.2.
DefWindowProc | Вызывается для сообщений, которые не обрабатываются функцией окна. | Глава 1.2, программа на Рис. 1.2.1.
DeleteCriticalSection | Удалить объект "критическая секция". | Глава 3.2. Критические секции.
DeleteDC | Удалить контекст, полученный посредством функций типа CreatePen или CreateDC. | Глава 2.1, программа на Рис. 2.1.6.
DeleteObject | Удалить объект, выбранный функцией SelectObject. | Глава 2.1, программа на Рис. 2.1.3.
DestroyMenu | Удалить меню из памяти. | Глава 2.4, программа на Рис. 2.4.1.
DestroyWindow | Удалить окно из памяти. | Глава 2.3, программа на Рис. 2.3.5.
DeviceIoControl | Вызов сервиса динамического виртуального драйвера. | Глава 4.7. См. раздел "Динамические драйверы".
DialogBox | Создать модальное диалоговое окно. | Глава 2.3.
DialogBoxParam | Создать немодальное диалоговое окно. | Глава 2.3, программа на Рис. 2.3.2.
DispatchMessage | Вернуть управление Windows с передачей сообщения предназначенному окну. | Глава 1.2.
Ellipse | Рисовать эллипс. | Глава 2.1.
EndDialog | Удалить модальное диалоговое окно. | Глава 2.3. См. Рис. 2.3.2.
EndPaint | Удалить контекст, полученный при помощи BeginPaint. | Глава 2.1, программа на Рис. 2.1.1.
EnterCriticalSection | Войти в критическую секцию. | Глава 3.2. Критические секции.
EnumWindows | Пересчитать окна. | Глава 3.5, раздел VIII.
ExitProcess | Закончить данный процесс со всеми подзадачами (потоками). | Глава 1.2.
ExitThread | Выход из потока с указанием кода выхода. | Глава 3.2.
FindFirstFile | Первый поиск файлов в каталоге. | Глава 2.5. Поиск файлов.
FindNextFile | Осуществить последующий поиск в каталоге. | Глава 2.5. Поиск файлов.
FlushViewOfFile | Сохранить отображаемый файл или его часть на диск. | Глава 3.5.
FreeConsole | Освободить консоль. | Глава 2.2. См. программу на Рис. 2.2.3.
FreeLibrary | Выгрузить динамическую библиотеку. | Глава 3.3. См. программу на Рис. 3.3.3.
GetCommandLine | Получить командную строку программы. | Глава 2.2.
GetCursorPos | Получить положение курсора в экранных координатах. | Глава 3.1, программа на Рис. 3.1.3.
GetDC | Получить контекст окна. | Глава 2.1. См. программу на Рис. 2.1.6.
GetDiskFreeSpace | Определяет объем свободного пространства на диске. | Глава 3.4., раздел I.
GetDlgItem | Получить дескриптор управляющего элемента в окне. | Глава 3.1. См. Рис. 3.1.3.
GetDriveType | Получить тип устройства. | Глава 3.4. См. программу на Рис. 3.4.1.
GetLocalTime | Получить местное время. | Глава 3.1. См. программу на Рис. 3.1.2.
GetMenuItemInfo | Получить информацию о выбранном пункте меню. | Глава 2.4. См. программу на Рис. 2.4.1.
GetMessage | Получить очередное сообщение из очереди сообщений данного приложения. | Глава 1.2.
GetModuleHandle | Получить дескриптор приложения. | Глава 1.2, программа на Рис. 1.2.2.
GetProcAddress | Получить адрес процедуры (в динамической библиотеке). | См. главу 3.3.
GetStdHandle | Получить дескриптор консоли. | Глава 2.2.
GetStockObject | Определить дескриптор стандартного объекта. | См. главу 1.4.
GetSystemDirectory | Получить системный каталог. | Глава 3.2.
GetSystemMetrics | Определить значение системных характеристик. | Глава 2.1, программа на Рис. 2.1.6.
GetSystemTime | Получить время по Гринвичу. | Упоминается в главе 3.1.
GetTextExtentPoint32 | Определить параметры текста в данном окне. | Глава 2.1, программа на Рис. 2.1.2.
GetWindowRect | Определить размер окна. | Глава 2.1, программа на Рис. 2.1.2.
GetWindowsDirectory | Получить каталог Windows. | Глава 3.2.
GetWindowText | Получить заголовок окна. | Глава 3.5, программа на Рис. 3.5.4.
GetWindowThreadProcessId | Получить идентификатор процесса. | Глава 3.5, программа на Рис. 3.5.4.
GlobalAlloc | Выделить блок памяти. | Глава 3.7. См. раздел III.
GlobalDiscard | Удалить удаляемый блок памяти. | Глава 3.7. См. раздел III.
GlobalFree | Освободить блок памяти. | Глава 3.7. См. раздел III.
GlobalLock | Фиксировать перемещаемый блок памяти. | Глава 3.7. См. раздел III.
GlobalReAlloc | Изменить размер блока памяти. | Глава 3.7. См. раздел III.
GlobalUnlock | Снять фиксацию блока памяти. | Глава 3.7. См. раздел III.
InitializeCriticalSection | Создать объект критическая секция. | Глава 3.2. Программа на Рис. 3.2.3.
InvalidateRect | Перерисовать окно. | Глава 2.1. См. программу на Рис. 2.1.6.
KillTimer | Удалить таймер. | Глава 3.1. См. программу на 3.1.1.
LeaveCriticalSection | Покинуть критическую секцию. | Глава 3.2. Программа на Рис. 3.2.3.
LineTo | Провести линию от текущей точки к заданной. | Глава 2.1. Программа на Рис. 2.1.6.
LoadAccelerators | Загрузить таблицу акселераторов. |
LoadCursor | Загрузить системный курсор или курсор, определенный в файле ресурсов. | Глава 1.2.2.
LoadIcon | Загрузить системную иконку или иконку, определенную в файле ресурсов. | Глава 1.2.2.
LoadLibrary | Загрузить динамическую библиотеку. | Глава 3.3, программа на Рис. 3.3.3.
LoadMenu | Загрузить меню, которое определено в файле ресурсов. | Глава 2.3. См. программу на Рис. 2.3.3.
LoadString | Загрузить строку, определенную в файле ресурсов. | Глава 2.3, программа на Рис. 2.3.2.
lstrcat | Производит конкатенацию двух строк. | Впервые упоминается в главе 2.5.
lstrcpy | Скопировать одну строку в другую. | Впервые упоминается в главе 2.5.
lstrlen | Получить длину строки. | Впервые упоминается в главе 2.5.
MapViewOfFile | Скопировать файл или части файла в память. | Глава 3.5. См. раздел II.
MessageBox | Выдать окно сообщения. | Глава 1.2.
MoveToEx | Сменить текущую точку. | Глава 2.1, программа на Рис. 2.1.6.
MoveWindow | Установить новое положение программа окна. | Глава 3.1, на Рис. 3.1.3.
OpenEvent | Открыть событие. | Глава 3.2. См. раздел События.
OpenSemaphore | Открыть семафор. | Глава 3.2. См. раздел "Семафоры".
PatBlt | Заполнить заданную прямоугольную область. | Глава 2.1, программа на Рис. 2.1.6.
Pie | Рисовать сектор эллипса. | Упоминается в главе 2.1.
PostMessage | Аналогична SendMessage, но сразу возвращает управление. | Глава 3.3, программа на Рис. 3.3.6.
PostQuitMessage | Послать текущему приложению сообщение WM_QUIT. | Глава 1.2.
ReadConsole | Читать из консоли. | Глава 2.2. См. раздел II.
ReadFile | Читать из файла или того, что было создано функцией CreateFile. | Глава 2.5
Rectangle | Рисовать прямоугольник. | Глава 2.1, раздел III.
RegisterClass | Зарегистрировать класс окон. | Глава 1.2.
RegisterHotKey | Зарегистрировать горячую клавишу. | Глава 2.4, программа на Рис. 2.4.2.
ReleaseDC | Удалить контекст, полученный при помощи GetDC | Глава 2.1, программа на Рис. 2.1.6.
ReleaseSemaphore | Освободить семафор | Глава 3.2. См. раздел "Семафоры".
ResetEvent | Сбросить событие | Глава 3.2. См. раздел "События".
ResumeThread | Запустить "спящий" процесс. | Глава 3.2, раздел II.
RoundRect | Рисовать прямоугольник с округленными углами. | Глава 2.1.
RtlMoveMemory | Копировать блок памяти в другой блок. В помощи по API-функциям она называется MoveMemory. | Глава 3.4, программа на Рис. 3.4.4.
SelectObject | Выбрать объект (перо, кисть) в указанном контексте. | Глава 2.1, программа на Рис. 2.1.3.
SendDlgItemMessage | Послать сообщение управляющему элементу окна. | Глава 2.4.
SendMessage | Послать сообщение окну. | Глава 1.3, программа на Рис. 1.3.2.
SetBkColor | Установить цвет фона для вывода текста. | Глава 2.1. Программа на Рис. 2.1.1.
SetConsoleCursorPosition | Установить курсор в заданную позицию в консоли. | Глава 2.2, программа на Рис. 2.2.3.
SetConsoleScreenBufferSize | Установить размер буфера консоли. | Глава 2.2, программа на Рис. 2.2.3.
SetConsoleTextAttribute | Установить цвет текста в консоли. | Глава 2.2, программа на Рис. 2.2.3.
SetConsoleTitle | Установить название окна консоли. | Глава 2.2, программа на Рис. 2.2.3.
SetEvent | Подать сигнал о наступлении события. | Глава 3.2. См. раздел "События".
SetFocus | Установить фокус на заданное окно. | Глава 1.3.
SetLocalTime | Установить время и дату. | Упоминается в главе 3.1.
SetMapMode | Установить соотношение между логическими единицами и пикселями. | Упоминается в главе 2.1.
SetMenu | Назначить новое меню данному окну. | Глава 2.3, программа на Рис. 2.3.3.
SetPixel | Установить заданный цвет пикселя. | Глава 2.1, программа на Рис. 2.1.6.
SetSystemTime | Установить время, используя гринвичские координаты. | Упоминается в главе 3.1.
SetTextColor | Установить цвет текста. | Глава 2.1, программа на Рис. 2.1.1.
SetTimer | Установить таймер. | Глава 3.1.
SetViewportExtEx | Установить область вывода. | Глава 2.1.
SetViewportOrgEx | Установить начало области вывода. | Глава 2.1.
SetWindowLong | Изменить атрибут уже созданного окна. | Глава 3.5. См. раздел III.
SetWindowsHookEx | Установить процедуру-фильтр. | Глава 3.7, программа на Рис. 3.7.5.
Shell_NotifyIcon | Посредством данной функции можно поместить иконку приложения на системную панель. | Глава 3.5. См. программу на Рис. 3.5.1.
SHFileOperation | Осуществляет групповую операцию над файлами и каталогами. | Глава 3.5. См. раздел VI.
SHGetDesktopFolder | Выводит диалоговое окно для выбора каталогов и файлов. | Глава 3.5. См. раздел VI.
ShowWindow | Показать окно, установить статус показа. | Глава 1.2.
Sleep | Вызывает задержку. | Глава 3.2. см. рис. 3.2.2.
TerminateProcess | Уничтожить процесс. | Глава 3.2. См. программу на Рис. 3.2.1.
TerminateThread | Удалить поток. | Глава 3.2. См. программу 3.2.1.
TextOut | Вывести текст в окно. | Глава 2.1.
timeKillEvent | Удалить таймер. | Глава 2.2. См. раздел III.
timeSetEvent | Установить таймер. | Глава 2.2. См. раздел III.
TranslateAccelerator | Транслирует акселераторные клавиши в команду выбора пункта меню. | Глава 2.3. См. раздел III.
TranslateMessage | Транслировать клавиатурные сообщения в ASCII-коды. | Глава 1.2.
UnhookWindowsHookEx | Снять процедуру-фильтр. | Глава 3.6. Программа на Рис. 3.6.5.
UnmapViewOfFile | Сделать указатель на отображаемый файл недействительным. | Глава 3.5. См. раздел II.
UnregisterHotKey | Снять регистрацию горячей клавиши. | Глава 2.4, программа на Рис. 2.4.2.
UpdateWindow | Обновить рабочую область окна. | Глава 1.2.
VirtualAlloc | Зарезервировать блок виртуальной памяти или отобразить на него физическую память. | Глава 3.6. См. раздел III.
VirtualFree | Снять резервирование с блока виртуальной памяти или сделать блок виртуальной памяти неотображенным. | Глава 3.6. См. раздел III.
WaitForSingleObject | Ожидает одно из двух событий: определенный объект сигнализирует о своем состоянии, вышло время ожидания (TimeOut). Работает с такими объектами, как семафор, событие, взаимоисключение, процесс, консольный ввод и др. | Глава 3.2. См. раздел "Семафоры".
WNetAddConnection2 | Осуществляет соединение с сетевым ресурсом локальной сети. | Глава 3.4.
WNetCancelConnection2 | Отсоединить от ресурса локальной сети. | Глава 3.4.
WNetCloseEnum | Найти все ресурсы локальной сети данного уровня. | Глава 3.4.
WNetGetConnection | Получить информацию о данном соединении. | Глава 3.4.
WNetOpenEnum | Открыть поиск ресурсов в локальной сети. | Глава 3.4.
WriteConsole | Вывод в консоль. | Глава 2.2, программа на Рис. 2.2.1.
wsprintf | Преобразовать последовательность параметров в строку. | Глава 2.2. См. программу на Рис. 2.2.4.

Таблица 2. Сообщение операционной системы Windows.

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

Сообщение системы | Назначение | Где существенным образом упоминается
WM_ACTIVATE | Посылается функции окна перед активизацией и деактивизацией этого окна. | Глава 2.4.
WM_ACTIVATEAPP | Посылается функции окна перед активизацией окна другого приложения. | Глава 2.4.
WM_CHAR | Сообщение, возникающее при трансляции сообщения WM_KEYDOWN функцией TranslateMessage. | Главы 1.2, 2.4.
WM_CLOSE | Сообщение, приходящее на процедуру окна при его закрытии. Приходит до WM_DESTROY. Дальнейшее выполнение DefWindowProc, EndDialog или WindowsDestroy и вызывает появление сообщения WM_DESTROY. | Глава 2.3.
WM_COMMAND | Сообщение, приходящее на функцию окна, при наступлении события с управляющим элементом, пунктом меню, а также от акселератора. | Главы 1.3, 2.3.
WM_CREATE | Первое сообщение, приходящее на функцию окна при его создании. Приходит один раз. | См. подробнее Гл. 1.2.
WM_DEADCHAR | Сообщение, возникающее при трансляции сообщения WM_KEYUP функцией TranslateMessage. | Глава 1.2.
WM_DESTROY | Сообщение, приходящее на функцию окна при его уничтожении. | Подробнее см. Гл. 1.2.
WM_GETTEXT | Посылается окну для получения текстовой строки, ассоциированной с данным окном (строка редактирования, заголовок окна и т.д.). | Глава 1.3, см. программу на Рис. 1.3.2.
WM_HOTKEY | Генерируется при нажатии горячей клавиши. | Глава 2.4 и программа на Рис. 2.4.2.
WM_INITDIALOG | Сообщение, приходящее на функцию диалогового окна вместо сообщения WM_CREATE. | Глава 2.3.
WM_KEYDOWN | Сообщение, генерируемое при нажатии клавиши клавиатуры и посылаемое окну, имеющему фокус ввода. | Главы 1.2, 2.3.
WM_KEYUP | Сообщение, генерируемое при отпускании клавиши клавиатуры и посылаемое окну, имеющему фокус ввода. | Главы 1.2, 2.3.
WM_LBUTTONDOWN | Сообщение генерируется при нажатии левой кнопки мыши. | Глава 1.2.
WM_MENUSELECT | Посылается окну, содержащему меню, при выборе пункта меню. | Глава 2.4.
WM_PAINT | Сообщение посылается окну перед его перерисовкой. | Главы 1.2, 1.3.
WM_QUIT | Сообщение, приходящее приложению (не окну) при выполнении функции PostQuitMessage. При получении этого сообщения происходит выход из цикла ожидания и, как следствие, выход из программы. | Глава 1.2. См. комментарий к программе на Рис. 1.2.2.
WM_RBUTTONDOWN | Сообщение генерируется при нажатии правой кнопки мыши. | Глава 1.2, программа на рис. 1.2.2.
WM_SETFOCUS | Сообщение, посылаемое окну, после того, как оно получило фокус. | Глава 1.3, программа на Рис.1.3.2
WM_SETICON | Приложение посылает окну данное сообщение, чтобы ассоциировать с ним новую иконку (значок). | Глава 2.3.
WM_SETTEXT | Сообщение, используемое приложением для посылки текстовой строки окну и интерпретируемое в зависимости от типа окна (обычное окно - заголовок, кнопка — надпись на кнопке, окно редактирования - содержимое этого окна и т.д.). | Глава 1.3, см. программу на Рис. 1.3.2.
WM_SIZE | Посылается функции окна после изменения его размера. | Глава 3.5, см. программу на Рис. 3.5.1.
WM_SYSCHAR | Сообщение, возникающее при трансляции сообщения WM_SYSKEYDOWN функцией TranslateMessage. | Глава 1.2.
WM_SYSCOMMAND | Генерируется при выборе пунктов системного меню или меню окна. | Глава 2.3. См. также Рис. 2.3.4.
WM_SYSDEADCHAR | Сообщение, возникающее при трансляции сообщения WM_SYSKEYUP функцией TranslateMessage. | Глава 1.2.
WM_SYSKEYDOWN | Сообщение аналогично WM_KEYDOWN, но генерируется, когда нажата и удерживается еще и клавиша Alt. | Главы 1.2, 2.3.
WM_SYSKEYUP | Сообщение аналогично WM_SYSKEYDOWN, но генерируется при отпускании клавиши. | Главы 1.2, 2.3.
WM_TIMER | Сообщение, приходящее на функцию окна или специально определенную таймерную процедуру после определения интервала таймера при помощи функции SetTimer. | Глава 3.1 полностью посвящена данному сообщению.
WM_VKEYTOITEM | Сообщение окну приложения, когда нажимается какая-либо клавиша при наличии фокуса на данном списке. Список должен иметь свойство LBS_WANTKEYBOARDINPUT. | Глава 2.4.


Приложения: Приложение 2. Справочник по командам и архитектуре Pentium

Приложение 2. Справочник по командам и архитектуре Pentium

В литературе при описании команд микропроцессоров часто встречаются досадные ошибки. Стараясь избежать таких ошибок, автор выверял описание команд по нескольким источникам [3,5,6,8,9,10]. Часть команд была проверена программным путем.

Список регистров микропроцессора Pentium

Регистры общего назначения

   EAX = (16+AX=(AH+AL))
   EBX = (16+BX=(BH+BL))
   ECX = (16+CX=(CH+CL))
   EDX = (16+DX=(DH+DL))
   ESI = (16+SI)
   EDI = (16+DI)
   EBP = (16+BP)
   ESP = (16+SP)

Регистры EAX, EBX, EDX, ECX называют рабочими регистрами. Регистры EDI, ESI - индексные регистры, играют особую роль в строковых операциях. Регистр EBP обычно используется для адресации в стеке параметров и локальных переменных. Регистр ESP - указатель стека, автоматически модифицируется командами PUSH, POP, RET, CALL. Явно используется реже.

Регистр флагов. Содержит 32 бита. Вот используемые значения битов.

  • 0-й бит, флаг переноса (CF), устанавливается в 1 если был перенос из старшего бита;
  • 1-й бит, 1;
  • 2-й бит, флаг четности (PF). Устанавливается в 1, если младший байт результата содержит четное число единиц.
  • 3-й бит, 0;
  • 4-й бит, флаг вспомогательного переноса (AF). Устанавливается в 1, если произошел перенос из третьего бита в четвертый.
  • 5-й бит, 0;
  • 6-й бит, флаг нуля (ZF). Устанавливается в единицу, если результат операции ноль;
  • 7-й бит, флаг знака (SF). Равен старшему биту результата;
  • 8-й бит, флаг ловушки (TF). Установка в единицу этого флага приводит к тому, что после каждой команды вызывается INT 3. Используется отладчиками в реальном режиме;
  • 9-й бит, флаг прерываний (IF). Сброс этого флага в 0 приводит к тому, что микропроцессор перестает воспринимать прерывания;
  • 10-й бит, флаг направления (DF). Данный флаг учитывается в строковых операциях. Если флаг равен 1, то в строковых операциях адрес автоматически уменьшается;
  • 11-й бит, флаг переполнения (OF). Устанавливается в единицу, если результат операции над числом со знаком вышел за допустимые пределы;
  • 12,13-й биты, уровень привилегий ввода-вывода (IOPL);
  • 14-бит, флаг вложенной задачи (NT);
  • 15-й бит, 0;
  • 16-й бит, флаг возобновления (RF). Используется совместно с регистрами точек отладочного останова;
  • 17-й бит, в защищенном режиме включает режим виртуального режима 8086 (VM);
  • 18-й бит, флаг контроля выравнивания (AC). При равенстве этого флага 1 и при обращении к невыровненному операнду вызывает исключение 17;
  • 19-й бит, виртуальная версия флага IF (VIF). Работает в защищенном режиме;
  • 20-й бит, виртуальный запрос прерывания (VIP);
  • 21-й бит, флаг доступности команды идентификации;
  • 22-31-й, 0;

Сегментные регистры

CS - сегмент кода,
DS - сегмент данных,
SS - сегмент стека,
ES,GS,FS - дополнительные регистры. Сегментные регистры 16-битны.

Управляющие регистры

Регистр CR0.

  • 0-й бит, разрешение защиты (РЕ). Переводит процессор в защищенный режим.
  • 1-й бит, мониторинг сопроцессора (МР). Вызывает исключение 7 по каждой команде WAIT.
  • 2-й бит, эмуляция сопроцессора (ЕМ). Вызывает исключение 7 по каждой команде сопроцессора.
  • 3-й бит, бит переключения задач (TS). Позволяет определить, относится данный контекст сопроцессора к текущей задаче или нет. Вызывает исключение 7 при выполнении следующей команды сопроцессора.
  • 4-й бит, индикатор поддержки инструкций сопроцессора (ЕТ).
  • 5-й бит, разрешение стандартного механизма сообщений об ошибке сопроцессора (NE).
  • 5-15-й бит, не используются.
  • 16-й бит, разрешение защиты от записи на уровне привилегий супервизора (WP).
  • 17-й бит, не используется.
  • 18-й бит, разрешение контроля выравнивания (AM).
  • 19-28-й бит, не используются.
  • 29-й бит, запрет сквозной записи кэша и циклов аннулирования (NW).
  • 30-й бит, запрет заполнения кэша (CD).
  • 31-й бит, включение механизма страничной переадресации.

Регистр CR1 пока не используется.

Регистр CR2 хранит 32-битный линейный адрес, по которому был получен последний отказ страницы памяти.

Регистр CR3 - в старших 20 битах хранится физический базовый адрес таблицы каталога страниц.

Остальные биты.

  • 3-й бит, кэширование страниц со сквозной записью (PWT).
  • 4-й бит, запрет кэширование страницы (PCD).

Регистр CR4

  • 0-й бит, разрешение использования виртуального флага прерываний в режиме V8086 (VME).
  • 1-й бит, разрешение использования виртуального флага прерываний в защищенном режиме (PVI).
  • 2-й бит, превращение инструкции RDTSC в привилегированную (TSD).
  • 3-й бит, разрешение точек останова по обращению к портам ввода-вывода (DE).
  • 4-й бит, включает режим адресации с 4-мегабайтными страницами (PSE).
  • 5-й бит, включает 36-битное физическое адресное пространство (РАЕ).
  • 6-й бит, разрешение исключения МС (МСЕ).
  • 7-й бит, разрешение глобальной страницы (PGE).
  • 8-й бит, разрешает выполнение команды RDPMC (РМС).
  • 9-й бит, разрешает команды быстрого сохранения/восстановления состояния сопроцессора (FSR).

Системные адресные регистры

GDTR - 6-байтный регистр, в котором содержится линейный адрес глобальной дескрипторной таблицы.
IDTR - 6-байтный регистр, содержащий 32-битный линейный адрес таблицы дескрипторов обработчиков прерываний.
LDTR - 10-байтный регистр, содержащий 16-битный селектор (индекс) для GDT и 8-байтный дескриптор.
TR - 10-байтный регистр, содержащий 16-битный селектор для GDT и весь 8-байтный дескриптор из GDT, описывающий TSS текущей задачи.

Регистры отладки
DR0...DR3 - хранят 32-битные линейные адреса точек останова.
DR6 (равносильно DR4) - отражает состояние контрольных точек.
DR7 (равносильно DR5) - управляет установкой контрольных точек.

Команды пересылки данных
MOV dest,src | Пересылка данных в регистр из регистра, памяти или непосредственного операнда. Пересылка данных в память из регистра или непосредственного операнда. Например, MOV AX,10; MOV EBX,ESI; MOV AL, BYTE PTR MEM.
XCHG r/m,r | Обмен данными между регистрами или регистром и памятью. Команда "память - память" в микропроцессоре Intel не предусмотрена.
BSWAP reg32 | Перестановка байт из порядка "младший - старший" в порядок "старший - младший". Разряды 7-0 обмениваются с разрядами 31-24, а разряды 15-8 с разрядами 23-16. Команда появилась в 486-м микропроцессоре.
MOVSXB r,r/m | Пересылка байта с его расширением до слова или двойного слова с дублированием знакового бита: MOVSXB AX,BL; MOVSXB EAX,byte ptr mem. Команда появилась с 386-ого процессора.
MOVSXW r,r/m | Пересылка слова с расширением до двойного слова с дублированием знакового бита: MOVSXW EAX,WORD PTR MEM. Команда появилась с 386-ого процессора.
MOVZXB r,r/m | Пересылка байта с его расширением до слова или двойного слова с дублированием нулевого бита: MOVSXB AX,BL; MOVSXB EAX,byte ptr mem. Команда появилась с 386-ого процессора.
MOVZXW r,r/m | Пересылка слова с расширением до двойного слова с дублированием нулевого бита: MOVZXW EAX,WORD PTR MEM. Команда появилась с 386-ого процессора.
XLAT | Загрузить в AL байт из таблицы в сегменте данных, на начало которой указывает EBX (ВХ), при этом начальное значение AL играет роль смещения.
LEA r,m | Загрузка эффективного адреса. Например, LEA EAX,MEM; LEA EAX,[EBX]. Данная команда обладает магичаскими свойствами, позволяющими эффективно выполнять арифметические действия. Например, команда LEA EAX,[EAX*8] умножает содержимое EAX на 8, LEA EAX,[EAX][EAX*4] на 5, Команда LEA ECX,[EAX][ESI+5] эквивалента 3(!) командам MOV ECX,EAX/ADD ECX,ESI/ADD ECX,5.
LDS r,m | Загрузить пару DS:reg из памяти. Причем вначале идет слово (или двойное слово), а в DS - последующее слово.
LES r,m | Аналогично предыдущему, но для пары ES:reg.
LFS r,m | Аналогично предыдущему, но для пары FS:reg.
LGS r,m | Аналогично предыдущему, но для пары GS:reg.
LSS r,m | Аналогично предыдущему, но для пары SS:reg.
SETcc r/m | Проверяет условие "cc", если выполняется, то первый бит байта устанавливается в 1, в противном случае в 0. Условия аналогичны в условных переходах (je, jc). Например, SETE AL. Команда появилась с 386-ого микропроцессора.
LAHF | Загрузить флаги в АН (устарела).
SAHF | Сохранить АН в регистре флагов (устарела).

Команды ввода-вывода
IN AL(AX,EAX),Port
IN AL(AX,EAX),DX 		Ввод в аккумулятор из порта ввода-вывода. Порт адресуется непосредственно или через регистр DX.
OUT port,AL(АХ,EAX)
OUT DX,AL(АХ,EAX) 		Вывод в порт ввода-вывода. Порт адресуется непосредственно или через регистр DX.
[REP] INSB
[REP] INSW
[REP] INSD 		Выводит данные из порта, адресуемого регистром DX в ячейку памяти ES:[EDI/DI]. После ввода байта, слова или двойного слова производится коррекция EDI/DI на 1,2,4. При наличии префикса REP процесс продолжается, пока содержимое СХ не станет равным 0.
[REP] OUTSB
[REP] OUTSW
[REP] OUTSD 		Выводит данные из ячейки памяти, определяемой регистрами DS:[ESI/SI], в выходной порт, адрес которого находится в регистре DX. После вывода байта, слова, двойного слова производится коррекция указателя ESI/SI на 1,2,4.

Инструкции работы со стеком
PUSH r/m | Поместить в стек слово или двойное слово. Поскольку при включении в стек слова нарушается выравнивание стека по границам двойных слов, рекомендуется в любом случае помещать в стек двойное слово.
PUSH const | Поместить в стек непосредственный 32-битный операнд.
PUSHA | Поместить в стек регистры EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP. Команда появилась с 386-ого процессора.
POP reg/mem | Извлечь из стека слово или двойное слово.
POPА | Извлечение из стека данных в регистры EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP. Команда появилась, начиная с 386-ого процессора.
PUSHF | Помещение в стек регистра флагов.
POPF | Извлечь данные в регистр флагов.

Инструкции целочисленной арифметики.
ADD dest,src | Сложение двух операндов. Первый операнд может быть регистром или ячейкой памяти, второй - регистром, ячейкой памяти, константой. Невозможно только, когда оба операнда являются ячейками памяти.
XADD dest,src | Данная операция производит в начале обмен операндами, а затем выполняет операцию ADD. Начиная 486-ого.
ADC dest,src | Сложение с учетом флага переноса - в младший бит добавляется бит (флаг) переноса.
INC r/m | Инкремент операнда.
SUB dest,src | Вычитание двух операндов. Остальное аналогично сложению (команда ADD).
SBB dest,src | Вычитание с учетом бита переноса. Из младшего бита вычитается бит (флаг) переноса.
DEC r/m | Декремент операнда.
CMP r/m,r/m | Вычитание без изменения операндов (сравнение).
CMPXCHG r,m,a | Сравнение с обменом. Воспринимает три операнда (регистр-операнд-источник, ячейка памяти-операнд- получатель, аккумулятор, т.е. AL,AX или EAX) Если значения в операнде-получателе и аккумуляторе равны, операнд-получатель заменяется операндом-источником, исходное значение операнда-получателя загружается в аккумулятор. Начиная с 486-ого.
CMPXCHG8B r,m,a | Сравнение и обмен восьми байт. Начиная с Pentium.
NEG r/m | Изменение знака операнда.
ААА | Коррекция после ASCII-сложения. Коррекция результата двоичного сложения двух неупакованных десятичных чисел. Например, АХ содержит число 9H. Пара команд ADD AL,8/AAA приводит к тому, что в АХ будет содержаться 0107, т.е. ASCII-число 17.
AAS | Коррекция после ASCII-вычитания. Например: MOV AX,205H; загрузить ASCII 25 SUB AL,8; двоичное вычитание AAS ;теперь АХ содержит 107Н, т.е. 17.
ААМ | Коррекция после ASCII-умножения. Для этой команды предполагается, что в регистре АХ находится результат двоичного умножения двух десятичных цифр (диапазон от 0 до 81). После выполнения команды образуется двухбайтное произведение в регистре АХ в ASCII-формате.
AAD | Коррекция перед ASCII-делением. Предполагается, что младшая цифра находится в AL, а старшая - в АН.
DAA | Коррекция после BCD-сложения.59
DAS | Коррекция после ВСD-вычитания.
MUL r/m | Умножение АL(AX,EAX) на целое беззнаковое число. Результат, соответственно, будет содержаться в АХ, DX:AX, EDX:EAX.
IMUL r/m | Знаковое умножение (аналогично MUL). Все операнды считаются знаковыми. Команда IMUL имеет также двухоперандный и трехоперандный вид. Двухоперандный вид IMUL r,src r<-r*src. Трехоперандный вид IMUL dst,src,imm dst<-src*imm.
DIV r/m (src) | Беззнаковое деление. Аналогично беззнаковому умножению. Осуществляет деление аккумулятора и его расширения (AH:AL, DX:AX, EDX:EAX) на делитель src. Частное помещается в аккумуляторе, а остаток - в расширении аккумулятора.
IDIV r/m | Знаковое деление. Аналогично беззнаковому.
CBW | Расширение байта (AL) в слово с копированием знакового бита.
CWD | Расширение слова (АХ) в двойное слово (DX:AX) с копированием знакового бита.
CWDE | Расширение слова (АХ) в двойное слово (EAX) с копированием знакового бита.
CDQ | Преобразование двойного слова (EAX) в учетверенное слово (EDX:EAX).

59 Напоминаю, что ASCII-число предполагает одну цифру на один байт, BCD-число - одну цифру на половину байта. Т.о. скажем в регистре АХ, может находиться двухразрядное ASCII-число и четырехразрядное BCD-число.

Логические операции.
AND dest,src | Логическая операция "AND". Обнуление бит dest, которые равны нулю у src.
TEST dest,src | Аналогична "AND", но не меняет dest. Используется для проверки ненулевых бит.
OR dest,src | Логическая "ИЛИ". В dest устанавливаются биты, отличные от нуля в src.
XOR dest,src | Исключающее "ИЛИ".
NOT dest | Переключение всех бит (инверсия).

Сдвиговые операции.

Начиная с 386-ого микропроцессора, непосредственный операнд src может быть не только 1, но произвольным числом. В ранних версиях для количества сдвигов использовался регистр CL.
RCL/RCR dest,src | Циклический сдвиг влево/вправо через бит переноса CF. Src может быть либо CL, либо непосредственный операнд.
ROL/ROR dest,src | Аналогично командам RCL/RCR, но по другому, работает с флагом CF. Флаг не участвует в цикле, но в него попадает бит, перешедший с начала на конец или наоборот.
SAL/SAR dest,src | Сдвиг влево/право. Называется еще арифметическим сдвигом. При сдвиге вправо дублируется старший бит. При сдвиге влево младший бит заполняется нулем. Ушедший бит помещается в CF.
SHL/SHR dest,src | Логический сдвиг влево/вправо. Сдвиг вправо отличается от SAR тем, что и старший бит заполняется нулем.
SHLD/SHRD dest,src,count | Трехоперандные команды сдвига влево/вправо. Первым операндом, как обычно, может быть либо регистр, либо ячейка памяти, вторым операндом должен быть регистр общего назначения, третьим - регистр CL или непосредственный операнд. Суть операции заключается в том, что dest и src в начале объединяются, а потом производится сдвиг на количество бит count. Результат снова помещается в dest.

Строковые операции
REP | Префикс, означающий повтор строковой операции до обнуления ECX. Префикс имеет также разновидности REPZ (REPE) - выполнять, пока не нуль (ZF=1), REPNZ (REPNE) - выполнять, пока нуль.
MOVS dest,src | Команда передает байт, слово или двойное слово из цепочки, адресуемой DS:[ESI], в цепочку dest, адресуемую ES:[EDI]. При этом EDI и ESI автоматически корректируются согласно значению флага DF. Допускается явная спецификация MOVSB (byte), MOVSW (word), MOVSD (dword). Dest и src можно явно не указывать.
LODS src | Команда загрузки цепочки в аккумулятор. Имеет разновидности LODSB, LODSW, LODSD. При выполнении команды байт, слово, двойное слово загружается соответственно в AL,AX,EAX. При этом ESI автоматически изменяется на 1 в зависимости от значения флага DF. Префикс REP не используется.
STOS dest | Команда, обратная LODS, т.е. передает байт, слово или двойное слово из аккумулятора в цепочку и автоматически корректирует EDI.
SCAS dest | Команда сканирования цепочки. Команда вычитает элемент цепочки dst из содержимого аккумулятора (AL\AX\EAX) и модифицирует флаги. Префикс REPNE позволяет найти в цепочке нужный элемент.
CMPS dest,src | Команда сравнения цепочек. Данная команда производит вычитание байта, слова или двойного слова цепочки dst из соответствующего элемента цепочки src. В зависимости от результата вычитания модифицируются флаги. Регистры EDI и ESI автоматически продвигаются на следующий элемент. При использовании префикса REPE команда означает - сравнивать, пока не будет, достигнут конец цепочки или пока элементы не будут равны. При использовании префикса REPNE команда означает - сравнивать, пока не достигнут конец цепочки или пока элементы будут равны.

Команды управления флагами
CLC | Сброс флага переноса.
CMC | Инверсия флага переноса.
STC | Установка флага переноса.
CLD | Сброс флага направления.
STD | Установка флага направления.
CLI | Запрет маскируемых аппаратных прерываний.
STI | Разрешение маскируемых аппаратных прерываний.
CTS | Сброс флага переключения задач.

Команды передачи управления
JMP target | Имеет пять форм, различающихся расстоянием назначения от текущего адреса, и способом задания целевого адреса. При работе в Windows используется в основном внутрисегментный переход (NEAR) в пределах 32-битного сегмента. Адрес перехода может задаваться непосредственно (в программе это метка) или косвенно, т.е. содержаться в ячейке памяти или регистре (JMP [EAX]). Другой тип перехода - короткий переход (SHORT), занимает всего 2 байта. Диапазон смещения, в пределах которого происходит переход: -128 ... 127. Использование такого перехода весьма ограниченно. Межсегментный переход может иметь следующий вид: JMP FWORD PTR L, L - указатель на структуру, содержащую 48-битный адрес, в начале которого 32-й адрес смещения, затем 16-й селектор (сегмента, шлюза вызова, сегмента состояния задачи). Возможен также и такой вид межсегментного перехода: JMP FWORD ES:[EDI].
Условные переходы | JA/JNBE - перейти, если выше. JAE/JNB - перейти, если выше или равно. JB/JNAE - перейти, если ниже. JBE/JNA - перейти, если ниже. JC - перейти, если перенос. JE/JZ - перейти, если нуль. JG/JNLE - перейти, если больше. JGE/JNL - перейти, если больше или равно. JL/JNGE - перейти, если меньше. JLE/JNG - перейти, если меньше или равно. JNC - перейтИ, если нет переноса. JNE/JNZ - перейти, если меньше или равно. JNO - перейти, если нет переполнения. JNP/JPO - перейти, если нет паритета. JNS - перейти, если нет знака. JO - перейти, если есть переполнения. JP/JPE - перейти, если есть паритет. JS - перейти, если есть знак. JCXZ - переход, если СХ=0. JECXZ - переход, если ECX=0. В плоской модели команды условного перехода осуществляют переход в пределах 32-битного регистра.
Команды управления циклом. Все команды этой группы уменьшают содержимое регистра ECX. | LOOP - переход, если содержимое ECX не равно нулю. LOOPE (LOOPZ) - переход, если содержимое ECX не равно нулю и флаг ZF=1. LOOPNE (LOOPNZ) - переход, если содержимое ECX не равно нулю и флаг ZF=0.
CALL target | Передает управление процедуре (метке) с сохранением в стеке адреса, следующей за CALL командой. В плоской модели адрес возврата представляет собой 32-битное смещение. Межсегментный вызов предполагает сохранение в стеке селектора и смещения, т.е. 48-битной величины (16 бит - селектор и 32 бита - смещение).
RET [N] | Возврат из процедуры. Необязательный параметр N предполагает, что команда также автоматически чистит стек (освобождает N байт). Команда имеет разновидности, которые выбираются ассемблером автоматически, в зависимости от того, является процедура ближней или дальней. Можно, однако, и явно указать тип возврата (RETN или RETF). В случае плоской модели по умолчанию берется RETN с четырехбайтным адресом возврата.

Команды поддержки языков высокого уровня
ENTER par1,par2 | Подготовка стека при входе в процедуру (см. главу 1.2.).
LEAVE | Приведение стека в исходное состояние.
BOUND REG16,MEM16 или BOUND REG32,MEM32 | Предполагается, что регистр содержит текущий индекс массива, а второй операнда определяет в памяти два слова или два двойных слова. Первое считается минимальным значением индекса, а второе - максимальным. Если текущий индекс оказывается вне границ, то генерируется команда INT 5. Используется для контроля нахождения индекса в заданных рамках, что является важным средством отладки.

Команды прерываний
INT n | Двухбайтная команда. В начале в стек помещается содержимое регистра флагов, затем полный адрес возврата. Кроме того, сбрасывается флаг TF. После этого осуществляется косвенный переход через n-й элемент дескрипторной таблицы прерываний. Однобайтная команда INT 3 называется прерыванием контрольного останова и используется в программах-отладчиках.
INTO | Равносильна команде INT 4, если флажок переполнения OF=1, если OF=0 - команда не производит никакого действия.
IRET | Команда возврата из прерываний. Извлекает из стека сохраненные в нем адрес возврата и регистр флажков. Бит уровня привилегий будет модифицироваться только в том случае, если текущий уровень привилегий равен 0.

Команды синхронизации процессора
HLT | Останавливает процессор. Из такого останова процессор может быть выведен внешним прерыванием.
LOCK | Представляет собой префикс блокировки шины. Он заставляет процессор сформировать сигнал LOCK# на время выполнения находящейся за префиксом команды. Этот сигнал блокирует запросы шины другими процессорами в мультипроцессорной системе.
NOP | Холостая команда. Не производит никаких действий.
WAIT (FWAIT) | Синхронизация с сопроцессором. Большинство команд сопроцессора автоматически вырабатывают эту команду.

Команды обработки цепочки бит.

Эти команды появились в 386-м процессоре
BSF(BSR) dest,src | dest - 16-битный или 32-битный регистр. src - регистр или ячейка памяти. При выполнении команды BSF операнд src просматривается с младших, а в команде BSR - со старших бит. Номер первого встречного бита, находящегося в состоянии 1, помещается в регистр dest, флажок ZF сбрасывается в 0. Если src содержит 0, то ZF=1, а содержимое dest не определено.
ВТ dest,src | Тестирование бита с номером из src в dest и перенос его во флаг CF.
ВТС dest,src | Проверка и инвертирование бита из src в dest.
BTR dest,src | Проверка и сброс бита из src в dest.
BTS dest,src | Проверка и установка бита из src в dest.

Команды управления защитой
LGDT src | Загрузка GDTR из памяти. Src указывает на 6-байтную величину.
SGDT dest | Сохранить GDTR в памяти.
LIDT src | Загрузить IDTR из памяти.
SIDT dest | Сохранить IDTR в памяти.
LLDT src | Загрузить LDTR из памяти (16 бит).
SLDT dest | Сохранить LDTR в регистре или памяти (16 бит).
LMSW src | Загрузка MSW.
SMSW dest | Сохранить MSW в регистре или памяти (16 бит).
LTR src | Загрузка регистра задачи из регистра или памяти (16 бит).
STR dest | Сохранение регистра задачи в регистре или памяти (16 бит).
LAR dest,src | Загрузка старшего байта dest байтом прав доступа дескриптора src.
LSL dest,src | Загрузка dest лимитом сегмента, дескриптор которого задан src.
ARPL r/m,r | Выравнивание RPL в селекторе до наибольшего числа из текущего уровня и заданного операндом.
VERR seg | Верификация чтения: установка ZF=1, если задаче позволено чтение в сегменте SEG.
VERW seg | Верификация чтения: установка ZF=1, если задаче позволена запись в сегменте SEG.

Команды обмена с управляющими регистрами
MOV CRn,src | Загрузка управляющего регистра CRn.
MOV dest,CRn | Чтение управляющего регистра CRn.
MOV DRn,src | Загрузка регистра отладки DRn.
MOV dest,DRn | Чтение регистра отладки DRn.
MOV TRn,src | Загрузка регистра тестирования TRn.
MOV dest,TRn | Чтение регистра тестирования TRn.
RDTSC | Чтение счетчика тактов.

Команды идентификации и управления архитектурой
CPUID | Получение информации о процессоре. Требует параметр в регистре EAX. EAX=0, процессор в регистрах EBX,EDX,ECX возвращает символьную строку, специфичную для производителя. Процессоры AMD возвращают строку "AuthenticAMD", процессоры Intel - "Genuinelntel". EAX=1, в младшем слове регистра EAX возвращает код идентификации. EAX=2, в регистрах EAX, EBX, ECX, EDX возвращаются параметры конфигурации процессора.
RDMSR r/m | Чтение модельно-специфического регистра в ECX.
WRMSR r/m | Запись ECX в модельно-специфический регистр.

Команды управления кэшированием

Внутренний кэш появился в процессоре, начиная с 486-ого. Процессоры 486 и Pentium имеют внутренний кэш первого уровня, Pentium Pro и Pentium II имеют уже и вторичный кэш.

INVD | Аннулирование данных в первичном КЭШе без обратной записи.
WBINVD | Обратная запись модифицированных строк и аннулирование кэш-памяти.
INVLPG r/m | Аннулирование элемента таблицы трансляции TLB (TLB - буфер ассоциативной трансляции таблиц каталогов и страниц памяти).

Команды арифметического сопроцессора.

Описание работы арифметического сопроцессора см. [1,5]. Здесь мы коснемся основных положений работы арифметического сопроцессора.60

1. Арифметический сопроцессор работает со своим набором команд и своим набором регистров. Однако выборку команд сопроцессора осуществляет процессор.

2. Арифметический сопроцессор выполняет операции со следующими типами данных: целое слово (16 бит), короткое целое (32 бита), длинное слово (64 бита), упакованное десятичное число (80 бит), короткое вещественное число (32 бита), длинное вещественное число (64 бита), расширенное вещественное число (80 бит).

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

4. Сопроцессор имеет восемь 80-битных рабочих регистров, представляющих собой стековую кольцевую структуру. Регистры называются R0,R1, ... R7, но доступ к ним напрямую невозможен. Каждый регистр может занимать любое положение в стеке. Название стековых (относительных) регистров - ST(0), ST(1), ST(2), ST(3), ST(4), ST(5), ST(6), ST(7). Кроме того, имеется еще регистр состояния, по флагам которого можно, в частности, судить о результате выполненной операции. Регистр управления содержит в себе биты, влияющие на выполнение команд сопроцессора.

5. Регистр тэгов содержит 16 бит, описывающих содержание регистров сопроцессора: по два бита на каждый рабочий регистр. Тэг говорит о содержимом регистре данных. Вот значение тэгов: 00 - действительное ненулевое число, 01 - истинный нуль, 10 - специальные числа, 11 - отсутствие данных.

6. При вычислении с помощью команд сопроцессора большую роль играют исключения или особые ситуации. Типичной особой ситуацией является деление на 0. Биты особых ситуаций хранятся в регистре состояний. Учет особых ситуаций необходим для получения правильных результатов.

7. Список особых ситуаций.

  1. Неточный результат (округление).
  2. Недействительная операция.
  3. Деление на ноль.
  4. Антипереполнение (слишком маленький результат).
  5. Переполнение (слишком большой результат).
  6. Денормализованный операнд.

8. Регистр состояния.

  • 0-й бит, флаг недопустимой операции.
  • 1-й бит, флаг денормализованной операции.
  • 2-й бит, флаг деления на ноль.
  • 3-й бит, флаг переполнения.
  • 4-й бит, флаг антипереполнения.
  • 5-й бит, флаг неточного результата.
  • 6-й бит, ошибка стека.
  • 7-й бит, общий флаг ошибки.
  • 8,9,10-й, флаги условий.
  • 11,12,13-й, число, показывающее, какой регистр является вершиной.
  • 14-й бит, условный флаг.
  • 15-й бит, флаг занятости.

9. Регистр управления.

  • 0-й бит, маска недействительной операции.
  • 1-й бит, маска денормализованного операнда.
  • 2-й бит, маска деления на ноль.
  • 3-й бит, маска переполнения.
  • 4-й бит, маска антипереполнения.
  • 5-й бит, маска неточного результата.
  • 6,7-й бит, резерв.
  • 8-9-й биты, управление точностью.
  • 10,11-й биты, управление округлением.
  • 12-й, управление бесконечностью.
  • 13,14,15-й, резерв.

Команды передачи данных
FLD src | Загрузить вещественное число в ST(0) (вершину стека) из области памяти. Область памяти может быть 32-, 64-, 80-битная.
FILD src | Загрузить целое число в ST(0) из памяти. Область памяти может быть 16-, 32-, 64-битной.
FBLD src | Загрузить BCD-число в ST(0) из 80-битной области памяти.
FLDZ | Загрузить 0 в ST(0).
FLD1 | Загрузить 0 в ST(0).
FLDPI | Загрузить PI в ST(0).
FLDL2T | Загрузить LOG2(10) в ST(0).
FLDTL2E | Загрузить LOG2(e) в ST(0).
FLDLG2 | Загрузить LG(2) в ST(0).
FLDLN2 | Загрузить LN(2) в ST(0).
FST dest | Запись вещественного числа из ST(0) в память. Область памяти 32-, 64- или 80-битная.
FSTP dest | Запись вещественного числа из ST(0) в память. Область памяти 32-, 64- или 80-битная. При этом происходит выталкивание вершины из стека.
FBST dest | Запись BCD-числа в память. Область памяти 80-битная.
FBSTP dest | Запись BCD-числа в память. Область памяти 80-битная. При этом происходит выталкивание вершины из стека.
FXCH st(i) | Обмен значениями вершины стека и регистра i.

Команды сравнения данных
FCOM | Сравнение вещественных чисел ST(0) и ST(1). Флаги устанавливаются, как при операции ST(0)-ST(1).
FCOM src | Сравнение ST(0) с операндом в памяти. Операнд может быть 32- или 64-битным.
FCOMP src | Сравнение вещественного числа в ST(0) с операндом с выталкиванием ST(0) из стека. Операнд может быть регистром и областью памяти.
FCOMPP | Сравнение ST(0) и ST(1) с двойным выталкиванием из стека.
FICOM src | Сравнение целых чисел в ST(0) с операндом. Операнд может быть 16- или 32-битным.
FICOMP src | Сравнение целых чисел в ST(0) с операндом. Операнд может быть 16- или 32-битной областью памяти или регистром. При выполнении операции происходит выталкивание ST(0) из стека.
FTST | Проверка ST(0) на нуль.
FUCOM ST(i) | Сравнение ST(0) с ST(i) без учета порядков.
FUCOMP ST(i) | Сравнение ST(0) с ST(i) без учета порядков. При выполнении операции происходит выталкивание из стека.
FUCOMPP ST(i) | Сравнение ST(0) с ST(i) без учета порядков. При выполнении операции происходит двойное выталкивание из стека.
FXAM | Анализ содержимого вершины стека. Результат помещается в биты С3-С0. 000 - неподдерживаемый формат. 001 - не число. 010 - нормализованное число. 011 - бесконечность. 100 - нуль. 101 - пустой операнд. 110 - денормализованное число.

Арифметические команды
FADD src
FADD ST(i),ST 		Сложение вещественных чисел. ST(0)<-ST(0)+src, src - 32- или 64-битное число ST(i)<-ST(i)+ST(0)
FADDP ST(i),ST | Сложение вещественных чисел, ST(i)<-ST(i)+ST(0). При выполнении операции происходит выталкивание стека.
FIADD src | Сложение целых чисел. ST(0)<-ST(0)+src, src - 16- или 32-битное число.

FSUB src
FSUB ST(i),ST

Вычитание вещественных чисел. ST(0)<-ST(0)-src, src - 32- или 64-битное число. ST(i)<-ST(i)-ST(0).
FSUBP ST(i),ST | Вычитание вещественных чисел, ST(i)<-ST(i)-ST(0). При выполнении операции происходит выталкивание стека.
FSUBR ST(i),ST | Обратное вычитание вещественных чисел. ST(0)<-ST(i)-ST(0)
FSUBRP ST(i),ST | Обратное вычитание вещественных чисел. ST(0)<-ST(i)-ST(0). При выполнении операции происходит выталкивание стека.
FISUB src | Вычитание целых чисел. ST(0)<-ST(0)-src, src - 16- или 32-битное число.
FISUBR src | Вычитание целых чисел. ST(0)<-ST(0)-src, src - 16- или 32-битное число. При выполнении операции происходит выталкивание из стека.
FMUL
FMUL ST(i)
FMUL ST(i),ST 		Умножение двух операндов.
В первом случае ST(0)<-ST(0)*ST(1).
Во втором случае ST(0)<-ST(i)*ST(0).
В третьем случае ST(i)<-ST(i)*ST(0).
FMULP ST(i),ST(0) | ST(i)<-ST(i)*ST(0) умножение и выталкивание из стека.
FIMUL src | Умножение ST(0) на целое число. ST(0)<-ST(0)*src. Операнд может быть 16- и 32-битным числом.
FDIV
FDIV ST(i)
FDIV ST(i),SY 		ST(0)<-ST(0)/ST(1)
ST(0)<-ST(0)/ST(i)
ST(i)<-ST(0)/ST(i)
FDIVP ST(i),ST | Деление с выталкиванием из стека. ST(i)<-ST(0)/ST(i).
FIDIV src | Деление целых чисел. ST(0)<-ST(i)/src. Делитель может быть 16- и 32-битным числом.
FDIVR ST(i),ST | Обратное деление вещественных чисел. ST(0)<-ST(i)/ST(0)
FDIVRP ST(i),ST | Обратное деление вещественных чисел и выталкивание из стека. ST(0)<-ST(i)/ST(0)
FIDIVR src | Обратное деление целых чисел. ST(0)<-src/ST(0).
FSQRT | Извлечь корень из ST(0) и поместить обратно.
FSCALE | Масштабирование. ST(0)<-ST(0)*2^ST(1)
FEXTRACT | Выделение мантиссы и порядка из числа ST(0). В ST(0) помещается порядок, в ST(1) - мантисса.
FPREM | Нахождение остатка от деления. ST(0)<-ST(0)MODST(1).
FPREM1 | Нахождение остатка от деления в стандарте IEEE.
FRNDINT | Округление до ближайшего целого числа, находящегося в ST(0). ST(0)<-int(ST(0))
FABS | Нахождение абсолютного значения. ST(0)<-ABS(ST(0))
FCSH | Изменение знака ST(0)<- -ST(0)

Трансцендентные функции
FCOS | Вычисление косинуса. ST(0)<-COS(ST(0)). Содержимое в ST(0) интерпретируется как угол в радианах.
FPTAN | Частичный тангенс. Содержимое в ST(0) интерпретируется как угол в радианах. Значение тангенса возвращается на место аргумента, а затем в стек включается 1.
FPATAN | Вычисление арктангенса. Вычисляется функция Arctg(ST(1)/ST(0)). После вычисления функции происходит выталкивание из стека, после чего значение функции помещается в вершину.
FSIN | Вычисление синуса. ST(0)<-SIN(ST(0)). Содержимое в ST(0) интерпретируется как угол в радианах.
FSINCOS | Вычисление синуса и косинуса. ST(0)<-SIN(ST(0)) и ST(1)<-COS(ST(0))
F2XM1 | Вычисление 2^X-1. ST(0)<-2^ST(0)-1.
FYL2X | Вычисление Y*LOG2(X). ST(0)=Y, ST(1)=X. Происходит выталкивание из стека, и только потом в вершину стека помещается результат вычисления.
FYL2XP1 | Вычисление Y*LOG2(X). ST(0)=Y, ST(1)=X. Происходит выталкивание из стека, и только потом в вершину стека помещается результат вычисления.

Команды управления сопроцессором
FINIT | Инициализация сопроцессора.
FSTSW АХ | Запись слова состояния в АХ.
FSTSW dest | Запись слова состояния в dest.
FLDCW src | Загрузка управляющего слова (16 бит) из dest.
FSTCW dest | Сохранение управляющего слова в dest.
FCLEX | Сброс исключений.
FSTENV dest | Сохранение состояния оборудования (SR, CR, TAGW, FIP, FDP) в памяти.
FLDENV src | Загрузка состояния оборудования из памяти.
FSAVE dest | Сохранение состояния оборудования и файла регистров в памяти.
FRSTOR src | Загрузка состояния оборудования и файла регистров в памяти.
FINCSTP | Инкремент указателя стека.
FDECSTP | Декремент указателя стека.
FFREE ST(i) | Освобождение регистра - пометка ST(i) как свободного.
FNOP | Холостая операция сопроцессора.
WAIT (FWAIT) | Ожидание процессором завершение текущей операции сопроцессором.

Расширение MMX.

Расширение MMX ориентировано в основном на использование в мультимедийных приложениях. Основная идея MMX заключается в одновременной обработке нескольких элементов данных за одну инструкцию. Расширение MMX появилось в процессорах модификации Pentium P54C и присутствует во всех последних модификациях этого процессора.

Расширение MMX использует новые типы упакованных данных: упакованные байты (восемь байт), упакованные слова (четыре слова), упакованные двойные слова (два двойных слова), учетверенное слово. Расширение MMX включает восемь регистров общего пользования (MM0-MM7). Размер регистров составляет 64 бита. Физически эти регистры пользуются младшими битами рабочих регистров сопроцессора. Команды MMX "портят" регистр состояния и регистр тэгов. По этой причине совместное использование команд MMX и команд сопроцессора может вызвать определенные трудности. Другими словами, перед каждым использованием команд MMX Вам придется сохранять контекст сопроцессора, а это может весьма замедлить работу программы. Важно отметить также, что команды MMX работают непосредственно с регистрами сопроцессора, а не с указателями на элементы стека.

Команды MMX расширения (по книге [3])

EMMS | Очистка стека регистров. Установка всех единиц в слове тегов.
MOVD mm,m32/ir32 | Пересылка данных в младшие 32 бита регистра MMX с заполнением старших бит нулями.
MOVD m32/ir32,mm | Пересылка данных из младших 32 бит регистра MMX.
MOVQ mm,mm/m64 | Пересылка данных в регистр MMX.
MOVQ mm/m64,mm | Пересылка данных из регистра MMX.
PACKSSDW mm,mm/m64 | Упаковка со знаковым насыщением двух двойных слов, расположенных в mm, и двух двойных слов mm/m64 в четыре слова, расположенных в mm.
PACKSSWB mm,mm/m64 | Упаковка со знаковым насыщением четырех слов, расположенных в mm, и четырех слов mm/m64 в восемь байт, расположенных в mm.
PACKUSWB mm,mm/m64 | Упаковка с насыщением четырех знаковых слов, расположенных в mm, и четырех слов mm/m64 в восемь беззнаковых байт, расположенных в mm.
PADDB mm,mm/m64
PADDW mm,mm/m64
PADDD mm,mm/m64 		Сложение упакованных байт (слов или двойных слов) без насыщения (с циклическим переполнением).
PADDSB mm,mm/m64
PADDSW mm,mm/m64 		Сложение упакованных байт (слов) со знаковым насыщением.
PADDUSB mm,mm/m64
PADDUSW mm,mm/m64 		Сложение упакованных байт (слов) с беззнаковым насыщением.
PAND mm,mm/m64 | Логическое "И".
PANDN mm,mm/m64 | Логическое "И-НЕ".
PCMPEQB mm,mm/m64
PCMPEQD mm,mm/m64
PCMPEQW mm,mm/m64 		Сравнение (на равенство) упакованных байт (слов, двойных слов). Все биты элемента результата будут единичными (true) при совпадении соответствующих элементов операндов и нулевыми (false) - при несовпадении.
PCMPGTB mm,mm/m64
PCMPGTD mm,mm/m64
PCMPGTW mm,mm/m64 		Сравнение (по величине) упакованных знаковых байт (слов, двойных слов). Все биты элемента результата будут единичными (true), если соответствующий элемент операнда назначения больше элемента операнда источника, и нулевыми (false) в противном случае.
PMADDWD mm,mm/m64 | Умножение четырех знаковых слов операнда источника на четыре знаковых слова операнда назначения. Два двойных слова результатов умножения младших слов суммируются и записываются в младшее двойное слово операнда назначения. Два двойных слова результатов умножения старших слов суммируются и записываются в старшее двойное слово операнда назначения.
PMULHW mm,mm/m64 | Умножение упакованных знаковых слов с сохранением только старших 16 бит элементов результата.
PMULLW mm,mm/m64 | Умножение упакованных знаковых или беззнаковых слов с сохранением только младших 16 бит результата.
POR mm,mm/m64 | Логическое "ИЛИ".
PSHIMD mm,imm
PSHIMQ mm,imm
PSHIMW mm,imm 		PSHIMD представляет инструкции PSLLD, PSRAD и PSRLD с непосредственным операндом-счетчиком.
PSHIMW представляет инструкции PSLLW, PSRAW, PSRLW.
PSHIMQ представляет инструкции PSLLQ и PSRLQ с непосредственным операндом-счетчиком.
PSLLD mm,mm/m64
PSLLQ mm,mm/m64
PSLLW mm,mm/m64 		Логический сдвиг влево упакованных слов (двойных, учетверенных) операнда назначения на количество бит, указанных в операнде-источнике, с заполнением младших бит нулями.
PSRAD mm,mm/m64
PSRAW mm,mm/m64 		Арифметический сдвиг вправо упакованных двойных (учетверенных) знаковых слов операнда назначения на количество бит, указанных в операнде-источнике, с заполнением младших бит битами знаковых разрядов.
PSRLD mm,mm/m64
PSRLQ mm,mm/m64
PSRLW mm,mm/m64 		Логический сдвиг вправо упакованных слов (двойных, учетверенных) операнда назначения на количество бит, указанных в операнде-источнике, с заполнением старших бит нулями.
PSUBB mm,mm/m64
PSUBW mm,mm/m64
PSUBD mm,mm/m64 		Вычитание упакованных байт (слов или двойных слов) без насыщения (с циклическим антипереполнением).
PSUBSB mm,mm/m64
PSUBSW mm,mm/m64 		Вычитание упакованных знаковых байт (слов) с насыщением.
PSUBUSB mm,mm/m64
PSUBUSW mm,mm/m64 		Вычитание упакованных беззнаковых байт (слов) с насыщением.
PUNPCKHBW mm,mm/m64 | Чередование в регистре назначения байт старшей половины операнда-источника с байтами старшей половины операнда назначения.
PUNPCKHWD mm,mm/m64 | Чередование в регистре назначения слов старшей половины операнда-источника со словами старшей половины операнда назначения.
PUNPCKHDQ mm,mm/m64 | Чередование в регистре назначения двойного слова старшей половины операнда-источника с двойным словом старшей половины операнда назначения.
PUNPCKLBW mm,mm/m64 | Чередование в регистре назначения байт младшей половины операнда-источника с байтами младшей половины операнда назначения.
PUNPCKLWD mm,mm/m64 | Чередование в регистре назначения слов младшей половины операнда-источника со словами младшей половины операнда назначения.
PUNPCKLDQ mm,mm/m64 | Чередование в регистре назначения двойного слова младшей половины операнда-источника с двойным словом младшей половины операнда назначения.
PXOR mm,mm/m64 | Исключающее "ИЛИ".

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


Приложения: Приложение 3. Справочник по командам и архитектуре Pentium

Приложение 3. Защищенный режим микропроцессора Pentium

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

В отличие от реального режима, сегментные регистры содержат в защищенном режиме не адреса, а селекторы. Рассмотрим структуру селектора:

  • 0-1-й биты, запрошенный программой уровень привилегий;
  • 2-й бит, определяет, использовать глобальную таблицу дескрипторов GDT (0) или локальную таблицу дескрипторов LDT (1);
  • 3-16-й биты, индекс дескриптора в таблице.
Дескриптор для защищенного режима - это 64-битная структура, которая может описывать сегмент кода, сегмент данных, сегмент состояния задачи, шлюз вызова, ловушки, прерывания или задачи. Дескриптор в глобальной дескрипторной таблице может описывать локальную дескрипторную таблицу.

Дескриптор кода и данных
Биты 24-31
базы сегмента 		Биты
доступа 		Базовый адрес
24 бита 		Предел
16 бит

Базовый адрес сегмента содержит физический адрес сегмента.

Предел содержит размер сегмента в байтах, уменьшенный на единицу.

Описание других битов.

6-й байт.

  • Биты 0-3 определяют биты 16-19 предела.
  • Бит 4 зарезервирован для операционной системы.
  • Бит 5 равен 0.
  • Бит 6 - разрядность: 0 - 16-битный, 1 - 32-битный.
  • Бит 7 - гранулярность: 0 - лимит в байтах, 1 - лимит в 4-килобайтных величинах.

5-й байт.

  • Бит 0, если 1, то к сегменту было обращение.
  • Бит 1 - разрешение чтения для кода, записи для данных.
  • Бит 2 - бит подчиненности для кода, бит расширения для данных.
  • Бит 3 - тип сегмента (0 - данные, 1 - код).
  • Бит 4 - тип дескриптора (1 - не системный).
  • Биты 5-6 - уровень привилегий дескриптора.
  • Бит 7 - бит присутствия сегмента.

Другие дескрипторы

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

  • 0 - зарезервированный тип.
  • 1 - свободный 16-битный TSS (TSS - сегмент состояния задачи).
  • 2 - дескриптор таблицы LDT.
  • 3 - занятый 16-битный TSS.
  • 4 - 16-битный шлюз вызова.
  • 5 - шлюз задачи.
  • 6 - 16-битный шлюз прерывания.
  • 7 - 16-битный шлюз ловушки.
  • 8 - зарезервировано.
  • 9 - свободный 32-битный TSS.
  • 10 - зарезервировано.
  • 11 - занятый 32-битный TSS.
  • 12 - 32-битный шлюз вызова.
  • 13 - зарезервировано.
  • 14 - 32-битный шлюз прерывания.
  • 15 - 32-битный шлюз ловушки.

Команды CALL или JMP на адрес с селектором, указывающим на дескриптор шлюза, приводит к передаче управления по адресу, указанному в дескрипторе. Если селектор указывает на шлюз задачи, то это приводит к переключению задач. Обычные же переходы JMP, CALL, RET, IRET возможны лишь к сегментам с тем же уровнем привилегий, либо более низким уровнем привилегий.

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

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

Четырехуровневая система привилегий

Уровни привилегий нумеруются от 0 до 3. Номер 3 является самым низким уровнем привилегий. Нулевой привилегией обладает ядро операционной системы. Уровни привилегий относятся к дескрипторам, селекторам и задачам. В регистре флагов имеется поле привилегий ввода-вывода, которое регулирует управление доступом к инструкциям ввода-вывода. Уровень привилегий задачи определяется двумя младшими битами сегмента CS. При страничной адресации имеется всего два уровня доступа - 3 и 0.

Страничное управление памятью

Механизм со страничным управлением памятью включается установкой бита PG в регистре CR0. Регистр CR2 хранит линейный адрес отказа и адрес памяти, по которому был обнаружен последний отказ страницы. Регистр CR3 хранит физический адрес каталога страниц. Младшие 12 бит этого регистра всегда равны нулю (выравнивание по границе страниц). Каталог страниц состоит из 32-битных элементов и имеет длину 4 Кб.
20 старших бит
адреса таблицы
следующего уровня 		Резерв
3
бита 		G P
S 		D A P
C
D 		P
W
T 		U
/
S 		R
/
W 		P

Каждая таблица страниц также имеет размер 4 Кб и элементы аналогичного формата. Но эти элементы содержат базовый адрес самих страниц и атрибуты страниц. Физический адрес собирается из базового адреса и младших 12 бит линейного адреса. Значение атрибутов страниц:

  • G - глобальная страница, страница не удаляется из буфера.
  • PS - размер страницы. Если 1, то размер страницы 4 Mb*
  • D - грязная страница. Устанавливается в 1 при записи на страницу.
  • А - бит доступа. Устанавливается в 1 при любом обращении к странице.
  • PCD - бит запрещения кэширования.
  • PWT - бит разрешения сквозной записи.
  • U/S - страница или таблица доступна для программ с уровнем доступа 3.
  • R/W - страница/таблица доступна для записи.
  • P - страница/таблица присутствуют.
* Автор не упомянул (точнее в оригинале опечатка :), но следует добавить, что 4 Мб - при нормальной 32-битной адресации, и 2 Мб - при разрешенной расширенной физической адресации).


Список литературы

Источники, отмеченные (F), дошли до меня в виде файлов.

  1. В.Ю. Пирогов. Assembler. Учебный курс. М., 2001 г.
  2. Мэтт Питрек. Секреты системного программирования в Windows 95. К., 1996
  3. Михаил Гук. Процессоры Intel от 8086 до Pentium II. С.-П., 1998 г.
  4. Герберт Шилдт. Программирование на С и C++ для Windows 95. К., 1996 г.
  5. В.Л. Григорьев. Микропроцессор i486. Архитектура и программирование (в 4-книгах). М., 1993 г.
  6. П.Брамм, Д.Брамм. Микропроцессор 80386 и его программирование. М., 1990
  7. Barry Kauler. Windows Assembly Language and Systems Programming. (F).
  8. И.И.Шагурин, В.Б.Бродин, Г.П.Мозговой. 80386. Описание и система команд М.,1992.
  9. Б.Э. Смит, М.Т. Джонсон. Архитектура и программирование микропроцессоpa INTEL 80386. М., 1992.
  10. С.П. Морс, Д.Д. Альберт. Архитектура микропроцессора 80286. М., 1990.
  11. Э.Страус. Микропроцессор 80286. М., 1992.
  12. Ч.Петзольд. Программирование для Windows. П., 1997.
  13. Зубков С.В. Assembler для DOS, Windows и Unix. M., 2000.
  14. Р.Дункан. Оптимизация программ на ассемблере. Барнаул, 1993. (F).


Алфавитный указатель

А

API-программирование

F

FAT32

H

Нех-редакторы [ref1], Hiew.exe

M

MMX расширение

А

Арифметический сопроцессор [Ref1], [Ref2]

Ассемблер [Ref1], [Ref2], [Ref3]

Б

Библиотекарь

В

Взаимодействие потоков

Взаимодействие процессов

Виртуальный драйвер

Всплывающие подсказки

Д

Двоичные файлы

Дескриптор приложения

Дескриптор процесса

Диалоговое окно

Дизассемблеры [Ref1], [Ref2]

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

Директивы времени выполнения

Директивы макроассемблера

Драйверы

Драйверы виртуальные

З

Защищенный режим

И

Идентификатор процесса

К

Команды микропроцессора

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

Критические секции

Л

Ловушки

Локальные метки

Локальные переменные [Ref1], [Ref2]

М

Макроассемблер

Макроопределения

Многопоточные приложения

Модальные диалоговые окна

Н

Немодальные диалоговые окна

О

Отладчики [Ref1], [Ref2]

П

Плоская модель памяти

Поиск процедуры окна

Потоки

Процедура окна

Процедуры обратного вызова

Процессы

Р

Регистры микропроцессора

Редактор связей [Ref1], [Ref2]

Рекурсивный поиск [Ref1], [Ref2]

Ресурсы

Ресурсы локальной сети

С

Связывание неявное

Связывание по имени

Связывание по порядковому номеру

Связывание позднее

Связывание раннее

Связывание явное

Сегментная адресация [Ref1], [Ref2]

Семафоры

События

Соглашение о вызовах

Сообщения операционной системы Windows

Стека структура

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

Т

Таймер

Таймер в консольном приложении

Текстовый файл

Транслятор ресурсов

У

Упрощенная сегментация

Условное ассемблирование

Ф

Файлы

Файлы отображаемые

Фильтры

Функции API

Ц

Цикл обработки сообщений


Пирогов Владислав Юрьевич

Ассемблер для Windows

Верстка: Матусовская Е.В.
Художник: Леонова Ю.В.

Издатель Молгачева С. В.
Лицензия ИД № 03567 от 19 декабря 2000 г.

www.knowledge.ru