Cамоучитель по Assembler

         

Часть III. Более сложные примеры программирования в Windows

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

Таймер является одним из мощных инструментов, предоставляемых операционной системой и позволяющих решать самые разнообразные задачи. С таймером Вы познакомились, когда занимались консольными приложениями. Там мы пользовались функциями timeSetEvent и timeKillEvent. Для консольных приложений это очень удобные функции. В оконных приложениях чаще используют функции SetTimer и KillTimer. Особенность таймера, создаваемого функцией SetTimer, заключается в том, что сообщение WM_TIMER, которое начинает посылать система приложению после выполнения функции SetTimer, приходит со всеми другими сообщениями наравне, на общих основаниях. Следовательно, интервал между двумя приходами сообщения WM_TIMER может несколько варьироваться. В большинстве случаев это не существенно.

У сообщения таймера есть еще одна особенность. Если система посылает сообщение приложению, а предыдущее сообщение еще стоит в очереди, то система объединяет эти два сообщения. Таким образом, "вынужденный простой" не приводит к приходу на приложение подряд нескольких сообщений таймера.

Вот те задачи, которые можно решить с помощью таймера.


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

Рассмотрим, как нужно обращаться с функцией SetTimer. Вот параметры этой функции.
1-й параметр - дескриптор окна, с которым ассоциируется таймер. Если этот параметр сделать равным NULL (0), то будет проигнорирован и второй параметр. 2-й параметр - определяет идентификатор таймера. 3-й параметр - определяет интервал посылки сообщения WM_TIMER. 4-й параметр - определяет адрес функции, на которую будет приходить сообщение WM_TIMER. Если параметр равен NULL, то сообщение будет приходить на функцию окна.

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

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

    Задан дескриптор окна, а четвертый параметр задается равным нулю. Задан дескриптор окна, а четвертый параметр определяет функцию, на которую будет приходить сообщение WM_TIMER. Дескриптор окна равен 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. С самой подсказкой более или менее ясно. Определимся теперь, как и где будет вызываться эта подсказка. Мне более импонирует такой подход: в основном диалоговом окне определяем таймер, в функции которого и будет проверяться положение курсора. В зависимости от этого положения и будет вызываться или удаляться подсказка. В функции таймера должно предусмотреть:

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

На Рис. 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.

Глава 2. Многозадачное программирование

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

I

Под процессом будем понимать объект, создаваемый операционной системой Windows обычно при загрузке исполняемого модуля и получающий в единоличное пользование:

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

Думаю, данное определение весьма ясно раскрывает суть понятия процесс. Но для большинства рассматриваемых в данной главе проблем, достаточно было бы дать и более простое определение. Например, такое: всякий исполняемый модуль (.ЕХЕ), запущенный в операционной системе 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 осуществляет поиск программы по определенному алгоритму:
    Поиск в каталоге, откуда была запущена программа. Поиск в текущем каталоге. Поиск в системном каталоге (можно получить через GetSystemDirectory). Обычно системным каталогом является C:\WINDOWS\SYSTEM. Поиск в каталоге Windows (можно получить через GetWindowsDirectory). Обычно этим каталогом является C:\WINDOWS. Поиск в каталогах, перечисленных в параметре 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_USESHOWWINDOW1hРазрешить поле dwShowWindow
STARTF_USESIZE2hРазрешить dwXSize и dwYSize
STARTF_USEPOSITI0N4hРазрешить dwX и dwY
STARTF_USECOUNTCHARS8hРазрешить dwXCountChars и dwYCountChars
STARTF_USEFILLATTR1BUTE10hРазрешить dwFillAttribute
STARTF_FORCEONFEEDBACK40hВключить возврат курсора
STARTF_FORCEOFFFEEDBACK80hВыключить возврат курсора
STARTF_USESTDHANDLES100hРазрешить 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 проблема совместного функционирования резидентных программ стояла весьма остро. Не смотря на то, что программисты, их писавшие, добивались весьма больших успехов, все же одновременная работа нескольких резидентных программ часто приводила к весьма заметным конфликтам.


Глава 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:

    Поиск в каталоге, откуда была запущена программа. Поиск в текущем каталоге. В системном директории (GetSystemDirectory). В директории Windows (GetWindowsDirectory). В каталогах, указанных в окружении (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









Глава 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

    Рассматривая программу на Рис. 3.4.3, прежде всего обратите внимание на использование локальных меток. MASM распознает локальные метки автоматически. TASM требует, чтобы локальная метка имела в начале два символа "@@". Кроме того, в начале программы требуется директива LOCALS. Отметим одну существенную особенность функции WNetAddConnection2A@16. При заполнении структуры NETRESOURCE поле dwType для операционной системы Windows NT должно заполняться нулем, а не RESOURCETYPE_DISK. К сожалению, имеются и другие подобные нюансы при работе с ресурсами локальной сети. Если Вы всерьез займетесь программированием в локальной сети, Вам придется проверять свою программу на разных компьютерах и разных сетях. Данная программа далека от совершенства. Попробуйте над ней поработать. В частности, не мешало бы проверять, является ли локальное устройство сетевым или нет, и если является, спросить разрешение на переподключение данного устройства. В этом случае необходимо вначале отключить устройство от старого сетевого ресурса при помощи известной Вам функции WNetCancelConnection2. Если необходимо подключать сетевой принтер, то поле 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, ему необходимо самостоятельно написать несколько программ. Мои программы должны служить отправными точками.

    Нам уже приходилось сталкиваться с локальными переменными, когда мы рассматривали поиск файлов по дереву каталогов. Данная задача весьма похожа, но есть и отличие. В данном случае мы используем слишком большой объем для локальных переменных. По этой причине мы явно указываем (заказываем) большой объем стека (опции STACK и S,Sc). По умолчанию компоновщик устанавливает всего 8 Кб, что явно не достаточно. Функция WNetEnumResource требует указать своим параметром массив структур NETRESOURCE. Объем одной структуры 32 байта. Мы резервируем тысячу таких структур. Не много ли, скажете Вы? Честно говоря, я не встречал локальной сети с тысячью сетевых компьютеров. Однако я встречал локальную сеть, где на одном из серверов было создано около восьмисот сетевых каталогов. Если говорить начистоту, то здесь я все же демонстрирую не лучший стиль программирования. Более корректный путь заключается в том, что функция WNetEnumResource вначале вызывается с указанием объема буфера меньше, чем 32 байта, - в этом случае в переменную, содержащую объем буфера, будет возвращен необходимый объем. Зная необходимый объем, программа должна запросить у системы нужный и вторично запустить WNetEnumResource. Данный подход более корректен, но более сложен. При рекурсивном вызове процедуры POISK первым параметром является указатель на элемент массива структур NETRESOURCE. Мы копируем элемент массива в локальную переменную NR1. В принципе, в данной программе можно этого не делать, а сразу воспользоваться полученным указателем. В более сложных программах, однако, скорее всего, придется этот делать. Обратите внимание, что в процедуре вывода информации мы копируем строку в буфер, который потом используем для вывода. Это не прихоть, а необходимость. Дело в том, что перед выводом мы добавляем в конец строки коды 13 и 10. Поскольку мы выводим строки, которые потом используем для дальнейшего поиска, нам приходится использовать для вывода дополнительный буфер.

Что осталось за бортом?

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

    Сетевое программирование на сервере Windows NT. Изучение сетевых протоколов как низкого (IPX, TCP/IP, NetBios и т.д.), так и высокого уровня (HTTP, FTP и т.д.). Программирование сокетов.

44 Сейчас часто под термином сетевое программирование понимают программирование в глобальной сети Интернет, либо использование подходов и средств Интернет в локальной сети (Интранет). Мы, в данном случае, ведем речь о достаточно узкой области задач, касающейся проблем только локальной сети.










Глава 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

В. Есть ли дополнительные средства, упрощающие файловую обработку?

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

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

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

    Открыть (или создать) файл с помощью обычной функции CreateFile. Функция, как известно, возвращает дескриптор открытого файла. Отобразить файл45 с помощью функции CreateFileMapping. Именно эта функция определяет размер отображения. Эта функция, как и предыдущая, возвращает дескриптор, только не обычного, а отображенного файла. Скопировать файл (или его часть) в созданную область при помощи функции MapViewOfFile. Эта функция возвращает указатель (вот оно!) на начало области, где будет расположен файл. После этого руки у Вас развязаны, и Вы можете делать с отображенным файлом все, что Вам заблагорассудится. При желании можно записать область памяти в файл при помощи функции FlushViewOfFile. Разумеется, сбрасываться на диск будет та область памяти, которую мы заказали при помощи функции CreateFileMapping. Записывать на диск можно, разумеется, и с помощью обычной функции WriteFile. Перед тем как закрывать отображенный файл, следует вначале сделать указатель недействительным. Это делается с помощью функции UnmapViewOfFile. Закрыть следует оба дескриптора. Вначале дескриптор, возвращенный функцией 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-й параметр. Указатель на структуру, определяющую атрибут доступа. Обычно NULL (0). 2-й параметр. Флаг. В случае ненулевого значения процесс требует немедленного владения объектом (!). 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. Вот основные положения, которые позволят Вам быстро разобраться в сути проблемы.

    Печать очень схожа с выводом на экран. При этом используются те же функции вывода, например TextOut или Ellipse. Как и в случае с экраном, для этого необходимо знать контекст принтера. Контекст принтера создается, а не получается, как в случае с экраном. Для этого используется функция CreateDC, у которой второй параметр - это указатель на строку, содержащую имя принтера. Все остальные три параметра, как правило, равны NULL. Функция PrintDlg позволяет выбрать название принтера в диалоге. При этом она возвращает тот контекст принтера. Если Вы все же решили использовать функцию CreateDC, Вам крайне необходимо узнать имена принтеров, которые есть в системе. Для этого Вам как раз подойдет функция EnumPrinters. Это весьма мощное средство может определить не только локальные, но и сетевые принтеры. Начало печати документа осуществляется функцией StartDoc. Конец печати - EndDoc. Эти две функции обрамляют блок, осуществляющий печать. В пределах этого блока можно выделять также страницы при помощи функций StartPage и EndPage. По окончании печати следует удалить контекст при помощи функции 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.

Глава 6. Некоторые вопросы системного программирования в Windows

Большая часть материала этой главы будет посвящена управлению памятью Windows 9x. Данный материал требует от читателя некоторой подготовки в области так называемого защищенного режима микропроцессоров Intel. Более подробно о защищенном режиме можно узнать в книгах [1,5].

I

О страничной и сегментной адресации. Начну изложение с некоторого исторического экскурса. Семейство микропроцессоров Intel47 ведет свое начало с микропроцессора Intel 8086. В настоящее время во всю работает уже седьмое поколение. Каждое новое поколение отличалось от предыдущего в программном отношении, главным образом, расширением набора команд. Но были в этой восходящей лестнице и две ступени, сыгравшие огромную роль в развитии компьютеров на базе микропроцессоров Intel. Это микропроцессор 80286 (защищенный режим) и микропроцессор 80386 (страничная адресация).

До появления микропроцессора 80286 микропроцессоры использовались в так называемом реальном режиме адресации. Кратко изложу, в чем заключался этот режим. Для программирования использовался так называемый логический адрес, состоящий из двух 16-битных компонент: сегмента и смещения. Сегментный адрес мог храниться в одном из трех сегментных регистров CS,DS, SS,ES. Смещение хранилось в одном из индексных регистров DI, SI,BX,BP,SP48. При обращении к памяти логический адрес подвергался преобразованию, заключающемуся в том, что к смещению прибавлялся сегментный адрес, сдвинутый на четыре бита влево. В результате получался 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.

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

Подытожим, что же давал нам защищенный режим.

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

Обратимся опять к Рис. 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 можно перевести как ловушка, да и по смыслу это ближе к понятию ловушка.



Глава 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.

Явное указание параметров в заголовке процедуры приводит:

    К имени процедуры в объектном модуле автоматически добавляется @8 (а не @0). Ассемблер автоматически управляется со стеком.

Трансляция.

    В проект Visual C++ включается уже объектный модуль, откомпилированный первоначально с помощью ml.exe. Если Вы хотите включить в проект ассемблерный текст. Вам придется для него указать способ трансляции, т.е. командную строку для программы 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

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