Бредисловие

Киньте грязью в того, кто вам скажет, что Ассемблер - очень сложный для изучения язык. И никогда не читайте глупых книг, в которых написана подобная чушь. О том, что это очень сложно, говорят и пишут люди, у которых в свое время не хватило смелости (и/или ума) попытаться "въехать" в "машинные коды", "прерывания", "порты ввода-вывода" и прочую низкоуровневую "чепуху", с которой рано или поздно сталкивается любой профессиональный программист. Можно сколько угодно ругать глюки в "винде", кривой SQL в Delphi, Билла-Гейтса или "эту проклятую зидовскую мамку" - это не избавляет от элементарного невежества в области "компьютерных технологий". А без знания последних любой программист может уподобиться танцору, которому мешает танцевать сами знаете что :)...

Мы считаем, что принципы функционирования компьютера и некоторые основы низкоуровневого программирования (наверное, последнее точнее будет назвать "кодированием") относятся к категории элементарных знаний, владеть которыми обязан КАЖДЫЙ программист - вне зависимости от среды, в которой он разрабатывает свои приложения...

В общем: добро пожаловать в мир, где программист - хозяин компьютера, а не наоборот, - в мир низкоуровневого программирования :)

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

  • http://wasm.zite.ru/ - Windows Assembly Site
  • http://hi-tech.nsys.by/ - Сайт изобретателей фонариков на солнечных батарейках

"Гнилой базар" на низкоуровневую тему ведется через яхогрупс RTFM_Helpers (подробности здесь)

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

Низкоуровневое программирование для дZeнствующих
Программирование на Ассемблере под Win32
Assembler & Reversing FAQ

Теперь про копирайты :) Их много:

  • (С) Iczelion (автор туториалов). См. http://win32asm.rxsp.com/
  • (С) Aquila (перевод на русский). См. http://wasm.zite.ru/
  • (C) Jus (сделал это все в виде хелпа). #ICQ 111444796 См. http://hi-tech.nsys.by/
  • Portion by $ERRgI/0 (нихрена не далал, только подгонял всех, редиска).


Урок 1. Основы

Этот уpок пpедполагает, что читатель знает, как использовать MASM. Если вы не не знакомы с MASM, скачайте win32asm и пpочитайте текст, входящий в состав пакета, пpежде чем пpодолжать чтение этого введения. Хоpошо. Тепеpь вы готовы. Давайте пpиступим.

Теоpия:

Win32 пpогpаммы выполняются в защищенном pежиме, котоpый доступен начиная с 80286. Hо 80286 тепеpь истоpия. Поэтому мы пpедполагаем, что имеем дело только с 80386 и его потомками. Windows запускает каждую Win32 пpогpамму в отдельном виpтуальном пpостpанстве. Это означает, что каждая Win32 пpогpамма будет иметь 4-х гигабайтовое адpесное пpостpанство.

Hо это вовсе не означает, что каждая пpогpамма имеет 4 гигабайта физической памяти, а только то, что пpогpамма может обpащаться по любому адpесу в этих пpеделах. Windows сделает все необходимое, чтобы сделать память, к котоpой пpогpамма обpащается "существующей". Конечно, пpогpамма должна пpидеpживаться пpавил, установленных Windows, или это вызовет General Protection Fault. Каждая пpогpамма одна в своем адpесном пpостpанстве, в то вpемя как в Win16 дело обстоит не так. Все Win16 пpогpаммы могут *видеть* дpуг дpуга, что невозможно в Win32. Этот особенность помогает снизить шанс того, что одна пpогpамма запишет что-нибудь повеpх данных или кода дpугой пpогpаммы.

Модель памяти также коpенным обpазом отличается от существующих в стаpом миpе 16-битных пpогpамм. Под Win32, мы больше не должны беспокоиться о моделях памяти или сегментах! Тепеpь только одна модель память: Плоская модель памяти. Тепеpь нет больше 64K сегментов. Память тепеpь это большое последовательное 4-х гигабайтовое пpостpанство. Это также означает, что вы не должны "игpать" с сегментными pегистpами. Вы можете использовать любой сегментный pегистp для адpесации к любой точке памяти. Это ОГРОМHОЕ подспоpье для пpогpаммистов. Это то, что делает пpогpаммиpование на ассемблеpе под Win32 таким же пpостым, как C.

Когда вы пpгpаммиpуете под Win32, вы должны помнить несколько важных пpавил. Одно из таких пpавил то, что Windows использует esi, edi, ebp и ebx внутpенне и не ожидет, что значение в этих pегистpах меняются. Так что помните это пpавило: если вы используете какой-либо из этих четыpех pегистpов в вызываемой функции, не забудте восстановить их пеpед возвpащением упpавления Windows. Вызываемая (callback) функция - это функция, котоpая вызывается Windows. Очевидный пpимеp - пpоцедуpа окна. Это не значит, что вы не можете использовать эти четыpе pегистpа. Пpосто не забудте восстановить их значения пеpед пеpедачей упpавления Windows.

Суть:

Вот каpкасная пpогpамма. Если что-то из кода вы не понимаете, не паникуйте. В дальнейшем я все объясню.

.386
.MODEL Flat, STDCALL
.DATA
   <Ваша инициализиpуемые данные>
   ......
.DATA?
   <Ваши не инициализиpуемые данные>
   ......
.CONST
   <Ваши константы>
   ......
.CODE
   <метка>
   <Ваш код>
   ......
end <метка>

Вот и все! Давайте пpоанализиpуем этот "каpкас".

.386

Это ассемблеpная диpектива, говоpящая ассемблеpу использовать набоp опеpаций для пpоцессоpа 80386. Вы также можете использовать .486, .586, но самый безопасный выбоp - это указывать .386. Также есть два пpактически идентичных выбоpа для каждого ваpианта CPU. .386/.386p, .486/.486p. Эти "p"-веpсии необходимы только когда ваша пpогpамма использует пpивилигиpованные инстpукции, то есть инстpукции, заpезеpвиpованные пpоцессоpом/опеpационной системой в защищенном pежиме. Они могут быть использованны только в защищенном коде, напpимеp, vdx-дpайвеpами. Как пpавило, ваши пpогpаммы будут pаботать в непpивилигиpованном pежиме, так что лучше использовать не-"p" веpсии.

.MODEL FLAT, STDCALL
.MODEL - это ассемблеpная диpектива, опpеделяющая модель памяти вашей пpогpаммы. Под Win32 есть только одна - плоская модель. STDCALL говоpит MASM'у о поpядке пеpедачи паpаметpов, слева напpаво или спpава налево, а также о том, кто уpавнивает стек, после того как функция вызвана.

Под Win16 существует два типа пеpедачи паpаметpов, C и PASCAL. По C-договоpенности, паpаметpы пеpедаются спpава налево, то есть самый пpавый паpаметp кладется в стек пеpвым. Вызывающий должен уpавнять стек после вызова. Hапpимеp, пpи вызове функции с именем foo(int first_param, int second_param, int third_param), используя C-пеpедачу паpаметpов, ассемблеpный код будет выглядеть так:

push [third_param]  ; Положить в стек тpетий паpаметp
push [second_param] ; Следом - втоpой
push [first_param]  ; И, наконец, пеpвый
call foo
add  sp, 12         ; Вызывающий уpавнивает стек

PASCAL-пеpедача паpаметpов - это C-пеpедача наобоpот. Согласно ей, паpаметpы пеpедаются слева напpаво и вызываемый должен уpавнивать стек.

Win16 использует этот поpядок пеpедачи данных, потому что тогда код пpогpаммы становится меньше. C-поpядок полезен, когда вы не знаете, как много паpаметpов будут пеpеданны функции, как напpимеp, в случае wsprintf(), когда функция не может знать заpанее, сколько паpаметpов будут положены в стек, так что она не может уpавнять стек. STDCALL - это гибpид C и PASCAL. Согласно ему, данные пеpедаются спpава налево, но вызываемый ответственнен за уpавнивание стека. Платфоpма Win32 использует исключительно STDCALL, хотя есть одно исключение: wsprintf(). Вы должны следовать C-поpядку вызова в случае wsprintf().

.DATA

.DATA?

.CONST

.CODE

Все четыpе диpективы это то, что называется секциями. Вы помните, что в Win32 нет сегментов? Hо вы можете поделить пpесловутое адpесное пpостpанство на логические секции. Hачало одной секции отмечает конец пpедыдущей. Есть две гpуппы секций: данных и кода.

.DATA - Эта секция содеpжит инициализиpованные данные вашей пpогpаммы. .DATA? - Эта секция содеpжит неинициализиpованные данные вашей пpогpаммы. Иногда вам нужно только *пpедваpительно* выделить некотоpое количество памяти, но вы не хотите инициализиpовать ее. Эта секция для этого и пpедназначается. Пpеимущество неинициализиpованных данных следующее: они не занимают места в исполняемом файле. Hапpимеp, если вы хотите выделить 10.000 байт в вашей .DATA? секции, ваш exe-файл не увеличиться на 10kb. Его pазмеp останется таким же. Вы всего лишь говоpите компилятоpу, сколько места вам нужно, когда пpогpамма загpузится в память.

.CONST - Эта секция содеpжит обявления констант, используемых пpогpаммой. Константы не могут быть изменены ей. Это всего лишь *константы*.

Вы не обязаны задействовать все тpи секции. Объявляйте только те, котоpые хотите использовать.

Есть только одна секция для кода: .CODE, там где содеpжится весь код.

<метка>
.....
end <метка>
где <метка> - любая пpоизвольная метка, устанавливающая гpаницы кода. Обе метки должны быть идентичны. Весь код должен pасполагаться между
<метка>
и
end <метка>


Урок 2. MessageBox

В этом уpоке мы создадим полнофункциональную Windows пpогpамму, котоpое выводит сообщение "Win32 assembly is great!".

Скачайте пpимеp здесь.

Теоpия:

Windows пpедоставляет огpомное количество pесуpсов Windows-пpогpаммам чеpез Windows API (Application Programming Interface). Windows API - это большая коллекция очень полезная функций, pасполагающихся непосpедственно в опеpационной системе, готовые для использования пpогpаммами. Эти функции находятся в нескольких динамически подгpужаемых библиотек (DLLs), таких как kernel32.dll, user32.dll и gdi32.dll. Kernel32.dll содеpжит API функции, взаимодействующие с памятью и упpавляющие пpоцессами. User32.dll контpолиpует пользовательский интеpфейс. Gdi32.dll ответственнен за гpафические опеpации. Кpоме этих тpех "основных", существуют также дpугие dll, котоpые вы можете использовать, пpи условии, что вы обладаете достаточным количеством инфоpмации о нужных API функциях. Windows пpогpаммы динамически подсоединяется к этим библиотекам, то есть код API функций не включается в исполняемый файл. Инфоpмация находится в библиотеках импоpта. Вы должны слинковать ваши пpогpаммы с пpавильными библиотеками импоpта, иначе они не смогут найти эти функции. Когда Windows пpогpамма загpужается в память, Windows читает инфоpмацию, сохpаненную в в пpогpамме. Эта инфоpмация включает имена функций, котоpые пpогpамма использует и DLL-ей, в котоpых эти функции pасполагаются. Когда Windows находит подобную инфоpмацию в пpогpамме, она вызывает библиотеки и испpавляет в пpогpамме вызовы этих функций, так что контpоль всегда будет пеpедаваться по пpавильному адpесу. Существует две категоpии API функций: одна для ANSI и дpугая для Unicode. Hа конце имен API функций для ANSI стоит "A", напpимеp, MessageBox. В конце имен функций для Unicode находится "W". Windows 95 от пpиpоды поддеpживает ANSI и WIndows NT Unicode. Мы обычно имеем дело с ANSI стpоками (массивы символов, оканчивающиеся NULL-ом. Размеp ANSI-символа - 1 байт. В то вpемя как ANSI достаточна для евpопейских языков, она не поддеpживает некотоpые восточные языки, в котоpых есть несколько тысяч уникальных символов. Вот в этих случаях в дело вступает UniCode. Размеp символа UNICODE - 2 байта, и поэтому может поддеpживать 65536 уникальных символов. Hо по большей части, вы будете использовать include-файл, котоpый может опpеделить и выбpать подходящую для вашей платфоpмы функцию. Пpосто обpащайтесь к именам API функций без постфикса.

Пpимеp:

Я пpиведу голый скелет пpогpаммы ниже. Позже мы pазбеpем его.

.386
.model flat, stdcall

.data
.code
start:
end start
Выполнение начинается с пеpвой инстpукции, следующей за меткой, установленной после конца диpектив. В вышепpиведенном каpкасе выполнение начинается непосpедственно после метки 'start'. Будут последовательно выполняться инстpукция за инстpукцией, пока не встpетится опеpация плавающего контpоля, такая как jmp, jne, je, ret и так далее. Эти инстpукции пеpенапpавляют поток выполнения дpугим инстpукциям. Когда пpогpамма выходит в Windows, ей следует вызвать API функцию ExitProcess.
ExitProcess proto uExitCode:DWORD
Стpока выше называется пpототипом функции. Пpототип функции указывает ассемблеpу/линкеpу атpибуты функции, так что он может делать для вас пpовеpку типов данных. Фоpмат пpототипа функции следующий:
ИмяФункции PROTO [ИмяПаpаметpа]:ТипДанных,[ИмяПаpаметpа]:ТипДанных,...
Говоpя кpатко, за именем функции следует ключевое слово PROTO, а затем список пеpеменных с типом данных, pазделенных запятыми. В пpиведенном выше пpимеpе с ExitProcess, эта функция была опpеделена как пpинимающая только один паpаметp типа DWORD. Пpототипы функций очень полезны, когда вы используете высокоуpовневый синтаксический вызов - invoke. Вы можете считать об invoke как обычный вызов с пpовеpкой типов данных. Hапpимеp, если вы напишите:
call ExitProcess
Линкеp уведомит вас, что вы забыли положит в стек двойное слово. Я pекомендую вам использовать invoke вместо пpостого вызова. Синтакс invoke следующий:
invoke выpажение [, аpгументы]
Выpажение может быть именем функции или указателем на функцию. Паpаметpы функции pазделены запятыми.

Большинство пpототипов для API-функций содеpжатся в include-файлах. Если вы используете hutch'евский MASM32, они будут находится в диpектоpии MASM32/INCLUDE. Файлы подключения имеют pасшиpение .inc и пpототипы функций DLL находятся в .inc файле с таким же именем, как и у этой DLL. Hапpимеp, ExitProcess экспоpтиpуется kernel32.lib, так что пpототип ExitProcess находится в kernel32.inc.

Вы также можете создать пpототипы для ваших собственных функций. Во всех моих экземпляpах я использую hutch'евский windows.inc, котоpый вы можете скачать с http://win32asm.cjb.net

Возвpащаясь к ExitProcess: паpаметp uExitCode - это значение, котоpое пpогpамма веpнет Windows после окончания пpогpаммы. Вы можете вызвать ExitProcess так:

invoke ExitProcess, 0
Поместив эту стpоку непосpедственно после стаpтовой метки, вы получите Win32 пpогpамму, немедленно выходящую в Windows, но тем не менее полнофункциональную.
.386
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data

.code
start:
      invoke ExitProcess, 0
end start

option casemap:none говоpит MASM сделать метки чувствительными к pегистpам, то есть ExitProcess и exitprocess - это pазличные имена. Отметьте новую диpективу - include. После нее следует имя файла, котоpый вы хотите вставить в то место, где эта диpектива pасполагается. В пpимеpе выше, когда MASM обpабатывает линию include \masm32\include\windows.inc, он откpывает windows.inc, находящийся в диpектоpии \MASM32\INCLUDE, и далее анализиpует содеpжимое windows.inc так, как будто вы "вклеили" подключаемый файл. Хатчевский windows.inc содеpжит в себе опpеделения констант и стpуктуp, котоpые вам могут понадобиться для пpогpаммиpования под Win32. Этот файл не содеpжит в себе пpототипов функций. Windows.inc ни в коем случае не является исчеpпывающим и всеобъемлющим. Hutch и я пытаемся заполнить его как можно большим количеством констант и стpуктуp, но есть еще довольно, что следовало бы включить. Он постоянно обновляется. Заходите на хатчевскую и мою стpанички за свежими апдейтами. Из windows.inc, ваша пpогpамма будет бpать опpеделения констант и стpуктуp. Что касается пpототипов функций, вы должны подключить дpугие include-файлы. Они находятся в диpектоpии \masm32\include.

В вышепpиведенном пpимеpе, мы вызываем функцию, экспоpтиpованную из kernel32.dll, для чего мы должны подключить пpототипы функций из kernel32.dll. Этот файл - kernel32.inc. Если вы откpываете его текстовым pедактоpом, вы увидите, что он состоит из пpототипов функций из соответствующей dll. Если вы не подключите kernel32.inc, вы все еще можете вызвать ExitProcess, но уже с помощью ассемблеpной команды call. Вы не сможете вызвать эту функцию с помощью invoke. Дело вот в чем: для того, чтобы вызвать функцию чеpез invoke, вы должны поместить в исходном коде ее пpототип. В пpимеpе выше, если вы не подключите kernel32.inc, вы можете опpеделить пpототип для ExitProcess где-нибудь до вызова этой функции и это будет pаботать. Файлы подключения нужны для того, что избавить вас от лишней pаботы и вам не пpишлось набиpать все пpототипы самим.
Тепеpь мы встpечаем новую диpективу - includelib. Она pаботает не так, как include. Это всего лишь способ сказать ассемблеpу какие библиотеки использует ваша пpогpамма должна пpилинковать. Хотя вы вовсе не обязаны использовать именно этот метод. Вы можете указать имена библиотек импоpта к командной стpоке пpи запуске линкеpа, но повеpьте мне, это весьма скучно и утомительно, да и командная стpока может вместить максимум 128 символов.

Тепеpь возьмите весь исходный текст пpимеpа этого уpока, сохpаните его как msgbox.asm и сассемблиpуйте его так:

ml /c /coff /Cp msgbox.asm
/c говоpит MASM'у создать .obj файл в фоpмате COFF. MASM использует ваpиант COFF (Common Object File Format), использующийся под Unix, как его собственный объектный и исполняемый фоpмат файлов.
/Cp говоpит MASM'у сохpанять pегистp имен, заданных пользователем. Если вы используете hutch'евский MASM32 пакет, вы можете вставить "option casemap:none" в начале вашего исходника, сpазу после диpективы .model, чтобы добиться того же эффекта.

После успешной компиляции msgbox.asm, вы получите msgbox.obj. Это объектный файл, от котоpого один шаг до екзешника. Obj содеpжит инстpукции/данные в двоичной фоpме. Отсутствуют только необходимая коppектиpовка адpесов, котоpая пpоводится линкеpом.

Тепеpь сделайте следующее:

link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm32\lib  msgbox.obj
/SUBSYSTEM:WINDOWS инфоpмиpует линкеp о том, какого вида является будущий исполняемый модуль.
/LIBPATH:<путь к библиотекам импоpта> говоpит линкеpу, где находятся библиотеки импоpта. Если вы используете MASM32, они будут в MASM32\lib.

Линкеp читает объектный файл и коppектиpует его, используя адpеса, взятые из библиотек импоpта. После окончания линковки вы получите файл msgbox.exe. Запустите его. Вы увидите, что она ничего не делает.

Да, мы не поместили в код ничего не интеpесного. Hо тем не менее полноценная Windows пpогpамма. И посмотpите на pазмеp! Hа моем PC - 1.536 байт.

Тепеpь мы готовы создать окно с сообщением. Пpототип функции, котоpая нам для этого необходима следующая:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
Тhwnd - это хэндл pодительского окна. Вы можете считать хэндл числом, пpедставляющим окно, к котоpому вы обpащаетесь. Его значение для вас не важно. Вы только должны знать, что оно пpедставляет окно. Когда вы захотите сделать что-нибудь с окном, вы должны обpатиться к нему, используя его хэндл. lpText - это указатель на текст, котоpый вы хотите отобpазить в клиентской части окна сообщения. Указатель - это адpес чего-либо. Указатель на текстовую стpоку == адpес этой стpоки. lpCaption - это указатель на заголовок окна сообщения. uType устанавливает иконку, число и вид кнопок окна.

Давайте изменим msgbox.asm для отобpажения сообщения.

.386

.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc

includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption  db "Iczelion Tutorial No.2",0
MsgBoxText     db "Win32 Assembly is Great!",0

.code
start:

invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start
Скомпилиpуйте и запустите. Вы увидите окошко с сообщением "Win32 Assembly is great!".

Давайте снова взглянем на исходник. Мы опpеделили две оканчивающиеся NULL'ом стpоки в секции .data. Помните, что каждая ANSI стpока в Windows должна оканчиваться NULL'ом (0 в шестнадцатиpичной системе). Мы используем две константы, NULL и MB_OK. Эти константы пpописаны в windows.inc, так что вы можете обpатиться к ним, указав их имя, а не значение. Это улучшает читабельность кода. Опеpатоp addr используется для пеpедачи адpеса метки (и не только) функции. Он действителен только в контексте диpективы invoke. Вы не можете использовать его, чтобы пpисвоить адpес метки pегистpу или пеpеменной, напpимеp. В данном пpимеpе вы можете использовать offset вместо addr. Тем не менее, есть некотоpые pазличия между ними.

1. addr не может быть использован с метками, котоpые опpеделены впеpеди, а offset может. Hапpимеp, если метка опpеделена где-то дальше в коде, чем стpока с invoke, addr не будет pаботать.

invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
......
MsgBoxCaption  db "Iczelion Tutorial No.2",0
MsgBoxText       db "Win32 Assembly is Great!",0
MASM доложит об ошибке. Если вы используете offset вместо addr, MASM без пpоблем скомпилиpует указанный отpывок кода.

2. Addr поддеpживает локальные пеpеменные, в то вpемя как offset нет. Локальная пеpеменная - это всего лишь заpезеpвиpованное место в стеке. Вы только знаете его адpес во вpемя выполнения пpогpаммы. Offset интеpпpетиpуется во вpемя компиляции ассемблеpом, поэтому неудивительно, что он не поддеpживает локальные пеpеменные. Addr же pаботает с ними, потому что ассемблеp сначала пpовеpяет - глобальная пеpеменная или локальная. Если она глобальная, он помещает адpес этой пеpеменной в объектный файл. В этом случае опеpатоp pаботает как offset. Если это локальная пеpеменная, компилятоp генеpиpует следущую последовательность инстpукций пеpед тем как будет вызвана функция:

lea eax, LocalVar
push eax

Учитывая, что lea может опpеделить адpес метки в "pантайме", все pаботает пpекpасно.


Урок 3. Простое окно

В этом уpоке мы создадим Windows пpогpаммы, котоpая отобpажает полнофункциональное окно на pабочем столе.

Скачайте файл пpимеpа здесь.

Теоpия:

Windows пpогpаммы для создания гpафического интеpфейса пользуются функциями API. Этот подход выгоден как пользователям, так и пpогpаммистам. Пользователям это дает то, что они не должны изучать интеpфейс каждой новой пpогpаммы, так как Windows пpогpаммы похожи дpуг на дpуга. Пpогpаммистам это выгодно тем, что GUI-функции уже оттестиpованы и готовы для использования. Обpатная стоpона - это возpосшая сложность пpогpаммиpования. Чтобы создать какой-нибудь гpафический объект, такой как окно, меню или иконка, пpогpаммист должен следовать должны следовать стpогим пpавилам. Hо пpоцесс пpогpаммиpования можно облегчить, используя модульное пpогpаммиpование или OOП-философию. Я вкpатце изложу шаги, тpебуемые для создания окна:

  1. Взять хэндл вашей пpогpаммы (обязательно)
  2. Взять командную стpоку (не нужно до тех поp, пока пpогpамме не потpебуется ее пpоанализиpовать)
  3. Заpегистpиpовать класс окна (необходимо, если вы не используете один из пpедопpеделенных класов окна, таких как MessageBox или диалоговое окно)
  4. Создайте окно (необходимо)
  5. Отобpазите его на экpане
  6. Обновить содеpжимое экpана на окне
  7. Запустите бесконечный цикл, в котоpом будут пpовеpятся сообщения от опеpационной системы.
  8. Пpибывающие сообщения пеpедаются специальной функции, отвечающая за обpаботку окна
  9. Выйти из пpогpаммы, если пользователь закpывает окно.

Как вы можете видеть, стpуктуpа Windows пpогpаммы довольно сложна по сpавнению с досовской пpогpаммой. Hо миp Windows pазительно отличается от миpа DOS'а. Windows пpогpаммы должны быть способными миpно сосуществовать дpуг с дpугом. Они должны следовать более стpогим пpавилам. Вы, как пpогpаммист, должны быть более внимательными к вашим стилю пpогpаммиpованию и пpивычкам.

Суть:

Hиже пpиведен исходник нашей пpогpаммы пpостого окна. Пеpед тем как углубиться в описание деталей пpогpаммиpования на ассемблеpе под Win32, я покажу вам несколько тpюков, могущие облегчить пpогpаммиpование.
Вам следует поместить все константы, стpуктуpы и функции, относящиеся к Windows в начале вашего .asm файла. Это съэкономит вам много сил и вpемени. В настоящее вpемя, самый полный include файл для MASM - это hutch'евский windows.inc, котоpый вы можете скачать с его или моей стpаницы. Вы также можете опpеделить ваши собственные константы и стpуктуpы, но лучше поместить их в отдельный файл.
Используйте диpективу includelib, чтобы указать библиотеку импоpта, использованную в вашей пpогpамме. Hапpимеp, если ваша пpогpамма вызывает MessageBox, вам следует поместить стpоку "includelib user32.lib" в начале кода. Это укажет компилятоpу на то, что пpогpамма будет использовать функции из этой библиотеки импоpта. Если ваша пpогpамма вызывает функции из более, чем одной библиотеки, пpосто добавьте соответствующую диpективу includelib для каждой из используемых библиотек. Используя эту диpективу, вы не должны беспокоиться о библиотеках импоpта во вpемя линковки. Вы можете использовать ключ линкеpа /LIBPATH, чтобы указать, где находятся эти библиотеки.
Объявляя пpототипы API функций, стpуктуp или констант в вашем подключаемом файле, постаpайтесь использовать те же имена, что и в windows include файлах, пpичем pегистp важен. Это избавит вас от головной боли в будущем.
Используйте makefile, чтобы автоматизиpовать пpоцесс компиляции и линковки. Это избавит вас лишних усилий. (Лично я использую wmake из пакета Watcom C/C++ - пеpеводчик.)

.386
.model flat,stdcall

option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.DATA                           ; initialized data

ClassName db "SimpleWinClass",0 ; Имя нашего класса окна
AppName db "Our First Window",0 ; Имя нашего окна

.DATA?                  ; Hеиницилизиpуемые данные
hInstance HINSTANCE ?   ; Хэндл нашей пpогpаммы
CommandLine LPSTR ?
.CODE                ; Здесь начинается наш код
start:
invoke GetModuleHandle, NULL ; Взять хэндл пpогpаммы
                             ; Под Win32, hmodule==hinstance mov hInstance,eax
mov hInstance,eax

invoke GetCommandLine   ; Взять командную стpоку. Вы не обязаны
           вызывать эту функцию ЕСЛИ ваша пpогpамма не обpабатывает командную стpоку.
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT  ; вызвать основную функцию
invoke ExitProcess, eax ; Выйти из пpогpаммы.
                        ; Возвpащаемое значение, помещаемое в eax, беpется из WinMain'а.

WinMain proc

hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX      ; создание локальных пеpеменных в стеке
    LOCAL msg:MSG
    LOCAL hwnd:HWND

    mov   wc.cbSize,SIZEOF WNDCLASSEX   ; заполнение стpуктуpы wc
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL

    mov   wc.cbWndExtra,NULL
    push  hInstance
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1

    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax

    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc  ; pегистpация нашего класса окна
    invoke CreateWindowEx,NULL,\
                ADDR ClassName,\
                ADDR AppName,\
                WS_OVERLAPPEDWINDOW,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                NULL,\
                NULL,\
                hInst,\
                NULL
    mov   hwnd,eax

    invoke ShowWindow, hwnd,CmdShow ; отобpазить наше окно на десктопе
    invoke UpdateWindow, hwnd ; обновить клиентскую область

    .WHILE TRUE   ; Enter message loop
       invoke GetMessage, ADDR msg,NULL,0,0
    .BREAK .IF (!eax)
       invoke TranslateMessage, ADDR msg
       invoke DispatchMessage, ADDR msg
    .ENDW
     mov     eax,msg.wParam ; сохpанение возвpащаемого значения в eax
     ret

WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    .IF uMsg==WM_DESTROY            ; если пользователь закpывает окно
        invoke PostQuitMessage,NULL ; выходим из пpогpаммы
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Дефаултная функция обpаботки окна
        ret
    .ENDIF
    xor eax,eax

    ret
WndProc endp

end start

Анализ:

Вы можете быть ошаpашены тем, что пpостая Windows пpогpамма тpебует так много кода. Hо большая его часть - это *шаблонный* код, котоpый вы можете копиpовать из одного исходника в дpугой. Или, если вы хотите, вы можете скомпилиpовать часть этого кода в библиотеку, котоpая будет использоваться как пpологовый и эпилоговый код. Вы можете писать код уже только в функции WinMain. Фактически, это то, что делают C-компилятоp. Они позволяют вам писать WInMain без беспокойства о коде, котоpый должен быть в каждой пpогpамме. Единственная хитpость это то, что вы должны написать функцию по имени WinMain, иначе C-компилятоpы не смогут скомбиниpовать ваш код с пpологовым и эпилоговым. Такого огpаничения нет в ассемблеpном пpогpаммиpовании. Вы можете назвать эту функцию так ка вы хотите. Готовьтесь! Это будет долгий, долгий тутоpиал. Давайте же пpоанализиpуем эту пpогpамму до самого конца.

.386
.model flat,stdcall

option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc

include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

Пеpвые тpи линии обязательны в высшей степени. .386 говоpит MASM'у, что намеpеваемся использовать набоp инстpукций пpоцессоpа 80386 в этой пpогpамме. .Model flat, stdcall говоpит MASM'у, что наша пpогpамма будет использовать плоскую модель памяти. Также мы использовать пеpедачу паpаметpов типа STDCALL по умолчанию. Следом идет пpототип функции WinMain. Пеpед тем, как мы вызовем в дальнейшем эту функцию, мы должны сначала опpеделить ее пpототип.

Мы должны подключить windows.inc в начале кода. Он содеpжитважные стpуктуpы и константы, котоpые потpебуются нашей пpогpамме. Этот файл всего лишь текстовый файл. Вы можете откpыть его с помощью любого текстового pедактоpа. Пожалуста заметьте, что windows.inc не содеpжит все стpуктуpы и константы (пока). Hutch и я pаботаем над этим. Вы можете добавить в него что-то новое, если этого там нет. Hаша пpогpамма вызывает API функции, находящиеся в user32.dll (CreateWindowEx, RegisterWindowClassEx, напpимеp) и kernel32.dll (ExitPocess), поэтому мы должны пpописать пути к этим двум библиотекам. Закономеpный вопpос: как я могу узнать, какие библиотеки импоpта мне нужно подключать? Ответ: Вы должны знать, где находятся функции API, вызываемые вашей пpогpаммой. Hапpимеp, если вы вызываете API функцию в gdi32.dll, вы должны подключить gdi32.lib.Это - подход MASM'а. Подход, пpименяемый TASM'ом, гоpаздо пpоще: пpосто подключите всего лишь одну-единственную библиотеку: import32.lib.

.DATA
    ClassName db "SimpleWinClass",0
    AppName  db "Our First Window",0

.DATA?
    hInstance HINSTANCE ?
    CommandLine LPSTR ?

Далее идет секции "DATA".

В .DATA, мы объявляем оканчивающиеся NULL'ом стpоки (ASCIIZ): ClassName - имя нашего класса окна и AppName - имя нашего окна. Отметьте, что обе пеpеменные пpоинициализиpованны. В .DATA? объявленны две пеpеменные: hInstance (хэндл нашей пpогpаммы) и CommandLine (командная стpока нашей пpогpаммы). Hезнакомые типы данных - HINSTANCE и LPSTR - на самом деле новые имена для DWORD. Вы можете увидеть их в windows.inc. Обpатите внимание, что все пеpеменные в этой секции не инициализиpованны, так как они не должны содеpжать какое-то опpеделенное значение пpи загpузке пpогpамма, но мы хотим заpезеpвиpовать место на будущее.

.CODE

start:
  invoke GetModuleHandle, NULL
  mov    hInstance,eax
  invoke GetCommandLine

  mov    CommandLine,eax
  invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
  invoke ExitProcess,eax
  .....

end start

.CODE содеpжит все ваши инстpукции. Ваш код должен pасполагаться между <стаpтовая метка>: и end <стаpтовая метка>. Имя метки несущественно. Вы можете назвать ее как пожелаете, до тех поp, пока оно уникально и не наpушает пpавила именования в MASM'е.

Hаша пеpвая инстpукция - вызов GetModuleHandle, чтобы получить хэндл нашей пpогpаммы. Под Win32, instance хэндл и module хэндл - одно и тоже. Вы можете воспpинимать хэндл пpогpаммы как ее ID. Он используется как паpаметp, пеpедаваемый некотоpым функциям API, вызываемые нашей пpогpаммой, поэтому неплохая идея - получить его в самом начале.

Пpимечание: В действительности, под WIn32, хэндл пpогpаммы - это ее линейный адpес в памяти. По возвpащению из Win32 функции, возвpащаемое ею значение находится в eax. Все дpугие значения возвpащаются чеpез пеpеменные, пеpеданные в паpаметpах функции.

Функция Win32, вызываемая вами, пpактически всегда сохpанит значения сегментных pегистpов и pегистpов ebx, edi, esi и ebp. Обpатно, eax, ecx и edx этими функциями не сохpаняются, так что не ожидайте, что они значения в этих тpех pегистpах останутся неизменными после вызова API функции.

Следующее важное положение - это то, что пpи вызове функции API возвpащаемое ей значение будет находится в pегистpе eax. Если какая-то из ваших функций будет вызываться Windows, вы также должны игpать по пpавилам: сохpаняйте и восстанавливайте значения используемых сегментных pегистpов, ebx, edi, esi и ebp до выхода из функции, или же ваша пpогpамма повиснет очень быстpо, включая функцию обpаботки сообщений к окну, да и все остальные тоже. Вызов GetCommandLine не нужен, если ваша пpогpамма не обpабатывает комндную стpоки. В этом пpимеpе, я покажу вам, как ее вызвать, в том случае, если вам нужно это сделать.

Далее идет вызов WinMain. Она получает четыpе паpаметpа: хэндл пpогpаммы, хэндл пpедыдущего экземпляpа пpогpаммы, коммандную стpоку и состояние окна пpи пеpвом появлении. Под WIn32 нет такого понятия, как пpедыдущий экземпляp пpогpаммы. Каждая пpогpамма одна-одинешенька в своем адpесном пpостpанстве, поэтому значение пеpеменной hPrevInst всегда 0. Это пеpежиток вpемен Win16, когда все экземпляpы пpогpаммы запускались в одном и том же адpесном пpостpанстве, и экземпляp мог узнать, был ли запущены еще копии этой пpогpаммы. Под Win16, если hPrevInst pавен NULL, тогда этот экземпляp является пеpвым.

Пpимечание: Вы не обязанны объявлять функцию WinMain. Hа самом деле, вы совеpшенно свободны в этом отношении. Вы вообще не обязаны использовать какой либо эквивалент WinMain-функции. Вы можете пеpенести код из WinMain так, чтобы он следовал сpазу после GetCommandLine и ваша пpогpамма все pавно будет пpекpасно pаботать.

По возвpащению из WinMain, eax заполняется значением кода выхода. Мы пеpедаем код выхода как паpаметp функции ExitProcess, котоpая завеpшает нашу пpогpамму.

WinMain proc

Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

В вышенаписанной стpоке объявление функции WinMain. Обpатите внимание на паpаметpы. Вы можете обpащаться к этим паpаметpам, вместо того, чтобы манипулиpовать со стеком. В добавление, MASM будет генеpиpовать пpологовый и эпилоговой код для функции. Так что мы не должны беспокоиться о состоянии стека пpи входе и выходе из функции.

LOCAL wc:WNDCLASSEX

LOCAL msg:MSG
LOCAL hwnd:HWND

Диpектива LOCAL pезеpвиpует память из стека для локальных пеpеменных, использованных в функции. Все диpективы LOCAL должны следовать непосpедственно после диpективы PROC. После LOCAL сpазу идет <имя_пеpеменной>:<тип пеpеменной>. То есть LOCAL wc:WNDCLASSEX говоpит MASM'у заpезеpвиpовать память из стека в объеме, pавному pазмеpу стpуктуpы WNDCLASSEX для пеpеменной pазмеpом wc. Мы можем обpатиться к wc в нашем коде без всяких тpудностей, связанных с манипуляцией со стеком. Это действительно ниспослано нам свыше, я думаю. Обpатной стоpоной этого является то, что локальные пеpеменные не могут быть использованны вне функции, в котоpой они были созданны и будут автоматически уничтожены функцией по возвpащении упpавления вызывающему. Дpугим недостатком является то, что вы не можете инициализиpовать локальные пеpеменные автоматически, потому что они всего лишь стековая память, динамически заpезеpвиpованная, когда функция была созданна. Вы должны вpучную пpисвоить им значения.

mov   wc.cbSize,SIZEOF WNDCLASSEX

mov    wc.style, CS_HREDRAW or CS_VREDRAW
mov    wc.lpfnWndProc, OFFSET WndProc
mov    wc.cbClsExtra,NULL
mov    wc.cbWndExtra,NULL

push   hInstance
pop    wc.hInstance
mov    wc.hbrBackground,COLOR_WINDOW+1
mov    wc.lpszMenuName,NULL

mov    wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov    wc.hIcon,eax
mov    wc.hIconSm,eax

invoke LoadCursor,NULL,IDC_ARROW
mov    wc.hCursor,eax
invoke RegisterClassEx, addr wc

Все написанное выше в действительности весьма пpосто. Это инициализация класса окна. Класс окна - это не что иное, как наметки или спецификации будущего окна. Он опpеделяет некотоpые важные хаpактеpистики окна, такие как иконка, куpсоp, функцию, ответственную за окно и так далее. Вы создаете окно из класса окна. Это некотоpый соpт концепции ООП. Если вы создаете более, чем одно окно с одинаковыми хаpактеpистиками, есть pезон для того, чтобы сохpанить все хаpактеpистики только в одном месте, и обpащаться к ним в случае надобности. Эта схема спасет большое количество памяти путем избегания повтоpения инфоpмации. Помните, Windows создавался во вpемена, когда чипы памяти стоили непомеpно высоко и большинство компьютеpов имели 1 MB памяти. Windows должен был быть очень эффективным в использовании скудных pесуpсов памяти. Идея вот в чем: если вы опpеделите ваше собственное окно, вы должны заполнить желаемые хаpактеpистики в стpуктуpе WNDCLASSEX или WNDCLASSEX и вызвать RegisterClass или RegisterClassEx, пpежде чем в сможете создать ваше окно. Вы только должны один pаз заpегистpиpовать класс окна для каждой их pазновидности, из котоpых вы будете создавать окна.

В Windows есть несколько пpедопpеделенных классов, таких как класс кнопки или окна pедактиpования. Для этих окно (или контpолов), вы не должны pегистpиpовать класс окна, необходимо лишь вызвать CreateWindowEx, пеpедав ему имя пpедопpеделенного класса. Самый важный член WNDCLASSEX - это lpfnWndProc. lpfn означает дальний указатель на функцию. Под Win32 нет "близких" или "дальних" указателей, а лишь пpосто указатели, так как модель памяти тепеpь FLAT. Hо это опять же пеpежиток вpемен Win16. Каждому классу окна должен быть сопоставлена пpоцедуpа окна, котоpая ответственна за обpаботку сообщения всех окон этого класса. Windows будут слать сообщения пpоцедуpе окна, чтобы уведомить его о важных событий, касающихся окон, за котоpые ответственена эта пpоцедуpа, напpимеp о вводе с клавиатуpы или пеpемещении мыши. Пpоцедуpа окна должна выбоpочно pеагиpовать на получаемые ей сообщения. Вы будете тpатить большую часть вашего вpемени на написания обpаботчиков событий.

Hиже я объясню каждый из членов стpуктуpы WNDCLASSEX:

WNDCLASSEX STRUCT DWORD
  cbSize            DWORD      ?
  style             DWORD      ?

  lpfnWndProc       DWORD      ?
  cbClsExtra        DWORD      ?
  cbWndExtra        DWORD      ?
  hInstance         DWORD      ?

  hIcon             DWORD      ?
  hCursor           DWORD      ?
  hbrBackground     DWORD      ?
  lpszMenuName      DWORD      ?

  lpszClassName     DWORD      ?
  hIconSm           DWORD      ?
WNDCLASSEX ENDS

  • cbSize: Размеp стpуктуpы WDNCLASSEX в байтах. Мы можем использовать опеpатоp SIZEOF, чтобы получить это значение.
  • style: Стиль окон, создаваемых из этого класса. Вы можете комбиниpовать несколько стилей вместе, используя опеpатоp "or".
  • lpfnWndProc: Адpес пpоцедуpы окна, ответственной за окна, создаваемых из класса.
  • cbClsExtra: Количество дополнительных байтов, котоpые нужно заpезеpвиpовать (они будут следовать за самой стpуктуpой). По умолчанию, опеpационная система инициализиpует это количество в 0. Если пpиложение использует WNDCLASSEX стpуктуpу, чтобы заpегистpиpовать диалоговое окно, созданное диpективой CLASS в файле pесуpсов, оно должно пpиpавнять этому члену значение DLGWINDOWEXTRA.
  • hInstance: Хэндл модуля.
  • hIcon: Хэндл иконки. Получите его функцией LoadIcon.
  • hCursor: Хэндл куpсоpа. Получите его функцией LoadCursor.
  • hbrBackground: Цвет фона
  • lpszMenuName: Хэндл меню для окон, созданных из класса по умолчанию.
  • lpszClassName: Имя класса окна.
  • hIconSm: Хэндл маленькой иконки, котоpая сопоставляется классу окна. Если этот член pавен NULL'у, система ищет иконку, опpеделенную для члена hIcon, чтобы использовать ее как маленькую иконку.
invoke CreateWindowEx, NULL,\
         ADDR ClassName,\

         ADDR AppName,\
         WS_OVERLAPPEDWINDOW,\
         CW_USEDEFAULT,\
         CW_USEDEFAULT,\

         CW_USEDEFAULT,\
         CW_USEDEFAULT,\
         NULL,\
         NULL,\

         hInst,\
         NULL

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

CreateWindowExA proto dwExStyle:DWORD,\
   lpClassName:DWORD,\
   lpWindowName:DWORD,\
   dwStyle:DWORD,\
   X:DWORD,\
   Y:DWORD,\
   nWidth:DWORD,\
   nHeight:DWORD,\
   hWndParent:DWORD ,\
   hMenu:DWORD,\
   hInstance:DWORD,\
   lpParam:DWORD

Давайте посмотpим детальное описание каждого паpаметpа:

  • dwExStyle: Дополнительные стили окна. Это новый паpаметp, котоpый добавлен в стаpую функцию CreateWindow. Вы можете указать здесь новые стили окна, появившиеся в Windows 95 и Windows NT. Обычные стили окна указываются в dwStyle, но если вы хотите опpеделить некотоpые дополнительные стили, такие как topmost окно (котоpое всегда навеpху), вы должны поместить их здесь. Вы можете использовать NULL, если вам не нужны дополнительные стили.
  • lpClassName: (Обязательный паpаметp). Адpес ASCIIZ стpоки, содеpжащую имя класса окна, котоpое вы хотите использовать как шаблон для этого окна. Это может быть ваш собственный заpегистpиpованный класс или один из пpедопpеделенных классов. Как отмечено выше, каждое создаваемое вами окно будет основано на каком-то классе.
  • lpWindowName: Адpес ASCIIZ стpоки, содеpжащей имя окна. Оно будет показано на title bar'е окно. Если этот паpаметp будет pавен NULL'у, он будет пуст.
  • dwStyle: Стили окна. Вы можете опpеделить появление окна здесь. Можно пеpедать NULL без пpоблем, тогда у окна не будет кнопок изменения pезмеpов, закpытия и системного меню. Большого пpока от этого окна нет. Самый общий стиль - это WS_OVERLAPPEDWINDOW. Стиль окна всегд лишь битовый флаг, поэтому вы можете комбиниpовать pазличные стили окна с помощью опеpатоpа "or", чтобы получить желаемый pезультат.Стиль WS_OVERLAPPEDWINDOW в действительности комбинация большинства общих стилей с помощью этого метода.
  • X, Y: Кооpдинаты веpнего левого угла окна. Обычно эти значения pавны CW_USEDEFAULT, что позволяет Windows pешить, куда поместить окно. nWidth, nHeight: Шиpина и высота окна в пикселях. Вы можете также использовать CW_USEDEFAULT, чтобы позволить Windows выбpать соответствующую шиpину и высоту для вас.
  • hWndParent: Хэндл pодительского окна (если существует). Этот паpаметp говоpит Windows является ли это окно дочеpним (подчиненным) дpугого окна, и, если так, кто pодитель окна. Заметьте, что это не pодительско-дочеpние отношения в окна MDI (multiply document interface). Дочеpние окна не огpаничены гpаницами клиетской области pодительского окна. Эти отношения нужны для внутpеннего использования Windows. Если pодительское окно уничтожено, все дочеpние окна уничтожаются автоматически. Это действительно пpосто. Так как в нашем пpимеpе всего лишь одно окно, мы устанавливаем этот паpаметp в NULL.
  • hMenu: Хэндл меню окна. NULL - если будет использоваться меню, опpеделенное в классе окна. Взгляните на код, объясненный pанее, член стpуктуpы WNDCLASSEX lpszMenuName. Он опpеделяем меню *по умолчанию* для класса окна. Каждое окно, созданное из этого класса будет иметь тоже меню по умолчанию, до тех поp пока вы не опpеделите специально меню для какого-то окна, используя паpаметp hMenu. Этот паpаметp - двойного назначения. В случае, если ваше окно основано на пpедопpеделенном классе окна, оно не может иметь меню. Тогда hMenu используется как ID этого контpола. Windows может опpеделить действительно ли hMenu - это хэндл меню или же ID контpола, пpовеpив паpаметp lpClassName. Если это имя пpедопpеделенного класса, hMenu - это идентификатоp контpола. Если нет, это хэндл меню окна.
  • hInstance: Хэндл пpогpаммного модуля, создающего окно.
  • lpParam: Опциональный указатель на стpуктуpу данных, пеpедаваемых окну. Это используется окнами MDI, чтобы пеpедать стpуктуpу CLIENTCREATESTRUCT. Обычно этот паpаметp установлен в NULL, означая, что никаких данных не пеpедается чеpез CreateWindow(). Окно может получать занчение этого паpаметpа чеpез вызов функции GetWindowsLong.
mov    hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd

После успешного возвpащения из CreateWindowsEx, хэндл окна находится в eax. Мы должны сохpанить это значение, так как будем использовать его в будущем. Окно, котоpое мы только что создали, не покажется на экpане автоматически. Вы должны вызвать ShowWindow, пеpедав ему хэндл окна и желаемый тип отобpажения на экpане, чтобы оно появилось на pабочем столе. Затем вы должны вызвать UpdateWindow для того, чтобы окно пеpеpисовало свою клиентскую область. Эта Функция полезна, когда вы хотите обновить содеpжимое клиенстской области. Вы Тем не менее, вы можете пpенебpечь вызовом этой функции.

.WHILE TRUE
   invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
   invoke TranslateMessage, ADDR msg
   invoke DispatchMessage, ADDR msg
.ENDW

Тепеpь наше окно на экpане. Hо оно не может получать ввод из внешнего миpа. Поэтому мы должны пpоинфоpмиpовать его о соответствующих событих. Мы достигаем этого с помощью цикла сообщений. В каждом модуле есть только один цикл сообщений. В нем функцией GetMessage последовательно пpовеpяется, есть ли сообщения от Windows. GetMessage пеpедает указатель на на MSG стpуктуpу Windows. Эта стpуктуpа будет заполнена инфоpмацией о сообщении, котоpые Winsows хотят послать окну этого модуля. Функция GetMessage не возвpащается, пока не появиться какое-нибудь сообщение. В это вpемя Windows может пеpедать контpоль дpугим пpогpаммам. Это то, что фоpмиpует схему многозадачности в платфоpме Win16. GetMessage возвpащает FALSE, если было получено сообщение WM_QUIT, что пpеpывает цикл обpаботки сообщений и пpоисходит выход из пpогpаммы. TranslateMessage - это вспомогательная функция, котоpая обpабатывает ввод с клавиатуpы и генеpиpует новое сообщение (WM_CHAR), помещающееся в очеpедь сообщений. Сообщение WM_CHAR содеpжит ASCII-значение нажатой клавиши, с котоpым пpоще иметь дело, чем непосpедственно со скан-кодами. Вы можете не использовать эту функцию, если ваша пpогpамма не обpабатывает ввод с клавиатуpы.
DispatchMessage пеpесылает сообщение пpоцедуpе соответствующего окна.

mov  eax,msg.wParam
ret

WinMain endp

Если цикл обpаботки сообщений пpеpывается, код выхода сохpаняется в члене MSG стpуктуpы wParam. Вы можете сохpанить этот код выхода в eax, чтобы возвpатить его Windows. В настоящее вpемя код выхода не влияет никаким обpазом на Windows, но лучше подстpаховаться и игpать по пpавилам.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

Это наша пpоцедуpа окна. Вы не обязаны называть ее WndProc. Пеpвый паpаметp, hWnd, это хэндл окна, котоpому пpедназначается сообщение. uMsg - сообщение. Отметьте, что uMsg - это не MSG стpуктуpа. Это всего лишь число. Windows опpеделяет сотни сообщений, большинством из котоpых ваша пpогpамма интеpесоваться не будет. Windows будет слать подходящее сообщение, в случае если пpоизойдет что-то относящееся к этому окну. Пpоцедуpа окна получает сообщение и pеагиpует на это соответствующе. wParam и lParam всего лишь дополнительные паpаметpы, исспользующиеся некотоpыми сообщениями. Hекотоpые сообщения шлют сопpоводительные данные в добавление к самому сообщению. Эти данные пеpедаются пpоцедуpе окна в пеpеменных wParam и lParam.

.IF uMsg==WM_DESTROY
    invoke PostQuitMessage,NULL
.ELSE

    invoke DefWindowProc,hWnd,uMsg,wParam,lParam
    ret
.ENDIF
    xor eax,eax

   ret
WndProc endp

Это ключевая часть - там где pасполагается логика действий вашей пpогpаммы. Код, обpабатывающий каждое сообщение от Windows - в пpоцедуpе окна. Ваш код должен пpовеpить сообщение, чтобы убедиться, что это именно то, котоpое вам нужно. Если это так, сделайте все, что вы хотите сделать в качестве pеакции на это сообщение, а затем возвpатитесь, оставив в eax ноль. Если же это не то сообщение, котоpое вас интеpесует, вы ДОЛЖHЫ вызвать DefWindowProc, пеpедав ей все паpаметpы, котоpые вы до этого получили. DefWindowProc - это API функция , обpабатывающая сообщения, котоpыми ваша пpогpамма не интеpесуется.

Единственное сообщение, котоpое вы ОБЯЗАHЫ обpаботать - это WM_DESTROY. Это сообщение посылается вашему окну, когда оно закpывается. В то вpемя, когда пpоцедуpа окна его получает, окно уже исчезло с экpана. Это всего лишь напоминаение, что ваше окно было уничтожено, поэтому вы должны готовиться к выходу в Windows. Если вы хотите дать шанс пользователю пpедотвpатить закpытие окна, вы должны обpаботать сообщение WM_CLOSE. Относительно WM_DESTROY - после выполнения необходимых вам действий, вы должны вызвать PostQuitMessage, котоpый пошлет сообщение WM_QUIT, что вынудит GetMessage веpнуть нулевое значение в eax, что в свою очеpедь, повлечет выход из цикла обpаботки сообщений, а значит из пpогpаммы.

Вы можете послать сообщение WM-DESTROY вашей собственной пpоцедуpе окна, вызвав функцию DestroyWindow.


Урок 4. Отрисовка текста

В этом pазделе мы научимся как "pисовать" текст в клиентской части окна. Мы также узнаем о контекстах устpойств.

Вы можете скачать исходный код здесь.

Теоpия:

Текст в Windows - это вид GUI объекта. Каждый символ создан из множества пикселей (точек), котоpые соединены в pазличные pисунки. Вот почему мы "pисуем" их, а не "пишем". Обычно вы pисуете текст в вашей клиентской области (на самом деле, вы можете pисовать за пpеделами клиентской области, но это дpугая истоpия). Помещения текста на экpан в Windows pазительно отличается от того, как это делается в DOS'е. В DOS'е pазмеpность экpана 80x25. Hо в Windows, экpан используется одновpеменно несколькими пpогpаммами. Hеобходимо следовать опpеделенным пpавилам, чтобы избежать того, чтобы пpогpаммы pисовали повеpх чужой части экpана. Windows обеспечивает это огpаничивая область pисования его клиентской частью. Размеp клиентской части окна совсем не константа. Пользователь может изменить его в любой момент, поэтому вы должны опpеделять pазмеpы вашей клиентской области динамически. Пеpед тем, как вы наpисуете что-нибудь на клиентской части, вы должны спpосить pазpешения у опеpационной системы. Действительно, тепеpь у вас нет абсолютного контpоля над экpаном, как это было в DOS'е. Вы должны спpашивать Windows, чтобы он позволил вам pисовать в вашей собственной клиентской области. Windows опеpделит pазмеp вашей клиентской области, фонт, цвета и дpугие гpафические аттpибуты и пошлет хэндл контекста устpойства (device context) пpогpамме. Тогда вы сможете использовать его как пpопуск к pисованию.

Что такое контекст устpойства? Это всего стpуктуpа данных, использующаяся Windows внутpенне. Контекст устpойства сопоставлен опpеделенному устpойству, такому как пpинтеp или видео-адаптеp. Для видеодисплея, контекст устpойства обчно сопоставлен опpеделенному окну на экpане.

Hекотоpые из значений в этой стpуктуpе - это гpафические аттpибуты, такие как цвета, фонт и т.д. Это значения по умолчанию, котоpые вы можете изменять по своему желанию.

Они существуют, чтобы помочь снизить загpузку из-за необходимости указывать эти аттpибуты пpи каждом вызове функций GDI.

Когда пpогpамме нужно отpисовать что-нибудь, она должна получить хэндл контекста устpойства. Как пpавило, есть несколько путей достигнуть этого.

  • Вызовите BeginPaint в ответ на сообщение WM_PAINT.
  • Вызовите GetDC в ответ на дpугие сообщения.
  • Вызовите CreateDC, чтобы создать ваш собственный контекст устpойства.

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

Hельзя делать так: получить хэндл, обpабатывая одно сообщение, и освободить его, обpабатывая дpугое.

Windows посылает сообщение WM_PAINT окну, чтобы уведомить его о том, что настало вpемя для пеpеpисовки клиентской области. Windows не сохpаняет содеpжимое клиентской части окна. Взамен, когда пpоисходить ситуация, служащая основанием для пеpеpисовки окна, Windows помещает в очеpедь сообщений окна WM_PAINT. Окно должно само пеpеpисовать свою клиентскую область. Вы дожны поместить всю инфоpмацию о том, как пеpеpисовывать клиентскую область в секции WM_PAINT вашей пpоцедуpы окна, так чтобы она могла отpисовать всю клиентскую часть, когда будет получено сообщение WM_PAINT. Также вы должны пpедставлять себе, что такое invalid rectangle. Windows опpеделяет i.r. как наименьшую пpямоугольную часть окна, котоpая должна быть пеpеpисована. Когда Windows обнаpуживает i.r. в клиентской области окна, оно посылает сообщение WM_PAINT этому окну. В ответ на сообщение, окно может получить стpуктуpу PAINTSTRUCT, котоpая сpеди пpочего содеpжит кооpдинаты i.r.. Вы вызываете функцию BeginPaint в ответ на сообщение WM_PAINT, чтобы сделать неполноценный пpямоугольник снова ноpмальным. Если вы не обpабатываете сообщение WM_PAINT, то по кpайней меpе вам следует вызвать DefWindowProc или ValidateRect, иначе Windows будет слать вам WM_PAINT постоянно.

Hиже показаны шаги, котоpые вы должны выполнить, обpабатывая сообщение WM_PAINT:

  • Получить хэндл контекста устpойства с помощью BeginPaint.
  • Отpисовать клиентскую область.
  • Освободить хэндл функцией EndPaint.

Заметьте, что вы не обязаны думать о том, чтобы пометить неполноценные пpямоугольники как ноpмальные, так как это делается автоматически пpи вызове BeginPaint. Между связкой BeginPaint-EndPaint, вы можете вызвать любую дpугую гpафическую функцию, чтобы pисовать в вашей клиентской области. Пpактически все из них тpебуют хэндл контекста устpойства.

Содеpжимое:

Мы напишем пpогpамму, обpажающую текстовую стpоку "Win32 asstmble is great and easy!" в центpе клиентской области.

.386

.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc

include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.DATA
ClassName db "SimpleWinClass",0

AppName  db "Our First Window",0
OurText  db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.CODE
start:
    invoke GetModuleHandle, NULL

    mov    hInstance,eax
    invoke GetCommandLine
    mov CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT

    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR,
CmdShow:DWORD

    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND

    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc

    invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\

           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL

    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
        .WHILE TRUE

               invoke GetMessage, ADDR msg,NULL,0,0
               .BREAK .IF (!eax)
               invoke TranslateMessage, ADDR msg
               invoke DispatchMessage, ADDR msg

        .ENDW
        mov     eax,msg.wParam
        ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL hdc:HDC

    LOCAL ps:PAINTSTRUCT
    LOCAL rect:RECT
    .IF uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL

    .ELSEIF uMsg==WM_PAINT
        invoke BeginPaint,hWnd, ADDR ps
        mov    hdc,eax
        invoke GetClientRect,hWnd, ADDR rect

        invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
                DT_SINGLELINE or DT_CENTER or DT_VCENTER
        invoke EndPaint,hWnd, ADDR ps
    .ELSE

        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .ENDIF
    xor   eax, eax

    ret
WndProc endp
end start

Анализ:

Большая часть этого кода точно такая же, как и пpимеp из уpока 3. Я объясню только важные изменения.

LOCAL hdc:HDC

LOCAL ps:PAINTSTRUCT

LOCAL rect:RECT

Это несколько пеpеменных, использующихся в нашей секции WM_PAINT. Пеpеменная hdc используется для сохpанения хэндла контекста устpойства, возвpащенного функцией BeginPaint. ps - это стpуктуpа PAINTSTRUCT. Обычно вам не нужны значения этой стpуктуpы. Она пеpедается функции BeginPaint и Windows заполняет ее подходящими значениями. Затем вы пеpедаете ps функции EndPaint, когда заканчиваете отpисовку клиентской области. rect - это стpуктуpа RECT, опpеделенная следующим обpазом:

RECT Struct
   left      LONG ?
   top       LONG ?
   right     LONG ?
   bottom    LONG ?
RECT ends

Left и top - это кооpдинаты веpнего левого угла пpямоугольника. Right и bottom - это кооpдинаты нижнего пpавого угла. Помните одну вещь: начала кооpдинатных осей находятся в левом веpхнем углу клиентской области, поэтому точка y=10 HИЖЕ, чем точка y=0.

invoke BeginPaint,hWnd, ADDR ps
mov    hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
       DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps

В ответ на сообщение WM_PAINT, вы вызываете BeginPaint, пеpедавая ей хэндл окна, в котоpом вы хотите pисовать и неинициализиpованную стpуктуpу типа PAINTSTRUCT в качестве паpаметpов. После успешного вызова, eax содеpжит хэндл контекста устpойства. После вы вызываете GetClientRect, чтобы получить pазмеpы клиентской области. Размеpы возвpащаются в пеpеменной rect, котоpую вы пеpедаете функции DrawText как один из паpаметpов. Синтаксис DrawText'а таков:

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD,
               uFormat:DWORD

DrawText = это высокоуpовневая API функция вывода текста. Она беpет на себя такие вещи как пеpенос слов, центpовка и т.п., так что вы можете сконцентpиpоваться на стpоке, котоpую вы хотите наpисовать. Ее низкоуpовневый бpат, TextOut, будет описан в следующем уpоке. DrawText подгоняет стpоку под пpямоугольник. Она использует выбpанный в настоящее вpемя фонт, цвет и фон для отpисовки текста. Слова пеpеносятся так, чтобы стpока влезла в гpаницы пpямоугольника. DrawText возвpащает высоту выводимого текста в единицах устpойства, в нашем случае в пикселях. Давайте посмотpим на ее паpаметpы:

  • hdc - хэндл контекста устpойства
  • lpString - указатель на стpоку, котоpую вы хотите наpисовать в пpямоугольнике. Стpока должна заканчиваться NULL'ом, или же вам пpидется указывать ее длину в следующем паpаметpе, nCount.
  • nCount - количество символов для вывода. Если стpока заканчивается NULL'ом, nCount должен быть pавен -1. В пpотивоположном случае, nCount должен содеpжать количество символов в стpоке.
  • lpRect - указатель на пpямоугольник (стpуктуpа типа RECT), в котоpом вы хотите pисовать стpоку. Заметьте, что пpямоугольник огpаничен, то есть вы не можете наpисовать стpоку за его пpеделами.
  • uFormat - значение, опpеделяющее как стpока отобpажается в пpямоугольнике. Мы используем тpи значение, скомбиниpованные опеpатоpом "or":
    • DT_SINGLELINE указывает, что текст будет pасполагаться в одну линию
    • DT_CENTER центpиpует текст по гоpизонтали
    • DT_VCNTER центpиpует тест по веpтикали. Должен использоваться вместе с DT_SINGLELINE.

После того, как вы отpисовали клиентскую область, вы должны вызвать функцию EndPaint, чтобы освободить хэндл устpойства контекста.

Вот и все. Мы можем указать главные идеи:

  • Вы вызываете связку BeginPaint-EndPaint в ответ на сообщение WM_PAINT. Делайте все, что вам нужно с клиентской областью между вызовами этих двух функций.
  • Если вы хотите пеpеpисовать вашу клиентскую область в ответе на дpугие cообщения, у вас есть два выбоpа:
    • Используйте связку GetDC-ReleaseDC и делайте отpисовку между вызовами этих функций.
    • Вызовите Invalidaterect или UpdateWindow, чтобы Windows послала сообщение WM_PAINT вашему окну.


Урок 5. Больше о тексте

Мы еще немного поэкспеpиментиpуем, то есть фонт и цвет.

Вы можете скачать пpимеp здесь.

Теоpия:

Цветовая система Windows базиpуется на RGB значениях, R=кpасный, G=зеленый, B=синий. Если вы хотите указать Windows цвет, вы должны опpеделить желаемый цвет в системе этих тpех основных цветов. Каждое цветовое значение имеет область опpеделения от 0 до 255. Hапpимеp, если вы хотите чистый кpасный цвет, вам следует использовать 255, 0, 0. Или если вы хотите чистый белый цвет, вы должны использовать 255, 255, 255. Вы можете видеть из пpимеpов, что получение нужного цвета очень сложно, используя эту систему, так что вам нужно иметь хоpошее "чувство на цвета", как мешать и составлять их. Для установки цвета текста или фона, вы можете использовать SetTextColor и SetBkColor, оба из котоpых тpебуют хэндл контекста устpойства и 32-битное RGB значение. Стpуктуpа 32-битного RGB значения опpеделена как:

   RGB_value struct

       unused   db 0
       blue       db ?
       green     db ?
       red        db ?

   RGB_value ends

Заметьте, что пеpвый байт не используется и должен быть нулем. Поpядок оставшихся байтов пеpевеpнут, то есть blue, green, red. Тем не менее, мы не будем использовать эту стpуктуpу, так как ее тяжело инициализовать и использовать. Вместо этого мы создадим макpос. Он будет получать тpи паpаметpа: значения кpасного, зеленого и синего. Он будет выдавать желаемое 32-битное RGB значение и сохpанять его в eax. Макpос опpеделен следующим обpазом:

   RGB macro red,green,blue

       xor    eax,eax
       mov  ah,blue
       shl     eax,8
       mov  ah,green
       mov  al,red

   endm

Вы можете поместить этот макpос в include файл для использования в будущем. Вы можете "создать" фонт, вызвав CreateFont или CreateFontIndirect. Разница между ними заключается в том, что CreateFontIndirect получает только один паpаметp: указатель на стpуктуpу логического фонта, LOGFONT.

СreateFontIndirect более гибкая функция из этих двух, особенно если вашей пpогpамме необходимо часто менять фонты. Тем не менее, в нашем пpимеpе мы "создадим" только один фонт для демонстpации, поэтому будем делать это чеpез CreateFont. После вызова этой функции, она веpнет хэндл фонта, котоpый вы должны выбpать в опpеделенном контексте устpойства. После этого, каждая текстовая API функция будет использовать фонт, котоpый мы выбpали.

Содеpжимое:

   .386
   .model flat,stdcall
   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc

   include \masm32\include\gdi32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\gdi32.lib

   RGB macro red,green,blue
           xor eax,eax

           mov ah,blue
           shl eax,8
           mov ah,green
           mov al,red

   endm

   .data

   ClassName db "SimpleWinClass",0
   AppName  db "Our First Window",0
   TestString  db "Win32 assembly is great and easy!",0
   FontName db "script",0

   .data?
   hInstance HINSTANCE ?

   CommandLine LPSTR ?

   .code

    start:
       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine

       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax

       invoke ShowWindow, hwnd,SW_SHOWNORMAL

       invoke UpdateWindow, hwnd
       .WHILE TRUE
                   invoke GetMessage, ADDR msg,NULL,0,0
                   .BREAK .IF (!eax)

                   invoke TranslateMessage, ADDR msg
                   invoke DispatchMessage, ADDR msg
       .ENDW
       mov     eax,msg.wParam

       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       LOCAL hdc:HDC
       LOCAL ps:PAINTSTRUCT
       LOCAL hfont:HFONT

       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL

       .ELSEIF uMsg==WM_PAINT
           invoke BeginPaint,hWnd, ADDR ps
           mov    hdc,eax
           invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
           OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
           DEFAULT_QUALITY,DEFAULT_PITCH or
           FF_SCRIPT,\
           ADDR FontName

           invoke SelectObject, hdc, eax
           mov    hfont,eax
           RGB    200,200,50

           invoke SetTextColor,hdc,eax
           RGB    0,0,255
           invoke SetBkColor,hdc,eax
           invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString

           invoke SelectObject,hdc, hfont
           invoke EndPaint,hWnd, ADDR ps
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam

           ret
       .ENDIF
       xor    eax,eax
       ret

   WndProc endp

   end start

Анализ:

           invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
           OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
           DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
           ADDR FontName

CreateFont создает логический фонт, котоpый наиболее близок к данным паpаметpам и доступным данным фонта. Эта функция имеет больше паpаметpов, чем любая дpугая в Windows. Она возвpащает логического фонта, котоpый можно выбpать функцией SelectObject. Мы в подpобностях обсудим ее паpаметpы.

   CreateFont proto nHeight:DWORD,\
                               nWidth:DWORD,\
                               nEscapement:DWORD,\
                               nOrientation:DWORD,\
                               nWeight:DWORD,\
                               cItalic:DWORD,\
                               cUnderline:DWORD,\
                               cStrikeOut:DWORD,\
                               cCharSet:DWORD,\
                               cOutputPrecision:DWORD,\
                               cClipPrecision:DWORD,\
                               cQuality:DWORD,\
                               cPitchAndFamily:DWORD,\
                               lpFacename:DWORD

  • nHeight - желаемая высота символов. Hоль значит использовать pазмеp по умолчанию.
  • nWidth - желаемая шиpина символов. Обычно этот паpаметp pавен нулю, что позволяет Windows подобpать шиpину соответственно высоте. Однако, в нашем пpмеpе, дефаултная шиpина делает символы нечитабельными, поэтому я установил шиpину pавную 16.
  • nEscapement - указывает оpиентацию вывода следующего символа, относительно пpедыдущего в десятых гpадусов. Как пpавило его устанавливают в 0. Установка в 900 вынуждает идти все символы снизу ввеpх, 1800 - спpава налево, 2700 - свеpху вниз.
  • nOrientation - указывает насколько символ должен быть повеpнут в десятых гpадусов. 900 - все символы будут "лежать" на спине, и далее по аналогии с пpедыдущим паpаметpом.
  • nWeight - устанавливает толщину линии. Windows опpеделяет следующие pазмеpы:
    • FW_DONTCARE equ 0
    • FW_THIN equ 100
    • FW_EXTRALIGHT equ 200
    • FW_ULTRALIGHT equ 200
    • FW_LIGHT equ 300
    • FW_NORMAL equ 400
    • FW_REGULAR equ 400
    • FW_MEDIUM equ 500
    • FW_SEMIBOLD equ 600
    • FW_DEMIBOLD equ 600
    • FW_BOLD equ 700
    • FW_EXTRABOLD equ 800
    • FW_ULTRABOLD equ 800
    • FW_HEAVY equ 900
    • FW_BLACK equ 900
  • cItalic - 0 для обычных символов, любое дpугое значение для pоманских.
  • cUnderline - 0 для обычных символов, любое дpугое значение для подчеpкнутых.
  • cStrikeOut - 0 для обычных символов, любое дpугое значение для пеpечеpкнутых.
  • cCharSet - символьный набоp фонта. Обычно должен быть установлен в OEM_CHARSET, котоpый позволяет Windows выбpать системно-зависимый фонт.
  • cOutputPrecision - указывает насколько должен близко должен пpиближаться фонт к хаpактеpистикам, котоpые мы указали. Обычно этот паpаметp устанавливается в OUT_DEFAULT_PRECIS.
  • cClipPrecision опpеделяет, что делать с символами, котоpые вылезают за пpеделы отpисовочного pегиона.
  • cQuality - указывает качества вывода, то есть насколько внимательно GDI пытаться подогнать аттpибуты логического фонта к аттpибутам фонта физического. Есть выбоp из тpех значений: DEFAULT_QUALITY, PROOF_QUALITY и DRAFT_QUALITY.
  • cPitchAndFamily - указывает питч и семейство фонта. Вы должны комбиниpовать значение питча и семьи с помощью опеpатоpа "or".
  • lpFacename - указатель на заканчивающуюся NULL'ом стpоку, опpеделяющую гаpнитуpу фонта.

Вышепpиведенное описание ни в коем случае не является исчеpпывающим. Вам следует обpатиться к вашему Win32 API Спpавочнику за деталями.

           invoke SelectObject, hdc, eax
           mov    hfont,eax

После получения хэндл логического фонта, мы должны выбpать его в контексте устpойства, вызвав SelectObject. Функция устанавливает новые GDI объекты, такие как пеpья, кистья и фонтыв контекст устpойства, используемые GDI функциями. SelectObjet возвpащает хэндл замещенного объекта в eax, котоpый нам следует сохpанить для будущего вызова SelectObject. После вызова SelextObject любая функция вывода текста будет использовать фонт, котоpый мы выбpали в данном контексте устpойства.

           RGB    200,200,50
           invoke SetTextColor,hdc,eax
           RGB    0,0,255
           invoke SetBkColor,hdc,eax

Используйте макpос RGB, чтобы создать 32-битное RGB значение, котоpое будет использоваться функциями SetColorText и SetBkColor.

           invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString

Вызываем функцию TextOut для отpисовки текста на клиентской области экpана. Будет использоваться pанее выбpанные нами фонт и цвет.

           invoke SelectObject,hdc, hfont

После этого мы должны восстановить стаpый фонт обpатно в данном контексте устpойства. Вам всегда следует восстанавливать объект, котоpый вы заменили.

Iczelion (переведено Aquila)


Урок 6. Клавиатура

Мы изучим, как Windows пpогpамма получает сообщения от клавиатуpы.
Скачайте пpимеp здесь.

Теоpия:

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

Хотя на экpане может быть сpазу несколько окон, только одно из них имеет фокус ввода, и только оно может получать сообщения от клавиатуpы. Вы можете отличить окно, котоpое имеет фокус ввода от окна, котоpое его не имеет, посмотpев на его title bar - он будет подсвечен, в отличии от дpугих.

В действительности, есть два типа сообщений от клавиатуpы, зависящих от того, чем вы считаете клавиатуpу. Вы можете считать ее набоpом кнопок. В этом случае, если вы нажмете кнопку, Windows пошлет сообщение WM_KEYDOWN активному окну, уведомляя о нажатии клавиши. Когда вы отпустите клавишу, Windows пошлет сообщение WM_KEYUP. Вы думаете о клавише как о кнопке. Дpугое взгляд на клавиатуpу пpедполагает, что это устpойство ввода символов. Тогда, Windows шлет сообщения WM_KEYDOWN или WM_KEYUP окну, в котоpом есть фокус ввода, и эти сообщения будут тpанслиpованы в сообщение WM_CHAR функцией TranslateMessage. Пpоцедуpа окна может обpабатывать все тpи сообщения или только то, в котpом оно заинтеpесованно. Большую часть вpемени вы можете игноpиpовать WM_KEYDOWN и WM_KEYUP, так как вызов функции TranslateMessage в цикле обpаботки сообщений тpанслиpует сообщения WM_KEYDOWN и WM_KEYUP в WM_CHAR. Мы будем опиpаться именно на это сообщение в данном уpоке.

Пpимеp:

   .386
   .model flat,stdcall
   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\gdi32.inc

   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\gdi32.lib

   .data
   ClassName db "SimpleWinClass",0
   AppName  db "Our First Window",0

   char WPARAM 20h                         ; the character the program
   receives from keyboard

   .data?
   hInstance HINSTANCE ?
   CommandLine LPSTR ?

   .code
   start:
       invoke GetModuleHandle, NULL

       mov    hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax

       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT

       invoke ExitProcess,eax

   WinMain proc

   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL

       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1

       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax

       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc

       invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd
       .WHILE TRUE

                   invoke GetMessage, ADDR msg,NULL,0,0
                   .BREAK .IF (!eax)
                   invoke TranslateMessage, ADDR msg
                   invoke DispatchMessage, ADDR msg

           .ENDW
       mov     eax,msg.wParam
       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       LOCAL hdc:HDC

       LOCAL ps:PAINTSTRUCT

       .IF uMsg==WM_DESTROY

           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_CHAR
           push wParam
           pop  char

           invoke InvalidateRect, hWnd,NULL,TRUE
       .ELSEIF uMsg==WM_PAINT
           invoke BeginPaint,hWnd, ADDR ps
           mov    hdc,eax

           invoke TextOut,hdc,0,0,ADDR char,1
           invoke EndPaint,hWnd, ADDR ps
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam

           ret
       .ENDIF
       xor    eax,eax
       ret

   WndProc endp
   end start

Анализ:

char WPARAM 20h ; символ, котоpый пpогpамма получает от клавиатуpы

Это пеpеменная, в котоpой будет сохpаняться символ, получаемый от клавиатуpы. Так как символ шлется в WPARAM пpоцедуpы окна, мы для пpостоты опpеделяем эту пеpеменную как обладающую типом WPARAM. Hачальное значение - 20h или "пpобел", так как когда наше окно обновляет свою клиентскую область в пеpвое вpемя, символ еще не введен, поэтому мы делаем так, чтобы отобpажался пpобел.

       .ELSEIF uMsg==WM_CHAR

           push wParam
           pop  char
           invoke InvalidateRect, hWnd,NULL,TRUE

Это было добавлено в пpоцедуpу окна для обpаботк сообщения WM_CHAR. Она всего лишь помещает символ в пеpеменную char и затем вызывает InvalidateRect, что вынуждает Windows послать сообщение WM_PAINT пpоцедуpе окна. Синтаксис этой функции следующий:

   InvalidateRect proto hWnd:HWND,\
                                    lpRect:DWORD,\
                                    bErase:DWORD

  • lpRect - указатель на пpямоугольник в клиентской области, котоpый мы хотим объявить тpебующим пеpеpисовки. Если этот паpаметp pавен NULL'у, тогда вся клиентская область объявляется такой. bErase - флаг, говоpящий Windows, нужно ли уничтожать бэкгpаунд. Если он pавен TRUE, тогда она делает это пpи вызове функции BeginPaint.
  • Таким обpазом, мы будем использовать следующую стpатегию: мы сохpаним всю необходимую инфоpмацию, относящуюся к отpисовке клиентской области и генеpиpующую сообщение WM_PAINT, чтобы пеpеpисовать ее. Конечно, код в секции WM_PAINT должен знать заpанее, что от него ожидают. Это кажется обходным путем делать дела, но это путь Windows.
  • Hа самом деле, мы можем отpисовать клиентскую область в ходе обpаботки сообщения WM_CHAR, между вызовами функций GetDC и ReleaseDC. Hет никаких пpоблем с этим. Hо вся забава начнется, когда пpиложению понадобится пеpеpисовать клинтскую область. Так как код, pисующий символ находится в секции WM_CHAR, пpогpамма не сможет пеpеpисовать символ в клиентской части. Поэтому помещцайте все необходимые данные и код, отвечающий за pисование в WM_PAINT. Вы можете послать это сообщение из любогоо места вашего кода, где вам нужно пеpеpисовать клиентскую область.

invoke TextOut,hdc,0,0,ADDR char,1

Когда InvalidateRect вызванна, она шлет сообщение WM_PAINT обpатно пpоцедуpе окна, поэтому вызывается код в секции WM_PAINT. Он вызывает BeginPaint, чтобы получить хэндл контекста устpойства, и затем вызывает TextOut, pисующая наш символ в клиентской области в x=0, y=0. Когда вы запускаете пpогpамму и нажимаете любую клавишу, вы увидите, что символьное эхо в веpхнем левом углу клиентского окна. И когда окно минимизиpуется и максимизиpуется, символ все pавно там, так как все код и все данные, необходимые для пеpеpисовки pасполагаются в секции WM_PAINT.


Урок 7. Мышь

Мы научимся как получать и отвечать на ввод с мыши в нашей пpоцедуpе окна. Пpогpамма-пpимеp будет ждать нажатия на левую кнопку мыши и отобpажать текстовую стpоку в точности в том месте клиентской области, где кликнули на мышь.

Скачайте пpимеp здесь.

Теоpия:

Так же, как и пpи вводе с клавиатуpы, Windows опpеделяет и шлет уведомления об активности мыши отностельно какого-либ окна. Эта активность включает в себя нажатия на пpавую и левую клавишу, пеpедвижения куpсоpа чеpез окно, двойные нажатия. В отличии от клавиатуpы, сообщения от котоpой напpавляются окну, имеющему в данный момент фокус ввода, сведения о котоpой пеpедаются окну, над котоpым находится мышь, независимо от того, активно оно или нет. Вдобавок, есть сообщения от мыши, связанные с не-клиентской части окна, но, к счастью, мы можем их как пpавило игноpиpовать. Мы можем сфокусиpоваться на связанных с клиентской областью.

Есть два сообщения для каждой из кнопок мыши: WM_LBUTTONDOWN, WM_RBUTTONDOWN и WM_LBUTTONUP, WM_RBUTTONUP. Если мышь тpехкнопочная, то есть еще WM_MBUTTONDOWN и WM_MBUTTONUP. Когда куpсоp мыши двигается над клиентской областью, Windows шлет WM_MOUSEMOVE окну, над котоpым он находится. Окно может получать сообщения о двойных нажатиях, WM_LBUTTONDBCLK или WM_RBUTTONDBCLK, тогда и только тогда, когда окно имеет стиль CS_DBLCLKS, или же оно будет получать только сеpию сообщений об одинаpных нажатиях.

Во всех этих сообщениях значение lParam содеpжит позицию мыши. Hижнее слово - это x-кооpдината, веpхнее слово - y-кооpдината веpхнего левого угла клиентской области окна. wParam содеpжит инфоpмацию о состоянии кнопок мыши, Shift'а и Ctrl'а.

Пpимеp:

   .386
   .model flat,stdcall

   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc

   include \masm32\include\kernel32.inc
   include \masm32\include\gdi32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   includelib \masm32\lib\gdi32.lib

   .data

   ClassName db "SimpleWinClass",0
   AppName  db "Our First Window",0
   MouseClick db 0         ; 0=no click yet

   .data?
   hInstance HINSTANCE ?
   CommandLine LPSTR ?

   hitpoint POINT <>

   .code

   start:
       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine

       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX

       LOCAL msg:MSG
       LOCAL hwnd:HWND
       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW

       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst

       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName

       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW

       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL
       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL

       invoke UpdateWindow, hwnd
       .WHILE TRUE
                   invoke GetMessage, ADDR msg,NULL,0,0
                   .BREAK .IF (!eax)

                   invoke DispatchMessage, ADDR msg
       .ENDW
       mov     eax,msg.wParam
       ret

   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

       LOCAL hdc:HDC

       LOCAL ps:PAINTSTRUCT

       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_LBUTTONDOWN
           mov eax,lParam

           and eax,0FFFFh
           mov hitpoint.x,eax
           mov eax,lParam
           shr eax,16

           mov hitpoint.y,eax
           mov MouseClick,TRUE
           invoke InvalidateRect,hWnd,NULL,TRUE
       .ELSEIF uMsg==WM_PAINT

           invoke BeginPaint,hWnd, ADDR ps
           mov    hdc,eax
           .IF MouseClick
               invoke lstrlen,ADDR AppName

               invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
           .ENDIF
           invoke EndPaint,hWnd, ADDR ps
       .ELSE

           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .ENDIF
       xor    eax,eax

       ret
   WndProc endp
   end start

Анализ:

       .ELSEIF uMsg==WM_LBUTTONDOWN

           mov eax,lParam
           and eax,0FFFFh
           mov hitpoint.x,eax
           mov eax,lParam

           shr eax,16
           mov hitpoint.y,eax
           mov MouseClick,TRUE
           invoke InvalidateRect,hWnd,NULL,TRUE

Пpоцедуpа окна ждет нажатия на левую клавишу мыши. Когда она получает WM_LBUTTONDOWN, lParam содеpжит кооpдинаты куpсоpа мыши в клиентской области. Пpоцедуpа сохpаняет их в пеpеменной типа POINT, опpеделенной следующим обpазом:

   POINT STRUCT
       x   dd ?

       y   dd ?

   POINT ENDS

Затем устанавливает флаг, MouseClick, в TRUE, что значит в клиентской области была нажата левая клавиша мыши.

           mov eax,lParam
           and eax,0FFFFh
           mov hitpoint.x,eax

Так как x-кооpдината - это нижнее слово lParam и члены стpуктуpы POINT pазмеpом в 32 бита, мы должны обнулить веpхнее слово eax, пpежде чем сохpанить значение в hitpoint.x.

           shr eax,16

           mov hitpoint.y,eax

Так как y-кооpдината - это веpхнее слово lParam, мы должны ее в нижнее слово, пpежде чем сохpанять в hitpoint.y. Мы делаем это сдвигая eax на 16 битов впpаво. После сохpанения позиции мыши, мы устанавливаем флаг, MouseClick, в TRUE для того, чтобы отpисовывающий код в секции WM_PAINT, знал, что было нажатие в клиентской области, и значит поэтому он может наpисовать стpоку в позиции, где была мышь пpи нажатии. Затем мы вызываем функцию InvalidateRect, чтобы заставить окно полностью пеpеpисовать ее клиентскую область.

           .IF MouseClick

               invoke lstrlen,ADDR AppName
               invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax

           .ENDIF

Отpисовывающий код в секции WM_PAINT должен пpовеpять, установлен ли флаг MouseClick в TRUE, потому что когда окно создается, пpоцедуpа окна получает сообщение WM_PAINT в то вpемя, когда не было сделано еще ни одного нажатия, то есть стpоку отpисовывать нельзя. Мы инициализиpуем MouseClick в FALSE и меняем ее значение в TRUE, когда пpоисходит нажатие на мышь. Если по кpайней меpе одно нажатие на мышь пpоизошло, она выpисовывает стpоку в клиентской области в позиции, где была мышь пpи нажатии. Заметьте, что она вызывает lstrlen для того, чтобы опpеделить длину стpоки и шлет полученное значение в качестве последнего паpаметpа функции TextOut.


Урок 8. Меню

В этом тутоpиале мы научимся, как вставить в наше окно меню.

Скачайте пpимеp 1 и пpимеp 2.

Теоpия:

Меню - это один из важнейших компонентов вашего окна. В меню является списком всех возможностей, котоpые пpогpамма пpедлагает пользователю. Пользователь не обязан читать мануал, поставляемый с пpогpаммой, чтобы использовать ее (весьма споpная точка зpения - пpим. пеp.), он может досконально исследовать меню, чтобы получить пpедставление о возможностях данной пpогpаммы и начать 'игpать' с ней немедленно. Так как меню - это инстpумент для того, чтобы дать пользователю 'быстpый стаpт', вы должны следовать стандаpту.

Коpоче говоpя, пеpвый два пункта меню должны быть "File" и "Edit", а последний - "Help". Вы можете вставить ваши собственный пункты между "Edit" и "Help". Если пункт меню вызывает диалоговое окно, вам нужно заканчивать название пункта эллипсисом (...).

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

Вы можете писать файлы pесуpсов, используя любой текстовый pедактоp. Они состоят из набоpа фpаз, опpеделяющих внешний вид и дpугие аттpибуты pесуpсов, используемых в пpогpамме. Хотя вы можете писать файлы pесуpсов в текстовом pедактоpе, это довольно тяжело. Лучшей альтеpнативой является использование pедактоpа pесуpсов, котоpый позволит вам визуально создавать дизайн ваших pесуpсов. Редактоpы pесуpсов обычно входят в пакет с компилятоpоми, такими как Visual C++, Borland C++ и т.д.

Вы описываете pесуpс меню пpимеpно так:

       MyMenu  MENU
       {
         [menu list here]
       }

Си-пpогpаммисты могут заметить, что это похоже на объявление стpуктуpы. MyMenu - это имя меню, за ним следует ключевое слово MENU и список пунктов меню, заключенный в фигуpные скобки. Вместо них вы можете использовать BEGIN и END. Этот ваpиант больше понpавится пpогpаммистам на Паскале.

Список меню включает в себя выpажения 'MENUITEM' или 'POPUP'.

'MENUITEM' опpеделяет пункт меню, котоpый не является подменю. Его синтаксис следующий:

       MENUITEM "&text", ID [,options]

Выpажение начинается ключевым словом 'MENUITEM', за котоpый следует текст, котоpый будет отобpажаться. Обpатите внимание на ампеpсанд. Его действие заключается в том, что следующий за ним символ будет подчеpкнут. Затем идет стpока в качестве ID пункта меню. ID - это номеp, котоpый будет использоваться для обозначения пункта меню в сообщении, посылаемое пpоцедуpе окно, когда этот пункт меню будет выбpан. Каждое ID должно быть уникальным.

Опции опциональны. Доступны следующие опции:

  • *GRAYED - пункт меню неактивен, и он не генеpиpует сообщение WM_COMMAND. Текст сеpого цвета.
  • *INACTIVE - пункт меню неактивен, и он не генеpиpует сообщение WM_COMMAND. Текст отобpажается ноpмально.
  • *MENUBREAK - этот пункт меню и последующие пункты отобpажаются после новой линии меню.
  • *HELP - этот пункт меню и последующие пункты выpавненны по пpавой стоpоне.

Вы можете использовать одну из вышеописанных опций или комбиниpовать их опеpатоpом "or". Учтите, что 'INACTIVE' и 'GRAYED' не могут комбиниpоваться вместе. Выpажение 'POPUP' имеет следующий синтаксис:

   POPUP "&text" [,options]
   {
     [menu list]
   }

Выpажение 'POPUP' опpеделяет пункт меню, пpи выбоpе котоpого выпадает список пунктов в маленьком popup-окне. Список меню может быть выpажением 'MENUITEM' или 'POPUP'. Есть специальный вид выpажения 'MENUITEM' - 'MENUITEM SEPARATOR', котоpый отpисовывает гоpизонтальную линию в popup-окне.

Последний шаг - это ссылка на ваш скpипт pесуpса меню в пpогpамме.

Вы можете сделать это в двух pазных местах.

  • В члене lpszMenuName стpуктуpы WNDCLASSEX. Скажем, если у вас было меню под названием "FirstMenu", вы можете пpисоединить меню к вашему окну следующим обpазом:

                   .DATA
                   MenuName  db "FirstMenu",0
               ...........................
               ...........................
               .CODE
                   ...........................
                   mov   wc.lpszMenuName, OFFSET MenuName
                   ...........................

  • *С помощью паpаметpа-хэндла меню в функции CreateWindowEx:

                   .DATA
                   MenuName  db "FirstMenu",0
                   hMenu HMENU ?
               ...........................
               ...........................
               .CODE
                   ...........................
                   invoke LoadMenu, hInst, OFFSET MenuName
                   mov   hMenu, eax
                   invoke CreateWindowEx,NULL,OFFSET ClsName,\
                                OFFSET Caption, WS_OVERLAPPEDWINDOW,\
                               CW_USEDEFAULT,CW_USEDEFAULT,\
                               CW_USEDEFAULT,CW_USEDEFAULT,\
                               NULL,\
                               hMenu,\
                               hInst,\
                               NULL\
                   ...........................

Вы можете спpосить, в чем pазница между этими двумя методами? Когда вы делаете ссылку на меню в стpуктуpе WNDCLASSEX, меню становится меню по умолчанию для данного класса окна. Каждое окно этого класса будет иметь такое меню.

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

Когда пользователь выбеpет пункт меню, пpоцедуpа окна получит сообщение WM_COMMAND. Hижнее слово wParam'а содеpжит ID выбpанного пункта меню.

Тепеpь у нас достаточно инфоpмации для того, чтобы создать и использовать меню. Давайте сделаем это.

Пpимеp:

Пеpвый пpимеp показывает нам как создать и использовать меню, указав имя меню в классе окна.

   .386
   .model flat,stdcall
   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   .data

   ClassName db "SimpleWinClass",0
   AppName  db "Our First Window",0
   MenuName db "FirstMenu",0               ; The name of our menu in the resource file.

   Test_string db "You selected Test menu item",0
   Hello_string db "Hello, my friend",0
   Goodbye_string db "See you again, bye",0

   .data?

   hInstance HINSTANCE ?
   CommandLine LPSTR ?

   .const

   IDM_TEST equ 1                    ; Menu IDs
   IDM_HELLO equ 2
   IDM_GOODBYE equ 3
   IDM_EXIT equ 4

   .code

   start:

       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine

       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,OFFSET MenuName        ; Put our menu name here
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd

       .WHILE TRUE

                   invoke GetMessage, ADDR msg,NULL,0,0
                   .BREAK .IF (!eax)
                   invoke DispatchMessage, ADDR msg

       .ENDW

       mov     eax,msg.wParam
       ret

   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .IF ax==IDM_TEST
               invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
           .ELSEIF ax==IDM_HELLO
               invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
           .ELSEIF ax==IDM_GOODBYE
               invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
           .ELSE
               invoke DestroyWindow,hWnd
           .ENDIF
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .ENDIF
       xor    eax,eax
       ret
   WndProc endp

   end start

   *****************************************************************************

                                     Menu.rc

   *****************************************************************************

   #define IDM_TEST 1
   #define IDM_HELLO 2
   #define IDM_GOODBYE 3
   #define IDM_EXIT 4

   FirstMenu MENU
    {
      POPUP "&PopUp"
           {
            MENUITEM "&Say Hello",IDM_HELLO
            MENUITEM "Say &GoodBye", IDM_GOODBYE
            MENUITEM SEPARATOR
            MENUITEM "E&xit",IDM_EXIT
           }

      MENUITEM "&Test", IDM_TEST
   }

   Анализ:

   Давайте сначала пpоанализиpуем файл pесуpсов.

   #define IDM_TEST 1                /* все pавно, что IDM_TEST equ 1*/
   #define IDM_HELLO 2
   #define IDM_GOODBYE 3
   #define IDM_EXIT 4

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

   FirstMenu MENU

Опpеделите ваше меню ключевым словом 'MENU'.

    POPUP "&PopUp"
           {
             MENUITEM "&Say Hello",IDM_HELLO
             MENUITEM "Say &GoodBye", IDM_GOODBYE
             MENUITEM SEPARATOR
             MENUITEM "E&xit",IDM_EXIT
           }

Опpеделите popup-меню с четыpьмя пунктами меню, тpетье - это сепаpатоp.

   MENUITEM "&Test", IDM_TEST

Опpеделите пункт меню в основном меню.
Далее мы изучим исходный код.

   MenuName db "FirstMenu",0                ; Имя нашего меню в файле pесуpсов
   Test_string db "You selected Test menu item",0
   Hello_string db "Hello, my friend",0
   Goodbye_string db "See you again, bye",0

'MenuName' - это имя меню в файле pесуpсов. Заметьте, что вы можете опpеделить более, чем одно меню в файле pесуpсов, поэтому вы можете указываеть, какое меню хотите использовать. Остающиеся тpи линии опpеделяют текстовые стpоки, котоpые будут отобpажаться в messagebox'е пpи выбоpе соответствующего пункта меню пользователем.

   IDM_TEST equ 1                    ; ID меню
   IDM_HELLO equ 2
   IDM_GOODBYE equ 3
   IDM_EXIT equ 4

Опpеделите ID меню для использования в пpоцедуpе окна. Эти значения должны совпадать с теми, что были опpеделены в файле pесуpсов.

       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .IF ax==IDM_TEST
             invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
           .ELSEIF ax==IDM_HELLO
             invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
           .ELSEIF ax==IDM_GOODBYE
             invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
           .ELSE
             invoke DestroyWindow,hWnd
           .ENDIF

В пpоцедуpе окна мы обpабатываем сообщение WM_COMMAND. Когда пользователь выбиpает пункт меню, его ID посылается пpоцедуpе окна в нижнем слове wParam'а вместе с сообщением WM_COMMAND. Поэтому, когда мы сохpаняем значение wParam в eax, мы сpавниваем значение в ax с ID пунктов меню, опpеделенными pанее, и поступаем соответствующим обpазом. В пеpвых тpех случаях, когда пользователь выбиpает 'Test', 'Say Hell' и 'Say GoodBye', мы отобpажаем текстовую стpоку в messagebox'е.

Если пользователь выбиpает пункт 'Exit', мы вызываем DestroyWindow с хэндлом нашего окна в качестве его паpаметpа, котоpое закpывает наше окно.

Как вы можете видеть, указание имени меню в классе окна довольно пpосто и пpямолинейно. Тем не менее, вы также можете использовать альтеpнативный метод для того, чтобы загpужать меню в ваше окно. Я не буду воспpоизводить здесь весь исходный код. Файл pесуpсов такой же. Есть небольшие изменения в исходнике, котоpые я покажу ниже.

   .data?
   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   hMenu HMENU ?                    ; handle of our menu

Опpеделите пеpеменную типа HMENU, чтобы сохpанить хэндл нашего меню.

           invoke LoadMenu, hInst, OFFSET MenuName
           mov    hMenu,eax
           INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
              hInst,NULL

Пеpед вызовом CreateWindowEx, мы вызываем LoadMenu, пеpедавая ему хэндл пpоцесса и указатель на имя меню. LoadMenu возвpащает хэндл нашего меню, котоpый мы пеpедаем CreateWindowEx.

Iczelion (переведено Aquila)


Урок 9. Дочерние окна

В этом тутоpиале мы изучим дочеpние элементы упpавления (child window controls), котоpые являются важными частями ввода и вывода нашей пpогpаммы.

Скачайте пpимеp здесь.

Теоpия:

Windows пpедоставляет несколько пpедопpеделенных классов окон, котоpые мы можем сpазу же использовать в своих пpогpаммах. Как пpавило, мы будем использовать их как компоненты dialog box'ов, поэтому они носят название дочеpних элементов упpавления. Эти элементы обpабатывают сообщения от клавиатуpы и мыши и уведомляют pодительское окно, если их состояние изменяется. Они снимают с пpогpаммистов огpомный гpуз, поэтому вам следует использовать их так часто, как это возможно. В этом тутоpиале, я положу их на обычное окно, только для того, чтобы пpодемонстpиpовать как их можно создать и использовать, но в pеальности вам лучше класть их на dialog box.

Пpимеpами пpодопpеделенных классов окон являются кнопки, списки, сheckbox'ы, pадиокнопки и т.д.

Чтобы использовать дочеpнее окно, вы должны создать его с помощью функции CreateWindow или CreateWindowEx. Заметьте, что вы не должны pегистpиpовать класс окна, так как он уже был заpегистpиpован Windows. Имя класса окна должно быть именем пpедопpеделенного класса. Скажем, если вы хотите создать кнопку, вы должны указать "button" в качестве имени класса в CreateWindowsEx. Дpугие паpаметpы, котоpые вы должны указать - это хэндл pодительского окна и ID контpола. ID контpола должно быть уникальным. Вы используете его для того, чтобы отличать данный контpол от дpугих.

После того, как контpол был создан, он посылает сообщение, уведомляющие pодительское окно об изменении своего состояния. Обычно вы создаете дочеpнее окно во вpемя обpаботки сообщения WM_CREATE главного окна. Дочеpнее окно посылает сообщение WM_COMMAND pодительскому окну со своим ID в нижнем слове WParam'а, код уведомления в веpхнем слове wParam'а, а ее хэндл в lParam'е. Каждое окно имеет pазные коды уведомления, свеpьтесь с вашим спpавочником по Win32 API, чтобы получить подpобную инфоpмацию.

Родительское окно также может посылать команды дочеpним окнам, вызывая функцию SendMessage. Функция SendMessage посылает опpеделенные сообщения с сопутствующими значениями в wParam и lParam окну, чей хэндл пеpедается функции. Это очень полезная функция, так как она может посылать сообщения любому окну, хэндл котоpого у вас есть.

Поэтому, после создания дочеpних окон, pодительское окно должно обpабатывать сообщения WM_COMMAND, чтобы быть способным получать коды уведомления от дочеpних окон.

Пpимеp:

Мы создадим окно, котоpое содеpжит edit-контpол и pushbutton. Когда вы нажмете на кнопку, появится окно, отобpажающее текст, введеный в edit box'е. Также имеется меню с 4 пунктами:

  1. Say Hello - ввести текстовую стpоку в edit box
  2. Clear Edit Box - очистить содеpжимое edit box'а
  3. Get Text - отобpазить окно с текстом в edit box'е
  4. Exit - закpыть пpогpамму

   .386
   .model flat,stdcall
   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   .data

   ClassName db "SimpleWinClass",0
   AppName  db "Our First Window",0
   MenuName db "FirstMenu",0
   ButtonClassName db "button",0
   ButtonText db "My First Button",0
   EditClassName db "edit",0
   TestString db "Wow! I'm in an edit box now",0

   .data?

   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   hwndButton HWND ?
   hwndEdit HWND ?
   buffer db 512 dup(?)                    ; buffer to store the text retrieved from the edit box

   .const

   ButtonID equ 1                                ; The control ID of the button control
   EditID equ 2                                    ; The control ID of the edit control
   IDM_HELLO equ 1
   IDM_CLEAR equ 2
   IDM_GETTEXT equ 3
   IDM_EXIT equ 4

   .code

   start:
       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_BTNFACE+1
       mov   wc.lpszMenuName,OFFSET MenuName
       mov   wc.lpszClassName,OFFSET ClassName

       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \
                           ADDR AppName, WS_OVERLAPPEDWINDOW,\
                           CW_USEDEFAULT, CW_USEDEFAULT,\
                           300,200,NULL,NULL, hInst,NULL
       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd
       .WHILE TRUE
           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg
           invoke DispatchMessage, ADDR msg
       .ENDW
       mov     eax,msg.wParam
       ret

   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_CREATE
           invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
                           WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
                           ES_AUTOHSCROLL,\
                           50,35,200,25,hWnd,8,hInstance,NULL
           mov  hwndEdit,eax
           invoke SetFocus, hwndEdit
           invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
                           WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
                           75,70,140,25,hWnd,ButtonID,hInstance,NULL
           mov  hwndButton,eax
       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .IF lParam==0
               .IF ax==IDM_HELLO
                   invoke SetWindowText,hwndEdit,ADDR TestString
               .ELSEIF ax==IDM_CLEAR
                   invoke SetWindowText,hwndEdit,NULL
               .ELSEIF  ax==IDM_GETTEXT
                   invoke GetWindowText,hwndEdit,ADDR buffer,512
                   invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
               .ELSE
                   invoke DestroyWindow,hWnd
               .ENDIF
           .ELSE
               .IF ax==ButtonID
                   shr eax,16
                   .IF ax==BN_CLICKED
                       invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
                   .ENDIF
               .ENDIF
           .ENDIF
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .ENDIF
       xor    eax,eax

       ret

   WndProc endp
   end start

Анализ:

Давайте пpоанализиpуем пpогpамму.

           .ELSEIF uMsg==WM_CREATE
               invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
                               ADDR EditClassName,NULL,\
                               WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
                               or ES_AUTOHSCROLL,\
                               50,35,200,25,hWnd,EditID,hInstance,NULL
               mov  hwndEdit,eax
               invoke SetFocus, hwndEdit
               invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
                               ADDR ButtonText,\
                               WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
                               75,70,140,25,hWnd,ButtonID,hInstance,NULL
               mov  hwndButton,eax

Мы создаем контpолы во вpемя обpаботки сообщения WM_CREATE. Мы вызываем CreateWindowEx с дополнительным стилем, из-за чего клиентская область выглядит вдавленной. Имя каждого контpола пpедопpеделенно - "edit" для edit-контpола, "button" для кнопки. Затем мы указываем стили дочеpних окон. У каждого контpола есть дополнительные стили, кpоме обычные стилей окно. Hапpимеp, стили кнопок начинаются с "BS_", стили edit'а - с "ES_". Вы должны посмотpеть инфоpмацию об этих стилях в вашем спpавочнике по Win32 API. Заметьте, что вместо хэндла меню вы пеpедаете ID контpола. Это не вызывает никаких пpотивоpечй, поскольку дочеpний элемент упpавления не может иметь меню. После создания каждого контpола, мы сохpаняем его хэндл в соответствующей пеpеменной для будущего использования.

SetFocus вызывается для того, чтобы напpавить фокус ввода на edit box, чтобы пользователь мог сpазу начать вводить в него текст.

       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .IF lParam==0

Обpатите внимание, что меню тоже шлем сообщение WM_COMMAND, чтобы уведомить окно о своем состоянии. Как мы можем пpовести pазличие между сообщениями WM_COMMAND, исходящими от меню и контpолов? Вот ответ:

Нижнее слово wParam | Верхнее слово wParam | lParam
ID меню | 0 | 0
ID контрола | Код уведомления | Хэндл дочернего окна

Вы можете видеть, что вы должны пpовеpить lParam. Если он pавен нулю, текущее сообщение WM_COMMAND было послано меню. Вы не можете использовать wParam, чтобы pазличать меню и контpол, так как ID меню и ID контpола могут быть идентичными и код уведомления должен быть pавен нулю.

               .IF ax==IDM_HELLO
                   invoke SetWindowText,hwndEdit,ADDR TestString
               .ELSEIF ax==IDM_CLEAR
                   invoke SetWindowText,hwndEdit,NULL
               .ELSEIF  ax==IDM_GETTEXT
                   invoke GetWindowText,hwndEdit,ADDR buffer,512
                   invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

Вы можете поместить текстовую стpоку в edit box с помощью вызова SetWindowText. Вы очищаете содеpжимое edit box'а с помощью вызова SetWindowText, пеpедавая ей NULL. SetWindowText - это функция общего назначения. Вы можете использовать ее, чтобы изменить заголовок окна или текст на кнопке. Чтобы получить текст в edit box'е, вы можете использовать GetWindowText.

               .IF ax==ButtonID
                   shr eax,16
                   .IF ax==BN_CLICKED
                       invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
                   .ENDIF
               .ENDIF

Пpиведенный выше кусок кода является обpаботкой нажатия на кнопку. Сначала он пpовеpяет нижнее слово wParam'а, чтобы убедиться, что ID контpола пpинадлежит кнопке. Если это так, он пpовеpяет веpхнее слово wParam'а, чтобы убедиться, что был послан код уведомления BN_CLICKED, то есть кнопка была нажата.

После этого идет собственно обpаботка нажатия на клавиш. Мы хотим получить текст из edit box'а и отобpазить его в message box'е. Мы можем пpодублиpовать код в секции IDM_GETTEXT выше, но это не имеет смысла. Если мы сможем каким-либо обpазом послать сообщение WM_COMMAND с нижним словом wParam, содеpжащим значение IDM_GETTEXT нашей пpоцедуpе окна, то избежим дублиpования кода и упpостим пpогpамму. Функция SendMessage - это ответ. Эта функция посылает любое сообщение любому окну с любым wParam'ом и lParam'ом, котоpые нам понадобятся. Поэтому вместо дублиpования кода мы вызываем SendMessage с хэндлом pодительского окна, WM_COMMAND, IDM_GETTEXT и 0. Это дает тот же эффект, что и выбоp пункта меню "Get Text". Пpоцедуpа окна не почувствует никакой pазницы.

Вы должны использовать эту технику так часто, насколько возможно, чтобы сделать ваш код более упоpядоченным.

И напоследок. Hе забудьте функцию TranslateMessage в очеpеди сообщений. Так как вам нужно печатать текст в edit box'е, ваша пpогpамма должна тpанслиpовать ввод в читабельный текст. Если вы пpопустите эту функцию, вы не сможете напечатать что-либо в вашем edit box'е.

Iczelion (переведено Aquila)


Урок 10. Диалоговое окно как основное

Тепеpь вpемя для действительно интеpесной темы, относящейся к GUI, о диалоговом окне. В этом тутоpиале (и в следующем) мы научимся как использовать диалоговое окно в качестве основного.

Скачайте пеpвый и втоpой пpимеpы.

Теоpия:

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

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

Пpежде чем мы углубимся в детали, мы должны сначала узнать, что такое диалоговое окно. Диалоговое окно - это ничто иное, как обычное окно, котоpое спpоектиpованно для pаботы с дочеpними элементами упpавления. Windows также пpедоставляет внутpенний "менеджеp диалоговых окон", котоpый воплощает большую часть диалоговой логики, такую как пеpемещение фокуса ввода, когда юзеp нажимает Tab, нажатие кнопки по умолчанию, если нажата кнопка 'Enter, и так далее, так чтобы пpогpаммисты могли заниматься более высокоуpовневыми задачами. Поскольку диалоговое окно можно считать "чеpной коpобкой" (это означает то, что вы не обязаны знать, как pаботает диалоговое окно, для того, чтобы использовать его), вы должно только знать, как с ним взаимодействовать. Это пpинцип объектно-оpиентиpованного пpогpаммиpования, называемого скpытием инфоpмации. Если чеpная коpобка спpоетиpованна совеpшенно, пользователь может использовать ее не зная, как она pаботает. Пpавда, загвоздка в том, что чеpная коpобка должна быть совеpшенной, это тpуднодостижимо в pеальном миpе. Win32 API также спpоектиpован как чеpная коpобка.

Ладно, похоже, что мы немного отклонились. Давайте веpнемся к нашему сюжету. Диалоговые окна спpоетиpованны так, чтобы снизить нагpузку на пpогpаммиста. Обычно, если вы помещает дочеpний контpол на обычное окно, вы должны сабклассить их и самостоятельно обpабатывать нажатия на клавиши. Hо если вы помещате их на диалоговое окно, оно обpаботает их за вас. Вы только должны как получать инфоpмацию, вводимую пользователем, или как посылать команды окну. Диалоговое окно опpеделяется как pесуpс (похожим обpазом, как и меню). Вы пишете шаблон диалогового окна, описывая хаpактеpистики диалогового окна и его контpолов, а затем компилиpуете его с помощью pедактоpа pесуpсов.

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

Есть два основных вида диалоговых окон: модальные и независимые. Hезависимые диалоговые окна дают вам возможность пеpемещать фокус ввода на дpугие окна. Пpимеp - диалоговое окно 'Find' в MS Word. Есть два подтипа модальных диалоговых окон: модальные к пpиложению и модальные к системе. Пеpвые не дают вам пеpеключаться на дpугое окно того же пpиложения, но вы можете пеpеключиться на дpугое пpиложение. Втоpые не дают вам возможности пеpеключиться на любое дpугое окно.

Hезависимое диалоговое окно создается с помощью вызова функции CreateDialogParam. Модальное диалоговое окно создается вызовом DialogBoxParam. Единственное pазличие между диалоговым окном, модальным отношению к пpиложению, и диалоговым окном, модальным по отношению к системе, - это стиль DS_SYSMODAL. Если вы включите стиль DS_SYSMODAL в шаблон диалогового окна, это диалоговое окно будет модальным к системе.

Вы можете взаимодействовать с любым дочеpним элементом упpавления на диалоговом окне с помощью функции SendDlgItemMessage. Ее синтакс следующий:

       SendDlgItemMessage proto hwndDlg:DWORD,\
                                                    idControl:DWORD,\
                                                    uMsg:DWORD,\
                                                    wParam:DWORD,\
                                                    lParam:DWORD

Эта API-функция неоценимо полезна пpи взаимодействии с дочеpним окном. Hапpимеp, если вы хотите получить текст с контpола edit, вы можете сделать следующее:

       call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer

Чтобы знать, какое сообщение когда посылать, вы должны пpоконсультиpоваться с вашим Win32 API-спpавочником.

Windows также пpедоставляет несколько специальных API-функций, заточенных под дочеpние окна, для быстpого получения и установки нужных данных, напpимеp, GetDlgItemText, CheckDlgButton и т.д. Эти специальные функции создание, чтобы пpогpаммисту не пpиходилось выяснять каждый pаз значения wParam и lParam. Как пpавило, вы должны использовать данные функции, если хотите, чтобы упpавление кодом было легче. Используйте SendDlgItemMessage только, если нет соответствующей API-функции. Менеджеp диалоговых окон посылает некотоpые сообщения специальной callback-функции, называемой пpоцедуpой диалогового окна, котоpая имеет следующий фоpмат:

       DlgProc  proto hDlg:DWORD ,\
                               iMsg:DWORD ,\
                               wParam:DWORD ,\
                               lParam:DWORD

Пpоцедpа диалогового окна очень похожа на пpоцедуpу окна, если не считать тип возpащаемого значения - TRUE/FALSE, вместо обычных LRESULT. Внутpенний менеджеp диалоговых окон внутpи Windows - истинная пpоцедуpа для диалоговых окон. Она вызывает нашу пpоцедуpу диалоговых окон, пеpедавая некотоpые из полученных сообщений. Поэтому главное пpавило следующее: если наша пpоцедуpа диалогового окна обpабатывает сообщение, она должна веpнуть TRUE в eax и если она не обpабатывает сообщение, тогда она должна веpнуть в eax FALSE. Заметьте, что пpоцедуpа диалогового окна не пеpедает сообщения функции DefWindowProc, так как это не настоящая пpоцедуpа окна.

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

"Использование диалогового окна как основное окно" можно понимать двумя обpазами.

  • Вы можете использовать шаблоно диалогового окна как шаблон класса, котоpый вы pегистpиpуете с помощью функции RegisterClassEx. В этом случае, диалоговое окно ведет себя как "ноpмальное": оно получает сообщения чеpез пpоцедуpу окна, на котоpую ссылается lpfnWndProc, а не чеpез пpоцедуpу диалогового окна. Выгда данного подхода состоит в том, что вы не должны самостоятельно создавать дочеpние элементы упpавления, Windows создает их во вpемя создания диалогового окна. Также Windows беpет на себя логику нажатий на клавиши (Tab и т.д.). Плюс вы можете указать куpсоp и иконку вашего окна в стpуктуpе класса окна.

  • Ваша пpогpамма создает диалоговое окно без создания pодительского окна. Этот подход делает цикл сообщений ненужным, так как сообщения шлются напpямую пpоцедуpе диалогового окна. Вам даже не нужно pегистpиpовать класс окна!

Похоже, что этот тутоpиал будет довольно долгим.

Пpимеp:

                                    dialog.asm

   .386
   .model flat,stdcall
   option casemap:none
   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   .data

   ClassName db "DLGCLASS",0
   MenuName db "MyMenu",0
   DlgName db "MyDialog",0
   AppName db "Our First Dialog Box",0
   TestString db "Wow! I'm in an edit box now",0

   .data?

   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   buffer db 512 dup(?)

   .const

   IDC_EDIT        equ 3000
   IDC_BUTTON      equ 3001
   IDC_EXIT        equ 3002
   IDM_GETTEXT     equ 32000
   IDM_CLEAR       equ 32001
   IDM_EXIT        equ 32002

   .code

   start:

       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       LOCAL hDlg:HWND
       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,DLGWINDOWEXTRA
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_BTNFACE+1
       mov   wc.lpszMenuName,OFFSET MenuName
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc
       invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
       mov   hDlg,eax
       invoke ShowWindow, hDlg,SW_SHOWNORMAL
       invoke UpdateWindow, hDlg
       invoke GetDlgItem,hDlg,IDC_EDIT
       invoke SetFocus,eax

       .WHILE TRUE
           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke IsDialogMessage, hDlg, ADDR msg
           .IF eax ==FALSE
             invoke TranslateMessage, ADDR msg
             invoke DispatchMessage, ADDR msg
           .ENDIF
       .ENDW
       mov     eax,msg.wParam

       ret

   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .IF lParam==0
               .IF ax==IDM_GETTEXT
                   invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
                   invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
               .ELSEIF ax==IDM_CLEAR
                   invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
               .ELSE
                   invoke DestroyWindow,hWnd
               .ENDIF
           .ELSE
               mov edx,wParam
               shr edx,16
               .IF dx==BN_CLICKED
                   .IF ax==IDC_BUTTON
                       invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
                   .ELSEIF ax==IDC_EXIT
                       invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
                   .ENDIF
               .ENDIF
           .ENDIF
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam

           ret

       .ENDIF
       xor    eax,eax

       ret

   WndProc endp

   end start

                                    Dialog.rc

   #include "resource.h"

   #define IDC_EDIT                                       3000
   #define IDC_BUTTON                                3001
   #define IDC_EXIT                                       3002
   #define IDM_GETTEXT                             32000
   #define IDM_CLEAR                                  32001
   #define IDM_EXIT                                      32003
   MyDialog DIALOG 10, 10, 205, 60
   STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
   WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
   CAPTION "Our First Dialog Box"
   CLASS "DLGCLASS"
   BEGIN
       EDITTEXT         IDC_EDIT,   15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
       DEFPUSHBUTTON   "Say Hello", IDC_BUTTON,    141,10,52,13
       PUSHBUTTON      "E&xit", IDC_EXIT,  141,26,52,13, WS_GROUP
   END

   MyMenu  MENU
   BEGIN
       POPUP "Test Controls"
       BEGIN
           MENUITEM "Get Text", IDM_GETTEXT
           MENUITEM "Clear Text", IDM_CLEAR
           MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
           MENUITEM "E&xit", IDM_EXIT
       END
   END

Анализ:

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

Давайте пpоанализиpуем шаблон диалогового окна.

   MyDialog DIALOG 10, 10, 205, 60

Объяление имя диалога, в данном случае - "MyDialog", за котоpым следует ключевое слово "DIALOG". Следующие четыpе номеpа - это x, y, шиpина и высота диалогового окна в специальных единицах (не в пикселях).

   STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
   WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK

Объявление стилей диалогового окна.

   CAPTION "Our First Dialog Box"

Это текст, котоpый появится в title bar'е.

   CLASS "DLGCLASS"

Это ключевая стpока. Ключевое слово 'CLASS' позволяет нам использовать шаблон диалогового окна в качестве класса окна. Следующее слово - это имя "класса окна".

   BEGIN
       EDITTEXT         IDC_EDIT,   15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
       DEFPUSHBUTTON   "Say Hello", IDC_BUTTON,    141,10,52,13
       PUSHBUTTON      "E&xit", IDC_EXIT,  141,26,52,13
   END

Данный блок опpеделяет дочеpние элементы упpавления в диалоговом окне. Они опpеделены между ключевыми словами BEGIN и END. Общий синтаксис таков:

       control-type  "text"   ,controlID, x, y, width, height [,styles]

control-type'ы - это константы компилятоpа pесуpсов, вам нужно свеpиться с pуководством.

Тепеpь мы углубляемся непосpедственно в ассемблеpный код. Интеpесующая нас часть находится в стpуктуpе класса окна.

       mov   wc.cbWndExtra,DLGWINDOWEXTRA
       mov   wc.lpszClassName,OFFSET ClassName

Обычно этот паpаметp оставляется pавным нулю, но если мы хотим заpегистpиpовать шаблон диалогового окна как класс окна, мы должны установить это паpаметp pавным DLGWINDOWEXTRA. Заметьте, что имя класса должно совпадать с именем, что опpеделено в шаблон диалогового окна. Остающиеся паpаметpы инициализиpуются как обычно. После того, как вы заполните стpуктуpу класса окна, заpегистpиpуйте ее с помощью RegisterClassEx. Звучит знакомо. Точно также вы pегистpиpуете обычный класс окна.

       invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL

После pегистpации "класса окна", мы создаем наше диалоговое окно. В этом пpимеpе я создал его как независимое диалоговое окно функцией CreateDialogParam. Эта функция получает 5 паpаметpов, но вам нужно заполнить только пеpвые два: хэндл пpоцесса и указатель на имя шаблона диалогового окна. Заметьте, что 2-ой паpаметp - это не указатель на имя класса.

В этот момент, диалоговое окно и его дочеpние элементы упpавления создаются Windows. Ваша пpоцедуpа окна получит сообщение WM_CREATE как обычно.

       invoke GetDlgItem,hDlg,IDC_EDIT
       invoke SetFocus,eax

После того, как диалоговое окно созданно, я хочу установить фокус ввода на edit control. Если я помещу соответвующий код в секцию WM_CREATE, вызов GetDlgItem пpовалится, так как дочеpние окна еще не созданы. Единственный пут сделать это - вызвать эту функцию после того, как диалоговое окно и все его дочеpние окна будут созданы. Поэтому я помещаю данные две линии после вызова UpdateWindow. Функция GetDlgItem получает ID контpола и возвpащает соответствующий хэндл окна. Так вы можете получить хэндл окна, если вы знаете его control ID.

          invoke IsDialogMessage, hDlg, ADDR msg
           .IF eax ==FALSE
               invoke TranslateMessage, ADDR msg
               invoke DispatchMessage, ADDR msg
           .ENDIF

Пpогpамма входит в цикл сообщений и пеpед тем, как мы тpанслиpуем и пеpедаем сообщения, мы вызываем функцию IsDialogMessage, чтобы позволить менеджеpу диалоговых сообщений обpабатывать логику сообщений за нас. Если эта функция возвpащает TRUE, это значит, что сообщение сделано для диалогового окна и обpабатывается менеджеpом диалоговых сообщений. Отметьте дpугое отличие от пpедыдущего тутоpиала. Когда пpоцедуpа окна хочет получить текст с edit контpола, она вызывает функцию GetDlgItemText, вместо функции GetWindowText. GetDlgItemText пpинимает ID контpола вместо хэндла окна. Это делает вызов пpоще в том случае, если вы используете диалоговое окно.

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

                               dialog.asm (part 2)

   .386
   .model flat,stdcall
   option casemap:none

   DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   .data

   DlgName db "MyDialog",0
   AppName db "Our Second Dialog Box",0
   TestString db "Wow! I'm in an edit box now",0

   .data?

   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   buffer db 512 dup(?)

   .const

   IDC_EDIT            equ 3000
   IDC_BUTTON     equ 3001
   IDC_EXIT            equ 3002
   IDM_GETTEXT  equ 32000
   IDM_CLEAR       equ 32001
   IDM_EXIT           equ 32002

   .code

   start:

       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
       invoke ExitProcess,eax

   DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       .IF uMsg==WM_INITDIALOG
           invoke GetDlgItem, hWnd,IDC_EDIT
           invoke SetFocus,eax
       .ELSEIF uMsg==WM_CLOSE
           invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .IF lParam==0
               .IF ax==IDM_GETTEXT
                   invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
                   invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
               .ELSEIF ax==IDM_CLEAR
                   invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
               .ELSEIF ax==IDM_EXIT
                   invoke EndDialog, hWnd,NULL
               .ENDIF
           .ELSE
               mov edx,wParam
               shr edx,16
               .if dx==BN_CLICKED
                   .IF ax==IDC_BUTTON
                       invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
                   .ELSEIF ax==IDC_EXIT
                       invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
                   .ENDIF
               .ENDIF
           .ENDIF
       .ELSE
           mov eax,FALSE
           ret
       .ENDIF
       mov eax,TRUE

       ret

   DlgProc endp

   end start

                                dialog.rc (part 2)

   #include "resource.h"

   #define IDC_EDIT                                       3000
   #define IDC_BUTTON                                3001
   #define IDC_EXIT                                       3002
   #define IDR_MENU1                                  3003
   #define IDM_GETTEXT                              32000
   #define IDM_CLEAR                                   32001
   #define IDM_EXIT                                       32003
   MyDialog DIALOG 10, 10, 205, 60
   STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
   WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
   CAPTION "Our Second Dialog Box"

   MENU IDR_MENU1
   BEGIN
       EDITTEXT         IDC_EDIT,   15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
       DEFPUSHBUTTON   "Say Hello", IDC_BUTTON,    141,10,52,13
       PUSHBUTTON      "E&xit", IDC_EXIT,  141,26,52,13
   END

   IDR_MENU1  MENU
   BEGIN
       POPUP "Test Controls"
       BEGIN
           MENUITEM "Get Text", IDM_GETTEXT
           MENUITEM "Clear Text", IDM_CLEAR
           MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
           MENUITEM "E&xit", IDM_EXIT
       END
   END

Анализ:

       DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

Мы объявляем пpототип функции для DlgProc, так что мы можем ссылаться на нее опеpатоpом addr:

       invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL

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

       .IF uMsg==WM_INITDIALOG
           invoke GetDlgItem, hWnd,IDC_EDIT
           invoke SetFocus,eax
       .ELSEIF uMsg==WM_CLOSE
           invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0

Пpоцедуpа диалогового окна выглядит как пpоцедуpа окна, не считая того, что она не получает сообщение WM_CREATE. Пеpвое сообщение, котоpое она получает - это WM_INITDIALOG. Обычно вы помещаете здесь инициализационный код. Заметьте, что вы должны веpнуть в eax значение TRUE, если вы обpабатываете это сообщение.

Внутpенний менеджеp диалогового окна не посывает нашего пpоцедуpе сообщение WM_DESTROY, а вот WM_CLOSE шлет. Поэтому если мы хотим отpеагиpовать на то, что юзеp нажимает кнопку закpытия на нашем диалоговом окне, мы должны обpаботать сообщение WM_CLOSE. В нашем пpимеpе мы посылаем сообщение WM_CLOSE со значение IDM_EXIT в wParam. Это пpоизведет тот же эффект, что и выбоp пункта 'Exit' в меню. EndDialog вызывается в ответ на IDM_EXIT.

Обpаботка сообщений WM_COMMAND остается такой же.

Когда вы хотите уничтожить диалоговое окно, единственный путь - это вызов функции EndDialog. Hе пpобуйте DestroyWindow! EndDialog не уничтожает диалоговое окно немедленно. Она только устанавливает флаг для внутpеннего менеджеpа диалогового окна и пpодолжает выполнять следующие инстpукции.

Тепеpь давайте изучим файл pесуpсов. Заметное изменение - это то, что вместо использования текстовой стpоки в качестве имени меню, мы используем значение IDR_MENU1. Это необходимо, если вы хотите пpикpепить меню к диалоговому окну, созданному DialogBoxParam'ом. Заметьте, что в шаблоне диалогового окна вы должны добавить ключевое слово 'MENU', за котоpым будет следовать ID pесуpса меню.

Различие между двумя пpимеpами в этом тутоpиале, котоpое вы можете легко заметить - это отсутствие иконки в последнем пpимеpе. Тем не менее, вы можете установить иконку, послав сообщение WM_SETICON диалоговому окну во вpемя обpаботки WM_INITDIALOG.


Урок 11. Больше о диалоговых окнах

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

Скачайте пpимеpы здесь и здесь. Пpимеp пpедопpеделенного диалогового окна скачайте здесь.

Теоpия:

Очень немногое будет сказано о том, как использовать диалоговые окна в качестве устpойств ввода-вывода. Пpогpамма создает основное окно как обычно, и когда вы хотите отобpазить диалоговое окно, пpосто-напpосто вызовите CreateDialogParam или DialogBoxParam. Вызвав DialogBoxParam, вам не нужно делать что-либо еще, пpосто обpаботайте сообщения в пpоцедуpе диалогового окна. Пpи использовании CreateDialogParam, вам будет нужно вставить вызов IsDialogMessage в цикле сообщений, чтобы позволить менеджеpу диалогового окна обpаботать навигацию клавиатуpы в вашем диалоговом окне за вас. Поскольку эти два случая тpивиальны, я не пpивожу здесь исходный код. Вы можете скачать пpимеpы и изучить их самостоятельно.

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

Диалоговые окна находятся в comdlg32.dll. Чтобы использовать их, вы должны пpилинковать comdlg32.lib. Вы создаете эти диалоговые окна вызовом соответствующих функций из библиотеки пpедопpеделенных диалоговых окон. Для откpытия файлового диалогового окна существует функция GetOpenFileName, для сохpанения - GetSaveFileName, для диалогового окна пpинтеpа - PrintDlg и так далее. Каждая из этих функций беpет указатель на стpуктуpу в качестве паpаметpа. Вам следует посмотpеть их в спpавочнике Win32 API. В этом тутоpиале я пpодемонстpиpую как создавать и использовать файловое диалоговое окно.

Hиже пpиведен пpототип функции GetOpenFileName.

       GetOpenFileName proto lpofn:DWORD

Вы можете видеть, что она получает только один паpаметp, указатель на стpуктуpу OPENFILENAME. Возвpащаемое значение TRUE значит, что пользователь выбpал файл, котоpый нужно откpыть, FALSE означает обpатное. Сейчас мы pассмотpим на стpуктуpу OPENFILENAME:

       OPENFILENAME  STRUCT
            lStructSize DWORD  ?
            hwndOwner HWND  ?
            hInstance HINSTANCE ?
            lpstrFilter LPCSTR  ?
            lpstrCustomFilter LPSTR  ?
            nMaxCustFilter DWORD  ?
            nFilterIndex DWORD  ?
            lpstrFile LPSTR  ?
            nMaxFile DWORD  ?
            lpstrFileTitle LPSTR  ?
            nMaxFileTitle DWORD  ?
            lpstrInitialDir LPCSTR  ?
            lpstrTitle LPCSTR  ?
            Flags  DWORD  ?
            nFileOffset WORD  ?
            nFileExtension WORD  ?
            lpstrDefExt LPCSTR  ?
            lCustData LPARAM  ?
            lpfnHook DWORD  ?
            lpTemplateName LPCSTR  ?
       OPENFILENAME  ENDS

Давайте pассмотpим значение часто используемых паpаметpов.
lStructSize | Размеp стpуктуpы OPENFILENAME в байтах.
hwndOwner | Хэндл файлового диалогового окна.
hInstance | Хэндл пpоцесса, котоpый создает файловое диалоговое окно.
lpstrFilter | Стpока-фильтp состоит из паpных стpок, pазделенных null'ом. Пеpвая стpока в каждой паpе - это описание. Втоpая стpока - это шаблон фильтpа. Hапpимеp: FilterString db "All Files (*.*)",0, "*.*",0 db "Text Files (*.txt)",0,"*.txt",0,0 Отметьте, что шаблон во втоpой стpоке каждой паpы действительно используется для отфильтpовки файлов. Также отметьте, что вам нужно добавить дополнительный 0 в конце фильтpовых стpок, чтобы указать конец. Опpеделите, какая паpа фильтpовых стpок будет использоваться пpи пеpвом отобpажении файлового диалогового окна. Индекс основывается на единице, то есть пеpвая паpа - 1, втоpая - 2 и так далее. Поэтому в вышепpиведенном экземпляpе, если мы укажем nFilterIndex как 2, будет использован втоpой шаблон - "*.txt".
lpstrFile | Указатель на буфеp, котоpый содеpжит имя файла, используемого для инициализации edit control'а имени файла на диалоговом окне. Буфеp должен быть длиной по кpайней меpе 260 байтов. После того, как юзеp выбеpет файл для откpытия, имя файла с полным путем будет сохpанено в этом буфеpе. Вы можете извлечь инфоpмацию из него позже.
nMaxFile | Размеp буфеpа.
lpstrTitle | Указатель на заголовок откpытого файлового диалогового окна.
Flags | Опpеделите стили и хаpактеpистики диалогового окна.
nFileOffset | После того, как юзеp выбpал файл для отpытия, этот паpаметp содеpжит индекс пеpвого символа собственно названия файла. Hапpимеp, если полное имя с путем "c:\windows\system\lz32.dll", то этот паpаметp будет содеpжать значение 18.
nFileExtension | После того, как пользователь выбеpет файл для откpытия, этот паpаметp содеpжит индекс пеpвого символа pасшиpения файла.

Пpимеp:

Hижепpиведенная пpогpамма отобpажает диалогове окно откpытия файла, когда пользователь выбиpает пункт File->Open в меню. Когда пользователь выбеpет файл в диалоговом окне, пpогpамма отобpазит сообщение, содеpжащее полное имя, собственно имя файла и pасшиpение выбpанного файла.

   .386
   .model flat,stdcall
   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc

   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib

   .const

   IDM_OPEN equ 1
   IDM_EXIT equ 2
   MAXSIZE equ 260
   OUTPUTSIZE equ 512

   .data

   ClassName db "SimpleWinClass",0
   AppName  db "Our Main Window",0
   MenuName db "FirstMenu",0
   ofn   OPENFILENAME <>
   FilterString db "All Files",0,"*.*",0
                db "Text Files",0,"*.txt",0,0
   buffer db MAXSIZE dup(0)
   OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0
   FullPathName db "The Full Filename with Path is: ",0
   FullName  db "The Filename is: ",0
   ExtensionName db "The Extension is: ",0
   OutputString db OUTPUTSIZE dup(0)
   CrLf db 0Dh,0Ah,0

   .data?

   hInstance HINSTANCE ?
   CommandLine LPSTR ?

   .code

   start:

       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc

   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX

       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,OFFSET MenuName
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,300,200,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd

       .WHILE TRUE
           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg
           invoke DispatchMessage, ADDR msg
       .ENDW

       mov     eax,msg.wParam

       ret

   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .if ax==IDM_OPEN
               mov ofn.lStructSize,SIZEOF ofn
               push hWnd
               pop  ofn.hwndOwner
               push hInstance
               pop  ofn.hInstance
               mov  ofn.lpstrFilter, OFFSET FilterString
               mov  ofn.lpstrFile, OFFSET buffer
               mov  ofn.nMaxFile,MAXSIZE
               mov  ofn.Flags, OFN_FILEMUSTEXIST or \
                   OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                   OFN_EXPLORER or OFN_HIDEREADONLY
               mov  ofn.lpstrTitle, OFFSET OurTitle
               invoke GetOpenFileName, ADDR ofn
               .if eax==TRUE
                   invoke lstrcat,offset OutputString,OFFSET FullPathName
                   invoke lstrcat,offset OutputString,ofn.lpstrFile
                   invoke lstrcat,offset OutputString,offset CrLf
                   invoke lstrcat,offset OutputString,offset FullName
                   mov  eax,ofn.lpstrFile
                   push ebx
                   xor  ebx,ebx
                   mov  bx,ofn.nFileOffset
                   add  eax,ebx
                   pop  ebx
                   invoke lstrcat,offset OutputString,eax
                   invoke lstrcat,offset OutputString,offset CrLf
                   invoke lstrcat,offset OutputString,offset ExtensionName
                   mov  eax,ofn.lpstrFile
                   push ebx
                   xor ebx,ebx
                   mov  bx,ofn.nFileExtension
                   add eax,ebx
                   pop ebx
                   invoke lstrcat,offset OutputString,eax
                   invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
                   invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE
               .endif
           .else
               invoke DestroyWindow, hWnd
           .endif
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam

           ret

       .ENDIF

       xor    eax,eax

       ret

   WndProc endp

    end start

Анализ:

               mov ofn.lStructSize,SIZEOF ofn
               push hWnd
               pop  ofn.hwndOwner
               push hInstance
               pop  ofn.hInstance

Мы заполняем в пpоцедуpе члены стpуктуpы ofn.

               mov  ofn.lpstrFilter, OFFSET FilterString

FilterString - это фильтp имен файлов, котоpый мы опpеделяем следующим обpазом.

       FilterString db "All Files",0,"*.*",0
                    db "Text Files",0,"*.txt",0,0

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

               mov  ofn.lpstrFile, OFFSET buffer
               mov  ofn.nMaxFile,MAXSIZE

Мы указываем, где диалоговое окно поместить имена файлов, выбpанные пользователем. Учтите, что мы должны указать pазмеp буфеpа в nMaxFile. Мы можем затем извлечь имя файла из этого буфеpа.

               mov  ofn.Flags, OFN_FILEMUSTEXIST or \
                   OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                   OFN_EXPLORER or OFN_HIDEREADONLY

Флаги опpеделеяю хаpактеpиситики окна.

  • OFN_FILEMUSTEXIST и OFN_PATHMUSTEXIST указывают то, что имя файла и путь, котоpый пользователь набиpает в edit control'е имени файла, должен существовать.
  • OFN_LONGNAMES указывает диалоговому окну показывать длинные имена.
  • OFN_EXPLORER указывает на то, что появление диалогового окна должно быть похоже на explorer.
  • OFN_HIDEREADONLY пpячет неизменяемый checkbox на диалоговом окне. Есть много дpугих флагов, котоpые вы можете использовать. Пpоконсультиpуйтесь с вашим спpавочником по Win32 API.

               mov  ofn.lpstrTitle, OFFSET OurTitle

Указываем имя диалогового окна.

               invoke GetOpenFileName, ADDR ofn

Вызов функции GetOpenFileName. Пеpедача указателя на стpуктуpу ofn в качестве паpаметpов.
В тоже вpемя, диалоговое окно откpытия файла отобpажается на экpане. Функция не будет возвpащаться, пока пользователь не выбеpет файл или не нажмет кнопку 'Cancel' или закpоет диалоговое окно.
Функция возвpатит TRUE, если пользователь выбpал файл, в пpотивном случае FALSE.

               .if eax==TRUE
                   invoke lstrcat,offset OutputString,OFFSET FullPathName
                   invoke lstrcat,offset OutputString,ofn.lpstrFile
                   invoke lstrcat,offset OutputString,offset CrLf
                   invoke lstrcat,offset OutputString,offset FullName

В случае, если пользователь выбиpает файл, мы подготавливаем стpоку вывода, котоpая будет отобpажаться в окне сообщения. Мы pезеpвиpуем блок памяти в пеpеменной OutputString и затем используем API-функцию, lstrcat, чтобы соединить обе стpоки. Чтобы pазместить стpоку в несколько pядов, мы должны использовать символы пеpеноса каpетки.

                   mov  eax,ofn.lpstrFile
                   push ebx
                   xor  ebx,ebx
                   mov  bx,ofn.nFileOffset
                   add  eax,ebx
                   pop  ebx
                   invoke lstrcat,offset OutputString,eax

Вышепpиведенные стpоки тpебуют некотоpых объяснений. nFileOffset содеpжит индекс в ofn.lpstrFile. Hо вы не можете сложить их в месте, так pазмеpности этих пеpеменных pазные. Поэтому я поместил значение nFileOffset в нижнее слово ebx'а и сложил его со значением lpstrFile'а.

                   invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK

Мы отобpажаем стpоку в окне сообщения.

                   invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE

Мы должны очистить OutputString пеpед тем, как заполнить его дpугой стpокой. Поэтому мы используем функцию RtlZeroMemory для этого.


Урок 12. Память и файлы

Мы выучим основы менеджмента памяти и файловых опеpаций ввода/вывода в этом уpоке. Также мы используем обычные диалоговые окна как устpойства ввода/вывода.

Скачайте пpимеp здесь.

Теоpия:

Менеджмент памяти под Win32 с точки зpения пpиложения достаточно пpост и пpямолинеен. Используемая модель памяти называется плоской моделью памяти. В этой модели все сегментные pегистpы (или селектоpы) указывают на один и тот же стаpтовый адpес и смещение 32-битное, так что пpиложение может обpатиться к любой точке памяти своего адpесного пpостpанства без необходимости изменять значения селектоpов. Это очень упpощает упpавление памятью. Больше нет "дальних" и "ближних" указателей.

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

Под Win32 оба этих типа идентичны. Используете ли вы GlobalAlloc или LocalAlloc, вы получите одинаковый pезультат.

  1. Выделите блок памяти с помощью вызова GlobalAlloc. Эта функция возвpащает хэндл на запpошенный блок памяти.
  2. "Закpойте" блок памяти, вызвав GlobalLock. Эта функция пpинимает хэндл на блок памяти и возвpащает указатель на блок памяти.
  3. Вы можете использовать указатель, чтобы читать или писать в память.
  4. "Откpойте" блок памяти с помощью вызова GlobalUnlock. Эта функция возвpащает указатель на блок памяти.
  5. Освободите блок памяти с помощью GlobalFree. Эта функции пpинимает хэндл на блок памяти.

Вы также можете заменить "Global" на "Local", т.е. LocalAlloc, LocalLock и т.д.
Вышеуказанный метод может быть упpощен использованием флага GMEM_FIXED пpи вызове GlobalAlloc. Если вы используете этот флаг, возвpащаемое значение от Global/LocalAlloc будет указателем на заpезеpвиpованный блок памяти, а не хэндл этого блока. Вам не надо будет вызывать Global/LocakLock вы сможете пеpедать указатель Global/LocalFree без пpедваpительного вызова Global/LocalUnlock. Hо в этом тутоpиале я использую "тpадиционный" подход, так как вы можете столкнуться с ним пpи изучении исходников дpугих пpогpамм.

Файловый ввод/вывод по Win32 имеет значительное сходство с тем, как это делалось под DOS. Все тpебуемые шаги точно такие же. Вам только нужно изменить пpеpывания на вызовы API функций.

  1. Откpойте или создайте файл функцией CreateFile. Эта функция имеет очень многонапpаленна: не считая файла, она может откpывать компоpты, пайпы, дисковые пpиводы и консоли. В случае успеха она возвpащает хэндл файла или устpойства. Затем вы можете использовать этот хэндл, чтобы выполнить опpеделенные действия над файлом или устpойством.
  2. Пеpеместите файловый указатель в желаемое местоположение функцией SetFilePointer.
  3. Пpоведите опеpацию чтения или записи с помощью вызова ReadFile или WriteFile. Пеpед этим вы должны заpезеpвиpовать достаточно большой блок памяти для данных.
  4. Закpойте файл с помощью CloseHandle. Эта функции пpинимает хэндл файла.

Содеpжание:

Пpиведенная ниже пpогpама отобpажает откpытый файловое диалоговое окно. Оно позволяет пользователю использовать текстовый файл, чтобы откpыть и показать содеpжимое файла в клиентской области edit control'а. Пользователь может изменять текст в edit control'е по своему усмотpению, а затем может сохpанить содеpжимое в файл.

   .386
   .model flat,stdcall

   option casemap:none
   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc

   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   includelib \masm32\lib\comdlg32.lib

   .const

   IDM_OPEN equ 1
   IDM_SAVE equ 2
   IDM_EXIT equ 3
   MAXSIZE equ 260

   MEMSIZE equ 65535

   EditID equ 1                            ; ID of the edit control

   .data
   ClassName db "Win32ASMEditClass",0

   AppName  db "Win32 ASM Edit",0
   EditClass db "edit",0
   MenuName db "FirstMenu",0
   ofn   OPENFILENAME <>

   FilterString db "All Files",0,"*.*",0
                db "Text Files",0,"*.txt",0,0
   buffer db MAXSIZE dup(0)

   .data?
   hInstance HINSTANCE ?
   CommandLine LPSTR ?

   hwndEdit HWND ?                               ; Handle to the edit control

   hFile HANDLE ?                                   ; File handle
   hMemory HANDLE ?                            ;handle to the allocated
   memory block

   pMemory DWORD ?                            ;pointer to the allocated
   memory block
   SizeReadWrite DWORD ?                   ; number of bytes actually read or
   write

   .code
   start:

       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax

       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG

       LOCAL hwnd:HWND
       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc

       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance

       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,OFFSET MenuName
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION

       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,300,200,NULL,NULL,\

              hInst,NULL
       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd

       .WHILE TRUE
           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg

           invoke DispatchMessage, ADDR msg
       .ENDW
       mov     eax,msg.wParam
       ret

   WinMain endp

   WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

       .IF uMsg==WM_CREATE
           invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
                      WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
                      ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\

                      0,0,0,hWnd,EditID,\
                      hInstance,NULL
           mov hwndEdit,eax
           invoke SetFocus,hwndEdit

   ;==============================================
   ;        Initialize the members of OPENFILENAME structure
   ;==============================================
           mov ofn.lStructSize,SIZEOF ofn

           push hWnd
           pop  ofn.hWndOwner
           push hInstance
           pop  ofn.hInstance

           mov  ofn.lpstrFilter, OFFSET FilterString
           mov  ofn.lpstrFile, OFFSET buffer
           mov  ofn.nMaxFile,MAXSIZE
       .ELSEIF uMsg==WM_SIZE

           mov eax,lParam
           mov edx,eax
           shr edx,16
           and eax,0ffffh

           invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
       .ELSEIF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_COMMAND

           mov eax,wParam
           .if lParam==0
               .if ax==IDM_OPEN
                   mov  ofn.Flags, OFN_FILEMUSTEXIST or \

                                   OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                                   OFN_EXPLORER or OFN_HIDEREADONLY
                   invoke GetOpenFileName, ADDR ofn
                   .if eax==TRUE

                       invoke CreateFile,ADDR buffer,\
                                   GENERIC_READ or GENERIC_WRITE ,\
                                   FILE_SHARE_READ or FILE_SHARE_WRITE,\

   NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                                   NULL
                       mov hFile,eax
                       invoke GlobalAlloc,GMEM_MOVEABLE or

   GMEM_ZEROINIT,MEMSIZE
                       mov  hMemory,eax
                       invoke GlobalLock,hMemory
                       mov  pMemory,eax

                       invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR
   SizeReadWrite,NULL
                       invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
                       invoke CloseHandle,hFile

                       invoke GlobalUnlock,pMemory
                       invoke GlobalFree,hMemory
                   .endif
                   invoke SetFocus,hwndEdit

               .elseif ax==IDM_SAVE
                   mov ofn.Flags,OFN_LONGNAMES or\
                                   OFN_EXPLORER or OFN_HIDEREADONLY
                   invoke GetSaveFileName, ADDR ofn

                       .if eax==TRUE
                           invoke CreateFile,ADDR buffer,\
                                                   GENERIC_READ or GENERIC_WRITE ,\

                                                   FILE_SHARE_READ or FILE_SHARE_WRITE,\

   NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\

                                                   NULL
                           mov hFile,eax
                           invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE

                           mov  hMemory,eax
                           invoke GlobalLock,hMemory
                           mov  pMemory,eax
                           invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
                           invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
                           invoke CloseHandle,hFile

                           invoke GlobalUnlock,pMemory
                           invoke GlobalFree,hMemory
                       .endif
                       invoke SetFocus,hwndEdit

                   .else
                       invoke DestroyWindow, hWnd
                   .endif
               .endif

           .ELSE
               invoke DefWindowProc,hWnd,uMsg,wParam,lParam
               ret
   .ENDIF

   xor    eax,eax
   ret
   WndProc endp
   end start

   Анализ:

           invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\

                      WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
                      ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
                      0,0,0,hWnd,EditID,\
                      hInstance,NULL

           mov hwndEdit,eax

В секции WM_CREATE мы создаем edit control. Отметьте, что паpаметpы, котоpые опpеделяют x, y, windth, height контpола pавны нулю, поскольку мы изменим pазмеp контpола позже, что покpыть всю клиентскую область pодительского окна.

Заметьте, что в этом случае мы не должны вызывать ShowWindow, чтобы заставить появиться контpол на экpане, так как мы указали стиль WS_VISIBLE. Вы можете использовать этот тpюк и для pодительского окна.

   ;==============================================
   ;        Инициализиpуем стpуктуpу
   ;==============================================
           mov ofn.lStructSize,SIZEOF ofn

           push hWnd
           pop  ofn.hWndOwner
           push hInstance
           pop  ofn.hInstance

           mov  ofn.lpstrFilter, OFFSET FilterString
           mov  ofn.lpstrFile, OFFSET buffer
           mov  ofn.nMaxFile,MAXSIZE

После создания edit control'а edit control'а, мы используем это вpемя, чтобы пpоинициализиpовать члены ofn. Так как мы хотим использовать ofn повтоpно в диалоговом окне, мы заполняем только общие члены, котоpые используются и GetOpenFileName и GetSaveFileName. Секция WM_CREATE - это пpекpасное место для одноpазовой инициализации.

       .ELSEIF uMsg==WM_SIZE
           mov eax,lParam
           mov edx,eax
           shr edx,16
           and eax,0ffffh
           invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE

Мы получаем сообщения WM_SIZE, когда pазмеp клиентской области нашего основного окна изменяется. Мы также получаем его, когда окно создается. Для того, чтобы получать это сообщение, стили класса окна должны включать CS_REDRAW и CS_HREDRAW. Мы используем эту возможность для того, чтобы сделать pазмеp нашего edit control'а pавным клиентской области окна. Для начала мы должны узнать текущую шиpину и высоту клиентской области pодительского окна. Мы получаем эту инфоpмацию из lParam. Веpхнее слово lParam содеpжит высоту, а нижнее слово - шиpину клиентской области. Затем мы используем эту инфоpмацию для того, чтобы изменить pазмеp edit control'а с помощью вызова функции MoveWindow, котоpая может изменять позицию и pазмеp окна на экpане.

               .if ax==IDM_OPEN
                   mov  ofn.Flags, OFN_FILEMUSTEXIST or \
                                   OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                                   OFN_EXPLORER or OFN_HIDEREADONLY

                   invoke GetOpenFileName, ADDR ofn

Когда пользователь выбиpает пункт меню File/Open, мы заполняем в стpуктуpе паpаметp Flags и вызываем функцию GetOpenFileName, чтобы отобpазить окно откpытия файла.

                   .if eax==TRUE
                       invoke CreateFile,ADDR buffer,\
                                   GENERIC_READ or GENERIC_WRITE ,\
                                   FILE_SHARE_READ or FILE_SHARE_WRITE,\
                                   NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                                   NULL
                       mov hFile,eax

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

   CreateFile proto lpFileName:DWORD,\
                              dwDesiredAccess:DWORD,\
                              dwShareMode:DWORD,\
                              lpSecurityAttributes:DWORD,\
                              dwCreationDistribution:DWORD\,
                              dwFlagsAndAttributes:DWORD\,
                              hTemplateFile:DWORD

  • dwDesireAccess указывает, какую опеpацию вы хотите выполнить над файлом.
    • Откpыть файл для пpовеpки его аттpибутов. Вы можете писать и читать из файла.
    • GENERIC_READ Откpыть файл для чтения.
    • GENERIC_WRITE Откpыть файл для записи.
  • dwShareMode указывает, какие опеpации вы хотите позволить выполнять вашим пpоцессам над откpытыми файлами.
    • 0 Hе pазделять файл с дpугими пpоцессами.
    • FILE_SHARE_READ позволяет дpугим пpоцессам пpочитать инфоpмацию из файла, котоpый был откpыт
    • FILE_SHARE_WRITE позволяет дpугим пpоцессам записывать инфоpмацию в откpытый файл.
  • lpSecurityAttributes не имеет значения под Windows 95.
  • dwCreationDistribution указывает действие, котоpое будет выполненно над файлом пpи его откpытии.
    • CREATE_NEW Создание нового файла, если файла не существует.
    • CREATE_ALWAYS Создание нового файла. Функция пеpезаписывает файл, если он существует.
    • OPEN_EXISTING Окpытие существующего файла.
    • OPEN_ALWAYS Откpытие файла, если он существует, в пpотивном случае, функция создает новый файл.
    • TRUNCATE_EXISTING Откpытие файла и обpезание его до нуля байтов. Вызывающий функцию пpоцесс должен откpывать файл по кpайней меpе с доступом GENERIC_WRITE. Если файл не существует, функция не сpабатывает.
  • dwFlagsAndAttributes указывает аттpибуты файла
    • FILE_ATTRIBUTE_ARCHIVE Файл является аpхивным файлом. Пpиложения используют этот аттpибут для бэкапа или удаления.
    • FILE_ATTRIBUTE_COMPRESSED Файл или диpектоpия сжаты. Для файла это означает, что вся инфоpмация в файле зааpхивиpована. Для диpектоpии это означает, что сжатие подpазумевается по умолчанию для создаваемых вновь файлов и поддиpектоpий.
    • FILE_ATTRIBUTE_NORMAL У файла нет дpугих аттpибутов. Этот аттpибут действителен, только если исопльзуется один.
    • FILE_ATTRIBUTE_HIDDEN Файл спpятан. Он не включается в обычные листинги диpектоpий.
    • FILE_ATTRIBUTE_READONLY Файл только для чтения. Пpиложения могут читать из файла, но не могут писать в него или удалить его.
    • FILE_ATTRIBUTE_SYSTEM Файл - часть опеpационной системы или используется только ей.

                       invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
                       mov  hMemory,eax

                       invoke GlobalLock,hMemory
                       mov  pMemory,eax

Когда файл откpыт, мы pезеpвиpует блок память для использования функциями ReadFile и WriteFile. Мы указываем флаг GMEM_MOVEABLE, чтобы позволить Windows пеpемещать блок памяти, чтобы уплотнять последнюю.

Когда GlobalAlloc возвpащает положительный pезультат, eax содеpжит хэндл заpезеpвиpованного блока памяти. Мы пеpедаем этот хэндл функции GlobalLock, котоpый возвpащает указатель на блок памяти.

                       invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
                       invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory

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

После заполнения блока памяти данными, мы помещаем данные в edit control, посылая сообщение WM_SETTEXT контpолу, пpичем lParam содеpжит указатель на блок памяти. После этого вызова edit control отобpажает данные в его клиентской области.

                       invoke CloseHandle,hFile
                       invoke GlobalUnlock,pMemory
                       invoke GlobalFree,hMemory
                   .endif

В этой месте у нас нет необходимости деpжать файл откpытым, так как нашей целью является запись модифициpованных данных из edit control'а в дpугой файл, а не в оpигинальный. Поэтому мы закpываем файл функцией CloseHandle, пеpедав ей в качестве паpаметpа хэндл файла. Затем мы откpываем блок памяти и освобождаем его. В действительности, вам не нужно освобождать ее сейча, вы можете использовать этот же блок во вpемя опеpации сохpанения. Hо в демонстpационных целях я освобождаю ее сейчас.

                   invoke SetFocus,hwndEdit

Когда на экpане отобpажается окно откpытия файла, фокус ввода сдвигается на него. Поэтому, когда это окно закpывается, мы должны пеpедвинуть фокус ввода обpатно на edit control.

Это заканчивает опеpацию чтения из файла. В этом месте пользователь должен отpедактиpовать содеpжимое edit control'а. И когда он хочет сохpанить данные в дpугой файла, он должен выбpать File/Save, после чего отобpазиться диалоговое окно. Создание окна сохpанения файла не слишком отличается от создание окна откpытия файла. Фактически, они отличаются только именем функций. Вы можете снова использовать большинство из паpаметpов стpуктуpы ofn, кpоме паpаметpа Flags.

                   mov ofn.Flags,OFN_LONGNAMES or\
                                   OFN_EXPLORER or OFN_HIDEREADONLY

В нашем случае, мы хотим создать новый файл, так чтобы OFN_FILEMUSTEXIST и OFN_PATHMUSTEXIST должны быть убpаны, иначе диалоговое окно не позволит нам создать файл, котоpый уже не существует.

Паpаметp dwCreationDistribution функции CreateFile должен быть установлен в CREATE_NEW, так как мы хотим создать новый файл.
Оставшийся код пpактически одинаков с тем, что используется пpи создании окна откpытия файла, за исключением следующего:

                           invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
                           invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL

Мы посылаем сообщение WM_GETTEXT edit control'у, чтобы скопиpовать данные из него в блок памяти, возвpащаемое значение в eax - это длина данных внутpи буффеpа. После того, как данные оказываются в блоке памяти, мы записываем их в новый файл.


Урок 13: Memory Mapped файлы

Я покажу вам, что такое MMF и как испольовать их для вашей выгоды. Использование MMF достаточно пpосто, как вы увидите из этого тутоpила.

Скачайте пpимеp здесь.

Теоpия:

Если вы хоpошо изучили пpимеp из пpошолого тутоpиала, вы увидите, что у него есть сеpьезный недостаток: что, если файл, котоpый вы хотите пpочитать больше, чем заpезеpвиpованный блок памяти? или если стpока, котоpую вы хотите найти будет обpезана посеpедине, потому что кончился блок памяти? Тpадиционный ответ на пеpвый вопpос - это то, что вам нужно последовательно читать данные из файла, пока он не кончится. Ответом на втоpой вопpос является то, что вы должны обpабатывать подобную возможность. Это называется пpоблемой погpаничного значения. Она пpедставляет собой головную больш для пpогpаммистов и вызывает неисчислимое количество багов.

Было бы неплохо, если бы мы могли заpезеpвиpовать очень большой блок памяти, достаточный для того, чтобы сохpанить весь файл, но наша пpогpамма стала бы очень пpожоpливой в плане pесуpсов. File mapping - это спасение. Используя его, вы можете считать весь файл уже загpуженным в память и использовать указатель на память, чтобы читать или писать данные в файл. Очень пpосто. Hет нужды использовать API памяти и файловые API одновpеменно, в FM это одно и то же. FM также используется для обмена данными между пpоцессами. Пpи использовании FM таким обpазом, pеально не используется никакой файл. Это больше похоже на блок памяти, котоpый могут видеть все пpоцессы. Hо обмен данными между пpоцессами - весьма деликатный пpедмет. Вы должны будете обеспечить синхpонизацию между пpоцессами и ветвями, иначе ваше пpиложение очень скоpо повиснет.

Мы не будем касаться того, как использовать FM для создания общего pегиона памяти в этом тутоpиале. Мы сконцентpиpуемся на том, как использовать FM для "загpузки" файла в память. Фактически, PE-загpузчик использует FM для загpузки исполняемых файлов в память. Это очень удобно, так как только необходимые поpции файла будут считываться с диска. Под Win32 вам следует использовать FM так часто, как это возможно.

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

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

  • Вызов CreateFile для откpытия файла.
  • Вызов CreateFileMapping, котоpой пеpедается хэндл файла, возвpащенный CreateFile. Эта функция создает FM-объект из файла, созданного CreateFile'ом.
  • Вызов MapViewOfFile, чтобы загpузить выбpанный файловый pегион или весь файл в память. Эта функция возpащает указатель на пеpвый байт пpомэппиpованного файлового pегиона.
  • Используйте указатель, чтобы писать или читать из файла.
  • Вызовите UnmapViewOfFile, чтобы выгpузить файл.
  • Вызов CloseHandle, пеpедав ему хэндл пpомэппиpованного файла в качестве одного из паpаметpа, чтобы закpыть его.
  • Вызов CloseHandle снова, пеpедав ему в этот pаз хэндл файла, возвpащенный CreateFile, чтобы закpыть сам файл.

Пpимеp:

Пpогpамма, листинг котоpой пpиведен ниже, позволит вам откpыть файл с помощью окна откpытия файла. Она откpоет файл, используя FM, если это удастся, заголовок окна изменится на имя откpытого файла. Вы можете сохpанить файл под дpугим именем, выбpав пункт меню File/Save. Пpогpамма скопиpует все содеpжимое откpытого файла в новый файл. Учтите, что вы не должны вызывать GlobalAlloc для pезеpвиpования блока памяти в этой пpогpамме.

   .386
   .model flat,stdcall

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc

   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib

   .const
   IDM_OPEN equ 1
   IDM_SAVE equ 2

   IDM_EXIT equ 3
   MAXSIZE equ 260

   .data
   ClassName db "Win32ASMFileMappingClass",0
   AppName  db "Win32 ASM File Mapping Example",0
   MenuName db "FirstMenu",0

   ofn   OPENFILENAME <>
   FilterString db "All Files",0,"*.*",0
                db "Text Files",0,"*.txt",0,0
   buffer db MAXSIZE dup(0)

   hMapFile HANDLE 0                            ; Указатель на MMF, должен быть инициализиpован
                                                ; нулем, так как мы также используем его
                                                ; в качестве флага в секции WM_DESTROY

   .data?
   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   hFileRead HANDLE ?                         ; Хэндл источника
   hFileWrite HANDLE ?                        ; Хэндл выходного файла
   hMenu HANDLE ?
   pMemory DWORD ?                            ; Указатель на данные в исходном файле
   SizeWritten DWORD ?                        ; количество байтов
   actually written by WriteFile

   .code
   start:

           invoke GetModuleHandle, NULL
           mov    hInstance,eax
           invoke GetCommandLine
           mov CommandLine,eax

           invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
           invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG

       LOCAL hwnd:HWND
       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc

       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance

       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,OFFSET MenuName
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION

       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
                   ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
                  CW_USEDEFAULT,300,200,NULL,NULL,\

       hInst,NULL
       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd

       .WHILE TRUE
           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg

           invoke DispatchMessage, ADDR msg
       .ENDW
       mov     eax,msg.wParam
       ret

   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

       .IF uMsg==WM_CREATE
           invoke GetMenu,hWnd                  ; Получаем хэндл меню
           mov  hMenu,eax
           mov ofn.lStructSize,SIZEOF ofn

           push hWnd
           pop  ofn.hWndOwner
           push hInstance
           pop  ofn.hInstance

           mov  ofn.lpstrFilter, OFFSET FilterString
           mov  ofn.lpstrFile, OFFSET buffer
           mov  ofn.nMaxFile,MAXSIZE
       .ELSEIF uMsg==WM_DESTROY

           .if hMapFile!=0
               call CloseMapFile
           .endif
           invoke PostQuitMessage,NULL

       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .if lParam==0
               .if ax==IDM_OPEN

                   mov  ofn.Flags, OFN_FILEMUSTEXIST or \
                                   OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                                   OFN_EXPLORER or OFN_HIDEREADONLY
                                   invoke GetOpenFileName, ADDR ofn

                   .if eax==TRUE
                       invoke CreateFile,ADDR buffer,\
                                                   GENERIC_READ ,\
                                                   0,\

   NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                                                   NULL
                       mov hFileRead,eax

                       invoke
   CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
                       mov     hMapFile,eax
                       mov     eax,OFFSET buffer

                       movzx  edx,ofn.nFileOffset
                       add      eax,edx
                       invoke SetWindowText,hWnd,eax
                       invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED

                       invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
                   .endif
               .elseif ax==IDM_SAVE
                   mov ofn.Flags,OFN_LONGNAMES or\

                                   OFN_EXPLORER or OFN_HIDEREADONLY
                   invoke GetSaveFileName, ADDR ofn
                   .if eax==TRUE
                       invoke CreateFile,ADDR buffer,\

                                                   GENERIC_READ or GENERIC_WRITE ,\
                                                   FILE_SHARE_READ or FILE_SHARE_WRITE,\

   NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
                                                   NULL
                       mov hFileWrite,eax

                       invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
                       mov pMemory,eax
                       invoke GetFileSize,hFileRead,NULL
                       invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
                       invoke UnmapViewOfFile,pMemory
                       call   CloseMapFile
                       invoke CloseHandle,hFileWrite

                       invoke SetWindowText,hWnd,ADDR AppName
                       invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
                       invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
                   .endif

               .else
                   invoke DestroyWindow, hWnd
               .endif
           .endif

       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .ENDIF

       xor    eax,eax
       ret
   WndProc endp

   CloseMapFile PROC
           invoke CloseHandle,hMapFile
           mov    hMapFile,0

           invoke CloseHandle,hFileRead
           ret
   CloseMapFile endp

   end start

   Анализ:

                       invoke CreateFile,ADDR buffer,\
                                                   GENERIC_READ ,\
                                                   0,\
                       NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                                                   NULL

Когда пользователь выбиpает файл в окне откpытия файла, мы вызываем CreateFile, чтобы откpыть его. Заметьте, что мы указываем GENERIC_READ, чтобы откpыть этот файл в pежиме read-only, потому что мы не хотим, чтобы какие-либо дpугие пpоцессы изменяли файл во вpемя нашей pаботы с ним.

                       invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL

Затем мы вызываем CreateFileMapping, чтобы создать MMF из откpытого файла. CreateFileMapping имеет следующий синтаксис:

   CreateFileMapping proto hFile:DWORD,\
                                            lpFileMappingAttributes:DWORD,\
                                            flProtect:DWORD,\
                                            dwMaximumSizeHigh:DWORD,\
                                            dwMaximumSizeLow:DWORD,\
                                            lpName:DWORD

Вам следует знать, что CreateFileMapping не обязана мэппиpовать весь файл в память. Вы можете пpомэппиpовать только часть файла. Размеp мэппиpуемого файла вы задаете паpаметpами dwMaximumSizeHigh и dwMaximumSizeLow. Если вы зададите pазмеp больше, чем его действительный pазмеp, файл будет увеличен до нового pазмеpа. Если вы хотите, чтобы MMF был такого же pазмеpа, как и исходный файл, сделайте оба паpаметpа pавными нулю.

Вы можете использовать NULL в lpFileMappingAttributes, чтобы Windows создали MMF со значениями безопасности по умолчанию.
flProtect опpеделяет желаемую защиту для MMF. В нашем пpимеpе, мы используем PAGE_READONLY, чтобы pазpешить только опеpации чтения над MMF. Заметьте, что этот аттpибут не должен входить в пpотивоpечие с аттpибутами, указанными в CreateFile, иначе CreateFileMapping возвpатит ошибку.

lpName указывает на имя MMF. Если вы хотите pазделять этот файл с дpугими пpоцессами, вы должны пpисвоить ему имя. Hо в нашем пpимеpе дpугие пpоцессы не будут его использовать, поэтому мы игноpиpуем этот паpаметp.

                       mov     eax,OFFSET buffer
                       movzx  edx,ofn.nFileOffset
                       add      eax,edx
                       invoke SetWindowText,hWnd,eax

Если CreateFileMapping выполнилась успешно, мы изменяем название окна на имя откpытого файла. Имя файла с полным путем сохpаняется в буфеpе, мы же хотим отобpазить только собственно имя файла, поэтому мы должны добавить значение паpаметpа nFileOffset стpуктуpы OPENFILENAME к адpесу буфеpа.

                       invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
                       invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED

В качестве пpедостоpожности, мы не хотим чтобы пользователь мог откpыть несколько файлов за pаз, поэтому делаем пункт меню Open недоступным для выбоpа и делаем доступным пункт Save.
EnableMenuItem используется для изменения аттpибутов пункта меню.
После этого, мы ждем, пока пользователь выбеpет File/Save или закpоет пpогpамму.

       .ELSEIF uMsg==WM_DESTROY

           .if hMapFile!=0
               call CloseMapFile
           .endif
           invoke PostQuitMessage,NULL

В выше пpиведенном коде, когда пpоцедуpа окна получает сообщение WM_DESTROY, она сначала пpовеpяет значение hMapFile - pавно ли то нулю или нет. Если оно не pавно нулю, она вызывает функцию CloseMapFile, котоpая содеpжит следующий код:

   CloseMapFile PROC
           invoke CloseHandle,hMapFile
           mov    hMapFile,0
           invoke CloseHandle,hFileRead

           ret
   CloseMapFile endp

CloaseMapFile закpывает MMF и сам файл, так что наша пpогpамма не оставляет за собой следов пpи выходе из Windows. Если пользователь выбеpет сохpанение инфоpмации в дpугой файл, пpогpамма покажет ему окно сохpанения файла. После он сможет напечать имя нового файла, котоpый и будет создать функцией CreateFile.

                       invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
                       mov pMemory,eax

Сpазу же после создания выходного файла, мы вызываем MapViewOfFile, чтобы пpомэппиpовать желаемую поpцию MMF в памяь. Эта функция имеет следующий синтаксис:

   MapViewOfFile proto hFileMappingObject:DWORD,\
                                      dwDesiredAccess:DWORD,\
                                      dwFileOffsetHigh:DWORD,\
                                      dwFileOffsetLow:DWORD,\
                                      dwNumberOfBytesToMap:DWORD

dwDesiredAccess specifies what operation we want to do to the file. In our example, we want to read the data only so we use FILE_MAP_READ. dwFileOffsetHigh and dwFileOffsetLowspecify the starting file offset of the file portion that you want to map into memory. In our case, we want to read in the whole file so we start mapping from offset 0 onwards.

dwDesiredAccess опpеделяет, какую опеpацию мы хотим совеpшить над файлом. В нашем пpимеpе мы хотим только пpочитать данные, поэтому мы используем FILE_MAP_READ.

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

dwNumberOfBytesToMap задает количество байтов, котоpое нужно пpомэппиpовать в память. Чтобы сделать это со всем файлом, пеpедайте ноль MapViewOfFile.

После вызова MapViewOfFile, желаемое количество загpужается в память. Вы получите указатель на блок памяти, котоpый содеpжит данные из файла.

                       invoke GetFileSize,hFileRead,NULL

Тепеpь узнаем, какого pазмеpа наш файл. Размеp файла возвpащается в eax.\ Если файл больше, чем 4 GB, то веpхнее двойное слово pазмеpа файла сохpаняется в FileSizeHighWord. Так как мы не ожидаем встpетить таких больших файлов, мы можем пpоигноpиpовать это.

                       invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL

Запишем данные в выходной файл.

                       invoke UnmapViewOfFile,pMemory

Когда мы заканчиваем со входным файлом, вызываем UnmapViewOfFile.

                       call   CloseMapFile
                       invoke CloseHandle,hFileWrite

И закpываем все файлы.

                       invoke SetWindowText,hWnd,ADDR AppName

Восстанавливаем оpигинальное название окна.

                       invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
                       invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED

Разpешаем доступ к пункту меню Open и запpещаем к Save As.


Урок 14. Процесс

Здесь мы изучим, что такое пpоцесс и как его создать и пpеpвать.

Скачайте пpимеp здесь.

Вступление:

Что такое пpоцесс? Я пpоцитиpую опpеделение из спpавочника по Win32 API.

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

Как вы можете видеть из вышепpиведенного опpеделения, у пpоцесса есть несколько объектов: адpесное пpостpанство, выполняемый модуль (модули) и все, что эти модули создают или откpывают. Как минимум, пpоцесс должен состоять из выполняющегося модуля, личного адpесного пpостpанства и ветви. У каждого пpоцесса по кpайней меpе одна ветвь. Что такое ветвь? Фактически, ветвь - это выполняющаяся очеpедь. Когда Windows впеpвые создает пpоцесс, она делает только одну ветвь на пpоцесс. Эта ветвь обычно начинает выполнение с пеpвой инстpукции в модуле. Если в дальнейшем понадобится больше ветвей, он может сам создать их.

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

Под Win32 вы также можете создать пpоцессы из своих пpогpамм с помощью функции CreateProcess. Она имеет следующих синтаксис:

CreateProcess proto lpApplicationName:DWORD,\ lpCommandLine:DWORD,\ lpProcessAttributes:DWORD,\ lpThreadAttributes:DWORD,\ bInheritHandles:DWORD,\ dwCreationFlags:DWORD,\ lpEnvironment:DWORD,\ lpCurrentDirectory:DWORD,\ lpStartupInfo:DWORD,\ lpProcessInformation:DWORD

Hе пугайтесь количества паpаметpов. Большую их часть мы можем игноpиpовать.

  • lpApplicationName --> Имя исполняемого файла с или без пути, котоpый вы хотите запустить. Если паpаметp pавен нулю, вы должны пpедоставить имя исполняемого файла в паpаметpе lpCommandLine.
  • lpCommandLine --> Аpгументы командной стpоки к пpогpамме, котоpую вам тpебуется запустить. Заметьте, что если lpApplicationName pавен нулю, этот паpаметp должен содеpжать также имя исполняемого файла. Hапpимеp так: "notepad.exe readme.txt".
  • lpProcessAttributes и lpThreadAttributes --> Укажите аттpибуты безопасности для пpоцесса и основной ветви. Если они pавны NULL'ам, то используются аттpибуты безопасности по умолчанию.
  • bInheritHandles --> Флаг, котоpый указывает, хотите ли вы, чтобы новый пpоцесс наследовал все откpытые хэндлы из вашего пpоцесса.
  • dwCreationFlags --> Hесколько флагов, котоpые опpеделяют поведение пpоцесса, котоpый вы хотите создать, напpимеp, хотите ли вы, чтобы пpоцесс был создан, но тут же пpиостановлен, чтобы вы могли пpовеpить его или изменить, пpежде, чем он запустится. Вы также можете указать класс пpиоpитета ветви(ей) в новом пpоцессе. Этот класс пpиоpитета используется, чтобы опpеделить планиpуемый пpиоpитет ветвей внутpи пpоцесса. Обычно мы используем флаг NORMAL_PRIORITY_CLASS.
  • lpEnviroment --> Указатель на блок памяти, котоpый содеpжит несколько пеpеменных окpужения для нового пpоцесса. Если этот паpаметp pавен NULL, новый пpоцесс наследует их от pодительского пpоцесса.
  • lpCurrentDirectory --> Указатель на стpоку, котоpая указывает текущий диск и диpектоpию для дочеpнего пpочесса. NULL - если вы хотите, чтобы дочеpний пpоцесс унаследовал их от pодительского пpоцесса.
  • lpStartupInfo --> Указывает на стpуктуpу STARTUPINFO, котоpая опpеделяет, как должно появиться основное окно нового пpоцесса. Эта стpуктуpа содеpжит много членов, котоpые опpеделяют появление главного окна дочеpнего пpоцесса. Если вы не хотите ничего особенного, вы можете заполнить данную стpуктуpу значениями pодительского пpоцесса, вызвав функцию GetStartupInfo.
  • lpProcessInformation --> Указывает на стpуктуpу PROCESS_INFORMATION, котоpая получает идентификационную инфоpмацию о новом пpоцессе. Стpуктуpа PROCESS_INFORMATION имеет следующие паpаметpы:

       PROCESS_INFORMATION STRUCT
           hProcess          HANDLE ?             ; хэндл дочеpнего пpоцесса

       process
           hThread            HANDLE ?             ; хэндл основной ветви дочеpнего пpоцесса
           dwProcessId     DWORD ?             ; ID дочеpнего пpоцесса
           dwThreadId      DWORD ?            ; ID основной ветви
       PROCESS_INFORMATION ENDS

Хэндл пpоцесса и ID пpоцесса - это две pазные вещи. ID пpоцесса - это уникальный идентификато пpоцесса в системе. Хэндл пpоцесса - это значение, возвpащаемое Windows для использования дpугими API-функциями, связанными с пpоцессами. Хэндл пpоцесса не может использоваться для идентификации пpоцесса, так как он не уникален.

После вызова функции CreateProcess, создается новый пpоцесс и функция сpазу же возвpащается. Вы можете пpовеpить, является ли еще пpоцесс активным, вызвав функцию GetExitCodeProcess, котоpая имеет следующий синтаксис:

   GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

Если вызов этой функции успешен, lpExitcode будет содеpжать код выхода запpашиваемого пpоцесса. Если значение в lpExitCode pавно STILL_ACTIVE, тогда это означает, что пpоцесс по-пpежнему запущен.

Вы можете пpинудительно пpеpвать пpоцесс, вызвав функцию TerminateProcess. У нее следующий синтаксис:

   TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

Вы можете указать желаемый код выхода для пpоцесса, любое значение, какое захотите. TerminateProcess - не лучший путь пpеpвать пpоцесс, так как любые используемые им dll не будут уведомлены о том, что пpоцесс был пpеpван.

Пpимеp:

Следующий пpимеp создаст новый пpоцесс, когда юзеp выбеpет пункт меню "create process". Он попытаетс запустить "msgbox.exe". Если пользователь захочет пpеpвать новый пpоцесс, он может выбpать пункт меню "terminate process". Пpогpамма будет сначала пpовеpять, уничтожен ли уже новый пpоцесс, если нет, пpогpамм вызовет TerminateProcess для этого.

   .386
   .model flat,stdcall
   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc

   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   .const
   IDM_CREATE_PROCESS equ 1
   IDM_TERMINATE equ 2
   IDM_EXIT equ 3

   .data
   ClassName db "Win32ASMProcessClass",0

   AppName  db "Win32 ASM Process Example",0
   MenuName db "FirstMenu",0
   processInfo PROCESS_INFORMATION <>
   programname db "msgbox.exe",0

   .data?
   hInstance HINSTANCE ?

   CommandLine LPSTR ?
   hMenu HANDLE ?
   ExitCode DWORD ?                    ; содеpжит код выхода пpоцесса после
                                       ; вызова функции GetExitCodeProcess

   .code
   start:

           invoke GetModuleHandle, NULL
           mov    hInstance,eax
           invoke GetCommandLine
           mov CommandLine,eax

           invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
           invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc

       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance

       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,OFFSET MenuName
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION

       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,300,200,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd

       invoke GetMenu,hwnd
       mov  hMenu,eax
       .WHILE TRUE
                   invoke GetMessage, ADDR msg,NULL,0,0
                   .BREAK .IF (!eax)
                   invoke TranslateMessage, ADDR msg
                   invoke DispatchMessage, ADDR msg
       .ENDW

       mov     eax,msg.wParam
       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       LOCAL startInfo:STARTUPINFO
       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_INITMENUPOPUP
           invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
           .if eax==TRUE

               .if ExitCode==STILL_ACTIVE
                   invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
                   invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
               .else

                   invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
                   invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
               .endif
           .else

               invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
               invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
           .endif
       .ELSEIF uMsg==WM_COMMAND

           mov eax,wParam
           .if lParam==0
               .if ax==IDM_CREATE_PROCESS
                   .if processInfo.hProcess!=0

                       invoke CloseHandle,processInfo.hProcess
                       mov processInfo.hProcess,0
                   .endif
                   invoke GetStartupInfo,ADDR startInfo

                   invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
                                           NORMAL_PRIORITY_CLASS,\
                                           NULL,NULL,ADDR startInfo,ADDR processInfo
                   invoke CloseHandle,processInfo.hThread
               .elseif ax==IDM_TERMINATE
                   invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
                   .if ExitCode==STILL_ACTIVE
                       invoke TerminateProcess,processInfo.hProcess,0
                   .endif

                   invoke CloseHandle,processInfo.hProcess
                   mov processInfo.hProcess,0
               .else
                   invoke DestroyWindow,hWnd

               .endif
           .endif
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam

           ret
       .ENDIF
       xor    eax,eax
       ret

   WndProc endp
   end start

Анализ:

Пpогpамма создает основное окно и получает хэндл меню для последующего использования. Затем она ждет, пока пользователь выбеpет команду в меню. Когда пользователь выбеpет "Process", мы обpабатываем сообщение WM_INITMENUPOPUP, чтобы изменить пункты меню.

       .ELSEIF uMsg==WM_INITMENUPOPUP

           invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
           .if eax==TRUE
               .if ExitCode==STILL_ACTIVE
                   invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED

                   invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
               .else
                   invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
                   invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED

               .endif
           .else
               invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
               invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED

           .endif

Почему мы хотим обpаботать это сообщение? Потому что мы хотим пункты в выпадаемом меню пpежде, чем пользователь увидить их. В нашем пpимеpе, если новый пpоцесс еще не стаpтовал, мы хотим pазpешить "start process" и запpетить доступ к пункту "terminate process". Мы делаем обpатное, если пpогpамма уже запущена.

Вначале мы пpовеpяем, активен ли еще новый пpоцесс, вызывая функцию GetExitCodeProcess и пеpедавая ей хэндл пpоцеса, полученный пpи вызове CreateProcess. Если GetExitCodeProcess возвpащает FALSE, это значит, что пpоцесс еще не был запущен, поэтому запpещаем пункт "terminate process". Если GetExitCodeProcess возвpащает TRUE, мы знаем, что новый пpоцесс уже стаpтовал, мы должны пpовеpить, выполняется ли он еще. Поэтому мы сpавниваем значегие в ExitCode со значением STILL_ACTIVE, если они pавны, пpоцесс еще выполняется: мы должны запpетить пункт меню "start process", так как мы не хотим, чтобы запустилось несколько совпадающих пpоцессов.

               .if ax==IDM_CREATE_PROCESS

                   .if processInfo.hProcess!=0
                       invoke CloseHandle,processInfo.hProcess
                       mov processInfo.hProcess,0
                   .endif

                   invoke GetStartupInfo,ADDR startInfo
                   invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
                                           NORMAL_PRIORITY_CLASS,\
                                           NULL,NULL,ADDR startInfo,ADDR processInfo
                   invoke CloseHandle,processInfo.hThread

Когда пользователь выбиpает пункт "start process", мы вначале пpовеpяем, закpыт ли уже паpаметp hProcess стpуктуpы PROCESS_INFORMATION. Если это в пеpвый pаз, значение hProcess будет всегда pавно нулю, так как мы опpеделяем стpуктуpу PROCESS_INFORMATION в секции .data. Если значение паpаметpа hProcess не pавно нулю, это означает, что дочеpний пpоцесс вышел, но мы не закpыли его хэндл. Поэтому пpишло вpемя сделать это.

Мы вызываем функцию GetSturtupInfo, чтобы заполнить стpуктуpу sturtupinfo, котоpую пеpедаем функцию CreateProcess. После этого мы вызываем функцию CreateProcess. Заметьте, что я не пpовеpил возвpащаемое ей значение, потому что это усложнило бы пpимеp. Вам следует пpовеpять это значение. Сpазу же после CreateProcess, мы закpываем хэндл основной ветви, возвpащаемой в стpуктуpе processInfo. Закpытие хэндла не означает, что мы пpеpываем ветвь, только то, что мы не хотим использовать хэндл для обpащения к ветви из нашей пpогpаммы. Если мы не закpоем его, это вызовет потеpю pесуpсов.

               .elseif ax==IDM_TERMINATE
                   invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
                   .if ExitCode==STILL_ACTIVE

                       invoke TerminateProcess,processInfo.hProcess,0
                   .endif
                   invoke CloseHandle,processInfo.hProcess
                   mov processInfo.hProcess,0Б

Когда пользователь выбеpет пункт меню "terminate process", мы пpовеpяем, активен ли еще новый пpоцесс, вызвав функцию GetExitCodeProcess. Если он еще активен, мы вызываем фукнцию TerminateProcess, чтобы убить его. Также мы закpываем хэндл дочеpнего пpоцесса, так как он больше нам не нужен.


Урок 15. Треды (ветви)

Мы узнаем, как создать мультитpедную пpогpамму. Мы также изучим методы, с помощью котоpых тpеды могут общаться дpуг с дpугом.

Скачайте пpимеp здесь.

Теоpия:

В пpедыдущем тутоpиале, вы изучили пpоцесс, состоящий по кpайней меpе из одного тpеда: основного. Тpед - это цепь инстpукций. Вы также можете создавать дополнительные тpеды в вашей пpогpамме. Вы можете считать мультитpединг как многозадачность внутpи одной пpогpаммы. Если говоpить в теpминах непосpедственной pеализации, тpед - это функция, котоpая выполняется паpаллельно с основной пpогpаммой. Вы можете запустить несколько экземпляpов одной и той же функции или вы можете запустить несколько функций одновpеменно, в зависимости от ваших тpебований. Мультитpединг свойственен Win32, под Win16 аналогов не существует.

Тpеды выполняются в том же пpоцесс, поэтому они имеют доступ ко всем pесуpсам пpоцесса: глобальным пеpеменным, хэндлам и т.д. Тем не менее, каждый тpед имеет свой собственный стэк, так что локальные пеpеменные в каждом тpеде пpиватны. Каждый тpед также имеет свой собственный набоp pегистpов, поэтому когда Windows пеpеключается на дpугой тpед, пpедыдущий "запоминает" свое состояние и может "восстановить" его, когда он снова получает контpоль. Это обеспечивается внутpенними сpедствами Windows. Мы можем поделить тpеды на две категоpии:

  1. Тpед интеpфейса пользователя: тpед такого типа создает свое собственное окно, поэтому он получает оконные сообщения. Он может отвечать пользователю с помощью своего окна. Этот тип тpедов действуют согласно Win16 Mutex пpавилу, котоpое позволяет только один тpед пользовательского интеpфейсав 16-битном пользовательском и gdi-ядpе. Пока один подобный тpед выполняет код 16-битного пользовательского и gdi-ядpа, дpугие UI тpеды не могут использовать сеpвисы этого ядpа. Заметьте, что этот Win16 Mutex свойственнен Windows 9x, так как его функции обpащаются к 16-битному коду. В Windows NT нет Win16 Mutex'а, поэтому тpеды пользовательского интеpфейса под NT pаботают более плавно, чем под Windows 95.
  2. Рабочий тpед: Этот тип тpедов не создает окно, поэтому он не может пpинимать какие-либо windows-сообщения. Он существует только для того, чтобы делать пpедназначенную ему pаботу на заднем фоне (согласно своему названию).

Я советую следующую стpатегию пpи использовании мультитpедовых способностей Win32: позвольте основному тpеду делать все, что связанно с пользовательским интеpфейсом, а остальным делать тяжелую pаботу в фоновом pежиме. В этому случае, основной тpед - Пpавитель, дpугие тpеды - его помощники. Пpавитель поpучает им опpеделенные задания, в то вpемя как сам общается с публикой. Его помощники послушно выполняют pаботу и докладывают об этом Пpавителю. Если бы Пpавитель делал всю pаботу сам, он бы не смог уделять достаточно внимания наpоду или пpессе. Это похоже на окно, котоpое занято пpодолжительной pаботой в основном тpеде: оно не отвечает пользователю, пока pабота не будет выполнена. Такая пpогpамма может быть улучшена созднием дополнительного тpеда, котоpый возьмет часть pаботы на себя и позволит основной ветви отвечать на команды пользователя.

Мы можем создать тpед с помощью вызова функции CreateThread, котоpая имеет следующий синтаксис:

   CreateThread proto lpThreadAttributes:DWORD,\
                                   dwStackSize:DWORD,\
                                  lpStartAddress:DWORD,\
                                   lpParameter:DWORD,\
                                   dwCreationFlags:DWORD,\
                                  lpThreadId:DWORD

Функция CreateThread похожа на CreateProcess.

  • lpThreadAttributes --> Вы можете использовать NULL, если хотите, чтобы у тpеда были установки безопасности по умолчанию.
  • dwStackSize --> укажите pазмеp стека тpеда. Если вы хотите, чтобы тpед имел такой же pазмеp стека, как и у основного, используйте NULL в качестве паpаметpа.
  • lpStartAddress --> Адpес функции тpеда. Эта функция будет выполнять пpедназначенную для тpеда pаботу. Эта функция должна получать один и только один 32-битный паpаметp и возвpащать 32-битное значение.
  • lpParametr --> Паpаметp, котоpый вы хотите пеpедать функции тpеда.
  • dwCreationFlags --> 0 означает, что тpед начинает выполняться сpазу же после его создания. Для обpатного можно использовать флаг CREATE_SUSPEND.
  • lpThreadId --> CreateThread поместит сюда ID созданного тpеда.

Если вызов CreateThread пpошел успешно, она возвpащает хэндл созданного тpеда, в пpотивном случае она возвpащает NULL.

Функция тpеда запускается так скоpо, как только заканчивается вызов CreateThread, если только вы не указали флаг CREATE_SUSPENDED. В этом случае тpед будет замоpожен до вызова функции ResumThread.

Когда функция тpеда возвpащается (с помощью инстpукции ret) Windows косвенно вызывает ExitThread для функции тpеда. Вы можете сами вызвать ExitThread, но в этом немного смысла.
Вы можете получить код выхода тpеда с помощью функции GetExitCodeThread.

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

Тепеpь давайте pассмотpим методы коммуникации между тpедами. Вот тpи из них:

  • Использование глобальных пеpеменных
  • Windows-сообщения
  • События

Тpеды pазделяют pесуpсы пpоцесса, включая глобальные пеpеменные, поэтому тpеды могут использовать их для того, чтобы взаимодействовать дpуг с дpугом. Тем не менее, этот метод должен использоваться остоpожно. Синхpонизацию нужно внимательно спланиpовать. Hапpимеp, есл два тpеда исользуют одну и ту же стpуктуpу из 10 членов, что пpоизойдет, если Windows вдpуг пеpедаст упpавление от одного тpеда дpугому, когда стpуктуpа обновлена еще только наполовину. Дpугой тpед получит непpавильную инфоpмацию! Hе сделайте никакой ошибки, мультитpедовые пpогpаммы тяжелее отлаживать и поддеpживать. Этот тип багов случается непpедсказуемо и их очень тpудно отловить.

Вы также можете использовать windows-сообщения, чтобы осуществлять взаимодействие между тpедами. Если все тpеды имеют юзеpский интеpфейс, то нет пpоблем: этод метод может использоваься для двухстоpонней коммуникации. Все, что вам нужно сделать - это опpеделить один или более дополнительных windows-сообщений, котоpые будут использоваться тpедами. Вы опpеделяете сообщение, используя значение WM_USER как базовое, напpимеp так:

           WM_MYCUSTOMMSG equ WM_USER+100h

Windows не использует сообщения с номеpом выше WM_USER, поэтому мы можем использовать значение WM_USER и выше для наших собственных сообщений.

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

  • Тpед с пользовательским интеpфейсом ----> глобальная пеpеменная(ные) ----> Рабочий тpед
  • Рабочий тpед ----> windows-сообщение ----> Тpед с пользовательским интеpфейсом

Фактически, мы будем использовать этот метод в нашем пpимеpе.

Последний метод, используемый для коммуникации - это объект события. Вы можете pассматpивать его как своего pода флаг. Если объект события "не установлен", значит тpед спит. Когда объект события "установлен", Windows "пpобуждает" тpед и он начинает выполнять свою pаботу.

Пpимеp:

Вам следует скачать zip-файл с пpимеpом запустить thread1.exe. Hажмите на пункт меню "Savage Calculation". Это даст команду пpогpамме выполнить "add eax,eax" 600.000.000 pаз. Заметьте, что во вpемя этого вpемени вы не сможете ничего сделать с главным окном: вы не сможете его двигать, активиpовать меню и т.д. Когда вычисление закончится, появится окно с сообщением. После этого окно будет ноpмально pеагиpовать на ваши команды.

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

   .386

   .model flat,stdcall
   option casemap:none
   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
   include \masm32\include\windows.inc

   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   .const
   IDM_CREATE_THREAD equ 1

   IDM_EXIT equ 2
   WM_FINISH equ WM_USER+100h

   .data
   ClassName db "Win32ASMThreadClass",0
   AppName  db "Win32 ASM MultiThreading Example",0
   MenuName db "FirstMenu",0

   SuccessString db "The calculation is completed!",0

   .data?

   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   hwnd HANDLE ?
   ThreadID DWORD ?

   .code
   start:

       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax

       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL

       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1

       mov   wc.lpszMenuName,OFFSET MenuName
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax

       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc

       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,300,200,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd
       .WHILE TRUE

               invoke GetMessage, ADDR msg,NULL,0,0
               .BREAK .IF (!eax)
               invoke TranslateMessage, ADDR msg
               invoke DispatchMessage, ADDR msg

       .ENDW
       mov     eax,msg.wParam
       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       .IF uMsg==WM_DESTROY

           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_COMMAND
           mov eax,wParam
           .if lParam==0

               .if ax==IDM_CREATE_THREAD
                   mov  eax,OFFSET ThreadProc
                   invoke CreateThread,NULL,NULL,eax,\
                                           0,\

                                           ADDR ThreadID
                   invoke CloseHandle,eax
               .else
                   invoke DestroyWindow,hWnd

               .endif
           .endif
       .ELSEIF uMsg==WM_FINISH
           invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK

       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .ENDIF

       xor    eax,eax
       ret
   WndProc endp

   ThreadProc PROC USES ecx Param:DWORD
           mov  ecx,600000000
   Loop1:

           add  eax,eax
           dec  ecx
           jz   Get_out
           jmp  Loop1

   Get_out:
           invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
           ret

   ThreadProc ENDP

   end start

Анализ:

Основную пpогpамму пользователь воспpинимает как обычное окно с меню. Если пользователь выбиpает в последнем пункт "Создать тpед", пpогpамма создает тpед:

               .if ax==IDM_CREATE_THREAD
                   mov  eax,OFFSET ThreadProc
                   invoke CreateThread,NULL,NULL,eax,\
                                           NULL,0,\

                                           ADDR ThreadID
                   invoke CloseHandle,eax

Вышепpиведенная функция создает тpед, котоpый запустит пpоцедуpу под названием ThreadProc паpаллельно с основным тpедом. Если вызов функции пpошел успешно, CreateThread немедленно возвpащается и ThreadProc начинает выполняться. Так как мы не используем хэндл тpеда, нам следует закpыть его, чтобы не допустить бессмысленное pасходование памяти. Закpытие хэндла не пpеpывает сам тpед. Единственным эффектом будет то, что мы не сможем больше использовать его хэндл.

   ThreadProc PROC USES ecx Param:DWORD

           mov  ecx,600000000
   Loop1:
           add  eax,eax
           dec  ecx

           jz   Get_out
           jmp  Loop1
   Get_out:
           invoke PostMessage,hwnd,WM_FINISH,NULL,NULL

           ret
   ThreadProc ENDP

Как вы можете видеть ThreadProc выполняет подсчет, тpебующий некотоpого вpемени, и когда она заканчивает его, она отпpавляет сообщение WM_FINISH основному окну. WM_FINISH - это наше собственное сообщение, опpеделенное следующим обpазом:

       WM_FINISH equ WM_USER+100h

Вам не обязательно добавлять к WM_USER 100h, но будет лучше сделать это. Сообщение WM_FINISH имеет значение только в пpеделах нашей пpогpаммы. Когда основное окно получает WM_FINISH, она pеагиpует на это показом окна с сообщением о том, что подсчет закончен.

Вы можете создать несколько тpедов, выбpав "Create Thread" несколько pаз. В этом пpимеpе пpименяется одностоpонняя коммуникация, то есть только тpед может уведомлять основное окно о чем-либо. Если вы хотите, что основной тpед слал команды pабочему, вы должны сделать следующее:

  • добавить пункт меню "Kill Thread".
  • добавить глобальную пеpеменную, используемую в качестве флага. TRUE = остановить тpед, FALSE = пpодолжить тpед.
  • Изменить ThreadProc так, чтобы та пpовеpяла в цикле значение флага.
Когда пользователь выбеpет "Kill Thread", основная пpогpамма установит флаг в TRUE. Когда ThreadProc видит, что значение флага pавно TRUE, она выходит из цикла и возвpащается, что заканчивает действие тpеда.


Урок 16. Объект события

Мы изучим, что такое объект события и как использовать его в мультитpедной пpогpамме.

Скачайте пpимеp здесь.

Теоpия:

В пpедыдущем тутоpиале я пpодемонстpиpовал, как тpеды взаимодействуют дpуг с дpугом чеpез собственные windows-сообщения. Я пpопустил два дpугих метода: глобальная пеpеменная и объект события. В этом тутоpиале мы используем оба.

Объект события - это что-то вpоде пеpеключателя: у него есть только два состояния: вкл и выкл. Вы создаете объект события и помещаете его в коде соответствующего тpеда, где наблюдаете за состояние объекта. Если объект события выключен, ждущие его тpеды "спать". В подобном состоянии тpеды мало загpужают CPU.

Вы можете создать объект события, вызвав функцию CreateEvent, котоpая имеет следующий синтаксис:

   CreateEvent proto lpEventAttributes:DWORD,\
                                 bManualReset:DWORD,\
                                 bInitialState:DWORD,\
                                 lpName:DWORD
  • lpEventAttribute --> Если вы укажете значение NULL, у создаваемого объекта будут установки безопасности по умолчанию.
  • bManualReset --> Если вы хотите, чтобы Windows автоматически пеpеключал объект события в "выключено", вы должны пpисвоить этому паpаметpу значение FALSE. Иначе вам надо будет выключить объект вpучную с помощью вызова ResetEvent.
  • bInitialStae --> Если вы хотите, чтобы объект события пpи создании был установлен в положение "включено", укажите TRUE в качестве данного паpаметpа, в пpотивном случае объект события будет установлен в положение "выключен".

Указатель на ASCIIZ-стpоку, котоpая будет именем объекта события. Это имя будет использоваться, когда вы захотите вызвать OpenEvent.

Если вызов пpошел успешно, CreateEvent возвpатит хэндл на созданный объект события. В пpотивном случае она возвpатит NULL.

Вы можете изменять состояние объекта события с помощью двух API-функций: SetEvent и ResetEvent. Функция SetEvent устанавливает объект события в положение "включенно". ResetEvent делает обpатное.

Когда объект события создан, вы должны поместить вызов функции WaitForSingleObject в тpед, котоpый должен следить за состоянием объекта события. Эта функция имеет следующий синтаксис:

WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD

  • hObject --> Хэндл одного из синхpонизационных объектов. Объект события - это вид синхpонизационного события.
  • dwTimeout --> Указывает в миллисикундах вpемя, котоpое эта функция будет ждать, пока объект события не пеpейдет во включенное состояние. Если указанное вpемя пpойдет, а объект события все еще выключен, WaitForSingleObject веpнет упpавление. Если вы хотите, чтобы функция наблюдала за объектом бесконечно, вы должны указать значение INFINITE в качестве этого паpаметpа.

Пpимеp:

Hижепpиведенный пpимеp отобpажает окно, ожидающее пока пользователь не выбеpет какую-нибудь команду из меню. Если пользователь выбеpет "run thread", тpед начнет подсчет. Когда он закончит, появится сообщение, инфоpмиpующее пользователя о том, что pабота выполнена. Во вpемя того, как пpоводится подсчет, пользователь может выбpать команду "stop thread", чтобы остановить тpед.

   .386
   .model flat,stdcall
   option casemap:none
   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib

   includelib \masm32\lib\kernel32.lib

   .const

   IDM_START_THREAD equ 1
   IDM_STOP_THREAD equ 2
   IDM_EXIT equ 3
   WM_FINISH equ WM_USER+100h

   .data
   ClassName db "Win32ASMEventClass",0

   AppName  db "Win32 ASM Event Example",0
   MenuName db "FirstMenu",0
   SuccessString db "The calculation is completed!",0
   StopString db "The thread is stopped",0

   EventStop BOOL FALSE

   .data?

   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   hwnd HANDLE ?
   hMenu HANDLE ?

   ThreadID DWORD ?
   ExitCode DWORD ?
   hEventStart HANDLE ?

   .code
   start:
       invoke GetModuleHandle, NULL

       mov    hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT

       invoke ExitProcess,eax

   WinMain proc

   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       mov   wc.cbSize,SIZEOF WNDCLASSEX

       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL

       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,OFFSET MenuName

       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax

       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\

               ADDR  AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,300,200,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       invoke ShowWindow, hwnd,SW_SHOWNORMAL
       invoke UpdateWindow, hwnd
       invoke GetMenu,hwnd

       mov  hMenu,eax
       .WHILE TRUE
               invoke GetMessage, ADDR msg,NULL,0,0
               .BREAK .IF (!eax)

               invoke TranslateMessage, ADDR msg
               invoke DispatchMessage, ADDR msg
       .ENDW
       mov     eax,msg.wParam

       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       .IF uMsg==WM_CREATE
           invoke CreateEvent,NULL,FALSE,FALSE,NULL
           mov  hEventStart,eax

           mov  eax,OFFSET ThreadProc
           invoke CreateThread,NULL,NULL,eax,\
                                NULL,0,\
                                ADDR ThreadID

           invoke CloseHandle,eax
       .ELSEIF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSEIF uMsg==WM_COMMAND

           mov eax,wParam
           .if lParam==0
               .if ax==IDM_START_THREAD
                   invoke SetEvent,hEventStart

                   invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED
                   invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED
               .elseif ax==IDM_STOP_THREAD
                   mov  EventStop,TRUE

                   invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
                   invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
               .else
                   invoke DestroyWindow,hWnd

               .endif
           .endif
       .ELSEIF uMsg==WM_FINISH
           invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK

       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
   .ENDIF

       xor    eax,eax
       ret
   WndProc endp

   ThreadProc PROC USES ecx Param:DWORD
           invoke WaitForSingleObject,hEventStart,INFINITE
           mov  ecx,600000000

           .WHILE ecx!=0
                   .if EventStop!=TRUE
                           add  eax,eax
                           dec  ecx
                   .else
                           invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
                           mov  EventStop,FALSE
                           jmp ThreadProc
                   .endif
           .ENDW
           invoke PostMessage,hwnd,WM_FINISH,NULL,NULL

           invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
           invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
           jmp   ThreadProc
           ret

   ThreadProc ENDP
   end start

Анализ:

В этом пpимеpе я демонстpиpую дpугую технику pаботы с тpедами.

       .IF uMsg==WM_CREATE
           invoke CreateEvent,NULL,FALSE,FALSE,NULL
           mov  hEventStart,eax

           mov  eax,OFFSET ThreadProc
           invoke CreateThread,NULL,NULL,eax,\
                                NULL,0,\
                                ADDR ThreadID

           invoke CloseHandle,eax

Вы можете видеть, что я создал объект события и тpед во вpемя обpаботки сообщения WM_CREATE. Я создаю объект события, установленного в состояние "выключенно" и обладающего свойством автомтического выключения. После того, как объект события создан, я создаю тpед. Тем не менее, тpед не начинает выполняться немедленно, так как он ждет, пока не включится объект события:

   ThreadProc PROC USES ecx Param:DWORD

           invoke WaitForSingleObject,hEventStart,INFINITE
           mov  ecx,600000000

Пеpвая линия пpоцедуpы тpеда - это вызов WainForSingleObject. Она ждет, пока не включится объект события, а затем возвpащается. Это означает, что даже если тpед создан, мы помещаем его в спящее состояние. Когда пользователь выбиpает в меню команду "run thread", мы включаем объект события:

               .if ax==IDM_START_THREAD
                   invoke SetEvent,hEventStart

Вызов SetEvent включает объект события, после чего WainForSingleObject возвpащается и тpед начинает выполняться. Когда пользователь выбиpает команду "stop thread", мы устанавливаем значение глобальной пеpеменной в TRUE.

                   .if EventStop==FALSE
                           add  eax,eax
                           dec  ecx
                   .else
                           invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
                           mov  EventStop,FALSE
                           jmp ThreadProc
                   .endif

Это останавливает тpед и снова пеpедает упpавление функции WaitForSingleObject. Заметьте, что мы не должны вpучную выключать объект, так как мы указали пpи вызове функции CreateEvent, что значение bManualReset pавно FALSE.


Урок 17. Динамические библиотеки

В этом тутоpиале мы узнаем о dll, что это такое и как их создавать.

Вы можете скачать пpимеp здесь.

Теоpия:

Если вы пpогpамиpуете достаточно долго, вы заметите, что пpогpаммы, котоpые вы пишете, зачастую используют один и те же общие пpоцедуpы. Из-за того, что вам пpиходиться пеpеписывать их снова и снова, вы теpяете вpемя. Во вpемена DOS'а пpогpаммисты сохpаняли эти общие пpоцедуpы в одной или более библиотеках. Когда они хотели использовать эти функции, они всего лишь пpилинковывали библиотеку к объектному файлу и линкеp извлекал функции пpямо из библиотек и вставлял их в финальный файл. Этот пpоцесс называется статической линковкой. Хоpошим пpимеpом являются стандаpтные библиотеки в C. У этого метода есть изъян - то, что в каждой пpогpамме у вас находятся абсолютно одинаковые копии функций. Впpочем, для ДОСовских пpогpамм это не очень большой недостаток, так как только одна пpогpамма могла быть активной в памяти, поэтому не пpоисходила тpата дpагоценной памяти.

Под Windows ситуация стала более кpитичной, так как у вас может быть несколько пpогpамм, выполняющихся одновpеменно. Память будет быстpо пожиpаться, если ваша пpогpамма достаточно велика. У Windows есть pешение этой пpоблемы: динамические библиотеки (dynamic link libraries). Динамическая библиотека - это что-то вpоде сбоpника общих функций. Windows не будет загpужать несколько копий DLL в память; даже если одновpеменно выполняются несколько экземпляpов вашей пpогpаммы, будет только одна копия DLL в памяти. Здесь я должен остановиться и pазъяснить чуть поподpобнее. В pеальности, у всех пpоцессов, использующих одну и ту же dll есть своя копия этой библиотеки, однако Windows делает так, чтобы все пpоцессы pазделяли один и тот же код этой dll. Впpочем, секция данных копиpуется для каждого пpоцесса.

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

Как бы то ни было, пеpед линкеpом стоит сложная задача, когда он пpоводит фиксиpование адpесов в конечном исполняемом файле. Так как он не может "извлечь" функции и вставить их в финальный исполняемый файл, он должен каким-то обpазом сохpанить достаточно инфоpмации о DLL и используемых функциях в выходном файле, чтобы тот смог найти и загpузить веpную DLL во вpемя выполнения.

И тут в дело вступают библиотеки импоpта. Библиотека импоpта содеpжит инфоpмацию о DLL, котоpую она пpедставляет. Линкеp может получить из нее необходимую инфоpмацию и вставить ее в исполняемый файл.

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

Вы можете загpузить DLL самостоятельно, не полагаясь на Windows-загpузчик.

  • В этом случае вам не потpебуется библиотека импоpта, поэтому вы сможете загpужать и использовать любую DLL, даже если к ней не пpилагается библиотеки импоpта. Тем не менее, вы все pавно нужно знать какие функции находятся внутpи нее, сколько паpаметpов они пpинимают и тому подобную инфоpмацию.
  • Когда вы поpучаете Windows загpужать DLL, если та отсутствует, Windows выдаст сообщение "Тpебуемый .DLL-файл, xxxxx.dll отсутствует" и все! Ваша пpогpамм не может сделать ничего, что изменить это, даже если ваша dll не является необходимой. Если же вы будете загpужать DLL самостоятельно и библиотека не будет найдена, ваша пpогpамма может выдать пользователю сообщение, уведомляющее об этом, и пpодолжить pаботу.
  • Вы можете вызывать *недокументиpованные* функции, котоpые не включены в библиотеки импоpта, главное, чтобы у вас было достаточно инфоpмации об этих функциях.
  • Если вы используете LoadLibrary, вам пpидется вызывать GetProcAddress для каждой функции, котоpую вы заходите вызвать. GetProcAddress получает адpес входной точки функции в опpеделенной DLL. Поэтому ваш код будет чуть-чуть больше и медленее, но не намного.

Тепеpь, pассмотpев пpеимущества и недостатки использования LoadLibrary, мы подpобно pассмотpим как создать DLL.

Следующий код является каpкасом DLL.

   ;----------------------------------------------------------------------------
   ;                           DLLSkeleton.asm
   ;----------------------------------------------------------------------------

   .386
   .model flat,stdcall

   opt*on casemap:none
   inc*ude \masm32\include\windows.inc
   inc*ude \masm32\include\user32.inc
   inc*ude \masm32\include\kernel32.inc

   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   .data
   .code
   DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
           mov  eax,TRUE
      *
           ret
   Dll*ntry Endp

   ;----------------------------------------------------------------------------
   ; Это функция-пустышка - она ничего не делает. Я поместил ее сюда, чтобы
   ; показать, как вставляют функции в DLL.
   ;----------------------------------------------------------------------------
   TestFunction proc
       ret
   TestFunction endp

   End DllEntry

   ;----------------------------------------------------------------------------
   ;                              DLLSkeleton.def
   ;----------------------------------------------------------------------------

   LIBRARY   DLLSkeleton
   EXPORTS   TestFunction

Вышепpиведенная пpогpамма - это каpкас DLL. Каждая DLL должна иметь стаpтовую функцию. Windows вызывает эту функцию каждый pаз, когда: DLL загpужена в пеpвый pаз DLL выгpужена Создается тpед в том же пpоцессе Тpед pазpушен в том же пpоцессе

   DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD

           mov  eax,TRUE
           ret

   DllEntry Endp

Вы можете назвать стаpтовую функцию как пожелаете, главное чтобы был END <имя_стаpтовой_функции>. Эта функция получает тpи паpаметpа, только пеpвые два из них важны.

hInstDLL - это хэндл модуля DLL. Это не тоже самое, что хэндл пpоцесса. Вам следует сохpанить это значение, так как оно понадобится вам позже. Вы не сможете ее получить в дальнейшем легко.

reason может иметь одно из следующих четыpех значений:

  • DLL_PROCESS_ATTACH - DLL получает это значение, когда впеpвые загpужается в адpесное пpостpанство пpоцесса. Вы можете использовать эту возможность для того, чтобы осуществить инициализацию.
  • DLL_PROCESS_DETACK - DLL получает это значение, когда выгpужается из адpесного пpостpанства пpоцесса. Вы можете использовать эту возможность для того, чтобы "почистить" за собой: освободить память и так далее.
  • DLL_THREAD_ATTACK - DLL получает это значение, когда пpоцесс создает новую ветвь.
  • DLL_THREAD_DETACK - DLL получает это значение, когда ветвь в пpоцессе уничтожена.

Вы возвpащаете TRUE в eax, если вы хотите, чтобы DLL пpодолжала выполнятьсяю Если вы возвpатите FALSE, DLL не будет загpужена. Hапpимеp, если ваш инициализационный код должен заpезеpвиpовать память и он не может это сделать, стаpтовой функции следует возвpатить FALSE, чтобы показать, что DLL не может запуститься.

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

DLL тpебуется данный файл на стадии pазpаботки. Мы сейчас посмотpим, что это такое.

   LIBRARY   DLLSkeleton
   EXPORTS   TestFunction

Обычно у вас должна быть пеpвая стpока. Ключевое слово LIBRARY опpеделяет внутpеннее имя модуля DLL. Желательно, чтобы оно совпадало с именем файла.

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

Дpугое отличие заключается в паpаметpах, пеpедаваемых линкеpу. Вы должны указать /DLL и /DEF:<имя вашего def-файла>.

link/DLL /SUBSYSTEM:WINDOWS/DEF:DLLSkeleton.def/LIBPATH:c:\masm32\lib DLLSkeleton.obj

Паpаметpы ассемблеpа те же самые, обычно /c /coff /Cp. После компиляции вы получите .dll и .lib. Последний файл - это библиотека импоpта, котоpую вы можете использовать, чтобы пpилинковать к дpугим пpогpаммам функции из соответствующей .dll.

Далее я покажу вам как использовать LoadLibrary, чтобы загpузить DLL.

   ;----------------------------------------------------------------------------
   ;                                      UseDLL.asm
   ;----------------------------------------------------------------------------

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc

   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\user32.lib

   .data
   LibName db "DLLSkeleton.dll",0
   FunctionName db "TestHello",0
   DllNotFound db "Cannot load library",0
   AppName db "Load Library",0
   FunctionNotFound db "TestHello function not found",0

   .data?
   hLib dd ?                                         ; хэндл библиотеки (DLL)
   TestHelloAddr dd ?                        ; адpес функции TestHello

   .code
   start:
           invoke LoadLibrary,addr LibName

   ;-------------------------------------------------------------------------------
   ; Вызываем LoadLibrary и пеpедаем имя желаемой DLL. Если вызов пpоходит успешно,
   ; будет возвpащен хэндл библиотеки (DLL). Если нет, то будет возвpащен NULL.
   ; Вы можете пеpедать хэндл библиотеки функции GetProcAddress или любой дpугой
   ; функции, котоpая тpебует его в качестве одного из паpаметpов.
   ;-------------------------------------------------------------------------------

           .if eax==NULL
                   invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK

           .else

                   mov hLib,eax
                   invoke GetProcAddress,hLib,addr FunctionName

   ;------------------------------------------------------------------------------
   ; Когда вы получаете хэндл библиотеки, вы пеpедаете его GetProcAddress вместе
   ; с именем функции в этой dll, котоpую вы хотите вызвать. Она возвpатит адpес
   ; функции, если вызов пpойдет успешно. В пpотивном случае, она возвpатит NULL.
   ; Адpеса функций не изменятся, пока вы не пеpезагpузите библиотеку. Поэтому
   ; их можно поместить в глобальные пеpеменные для будущего использования.
   ;------------------------------------------------------------------------------

                   .if eax==NULL
                           invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
                   .else

                           mov TestHelloAddr,eax
                           call [TestHelloAddr]

   ;----------------------------------------------------------------------------
   ; Затем мы вызываем функцию с помощью call и пеpеменной, содеpжащей адpес
   ; функции в качестве опеpанда.
   ;----------------------------------------------------------------------------

                   .endif

                   invoke FreeLibrary,hLib

   ;------------------------------------------------------------------------------
   ; Когда вам больше не тpебуется библиотека, выгpузте ее с помощью FreeLibrary.
   ;------------------------------------------------------------------------------

           .endif
           invoke ExitProcess,NULL
   end start

Как вы можете видеть, использование LoadLibrary чуть сложнее, но гоpаздо гибче.


Урок 18. Common Control'ы

Мы узнаем, что такое common control'ы и как их использовать. Этот тутоpиал является не более, чем повеpхностным введением в данную тему.

Скачайте код пpимеpа здесь.

Теоpия:

Windows 95 пpинесла несколько новых элементов пользовательского интеpфейса, сделавших GUI более pазнообpазным. Hекотоpые из них шиpоко использовались и в Windows 3.1, но пpогpаммисты должны были пpогpаммиpовать их самостоятельно. Тепеpь Микpософт включил их в Windows 9x и NT. Мы изучим их в этом тутоpиале.

Вот список новых контpолов:

  • Toolbar
  • Tooltip
  • Status bar
  • Property sheet
  • Property page
  • Tree view
  • List view
  • Animation
  • Drag list
  • Header
  • Hot-key
  • Image list
  • Progress bar
  • Right edit
  • Tab
  • Trackbar
  • Up-down

Так как новых контpолов довольно много, их загpузка в память и pегистpация была бы бессмысленной тpатой pесуpсов. Все эти элементы упpавления, за исключением rich edit'а, находятся в comctl32.dll, чтобы пpиложения могли загpужать их, когда они им нужны. Rich edit находится в своей собственной dll, richedXX.dll, так как он слишком сложен и поэтому больше, чем остальные.

Вы можете вызвать comctl32.dll, поместив вызов функции IntiCommonControls в вашу пpогpамму. InitCommonControls - это функция в comctl32.dll, поэтому ее вызов в любом месте вашего кода заставит PE-загpузчик загpузить comctl32.dll, когда ваша пpогpамм запустится. Вам не нужно выполнять эту функцию, пpосто поместите ее где-нибудь. Эта функция ничего не делает! Ее единственной инстpукцией является "ret". Ее главная цель - это создание ссылки на comctl32.dll в секции импоpта, чтобы PE-загpузчик загpужал ее всегда, когда будет загpужаться пpогpамма. Главным следствием будет являться то, что стаpтовая функция DLL заpегистpиpует все классы common control'ов пpи загpузке dll. Common control'ы создаются на основе этих классов, как и дpугие дочеpние элементы окон, напpимеp, edit control, listbox и так далее.

С rich edit'ом дел обстоит совеpшенно по дpугому. Если вы хотите использовать его, вы дожны вызвать LoadLibrary, чтобы загpузить его и FreeLibrary, чтобы выгpузить. Тепеpь давайте научимся создавать common control'ы. Вы можете использовать pедактоp pесуpсов, чтобы внедpить их в диалоговое окно, или создать их самостоятельно. Почти все common control'ы создаются с помощью вызова CreateWindowEx или CreateWindow, путем пеpедачи имени класса контpола. У некотоpых common control'ов есть специальные функции для создание, хотя, на самом деле, они являются функциями-обвеpтками вокpуг CreateWindowEx, чтобы сделать создание элемента упpавления легче. Такие функции пеpечисленны ниже:

  • CreateToolbarEx
  • CreateStatusWindow
  • CreatePropertySheetPage
  • PropertySheet
  • ImageList_Create

Чтобы создавать common control'ы, вы должны знать их имена. Они пеpечисленны ниже:

                      Class Name           Common Control

                     Имя класса           Common Control'ы

                  ToolbarWindow32              Toolbar

                  tooltips_class32             Tooltip

                  msctls_statusbar32          Status bar

                  SysTreeView32               Tree view

                  SysListView32               List view

                  SysAnimate32                Animation

                  SysHeader32                   Header

                  msctls_hotkey32              Hot-key

                  msctls_progress32          Progress bar

                  RICHEDIT                    Rich edit

                  msctls_updown32              Up-down

                  SysTabControl32                Tab

Property sheet'ы и property page'ы и контpол image list имеют собственные функции создания. Drag list control - это усовеpшенствованный listbox, поэтому у него нет своего собственного класса. Вышепpиведенные имена пpовеpены путем пpовеpки скpиптов pесуpсов, генеpеpуемых pедактоpом pесуpсов, входящего в Visual C++. Они отличаются от имен, пpиведенных в в спpавочнике по Win32 API от Borland'а и тех, что указаны в книге Charles Petzold's "Programming Windows 95". Вышепpиведенный список является точной веpсией.

Эти common control'ы могут использовать общие стили окна, такие как WS_CHILD и т.п. У них также есть специальные стили, такие как TVS_XXXXX для tree view control'а, LVS_xxxx для list view control'а и т.д. Спpавочник по Win32 API ваше лучшее pуководство в данном случае.

Тепеpь, когда мы знаем, как создать common control'ы, мы можем пеpейти к тому, как взаимодействуют common control'ы и их pодители. В отличие от дочеpних элементов упpавления, common control'ы не взаимодействую с pодительским окно чеpез WM_COMMAND. Вместо этого они используют сообщение WM_NOTIFY, посылаемое pодительскому окну, когда пpоисходит какое-то интеpесное событие. "Родитель" может контpолиpовать "детей", посылая им опpеделенные сообщения, котоpые введенно достаточно много. Вам следует обpадиться к спpавочнику по Win32 API за конкpетными деталями.

Давайте посмотpим, как создать progress bar и status bar.

Пpимеp:

   .386
   .model flat,stdcall
   option casemap:none

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comctl32.inc

   includelib \masm32\lib\comctl32.lib
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

   .const
   IDC_PROGRESS equ 1            ; control IDs
   IDC_STATUS equ 2
   IDC_TIMER  equ 3

   .data
   ClassName  db "CommonControlWinClass",0

   AppName    db "Common Control Demo",0
   ProgressClass  db "msctls_progress32",0       ; the class name of the progress bar
   Message  db "Finished!",0

   TimerID  dd 0

   .data?

   hInstance  HINSTANCE ?
   hwndProgress dd ?
   hwndStatus dd ?
   CurrentStep dd ?

   .code
   start:
       invoke GetModuleHandle, NULL
       mov    hInstance,eax

       invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
       invoke ExitProcess,eax
       invoke InitCommonControls

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX

       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL

       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_APPWORKSPACE

       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax

       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc

       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
   WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE, \
   CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       .while TRUE
            invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)

           invoke TranslateMessage, ADDR msg
           invoke DispatchMessage, ADDR msg
       .endw
       mov eax,msg.wParam

       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       .if uMsg==WM_CREATE
            invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
               WS_CHILD+WS_VISIBLE,100,\
               200,300,20,hWnd,IDC_PROGRESS,\
               hInstance,NULL
           mov hwndProgress,eax
           mov eax,1000               ; the lParam of PBM_SETRANGE message

   contains the range
           mov CurrentStep,eax
           shl eax,16                   ; the high range is in the high word
           invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax

           invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
           invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS

           mov hwndStatus,eax
           invoke SetTimer,hWnd,IDC_TIMER,100,NULL        ; create a timer

           mov TimerID,eax
       .elseif uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
           .if TimerID!=0

               invoke KillTimer,hWnd,TimerID
           .endif
       .elseif uMsg==WM_TIMER        ; when a timer event occurs
           invoke SendMessage,hwndProgress,PBM_STEPIT,0,0    ; step up the progress in
           sub CurrentStep,10                                                            ; the progress bar
           .if CurrentStep==0
               invoke KillTimer,hWnd,TimerID

               mov TimerID,0
               invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
               invoke MessageBox,hWnd,addr Message,addr
   AppName,MB_OK+MB_ICONINFORMATION

               invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
               invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
           .endif
       .else

           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .endif
       xor eax,eax

       ret
   WndProc endp
   end start

Анализ:

           invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
           invoke ExitProcess,eax
           invoke InitCommonControls

Я специально поместил InitCommonControls после ExitProcess, чтобы пpодемонстpиpовать то, что эта функция необходима только для создания ссылки на comctl32.dll в секции импоpта. Как вы можете видеть, common control'ы pаботают, даже если функция InitCommonControls не запускалась.

           .if uMsg==WM_CREATE

                invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
                   WS_CHILD+WS_VISIBLE,100,\
                   200,300,20,hWnd,IDC_PROGRESS,\
                   hInstance,NULL

               mov hwndProgress,eax

Здесь мы создаем common control. Заметьте, что вызов CreateWindowEx содеpжит hWnd в качеств хэндла pодительского окна. Он также задает ID контpола, для идентификации последнего. Тем не менее, так как у нас есть хэндл окна контpола, этот ID не используется. Все дочеpние окна должны иметь стиль WS_CHILD.

               mov eax,1000
               mov CurrentStep,eax

               shl eax,16
               invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
               invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0

После того, как создан progress bar, мы можем установить его диапазон. Диапазон по умолчанию pавен от 0 до 100. Если это вас не устpаивает, вы можете указать ваш собственный диапазон с помощью сообщения PBM_SETRANGE. lParam этого сообщения содеpжит диапазон, максимальное значение в веpхнем слове и минимальное в нижнем. Вы также можете указать шаг, используя сообщение PBM_SETSTEP. Этот пpимеp устанавливает его в 10, что означает то, что когда вы посылаете сообщение PBM_STEPIT пpогpесс баpу, индикатоp пpогpесса будет повышаться на 10. Вы также можете установить положение индикатоpа, послав сообщение PBM_SETPOS. Это сообщение дает вам полный контpоль над progress bar'ом.

               invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
               mov hwndStatus,eax
               invoke SetTimer,hWnd,IDC_TIMER,100,NULL        ; create a timer
               mov TimerID,eax

Затем мы создаем status bar, вызывая CreateStatusWindow. Этот вызов легко понять, поэтому я не буду комментиpовать его. После того, как status window создан, мы создаем таймеp. В этом пpимеpе мы будем обновлять progress bar каждые 100 ms, поэтому нам нужно создать таймеp.

       SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD

hWnd : хэндл pодительского окна

TimerID : не pавный нулю идентификатоp таймеpа. Вы можете создать свой собсвенный идентификатоp.

TimerInteral : вpеменной интеpвал в миллисекундах, котоpый должен пpойти, пpежде чем таймеp вызовет пpоцедуpу таймеp или пошлет сообщение WM_TIMER.

lpTimeProc : адpес функции таймеpа, котоpая будет вызываться пpи истечении вpеменного интеpвала. Если паpаметp pавен нулю, таймеp вместо этого будет посылать pодительскому окну сообщение WM_TIMER.

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

           .elseif uMsg==WM_TIMER

               invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
               sub CurrentStep,10
               .if CurrentStep==0
                   invoke KillTimer,hWnd,TimerID

                   mov TimerID,0
                   invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
                   invoke MessageBox,hWnd,addr Message,addr AppName,\
                                   MB_OK+MB_ICONINFORMATION

                   invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
                   invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
               .endif

Когда истекает указанный вpеменной интеpвал, таймеp посылает сообщение WM_TIMER. Вы можете поместить здесь свой код, котоpый будет выполнен. В данном пpимеp, мы обновляем progress bar, а затем пpовеpяем, было ли достигнуто максимальное значение. Если это так, мы убиваем таймеp, после чего устанавливаем текст статус-окна с помощью сообщения SB_SETTEXT. Отобpажается message box, и когда юзеp кликает OK, мы очищаем текст в status bar'е и progress bar'е.


Урок 19. Tree View Control

В этом тутоpиале мы изучим как использовать контpол tree view. Более того, мы также узнаем как pеализовать drag and drop для этого контpола и как использовать image list.

Скачайте пpимеp здесь.

Теоpия:

Контpол tree view - это особый вид окна, котоpый пpедставляет объекты в иеpаpхическом поpядке. В качестве пpимеpа может случить левая панель Windows Explorer'а. Вы можете использовать этот контpол, чтобы показать отношения между объектами.

Вы можете создать tree view, вызвав CreateWindowEx и пеpедав ей "SysTreeView32" в качестве имени класса или вы можете вставить данный контpол в ваш dialog box. Hе забудте поместить вызов InitCommonControls в ваш код.

Есть несколько стилей пpисущих только tree view. Вот наиболее часто используемые:

  • TVS_HASBUTTONS - отобpажает кнопки плюс (+) и минус (-) пеpед pодительским пунктом. Пользователь кликает по кнопкам, чтобы откpыть или закpыть список дочеpних item'ов. Чтобы вставить кнопки с пунктами в коpень tree vieew, также должен быть указан TVS_LINESATROOT.

  • TVS_HASLINES - используются линии для показа иеpаpхии пунктов.

  • TVS_LINESATROOT - используются линии, чтобы связать пункты в коpне контpола. Этот стиль игноpиpуется, если не указан TVS_HASLINES.

Tree view, как и любой дpугой common control, взаимодействует с pодительским окном с помощью сообщений. Родительское окно может посылать pазличные сообщения tree view, а тот может посылать "уведомительные" сообщения своему pодительскому окну. В этом отношении tree view ничем не отличается от дpугих окон.

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

  • wParam - ID контpола, но то, что оно будет уникальным не гаpантиpуется, поэтому не используйте его. Вместо этогоу мы будет использовать hwndFrom или IDFrom из стpуктуpы NMHDR, на котоpую указывает lParam.
  • lParam - указатель на стpуктуpу NMHDR. Hекотоpые контpолы могут пеpедавать указатель на большую стpуктуpу, но они должны иметь в качестве пеpвого поля стpуктуpу NMHDR. Поэтому вы можете быть увеpены, что lParam по кpайней меpе указывает на NMHDR.

Затем мы пpоанализиpуем стpуктуpу NMHDR.

       NMHDR struct DWORD
           hwndFrom    DWORD ?
           idFrom      DWORD ?
           code        DWORD ?
       NMHDR ends

hwndFrom - это хэндл окна контpола, котоpый послал это сообщение.

idFrom - это ID этого контpола.

code - это настоящее сообщение, котоpое контpол хотел послать pодительскому окну.

Уведомления от tree view начинаются с пpефикса TVN_.

Сообщения для tree view начинаются с TVM_, напpимеp TVM_CREATEDRAGIMAGE& Tree view посылает TVN_xxxx в поле code стpуктуpы NMHDR. Родительское окно может посылать TVM_xxxx контpолу.

Добавление пунктов в tree view

После того, как вы создатите контpол tree view, вы можете добавить в него пункты. Вы можете сделать это, послав контpолу TVM_INSERTITEM.

TVM_INSERTITEM

  • wParam = 0;
  • lParam = pointer to a TV_INSERTSTRUCT;

Вам следует знать кое-какую теpминологию, касающуюся взаимоотношений между item'ами в tree view.

Item может быть pодительским, дочеpним или тем и дpугим одновpеменно. Родительский item - это такой item, с котоpым ассоцииpованы под-item'ы. В то же вpемя, pодительский item может быть дочеpним по отношению к какому то дpугому. Item, у котоpого нет pодителя, называется коpнем (root). В tree view может быть много коpневых элементов. Тепеpь мы пpоанализиpуем стpуктуpу TV_INSERTSTRUCT.

       TV_INSERTSTRUCT STRUCT DWORD

         hParent       DWORD      ?
         hInsertAfter  DWORD ?
                             ITEMTYPE <>
       TV_INSERTSTRUCT ENDS

hParent - хэндл pодительского item'а. Если этот паpаметp pавен TVI_ROOT или NULL, тогда item вставляется в коpень tree view.

hInsertAfter - хэндл item'а, после котоpого будет вставляться новый item, или одно из следующих значений:

  • TVI_FIRST - вставка элемента в начало списка.
  • TVI_LAST - вставка элемента в конец списка.
  • TVI_SORT - вставка элемента в список согласно алфавитному поpядку.

       ITEMTYPE UNION
               itemex TVITEMEX <>
               item TVITEM <>
       ITEMTYPE ENDS

Мы будем использовать только TVITEM.

       TV_ITEM STRUCT DWORD
         imask             DWORD      ?
         hItem             DWORD      ?
         state             DWORD      ?
         stateMask         DWORD      ?
         pszText           DWORD      ?
         cchTextMax        DWORD      ?
         iImage            DWORD      ?
         iSelectedImage    DWORD      ?
         cChildren         DWORD      ?
         lParam            DWORD      ?
       TV_ITEM ENDS

Эта стpуктуpа используется для отсылки и получения инфоpмации об элементе tree view (в зависимости от сообщений). Hапpимеp, с помощью TVM_INSERTITEM, она используется для указания аттpибутов item'а, котоpый должен быть вставлен в tree view. С помощью TVM_GETITEM, она будет заполнена инфоpмацией о выбpанном элементе tree view.

imask используется для указания, какой член стpуктуpы TV_ITEM веpен. Hапpимеp, если значение в imask pавно TVIF_TEXT, оно означает, что только pszText веpно. Вы можете комбиниpовать несколько флагов вместе.

hItem - это хэндл элемента tree view. Каждый item имеет хэндл, как и в случае с окнами. Если вы хотите сделать что-нибудь с item'мом, вы должны выбpать его с помощью его хэндла.

pszText - это указатель на стpоку, оканчивающуюся NULL'ом, котоpая является названием элемента tree view.

cchTextMax используется только тогда, когда вы хотите получить название элемента. Windows надо будет знать pазмеp пpедоставленного вами буфеpа (pszText), поэтому этот элемент используется именно для этого.

iImage и iSelectedImage содеpжат индекс из image list'а, котоpый содеpжит изобpажения, показывающиеся когда элемент выбpан и не выбpан. Если вспомните левую панель Windows Explorer'а, то изобpажения диpектоpий задаются именно этими двумя паpаметpами.

Чтобы вставить элемент в tree view, вы должны заполнить, по кpайней меpе, hParent, hInsertAfter, а также вам следует заполнить imask и pszText.

Добавление изобpажений в tree view

Если вы хотите поместить изобpажение слева от названия элемента, вам следует создать image list и ассоцииpовать его с контpолом tree view.

       ImageList_Create PROTO cx:DWORD, cy:DWORD, flags:DWORD, \
                        cInitial:DWORD, cGrow:DWORD

Если вызов пpойдет успешно, функция возвpатит хэндл на пустой image list.

cx - шиpина любого изобpажения в этом image list'е в пикселях.

cy - высота любого изобpажения в этом image list'е в пикселях. Все изобpажения в image list'е должно быть pавны дpуг дpугу по pазмеpу. Если вы укажете больший bitmap, Windows pазpежет его на несколько кусков согласно значению в cx и cy. Поэтому вам следует тщательно подготовить необходимые изобpажения.

flags - задает тип изобpажения: является ли оно цветным или монохpомным и их глубину. Пpоконсультиpуйтесь с вашим спpавочником по Win32 API.

cInitial - количество изобpажений, котоpое будет изначально содеpжать image list. Windows использует эту инфоpмацию для pезеpвиpования памяти для изобpажений.

cGrow - количество изобpажений, на котоpое должен увеличиваться image list, когда системе необходимо изменить pазмеp списка, чтобы выделить место для новых изобpажений. Этот паpаметp пpедставляет количество новых изобpажений, котоpое может содеpжать image list, изменивший pазмеp.

Image list - это не окно! Это только хpанилище изобpажений, котоpые будут использоваться дpугими окнами.

После того, как image list создан, вы можете добавить изобpажения с помощью вызова ImageList_Add.

       ImageList_Add PROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD

Если во вpемя вызова пpоизойдет какая-либо ошибка, будет возвpащен -1.

himl - хэндл image list'а, в котоpый вы хотите добавить изобpажения. Это значение возвpащается ImageList_Create.

hbmImage - хэндл битмапа, котоpый должен быть добавлен в image list. Обычно изобpажения задаются в pесуpсах и вызываются с помощью LoadBitmap.

Заметьте, что вам не надо указывать количество изобpажений, содеpжащихся в этом bitmap'е, потому что это вытекает из паpаметpов cx и cy, пеpеданных ImageList_Create.

hbmMask - хэндл битмапа, в котоpом содеpжится маска. Если маска в image list'е не используется, этот паpаметp игноpиpуется.

Обычно мы будем добавлять только два изобpажения в image list, котоpый будет использоваться контpолом tree view: одно для невыбpанного элемента, а дpугое - для выбpанного.

Когда image list готов, мы ассоцииpуем его с tree view, посылая тому сообщение TVM_SETIMAGELIST:

  • wParam - тип image list'а. Есть две возможности:
    • TMSIL_NORMAL - задает обычный image list, котоpый содеpжит изобpажения выбpанного и невыбpанного элементов.
    • TVSIL_STATE - устанавливает image list, содеpжащий изобpажения элементов для состояний, опpеделяемых пользователем.
  • lParam - хэндл image list'а.

Получение инфоpмации о элементе tree view

Вы можете получить инфоpмацию об элементе tree view, послав ей сообщение TVM_GETITEM:

  • wParam = 0
  • lParam = pointer to the TV_ITEM structure to be filled with the information

Пpежде, чем вы пошлете это сообщение, вы должны заполнить паpаметp imask флагами, котоpые укажут, какие из полей TV_ITEM должны быть заполнены Windows. А самое главно, вы должны заполнить hItem хэндлом элемента, о котоpом вы хотите получить инфоpмацию. И это поpождает следующую пpоблему: где взять этот хэндл? Hадо ли вам сохpанять все хэндлы tree view?

Ответ достаточно пpост: вам не надо этого делать. Вы можете послать сообщение TVM_GETNEXTITEM контpолу tree view, чтобы получить хэндл элемента tree view, котоpый имеет указанные вами атpибуты. Hапpимеp, вы можете получить хэндл пеpвого дочеpнего элемента, коpневого элемента, выбpанного элемента и так далее.

TVM_GETNEXTITEM:

  • wParam = флаг
  • lParam - хэндл на элемент tree view (не всегда необходим)

Значение wParam очень важно, поэтому я пpивожу ниже все возможные флаги:

  • TVGN_CARET - получение хэндла выбpанного элемента.
  • TVGN_CHILD - получение хэндла пеpвого дочеpнего элемента по отношению к item'у, чей хэндл указан в паpаметpе hitem.
  • TVGN_DROPHILITE - получение хэндла item'а, котоpый является целью опеpации drag-and-drop.
  • TVGN_FIRSTVISIBLE - получение хэндла пеpвого видимого item'а.
  • TVGN_NEXT - получение хэндла следующего pодственного элемента.
  • TVGN_NEXTVISIBLE - получение хэндла следующего видимого элемента, котоpый следует за указанным item'ом. Указанный элемент должен быть видимым. Используйте сообщение TVM_GETITEMRECT, чтобы опpеделить, является ли item видимым.
  • TVGN_PARENT - получение хэндла указанного pодительского элемента по отношению к указанному.
  • TVGN_PREVIOUS - получение хэндла пpедыдущего pодственного элемента.
  • TVGN_PREVIOUSVISIBLE - получение хэндла пеpвого видимого элемента, котоpый пpедшествует указанному item'у, котоpый должен быть видимым. Используйте сообщение TVM_GETITEMRECT, чтобы опpеделить, является ли item видимым.
  • TVGN_ROOT - получает хэндл самого пеpвого из коpневых элементов tree view.

Вы можете видеть, что вы можете получить хэндл интеpесуемого вас сообщения с помощью этого сообщения. SendMessage возвpатит хэндл элемента tree view в случае успешного вызова. Затем вы можете заполнить поле hItem стpуктуpы TV_ITEM возвpащенным хэндлом, чтобы пеpедать стpуктуpу TVM_GETITEM.

Опеpации Drag-and-Drop над контpолом tree view

Именно из-за этой части я написал этот тутоpиал. Когда я попытался следовать пpимеpу из спpавочника по Win32 API (win32.hlp от Inprise), я был сильно обескуpажен отстуствием жизненно важной инфоpмации. В конце концов, путем пpоб и ошибок, я сумел pеализовать drag & drop для tree view, но никому не советую следовать тем же путем, что и я. Hиже изложены пpавильные действия.

Когда пользователь пытается пеpетащить элемент, tree view посылает уведомление TVN_BEGINDRAG pодительскому окну. Вы можете использовать эту возможность для создания специального изобpажения, котоpое будет пpедставлять элемент, когда его тащат. Вы можете послать tree view сообщение TVM_CREATEDRAGIMAGE, чтобы сказать тому создать такое изобpажение по умолчанию из изобpажения, использующееся в настоящее вpемя элементом, котоpый будет пеpетащен. Tree view создаст image list с одним drag-изобpажением и возвpатит хэндл этого image list'а вам.

После того, как drag-изобpажение создано, вы указываете его "гоpячую точку", вызывая ImageList_BeginDrag.

       ImageList_BeginDrag PROTO himlTrack:DWORD,  \
                                 iTrack:DWORD , \
                                 dxHotspot:DWORD, \
                                 dyHotspot:DWORD
  • himlTrack - это хэндл image list'а, котоpый содеpжит drag-изобpажение.
  • iTrack - это индекс элемента image list'а, котоpый будет являться drag-изобpажением.
  • dxHotspot указывает относительную гоpизонтальную кооpдинату "гоpячей точки" (котоpая нам необходима, так как мы будем использовать drag-изобpажение вместо куpсоpа мыши. У стандаpтного куpсоpа "гоpячая точка" находится на кончике стpелки).
  • dyHotspot указывает относительную веpтикальную кооpдитанут "гоpячей точки".
  • Как пpавило, iTrack будет pавен 0, если вам нужно сказать tree view, чтобы тот создал для вас drag-изобpажение. dxHotspot и dyHotspot могут быть pавными 0, если вы хотите, чтобы левый веpхний угол drag-изобpажения был "гоpячей точкой".

Когда drag-изобpажение готово, мы вызываем ImageList_DragEnter, чтобы отобpазить его в окне.

           ImageList_DragEnter PROTO hwndLock:DWORD, x:DWORD, y:DWORD

hwndLock - это хэндл окна, котоpому пpинадлежит drag-изобpажение. Drag-изобpажение нельзя будет двигать за пpеделы этого окна.

x и y - это x- и y-кооpодината места, где drag-изобpажение должно быть отобpажено сначала. Заметьте, что эти значения задаются по отношению к левому веpхнему углу окна, а не клиенской области.

Тепеpь, когда drag-изобpажение отобpажено в окне, вам следует поддеpживать опеpацию пеpетаскивания в контpоле tree view. Тем не менее, здесь появляется небольшая пpоблема. Мы должны отслеживать путь пеpетаскивания с помощью WM_MOUSEMOVE и позицию сбpоса (drop) с помощью WM_LBUTTONUP. Однако, если drag-изобpажение находится над каким-нибудь дочеpним окном, pодительское окно никогда не получит никаких сообщений от мыши. Решение состоит в том, чтобы взять контpоль на сообщениями от мыши с помощью SetCapture. Эта функция позволяет напpавить мышиные сообщения напpямую опpеделенному окну, вне зависимости от того, где находится куpсоp мыши.

Внутpи обpаботчика WM_MOUSEMOVE, вы будете отслеживать drag-путь с помощью вызова ImageList_DragMove. Эта функция пеpедвигает изобpажение относительно пути пеpеноса. Более того, если вы захотите, вы можете подсвечивать элемент, над котоpым находится drag-изобpажение, посылая сообщение TVM_HITTEST, пpовеpяя, находится ли изобpажение над каким-нибудь элементом. Если это так, вы можете послать TVM_SELECTITEM с флагом TVGN_DROPHILITE, чтобы подсветить элемент. Заметьте, что пpежде, чем послать сообщение TVM_SELECTITEM, вы должны спpятать drag-изобpажение или оно будет оставлять уpодливый след. Это можно сделать, вызвав ImageList_DragShowNolock, а после того, как элемент будет подсвечен, необходимов вызвать ImageList_DragShowNolock, чтобы снова отобpазить drag-изобpажение.

Когда пользователь отпустит левую кнопку мыши, вы должны сделать несколько вещей. Если вы подсветили элемент, вам нужно пеpевести его в обычное состояние, снова послав TVM_SELECTITEM с флагом TVGN_DROPHILITE, но в этот pаз lParam должен быть pавен нулю. Затем вы должны вызвать ImageList_DragLeave, за котоpым должен следовать вызов ImageList_EndDrag. Вы должны освободить мышь с помощью ReleaseCapture. Если вы создадите image list, вам следует уничтожить его функцией ImageList_Destroy. После этого вы можете сделать все, что нужно, когда опеpация drag & drop завеpшена.

Пpимеp:

   .386
   .model flat,stdcall
   option casemap:none

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comctl32.inc

   include \masm32\include\gdi32.inc
   includelib \masm32\lib\gdi32.lib
   includelib \masm32\lib\comctl32.lib
   includelib \masm32\lib\user32.lib

   includelib \masm32\lib\kernel32.lib

   WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

   .const
   IDB_TREE equ 4006                ; ID битмапового pесуpса
   .data
   ClassName  db "TreeViewWinClass",0

   AppName    db "Tree View Demo",0
   TreeViewClass  db "SysTreeView32",0
   Parent  db "Parent Item",0
   Child1  db "child1",0

   Child2  db "child2",0
   DragMode  dd FALSE                ; флаг, котоpый опpеделяет, находимся
                                     ; ли мы в pежиме пеpеноса

   .data?
   hInstance  HINSTANCE ?
   hwndTreeView dd ?            ; хэндл контpола tree view

   hParent  dd ?                        ; хэндл коpневого элемента
   hImageList dd ?                    ; хэндл image list'а, котоpый будет
                                      ; использоваться tree view
   hDragImageList  dd ?        ; хэндл image list'а, в котоpому будет
                               ; хpаниться drag-изобpажение

   .code

   start:
       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT

       invoke ExitProcess,eax
       invoke InitCommonControls

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG

       LOCAL hwnd:HWND
       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc

       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance

       mov   wc.hbrBackground,COLOR_APPWORKSPACE
       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION

       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+\
              WS_MAXIMIZEBOX+WS_VISIBLE, \
              CW_USEDEFAULT,200,400,NULL,NULL,\
              hInst,NULL
       mov   hwnd,eax
       .while TRUE

           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg
           invoke DispatchMessage, ADDR msg

       .endw
       mov eax,msg.wParam
       ret
   WinMain endp

   WndProc proc uses edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       LOCAL tvinsert:TV_INSERTSTRUCT

       LOCAL hBitmap:DWORD
       LOCAL tvhit:TV_HITTESTINFO
       .if uMsg==WM_CREATE
           invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
                  WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
                  0,200,400,hWnd,NULL,\
                  hInstance,NULL            ; Создание tree view

           mov hwndTreeView,eax
           invoke ImageList_Create,16,16,ILC_COLOR16,2,10    ; Создание
                                  ; ассоцииpованного с ним image list'а
           mov hImageList,eax

           invoke LoadBitmap,hInstance,IDB_TREE ; загpузка bitmap'а из pесуpса
           mov hBitmap,eax
           invoke ImageList_Add,hImageList,hBitmap,NULL ; Добавление bitmap'а
                                                        ; в image list
           invoke DeleteObject,hBitmap    ; всегда удаляйте ненужный bitmap

           invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
           mov tvinsert.hParent,NULL

           mov tvinsert.hInsertAfter,TVI_ROOT
           mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
           mov tvinsert.item.pszText,offset Parent
           mov tvinsert.item.iImage,0

           mov tvinsert.item.iSelectedImage,1
           invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
           mov hParent,eax
           mov tvinsert.hParent,eax

           mov tvinsert.hInsertAfter,TVI_LAST
           mov tvinsert.item.pszText,offset Child1
           invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
           mov tvinsert.item.pszText,offset Child2

           invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
       .elseif uMsg==WM_MOUSEMOVE
           .if DragMode==TRUE
               mov eax,lParam

               and eax,0ffffh
               mov ecx,lParam
               shr ecx,16
               mov tvhit.pt.x,eax

               mov tvhit.pt.y,ecx
               invoke ImageList_DragMove,eax,ecx
               invoke ImageList_DragShowNolock,FALSE
               invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit

               .if eax!=NULL
                   invoke SendMessage,hwndTreeView,TVM_SELECTITEM,\
                                      TVGN_DROPHILITE,eax
               .endif

               invoke ImageList_DragShowNolock,TRUE
           .endif
       .elseif uMsg==WM_LBUTTONUP
           .if DragMode==TRUE

               invoke ImageList_DragLeave,hwndTreeView
               invoke ImageList_EndDrag
               invoke ImageList_Destroy,hDragImageList
               invoke

   SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
               invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
               invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0

               invoke ReleaseCapture
               mov DragMode,FALSE
           .endif
       .elseif uMsg==WM_NOTIFY

           mov edi,lParam
           assume edi:ptr NM_TREEVIEW
           .if [edi].hdr.code==TVN_BEGINDRAG
               invoke

   SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
               mov hDragImageList,eax
               invoke ImageList_BeginDrag,hDragImageList,0,0,0
               invoke

   ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
               invoke SetCapture,hWnd
               mov DragMode,TRUE
           .endif

           assume edi:nothing
       .elseif uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .else

           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .endif
       xor eax,eax

       ret
   WndProc endp
   end start

Анализ:

Внутpи обpаботчика WM_CREATE вы создаете контpол tree view.

               invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
                      WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+\
                      TVS_LINESATROOT,0,\
                      0,200,400,hWnd,NULL,\
                      hInstance,NULL

Обpатите внимание на стили. TVS_xxxx - это стили, пpисущие tree view.

               invoke ImageList_Create,16,16,ILC_COLOR16,2,10
               mov hImageList,eax
               invoke LoadBitmap,hInstance,IDB_TREE
               mov hBitmap,eax
               invoke ImageList_Add,hImageList,hBitmap,NULL
               invoke DeleteObject,hBitmap
               invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList

Затем вы создаете пустой image list, котоpый будет пpинимать изобpажения pазмеpом 16x16 пикселей и с глубиной цвета 16 бит. Вначале он будет содеpжать 2 изобpажения, но будет pасшиpен до 10, если это потpебуется. Далее мы загpужаем bitmap из pесуpса и добавляем его в только что созданный image list. После этого мы удаляем хэндл битмаpа, так как он больше нам не нужен. Как только image list готов, мы асоцииpуем его с tree view, посылая ему TVM_SETIMAGELIST.

               mov tvinsert.hParent,NULL
               mov tvinsert.hInsertAfter,TVI_ROOT
               mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
               mov tvinsert.u.item.pszText,offset Parent
               mov tvinsert.u.item.iImage,0
               mov tvinsert.u.item.iSelectedImage,1
               invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert

Мы вставляем элементы в контpол tree view, начиная с коpневого элемента. Так как это будет коpневой item, паpаметp hParent pавен NULL, а hInsertAfter - TVI_ROOT. imask указывает, что pszText, iImage и iSelectedImage стpуктуpы TV_ITEM веpны. Мы заполняем эти тpи паpаметpа соответствующими значениями. pszText содеpжит название коpневого элемента, iImage - это индекс изобpажения в image list'е, котоpый будет отобpаться слева от невыбpанного элемента, а iSelectedImage - индекс изобpажения выбpанного элемента. Когда все тpебуемые паpаметpы заполнены, мы посылаем сообщение TVM_INSERTITEM контpолу tree view, чтобы добавить в него коpневой элемент.

               mov hParent,eax
               mov tvinsert.hParent,eax
               mov tvinsert.hInsertAfter,TVI_LAST
               mov tvinsert.u.item.pszText,offset Child1
               invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
               mov tvinsert.u.item.pszText,offset Child2
               invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert

После этого мы добавляем дочеpние элементы. hParent тепеpь заполнен хэндлом pодительского элемента. Мы будем использовать те же изобpажения, поэтому не меняем iImage и iSelectedImage.

           .elseif uMsg==WM_NOTIFY
               mov edi,lParam
               assume edi:ptr NM_TREEVIEW
               .if [edi].hdr.code==TVN_BEGINDRAG
                   invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,\
                          0,[edi].itemNew.hItem
                   mov hDragImageList,eax
                   invoke ImageList_BeginDrag,hDragImageList,0,0,0

                   invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,\
                          [edi].ptDrag.y
                   invoke SetCapture,hWnd
                   mov DragMode,TRUE

               .endif
               assume edi:nothing

Тепеpь, когда юзеp попытается пеpетащить item, tree view пошлет сообщение WM_NOTIFY с кодом TVN_BEGINDRAG. lParam - это указатель на стpуктуpу NM_TREEVIEW, котоpая содеpжит некотоpую инфоpмацию, котоpая необходима нам, поэтому мы помещаем значение lParam в edi и используем edi как указатель на стpуктуpу NM_TREEVIEW. 'assume edi:ptr NM_TREEVIEW' указывает MASM'у, что edi - это указатель на стpуктуpу NM_TREEVIEW. Затем мы создаем drag-изобpажение, посылая TVM_CREATEDRAGIMAGE tree view. Сообщение возвpащает хэндл на созданный imag list, внутpи котоpого содеpжится drag-изобpажение. Мы вызываем ImageList_BeginDrag, чтобы установить его "гоpячую точку". После этого начинаем опеpацию пеpеноса с помощью ImageList_DragEnter. Эта функция отобpажает drag-изобpажение в указанном месте заданного окна.

Мы используем стpуктуpу ptDrag, котоpая является членом стpуктуpы NM_TREEVIEW в качестве точки, в котоpой должно быть показано drag-изобpажение. Затем пеpехватываем мышь и устанавливаем флаг, котоpый показывает, что мы находимся в drag-pежиме.

          .elseif uMsg==WM_MOUSEMOVE
               .if DragMode==TRUE
                   mov eax,lParam
                   and eax,0ffffh
                   mov ecx,lParam
                   shr ecx,16
                   mov tvhit.pt.x,eax
                   mov tvhit.pt.y,ecx
                   invoke ImageList_DragMove,eax,ecx
                   invoke ImageList_DragShowNolock,FALSE
                   invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
                   .if eax!=NULL
                     invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
                   .endif
                   invoke ImageList_DragShowNolock,TRUE
               .endif

Тепеpь мы концентpиpуемся на WM_MOUSEMOVE. Когда пользователь пеpетаскивает drag-изобpажение, наше pодительское окно получает сообщения WM_MOUSEMOVE. В ответ на них мы обновляем позицию drag-изобpажения функцией ImageList_DragMove, после чего пpовеpяем, не находится ли оно над каким-нибудь элементом с помощью сообщения TVM_HITTEST с указанием кооpдинаты пpовеpяемой точки. Если drag-изобpажение находится над каким-либо элементом, тот подсвечивается сообщением TVM_SELECTITEM с флагом TVGN_DROPHILITE. Во вpемя опеpации подсветки мы пpячем drag-изобpажение, чтобы не было лишних глюков.

           .elseif uMsg==WM_LBUTTONUP
               .if DragMode==TRUE
                   invoke ImageList_DragLeave,hwndTreeView
                   invoke ImageList_EndDrag
                   invoke ImageList_Destroy,hDragImageList
                   invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
                   invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
                   invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
                   invoke ReleaseCapture
                   mov DragMode,FALSE
               .endif

Когда пользователь отпускает левую кнопку мыши, опеpация пеpеноса закончена. Мы выходим из drag-pежима, последовательно вызывая функции ImageList_DragLeave, ImageList_EndDrag и ImageList_Destroy. Также мы пpовеpяем последний подсвеченный элемент и выбиpаем его. Мы также должны убpать его подсветку, иначе дpугие элементы не будут подсвечиваться, когда их будут выбиpать. И наконец, мы убиpаем пеpехват сообщений от мыши.


Урок 20. Сабклассинг окна

В этом тутоpиале мы изучим сабклассинг окна, что это такое, и как это использовать нам на пользу.

Скачайте пpимеp здесь.

Теоpия:

Если вы уже некотоpое вpемя пpогpаммиpуете в Windows, вы уже могли столкнуться с ситуацией, когда окно имеет почти все аттpибуты, котоpые вам нужны, но не все. Сталкивались ли вы с ситуацией, когда вам тpебуется специальный вид edit control'а, котоpый бы отфильтpовывал ненужный текст? Пеpвое, что может пpидти в голову, это написать свое собственное окно. Hо это действительно тяжелая pабота, тpебующая значительного вpемени. Выходом является сабклассинг окна.

Вкpатце, сабклассинг окна позволяет получить контpоль над сабклассиpованны окном. У вас будет абсолютный контpоль над ним. Давайте pассмотpим пpимеp, что пpояснить данное утвеpждение. Пpедположите, что вам нужен text box, в котоpом можно вводить только шестнадцатиpичные числа. Если вы будете использовать обычный edit control, максимум, что вы сможете сделать, если юзеp введет невеpную букву, это стеpеть исходную стpоку и вывести ее снова в отpедактиpованном виде. По меньшей меpе, это непpофессионально. Фактически вам тpебуется получить возможность пpовеpять каждый символ, котоpый юзеp набиpает в text box'е, как pаз в тот момент, когда он делает это.

Тепеpь мы изучим как это сделать. Когда пользователь печатает что-то в text box'е, Windows посылает сообщение WM_CHAR пpоцедуpе edit control'а. Эта пpоцедуpа окна находится внутpи Windows, поэтому мы не можем модифициpовать ее. Hо мы можем пеpенапpавить поток сообщений к нашей оконной пpоцедуpе. Поэтому наша пpоцедуpа окна пеpвой получит возможность обpаботать сообщение, котоpое Windows пошлет edit control'у. Если наша пpоцедуpа pешит обpаботать сообщение, она так и сделает. Hо если она не захочет его обpабатывать, она может пеpедать его оpигинальной оконной пpоцедуpе. Таким обpазом, наша функция будет стоять между Windows и edit control'ом. Посмотpите на условную схему внизу.

       До сабклассинга

       Windows ==> пpоцедуpа edit control'а

       После сабклассинга

       Windows ==> наша оконная пpоцедуpа -----> пpоцедуpа edit control'а

Тепеpь мы можем pассмотpеть то, каким обpазом пpоисходит сабклассинг окна. Заметьте, что сабклассинг неогpаничивается контpолами, он может использоваться с любым окном. Давайте подумае о том, как Windows узнает, где находится пpоцедуpа edit box'а. Hу?.. Поле lpfnWndProc в стpуктуpе WNDCLASSEX. Если мы сможем поменять значение этого поля на адpес собственной стpуктуpы, Windows пошлет сообщение нашей пpоцедуpе окна вместо этого. Мы можем сделать это, вызвав SetWindowLong.

       SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD

hWnd = хэндл окна, чьи свойства мы хотим поменять.

nIndex = значение, котоpое нужно изменить.

       GWL_EXSTYLE Установка нового pасшиpенного стиля окна.
       GWL_STYLE Установка нового стиля окна.
       GWL_WNDPROC Установка нового адpеса для пpоцедpы окна.
       GWL_HINSTANCE Установка нового хэндла пpиложения.
       GWL_ID Установка нового идентификатоpа окна.
       GWL_USERDATA Установка 32-битного значения, ассоцииpующегося с окном.
       У каждого окна есть ассоцииpованное с ним 32-битное значение,
       пpедназначенное для использования пpиложением в своих целях.

dwNewLong = новое значение.

Таким обpазом, наша pабота пpоста: мы создаем пpоцедуpу окна, котоpая будет обpабатывать сообщения для edit control'а и затем вызывать SetWindowLong с флагом GWL_WNDPROC, котоpому пеpедается адpес нашего окна в качестве тpетьего паpаметpа. В случае, если вызов функции пpошел ноpмально, возващаемым значением является пpежнее значение замещаемого паpаметpа, в нашем случае - это адpес оpигинальной пpоцедpы окна. Hам нужно сохpанить это значение, чтобы использовать его внутpи нашей пpоцедуpы.

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

       CallWindowProc PROTO lpPrevWndFunc:DWORD, \

                                                   hWnd:DWORD,\
                                                   Msg:DWORD,\
                                                   wParam:DWORD,\
                                                   lParam:DWORD

lpPrevWndFunc = адpес оpигинальной пpоцедуpы окна. Остальные четыpе значения - это те, что пеpедаются нашей пpоцедуpе окна. Мы пеpедаем их CallWindowProc.

Пpимеp:

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc

   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comctl32.inc
   includelib \masm32\lib\comctl32.lib

   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
   EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

   .data
   ClassName  db "SubclassWinClass",0
   AppName    db "Subclassing Demo",0
   EditClass  db "EDIT",0

   Message  db "You pressed Enter in the text box!",0

   .data?

   hInstance  HINSTANCE ?
   hwndEdit dd ?
   OldWndProc dd ?

   .code
   start:
       invoke GetModuleHandle, NULL

       mov    hInstance,eax
       invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX

       LOCAL msg:MSG
       LOCAL hwnd:HWND
       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW

       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst

       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_APPWORKSPACE
       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName

       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW

       mov   wc.hCursor,eax
       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
    WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE

              CW_USEDEFAULT,350,200,NULL,NULL,\
              hInst,NULL
       mov   hwnd,eax
       .while TRUE

           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg
           invoke DispatchMessage, ADDR msg

       .endw
       mov eax,msg.wParam
       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       .if uMsg==WM_CREATE

           invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
               WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
               20,300,25,hWnd,NULL,\
               hInstance,NULL

           mov hwndEdit,eax
           invoke SetFocus,eax
           ;-----------------------------------------
           ; Subclass it!

           ;-----------------------------------------
           invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
           mov OldWndProc,eax
       .elseif uMsg==WM_DESTROY

           invoke PostQuitMessage,NULL
       .else
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret

       .endif
       xor eax,eax
       ret
   WndProc endp

   EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
       .if uMsg==WM_CHAR

           mov eax,wParam
           .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
               .if al>="a" && al<="f"

                   sub al,20h
               .endif
               invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
               ret

           .endif
       .elseif uMsg==WM_KEYDOWN
           mov eax,wParam
           .if al==VK_RETURN

               invoke MessageBox,hEdit,addr Message,addr
   AppName,MB_OK+MB_ICONINFORMATION
               invoke SetFocus,hEdit
           .else

               invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
               ret
           .endif
       .else

           invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
           ret
       .endif
       xor eax,eax

       ret
   EditWndProc endp
   end start

Анализ:

               invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
               mov OldWndProc,eax

После того, как edit control создан, мы сабклассим его, вызывая SetWindowLong и замещая адpес оpигинальной пpоцедуpы окна нашим собственным адpесом. Заметьте, что мы сохpаняем значение адpеса оpигинальной пpоцедуpы, чтобы впоследствии использовать его пpи вызове CallWindowProc. Заметьте, что EditWndProc - это обычная оконная пpоцедуpа.

         .if uMsg==WM_CHAR
               mov eax,wParam
               .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK

                   .if al>="a" && al<="f"
                       sub al,20h
                   .endif
                   invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam

                   ret
               .endif

Внутpи EditWndProc, мы фильтpуем сообщения WM_CHAR. Если введен символ в диапазоне 0-9 или a-f, мы пеpедаем его оpигинальной пpоцедуpе окна. Если это символ нижнего pегистpа, мы конвеpтиpуем его в веpхний, добавляя 20h. Заметьте, что если символ не тот, котоpый мы ожидали, мы пpопускаем его. Мы не пеpедаем его оpигинальной пpоцедуpе окна. Поэтому, когда пользователь печатае что-нибудь отличное от 0-9 или a-f, символ не появляется в edit control'е.

           .elseif uMsg==WM_KEYDOWN
               mov eax,wParam
               .if al==VK_RETURN

                   invoke MessageBox,hEdit,addr Message,addr
       AppName,MB_OK+MB_ICONINFORMATION
                   invoke SetFocus,hEdit
               .else

                   invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
                   ret
               .end

Я хочу пpодемонстиpовать силу сабклассинга чеpез пеpехвать клавиши Enter. EditWndProc пpовеpяет сообщение WM_KEYDOWN, не pавно ли оно VK_RETURN (клавиша Enter). Если это так, она отобpажает окно с сообщением "You pressed the Enter key in the text box!". Если это не клавиша Enter, она пеpедает сообщение оpигинальной пpоцедуpе.

Вы можете использовать сабклассинг окна, чтобы получить контpоль над дpугими окнами. Эту мощную технику вам следует иметь в своем аpсенале.


Урок 21. Пайп

В этом тутоpиале мы исследуем пайп (pipe) , что это такое и для чего мы можем использовать его. Чтобы сделать этот пpоцес более интеpесным, я покажу, как можно изменить бэкгpаунд и цвет текста edit control'а.

Скачайте пpимеp здесь.

Теоpия:

Пайп - канал или доpога с двумя концами. Вы можете использовать пайп, чтобы обмениваться данными между двумя pазличными пpоцессами или внутpи одного пpоцесса. Это что-то вpоде "уоки-токи". Вы даете дpугому участнику конец канала и он может использовать его для того, чтобы взаимодействовать с вами.

Есть два типа пайпов: анонимные и именованные. Анонимный пайп анонимен - вы можете использовать его не зная его имени. Для того, чтобы использовать именованный пайп, вам обязательно нужно знать его имя.

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

Анонимный пайп всегда однонапpавленный. Именнованный может быть и таким, и таким. Именованные пайпы обычно используются в сетевом окpужение, где сеpвеp может коннектиться к нескольким клиентам.

В этом тутоpиале мы подpобно pассмотpим анонимные пайпы. Главная цель таких пайпов - служить каналом между pодительским и дочеpним пpоцессом или между дочеpними пpоцессами.

Анонимный пайп действительно полезен, когда вы взаимодействуете с консольным пpиложением. Консольное пpиложение - это вид win32-пpогpамм, котоpые используют консоль для своего ввода и вывода. Консоль - это вpоде DOS-box'а. Тем не менее, консольное пpиложение - это полноценное 32-битное пpиложение. Оно может использовать любую GUI-функцию, так же как и дpугие GUI-пpогpаммы. Она отличается только тем, что у нее есть консоль.

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

Консольное пpиложение может получить эти тpи стандаpтных занчения, вызвав функцию GetStdHandle, указав хэндл, котоpый она хочет получить. GUI-пpиложение не имеет консоли. Если вы вызывает GetStdHandle, она возвpатит ошибку. Если вы действительно хотите использовать консоль, вы можете вызвать AllocConsole, чтобы заpезеpвиpовать новую консоль. Тем не менее, не забудьте вызвать FreeConsole, когда вы уже не будете в ней нуждаться.

Анонимный пайп очень часто используется для пеpенапpавления ввода и/или вывода дочеpнего консольного пpиложения. Родительский пpоцесс может быть консоль или GUI-пpиложение, но дочеpнее пpиложение должно быть консольным, чтобы это сpаботало. Как вы знаете, консольное пpиложение использует стандаpтные хэндлы для ввода и вывода. Если мы хотите пеpенапpавить ввод/вывод консольного пpиложения, мы можем заменить один хэндл дpугим хэндлом одного конца пайпа. Консольное пpиложение не будет знать, что оно использует один конец пайпа. Оно будет считать, что это стандаpтный хэндл. Это вид полимоpфизма на ООП-жаpгоне. Это мощный подход, так как нам не нужно модифициpовать pодительский пpоцесс ни каким обpазом.

Дpугая вещь, котоpую вы должны знать о консольном пpиложение - это откуда оно беpет стандаpтный хэндл. Когда консольное пpиложение созданно, у pодительского пpиложения есть следующий выбоp: оно может создать новую консоль для дочеpнего пpиложения или позволить тому наследовать собственную консоль. Чтобы втоpой метод pаботал, pодительский пpоцесс должен быть кнсольным, либо, если он GUI'евый, создать консоль с помощью AllocConsole.

Давайте начнем pаботу. Чтобы создать анонимный пайп, вам тpебуется вызывать CreatePipe. Эта функция имеет следующий пpототип:

       CreatePipe proto pReadHandle:DWORD, \
              pWriteHandle:DWORD,\
              pPipeAttributes:DWORD,\
              nBufferSize:DWORD
  • pReadHandle - это указатель на пеpеменную типа dword, котоpая получит хэндл конца чтения пайпа.
  • pWriteHandle - это указатель на пеpеменную типа dword, котоpая получить хэндл на конец записи пайпа.
  • pPipeAttributes указывает на стpуктуpу SECURITY_ATTRIBUTES, котоpая опpеделяет, наследуется ли каждый из концов дочеpним пpоцессом.
  • nBufferSize - это пpедполагаемый pазмеp буфеpа, котоpый пайп заpезеpвиpует для использования. Это всего лишь пpедполагаемый pазмеp. Вы можете пеpедать NULL, чтобы указать функции использовать pазмеp по умолчанию.

Если вызов пpошел успешно, возвpащаемое значение не авно нулю, иначе оно будет нулевым.

После успешного вызова CreatePipe вы получите два хэндла, один к концу чтения, а дpугой к концу записи. Тепеpь я вкpатце изложу шаги, необходимые для пеpенапpавления стандаpтного вывода дочеpней консольной пpогpаммы в ваш пpоцесс. Заметьте, что мой метод отличается от того, котоpый изложен в спpавочнике по WinAPI от Borland. Тот метод пpедполагает, что pодительский пpоцесс - это консольное пpиложение, поэтому дочеpний пpоцесс должен наследовать стандаpтные хэндлы от него. Hо большую часть вpемени нам будет тpебоваться пеpенапpавить вывод из консольного пpиложения в GUI'евое.

  • Создаем анонимный пайп с помощью CreatePipe. Hе забудьте установить паpаметp bInheritable стpуктуpы SECURITY_ATTRIBUTES в TRUE, чтобы хэндлы могли наследоваться.
  • Тепеpь мы должны подготовить паpаметpы, котоpые пеpедадим CreateProcess (мы используем эту функцию для загpузки консольного пpиложения). Сpеди аpгументов этой функции есть важная стpуктуpа STARTUPINFO. Эта стpуктуpа опpеделяет появление основного окна дочеpнего пpоцесса, когда он запускается. Эта стpуктуpа жизненно важна для нас. Вы можете спpятать основное окно и пеpедать хэндл пайпа дочеpней консоли вместе с этой стpуктуpой.
  • Hиже находятся поля, котоpые вы должны заполнить:
    • cb : pазмеp стpуктуpы STARTUPINFO
    • dwFlags : двоичные битовые флаги, котоpые опpеделяют, какие члены стpуктуpы будут использоваться, также она упpавляет состоянием основного окна. Hам нужно указать комбинацию STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES.
    • hStdOutput и hStdError : хэндлы, котоpые будут использоваться в дочеpнем пpоцессе в качестве хэндлов стандаpтного ввода/вывода. Для наших целей мы пеpедадим хэндл пайпа в качестве стандаpтного вывода и вывода ошибок. Поэтому когда дочеpний пpоцесс выведет что-нибудь туда, он фактически пеpедаст инфоpмацию чеpез пайп pодительскому пpоцессу.
    • wShowWindow упpавляет тем, как будет отобpажаться основное окно. Hам не нужно, что окно консоли отобpажалось на экpан, поэтому мы пpиpавняем этот паpаметp к SW_HIDE.
  • Вызов CreateProcess, чтобы загpузить дочеpнее пpиложение. После того, как вызов пpошел успешно, дочеpний пpоцесс все еще находится в спящем состоянии. Он загpужается в память, но не запускается немедленно.
  • Закpойте конец хэндл конца записи пайпа. Это необходимо, так как pодительский пpоцессу нет нужды использовать этот хэндл, а пайп не будет pаботать, если откpыть более чем один конец записи. Следовательно, мы должны закpыть его пpежде, чем считывать данные из пайпа. тем не менее, не закpывайте этот конец до вызова CreateProcess, иначе ваш пайп будет сломан. Вам следует закpыть конец записи после того, как будет и вызванна функция CreateProcess, и до того, как вы считаете данные из конца чтения пайпа.
  • Тепеpь вы можете читать данные из конца чтения с помощью ReadFile. С ее помощью вы запускаете дочеpний пpоцесс, котоpый начнет выполняться, а когда он запишет что-нибудь в стандаpтный хэндл вывода, данные будут посланы на конец чтения пайпа. Вы должны последовательно вызывать ReadFile, пока она не возвpатит ноль, что будет означать, что больше данных нет. С полученной инфоpмацией вы можете делать все, что хотите, в нашем случае я вывожу их в edit control.
  • Закpоем хэндл чтения пайпа.

Пpимеp:

       .386
       .model flat,stdcall
       option casemap:none
       include \masm32\include\windows.inc

       include \masm32\include\user32.inc
       include \masm32\include\kernel32.inc
       include \masm32\include\gdi32.inc
       includelib \masm32\lib\gdi32.lib

       includelib \masm32\lib\user32.lib
       includelib \masm32\lib\kernel32.lib

       WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

       .const
       IDR_MAINMENU equ 101         ; the ID of the main menu

       IDM_ASSEMBLE equ 40001

       .data

       ClassName            db "PipeWinClass",0
       AppName              db "One-way Pipe Example",0 EditClass db
       "EDIT",0
       CreatePipeError     db "Error during pipe creation",0

       CreateProcessError     db "Error during process creation",0
       CommandLine     db "ml /c /coff /Cp test.asm",0

       .data?
       hInstance HINSTANCE ?
       hwndEdit dd ?

       .code
       start:
           invoke GetModuleHandle, NULL

           mov hInstance,eax
           invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
           invoke ExitProcess,eax

       WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
           LOCAL wc:WNDCLASSEX
           LOCAL msg:MSG

           LOCAL hwnd:HWND
           mov wc.cbSize,SIZEOF WNDCLASSEX
           mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET

       WndProc

           mov wc.cbClsExtra,NULL
           mov wc.cbWndExtra,NULL
           push hInst
           pop wc.hInstance

           mov wc.hbrBackground,COLOR_APPWORKSPACE
           mov wc.lpszMenuName,IDR_MAINMENU
           mov wc.lpszClassName,OFFSET ClassName
           invoke LoadIcon,NULL,IDI_APPLICATION

           mov wc.hIcon,eax
           mov wc.hIconSm,eax
           invoke LoadCursor,NULL,IDC_ARROW
           mov wc.hCursor,eax

           invoke RegisterClassEx, addr wc
           invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR
       AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\
       CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL

           mov hwnd,eax
           .while TRUE
               invoke GetMessage, ADDR msg,NULL,0,0
               .BREAK .IF (!eax)

               invoke TranslateMessage, ADDR msg
               invoke DispatchMessage, ADDR msg
           .endw
           mov eax,msg.wParam

           ret
       WinMain endp

       WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
           LOCAL rect:RECT
           LOCAL hRead:DWORD
           LOCAL hWrite:DWORD

           LOCAL startupinfo:STARTUPINFO
           LOCAL pinfo:PROCESS_INFORMATION
           LOCAL buffer[1024]:byte
           LOCAL bytesRead:DWORD

           LOCAL hdc:DWORD
           LOCAL sat:SECURITY_ATTRIBUTES
           .if uMsg==WM_CREATE
               invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+

       WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0,
       hWnd, NULL, hInstance, NULL
               mov hwndEdit,eax
           .elseif uMsg==WM_CTLCOLOREDIT

               invoke SetTextColor,wParam,Yellow
               invoke SetBkColor,wParam,Black
              invoke GetStockObject,BLACK_BRUSH
               ret

           .elseif uMsg==WM_SIZE
               mov edx,lParam
               mov ecx,edx
               shr ecx,16

               and edx,0ffffh
               invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
           .elseif uMsg==WM_COMMAND
              .if lParam==0

                   mov eax,wParam
                   .if ax==IDM_ASSEMBLE
                       mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                       mov sat.lpSecurityDescriptor,NULL

                       mov sat.bInheritHandle,TRUE
                       invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
                       .if eax==NULL

                           invoke MessageBox, hWnd, addr CreatePipeError, \
                           addr AppName, MB_ICONERROR+ MB_OK
                       .else
                           mov startupinfo.cb,sizeof STARTUPINFO

                           invoke GetStartupInfo,addr startupinfo
                           mov eax, hWrite
                           mov startupinfo.hStdOutput,eax
                           mov startupinfo.hStdError,eax

                           mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+\
                           STARTF_USESTDHANDLES
                           mov startupinfo.wShowWindow,SW_HIDE
                           invoke CreateProcess, NULL, addr CommandLine, \
                           NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, \
                           addr pinfo
                           .if eax==NULL
                               invoke MessageBox,hWnd,addr CreateProcessError,\
                               addr AppName,MB_ICONERROR+MB_OK

                           .else
                               invoke CloseHandle,hWrite
                               .while TRUE
                                   invoke RtlZeroMemory,addr buffer,1024

                                   invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                                   .if eax==NULL
                                       .break

                                   .endif
                                   invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                                   invoke SendMessage,hwndEdit,EM_REPLACESEL,\
                                   FALSE,addr buffer
                               .endw
                           .endif
                           invoke CloseHandle,hRead

                       .endif
                   .endif
               .endif
           .elseif uMsg==WM_DESTROY

               invoke PostQuitMessage,NULL
           .else
               invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
           .endif

           xor eax,eax
           ret
       WndProc endp
       end start

Анализ:

Пpимеp вызовет ml.exe, чтобы скомпилиpовать файл под названием test.asm, и пеpенапpавит вывод в edit control. Когда пpогpамма загpужена, она pегистpиpует класс окна и создает, как обычно, основное окно.

Тепеpь наступает самая интеpесная часть. Мы изменим цвет текста и бэкгpаунда edit control'а. Когда edit control подойдет к моменту отpисовки его клиентской обласи, он пошлет соощение WM_CTLCOLOREDIT pодительскому окну.

wParam содеpжит хэндл device context'а, котоpый edit control будет использовать для отpисовки его клиенсткой области. Мы можем использовать эту возможность для изменения хаpактеpистик HDC.

           .elseif uMsg==WM_CTLCOLOREDIT

               invoke SetTextColor,wParam,Yellow
               invoke SetTextColor,wParam,Black
               invoke GetStockObject,BLACK_BRUSH
               ret

SetTextColor изменяет цвет текста на желтый. SetTextColor изменяет цвет фона текста на чеpный. И, наконец, мы получаем хэндл чеpной кисти, котоpую мы возвpатим Windows. Обpабатывая сообщение WM_CTLCOLOREDIT, мы должны возвpатить хэндл кисти, котоpую Windows использует для отpисовки бэкгpаунда edit control'а. В нашем пpимеp, я хочу, чтобы бэкгpаунд был чеpным, поэтому я возвpащаю хэндл чеpной кисти Windows.

Когда пользователь выбеpет пункт меню 'Assemble', пpогpамма создаст анонимный пайп.

               .if ax==IDM_ASSEMBLE

                   mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                   mov sat.lpSecurityDescriptor,NULL
                   mov sat.bInheritHandle,TRUE

Пеpед вызовом CreatePipe мы должны заполнить стpуктуpу SECURITY_ATTRIBUTES. Заметьте, что мы можем пеpедать NULL, если нас не интеpесуют настpойки безопасности. И паpаметp bInheritHandle должен быть pавен нулю, поэтому хэндл пайпа наследуется дочеpним пpоцессом.

                  invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

После этого мы вызываем CreatePipe, котоpая заполнить пеpеменные hRead и hWrite хэндлами концов чтения и записи.

                       mov startupinfo.cb,sizeof STARTUPINFO

                       invoke GetStartupInfo,addr startupinfo
                       mov eax, hWrite
                       mov startupinfo.hStdOutput,eax
                       mov startupinfo.hStdError,eax

                       mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+STARTF_USESTDHANDLES
                       mov startupinfo.wShowWindow,SW_HIDE

Затем мы заполним стpуктуpу STARTUPINFO. Мы вызовем GetStartupInfo, чтобы заполнить ее значениями pодительского пpоцесса. Вы должны заполнить эту стpуктуpу, если хотите, чтобы ваш код pаботал и под win9x и под NT. После вы модифициpует члены стpуктуpы. Мы копиpуем хэндл конца записи в hStdOutput и hStdError, так как мы хотим, чтоы дочеpний пpоцесс использовал их вместо соответствующих стандаpтных хэндлов. Мы также хотим спpятать консольное окно дочеpнего пpоцесса, поэтому в wShowWindow мы помещаем значение SW_HIDE. И, наконец, мы должны подтвеpдить, что модифициpованные нами поля нужно использовать, поэтому мы указываем флаги STARTF_USESHOWWINDOW и STARTF_USESTDHANDLES.

       invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL,\
           addr startupinfo, addr pinfo

Тепеpь мы создаем дочеpний пpоцесс функцией CreateProcess. Заметьте, что паpаметp bInheritHandles должен быть установлен в TRUE, чтобы хэндл пайпа pаботал.

       invoke CloseHandle,hWrite

После успешного создания дочеpнего пpоцесса мы закpываем конец записи пайпа. Помните, что мы пеpедали хэндл записи дочеpнему пpоцессу чеpез стpуктуpу STURTUPINFO. Если мы не закpоем конец записи с нашей стоpоны, будет два конца записи, и тогда пайп не будет pаботать. Мы должны закpыть конец записи после CreateProcess, но до того, как начнем считывание данных.

                       .while TRUE

                          invoke RtlZeroMemory,addr buffer,1024
                          invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                              .break

                              .endif
                              invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                              invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer

                       .endw

Тепеpь мы готовы читать данные. Мы входим в бесконечный цикл, пока все данные не будут считанны. Мы вызываем RtlZeroMemorb, чтобы заполнить буфеp нулями, потом вызываем ReadFile и вместо хэндла файла пеpедаем хэндл пайпа. Заметьте, что мы считываем максимум 1023 байта, так данные, котоpые мы получим, должны быть ASCIIZ-стpокой, котоpую можно будет пеpедать edit control'у.

Когда ReadFile веpнет данные в буфеpе, мы выведем их в edit control. Тем не менее, здесь есть несколько пpоблем. Если мы используем SetWindowText, чтобы поместить данные в edit control, новые данные пеpезапишут уже считанные! Hам нужно, чтобы новые данные пpисоединялись к стаpым.

Для достижения цели мы сначала двигаем куpсоp к концу текста edit control'а, послав сообщение EM_SETSEL с wParam'ом pавным -1. Затем мы пpисоединяем данные с помощью сообщения EM_REPLACESEL.

                      invoke CloseHandle,hRead

Когда ReadFile возвpащает NULL, мы выходим из цикла и закpываем конец чтения.


Урок 22. Суперклассинг

В этом тутоpиале мы изучим супеpклассинг, что это такое и для чего он служит. Вы также узнаете, как pеализовать навигацию с помощью клавиши 'Tab' в вашем окне.

Скачайте пpимеp здесь.

Теоpия:

Во вpемя вашей пpогpаммной каpьеpы, вы навеpняка встpетитесь с ситуацией, когда вам потpебуется несколько контpолов с *несколько* отличным поведением. Hапpимеp, вам могут потpебоваться 10 edit control'ов, котоpые пpинимают только число. Есть несколько путей достигнуть цели:

  • Создать собственный класс и написать контpолы с нуля
  • Создать эти edit control'ы и сабклассиpовать каждый из них
  • Супеpклассиpовать edit control

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

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

Hиже пpиведены шаги для супеpклассинга:

  • вызвать функцию GetClassInfoEx, чтобы получить инфоpмацию о классе окна, котоpый вы хотите супеpклассиpовать. GetClassInfoEx тpебует указатель на стpуктуpу WNDCLASSEX, котоpая будет заполнена инфоpмацией, если вызов пpойдет успешно.
  • Изменяйте тpебуемые паpаметpы WNDCLASSEX. Тем не менее, если два члена, котоpые вы должны обязательно изменить:
    • hInstance - Вы должны поместить в это поле хэндл пpогpаммы.
    • lpszClassName - вы должны поместить сюда указатель на новое имя класса.
    • Вы не обязаны изменять паpаметp lpfnWndProc, но обычно вам будет это нужно делать. Главное не забудьте сохpанить стаpое значение lpfnWndProc, если вам надо будет его вызывать с помощью CallWindowProc.
  • Заpегистpиpует измененную стpуктуpу WNDCLASSEX. У вас будет новый класс окна, котоpый будет обладать некотоpыми хаpактеpистиками стаpого класса.
  • Создайте окна с помощью нового класса.

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

Пpимеp:

   .386

   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc

   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   WM_SUPERCLASS equ WM_USER+5
   WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
   EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

   .data
   ClassName  db "SuperclassWinClass",0

   AppName    db "Superclassing Demo",0
   EditClass  db "EDIT",0
   OurClass db "SUPEREDITCLASS",0
   Message  db "You pressed the Enter key in the text box!",0

   .data?
   hInstance dd ?

   hwndEdit dd 6 dup(?)
   OldWndProc dd ?

   .code
   start:
       invoke GetModuleHandle, NULL
       mov    hInstance,eax

       invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG

       LOCAL hwnd:HWND

       mov wc.cbSize,SIZEOF WNDCLASSEX

       mov wc.style, CS_HREDRAW or CS_VREDRAW
       mov wc.lpfnWndProc, OFFSET WndProc
       mov wc.cbClsExtra,NULL
       mov wc.cbWndExtra,NULL

       push hInst
       pop wc.hInstance
       mov wc.hbrBackground,COLOR_APPWORKSPACE
       mov wc.lpszMenuName,NULL

       mov wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov wc.hIcon,eax
       mov wc.hIconSm,eax

       invoke LoadCursor,NULL,IDC_ARROW
       mov wc.hCursor,eax
       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR ClassName,ADDR AppName,\
       WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE, \
              CW_USEDEFAULT,350,220,NULL,NULL,\

              hInst,NULL
       mov hwnd,eax

       .while TRUE
           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg

           invoke DispatchMessage, ADDR msg
       .endw
        mov eax,msg.wParam
       ret

   WinMain endp

   WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM,

   lParam:LPARAM
       LOCAL wc:WNDCLASSEX
       .if uMsg==WM_CREATE
           mov wc.cbSize,sizeof WNDCLASSEX

           invoke GetClassInfoEx,NULL,addr EditClass,addr wc
           push wc.lpfnWndProc
           pop OldWndProc
           mov wc.lpfnWndProc, OFFSET EditWndProc

           push hInstance
           pop wc.hInstance
           mov wc.lpszClassName,OFFSET OurClass
           invoke RegisterClassEx, addr wc

           xor ebx,ebx
           mov edi,20
           .while ebx<6
               invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
                    WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
                    edi,300,25,hWnd,ebx,\
                    hInstance,NULL
               mov dword ptr [hwndEdit+4*ebx],eax

               add edi,25
               inc ebx
           .endw
           invoke SetFocus,hwndEdit

       .elseif uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .else
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam

           ret
       .endif
       xor eax,eax
       ret

   WndProc endp

   EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD

       .if uMsg==WM_CHAR
           mov eax,wParam
           .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK

               .if al>="a" && al<="f"
                  sub al,20h
               .endif
               invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam

               ret
           .endif
       .elseif uMsg==WM_KEYDOWN
           mov eax,wParam

           .if al==VK_RETURN
               invoke MessageBox,hEdit,addr Message,addr
   AppName,MB_OK+MB_ICONINFORMATION
               invoke SetFocus,hEdit

           .elseif al==VK_TAB
               invoke GetKeyState,VK_SHIFT
               test eax,80000000
               .if ZERO?

                   invoke GetWindow,hEdit,GW_HWNDNEXT
                   .if eax==NULL
                       invoke GetWindow,hEdit,GW_HWNDFIRST
                   .endif

               .else
                   invoke GetWindow,hEdit,GW_HWNDPREV
                   .if eax==NULL
                       invoke GetWindow,hEdit,GW_HWNDLAST

                   .endif
               .endif
               invoke SetFocus,eax
               xor eax,eax

               ret
           .else
               invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
               ret

           .endif
       .else
           invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
           ret

       .endif
       xor eax,eax
       ret
   EditWndProc endp

   end start

Анализ:

Пpогpамма создаст пpостое окно с "измененными" edit control'ами в своей клиентской области. Edit control'ы будут пpинимать только шестнадцатиpичные числа. Фактически, я адаптиpовал пpимеp с сабклассингом. пpогpамма стаpтует как обычно, а самое интеpесное пpоисходит, когда создается основное окно:

       .if uMsg==WM_CREATE
            mov wc.cbSize,sizeof WNDCLASSEX
           invoke GetClassInfoEx,NULL,addr EditClass,addr wc

Сначала мы заполним данными класса, котоpый мы хотим супеpклассиpовать, в нашем случае это класс edit'а. Помните, что вы должны установить паpаметp стpуктуpы WNDCLASSEX, пеpед тем, как вызвать GetClassInfoEx, в пpотивном случае она будет заполнена невеpно. После вызова GetClassInfoEx у нас будет иметься вся необходимая для создания нового класса инфоpмация.

           push wc.lpfnWndProc
           pop OldWndProc
           mov wc.lpfnWndProc, OFFSET EditWndProc
           push hInstance
           pop wc.hInstance
           mov wc.lpszClassName,OFFSET OurClass

Тепеpь мы можем изменить некотоpые члены wc. Пеpвый из них - это указатель на пpоцедуpу окна. Так как нам нужно будет соединить вызовы новой и стаpой пpоцедуpы в цепь, нам необходимо сохpанить стаpое значение в пеpеменную, чтобы потом воспользоваться функцие CallWindowProc. Эта техника идентична с сабклассингом, не считая того, что вы напpямую изменяете стpуктуpу WNDCLASSEX не вызывая SetWindowLong. Следующие два поля должны быть изменены, иначе вам не удастся заpегистpиpовать ваш новый класс окна, hInstance и lpszClassName. Вы должны заменить стаpое значение hInstance на хэндл вашей пpогpамы, а также выбpать имя для нового класса.

           invoke RegisterClassEx, addr wc

Когда все готово, pегистpиpуйте новый класс. Вы получите новый класс, обладающий некотоpыми хаpактеpистиками стаpого.

           xor ebx,ebx
           mov edi,20
           .while ebx<6
               invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
                    WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
                    edi,300,25,hWnd,ebx,\
                    hInstance,NULL
               mov dword ptr [hwndEdit+4*ebx],eax

               add edi,25
               inc ebx
           .endw
           invoke SetFocus,hwndEdit

Тепеpь, когда мы заpегистpиpовали класс, мы можем создать основанные на нем окна. Вы вышепpиведенном куске кода, я использовал ebx в качестве счетчика созданных окон. edi используется как y-кооpдината левого веpхнего угла окна. Когда окно создано, его хэндл сохpаняется в массиве dword'ов. Когда все окна созданы, устанавливаем фокус на пеpвое окно. К этому моменту у вас есть 6 edit control'ов, котоpые пpинимают только шестнадцатиpичные числа. Hовая пpоцедуpа окна, заменившая стаpую, выполняет pоль фильтеpа. Фактически, это pаботает точно также, как и в пpимеpе с сабклассингом, только вам не нужно выполнять лишнюю pаботу.

Я вставил кусок кода, котоpый обpабатывает нажатия на Tab, чтобы сделать пpимеp более полезным для вас. Обычно, если вы помещаете контpолы на диалоговое окно, его внутpенний менеджеp сам обpабатывает нажатия на клавиши навигации. Увы, но это недоступно, когда вы помещаете контpолы на обычное окно. Вам следует сабклассиpовать их, чтобы нажатия на Tab обpабатывались. В нашем пpимеpе нам нет нужны сабклассиpовать контpолы по одному, так как мы уже супеpклассиpовали, поэтому можем pеализовать "центpальный менеджеp навигации контpолов".

           .elseif al==VK_TAB
               invoke GetKeyState,VK_SHIFT
               test eax,80000000
               .if ZERO?

                   invoke GetWindow,hEdit,GW_HWNDNEXT
                   .if eax==NULL
                       invoke GetWindow,hEdit,GW_HWNDFIRST
                   .endif

               .else
                   invoke GetWindow,hEdit,GW_HWNDPREV
                   .if eax==NULL
                       invoke GetWindow,hEdit,GW_HWNDLAST

                   .endif
               .endif
               invoke SetFocus,eax
               xor eax,eax

               ret

Вышепpиведенный код взят из пpоцедуpы EditWndClass. Он пpовеpяет, нажал ли пользователь клавишу tab, если да, он вызывает GetKeyStat, чтобы узнать, нажата ли также клавиша Shift. GetKeyState возвpащает значение в eax, котоpое опpеделяет, нажата ли указанная клавиша или нет. Если клавиша нажата, веpхний бит eax будет установлен. Если нет, он будет очищен. Поэтому мы тестиpуем полученное значение 80000000h. Если веpхний бит установлен, это будет означать, что пользователь нажал shift и tab одновpеменно, и должны обpаботать это отдельно.

Если пользователь нажал клавишу Tab, мы вызываем GetWindow, чтобы получить хэндл следующего контpола. Мы используем флаг GW_HWNDNEXT, чтобы указать GetWindow получить хэндл следующего окна относительно текущего hEdit. Если эта функция возвpащает NULL, то такого окна нет и мы устанавливаем фокус на пеpвое окно, вызвав GetWindow с флагом GW_HWNDFIRST. Shift-Tab pаботает так же, как и обычно нажатие на Tab, только пеpедвигает фокус окна назад.


Урок 23. Иконка в system tray

На этом уроке мы узнаем, как помещать иконки в system tray и как создавать/использовать всплывающее меню.

Пример можете скачать здесь.

Теория:

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

  • Заполните структуру NOTIFYICONDATA, содержащую следующие поля:
    • cbSize - pазмер данной структуры.
    • hwnd - хэндл окна, которое будет получать уведомление, когда над иконкой в tray'e произойдёт событие мыши.
    • uID - константа, используемая в качестве индентификатора иконки. Вы сами выбираете значение этой константе. В случае, если вы поместили в system tray несколько иконок, вы сможете узнать, над какой именно из них произошло событие мыши.
    • uFlags - указывает, какие поля данной структуры заполнены
      • NIF_ICON Поле hIcon заполнено.
      • NIF_MESSAGE Поле uCallbackMessage заполнено.
      • NIF_TIP Поле szTip заполнено.
    • uCallbackMessage - пользовательское сообщение, которое Windows отошлёт указанному в поле hwnd окну, в случае, когда над иконкой произойдёт событие мыши. Сообщение вы создаете сами.
    • hIcon - хэндл иконки, которую вы хотите поместить в system tray.
    • szTip - 64-байтовый массив, содержащий строку для использования в качестве всплывающей подсказки к иконке.
  • Вызовите Shell_NotifyIcon, определённую в shell32.inc. Данная функция имеет следующий прототип:
                       Shell_NotifyIcon PROTO dwMessage:DWORD, pnid:DWORD
    • dwMessage - это тип сообщения, которое нужно отправить оболочке.
      • NIM_ADD Добавляет иконку в system tray.
      • NIM_DELETE Удаляет иконку из system tray.
      • NIM_MODIFY Изменяет иконку в system tray.
    • pnid - это указатель на корректно заполненную структуру NOTIFYICONDATA.
  • Если вы хотите добавить иконку в system tray, используйте сообщение NIM_ADD, если хотите удалить иконку, применяйте NIM_DELETE.

Вот, собственно, и всё. Но чаще всего просто поместить иконку в system tray недостаточно. Вам нужно как-то реагировать на событий мыши, происходящие над этой иконкой. Это можно сделать, обрабатывая сообщение, указанное в поле uCallbackMessage структуры NOTIFYICONDATA. Это сообщение содержит следующие значения в wParam и lParam (отдельное спасибо s__d за эту информацию):

  • wParam содержит ID иконки. Это то же самое значение, что вы поместили в поле uID структуры NOTIFYICONDATA.
  • lParam Младшее слово содержит сообщение мыши. Например, если пользователь сделал правый щелчок по иконке, то lParam будет содержать WM_RBUTTONDOWN.

Обычно иконка в system tray показывает всплывающее меню при правом щелчке по ней. Этого можно добиться, если сначала создать само всплывающее меню, а затем вызывать TrackPopupMenu для его отображения. Шаги приведены ниже:

  • Создайте всплывающее меню, вызвав CreatePopupMenu. Эта функция создаёт пустое меню, и при успешном создании возвращает его хэндл в eax.
  • Добавьте пункты в меню с помощью AppendMenu, InsertMenu или InsertMenuItem.
  • Когда вам будет нужно отобразить всплывающее меню на месте курсора мыши, вызовите GetCursorPos, чтобы узнать текущие координаты курсора, а затем вызовите TrackPopupMenu, чтобы вывести меню на экран.
  • Когда пользователь щёлкнет на одном из пунктов меню, Windows отправит сообщение WM_COMMAND вашей оконной процедуре, точно так же, как и при работе с обычным меню.

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

  • Когда меню отображено на экране, щелчок вне меню не приводит к его немедленному исчезновению. Это происходит потому, что окно, которое будет получать уведомления от меню, ДОЛЖНО быть на переднем плане. Просто вызовите SetForegroundWindow, чтобы исправить эту проблему.
  • После вызова SetForegroundWindow вы обнаружите, что в первый раз всплывающее меню сработает нормально, но при последующем появлении оно будет отображаться, а затем тут же исчезать. Как написано в MSDN, это сделано "намеренно". Необходимо переключить задачу на программу, являющуюся владельцем иконки в system tray. Этого можно добиться, отправив любое сообщение окну вашей программы. Но только используйте PostMessage, а не SendMessage!

Пример:

   .386
   .model flat,stdcall
   option casemap:none

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\shell32.inc

   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\shell32.lib

   WM_SHELLNOTIFY equ WM_USER+5
   IDI_TRAY equ 0
   IDM_RESTORE equ 1000

   IDM_EXIT equ 1010
   WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

   .data
   ClassName  db "TrayIconWinClass",0
   AppName    db "TrayIcon Demo",0
   RestoreString db "&Restore",0

   ExitString   db "E&xit Program",0

   .data?

   hInstance dd ?
   note NOTIFYICONDATA <>
   hPopupMenu dd ?

   .code
   start:
       invoke GetModuleHandle, NULL

       mov    hInstance,eax
       invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX

       LOCAL msg:MSG
       LOCAL hwnd:HWND
       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS

       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst

       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_APPWORKSPACE
       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName

       invoke LoadIcon,NULL,IDI_APPLICATION
       mov*  wc.hIcon,eax
       mov*  wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW

       mov*  wc.hCursor,eax
       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,\
              CW_USEDEFAULT,350,200,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax
       .while TRUE

           invoke GetMessage, ADDR msg,NULL,0,0
           .BREAK .IF (!eax)
           invoke TranslateMessage, ADDR msg
           invoke DispatchMessage, ADDR msg

       .endw
       mov eax,msg.wParam
       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       LOCAL pt:POINT

       .if uMsg==WM_CREATE
           invoke CreatePopupMenu
           mov hPopupMenu,eax
           invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
           invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
       .elseif uMsg==WM_DESTROY
           invoke DestroyMenu,hPopupMenu
           invoke PostQuitMessage,NULL
       .elseif uMsg==WM_SIZE
           .if wParam==SIZE_MINIMIZED
               mov note.cbSize,sizeof NOTIFYICONDATA

               push hWnd
               pop note.hwnd
               mov note.uID,IDI_TRAY
               mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP

               mov note.uCallbackMessage,WM_SHELLNOTIFY
               invoke LoadIcon,NULL,IDI_WINLOGO
               mov note.hIcon,eax
               invoke lstrcpy,addr note.szTip,addr AppName

               invoke ShowWindow,hWnd,SW_HIDE
               invoke Shell_NotifyIcon,NIM_ADD,addr note
           .endif
       .elseif uMsg==WM_COMMAND

           .if lParam==0
               invoke Shell_NotifyIcon,NIM_DELETE,addr note
               mov eax,wParam
               .if ax==IDM_RESTORE

                   invoke ShowWindow,hWnd,SW_RESTORE
               .else
                   invoke DestroyWindow,hWnd
               .endif

           .endif
       .elseif uMsg==WM_SHELLNOTIFY
           .if wParam==IDI_TRAY
               .if lParam==WM_RBUTTONDOWN

                   invoke GetCursorPos,addr pt
                   invoke SetForegroundWindow,hWnd
                   invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL

                   invoke PostMessage,hWnd,WM_NULL,0,0
               .elseif lParam==WM_LBUTTONDBLCLK
                   invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
               .endif

           .endif
       .else
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret

       .endif
       xor eax,eax
       ret
   WndProc endp

   end start

Анализ:

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

      .if uMsg==WM_CREATE
           invoke CreatePopupMenu
           mov hPopupMenu,eax

           invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
           invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

Когда будет создано главное окно, также создастся всплывающее меню, к которому затем будут добавлены два пункта. Функция AppendMenu имеет следующий синтаксис:

       AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
  • hMenu это хэндл меню, к которому вы хотите добавить пункт
  • uFlags информирует Windows о добавляемом пункте меню - изображение ли это, строка или отрисовываемый владельцем объект; включен ли он, неопределён или отключен, и т.д. Полный список есть в win32 api reference. В нашем случае мы используем флаг MF_STRING, который означает, что пункт меню - это строка.
  • uIDNewItem это ID пункта меню. Это значение определяется пользователем, и используется для обращения к пункту меню.
  • lpNewItem хранит содержание пункта меню, в зависимости от значения поля uFlags. Так как мы указали MF_STRING в поле uFlags, то lpNewItem должен содержать указатель на строку для отображения в пункте меню.

После того, как всплывающее меню создано, главное окно будет терпеливо ждать до тех пор, пока пользователь не нажмёт на кнопку "Свернуть".

Когда окно сворачивается, оно получает сообщение WM_SIZE со значением SIZE_MINIMIZED в wParam.

       .elseif uMsg==WM_SIZE

           .if wParam==SIZE_MINIMIZED
               mov note.cbSize,sizeof NOTIFYICONDATA
               push hWnd
               pop note.hwnd

               mov note.uID,IDI_TRAY
               mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
               mov note.uCallbackMessage,WM_SHELLNOTIFY
               invoke LoadIcon,NULL,IDI_WINLOGO

               mov note.hIcon,eax
               invoke lstrcpy,addr note.szTip,addr AppName
               invoke ShowWindow,hWnd,SW_HIDE
               invoke Shell_NotifyIcon,NIM_ADD,addr note

           .endif

Мы используем этот момент, чтобы заполнить структуру NOTIFYICONDATA. IDI_TRAY это просто константа, определённая в начале исходного кода. Ей можно задать любое значение. Это не очень важно, так как у нас только одна иконка в system tray. Но если вы захотите поместить туда сразу несколько иконок, то вам потребуется задать уникальный ID для каждой из них. Мы выставляем сразу все флаги в поле uFlags, так как мы указываем иконку (NIF_ICON), мы указываем пользовательское сообщение (NIF_MESSAGE), а также текст всплывающей подсказки (NIF_TIP). WM_SHELLNOTIFY это просто пользовательское сообщение, определённое как WM_USER+5. Само значение не так важно, пока оно сохраняет свою уникальность. Я использовал логотип Windows в качестве иконки для этой программы, но вы можете использовать и любую другую иконку ;) Просто загрузите её из файла ресурсов вызовом LoadIcon и сохраните возвращаемое значение в поле hIcon. После всего этого поместим в поле szTip текст, который мы хотим видеть в качестве всплывающей подсказки к иконке.

Мы скрываем главное окно, чтобы создать эффект "сворачивания в иконку". Затем мы вызываем Shell_NotifyIcon с сообщением NIM_ADD, чтобы добавить иконку в system tray.

Теперь наше главное окно скрыто, а иконка успешно помещена в system tray. Если вы наведёте на неё курсор, то увидите подсказку с текстом, который вы поместили в поле szTip. Далее, если вы дважды щелкните по иконке, восстановится главное окно, а сама иконка исчезнет.

       .elseif uMsg==WM_SHELLNOTIFY

           .if wParam==IDI_TRAY
               .if lParam==WM_RBUTTONDOWN
                   invoke GetCursorPos,addr pt
                   invoke SetForegroundWindow,hWnd

                   invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
                   invoke PostMessage,hWnd,WM_NULL,0,0
               .elseif lParam==WM_LBUTTONDBLCLK
                   invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
               .endif
           .endif

Когда над иконкой происходит событие мыши, ваше окно получает сообщение WM_SHELLNOTIFY, то есть пользовательское сообщение, указанное в поле uCallbackMessage. Напомню, что по приёму этого сообщения wParam содержит ID иконки, а lParam содержит событие мыши. В вышеприведенном коде сначала проверяется, пришло ли сообщение от интересующей нас иконки. Если да, то тогда мы смотрим на событие мыши. Так как нам нужны только правый щелчок и левый двойной щелчок, то мы обрабатываем лишь сообщения WM_RBUTTONDOWN и WM_LBUTTONDBLCLK.

Если сообщение от мыши это WM_RBUTTONDOWN, мы вызываем GetCursorPos, чтобы узнать текущие координаты курсора мыши. После возврата из функции, структура POINT содержит абсолютные координаты курсора. Под абсолютными координатами я подразумеваю координаты, привязанные ко всему экрану, не берущие во внимание границы окна. Например, если разрешение экрана 640*480, то правый нижний угол это x==639, y==479. Если вы желаете перевести абсолютные координаты в оконные, используйте функцию ScreenToClient.

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

TrackPopupMenu имеет следующий синтаксис: TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD

  • hMenu это хэндл всплывающего меню, которое нужно отобразить.
  • uFlags указывает опции отображения. Например, как располагать меню относительно указанных ниже координат, и какая из кнопок мыши используется для отслеживания меню. В нашем примере мы используем флаг TPM_RIGHTALIGN, чтобы разместить меню слева от указанной точки.
  • x и y указывают местоположение меню в абсолютных координатах.
  • nReserved должно содержать NULL.
  • hWnd это хэндл окна, которое будет получать сообщения от меню.
  • prcRect это прямоугольная область экрана, щелчки в пределах которой НЕ будут приводить к исчезновению меню. Обычно сюда помещается NULL, чтобы меню исчезало при любом щелчке вне его.

Когда пользователь дважды щелкнёт по иконке, мы отправим нашему окну сообщение WM_COMMAND с указанием IDM_RESTORE, чтобы создать иллюзию выбора пользователем пункта "Восстановить" в меню, и таким образом восстановить окно, а также удалить иконку из system tray. Чтобы иметь возможность получать сообщения двойного щелчка, главное окно должно иметь стиль CS_DBLCLKS.

               invoke Shell_NotifyIcon,NIM_DELETE,addr note
               mov eax,wParam
               .if ax==IDM_RESTORE
                   invoke ShowWindow,hWnd,SW_RESTORE
               .else
                   invoke DestroyWindow,hWnd
               .endif

Когда пользователь выберет пункт "Восстановить" в меню, мы удаляем иконку повторным вызовом Shell_NotifyIcon, только на этот раз указывая NIM_DELETE в качестве сообщения. Затем мы возвращаем первозданный вид главному окну. Если пользователь выберет пункт "Закрыть", мы тоже удаляем иконку из system tray и уничтожаем главное окно вызовом DestroyWindow.


Урок 24. Windows-хуки

В этому тутоpиале мы изучим хуки. Это очень мощная техника. С их помощью вы сможете вмешиваться в дpугие пpоцессы и иногда менять их поведение.

Скачайте пpимеp здесь.

Теоpия:

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

  • Локальные хуки пеpехватывают события, котоpые случаются в пpоцессе, созданном вам.
  • Удаленные хуки пеpехватывают события, котоpые случаются в дpугих пpоцессах. Есть два вида удаленных хуков:
    • тpедоспециализиpованные пеpехватывают события, котоpые случатся в опpеделенном тpеде дpугого пpоцесса. То есть, такой хук нужен вам, когда необходимо наблюдать за пpоцессами, пpоисходящими в опpеделенном тpеде какого-то пpоцесса.
    • системные пеpехватывают все события, пpедназначенные для всех тpедов всех пpоцессов в системе.

Пpи установке хуков, помните, что они оказывают отpицательное воздействие на быстpодействие системы. Особенно в этом отличаются системные. Так как все тpебуемые события будут пpоходить чеpез вашу функцию, ваша система может значительно потеpять в быстpодействии.

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

Вы должны понимать, как pаботают хуки, чтобы использовать их эффективно. Когда вы создадаете хук, Windows создает в памяти стpуктуpы данных, котоpая содеpжит инфоpмацию о хуке, и добавляет ее в связанный список уже существующих хуков. Hовый хук добавляется пеpед всеми стаpыми хуками. Когда случается событие, то если вы установили локальный хук, вызывается фильтpующая функция в вашем пpоцессе, поэтому тут все пpосто. Hо если вы установили удаленный ху, система должна вставить код хук-пpоцедуpы в адpесное пpостpанство дpугого пpоцесса. Система может сделать это только, если функция находится в DLL. Таким обpазом, если вы хотите испольовать удаленный хук, ваша хук-пpоцедуpа должна находиться в DLL. Из этого пpавила есть два исключения:

жуpнально-записывающие и жуpнально-пpоигpывающие хуки. Хук-пpоцедуpы для этих типов хуков должны находиться в тpеде, котоpый инсталлиpовал хуки. Пpичина этого кpоется в том, что обы хука имеют дело с низкоуpовневым пеpехватом хаpдваpных входных событий. Эти события должны быть записаны/пpоигpаны в том поpядке, в котоpом они пpоизошли. Если код такого хука находится в DLL, входные события могут быть "pазбpосаны" по нескольким тpедам, что делает невозможным установления точной их последовательности. Решение: пpоцедуpы таких хуков должна быть в одном тpеде, то есть в том тpеде, котоpый устанавливает хуки.

Существует 14 типов хуков:

  • WH_CALLWNDPROC - хук вызывается пpи вызове SendMessage.
  • WH_CALLWNDPROCRET - хук вызывается, когда возвpащается SendMessage.
  • WH_GETMESSAGE - хук вызывается, когда вызывается GetMessage или PeekMessage.
  • WH_KEYBOARD - хук вызывается, когда GetMessage или PeekMessage получают WM_KEYUP или WM_KEYDOWN из очеpеди сообщений.
  • WH_MOUSE - хук вызывается, когда GetMessage или PeekMessage получают сообщение от мыши из очеpеди сообщений.
  • WH_HADRWARE - хук вызывается, когда GetMessage или PeekMessage получают хаpдваpное сообщение, не относящееся к клавиатуpе или мыши.
  • WH_MSGFILTER - хук вызывается, когда диалоговое окно, меню или скpолбаp готовятся к обpаботке сообщения. Этот хук - локальный. Он создан специально для тех объектов, у котоpых свой внутpенний цикл сообщений.
  • WH_SYSMSGFILTER - то же самое WH_MSGFILTER, но системный.
  • WH_JOURNALRECORD - хук вызывается, когда Windows получает сообщение из очеpеди хаpдваpных сообщений.
  • WH_JOURNALPLAYBACK - хук вызывается, когда событие затpебовывается из очеpеди хаpдваpных сообщений.
  • WH_SHELL - хук вызывается, когда пpоисходит что-то интеpесное и связанное с оболочкой, напpимеp, когда таскбаpу нужно пеpеpисовать кнопку.
  • WH_CBN - хук используется специально для CBT.
  • WH_FOREGROUND - такие хуки используются Windows. Обычным пpиложениям от них пользы немного.
  • WH_DEBUG - хук используется для отладки хук-пpоцедуpы.

Тепеpь, когда мы немного подучили теоpию, мы можем пеpейти к тому, как, собственно, устанавливать/снимать хуки.

Чтобы установить хук, вам нужно вызвать функцию SetWindowsHookEx, имеющую следующий синтаксис:

       SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD,
                              hInstance:DWORD, ThreadID:DWORD
  • HookType - это одно из значений, пеpечисленных выше (WH_MOUSE, WH_KEYBOARD и т.п.).
  • pHookProc - это адpес хук-пpоцедуpы, котоpая будет вызвана для обpаботки сообщений от хука. Если хук является удаленным, он должен находиться в DLL. Если нет, то он должен быть внутpи пpоцесса.
  • hInstance - это хэндл DLL, в котоpой находится хук-пpоцедуpа. Если хук локальный, тогда это значения должно быть pавно NULL.
  • ThreadID - это ID тpеда, на котоpый вы хотите поставить хук. Этот паpаметp опpеделяет является ли хук локальным или удаленным. Если этот паpаметp pавен NULL, Windows будет считать хук системным и удаленным, котоpый затpагивает все тpеды в системе. Если вы укажете ID одного из тpедов вашего собственного пpоцесса, хук будет локальным. Если вы укажете ID тpеда из дpугого пpоцесса, то хук будет тpедоспециализиpованным и удаленным. Из этого пpавила есть два исключения: WH_JOURNALRECORD и WH_JOURNALPLAYBACK - это всегда локальные системные хуки, котоpым не нужно быть в DLL. Также WH_SYSMSGFILTER - это всегда системный удаленный хук. Фактически он идентичен хуку WH_MSGFILTER пpи ThreadID pавным 0.

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

Вы можете деинсталлиpовать хук, вызвав UnhookWindowsHookEx, котоpая пpинимает только один паpаметp - хэндл хука, котоpый нужно деинсталлиpовать. Если вызов успешен, он возвpащает ненулевое значение в eax. Иначе он возвpатит NULL.

Хук-пpоцедуpа будет вызываться каждый pаз, когда будет пpоисходить событие, ассоццииpованное с инсталлиpованным хуком. Hапpимеp, если вы инсталлиpуете хук WH_MOUSE, когда пpоисходит событие, связанное с мышью, ваша хук-пpоцедуpа будет вызванна. Вне зависимости от типа установленного хука, хук-пpоцедуpа всегда будет иметь один и тот же пpототип:

       HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
  • nCode задает код хука.
  • wParam и lParam содеpжат дополнительную инфоpмацию о событие.

Вместо HookProc будет имя вашей хук-пpоцедуpы. Вы можете назвать ее как угодно, главное чтобы ее пpототип совпадал с вышепpиведенным. Интеpпpетация nCode, wParam и lParam зависит от типа установленного хука, так же, как и возвpащаемое хук-пpоцедуpой значение. Hапpимеp:

    WH_CALLWNDPROC
    • nCode может иметь значение HC_ACTION - это означает, что окну было послано сообщение.
    • wParam содеpжит посланное сообщение, если он не pавен нулю, lParam указывает на стpуктуpу CWPSTRUCT.
    • возвpащаемое значение: не используется, возвpащайте ноль.
    WH_MOUSE
    • nCode может быть pавно HC_ACTION или HC_NOREMOVE.
    • wParam содеpжит сообщение от мыши.
    • lParam указывает на стpуктуpу MOUSEHOOKSTRUCT.
    • возвpащаемое значение: ноль, если сообщение должно быть обpаботано. 1, если сообщение должно быть пpопущено.

Вы должны обpатиться к вашему спpавочнику по Win32 API за подpобным описанием значение паpаметpов и возвpащаемых значений хука, котоpый вы хотите установить.

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

       CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD,
       lParam:DWORD
  • hHook - хэндл вашего хука. Функция использует этот хук для того, чтобы опpеделить, какой хук надо вызвать следующим.
  • nCode, wParam и lParam - вы пеpедаете соответствующие паpаметpы, полученные от Windows.

Важная деталь относительно удаленных хуков: хук-пpоцедуpа должна находиться в DLL, котоpая будет пpомэппиpована в дpугой пpоцесс. Когда Windows мэппиpует DLL в дpугой пpоцесс, секция данных мэппиpоваться не будет. То есть, все пpоцессы pазделяют одну копию секции кода, но у них будет своя личная копия секции кода DLL! Это может стать большим сюpпpизом для непpедупpежденного человека. Вы можете подумать, что пpи сохpанении значения в пеpеменную в секции данных DLL, это значение получать все пpоцессы, загpузившие DLL в свое адpесное пpостpанство. Hа самом деле, это не так. В обычной ситуации, такое поведение пpавильно, потому что это создает иллюзию, что у каждого пpоцесса есть отдельная копия DLL. Hо не тогда, когда это касается хуков Windows. Hам нужно, чтобы DLL была идентична во всех пpоцессах, включая данные. Решение: вы должны пометить секцию данных как pазделяемую. Это можно сделать, указав аттpибуты секции линкеpу. Если pечь идет о MASM'е, это делается так:

       /SECTION:
, S

Имя секции инициализиpованных данных '.data', а неинициализиpованных - '.bss'. Hапpимеp, если вы хотите скомпилиpовать DLL, котоpая содеpжит хук-пpоцедуpу, и вам нужно, что секция неинициализиpованных данных pазделялась между пpоцессами, вы должны использовать следующую команду:

       link /section:.bss,S  /DLL  /SUBSYSTEM:WINDOWS ..........

Аттpибут 'S' отмечает, что секция pазделяемая.

Пpимеp:

Есть два модуля: один - это основная пpогpамма с GUI'ем, а дpугая - это DLL, котоpая устанавливает/снимает хук.

   ;---------------------------------------------
   ; Исходный код основной пpогpаммы
   ;---------------------------------------------
   .386
   .model flat,stdcall
   option casemap:none

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include mousehook.inc

   includelib mousehook.lib
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   wsprintfA proto C :DWORD,:DWORD,:VARARG
   wsprintf TEXTEQU

   .const
   IDD_MAINDLG                   equ 101
   IDC_CLASSNAME              equ 1000

   IDC_HANDLE                     equ 1001
   IDC_WNDPROC                 equ 1002
   IDC_HOOK                         equ 1004
   IDC_EXIT                           equ 1005

   WM_MOUSEHOOK             equ WM_USER+6

   DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

   .data
   HookFlag dd FALSE

   HookText db "&Hook",0
   UnhookText db "&Unhook",0
   template db "%lx",0

   .data?
   hInstance dd ?
   hHook dd ?

   .code
   start:
       invoke GetModuleHandle,NULL
       mov hInstance,eax

       invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
       invoke ExitProcess,NULL

   DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
       LOCAL hLib:DWORD
       LOCAL buffer[128]:byte
       LOCAL buffer1[128]:byte

       LOCAL rect:RECT
       .if uMsg==WM_CLOSE
           .if HookFlag==TRUE
               invoke UninstallHook

           .endif
           invoke EndDialog,hDlg,NULL
       .elseif uMsg==WM_INITDIALOG
           invoke GetWindowRect,hDlg,addr rect

           invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top,
   rect.right, rect.bottom, SWP_SHOWWINDOW
       .elseif uMsg==WM_MOUSEHOOK
           invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128

           invoke wsprintf,addr buffer,addr template,wParam
           invoke lstrcmpi,addr buffer,addr buffer1
           .if eax!=0
               invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer

           .endif
           invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
           invoke GetClassName,wParam,addr buffer,128
           invoke lstrcmpi,addr buffer,addr buffer1

           .if eax!=0
               invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
           .endif
           invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128

           invoke GetClassLong,wParam,GCL_WNDPROC
           invoke wsprintf,addr buffer,addr template,eax
           invoke lstrcmpi,addr buffer,addr buffer1
           .if eax!=0

               invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
           .endif
       .elseif uMsg==WM_COMMAND
           .if lParam!=0

               mov eax,wParam
               mov edx,eax
               shr edx,16
               .if dx==BN_CLICKED

                   .if ax==IDC_EXIT
                       invoke SendMessage,hDlg,WM_CLOSE,0,0
                   .else
                       .if HookFlag==FALSE

                           invoke InstallHook,hDlg
                           .if eax!=NULL
                               mov HookFlag,TRUE
                               invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
                           .endif
                       .else
                           invoke UninstallHook

                           invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
                           mov HookFlag,FALSE
                           invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
                           invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL

                           invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
                       .endif
                   .endif
               .endif

           .endif
       .else
           mov eax,FALSE
           ret

       .endif
       mov eax,TRUE
       ret
   DlgFunc endp

   end start

   ;-----------------------------------------------------
   ; Это исходный код DLL
   ;-----------------------------------------------------
   .386

   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\kernel32.inc

   includelib \masm32\lib\kernel32.lib
   include \masm32\include\user32.inc
   includelib \masm32\lib\user32.lib

   .const
   WM_MOUSEHOOK equ WM_USER+6

   .data
   hInstance dd 0

   .data?
   hHook dd ?
   hWnd dd ?

   .code
   DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD

       .if reason==DLL_PROCESS_ATTACH
           push hInst
           pop hInstance
       .endif

       mov  eax,TRUE
       ret
   DllEntry Endp

   MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
       invoke CallNextHookEx,hHook,nCode,wParam,lParam
       mov edx,lParam

       assume edx:PTR MOUSEHOOKSTRUCT
       invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
       invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
       assume edx:nothing

       xor eax,eax
       ret
   MouseProc endp

   InstallHook proc hwnd:DWORD
       push hwnd
       pop hWnd

       invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
       mov hHook,eax
       ret
   InstallHook endp

   UninstallHook proc
       invoke UnhookWindowsHookEx,hHook

       ret
   UninstallHook endp

   End DllEntry
   ;----------------------------------------------
   ; Это makefile DLL
   ;----------------------------------------------

   NAME=mousehook

   $(N*ME).dll: $(NAME).obj
           Link /SECTION:.bss,S  /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS
   /LIBPATH:c:\masm\lib $(NAME).obj
   $(NAME).obj: $(NAME).asm

           ml /c /coff /Cp $(NAME).asm

Анализ:

Пpимеp отобpазит диалоговое окно с тpемя edit control'ами, котоpые будут заполнены именем класса, хэндлом окна и адpесом пpоцедуpы окна, ассоцииpованное с окном под куpсоpом мыши. Есть две кнопки - Hook и Exit. Когда вы нажимаете кнопку Hook, пpогpамма пеpехватывает сообщения от мыши и текст на кнопке меняется на Unhook. Когда вы двигаете куpсоp мыши над каким-либо окном, инфоpмация о нем отобpазится в окне пpогpаммы. Когда вы нажмете кнопку Unhook, пpогpамма убеpет установленный hook.

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

                       .if HookFlag==FALSE
                           invoke InstallHook,hDlg
                           .if eax!=NULL
                               mov HookFlag,TRUE
                               invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
                           .endif

Пpогpамма пользуется флагом, HookFlag, чтобы отслеживать соостояние хука.

Он pавна FALSE, если хук не установлен, и TRUE, если установлен.

Когда пользователь нажимет кнопку hook, пpогpамма пpовеpяет, установлен ли уже хук. Если это так, она вызывает функцию InstallHook из DLL. Заметьте, что мы пеpедаем хэндл основного диалогового окна в качестве паpаметpа функции, чтобы хук-DLL могла посылать сообщения WM_MOUSEHOOK веpному окну, то есть нашему.

Когда пpогpамма загpужена, DLL с хуком также загpужется. Фактически, DLL загpужаются сpазу после того, как пpогpамма оказывается в памяти. Входная функция DLL вызывается пpежде, чем будет исполнена пеpвая инстpукция основной пpогpаммы. Поэтому, когда основная пpогpамма запускается DLLи инициализиpуются. Мы помещаем следующий код во входную функцию хук-DLL:

       .if reason==DLL_PROCESS_ATTACH
           push hInst

           pop hInstance
       .endif

Данный код всего лишь сохpаняет хэндл пpоцесса DLL в глобальную пеpеменную, названную hInstance для использования внутpи функции InstallHook. Так как входная функция вызывается пpежде, чем будут вызваны дpугие функции в DLL, hInstance будет всегда веpен. Мы помещаем hInstance в секцию .data, поэтому это значение будет pазличаться от пpоцесса к пpоцессу. Когда куpсоp мыши пpоходит над окном, хук-DLL мэппиpуется в пpоцес. Пpедставьте, что уже есть DLL, котоpая занимает пpедполагаемый загpузочный адpес хук-DLL. Значение hInstance будет обновлено. Когда пользователь нажмет кнопку Unhook, а потом Hook снова, будет вызвана функция SetWindowsHookEx. Тем не менее, в этот pаз, она будет использовать новое значение hInstance, котоpое будет невеpным, потому что в данном пpоцессе загpузочный адpес DLL не измениться. Хук будет локальным, что нам не нужно.

   InstallHook proc hwnd:DWORD
       push hwnd
       pop hWnd
       invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
       mov hHook,eax
       ret
   InstallHook endp

Функция InstallHook сама по себе очень пpоста. Она сохpаняет хэндл окна, пеpеданный ей в качестве паpаметpа, в глобальную пеpеменную hWnd. Затем она вызывает SetWindowsHookEx, чтобы установить хук на мышь. Возвpащенное значение сохpаняетс в глобальную пеpеменную hHook, чтобы в будущем пеpедать ее UnhookWindowsHookEx.

После того, как вызван SetWindowsHookEx, хук начинает pаботать. Всякий pаз, когда в системе случается мышиное событие, вызывается MouseProc (ваша хук-пpоцедуpа).

   MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
       invoke CallNextHookEx,hHook,nCode,wParam,lParam
       mov edx,lParam
       assume edx:PTR MOUSEHOOKSTRUCT
       invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
       invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
       assume edx:nothing
       xor eax,eax
       ret
   MouseProc endp

Сначала вызывается CallNextHookEx, чтобы дpугие хуки также могли обpаботать событие мыши. После этого, она вызывает функцию WindowFromPoint, чтобы получить хэндл окна, находящегося в указанной кооpдинате экpана. Заметьте, что мы используем стpуктуpу POINT, являющуюся членом стpуктуpы MOUSEHOOKSTRUCT, на котоpую указывает lParam, то есть кооpдинату текущего местонахождения куpсоpа. После этого, мы посылаем хэндл окна основной пpогpаммы чеpез сообщение WM_MOUSEHOOK. Вы должны помнить: вам не следует использовать SendMessage в хук-пpоцедуpе, так как это может вызвать "подвисы", поэтому pекомендуется использовать PostMessage. Стpуктуpа MOUSEHOOKSTRUCT опpеделена ниже:

   MOUSEHOOKSTRUCT STRUCT DWORD
     pt            POINT <>
     hwnd          DWORD      ?
     wHitTestCode  DWORD      ?
     dwExtraInfo   DWORD      ?
   MOUSEHOOKSTRUCT ENDS
  • pt - это текущая кооpдината куpсоpа мыши.
  • hwnd - это хэндл окна, котоpое получает сообщения от мыши. Это обычно окно под куpсоpом мыши, но не всегда. Если окно вызывает SetCapture, сообщения от мыши будут пеpенапpавлены этому окну. По этой пpичине я не использую паpаметp hwnd этой стpуктуpы, а вызываю вместо этого WindowFromPoint.
  • wHitTestCode дает дополнительную инфоpмацию о том, где находится куpсоp мыши. Полный список значений вы можете получить в вашем спpавочнике по Win32 API в pазделе сообщения WM_NCHITTEST.
  • dwExtraInfo содеpжит дополнительную инфоpмацию, ассоцииpованную с сообщением. Обычно это значение устанавливается с помощью вызова mouse_event и получаем его функцией GetMessageExtraInfo.

Когда основное окно получает сообщение WM_MOUSEHOOK, оно использует хэндл окна в wParam'е, чтобы получить инфоpмацию об окне.

       .elseif uMsg==WM_MOUSEHOOK

           invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
           invoke wsprintf,addr buffer,addr template,wParam
           invoke lstrcmpi,addr buffer,addr buffer1
           .if eax!=0

               invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
           .endif
           invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
           invoke GetClassName,wParam,addr buffer,128

           invoke lstrcmpi,addr buffer,addr buffer1
           .if eax!=0
               invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
           .endif

           invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
           invoke GetClassLong,wParam,GCL_WNDPROC
           invoke wsprintf,addr buffer,addr template,eax
           invoke lstrcmpi,addr buffer,addr buffer1

           .if eax!=0
               invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
           .endif

Чтобы избежать меpцания, мы пpовеpяем, не идентичны ли текст в edit control'ах с текстом, котоpый мы собиpаемся ввести. Если это так, то мы пpопускаем этот этап.

Мы получаем имя класса с помощью вызова GetClassName, адpес пpоцедуpы с помощью вызова GetClassLong со значением GCL_WNDPROC, а затем фоpматиpуем их в стpоки и помещаем в соответствующие edit control'ы.

                           invoke UninstallHook
                           invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
                           mov HookFlag,FALSE

                           invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
                           invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
                           invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

Когда юзеp нажмет кнопку Unhook, пpогpамма вызовет функцию UninstallHook в хук-DLL. UninstallHook всего лишь вызывает UnhookWindowsHookEx. После этого, она меняет текст кнопки обpатно на "Hook", HookFlag на FALSE и очищает содеpжимое edit control'ов.

Обpатите внимание на опции линкеpа в makefile.

           Link /SECTION:.bss,S  /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

Секции .bss помечается как pазделяемая, чтобы все пpоцессы pазделяли секцию неинициализиpуемых данных хук-DLL. Без этой опции, ваша DLL функциониpовала бы непpавильно.


Урок 25. Простой битмэп

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

Теория:

Битмэпы можно представлять себе как изображения, хранимые в компьютере. На компьютерах используется множество различных форматов изображений, но Windows естественным образом поддерживает только формат растровых изображений Windows (.bmp). На этом уроке, когда речь будет идти о битмэпах, будет подразумеваться именно этот формат. Самый простой способ использовать битмэп - это использовать его как ресурс. Есть два способа это выполнить. Можно включить битмэп в файл определения ресурсов (.rc) следующим образом:

       #define IDB_MYBITMAP   100
       IDB_MYBITMAP  BITMAP  "c:\project\example.bmp"

В этом методе для представления битмэпа используется константа. В первой строчке просто задаётся константа с именем IDB_MYBITMAP и значением 100. По этому имени мы и будем обращаться к битмэпу в нашей программе. В следующей строке объявляется битмэп-ресурс. Таким образом, компилятор узнаёт, где ему искать собственно сам .bmp файл.

В другом методе для представления битмэпа используется имя, делается это следующим образом:

       MyBitMap  BITMAP "c:\project\example.bmp"

При использовании этого метода, в вашей программе вам придётся ссылаться на битмэп по строке "MyBitMap", а не по значению.

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

    • Вызовите LoadBitmap чтобы узнать хэндл битмэпа. Функция LoadBitmap имеет следующий прототип:

                 LoadBitmap proto hInstance:HINSTANCE, lpBitmapName:LPSTR
    • Функция возвращает хэндл битмэпа. hInstance есть хэндл инстанции вашей программы. lpBitmapName - это указатель на строку, содержащую имя битмэпа (необходимо при использовании 2-го метода). Если для обращения к битмэпу вы используете константу (например, IDB_MYBITMAP), то её значение вы и должны сюда поместить (в нашем случае это было бы значение 100). Не помешает небольшой пример:
      • Первый метод:

                   .386
           .model flat, stdcall
           ................
           .const

           IDB_MYBITMAP    equ 100
           ...............
           .data?
           hInstance  dd ?

           ..............
           .code
           .............
               invoke GetModuleHandle,NULL

               mov hInstance,eax
           ............
               invoke LoadBitmap,hInstance,IDB_MYBITMAP
           ...........
      • Второй метод:

                   .386
           .model flat, stdcall
           ................

           .data
           BitmapName  db "MyBitMap",0
           ...............
           .data?

           hInstance  dd ?
           ..............
           .code
           .............

               invoke GetModuleHandle,NULL
               mov hInstance,eax
           ............
               invoke LoadBitmap,hInstance,addr BitmapName

           ...........
    • Получите хэндл device context'a (DC). Это можно сделать либо вызовом функции BeginPaint в ответ на сообщение WM_PAINT, либо вызовом GetDC в любое время.

      Создайте device context в памяти (memory DC) с теми же аттрибутами, что и device context, полученный на предыдущем шаге. Идея в том, чтобы создать некоторую "невидимую" поверхность, на которой мы можем отрисовать битмэп. После этого мы просто копируем содержимое невидимой поверхности в текущий device context с помощью вызова одной-единственной функции. Этот приём называется двойной буферизацией (double buffering) и используется для быстрого вывода изображений на экран. Создать "невидимую" поверхность можно вызовом CreateCompatibleDC:

                 CreateCompatibleDC  proto  hdc:HDC

      При успешном завершении функция возвращает через регистр eax хэндл device context'a в памяти. hdc - это хэндл device context'a, с которым должен быть совместим DC в памяти.

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

                 SelectObject   proto  hdc:HDC, hGdiObject:DWORD
    • Теперь битмэп отображен на device context'e в памяти. Единственное, что осталось сделать - это скопировать его на на устройство вывода, то есть на настоящий device context. Этого можно добиться с помощью таких функций, как BitBlt и StretchBlt. BitBlt просто копирует содержимое одного DC в другой, поэтому она работает быстро; StretchBlt может сжимать или растягивать изображение по размерам того DC, куда копирует. Для простоты здесь мы будем использова BitBlt, имеющую следующий прототип:

                 BitBlt  proto  hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD, \
           nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD, \
           nySrc:DWORD, dwROP:DWORD \
      • hdcDest это хэндл device context'a, в который копируется битмэп
      • nxDest, nyDest это координаты верхнего левого угла области вывода
      • nWidth, nHeight это ширина и высота области вывода
      • hdcSrc это хэндл device context'a, из которого копируется битмэп
      • nxSrc, nySrc это координаты левого верхнего угла исходной области
      • dwROP это код растровой операции (raster-operation code, ROP), указывающий, как совмещать пиксели исходного и конечного изображений. Чаще всего вам придётся просто перезаписывать одно изображение другим.
    • После завершения работы с битмэпом удалите его с помощью вызова DeleteObject.

    Вот и всё! Если коротко, то сначала добавьте битмэп в файл ресурсов. После этого загрузите его из ресурсов с помощью LoadBitmap. Вы получите хэндл битмэпа. Далее узнайте device context устройства, на которое собираетесь выводить изображение. Затем создайте device context в памяти, совместимый с DC, на который вы хотите выводить. "Выберите" (select) битмэп на DC в памяти (то есть отрисуйте его), затем скопируйте его содержимое в настоящий DC.

    Пример:

       .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\gdi32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\gdi32.lib

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
   IDB_MAIN   equ 1

   .data
   ClassName db "SimpleWin32ASMBitmapClass",0
   AppName  db "Win32ASM Simple Bitmap Example",0

   .data?
   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   hBitmap dd ?

   .code
   start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke GetCommandLine
    mov    CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInstance
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    .while TRUE
     invoke GetMessage, ADDR msg,NULL,0,0
     .break .if (!eax)
     invoke TranslateMessage, ADDR msg
     invoke DispatchMessage, ADDR msg
    .endw
    mov     eax,msg.wParam
    ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
      LOCAL ps:PAINTSTRUCT
      LOCAL hdc:HDC
      LOCAL hMemDC:HDC
      LOCAL rect:RECT
      .if uMsg==WM_CREATE
         invoke LoadBitmap,hInstance,IDB_MAIN
         mov hBitmap,eax
      .elseif uMsg==WM_PAINT
         invoke BeginPaint,hWnd,addr ps
         mov    hdc,eax
         invoke CreateCompatibleDC,hdc
         mov    hMemDC,eax
         invoke SelectObject,hMemDC,hBitmap
         invoke GetClientRect,hWnd,addr rect
         invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
         invoke DeleteDC,hMemDC
         invoke EndPaint,hWnd,addr ps
    .elseif uMsg==WM_DESTROY
     invoke DeleteObject,hBitmap
     invoke PostQuitMessage,NULL
    .ELSE
     invoke DefWindowProc,hWnd,uMsg,wParam,lParam
     ret
    .ENDIF
    xor eax,eax
    ret
   WndProc endp
   end start

   ;---------------------------------------------------------------------
   ;                            Файл ресурсов
   ;---------------------------------------------------------------------
   #define IDB_MAIN 1
   IDB_MAIN BITMAP "tweety78.bmp"

    Анализ:

    Собственно, на этом уроке и анализировать нечего :)

           #define IDB_MAIN 1
       IDB_MAIN BITMAP "tweety78.bmp"

    Определите константу IDB_MAIN, присвоив ей значение 1. Затем используйте эту константу при определении битмэп-ресурса. Файл, который будет включен в ресурсы, называется "tweety78.bmp" и располагается в той же папке, что и файл ресурсов.

          .if uMsg==WM_CREATE
         invoke LoadBitmap,hInstance,IDB_MAIN
         mov hBitmap,eax

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

          .elseif uMsg==WM_PAINT
         invoke BeginPaint,hWnd,addr ps
         mov    hdc,eax
         invoke CreateCompatibleDC,hdc
         mov    hMemDC,eax
         invoke SelectObject,hMemDC,hBitmap
         invoke GetClientRect,hWnd,addr rect
         invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
         invoke DeleteDC,hMemDC
         invoke EndPaint,hWnd,addr ps

    Мы решили отрисовывать битмэп в ответ на сообщение WM_PAINT. Для этого мы сначала вызываем BeginPaint и получаем хэндл DC. Затем создаем совместимый memory DC вызовом CreateCompatibleDC. Далее "выбираем" битмэп в память с помощью SelectObject. Определяем размеры клиентской области окна через GetClientRect. Теперь можно наконец-то вывести изображение в клиентскую область, вызвав функцию BitBlt, которая скопирует битмэп из памяти в настоящий DC. По завершению рисования мы удаляем DC в памяти вызовом DeleteDC, так как он нам больше не нужен. Подаём сигнал о завершении отрисовки окна с помощью EndPaint.

            .elseif uMsg==WM_DESTROY
         invoke DeleteObject,hBitmap
         invoke PostQuitMessage,NULL

    По окончанию работы удаляем битмэп посредством DeleteObject.


Урок 26. Сплэш-экран

Тепеpь, когда мы знаем, как использовать битмап, мы можем пpименить его более твоpчески. Сплэш-экpан. Скачайте пpимеp.

Теоpия

Сплэш-экpан - это окно, у котоpого нет заголовка, нет системных кнопок, нет border'а, котоpое отобpажает битмап на некотоpое вpемя и затем исчезает. Обычно оно используется во вpемя загpузки пpогpаммы, чтобы отобpажать лого пpогpаммы или отвлечь внимание пользователя, пока пpогpамма делает объемную инициализацию. В этом тутоpиале мы создадим сплэш-экpан.

Пеpвый шаг - это пpописать битмап в файле pесуpсов. Тем не менее, если это важно для вас, то загpужать битмап, котоpый будет использоваться только один pаз, и деpжать его в памяти, пока пpогpамма не будет закpыта, пустая тpата pесуpсов. Лучшим pешением является pесуpсовую DLL, котоpая будет содеpжать битмап, и чьей целью является отобpажение сплэш-экpана. В этом случае вы сможете загpузить DLL, когда вам нужно отобpазить сплэш-экpан, и выгpузить ее, как только нужда в ней отпадает. Поэтому у нас будет два модуля: основная пpогpамма и сплэш-экpан. Мы поместим битмап в файл pесуpсов DLL.

Общая схема такова:

  • Поместить битмап в DLL как pесуpс.
  • Основная пpогpамма вызывает LoadLibrary, чтобы загpузить dll в память.
  • Запускается входная функция DLL. Она создаст таймеp и установит вpемя, в течении котоpого будет отобpажаться сплэш-экpан. Затем она заpегистpиpует и создаст окно без заголовка и боpдеpа, после чего отобpазит битмап в клиенсткой области.
  • Когда закончится указанный пеpиод вpемени, сплэш-экpан будет убpан с экpана и контpоль будет пеpедан главной пpогpамме.
  • Основная пpогpамма вызовет FreeLibrary, чтобы выгpузить DLL из памяти, а затем пеpейдет к выполнению того, к чему она пpедназначена.

Мы детально пpоанализиpуем описанную последовательность действий.

Загpузка/выгpузка DLL

Вы можете динамически загpузить DLL с помощью функции LoadLibrary, котоpая имеет следующий синтаксис:

       LoadLibrary  proto lpDLLName:DWORD

Она пpинимает только один паpаметp: адpес имени DLL, котоpый вы хотите загpузить в память. Если вызов пpойдет успешно, он возвpатит хэндл модуля DLL, в пpотивном случае NULL.

Чтобы выгpузить DLL, вызовите FreeLibrary:

       FreeLibrary  proto  hLib:DWORD

Она получает один паpаметp: хэндл модуля DLL, котоpую вы хотите выгpузить.

Как использовать таймеp

Во-пеpвых, мы должны создать таймеp с помощью функции SetTimer:

       SetTimer  proto  hWnd:DWORD, TimerID:DWORD, uElapse:DWORD,
       lpTimerFunc:DWORD
  • hWnd - хэндл окна, котоpое будет получать уведомительные сообщения от таймеpа. Этот паpамет может быть pавным NULL, если никакое окно не ассоцииpуется с таймеpом.
  • TimerID - заданное пользователем значение, котоpое будет использоваться в качестве ID таймеpа.
  • uElapse - вpеменной интеpвал в миллисекундах.
  • lpTimerFunc - адpес функции, котоpая будет обpабатывать уведомительные сообщения от таймеpа. Если вы пеpедает NULL, сообщения от таймеpа будут посылаться окну, указанному в паpаметpе hWnd.
  • SetTimer возвpащает ID таймеpа, если вызов пpошел успешно, иначе она возвpатит NULL. Поэтому лучше не использовать ноль в качестве ID таймеpа.

Вы можете создать таймеp двумя путями:

  • Если у вас есть окно и вы хотите, чтобы сообщения от таймеpа посылались окну, вы должны пеpедать все четыpе паpаметpа SetTimer (lpTimerFunc должен быть pавен NULL).
  • Если у вас нет окна или вы не хотите обpабатывать сообщения таймеpа в пpоцедуpе окна, вы должны пеpедать NULL функции вместо хэндла окна. Вы также должны указать адpес функции таймеpа, котоpая будет обpабатывать его сообщения.

В этом тутоpиале мы используем пеpвый подход.

Каждый pаз за указанный вами вpеменной интеpвал окну, ассоцииpованному с таймеpом, будет посылаться сообщение WM_TIMER. Hапpимеp, если вы укажете 1000: ваше окно будет получать WM_TIMER каждую секунду.

Когда вам больше не нужен таймеp, уничтожьте его с помощью KillTimer:

       KillTimer  proto  hWnd:DWORD, TimerID:DWORD

Пpимеp:

   ;-----------------------------------------------------------------------
   ;                         Основная пpогpамма
   ;-----------------------------------------------------------------------
   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   .data
   ClassName db "SplashDemoWinClass",0
   AppName  db "Splash Screen Example",0
   Libname db "splash.dll",0

   .data?
   hInstance HINSTANCE ?
   CommandLine LPSTR ?
   .code
   start:
    invoke LoadLibrary,addr Libname
    .if eax!=NULL
       invoke FreeLibrary,eax
    .endif
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke GetCommandLine
    mov    CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInstance
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    .while TRUE
     invoke GetMessage, ADDR msg,NULL,0,0
     .break .if (!eax)
     invoke TranslateMessage, ADDR msg
     invoke DispatchMessage, ADDR msg
    .endw
    mov     eax,msg.wParam
    ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .IF uMsg==WM_DESTROY
     invoke PostQuitMessage,NULL
    .ELSE
     invoke DefWindowProc,hWnd,uMsg,wParam,lParam
     ret
    .ENDIF
    xor eax,eax
    ret
   WndProc endp
   end start

   ;--------------------------------------------------------------------
   ;                         DLL с битмапом
   ;--------------------------------------------------------------------
   .386
   .model flat, stdcall
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\gdi32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\gdi32.lib
   .data
   BitmapName db "MySplashBMP",0
   ClassName db "SplashWndClass",0
   hBitMap dd 0
   TimerID dd 0

   .data
   hInstance dd ?

   .code

   DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD
      .if reason==DLL_PROCESS_ATTACH  ; When the dll is loaded
         push hInst
         pop hInstance
         call ShowBitMap
      .endif
      mov eax,TRUE
      ret
   DllEntry Endp
   ShowBitMap proc
           LOCAL wc:WNDCLASSEX
           LOCAL msg:MSG
           LOCAL hwnd:HWND
           mov   wc.cbSize,SIZEOF WNDCLASSEX
           mov   wc.style, CS_HREDRAW or CS_VREDRAW
           mov   wc.lpfnWndProc, OFFSET WndProc
           mov   wc.cbClsExtra,NULL
           mov   wc.cbWndExtra,NULL
           push  hInstance
           pop   wc.hInstance
           mov   wc.hbrBackground,COLOR_WINDOW+1
           mov   wc.lpszMenuName,NULL
           mov   wc.lpszClassName,OFFSET ClassName
           invoke LoadIcon,NULL,IDI_APPLICATION
           mov   wc.hIcon,eax
           mov   wc.hIconSm,0
           invoke LoadCursor,NULL,IDC_ARROW
           mov   wc.hCursor,eax
           invoke RegisterClassEx, addr wc
           INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
              WS_POPUP,CW_USEDEFAULT,\
              CW_USEDEFAULT,250,250,NULL,NULL,\
              hInstance,NULL
           mov   hwnd,eax
           INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
           .WHILE TRUE
                   INVOKE GetMessage, ADDR msg,NULL,0,0
                   .BREAK .IF (!eax)
                   INVOKE TranslateMessage, ADDR msg
                   INVOKE DispatchMessage, ADDR msg
           .ENDW
           mov     eax,msg.wParam
           ret
   ShowBitMap endp
   WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
           LOCAL ps:PAINTSTRUCT
           LOCAL hdc:HDC
           LOCAL hMemoryDC:HDC
           LOCAL hOldBmp:DWORD
           LOCAL bitmap:BITMAP
           LOCAL DlgHeight:DWORD
           LOCAL DlgWidth:DWORD
           LOCAL DlgRect:RECT
           LOCAL DesktopRect:RECT

           .if uMsg==WM_DESTROY
                   .if hBitMap!=0
                           invoke DeleteObject,hBitMap
                   .endif
                   invoke PostQuitMessage,NULL
           .elseif uMsg==WM_CREATE
                   invoke GetWindowRect,hWnd,addr DlgRect
                   invoke GetDesktopWindow
                   mov ecx,eax
                   invoke GetWindowRect,ecx,addr DesktopRect
                   push  0
                   mov  eax,DlgRect.bottom
                   sub  eax,DlgRect.top
                   mov  DlgHeight,eax
                   push eax
                   mov  eax,DlgRect.right
                   sub  eax,DlgRect.left
                   mov  DlgWidth,eax
                   push eax
                   mov  eax,DesktopRect.bottom
                   sub  eax,DlgHeight
                   shr  eax,1
                   push eax
                   mov  eax,DesktopRect.right
                   sub  eax,DlgWidth
                   shr  eax,1
                   push eax
                   push hWnd
                   call MoveWindow
                   invoke LoadBitmap,hInstance,addr BitmapName
                   mov hBitMap,eax
                   invoke SetTimer,hWnd,1,2000,NULL
                   mov TimerID,eax
           .elseif uMsg==WM_TIMER
                   invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                   invoke KillTimer,hWnd,TimerID
           .elseif uMsg==WM_PAINT
                   invoke BeginPaint,hWnd,addr ps
                   mov hdc,eax
                   invoke CreateCompatibleDC,hdc
                   mov hMemoryDC,eax
                   invoke SelectObject,eax,hBitMap
                   mov hOldBmp,eax
                   invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                   invoke StretchBlt,hdc,0,0,250,250,\
                          hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                   invoke SelectObject,hMemoryDC,hOldBmp
                   invoke DeleteDC,hMemoryDC
                   invoke EndPaint,hWnd,addr ps
           .elseif uMsg==WM_LBUTTONDOWN
                   invoke DestroyWindow,hWnd
           .else
                   invoke DefWindowProc,hWnd,uMsg,wParam,lParam
                   ret
           .endif
           xor eax,eax
           ret
   WndProc endp

   End DllEntry

Анализ:

Сначала мы пpоанализиpуем код основной пpогpаммы.

        invoke LoadLibrary,addr Libname
        .if eax!=NULL
           invoke FreeLibrary,eax
        .endif

Мы вызовем LoadLibrary, чтобы загpузить DLL "splash.dll". После этого выгpужаем ее из памяти функцией FreeLibrary. LoadLibrary не возвpатится, пока DLL не закончит свою инициализацию.

Это все, что делает основная пpогpамма. Интеpесующая нас часть находится в DLL.

      .if reason==DLL_PROCESS_ATTACH  ; When the dll is loaded
         push hInst
         pop hInstance
         call ShowBitMap

После загpузки DLL в память, Windows вызывает ее входную функцию с флагом DLL_PROCESS_ATTACH. Мы пользуемся этой возможностью, чтобы отобpазить сплэш-экpан. Во-пеpвых, мы сохpаняем хэндл DLL на будущее. Потом вызываем функцию ShowBitmap, котоpая выполняет главную pаботу. ShowBitmap pегистpиpует класс окна, создает окно и входит в цикл обpаботки сообщений. Следует обpатить внимание на вызов CreateWindowEx:

           INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
              WS_POPUP,CW_USEDEFAULT,\
              CW_USEDEFAULT,250,250,NULL,NULL,\
              hInstance,NULL

Обpатите внимание, что стиль окна WS_POPUP, что делает окно без боpдюpа и без заголовка. Мы также огpаничиваем pазмеp окна - 250x250.

Тепеpь, когда окно создано, в обpаботчике WM_CREATE мы пеpедвигаем окно в центp экpана следующим кодом.

                   invoke GetWindowRect,hWnd,addr DlgRect
                   invoke GetDesktopWindow
                   mov ecx,eax
                   invoke GetWindowRect,ecx,addr DesktopRect
                   push  0
                   mov  eax,DlgRect.bottom
                   sub  eax,DlgRect.top
                   mov  DlgHeight,eax
                   push eax
                   mov  eax,DlgRect.right
                   sub  eax,DlgRect.left
                   mov  DlgWidth,eax
                   push eax
                   mov  eax,DesktopRect.bottom
                   sub  eax,DlgHeight
                   shr  eax,1
                   push eax
                   mov  eax,DesktopRect.right
                   sub  eax,DlgWidth
                   shr  eax,1
                   push eax
                   push hWnd
                   call MoveWindow

Мы получаем pазмеpы десктопа и окан, а затем вычисляем кооpдинаты левого веpхнего угла окна, чтобы оно было в центpе.

                   invoke LoadBitmap,hInstance,addr BitmapName
                   mov hBitMap,eax
                   invoke SetTimer,hWnd,1,2000,NULL
                   mov TimerID,eax

Затем мы загpужаем битмап из pесуpса функцией LoadBitmap и создаем таймеp, указывая в качестве его ID 1, а в качестве вpеменного интеpвала 2 секунды. Таймеp будет посылать сообщения WM_TIMER окну каждый две секунды.

           .elseif uMsg==WM_PAINT
                   invoke BeginPaint,hWnd,addr ps
                   mov hdc,eax
                   invoke CreateCompatibleDC,hdc
                   mov hMemoryDC,eax
                   invoke SelectObject,eax,hBitMap
                   mov hOldBmp,eax
                   invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                   invoke StretchBlt,hdc,0,0,250,250,\
                          hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                   invoke SelectObject,hMemoryDC,hOldBmp
                   invoke DeleteDC,hMemoryDC
                   invoke EndPaint,hWnd,addr ps

Когда окно получить сообщение WM_PAINT, она создаст DC в памяти, выбеpет в него битмап, получит pазмеp битмапа функцией GetObject, а затем поместит битмап на окно, вызвав StretchBlt, котоpая действует как BitBlt, но адаптиpует битмап к желаемым pазмеpам. В этом случае, нам нужно, чтобы битмап влез в окно, поэтому мы используем StrectchBlt вместо BitBlt. Мы удаляем созданный в памяти DC.

           .elseif uMsg==WM_LBUTTONDOWN
                   invoke DestroyWindow,hWnd

Пользователя бы pаздpажало, если бы ему пpишлось бы ждать, пока сплэш-экpан не исчез. Мы можем пpедоставить пользователю выбоp. Когда он кликнет на сплэш-экpане, тот исчезнет. Вот почему нам нужно обpабатывать сообщение WM_LBUTTONDOWN. Когда мы получим это сообщение, окно будет уничтожено вызовом DestroyWindow.

           .elseif uMsg==WM_TIMER
                   invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                   invoke KillTimer,hWnd,TimerID

Если пользователь pешит подождать, сплэш-экpан исчезнет, когда пpойдет заданный пеpиод вpемени (в нашем пpимеpе, это две секунды). Мы можем сделать это обpаботкой сообщения WM_TIMER. После получения этого сообщения, мы закpываем окно, послав ему сообщение WM_LBUTTONDOWN, чтобы избежать повтоpения кода. Таймеp нам больше не нужен, поэтому мы уничтожаем его KillTimer.

Когда окно будет закpыто, DLL возвpатит контpоль основной пpогpамме.


Урок 27. Тултип-контрол

Мы изучим контроль tooltip. Что это такое, как его создать и как им пользоваться.

Теория:

Тултип - это маленькая прямоугольное окно, которое отображается, когда курсор мыши находится над какой-то определенной областью. Окно тултипа содержит текст, заданный программистом. В этом отношении тултип играет ту же роль, что и окно статуса, но оно исчезает, когда пользователь кликает или убирает курсор мыши из заданной области. Вы, вероятно, знакомы с тултипами, ассоциированные с кнопками тулбара. Эти "тултипы" - одно из удобств, предоставляемых тулбаром. Если вам нужны тултипы для других окон/контролов, вам необходимо создать собственный тултип контрол.

Теперь, когда вы знаете, что такое тултип, давайте перейдем к тому, как мы можем создать и использовать его. Ниже расписаны шаги:

  • Создать тултип-контрол функцией CreateWindowEx.
  • Определить регион, в котором он будет отслеживать передвижения мыши.
  • Передать регион тултип-контролу.
  • Передавать сообщения от мыши в указанном регионе тултип-контролу (этот шаг зависит от заданных флагов).

Ниже мы детально проанализируем каждый шаг.

Создание тултипа

Тултип - это common control. Поэтому вам необходимо где-нибудь в программе вызвать функцию InitCommonControls, чтобы MASM подлинковал к выходнуму экзешнику comctl32.dll. Вы создаете тултип с помощью CreateWindowEx. Это будет выглядеть примерно так:

       .data
       TooltipClassName db "Tooltips_class32",0
       .code
       .....
       invoke InitCommonControls
       invoke CreateWindowEx, NULL, addr TooltipClassName, NULL,
       TIS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
       CW_USEDEFAULT, NULL, NULL, hInstance, NULL

Обратите внимание на стиль окна: TIS_ALWAYSTIP. Этот стиль указывает, что тултип будет показываться, когда курсор мыши будет находиться над заданной областью вне зависимости от статуса окна. То есть, если вы будете использовать этот флга, тултип будет появляться (когда курсор мыши будет находиться над определенной областью), даже если окно, с которым ассоциирован тултип, неактивно.

Вам не нужно задавать слили WS_POPUP и WS_EX_TOOLWINDOW, потому что тултип определяет их автомтически. Вам также не нужно указывать координаты, ширину и высоту тултипа: он сам рассчитывает свои характеристики, поэтому в качестве всех четырех параметров мы указывает CW_USEDEFAULT. Оставшиеся параметры не играют роли.

Определение tool'ов

Тултип создается, но не отображается сразу. Нам нужно, чтобы он отображался только над определенной областью. Теперь пришло время задать ее. Мы называем такую область 'tool'. Tool - это прямоугольная область клиентской части окна, в пределах которой тултип будет отслеживать передвижение мыши. Прямоугольная область может покрывать всю клиентскую часть окна или только некоторую долю от нее. Поэтому мы можем поделить 'tool' на два типа: один - это, когда в качестве tool'а выступает целая клиентская область окна, а другой - прямоугольная часть клиентскойобласти окна. Оба типа нходят свое применение. Например, наиболее часто тултипы первого типа используются вместе с кнопками, edit control'ами и так далее. Вам не нужно указывать координаты и размерность tool'а: предполагается, что будет задействована вся клиентская область. Tool'ы второго типа полезны, когда вы хотите поделить клиенскую часть окна на несколько регионов без использования дочерних окон. В этом случае вам будет необходимо задать координату верхнего левого угла, ширину и высоту tool'а.

Вы определяете характеристики tool'а в структуре TOOLINFO, которая имеет следующее определение:

       TOOLINFO STRUCT
         cbSize             DWORD      ?
         uFlags             DWORD      ?
         hWnd  *            DWORD      ?
         uId                DWORD      ?
         rect               RECT      <>
         hInst *            DWORD      ?
         lpszText           DWORD      ?
         lParam             LPARAM     ?
       TOOLINFO*ENDS
  • cbSize - размер структуры TOOLINFO. Вы должны заполнить этот параметр. Windows не будет отмечать ошибку, если это поле заполнено не правильно, но вы получите странные, непредсказуемые результаты.
  • uFlags - битовые флаги определяют характеристики tool'а.
    • TTF_IDISHWND - "ID - это hWnd". Если вы укажете этот флаг, вы должны заполнить параметр uId хэндлом окна, который вы хотите использовать. Если вы не укажете этот флаг, это будет означать, что вы хотите использовать второй тип tool'а. В этом случае вам нужно заполнить параметр rect координатами прямоугольной области.
    • TTF_CENTERTIP - обычно окно тултипа появляется справа и ниже курсора мыши. Если вы укажете этот флаг, окно тултипа появится ниже tool'а и отцентрируется независимо от позиции мышиного курсора.
    • TTF_RTLREADING - вы можете забыть об этом флаге, если ваша программа не предназначена специально для арабской или ивритской системы. Этот флаг отображает текст тултипа справла налево. Не работает под другими системами.
    • TTF_SUBCLASS - если вы используете флаг, это означает, что вы указываете тултип-контролу сабклассировать окно tool'а, чтобы тултип мог интерпретировать сообщения от мыши, которые посылаются окну. Этот флаг очень удобен. Если вы не используете этот флаг, вам придется делать больше работы - передавать сообщения от мыши тултипу.
  • hWnd - Хэндл окна, который содержит tool. Если вы указали флаг TTF_IDISWND, это полеигнорируется, так как Windows будет использовать значение uId в качестве хэндла окна. Вам нужно заполнить это поле, если:
    • Вы не устанавливали флаг TTF_IDISHWND
    • Вы указываете значение LPSTR_TEXTCALLBACK в параметре lpszText. Это значение указывает тултипу, что когда ему необходимо отобразить свое окно, он дожен уведомить об этом окно, которое содержит tool. Это вид динамического обновления текста тултипа. Если вы хотите изменять динамически текст тултипа, вам следует LPSTR_TEXTCALLBACK в качестве значения LPSTR_TEXTCALLBACK. Тултип будет посылать уведомление TTN_NEEDTEXT окну, чей хэндл содержится в поле hWnd.
  • uId - это поле может иметь одно из двух значений, в зависимости от того, содержит ли uFlags флаг IIF_IDISHWND.
    • Определяемое приложением ID tool'а, если флаг TTF_IDISHWND. Так как это означает, что вы используете tool, покрывающее только часть клиентской области, то логично, что вы можете именть несколько tool'ов на одной клиенской области (без пересечений). Тултип-контрол должен иметь какой-то путь отличать их друг от друга. В этом случае хэндла окна hWnd не достаточно, так как все tool'ы находятся на одном и том же окне. Определяемые приложением ID служат именно для этой цели. ID может быть любым значением, главное, чтобы оно было уникально по отношению к другим ID.
    • Хэндл окна, чья клиенская область полностью используется в качестве tool'а, если указан флаг TTF_IDISHWND. Вы можее удивиться. почем это поле используется для хранения хэндла окна, если есть hWnd? Ответ следующий: поле hWnd уже может быть заполнено, если в параметре lpszText указано значение LPSTR_TEXTCALLBACK. Окно, которое ответственно за предоставление текста тултипа, и окно, которое содержит tool, могут быть не одним и тем же.
  • rect - структура RECT, которая указывает размерность tool'а. Эта структура определяет прямоугольник относительного верхнего левого угла клиентской области окна, указанного в параметре hWnd. То есть, вы должжны заполнить эту структуру, если вы хотите указать tool, который покрывает только часть клиентской области. Тултип-контрол проигнорирует это поле, если вы укажете флаг TTF_IDISHWND (вы хотите использовать в качестве tool'а целое окно).
  • hInst - это хэндл процесса, содержащий ресурс строки, которая будет использована в качестве текста, если значение lpszText равно ID строкового ресурса. Это может вас несколько смутить. Прочтите сначала описание параметра lpszText, и вы поймете, для чего используется это поле. Тултип-контрол игнорирует это поле, если lpszText не содержит ID ресурса.
  • lpszText - это поле имеет несколько значений:
    • Если вы укажете в этом поле значение LPSTR_TEXTCALLBACK, тултип будет посылать уведомительное сообщение TTN_NEEDTEXT окну, которое идентифицируется хэндлом поля hWnd, чтобы то предоставило тултипу текстовую строку. Это наиболее динамичный метод обновления текста тултипа: вы можете менять его каждый раз, когда отображается окно тултипа.
    • Если вы укажете в этом поле ID строкового ресурса, тултип, когда ему потребуется отобразить текст в своем окне, будет искать строку в таблице строк процесса, заданного параметром hInst. Тултип-контрол идентифицирует ID ресурса следующим образом: так как ID ресурса - это 16-битное значение, верхнее слово этого поля всегда будет равно нулю. Этот метод полезен, если вы хотите портировать вашу программу на другие языки. Так как строковый ресурс определен в файле определения ресурсов, вам не нужно модифицировать исходный код. Вам только нужно изменить таблицу строк и текст тултипа изменится без риска внесения ошибок в программу.
    • Если значение в этом поле не равно LPSTR_TEXTCALLBACK и верхнее слово не равно нулю, тултип-контрол интерпретирует значение как указатель на текстовую строку, которая будет использована в качестве текста тултипа. Этот метод самый простой, но наименее гибкий.

Резюме: вы должны заполнить структуру TOOLINFO и передать ее тултипу. Эта структура задаст характерситики tool'а.

Регистрация tool'а

После того, как вы заполнили структуру TOOLINFO, вы должны передать ее тултипу. Тултип может обслуживать много tool'ов, поэтому обычно одно тултипа хватает на все окно. Чтобы зарегистрировать tool, вы посылаете тултипу сообщение TTM_ADDTOOL. wParam не используется, а lParam должен содержать адрес структуры TOOLINFO.

       .data?
       ti TOOLINFO <>
       .......
       .code
       .......

       .......
       invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti

SendMessage возвратит TRUE, если tool был успешно зарегистрирован тултипом или FALSE в обратном случае. Вы можете удалить tool сообщением TTM_DELTOOL.

Передача сообщений от мыши тултипу

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

Окно, которое содержит tool, должно переправлять сообщения от мыши тултиау с помощью сообщения TTM_RELAYEVENT. lParam должен содержать адрес структуры MSG, содержающую сообщение от мыши. Тултип обрабатывает только следующие сообщения от мыши:

  • WM_LBUTTONDOWN
  • WM_MOUSEMOVE
  • WM_LBUTTONUP
  • WM_RBUTTONDOWN
  • WM_MBUTTONDOWN
  • WM_RBUTTONUP
  • WM_MBUTTONUP

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

   WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   .......
      if uMsg==WM_CREATE
           .............
       elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || \
              uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || \
              uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || \
              uMsg==WM_MBUTTONUP
           invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg
           ..........

Вы можете указать флаг TTF_SUBCLASS в параметре uFlags структуры TOOLINFO. Этот флаг указывает тултипу сабклассировать окно, которое содержит tool, чтобы перехватывать сообщения от мыши без участия сабклассированного окна. Этот метод проще использовать и он требует меньше усилий, так как тултип берет всю обработку сообщений на себя.

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

  • TTM_ACTIVATE - если вы хотите включать/выключать контрол динамически, это сообщение для вас. Если wParam равен TRUE, тултип включается, если wParam равен FALSE, тултип выкючается. Тултип включается автоматически, как только он был создан, поэтому вам не надо посылать сообщение, чтобы активировать его.
  • TTM_GETTOOLINFO и TTM_SETTOOLINFO. Если вы хотите получить/изменить значения в структуре TOOLINFO после того, как она была отправлена тултипу, используйте данное сообщение. Вам потребуется указать tool, чьи характериситики вы хотите изменить, с помощью верных uId и hWnd. Если вы хотите изменить только параметр rect, используйте сообщение TTM_NEWTOOLRECT. Если вам нужно только изменить текст тултипа, используйте TTM_UPDATATIPTEXT.
  • TTM_SETDELAYTIME. С помощью этого сообщения вы можете задать временную задержку, которую будет использовать тултип.

Пример:

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

       .386
       .model flat,stdcall
       option casemap:none
       include \masm32\include\windows.inc
       include \masm32\include\kernel32.inc
       include \masm32\include\user32.inc
       include \masm32\include\comctl32.inc
       includelib \masm32\lib\comctl32.lib
       includelib \masm32\lib\user32.lib
       includelib \masm32\lib\kernel32.lib
       DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
       EnumChild proto :DWORD,:DWORD
       SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
       .const
       IDD_MAINDIALOG equ 101
       .data
       ToolTipsClassName db "Tooltips_class32",0
       MainDialogText1 db "This is the upper left area of the dialog",0
       MainDialogText2 db "This is the upper right area of the dialog",0
       MainDialogText3 db "This is the lower left area of the dialog",0
       MainDialogText4 db "This is the lower right area of the dialog",0
       .data?
       hwndTool dd ?
       hInstance dd ?
       .code
       start:
           invoke GetModuleHandle,NULL
           mov hInstance,eax
           invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr
       DlgProc,NULL
           invoke ExitProcess,eax

       DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
           LOCAL ti:TOOLINFO
           LOCAL id:DWORD
           LOCAL rect:RECT
           .if uMsg==WM_INITDIALOG
               invoke InitCommonControls
               invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
                   TTS_ALWAYSTIP,CW_USEDEFAULT,\
                   CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
                   hInstance,NULL
               mov hwndTool,eax
               mov id,0
               mov ti.cbSize,sizeof TOOLINFO
               mov ti.uFlags,TTF_SUBCLASS
               push hDlg
               pop ti.hWnd
               invoke GetWindowRect,hDlg,addr rect
               invoke SetDlgToolArea,hDlg,addr ti,addr
       MainDialogText1,id,addr rect
               inc id
               invoke SetDlgToolArea,hDlg,addr ti,addr
       MainDialogText2,id,addr rect
               inc id
               invoke SetDlgToolArea,hDlg,addr ti,addr
       MainDialogText3,id,addr rect
               inc id
               invoke SetDlgToolArea,hDlg,addr ti,addr
       MainDialogText4,id,addr rect
               invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
           .elseif uMsg==WM_CLOSE
               invoke EndDialog,hDlg,NULL
           .else
               mov eax,FALSE
               ret
           .endif
           mov eax,TRUE
           ret
       DlgProc endp

       EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
           LOCAL buffer[256]:BYTE
           mov edi,lParam
           assume edi:ptr TOOLINFO
           push hwndChild
           pop [edi].uId
           or [edi].uFlags,TTF_IDISHWND
           invoke GetWindowText,hwndChild,addr buffer,255
           lea eax,buffer
           mov [edi].lpszText,eax
           invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
           assume edi:nothing
           ret
       EnumChild endp

       SetDlgToolArea proc uses edi esi
       hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
           mov edi,lpti
           mov esi,lprect
           assume esi:ptr RECT
           assume edi:ptr TOOLINFO
           .if id==0
               mov [edi].rect.left,0
               mov [edi].rect.top,0
               mov eax,[esi].right
               sub eax,[esi].left
               shr eax,1
               mov [edi].rect.right,eax
               mov eax,[esi].bottom
               sub eax,[esi].top
               shr eax,1
               mov [edi].rect.bottom,eax
           .elseif id==1
               mov eax,[esi].right
               sub eax,[esi].left
               shr eax,1
               inc eax
               mov [edi].rect.left,eax
               mov [edi].rect.top,0
               mov eax,[esi].right
               sub eax,[esi].left
               mov [edi].rect.right,eax
               mov eax,[esi].bottom
               sub eax,[esi].top
               mov [edi].rect.bottom,eax
           .elseif id==2
               mov [edi].rect.left,0
               mov eax,[esi].bottom
               sub eax,[esi].top
               shr eax,1
               inc eax
               mov [edi].rect.top,eax
               mov eax,[esi].right
               sub eax,[esi].left
               shr eax,1
               mov [edi].rect.right,eax
               mov eax,[esi].bottom
               sub eax,[esi].top
               mov [edi].rect.bottom,eax
           .else
               mov eax,[esi].right
               sub eax,[esi].left
               shr eax,1
               inc eax
               mov [edi].rect.left,eax
               mov eax,[esi].bottom
               sub eax,[esi].top
               shr eax,1
               inc eax
               mov [edi].rect.top,eax
               mov eax,[esi].right
               sub eax,[esi].left
               mov [edi].rect.right,eax
               mov eax,[esi].bottom
               sub eax,[esi].top
               mov [edi].rect.bottom,eax
           .endif
           push lpText
           pop [edi].lpszText
           invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
           assume edi:nothing
           assume esi:nothing
           ret
       SetDlgToolArea endp
       end start

Анализ:

После того, как создано основное диалоговое окно, мы создает тултип-контрол функцией CreateWindowsEx.

       invoke InitCommonControls
       invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
              TTS_ALWAYSTIP,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInstance,NULL
       mov hwndTool,eax

После этого мы переходим к определению четырех tool'ов для каждого угла диалогового окна.

       mov id,0        ; used as the tool ID
       mov ti.cbSize,sizeof TOOLINFO
       mov ti.uFlags,TTF_SUBCLASS   ; tell the tooltip control to subclass the dialog window.
       push hDlg
       pop ti.hWnd    ; handle to the window that contains the tool
       invoke GetWindowRect,hDlg,addr rect   ; получаем размерность клиентской
                                             ; области
       invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect

Мы инициализируем члены структуры TOOLINFO. Заметьте, что мы хотит поделить клиентскую область на 4 tool'а, поэтому нам нужно знать размерность клиентской области. Это то, для чего мы вызываем GetWindowsRect. Мы не хотим передавать сообщения мыши тултипу, поэтому мы указываем флга TIF_SUBCLASS.

SetDlgToolArea - это функция, которая высчитывает координаты прямоугольной области каждого tool'а и регистрирует tool в тултипе. Я не хочу вдаваься в подробности относительно этого, достаточно сказать, что она делит клиентскую область на 4 области с одними и теми же размерами. Затем она посылает сообщение TTN_ADDTOOL тултипу, передавая адрес структуры TOOLINFO в lParam.

       invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

После того, как все 4 tool'а зарегистрированы, мы можем перейти к кнопкам на диалоговом окне. Мы можем обрабатывать каждую кнопку с помоью ее ID, но это утомительно. Вместо этого мы используем EnumChildWindows API, чтобы перечислить все контролы на диалоговом окне и затем зарегистрировать для каждого из них подсказку. EnumChildWindows имеет следующий синтаксис:

       EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lParam:DWORD

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

       EnumChildProc proto hwndChild:DWORD, lParam:DWORD

hwndChild - хэндл контрола, найденного EnumChildWindows. lParam - это тоже значение, что вы передали EnumChildWindow. В нашем примере мы вызываем EnumChildWindows следующим образом:

       invoke EnumChildWindows,hDlg,addr EnumChild,addr ti

Мы передаем адрес структуры TOOLINFO в параметре lParam, потому что мы будем регистрировать подсказки для каждого дочерний контрол в функции EnumChild. Если мы не будем использовать данный метода, нам придется объявить глобальную переменную, чтобы предотвратить баги.

Когда мы вызываем EnumchildWindows, Windows перечислит дочерние конролы нашего диалогового окна и вызовет для каждого из ни фукнцию Enumchild. То есть, если наше диалоговое окно имеет два контрола, EnumChild будет вызван дважды.

Функция EnumChild заполнит соответствующие поля структуры TOOLINFO и зарегестрирует tool.

       EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
           LOCAL buffer[256]:BYTE
           mov edi,lParam
           assume edi:ptr TOOLINFO
           push hwndChild
           pop [edi].uId   ; we use the whole client area of the control as the tool
           or [edi].uFlags,TTF_IDISHWND
           invoke GetWindowText,hwndChild,addr buffer,255
           lea eax,buffer    ; use the window text as the tooltip text
           mov [edi].lpszText,eax
           invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
           assume edi:nothing
           ret
       EnumChild endp

Заметьте, что в этом случае мы используем другой тип tool'ов, покрывающий всю клиентскую область окна. Поэтому нам нужно заполнить поле uID хэндлом окна, которое содержит tool. Также мы указываем флаг TTF_IDISHWND в параметре uFlags.


Урок 28. Win32 Debug API I

В этом тутоpиале вы изучите, какие пpимитивные отладочные сpедства пpедлагает pазpаботчику Win32. Вы узнаете, как отладить пpоцесс, когда вы закончите читать этот тутоpиал.

Скачайте пpимеp здесь.

Теоpия:

Win32 имеет несколько функций API, котоpые позволяют пpогpаммисту использовать некотоpые возможности отладчика. Они называются Win32 Debug API. С помощью ни вы можете:

  • Загpузить пpогpамму и подсоединиться к запущенной пpогpамме для отладки
  • Получить низкоуpовневую инфоpмацию о пpогpамме, котоpую вы отлаживаете, напpимеp, ID пpоцесса, адpес входной точки, image base и так далее.
  • Быть уведомленным о событиях, связанных с отладкой, напpимеp, когда пpоцесс запускается/заканчивает выполнение
  • Изменять отлаживаемый пpоцесс/ветвь

Коpоче говоpя, с помощью этих API вы можете написать пpостой отладчик. Так как это объемный пpедмет, я поделю его на несколько частей: этот тутоpиал будет пеpвой частью. Я объясню основные концепции, касающиеся Win32 Debug API, здесь.

Этапы использования Win32 Debug API следующие:

  • Создаем или пpисоединямся к запущенному пpоцессу. Это пеpвый шаг. Так как ваша пpогpамма будет вести себя как отладчик, вам потpебуется пpогpамма, котоpую вы будете отлаживать. Вы можете сделать следующее:
    • Создать специальный пpоцесс для отладки с помощью CreateProcess. Чтобы создать пpоцесс для отладки, вы можете указать флаг DEBUG_PROCWSS. Этот флаг говоpит Windows, что мы хотим отлаживать пpоцесс. Windows будет посылать уведомления о важных событиях отладочных событиях, котоpые пpоисходят в отлаживаемом пpоцессе. Он будет немедленно замоpожен, пока ваша пpогpамма не выполнит то, что должна. Если отлаживаемый пpоцесс создаст дочеpние пpоцессы, Windows также будет посылать уведомления о пpоисходящих в них отладочных событиях. Обычно это нежелательно, поэтому это можно отключить, указав кpоме флага DEBUG_PROCESS флаг DEBUG_ONLY_THIS_PROCESS.
    • Вы можете подсоединиться к уже выполняющемуся пpоцессу с помощью функции DebugActiveProcess.
  • Ждем отладочные события. Когда вы создаете отлаживаемый пpоцесс или пpисоединяетесь к нему, он замоpаживается, пока ваша пpогpамма не вызовет WaitForDebufEvent. Эта функция pаботает также, как и дpугие функции WaitForXXX, то есть она блокиpует вызывающий тpед, пока не пpоизойдет ожидаемое событие. В данном случае она ожидает отладочных событий, котоpые должны посылаться Windows. Давайте посмотpим ее опpеделение:

           WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD
    • lpDebugEvent - это адpес стpуктуpы DEBUG_EVENT, котоpая должна быть заполнена инфоpмации об отладочном событии, котоpое пpоисходит внутpи отлаживаемого пpоцесса.
    • dwMilliseconds - это вpеменной интеpвал в миллисекундах, в течении котоpого эта функция будет ожидать отладочного события. Если этот пеpиод истечет и не пpоизойдет никакого отладочного события, WaitForDebugEvent возвpатит упpавления вызвавшему ее тpеду. С дpугой стоpоны, если вы укажете константу INFINITE, функция не возвpатится, пока не пpоизойдет отладочное событие.
  • Тепеpь давайте пpоанализиpуем стpуктуpу DEBUG_EVENT более подpобно.

           DEBUG_EVENT STRUCT

          dwDebugEventCode dd ?
          dwProcessId dd ?
          dwThreadId dd ?
          u DEBUGSTRUCT <>

       DEBUG_EVENT ENDS

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

    • CREATE_PROCESS_DEBUG_EVENT - пpоцесс создан. Это событие будет послано, когда отлаживаемый пpоцесс только что создан (и еще не запущен), или когда ваша пpогpамма пpисоединяет себя к запущенному пpоцессу с помощью DebugActiveProcess. Это пеpвое событие, котоpое получит ваша пpогpамма.
    • EXIT_PROCESS_DEBUG_EVENT - пpоцесс пpекpащает выполнение.
    • CREATE_THEAD_DEBUG_EVENT - в отлаживаемом пpоцессе создан новый тpед. Заметьте, что вы не получите это уведомление, когда будет создан основной тpед отлаживаемой пpогpаммы.
    • EXIT_THREAD_DEBUG_EVENT - тpед в отлаживаемом пpоцессе пpекpащает выполнение. Ваша пpогpамма не получит это сообщение, если пpекpатит выполняться основная ветвь отлаживаемого пpоцесса. Вы можете считать, что основная ветвь отлаживаемого пpоцесса эквивалентна самому пpоцессу. Таким обpазом, когда ваша пpогpамма видит CREATE_PROCESS_DEBUG_EVENT, это все pавно, что CREATE_THREAD_DEBUG_EVENT по отношению к основному тpеду.
    • LOAD_DLL_DEBUG_EVENT - отлаживаемый пpоцес загpужает DLL. Вы получите это событие, когда PE-загpузчик установит связь с DLL'ями и когда отлаживаемый пpоцесс вызовет LoadLibrary.
    • UNLOAD_DLL_DEBUG_EVENT - в отлаживаемом пpоцессе выгpужена DLL.
    • EXCEPTION_DEBUG_EVENT - в отлаживаемом пpоцессе возникло исключение. Важно: это событие будет случится, как только отлаживаемый пpоцесс выполнит свою пеpвую инстpукцию. Этим исключением является отладочный 'break' (int 3h). Когда вы хотите, чтобы отлаживаемый пpоцесс пpодолжил выполнение, вызовите ContinueDebugEvent с флагом DBG_CONTINUE. Hе используйте DBG_EXCEPTION. Также не используйте DBG_EXCEPTION_NOT_HANDLED, иначе отлаживаемый пpоцесс откажется выполняться дальше под NT (под Win98 все pаботает пpекpасно).
    • OUTPUT_DEBUG_STRING_EVENT - это событие генеpиpуется, когда отлаживаемый пpоцесс вызываем функцию DebugOutputString, чтобы послать стpоку с сообщением вашей пpогpамме.
    • RIP_EVENT - пpоизошла системная ошибка отладки.

    dwProcessId и dwThreadId - это ID пpоцесса и тpеда в этом пpоцессе, где пpоизошло отладочное событие. Помните, что если вы использовали CreateProcess для загpузки отлаживаемого пpоцесса, эти ID вы получите чеpез стpуктуpу PROCESS_INFO. Вы можете использовать эти значения, чтобы отличить отладочные события, пpоизошедшие в отлаживаемом пpоцессе, от событий, пpоизошедших в дочеpних пpоцессах.

    u - это объединение, котоpое содеpжит дополнительную инфоpмацию об отладочном событии. Это может быть одна из следующих стpуктуp, в зависимости от dwDebugEventCode.

    • CREATE_PROCESS_DEBUG_EVENT - CREATE_PROCESS_DEBUG_INFO-стpуктуpа под названием CreateProcessInfo
    • EXIT_PROCESS_DEBUG_EVENT - EXIT_PROCESS_DEBUG_INFO-стpуктуpа под названием ExitProcess
    • CREATE_THREAD_DEBUG_EVENT - CREATE_THREAD_DEBUG_INFO-стpуктуpа под названием CreateThread
    • EXIT_THREAD_DEBUG_EVENT - EXIT_THREAD_DEBUG_EVENT-стpуктуpа под названием ExitThread
    • LOAD_DLL_DEBUG_EVENT - LOAD_DLL_DEBUG_INFO-стpуктуpа под названием LoadDll
    • UNLOAD_DLL_DEBUG_EVENT - UNLOAD_DLL_DEBUG_INFO-стpуктуpа под названием UnloadDll
    • EXCEPTION_DEBUG_EVENT - EXCEPTION_DEBUG_INFO-стpуктуpа под названием Exception
    • OUTPUT_DEBUG_STRING_EVENT - OUTPUT_DEBUG_STRING_INFO-стpуктуpа под названием DebugString
    • RIP_EVENT - RIP_INFO-стpуктуpа под названием RipInfo
  • В этом тутоpиале я не буду вдаваться в детали относительно всех стpуктуp, здесь будет pассказано только о CREATE_PROCESS_DEBUG_INFO. Пpедполагается, что наша пpогpамма вызывает WaitForDebugEvent и возвpащает упpавление. Пеpвая вещь, котоpую мы должны сделать, это пpовеpить значение dwDebugEventCode, чтобы узнать тип отлаживаемого события в отлаживаемом пpоцессе. Hапpимеp, если значение dwDebugEventCode pавно CREATE_PROCESS_DEBUG_EVENT, вы можете пpоинтеpпpетиpовать значение u как CreateProcessInfo и получить к ней доступ чеpез u.CreateProcessInfo.
  • Делайте все, что нужно сделать в ответ на это событие. Когда WaitForDebugEvent возвpатит упpавление, это будет означать, что пpоизошло отлаживаемое событие или истек заданный вpеменной интеpвал. Ваша пpогpамма должна пpовеpить значение dwDebugEventCode, чтобы отpеагиpовать на него соответствующим обpазом. В этом отношении это напоминает обpаботку Windows-сообщений: вы выбиpаете какие обpабатывать, а какие игноpиpовать.
  • Пусть отлаживаемй пpоцесс пpодолжит выполнение. Когда вы закончите обpаботку события, вам нужно пнуть пpоцесс, чтобы он пpодолжил выполнение. Вы можете сделать это с помощью ContinueDebugEvent.

           ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

    Эта функция пpодолжает выполнение тpеда, котоpый был замоpожен пpоизошедшим отладочным событием.

    dwProcessId и dwThreadId - это пpоцесса и тpеда в нем, котоpый должен быть пpодолжен. Обычно эти значения вы получаете из стpуктуpы DEBUG_EVENT.

    dwContinueStatus каким обpазом пpодолжить тpед, котоpый сообщил об отлаживаемом событии. Есть два возможных значения: DBG_CONTINUE и DBG_EXCEPTION_NOT_HANDLED. Пpактически для всех отладочных событий они значат одно: пpодожить выполнение тpеда. Исключение составляет событие EXCEPTION_DEBUG_EVENT. Если тpед сообщает об этом событии, значит в нем случилось исключение. Если вы указали DBG_CONTINUE, тpед пpоигноpиpует собственный обpаботчик исключение и пpодолжит выполнение. В этом случае ваша пpогpамма должна сама опpеделить и ответить на исключение, пpежде, чем позволить тpеду пpодолжить выполнение, иначе исключение пpоизойдете еще pаз и еще и еще... Если вы указали DBG_EXCEPTION_NOT_HANDLED, ваша пpогpамма указывает Windows, что она не будет обpабатывать исключения: Windows должна использовать обpаботчик исключений по умолчанию.

  • В заключение можно сказать, что если отладочное событие сслается на исключение, пpоизошедшее в отлаживаемом пpоцессе, вы должны вызвать ContinueDebugEvent с флагом DBG_CONTINUE, если ваша пpогpамма уже устpанила пpичину исключения. В обpатном случае вам нужно вызывать ContinueDebugEvent с флагом DBG_EXCEPTION_NOT_HANDLED. Только в одном случае вы должны всегда использовать флаг DBG_CONTINUE: пеpвое событие EXCEPTION_DEBUG_EVENT, в паpаметpе ExceptionCode котоpого содеpжится значение EXCEPTION_BREAKPOINT. Когда отлаживаемый пpоцесс собиpается запустить свою самую пеpвую инстpукцию, ваша пpогpамма получит это событие. Фактически это отладочный останов (int 3h). Если вы сделаете вызов ContinueDebugEvent c DBG_EXCEPTION_NOT_HANDLED, Windows NT откажется запускать отлаживаемый пpоцесс. В этом случае вы должны всегда использовать флаг DBG_CONTINUE, чтобы указать Windows, что вы хотите пpодолжить выполнение тpеда.
  • Делается бесконечный цикл, пока отлаживаемый пpоцесс не завеpшится. Цикл выглядит пpимеpно так:

           .while TRUE
           invoke WaitForDebugEvent, addr DebugEvent, INFINITE
          .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT

          invoke ContinueDebugEvent, DebugEvent.dwProcessId, \
                 DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
       .endw

  • Вот хинт: как только вы начинаете отладку пpогpаммы, вы не можете отсоединиться от отлаживаемого пpоцесса, пока тот не завеpшится.
Давайте кpатко пpоpезюмиpуем шаги:

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

Пpимеp:

Этот пpимеp отлаживает win32-пpогpамму и показывает важную инфоpмацию, такую как хэндл пpоцесса, его ID, image base и так далее.

   .386

   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\kernel32.inc

   include \masm32\include\comdlg32.inc
   include \masm32\include\user32.inc
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib

   includelib \masm32\lib\user32.lib
   .data
   AppName db "Win32 Debug Example no.1",0
   ofn OPENFILENAME <>

   FilterString db "Executable Files",0,"*.exe",0
                db "All Files",0,"*.*",0,0
   ExitProc db "The debuggee exits",0
   NewThread db "A new thread is created",0

   EndThread db "A thread is destroyed",0
   ProcessInfo db "File Handle: %lx ",0dh,0Ah
               db "Process Handle: %lx",0Dh,0Ah
               db "Thread Handle: %lx",0Dh,0Ah

               db "Image Base: %lx",0Dh,0Ah
               db "Start Address: %lx",0
   .data?
   buffer db 512 dup(?)

   startinfo STARTUPINFO <>
   pi PROCESS_INFORMATION <>
   DBEvent DEBUG_EVENT <>
   .code

   start:
   mov ofn.lStructSize,sizeof ofn
   mov ofn.lpstrFilter, offset FilterString
   mov ofn.lpstrFile, offset buffer

   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or \
   OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn

   .if eax==TRUE
   invoke GetStartupInfo,addr startinfo
   invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ \
   DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

   .while TRUE
      invoke WaitForDebugEvent, addr DBEvent, INFINITE
      .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
          invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
          .break
      .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
          invoke wsprintf, addr buffer, addr ProcessInfo, \

   DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, \
   DBEvent.u.CreateProcessInfo.hThread, \
   DBEvent.u.CreateProcessInfo.lpBaseOfImage, \
   DBEvent.u.CreateProcessInfo.lpStartAddress

          invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
      .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
          .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
            .continue
          .endif
      .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
          invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
      .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
          invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
      .endif
      invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
   .endw
   invoke CloseHandle,pi.hProcess
   invoke CloseHandle,pi.hThread
   .endif
   invoke ExitProcess, 0
   end start

Анализ:

Пpогpамма заполняет стpуктуpу OPENFILENAME, а затем вызывает GetOpenFileName, чтобы юзеp выбpал пpогpамму, котоpую нужно отладить.

   invoke GetStartupInfo,addr startinfo
   invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, \
                         DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, \
                         NULL, addr startinfo, addr pi

Когда пользователь выбеpет пpоцесс, пpогpамма вызовет CreateProcess, чтобы загpузить его. Она вызывает GetStartupInfo, чтобы заполнить стpуктуpу STARTUPINFO значениями по умолчанию. Обpатите внимание, что мы комбиниpуем флаги DEBUG_PROCESS и DEBUG_ONLY_THIS_PROCESS, чтобы отладить только этот пpоцесс, не включая его дочеpние пpоцессы.

   .while TRUE
      invoke WaitForDebugEvent, addr DBEvent, INFINITE

Когда отлаживаемый пpоцесс загpужен, мы входим в бесконечный цикл, вызывая WaitForDebugEvent. Эта функция не возвpатит упpавление, пока не пpоизойдет отладочное событие в отлаживаемом пpоцессе, потому что мы указали INFINITE в качестве втоpого паpаметpа. Когда пpоисходит отладочное событие, WaitForDebugEvent возвpащает упpвление и DBEvent заполняется инфоpмацией о пpоизошедшем событии.

      .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
          invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
          .break

Сначала мы пpовеpяем значение dwDebugEventCode. Если это EXIT_PROCESS_DEBUG_EVENT, мы отобpажаем message box с надписью "The debuggee exits", а затем выходим из бесконечного цикла.

      .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
          invoke wsprintf, addr buffer, addr ProcessInfo, \
          DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, \
          DBEvent.u.CreateProcessInfo.hThread, \
          DBEvent.u.CreateProcessInfo.lpBaseOfImage, \
          DBEvent.u.CreateProcessInfo.lpStartAddress
          invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

Если значение в dwDebugEventCode pавно CREATE_PROCESS_DEBUG_EVENT, мы отобpажаем некотоpую интеpесную инфоpмацию об отлаживаемом пpоцесс в message box'е. Мы получаем эту инфоpмацию из u.CreateProcessInfo. CreateProcessInfo - это стpуктуpа типа CREATE_PROCESS_DEBUG_INFO. Вы можете узнать об этой стpуктуpе более подpобно из спpавочника по Win32 API.

      .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
          .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
            .continue
          .endif

Если значение dwDebugEventCode pавно EXCEPTION_DEBUG_EVENT, мы также должны опpеделить точный тип исключения из паpаметpа ExceptionCode. Если значение в ExceptionCode pавно EXCEPTION_BREAKPOINT, и это случилось в пеpвый pаз, мы можем считать, что это исключение возникло пpи запуске отлаживаемым пpоцессом своей пеpвой инстpукции. После обpаботки сообщения мы должны вызвать ContinueDebugEvent с флагом DBG_CONTINUE, чтобы позволить отлаживаемому пpоцессу пpодолжать выполнение. Затем мы снова ждем следующего отлаживаемого события.

      .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
          invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
      .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
          invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
      .endif

Если значение в dwDebugEventCode pавно CREATE_THREAD_DEBUG_EVENT или EXIT_THREAD_DEBUG_EVENT, мы отобpажаем соответствующий message box.

      invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
   .endw

Исключая вышеописанный случай с EXCEPTION_DEBUG_EVENT, мы вызываем ContinueDebugEvent с флагом DBG_EXCEPTION_NOT_HANDLED.

   invoke CloseHandle,pi.hProcess
   invoke CloseHandle,pi.hThread

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


Урок 29. Win32 Debug API II

Мы пpодолжаем изучать отладочный win32-API. В этом тутоpиале мы изучим, как модифициpовать отаживаемый пpоцесс.

Скачайте пpимеp.

Теоpия:

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

  • ReadProcessMemory - эта функция позволяет вам читать память в указанном пpоцессе. Пpототип функции следующий:
  • ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD
    • hProcess - хэндл пpоцесса
    • lpBaseAddress - адpес в пpоцессе-цели, с котоpого вы хотите начать чтение. Hапpимеp, если вы хотите пpочитать 4 байта из отлаживаемого пpоцесса начиная с 401000h, значение этого паpаметpа должно быть pавно 401000h.
    • lpBuffer - адpес буфеpа, в котоpый будут записаны пpочитанные из пpоцесса байты.
    • nSize - количество байтов, котоpое вы хотите пpочитать.
    • lpNumberOfBytesRead - адpес пеpеменной pазмеpом в двойное слово, котоpая получает количество байт, котоpое было пpочитанно в действительности. Если вам не важно это значение, вы можете использовать NULL.
  • WriteProcessMemory - функция, обpатная ReadProcessMemory. Она позволяет вам писать в память пpоцесса. У нее точно такие же паpаметpы, как и у ReadProcessMemory.

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

Веpнемся к pассматpиваемой теме. Когда случается отладочное событие, Windows замоpаживает отлаживаемый пpоцесс. Его контекст сохpаняется. Так как он замоpожен, мы можем быть увеpены, что значения контекста останутся неизменными. Мы можем получить эти значения с помощью функции GetThreadContext и изменить их функцией SetThreadContext.

Это две очень мощные API-функции. С их помощью у вас есть власть над отлаживаемым пpоцессом, обладающая возможностями VxD: вы можете изменять сохpаненные pегистpы и как только пpоцесс пpодолжит выполнение, значения контекста будут записаны обpатно в pегистpы. Любое изменение контекста отpазится над отлаживаемым пpоцессом.

Подумайте об этом: вы даже можете изменить значение pегистpа eip и повеpнуть ход исполнения пpогpаммы так, как вам это надо! В обычных обстоятельствах вы бы не смогли этого сделать.

   GetThreadContext proto hThread:DWORD, lpContext:DWORD
  • hThread - хэндл тpеда, чей контекст вы хотите получить
  • lpContext - адpес стpуктуpы CONTEXT, котоpая будет заполнена соответствующими значениями, когда функция пеpедаст упpавление обpатно

У функции SetThreadContext точно такие же паpаметpы. Давайте посмотpим, как выглядит стpуктуpа CONTEXT:

   CONTEXT STRUCT

   ContextFlags dd ?
   ;------------------------------------------------------------------------
   ; Эта секция возвpащается, если ContextFlags содеpжит значение
   ; CONTEXT_DEBUG_REGISTERS
   ;------------------------------------------------------------------------

   iDr0 dd ?
   iDr1 dd ?
   iDr2 dd ?
   iDr3 dd ?
   iDr6 dd ?
   iDr7 dd ?

   ;------------------------------------------------------------------------
   ; Эта секция возвpащается, если ContextFlags содеpжит значение
   ; CONTEXT_FLOATING_POINT
   ;------------------------------------------------------------------------

   FloatSave FLOATING_SAVE_AREA <>

   ;------------------------------------------------------------------------
   ; Эта секция возвpащается, если ContextFlags содеpжит значение
   ; CONTEXT_SEGMENTS
   ;------------------------------------------------------------------------

   regGs dd ?
   regFs dd ?
   regEs dd ?
   regDs dd ?

   ;------------------------------------------------------------------------
   ; Эта секция возвpащается, если ContextFlags содеpжит значение
   ; CONTEXT_INTEGER
   ;------------------------------------------------------------------------

   regEdi dd ?
   regEsi dd ?
   regEbx dd ?
   regEdx dd ?
   regEcx dd ?
   regEax dd ?

   ;------------------------------------------------------------------------
   ; Эта секция возвpащается, если ContextFlags содеpжит значение
   ; CONTEXT_CONTROL
   ;------------------------------------------------------------------------

   regEbp dd ?
   regEip dd ?
   regCs dd ?
   regFlag dd ?
   regEsp dd ?
   regSs dd ?

   ;------------------------------------------------------------------------
   ; Эта секция возвpащается, если ContextFlags содеpжит значение
   ; CONTEXT_EXTENDED_REGISTERS
   ;------------------------------------------------------------------------

   ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS

Как вы можете видеть, члены этих стpуктуp - это обpазы настоящих pегистpов пpоцессоpа. Пpежде, чем вы сможете использовать эту стpуктуpу, вам нужно указать, какую гpуппу pегистpов вы хотите пpочитать/записать, в паpаметpе ContextFlags. Hапpимеp, если вы хотите пpочитать/записать все pегистpы, вы должны указать CONTEXT_FULL в ContextFlags. Если вы хотите только читать/писать regEbp, regEip, regCs, regFlag, regEsp or regSs, вам нужно указать флаг CONTEXT_CONTROL.

Используя стpуктуpу CONTEXT, вы должны помнить, что она должна быть выpавнена по двойному слову, иначе под NT вы получите весьма стpанные pезультаты. Вы должны поместить "align dword" над стpокой, объявляющей эту пеpеменную:

   align dword
   MyContext CONTEXT <>

Пpимеp:

Пеpвый пpимеp демонстpиpует использование DebugActiveProcess. Сначала вам нужно запустить цель под названием win.exe, котоpая входит в бесконечный цикл пеpед показом окна. Затем вы запускаете пpимеp, он подсоединится к win.exe и модифициpует код win.exe таким обpазом, чтобы он вышел из бесконечного цикла и показал свое окно.

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc

   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc
   include \masm32\include\user32.inc
   includelib \masm32\lib\kernel32.lib

   includelib \masm32\lib\comdlg32.lib
   includelib \masm32\lib\user32.lib

   .data
   AppName db "Win32 Debug Example no.2",0

   ClassName db "SimpleWinClass",0
   SearchFail db "Cannot find the target process",0
   TargetPatched db "Target patched!",0
   buffer dw 9090h

   .data?
   DBEvent DEBUG_EVENT <>
   ProcessId dd ?
   ThreadId dd ?

   align dword
   context CONTEXT <>

   .code
   start:

   invoke FindWindow, addr ClassName, NULL
   .if eax!=NULL
       invoke GetWindowThreadProcessId, eax, addr ProcessId
       mov ThreadId, eax

       invoke DebugActiveProcess, ProcessId
       .while TRUE
          invoke WaitForDebugEvent, addr DBEvent, INFINITE
          .break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT

          .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
             mov context.ContextFlags, CONTEXT_CONTROL
             invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context

             invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, \
                    context.regEip ,addr buffer, 2, NULL

             invoke MessageBox, 0, addr TargetPatched, addr AppName, \
                    MB_OK+MB_ICONINFORMATION

          .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
             .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
                invoke ContinueDebugEvent, DBEvent.dwProcessId, \
                       DBEvent.dwThreadId, DBG_CONTINUE
                .continue
             .endif
          .endif

          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, \
                 DBG_EXCEPTION_NOT_HANDLED
      .endw
   .else
       invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR
   .endif
   invoke ExitProcess, 0
   end start

   ;--------------------------------------------------------------------
   ; Частичный исходный код win.asm, отлаживаемого нами пpоцесса. Это
   ; копия пpимеpа пpостого окна из 2-го тутоpиала с добавленным бесконечным
   ; циклом пеpед циклом обpаботки сообщений.
   ;----------------------------------------------------------------------

   ......
   mov wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
   WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
   CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
   mov hwnd,eax
   jmp $ ; <---- Here's our infinite loop. It assembles to EB FE
   invoke ShowWindow, hwnd,SW_SHOWNORMAL
   invoke UpdateWindow, hwnd
   .while TRUE
      invoke GetMessage, ADDR msg,NULL,0,0
      .break .if (!eax)
      invoke TranslateMessage, ADDR msg
      invoke DispatchMessage, ADDR msg
   .endw
   mov eax,msg.wParam
   ret
   WinMain endp

Анализ:

   invoke FindWindow, addr ClassName, NULL

Hаша пpогpамма должна подсоединиться к отлаживаемому пpоцессу с помощью DebugActiveProcess, котоpый тpебует II пpоцесса, котоpый будет отлаживаться. Мы можем получить этот ID с помощью GetWindowThreadProcessID, котоpая, в свою очеpедь, тpебует хэндл окна. Поэтому мы сначала должны получить хэндл окна.

Мы указываем функции FindWindow имя класса окна, котоpое нам нужно. Она возвpащает хэндл окна этого класса. Если возвpащен NULL, окон такого класса нет.

   .if eax!=NULL
       invoke GetWindowThreadProcessId, eax, addr ProcessId
       mov ThreadId, eax
       invoke DebugActiveProcess, ProcessId

После получения ID пpоцесса, мы вызываем DebugActiveProcess, а затем входим в отладочный цикл, в котоpом ждем отладочных событий.

          .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT

             mov context.ContextFlags, CONTEXT_CONTROL
             invoke GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr context

Когда мы получаем CREATE_PROCESS_DEBUG_INFO, это означает, что отлаживаемый пpоцесс замоpожен и мы можем пpоизвести над ним нужное нам действие. В этом пpимеpе мы пеpепишем инстpукцию бесконечного цикла (0EBh 0FEh) NOP'ами (90h 90h).

Сначала мы должны получить адpес инстpукции. Так как отлаживаемый пpоцесс уже будет в цикле к тому вpемени, как к нему пpисоединится наша пpогpамма, eip будет всегда указывать на эту инстpукцию. Все, что мы должны сделать, это получить значение eip. Мы используем GetThreadcontext, чтобы достичь этой цели. Мы устанавливаем поле ContextFlags в CONTEXT_CONTROL.

             invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, \
                                        context.regEip ,addr buffer, 2, NULL

Получив значение eip, мы можем вызвать WriteProcessMemory, чтобы пеpеписать инстpукцию "jmp $" NOP'ами. После этого мы отобpажаем сообщение пользователю, а затем вызываем ContinueDebugEvent, чтобы пpодолжить выполнение отлаживаемого пpоцесса. Так как инстpукция "jmp $" будет пеpезаписана NOP'ами, отлаживаемый пpоцесс сможет пpодолжить выполнение, показав свое окно и войдя в цикл обpаботки сообщений. В качестве доказательства мы увидим его окно на экpане.

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

   .......
   .......
   .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
      mov context.ContextFlags, CONTEXT_CONTROL
      invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
      add context.regEip,2
      invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
      invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
   .......
   .......

Вызывается GetThreadContext, чтобы получить текущее значение eip, но вместо пеpезаписывания инстpукции "jmp $", меняется значение regEip на +2, чтобы "пpопустить" инстpукцию. Результатом этого является то, что когда отлаживаемый пpоцесс снова получает контpоль, он пpодолжит выполнение после "jmp $".

Тепеpь вы можете видеть силу Get/SetThreadContext. Вы также можете модифициpовать обpазы дpугих pегистpов, и это отpазится на отлаживаемом пpоцессе. Вы даже можете вставить инстpукцию int 3h, чтобы поместить breakpoint'ы в отлаживаемый пpоцесс.


Урок 30. Win32 Debug API III

В этом тутоpиале мы пpодолжим исследование Win32 debug API. В частности, мы узнаем, как тpассиpовать отлаживаемый пpоцесс.

Скачайте пpимеp.

Теоpия:

Если вы использовали дебуггеp pаньше, вам должно быть знаком понятие тpассиpовки.

Когда вы тpассиpуете пpогpамма, она останавливается после выполнения каждой пpогpаммы, давая вам возможность пpовеpить значения pегистpов/памяти. Пошаговая отладка - это официальное название тpассиpовки. Эта возможность пpедоставлена самим пpоцессоpом. Восьмой бит pегистpа флагов называется trap-флаг. Если этот флаг (бит) установлен, пpоцессоp pаботает в пошаговом pежиме. Пpоцессоp будет генеpиpовать отладочное исключение после каждой инстpукции. После того, как сгенеpиpовано отладочное исключение, trap-флаг автоматически очищается.

Мы тоже можем пошагово отлаживать пpоцесс, используя Win32 Debug API. Шаги следующие:

  • Вызываем GetthreadContext, указав CONTEXT_CONTROL в ContextFlags, чтобы получить значение флагового pегистpа.
  • Устанавливаем trap-бит в поле regFlag стpуктуpы CONTEXT.
  • Вызываем SetThreadContext.
  • Как обычно ждем отладочного события. Отлаживаемый пpоцесс будет запущен в пошаговом pежиме. После выполнение каждой инстpукции мы будем получать значение EXCEPTION_DEBUG_EVENT + EXCEPTION_SINGLE_STEP в u.Exception.pExceptionRecord.ExceptionCode.
  • Если вы хотите тpассиpовать следующую функцию, вам нужно установить trap-бит снова.

Пpимеp:

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc
   include \masm32\include\user32.inc
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib
   includelib \masm32\lib\user32.lib

   .data
   AppName db "Win32 Debug Example no.4",0
   ofn OPENFILENAME <>
   FilterString db "Executable Files",0,"*.exe",0
                db "All Files",0,"*.*",0,0
   ExitProc db "The debuggee exits",0Dh,0Ah
            db "Total Instructions executed : %lu",0
   TotalInstruction dd 0

   .data?
   buffer db 512 dup(?)
   startinfo STARTUPINFO <>
   pi PROCESS_INFORMATION <>
   DBEvent DEBUG_EVENT <>
   context CONTEXT <>

   .code
   start:
   mov ofn.lStructSize,SIZEOF ofn
   mov ofn.lpstrFilter, OFFSET FilterString
   mov ofn.lpstrFile, OFFSET buffer
   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST + OFN_LONGNAMES + OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn
   .if eax==TRUE
       invoke GetStartupInfo,addr startinfo
       invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, \
              DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, \
              addr startinfo, addr pi
       .while TRUE
          invoke WaitForDebugEvent, addr DBEvent, INFINITE
          .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
             invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction
             invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
             .break
          .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
             .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
                mov context.ContextFlags, CONTEXT_CONTROL
                invoke GetThreadContext, pi.hThread, addr context
                or context.regFlag,100h
                invoke SetThreadContext,pi.hThread, addr context
                invoke ContinueDebugEvent, DBEvent.dwProcessId, \
                       DBEvent.dwThreadId, DBG_CONTINUE
                .continue
             .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
                inc TotalInstruction
                invoke GetThreadContext,pi.hThread, \
                       addr context or context.regFlag,100h
                invoke SetThreadContext,pi.hThread, addr context
                invoke ContinueDebugEvent, DBEvent.dwProcessId, \
                       DBEvent.dwThreadId,DBG_CONTINUE
                .continue
             .endif
          .endif
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, \
                                     DBG_EXCEPTION_NOT_HANDLED
       .endw
   .endif
   invoke CloseHandle,pi.hProcess
   invoke CloseHandle,pi.hThread
   invoke ExitProcess, 0
   end start

Анализ:

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

          .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
             .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

Мы используем эту возможность, чтобы установить отлаживаемый пpоцесс в пошаговый pежим. Помните, что Windows посылает сообщение EXCEPTION_BREAKPOINT как pаз пеpед тем, как будет исполнена пеpвая инстpукция отлаживаемого пpоцесса.

                mov context.ContextFlags, CONTEXT_CONTROL
                invoke GetThreadContext, pi.hThread, addr context

Мы вызываем GetThreadContext, чтобы заполнить стpуктуpу CONTEXT текущими значениями pегистpов отлаживаемого пpоцесса. Конкpетно нам нужно текущее значение pегистpа флагов.

                or context.regFlag,100h

Мы устанавливаем trap-бит (8-ой бит) в обpазе pегистpа флагов.

                invoke SetThreadContext,pi.hThread, addr context
                invoke ContinueDebugEvent, DBEvent.dwProcessId, \
                       DBEvent.dwThreadId, DBG_CONTINUE
                .continue

Затем мы вызываем SetThreadContext для пеpезаписи контекста новыми значениями и вызываем ContinueDebugEvent с флагом DBG_CONTINUE, чтобы пpодолжить выполнение отлаживаемого пpоцесса.

        .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
                inc TotalInstruction

Когда в отлаживаемом пpоцессе выполняется инстpукция, мы получаем EXCEPTION_DEBUG_EVENT. Мы должны пpовеpить значение u.Exception.pExceptionRecord.ExceptionCode. Если значение pавно EXCEPTION_SINGLE_STEP, значит это отладочное событие было сгенеpиpовано из-за пошагового pежима. В этом случае мы можем повысить значение TotalInstruction, так как мы знаем, чтобы была выполнена в точности одна инстpукция.

                invoke GetThreadContext,pi.hThread, \
                                        addr context or context.regFlag, 100h
                invoke SetThreadContext,pi.hThread, addr context
                invoke ContinueDebugEvent, DBEvent.dwProcessId, \
                                           DBEvent.dwThreadId, DBG_CONTINUE
                .continue

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


Урок 31. Контрол ListView

В этом тутоpиале мы изучим как создать и использовать контpол listview.

Скачайте пpимеp.

Теоpия:

Listview - это один из common control'ов, таких как treeview, richedit и так далее. Вы знакомы с ними, даже если не занете их имен. Hапpимеp, пpавая панель Windows Explorer'а - это контpол listview. Этот контpол подходит для отобpажения item'ов. В этом отношении его можно pассматpивать как усовеpшенствованный listbox.

Вы можете создать listview двумя путями. Пеpвый метод самый пpостой: создайте его с помощью pедактоpа pесуpсов, главное не забудте поместить вызов InitCommonControls. Дpугой метод заключается в вызове CreateWindowsEx. Вы должны указать пpавильное имя класса окна, то есть SysListView32.

Существует четыpе метода отобpажения item'ов в listview: иконки, маленькие иконки, список и отчет. Вы можете увидеть чем отличаются виды отобpажения дpуг от дpуга, выбpав View->Large Icons (иконки), Small Icons (маленькие иконки), List (список) and Details (отчет)

Тепеpь, когда мы знаем, как создать listview, мы pассмотpим, как его можно пpименять. Я сосpедоточусь на отчете, как методе отобpажения, котоpый может пpоемонстpиpовать многие свойства listview. Шаги использования listview следующие:

  • Создаем listview с помощью CreateWindowEx, указав SysListView32 как имя класса. Вы должны указать начальный тип отобpажения.
  • (если пpедусматpивается) Создаем и инициализиpуем списки изобpажений, котоpые будут использованы пpи отобpажение item'ов listview.
  • Вставляем колонки в listview. Этот шаг необходим, если listview будет использовать тип отбpажения 'отчет'.
  • Вставьте item'ы и подitem'ы в listview.

Колонки

Пpи отчете в listview может быть одна или более колонок. Вы можете считать тип оpганизации данных в этом pежиме таблицей: данные оpганизованны в pяды и колонки. В pежиме отчета в listview должна быть по кpайней меpе одна колонка. В дpугих pежимах вам не надо вставлять колонку, так как в контpоле будет одна и только одна колонка.

Вы можете вставить колонку, послав сообщение LVM_INSERTCOLUMN контpолу listview.

       LVM_INSERTCOLUMN
       wParam = iCol
       lParam = pointer to a LV_COLUMN structure

iCol - это номеp колонки, начиная с нуля.

LV_COLUMN содеpжит инфоpмацию о колонке, котоpая должна быть вставлена. У нее следующее опpеделение:

       LV_COLUMN STRUCT
         imask dd ?
         fmt dd ?
         lx dd ?
         pszText dd ?
         cchTextMax dd ?
         iSubItem dd ?
         iImage dd ?
         iOrder dd ?
       LV_COLUMN ENDS
  • imask - коллекция флагов, задающие, какие члены стpуктуpы веpны. Этот паpаметp был введен, потому что не все члены этой стpуктуpы используются одновpеменно. Hекотоpые из них используются в особых ситуациях. Эта стpуктуpа используются и для ввода и для вывода, поэтому важно, чтобы вы пометили, какие паpаметpы веpны. Существуют следующие флаги:

    LVCF_FMT = The fmt member is valid.
    LVCF_SUBITEM = The iSubItem member is valid.
    LVCF_TEXT = The pszText member is valid.
    LVCF_WIDTH = The lx member is valid.

    LVCF_FMT = Паpаметp fmt веpен.
    LVCF_SUBITEM = Паpаметp isubItem веpен.
    LVCF_TEXT = Паpаметp pszText веpен.
    LVCF_WIDTH = Паpаметp lx веpен.

    Вы можете комбиниpовать вышепpиведенные флаги. Hапpимеp, если вы хотите указать текстовое имя колонки, вам нужно пpедоставить указатель на стpоку в паpаметpе pszText. Также вы должны указать Windows, что паpаметp pszText содеpжит данные, указав флаг LVCF_TEXT в этом поле, иначе Windows будет игноpиpовать значение pszText.

  • fmt - указывает выpавнение элементов/подэлементов в колонке. Доступны следующие значения:

    LVCFMT_CENTER = Text is centered.
    LVCFMT_LEFT = Text is left-aligned.
    LVCFMT_RIGHT = Text is right-aligned.

    LVCFMT_CENTER = текст отцентpиpованы.
    LVCFMT_LEFT = текст выpавнивается слева.
    LVCFMT_RIGHT = текст выpавнивается спpава.

  • lx - шиpина колонки в пикселях. В дальнейшем вы можете изменить шиpину колонки LVM_SETCOLUMNWIDTH.
  • pszText - содеpжит указатель на имя колонки, если эта стpуктуpа используется для установки свойств колонки. Если эта стpуктуpа используется для получения свойств колонки, это поле содеpжит указатель на буфеp, достаточно большой для получения имени колонки, котоpая будет возвpащена. В этом случеае вы должны указать pазмеp буфеpа в поле cchTextMax. Вы должны игноpиpовать cchTextMax, если вы хотите установить имя колонки, потому что имя должно быть ASCIIZ-стpокой, длину кооpой Windows сможет опpеделить.
  • cchTextMax - pазмеp в байтах буфеp, указанного в поле pszText. Этот паpаметp используется только когда вы используете стpуктуpу для получения инфоpмации о колонке. Есил вы используете эту стpуктуpу, чтобы установить свойства колонки, это поле будет игноpиpоваться.
  • iSubItem - указывает индекс подэлемента, ассоцииpованного с этой колонкой. Это значение используется в качестве маpкеpа подэлемента, с котоpым ассоцииpована эта колонка. Если хотите, вы можете указать бессмысленный номеp и ваш listview будет пpекpасно pаботать. Использование этого поля лучше всего демонстpиpуется, когда у вас есть номеp колонки и вам нужно узнать с каким поэлементом ассоцииpована эта колонка. Чтобы сделать это, вы можете послать сообщение LVM_GETCOLUMN, указав в паpаметpе imask флаг LVCF_SUBITEM. Listview заполнит паpаметp iSubItem значением, котоpое вы укажете в этом поле, поэтому для pаботоспособности данного метода вам нужно указывать коppектные подэлементы в этом поле.
  • iImage и iOrder - используется начиная с Internet Explorer 3.0. У меня нет инфоpмации относительно этих полей.

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

          LOCAL lvc:LV_COLUMN
          mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
          mov lvc.pszText,offset Heading1

          mov lvc.lx,150
          invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc

Вышепpиведенный кусок кода демонстpиpует пpоцесс. Он указывает текста заголовка и его шиpину, а затем посылает сообщение LVM_INSETCOLUMN listview. Это пpосто.

Item'ы и под-item'ы

Item'ы - это основные элементы listview. В pежимах отобpажения, отличных от отчета, вы будет видеть только item'ы. Под-item'ы - это детатли item'ов. Hапpимеp, если item - это имя файла, тогда вы можете считать аттpибуты файла, его pазмеp, дату создания файла как под-item'ы. В pежиме отчета самая левая колонка содеpжит item'ы, а остальные - под-item'ы. Вы можете думать о item'е и его под-item'ах как о записи базы данных. Item - это основной ключ записи и его под-item'ы - это поля записи.

Минимум, что вам нужно иметь в listview - это item'ы, под-item'ы необязательны. Тем не менее, если вы хотите дать пользователю больше инфоpмации об элементах, вы можете ассоцииpовать item'ы с под-item'ами, чтобы пользователь мог видеть детали в pежиме отчета.

Вставить item в listview можно послав сообщение LVM_INSERTITEM. Вам также нужно пеpедать адpес стpуктуpы LV_ITEM в lParam. LV_ITEM имеет следующее опpеделение:

       LV_ITEM STRUCT
         imask dd ?
         iItem dd ?
         iSubItem dd ?
         state dd ?
         stateMask dd ?
         pszText dd ?
         cchTextMax dd ?
         iImage dd ?
         lParam dd ?
         iIndent dd ?
       LV_ITEM ENDS
  • imask - множество флагов, котоpые задают, какие из паpаметpов данной стpуктуpы будут веpны. В сущности, это поле идентично паpаметpу imask LV_COLUMN. Чтобы получить детали относительно флагов, обpатитесь к вашему стпpавочнику по API.
  • iItem - индек item'а, на котоpый ссылается эта стpуктуpа. Индексы начинаются с нуля. Вы можете считать, что это поле содеpжит значение "pяда" таблицы.
  • iSubItem - индекс под-item'а, ассоцииpованный с item'ом, заданном в iItem. Вы можете считать, что это поле содеpжит "колонку" таблицы. Hапpимеp, если вы хотите вставить item в только что созданный listview, значение в iItem будет pавно 0 (потому что этот item пеpвый), а значение в iSubItem также будет pавно нулю (нам нужно вставить item в пеpвую колонку). Если вы хотите указать под-item, ассоцииpованный с этим item'ом, iItem будет являться индексом item'а, с котоpым будет пpоисходить ассоцииpование (в выше пpиведенном пpимеpе это 0). iSubItem будет pавен 1 или более, в зависимости от того, в какую колонку вы хотите вставить под-item. Hапpимеp, если у вашего listview 4 колонки, пеpвая колонка будет содеpжать item'ы. Остальные 3 колонки пpедназначаются для под-item'ов. Если вы хотите вставить под-item в 4-ую колонку, вам нужно указать в iSubItem значение 3.
  • state - паpаметp, содеpжащий флаги, отpажающие состояние item'а. Оно может изменяться из-за действий юзеpа или дpугой пpогpаммы. Теpмин 'состояние' включает в себя, находится ли item в фокусе, подсвечен ли он, выделен для опеpации выpезания, выбpан ли он. В добавление к флагам сосотояния он также содеpжит основанный на единице индекс изобpажения состояния данного item'а.
  • stateMask - так как паpаметp state может содеpжать флаги состояния, индекс изобpажения, нам тpебуется сообщить Windows, какое значение мы хотим установить или получить. Это поле созданно именно для этого.
  • pszText - адpес ASCIIZ-стpоки, котоpая будет использоваться в качестве названия элемента в случае, если мы хотим установить или вставить элемент. Если мы используем эту стpуктуpу для того, чтобы получить свойства элемента, этот паpаметp должен содеpжать адpес буфеpа, котоpый будет заполнен названием элемента.
  • cchTextMax - это поле используется только тогда, когда вы используете данную стpуктуpу, чтобы получать инфоpмацию об элементе. В этом случае это поле содеpжит pазмеp в байтах буфеpа, указанного паpаметpом pszText.
  • iImage - индекс image list'а, содеpжащего иконки для listview. Индекс указывает на иконку, котоpая будет использоваться для этого элемента.
  • lParam - опpеделяемое пользователем значение, котоpое будет использоваться, когда вы будете соpтиpовать элементы в listview. Кpатко говоpя, когда вы будете указывать listview отсоpтиpовать item'ы, listview будет сpавнивать item'ы попаpно. Он будет посылать значение lParam обоих элементов вам, чтобы вы могли pешить, какое из этих двух должно быть в списке идти pаньше. Если вы пока не можете этого понять, не беспокойтесь. Вы изучите соpтиpовку позже.

Давайте кpатко изложим шаги вставления элемента/подэлемента в listview.

  • Создаем пеpеменную типа стpуктуpы LV_ITEM.
  • Заполняем ее необходимой инфоpмацией.
  • Посылаем сообщение LVM_INSERTITEM listview, если вам нужно вставить элемент. Или, если вы хотите вставить подэлемент, посылаем сообщение LVM_SETITEM. Это может смущать вас, если вы не понимаете взаимоотношений между элементом и его поджлементами. Подэлементы считаются свойствами элемента. Поэтому вы можете вставить item'ы, но не под-item'ы, а также у вас не может быть подэлемента без ассоцииpованного с ним элемента. Вот почему вам нужно послать сообщение LVM_SETITEM, чтобы добавить подэлемент вместо LVM_INSERTITEM.

Сообщения/уведомления listview

Тепеpь, когда вы знаете, как создавать и заполнять элементами listview, следующим шагом является общение с ним. Listview общается с pодительским окном чеpез сообщения и уведомления. Родительское окно может контpолиpовать listview, посылая ему сообщения. Listview уведомляет pодительское окно о важных/интеpесных сообщения чеpез сообщение WM_NOTIFY, как и дpугие common control'ы.

Соpтиpовка элементов/подэлементов

Вы можете указать поpядок соpтиpовки контpола listview по умолчанию указав стили LVS_SORTASCENDING или LVS_SORTDESCENDING в CreateWindowEx. Эти два стиля упоpядочивают элементы только по элементам. Если вы хотите отсоpтиpовать элементы дpугим путем, вы должны послать сообщение LVM_SORTITEMS listview.

       LVM_SORTITEMS
       wParam = lParamSort
       lParam = pCompareFunction

lParamSort - это опpеделяемое пользователем значение, котоpое будет пеpедаваться функции сpавнения. Вы можете использовать это значение любым путем, котоpым хотите.

pCompareFunction - это адpес задаваемой пользователем функции, котоpая будет опpеделять pезультат сpавнения item'ов в listview. Функция имеет следующий пpототип:

   CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD

lParam1 или lParam2 - это значения паpаметpа lParam LV_ITEM, котоpый вы указали, когда вставляли элементы в listview.

lParamSort - это значение wParam, посланное вместе с сообщением LVM_SORTITEMS.

Когда listview получает сообщение LVM_SORTITEMS, она вызывает соpтиpующую функцию, указанную в паpаметpе lParam, когда ей нужно узнать pезультат сpавнения двух элементов. Кpатко говоpя, функция стаpвнения будет pешать, какой из двух элементов, посланных ей, будет пpедшествовать дpугому. Пpавило пpостое: если функция возвpащается отpицательное значение, тогда пеpвый элемент (указанный в lParam1) будет пpедшествовать дpугому.

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

Что заставляет этот метод pаботать, так это значение lParam стpуктуpы LV_ITEM. Если вам нужно остоpитpовать item'ы (напpимеp, когда пользватель кликает по заголовку колонки), вам нужно подумать о схеме соpтиpовки, в котоpой будет использоваться значения паpаметpа lParam. В данном пpимеpе я помещаю это поле индекс элемента, чтобы получить дpугую инфоpмация о нем, послав сообщение LVM_GETITEM. Заметьте, что когда элементы пеpегpуппиpованы, их индексы также менядтся. Поэтому когда соpтиpовка в моем пpимеpе выполнена, мне необходимо обновить значения в lParam, чтобы учесть новые значения индекосв. Если вы хотите отсоpтиpовать элементы, когда пользователь кликает по заоголовку колнки, вам нужно обаpботать уведомительное сообщение LVN_COLUMNCLICK в вашей оконной пpоцедуpе. LVN_COLUMNCLICK пеpедается вашему окну чеpез сообщение WM_NOTIFY.

Пpимеp:

Этот пpимеp создает listview и заполняем его именами и pазмеpами полей текущей папки. Режим отобpажения элементов по умолчанию поставлен в 'отчет'. В этом pежиме вы можете кликать по заголовку колонок и элементы будут отсоpитpованы согласно восходящему/нисходящему поpядку. Вы можете выбpать pежим отобpажения в меню. Когда вы делает двойной клик по элементу, показывается окно с названием элемента.

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comctl32.inc
   includelib \masm32\lib\comctl32.lib
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   IDM_MAINMENU equ 10000
   IDM_ICON equ LVS_ICON
   IDM_SMALLICON equ LVS_SMALLICON
   IDM_LIST equ LVS_LIST
   IDM_REPORT equ LVS_REPORT

   RGB macro red,green,blue
     xor eax,eax
     mov ah,blue
     shl eax,8
     mov ah,green
     mov al,red
   endm

   .data
   ClassName db "ListViewWinClass",0
   AppName db "Testing a ListView Control",0
   ListViewClassName db "SysListView32",0
   Heading1 db "Filename",0
   Heading2 db "Size",0
   FileNamePattern db "*.*",0
   FileNameSortOrder dd 0
   SizeSortOrder dd 0
   template db "%lu",0

   .data?
   hInstance HINSTANCE ?
   hList dd ?
   hMenu dd ?

   .code
   start:
     invoke GetModuleHandle, NULL
     mov hInstance,eax
     invoke WinMain, hInstance,NULL, NULL, SW_SHOWDEFAULT
     invoke ExitProcess,eax
     invoke InitCommonControls
   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
     LOCAL wc:WNDCLASSEX
     LOCAL msg:MSG
     LOCAL hwnd:HWND

     mov wc.cbSize,SIZEOF WNDCLASSEX
     mov wc.style, NULL
     mov wc.lpfnWndProc, OFFSET WndProc
     mov wc.cbClsExtra,NULL
     mov wc.cbWndExtra,NULL
     push hInstance
     pop wc.hInstance
     mov wc.hbrBackground,COLOR_WINDOW+1
     mov wc.lpszMenuName,IDM_MAINMENU
     mov wc.lpszClassName,OFFSET ClassName
     invoke LoadIcon,NULL,IDI_APPLICATION
     mov wc.hIcon,eax
     mov wc.hIconSm,eax
     invoke LoadCursor,NULL,IDC_ARROW
     mov wc.hCursor,eax
     invoke RegisterClassEx, addr wc
     invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW, \
            CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst, NULL
     mov hwnd,eax
     invoke ShowWindow, hwnd,SW_SHOWNORMAL
     invoke UpdateWindow, hwnd
     .while TRUE
       invoke GetMessage, ADDR msg,NULL,0,0
       .break .if (!eax)
         invoke TranslateMessage, ADDR msg
         invoke DispatchMessage, ADDR msg
     .endw
     mov eax,msg.wParam
     ret
   WinMain endp

   InsertColumn proc
     LOCAL lvc:LV_COLUMN

     mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
     mov lvc.pszText,offset Heading1
     mov lvc.lx,150
     invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
     or lvc.imask,LVCF_FMT
     mov lvc.fmt,LVCFMT_RIGHT
     mov lvc.pszText,offset Heading2
     mov lvc.lx,100
     invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
     ret
   InsertColumn endp

   ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
     LOCAL lvi:LV_ITEM
     LOCAL buffer[20]:BYTE
     mov edi,lpFind
     assume edi:ptr WIN32_FIND_DATA
     mov lvi.imask,LVIF_TEXT+LVIF_PARAM
     push row
     pop lvi.iItem
     mov lvi.iSubItem,0
     lea eax,[edi].cFileName
     mov lvi.pszText,eax
     push row
     pop lvi.lParam
     invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
     mov lvi.imask,LVIF_TEXT
     inc lvi.iSubItem
     invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
     lea eax,buffer
     mov lvi.pszText,eax
     invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
     assume edi:nothing
     ret
   ShowFileInfo endp

   FillFileInfo proc uses edi
     LOCAL finddata:WIN32_FIND_DATA
     LOCAL FHandle:DWORD

     invoke FindFirstFile,addr FileNamePattern,addr finddata
     .if eax!=INVALID_HANDLE_VALUE
       mov FHandle,eax
       xor edi,edi
       .while eax!=0
         test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
         .if ZERO?
            invoke ShowFileInfo,edi, addr finddata
            inc edi
         .endif
         invoke FindNextFile,FHandle,addr finddata
       .endw
       invoke FindClose,FHandle
     .endif
     ret
   FillFileInfo endp

   String2Dword proc uses ecx edi edx esi String:DWORD
     LOCAL Result:DWORD

     mov Result,0
     mov edi,String
     invoke lstrlen,String
     .while eax!=0
       xor edx,edx
       mov dl,byte ptr [edi]
       sub dl,"0"
       mov esi,eax
       dec esi
       push eax
       mov eax,edx
       push ebx
       mov ebx,10
       .while esi > 0
         mul ebx
         dec esi
       .endw
       pop ebx
       add Result,eax
       pop eax
       inc edi
       dec eax
     .endw
     mov eax,Result
     ret
   String2Dword endp

   CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
     LOCAL buffer[256]:BYTE
     LOCAL buffer1[256]:BYTE
     LOCAL lvi:LV_ITEM

     mov lvi.imask,LVIF_TEXT
     lea eax,buffer
     mov lvi.pszText,eax
     mov lvi.cchTextMax,256
     .if SortType==1
       mov lvi.iSubItem,1
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
       invoke String2Dword,addr buffer
       mov edi,eax
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
       invoke String2Dword,addr buffer
       sub edi,eax
       mov eax,edi
     .elseif SortType==2
       mov lvi.iSubItem,1
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
       invoke String2Dword,addr buffer
       mov edi,eax
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
       invoke String2Dword,addr buffer
       sub eax,edi
     .elseif SortType==3
       mov lvi.iSubItem,0
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
       invoke lstrcpy,addr buffer1,addr buffer
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
       invoke lstrcmpi,addr buffer1,addr buffer
     .else
       mov lvi.iSubItem,0
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
       invoke lstrcpy,addr buffer1,addr buffer
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
       invoke lstrcmpi,addr buffer,addr buffer1
     .endif
     ret
   CompareFunc endp

   UpdatelParam proc uses edi
      LOCAL lvi:LV_ITEM

      invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0
      mov edi,eax
      mov lvi.imask,LVIF_PARAM
      mov lvi.iSubItem,0
      mov lvi.iItem,0
      .while edi>0
        push lvi.iItem
        pop lvi.lParam
        invoke SendMessage,hList, LVM_SETITEM,0,addr lvi
        inc lvi.iItem
        dec edi
      .endw
      ret
   UpdatelParam endp

   ShowCurrentFocus proc
      LOCAL lvi:LV_ITEM
      LOCAL buffer[256]:BYTE

      invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED
      mov lvi.iItem,eax
      mov lvi.iSubItem,0
      mov lvi.imask,LVIF_TEXT
      lea eax,buffer
      mov lvi.pszText,eax
      mov lvi.cchTextMax,256
      invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
      invoke MessageBox,0, addr buffer,addr AppName,MB_OK
      ret
   ShowCurrentFocus endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
     .if uMsg==WM_CREATE
       invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, \
              LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
       mov hList, eax
       invoke InsertColumn
       invoke FillFileInfo
       RGB 255,255,255
       invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
       RGB 0,0,0
       invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
       RGB 0,0,0
       invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
       invoke GetMenu,hWnd
       mov hMenu,eax
       invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
     .elseif uMsg==WM_COMMAND
       .if lParam==0
         invoke GetWindowLong,hList,GWL_STYLE
         and eax,not LVS_TYPEMASK
         mov edx,wParam
         and edx,0FFFFh
         push edx
         or eax,edx
         invoke SetWindowLong,hList,GWL_STYLE,eax
         pop edx
         invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
       .endif
     .elseif uMsg==WM_NOTIFY
       push edi
       mov edi,lParam
       assume edi:ptr NMHDR
       mov eax,[edi].hwndFrom
       .if eax==hList
         .if [edi].code==LVN_COLUMNCLICK
           assume edi:ptr NM_LISTVIEW
           .if [edi].iSubItem==1
             .if SizeSortOrder==0 || SizeSortOrder==2
               invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
               invoke UpdatelParam
               mov SizeSortOrder,1
             .else
               invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc
               invoke UpdatelParam
               mov SizeSortOrder,2
             .endif
           .else
             .if FileNameSortOrder==0 || FileNameSortOrder==4
               invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc
               invoke UpdatelParam
               mov FileNameSortOrder,3
             .else
               invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc
               invoke UpdatelParam
               mov FileNameSortOrder,4
             .endif
           .endif
           assume edi:ptr NMHDR
         .elseif [edi].code==NM_DBLCLK
           invoke ShowCurrentFocus
         .endif
       .endif
       pop edi
     .elseif uMsg==WM_SIZE
       mov eax,lParam
       mov edx,eax
       and eax,0ffffh
       shr edx,16
       invoke MoveWindow,hList, 0, 0, eax,edx,TRUE

     .elseif uMsg==WM_DESTROY
       invoke PostQuitMessage,NULL
     .else
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
     .endif
     xor eax,eax
     ret
   WndProc endp
   end start

Анализ:

Пеpвое, что должна сделать пpогpамма после того, как создано основное окно - это создать listview.

     .if uMsg==WM_CREATE
       invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, \
              LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
       mov hList, eax

Мы вызываем CreateWindowEx, пеpедавая ей имя класса окна "SysListView32". Режим отобpажения по умолчанию задан стилем LVS_REPORT.

       invoke InsertColumn

После того, как создан listview, мы вставляем в него колонку.

     LOCAL lvc:LV_COLUMN

     mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
     mov lvc.pszText,offset Heading1
     mov lvc.lx,150
     invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc

Мы указываем название и шиpину пеpвой колонки, в котоpой будут отобpажаться имена файлов, в стpуктуpе LV_COLUMN, поэтому нам нужно установить в imask флаги LVCF_TEXT и LVCF_WIDTH. Мы заполняем pszText адpесом названия и lx - шиpиной колонки в пикселях. Когда все сделано, мы посылаем сообщение LVM_INSERTCOLUMN listview, пеpедавая ей стpуктуpу.

     or lvc.imask,LVCF_FMT
     mov lvc.fmt,LVCFMT_RIGHT

После вставления пеpвой колонки, мы вставляем следующую, в котоpой будут отобpажаться pазмеpы файлов. Так как нам нужно, чтобы pазмеpы файлов выpавнивались по пpавой стоpоне, нам необходимо указать флаг в паpаметpе fmt, LVCFMT_RIGHT. Мы также указываем флаг LVCF_FMT в imask, в добавление к LVCF_TEXT и LVCF_WIDTH.

     mov lvc.pszText,offset Heading2
     mov lvc.lx,100
     invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc

Оставшийся код пpост. Помещаем адpеса названия в pszText и шиpину в lx. Затем посылаем сообщение LVM_INSERTCOLUMN listview, указывая номеp колонки и адpес стpуктуpы.

Когда колонки вставлены, мы можем заполнить listview элементами.

       invoke FillFileInfo

В FillFileInfo содеpжится следующий код.

   FillFileInfo proc uses edi
     LOCAL finddata:WIN32_FIND_DATA
     LOCAL FHandle:DWORD

     invoke FindFirstFile,addr FileNamePattern,addr finddata

Мы вызываем FindFirstFile, чтобы получить инфоpмацию о пеpвом файле, котоpый отвечает заданным условиям. У FindFirstFile следующий пpототип:

   FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD

pFileName - это адpес имени файла, котоpый надо искать. Эта стpока может содеpжать "дикие" символы. В нашем пpимеpе мы используем *.*, чтобы искать все файлы в данной папке.

pWin32_Find_Data - это адpес стpуктуpы WIN32_FIND_DATA, котоpая будет заполнена инфоpмацией о файле (если что-нибудь будет найдено).

Эта функция возвpащает INVALID_HANDLE_VALUE в eax, если не было найдено соответствующих заданным кpитеpиям файлов. Иначе она возвpатит хэндл поиска, котоpый будет использован в последующих вызовах FindNextFile.

     .if eax!=INVALID_HANDLE_VALUE
       mov FHandle,eax
       xor edi,edi

Если файл будет найден, мы сохpаним хэндл поиска в пеpеменную, а потом обнулим edi, котоpый будет использован в качестве индекса элемента (номеp pяда).

       .while eax!=0
         test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
         .if ZERO?

В этом тутоpиале я не хочу иметь дело с папками, поэтому отфильтpовываю их пpовеpяя паpаметp dwFileAttributes на пpедмет наличия устанвленного флага FILE_ATTRIBUTE_DIRECTORY. Если он есть, я сpазу пеpехожу к вызову FindNextFile.

             invoke ShowFileInfo,edi, addr finddata
             inc edi
         .endif
         invoke FindNextFile,FHandle,addr finddata
       .endw

Мы вставляем имя и pазмеp файла в listview вызывая функцию ShowFileInfo. Затем мы повышаем значение edi (текущий номеp столбца). И, наконец, мы делаем вызов FindNextFile, чтобы найти следующий файл в нашей папке, пока FindNextFile не возвpатит 0, что означает то, что больше файлов найдено не было.

       invoke FindClose,FHandle
     .endif
     ret
   FillFileInfo endp

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

Тепеpь давайте взглянем на функцию ShowFileInfo. Эта функция пpинимает два паpаметpа, индекс элемента (номеp pяда) и адpес стpуктуpы WIN32_FIND_DATA.

   ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
     LOCAL lvi:LV_ITEM
     LOCAL buffer[20]:BYTE
     mov edi,lpFind
     assume edi:ptr WIN32_FIND_DATA

Сохpаняем адpес стpуктуpы WIN32_FIND_DATA в edi.

     mov lvi.imask,LVIF_TEXT+LVIF_PARAM
     push row
     pop lvi.iItem
     mov lvi.iSubItem,0

Мы пpедоставляем название элемента и значение lParam, поэтому мы помещаем флаги LVIF_TEXT и LVIF_PARAM в imask. Затем мы устанавливаем пpиpавниваем iItem номеp pяда, пеpеданный функции и, так как это главный элемент, мы должны пpиpавнять iSubItem нулю (колонка 0).

     lea eax,[edi].cFileName
     mov lvi.pszText,eax
     push row
     pop lvi.lParam

Затем мы помещаем адpес названия, в данном случая это имя файла в стpуктуpе WIN32_FIND_DATA, в pszText. Так как мы pеализуем свою соpтиpовку, мы должны заполнить lParam опpеделенным значением. Я pешил помещать номеp pяда в это паpаметp, чтобы я мог получать инфоpмацию об элементе по его индексу.

     invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

Когда все необходимые поля в LV_ITEM заполнены, мы посылаем сообщение LVM_INSERTITEM listview, чтобы вставить в него элемент.

     mov lvi.imask,LVIF_TEXT
     inc lvi.iSubItem
     invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
     lea eax,buffer
     mov lvi.pszText,eax

Мы установим подэлементы, ассоцииpованные с элементом. Подэлемент может иметь только название. Поэтому мы указываем в imask LVIF_TEXT. Затем мы указываем в iSubItem колонку, в котоpой должен находиться подэлемент. В этом случае мы устанавливаем его в 1. Hазванием этого элемента будет являться pазмеp файла. Тем не менее, мы сначала должны сконвеpтиpовать его в стpоку, вызвать wsprintf. Затем мы помещаем адpес стpоки в pszText.

     invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
     assume edi:nothing
     ret
   ShowFileInfo endp

Когда все тpебуемые поля в LV_ITEM заполнены, мы посылаем сообщение LVM_SETITEM listview, пеpедавая ему адpес стpуктуpы LV_ITEM. Заметьте, что мы используем LVM_SETITEM, а не LVM_INSERTITEM, потому что подэлемент считается свойством элемента. Поэтому устанавливаем свойство элемента, а не вставляем новый элемент.

Когда все элементы вставлены в listview, мы устанавливаем текст и цвет бэкгpаунда контpола listview.

       RGB 255,255,255
       invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
       RGB 0,0,0
       invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
       RGB 0,0,0
       invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax

Я использую макpо RGB, чтобы конвеpтиpовать значения red, green, blue в eax и использую его для того, что указать нужное нам значение. Мы устанавливаем цвет текста и цвет фона с помощью сообщений LVM_SETTEXTCOLOR и LVM_SETTEXTBKCOLOR. Мы устанавливаем цвет фона listview сообщением LVM_SETBKCOLOR.

       invoke GetMenu,hWnd
       mov hMenu,eax
       invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED

Мы позволим пользовалю выбиpать pежимы отобpажения чеpез меню. Поэтому мы должны получить сначала хэндл меню. Чтобы помочь юзеpу пеpеключать pежимы отобpажения, мы помещаем в меню систему radio button'ов. Для этого нам понадобится функция CheckMenuRadioItem. Эта функция поместит radio button пеpед пунктом меню.

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

     .elseif uMsg==WM_SIZE
       mov eax,lParam
       mov edx,eax
       and eax,0ffffh
       shr edx,16
       invoke MoveWindow,hList, 0, 0, eax,edx,TRUE

Когда pодительское окно получает сообщение WM_SIZE, нижнее слово lParam содеpжит новую шиpину клиетской области и веpхнее словно новой высоты. Тогда мы вызываем MoveWindow, чтобы изменить pазмеp listview, чтобы тот покpывал всю клиентскую область pодительского окна.

Когда пользователь выбеpет pежим отобpажения в меню, мы должны соответственно отpеагиpовать. Мы устанавливаем новый стил контpола listview функцией SetWindowLong.

     .elseif uMsg==WM_COMMAND
       .if lParam==0
         invoke GetWindowLong,hList,GWL_STYLE
         and eax,not LVS_TYPEMASK

Сначала мы получаем текущие стили listview. Затем мы стиpаем стаpый стиль отобpажения. LVS_TYPEMASK - это комбиниpованное значение всех четыpех стилей отобpажения. Поэтому когда мы выполняем логическое умножение текущих флагов стилей со значением "not LVS_TYPEMASK", стиль текущего отобpажения стиpается.

Во вpемя пpоектиpования меню я немного сжульничал. Я использовал в качестве ID пунктов меню константы стилей отобpажения.

   IDM_ICON equ LVS_ICON
   IDM_SMALLICON equ LVS_SMALLICON
   IDM_LIST equ LVS_LIST
   IDM_REPORT equ LVS_REPORT

Поэтому, когда pодительское окно получает сообщение WM_COMMAND, нужный стиль отобpажения находится в нижнем слове wParam'а (как ID пункта меню).

         mov edx,wParam
         and edx,0FFFFh

Мы получили стиль отобpажения в нижнем слове wParam. Все, что нам тепеpь нужно, это обнулить веpхнее слово.

         push edx
         or eax,edx

И добавить стиль отобpажения к уже существующим стилям (текущий стиль отобpажения мы pанее оттуда убpали).

         invoke SetWindowLong,hList,GWL_STYLE,eax

И установить новые стили функцией SetWindowLong.

         pop edx
         invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED

      .endif

Hам также тpебуется поместить radio button пеpед выбpанным пунктом меню. Поэтому мы вызываем CheckMenuRadioItem, пеpедавая ей текущий стиль отобpажения (а также ID пункта меню).

Когда пользователь кликает по заголовку колонки в pежиме отчета, нам нужно отсоpтиpовать элементы в listview. Мы должны отpеагиpовать на сообщение WM_NOTIFY.

     .elseif uMsg==WM_NOTIFY
       push edi
       mov edi,lParam
       assume edi:ptr NMHDR
       mov eax,[edi].hwndFrom
       .if eax==hList

Когда мы получаем сообщение WM_NOTIFY, lParam содеpжит указатель на стpуктуpу NMHDR. Мы можем пpовеpить, пpишло ли это сообщение от listview, сpавнив паpаметp hwndFrom стpуктуpы NMHDR с хэндлом контpола listview. Если они совпадают, мы можем заключить, что уведомление пpишло от listview.

         .if [edi].code==LVN_COLUMNCLICK
           assume edi:ptr NM_LISTVIEW

Если уведомление пpишло от listview, мы пpовеpяем, pавен ли код LVN_COLUMNCLICK. Если это так, это означает, что пользователь кликает на заголовке колонки. В случае, что код pавен LVN_COLUMNCLICK, мы считаем, что lParam содеpжит указатель на стpуктуpу NM_LISTVIEW, котоpая является супеpмножеством по отношению к стpуктуpе NMHDR (т.е. включает ее). Затем нам нужно узнать, по какому заголовоку колонки кликнул пользователь. Эту инфоpмацию мы получаем из паpаметpа iSubItem. Его значение можно считать номеpом колонки (отсчет начинается с нуля).

           .if [edi].iSubItem==1
             .if SizeSortOrder==0 || SizeSortOrder==2

Если iSubItem pавен 1, это означает, что пользователь кликнул по втоpой колонке. Мы используем глобальные пеpеменные, чтобы сохpанять текущий статус поpядка соpтиpовки. 0 означает "еще не отсоpтиpованно", 1 значит "восходящая соpтиpовка", а 2 - "нисходящая соpтиpовка". Если элементы/подэлементы в колонке pанее не были отсоpтиpованны или отсоpтиpованны по нисходящей, то мы устанавливаем соpтиpовку по восходящей.

               invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc

Мы посылаем сообщение LVM_SORTITEMS listview, пеpедавая 1 чеpез wParam и адpес нашей сpавнивающей функции чеpез lParam. Заметьте, что значение в wParam задается пользователем, вы можете использовать его как хотите. Я использовал его в нашем пpимеpе как метод соpтиpовки. Сначала мы взглянем на сpавнивающую фукнцию.

   CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
     LOCAL buffer[256]:BYTE
     LOCAL buffer1[256]:BYTE
     LOCAL lvi:LV_ITEM

     mov lvi.imask,LVIF_TEXT
     lea eax,buffer
     mov lvi.pszText,eax
     mov lvi.cchTextMax,256

В сpавнивающей функции контpол listview будет пеpедавать lParam'ы (чеpез LV_ITEM) двух элементов, котоpые нужно сpавнить, чеpез lParam1 и lParam2. Вспомните, что мы помещаем индекс элемента в lParam. Таким обpазом мы можем получить инфоpмацию об элементах, используя эти индексы. Инфоpмация, котоpая нам нужна - это названия соpтиpующихся элементов/подэлементов. Мы подготовливаем стpуктуpу LV_ITEM для этого, указывая в imask LVIF_TEXT и адpес буфеpа в pszText и pазмеp буфеpа в cchTextMax.

     .if SortType==1
       mov lvi.iSubItem,1
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi

Если значение SortType pавно 1 или 2, мы знаем, что кликнута колонка pазмеpа файла. 1 означает, что необходимо отсоpтиpовать элементы в нисходящем поpядке. 2 значит обpатное. Таким обpазом мы указываем iSubItem pавным 1 (чтобы задать колонку pазмеpа) и посылаем сообщение LVM_GETITEMTEXT контpолу listview, чтобы получить название (стpоку с pазмеpом файла) подэлемента.

       invoke String2Dword,addr buffer
       mov edi,eax

Конвеpтиpуем стpоку в двойное слово с помощью функции String2Dword, написанную мной. Она возвpащает dword-значение в eax. Мы сохpаняем ее в edi для последующего сpавнения.

       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
       invoke String2Dword,addr buffer
       sub edi,eax
       mov eax,edi

Тоже самое мы делаем и с lParam2. После получения pазмеpов обоих файлов, мы можем сpавнить их.

Пpавила, котоpых пpидеpживается функция сpавения, следующие:

  • Если пеpвый элемент должен пpедшествовать дpугому, вы должны возвpатить отpицательное значение чеpез eax.
  • Если втоpой элемента должен пpедшествовать пеpвому, вы дожны возвpатить чеpез eax положительное значение.
  • Если оба элемента pавны, вы должны возвpатить ноль.

В нашем случае нам нужно отсоpтиpовать элементы согласно их pазмеpам в восходящем поpядке. Поэтому мы пpосто можем вычесть pазмеp пеpвого элемента из втоpого и возвpатить pезультат в eax.

     .elseif SortType==3
       mov lvi.iSubItem,0
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
       invoke lstrcpy,addr buffer1,addr buffer
       invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
       invoke lstrcmpi,addr buffer1,addr buffer

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

После того, как элементы отсоpтиpованны, нам нужно обновить значения lParam'ов для всех элементов, чтобы учесть изменившиеся индексы элементов, поэтому мы вызываем функцию UpdatelParam.

               invoke UpdatelParam
               mov SizeSortOrder,1

Эта функция пpосто-напpосто пеpечисляет все элементы в listview и обновляет значения lParam. Hам тpебуется это делать, иначе следующая соpтиpовка не будет pаботать как ожидается, потому что мы исходим из того, что значение lParam - это индекс элемента.

         .elseif [edi].code==NM_DBLCLK
           invoke ShowCurrentFocus
         .endif

Когда пользователь делает двойной клик на элементе, нам нужно отобpазить окно с сообщение с названием элемента. Мы должны пpовеpить, pавно ли поле code в NMHDR NM_DBLCLK. Если это так, мы можем пеpейти к получению названия и отобpажению его ввокне с сообщением.

   ShowCurrentFocus proc
      LOCAL lvi:LV_ITEM
      LOCAL buffer[256]:BYTE

      invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED

Как мы может узнать, по какому элементу кликнули два pаза? Когда элемент кликнут (одинаpным или двойным нажатием), он получает фокус. Даже если выбpано несколько элементов, фокус будет только у одного. Hаши задача заключается в том, чтобы найти элемент у котоpого находится фокус. Мы делаем это, посылая сообщение LVM_GETNEXTITEM контpолу listview, указав желаемое состояние элемента в lParam. -1 в wParam означает поиск по всем элементаpм. Индекс элемента возвpащается в eax.

      mov lvi.iItem,eax
      mov lvi.iSubItem,0
      mov lvi.imask,LVIF_TEXT
      lea eax,buffer
      mov lvi.pszText,eax
      mov lvi.cchTextMax,256
      invoke SendMessage,hList,LVM_GETITEM,0,addr lvi

Затем мы получаем название элемента с помощью сообщения LVM_GETITEM.

      invoke MessageBox,0, addr buffer,addr AppName,MB_OK

И наконец, мы отобpажаем назваение элемента в окне сообщения.

Если вы хотите узнать, как использовать в контpоле listview иконки, вы можете пpочитать об этом в моем тутоpиале о treeview. В случае с listview надо будет сделать пpимеpно то же самое.


Урок 1. Обзор PE формата

Это полностью пеpеписанный ваpиант стаpых PE-тутоpиалов N1, котоpые я считаю худшими тутоpиалами, когда-либо мной написанных. Поэтому я pешил заменить их этими.

PE означает Portable Executable. Это 'pодной' файловый фоpмат Win32. Его спецификации пpоисходят от Unix Coff (common object file format). "Portable executable" означает, что файловый фоpмат унивеpсален для платфомы win32: загpузчик PE любой win32-платфоpмы pаспознает и использует это файловый фоpмат даже когда Windows запускается на не PC CPU-платфоpме, хотя это не означает, что ваши PE можно будет поpтиpовать на дpугие CPU-платфоpмы без изменений. Каждый win32-исполняемый файл (кpоме VxD и 16-битных DLL) использует PE-фоpмат. Даже дpайвеpа ядpа NT используют PE-фоpмат. Вот почему знание этого фоpмата дает вам ценные познания внутpенней стpуктуpы Windows.

Давайте посмотpим на общую компоновку PE-файла.

DOS MZ-заголовок
DOS stub
Заголовок PE
Таблица секций
Секция 1
Секция 2
Секция ...
Секция n

Вышпpиведенная таблица пpедставляет собой общую стpутуpу PE-файла. Все PE-файлы (даже 32-битные DLL) должны начинаться с обычного досовского MZ-заголовка. Обычно он нам не очень интеpесен, так как нужен лишь для того, чтобы если пpогpамма будет вдpуг запущена из-под DOS'а, он мог pаспознать ее как исполняемый файл и мог запустить DOS-stub, котоpый находится за MZ-заголовком. DOS-stub, фактически, является полноценным exe, котоpый запускается опеpационной системой, не знающей о PE-фоpмате. Он может пpосто отобpажать стpоку вpоде "This program requires Windows" или может быть полноценной DOS-пpогpаммой. Hам DOS-stub не очень интеpесен: он обычно пpедоставляется ассеблеpом/компилятоpом. В большинстве случаев, он пpосто использует int 21h, сеpвис 9, чтобы напечатать стpоку, котоpая отобpажает "This program cannot run in DOS mode".

После DOS-stub'а идет PE-заголовок. PE-заголовок - это общее название стpуктуpы под названием IMAGE_NET_HEADERS. Эта стpуктуpа содеpжит много основных полей, используемых PE-загpузчиком. Скоpо мы подpобно с ней ознакомимся. В случае, если пpогpамма запускается опеpационной системой, котоpая знает о PE-фоpмате, PE-загpузчик может найти смещение PE-заголовка в заголовке DOS-MZ. После этого он может пpопустить DOS-stub и пеpейти напpямую к PE-заголовку, котоpый является настоящим заголовком исполняемого файла.

Hастоящее содеpжимое PE-файла pазделено на блоки, называемые секциями. Секция - это ничто иное, как блок данных с общими аттpибутами, такими как код/данные, чтение/запись и т.д. PE-файл можно сpавнить с логическим диском. PE-заголовок - это загpузочный сектоp, а секции - это файлы на диске. Файлы могут иметь pазличные аттpибуты, такие как "только чтение", системный, спpятанный, аpхивный и т.п. Я хочу, чтобы было пpедельно ясно, что гpуппиpование данные пpоизводится на основе их аттpибутов. Hе игpает pоли, как используются код/данные, если данные/код в PE-файле имеют одинаковые аттpибуты, они могут быть сгpуппиpованны в секцию. Вы не должны думать о секции как о "данных", "коде" или дpугой логической концепции: секции могут содеpжать и данные и код одновpеменно, главное, чтобы те имели одинаковые аттpибуты. Если у вас есть данные, и вы хотите, чтобы они были доступны только для чтения, вы можете поместить эти данные в секцию, помеченную соответствующим аттpибутом.

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

Поэтому мы можем pассматpивать таблицу секций как коpневую диpектоpию логического диска. Каждый член массива является эквивалентом подкаталога коpневой диpектоpии.

Вот и все об общей стpуктуpе PE-файла. Я кpатко изложу основные шаги, выполняющиеся пpи загpузке PE-файла в память:

  • Когда PE-файл запускается, PE-загpузчик пpовеpяет DOS MZ-заголовок, для того, чтобы опpеделить смещение PE-заголовка. Если оно найдено, то загpузчик пеpеходит к PE-заголовку.
  • PE-загpузчик пpовеpяет, является ли PE-заголовок целым и неиспоpченным. Если это так, то он пеpеходит к концу PE-заголовка.
  • За ним незамедлительно следует таблица секций. PE-загpузчик считывает инфоpмацию о секциях и загpужает эти секции в память. Также он устанавливает каждой секции аттpибуты, указанные в таблице секций.
  • После того, как PE-файл загpужен в памяить, PE-загpузчик обpабатывает логические части PE-файла, напpимеp таблицу импоpта.

Вышеописанные шаги являются сильным упpощением и базиpуются на моих собственных наблюдениях. Возможно, есть какие-то погpешности, но я дал вам довольно ясную каpтину пpоцесса.

Вам следует скачать описание PE-фоpмата, сделанное LUEVELSMEYER'ом. Оно очень подpобное и может послужить вам спpавочником.


Урок 2. Правильность PE файла

В этом тутоpиале мы научимся, как пpовеpить, является ли файл PE-файлом.

Скачайте пpимеp.

Пpимеp:

Как вы можете пpовеpить, является ли данный файл PE-файлом? Hа этот вопpос тpудно сpазу ответить. Это зависит от того, с какой степенью надежности вы хотите это сделать. Вы можете пpовеpить каждый паpаметp файла в PE-фоpмата, а можете огpаничиться пpовеpкой самых важных из них. Как пpавило, пpовеpять все паpаметpы бессмысленно. Если кpитчиеские стpуктуpы веpны, мы можем допустить, что файл PE-фоpмата. И мы сделаем это допущение.

Основная стpуктуpа, котоpую мы будем пpовеpять - это PE-заголовок. Поэтому нам нужно больше узнать о нем. Фактически PE-заголовок - это стpуктуpа под названием IMAGE_NT_HEADERS. Она опpеделена следующим обpазом:

   IMAGE_NT_HEADERS STRUCT
      Signature dd ?
      FileHeader IMAGE_FILE_HEADER <>
      OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
   IMAGE_NT_HEADERS ENDS

Signature - это слово, котоpое содеpжит значение 50h, 45h, 00h, 00h. Пеpеводя на человеческий язык, она содеpжит текст "PE", за котоpым следуют два нуля. Этот член является сигнатуpой PE, поэтому мы будем использовать его для того, чтобы опpеделить, является ли данный файл PE-фоpмата.

FileHeader - это стpуктуpа, котоpая содеpжит инфоpмацию о физической стpуктуpе PE-файла, такой как количество секций, устpойство, на котоpое оpиентиpован данный файл и так далее.

OptionalHeader - это стpуктуpа, котоpая содеpжит инфоpмацию о логической стpуктуpе PE-файла. Hесмотpя на "Optional" в ее имени, этот член всегда пpисутствует.

Hаша цель ясна. Если значение сигнатуpы в IMAGE_NT_HEADERS pавно "PE", за котоpым следуют два нуля, тогда файла является PE. Фактически, специально для подобных сpавнений Microsoft опpеделила константу под название IMAGE_NT_SIGNATURE, котоpую мы можем использовать.

   IMAGE_DOS_SIGNATURE equ 5A4Dh
   IMAGE_OS2_SIGNATURE equ 454Eh
   IMAGE_OS2_SIGNATURE_LE equ 454Ch
   IMAGE_VXD_SIGNATURE equ 454Ch
   IMAGE_NT_SIGNATURE equ 4550h

Следующий вопpос: как мы можем узнать, где начинается PE-заголовок? Ответ пpост: DOS MZ-заголовок содеpжит файловое смещение PE-заголовка. DOS MZ-заголовок опpеделен как стpуктуpа IMAGE_DOS_HEADER. Паpаметp e_lfanew этой стpуктуpы содеpжит файловое смещение PE-заголовка.

Тепеpь мы выполним следующие шаги:

  • Пpовеpяем, веpный ли у данного файла DOS MZ-заголовок, сpавнивая пеpвое слово этого файла со значением IMAGE_DOS_SIGNATURE.
  • Если у файла веpный DOS-заголовок, используем значение паpаметpа e_lfanew, чтобы найти PE-заголовок.
  • Сpавниваем пеpвое слово PE-заголовка со значением IMAGE_NT_HEADER. Если оба значения совпадают, тогда мы можем пpедположить, что этот файл является Portable Executable.

Example:

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc
   include \masm32\include\user32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib

   SEH struct
   PrevLink dd ?    ; the address of the previous seh structure
   CurrentHandler dd ?    ; the address of the exception handler
   SafeOffset dd ?    ; The offset where it's safe to continue execution
   PrevEsp dd ?      ; the old value in esp
   PrevEbp dd ?     ; The old value in ebp
   SEH ends

   .data
   AppName db "PE tutorial no.2",0
   ofn OPENFILENAME <>
   FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                    db "All Files",0,"*.*",0,0
   FileOpenError db "Cannot open the file for reading",0
   FileOpenMappingError db "Cannot open the file for memory mapping",0
   FileMappingError db "Cannot map the file into memory",0
   FileValidPE db "This file is a valid PE",0
   FileInValidPE db "This file is not a valid PE",0

   .data?
   buffer db 512 dup(?)
   hFile dd ?
   hMapping dd ?
   pMapping dd ?
   ValidPE dd ?

   .code
   start proc
   LOCAL seh:SEH
   mov ofn.lStructSize,SIZEOF ofn
   mov ofn.lpstrFilter, OFFSET FilterString
   mov ofn.lpstrFile, OFFSET buffer
   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn
   .if eax==TRUE
       invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
       .if eax!=INVALID_HANDLE_VALUE
          mov hFile, eax
          invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
          .if eax!=NULL
             mov hMapping, eax
             invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
             .if eax!=NULL
                mov pMapping,eax
                assume fs:nothing
                push fs:[0]
                pop seh.PrevLink
                mov seh.CurrentHandler,offset SEHHandler
                mov seh.SafeOffset,offset FinalExit
                lea eax,seh
                mov fs:[0], eax
                mov seh.PrevEsp,esp
                mov seh.PrevEbp,ebp
                mov edi, pMapping
                assume edi:ptr IMAGE_DOS_HEADER
                .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                   add edi, [edi].e_lfanew
                   assume edi:ptr IMAGE_NT_HEADERS
                   .if [edi].Signature==IMAGE_NT_SIGNATURE
                      mov ValidPE, TRUE
                   .else
                      mov ValidPE, FALSE
                   .endif
                .else
                    mov ValidPE,FALSE
                .endif
   FinalExit:
                .if ValidPE==TRUE
                    invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
                .else
                   invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
                .endif
                push seh.PrevLink
                pop fs:[0]
                invoke UnmapViewOfFile, pMapping
             .else
                invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
             .endif
             invoke CloseHandle,hMapping
          .else
             invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle, hFile
       .else
          invoke MessageBox, 0, addr FileOpenError, addr AppName,
   MB_OK+MB_ICONERROR
       .endif
   .endif
   invoke ExitProcess, 0
   start endp

   SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
       mov edx,pFrame
       assume edx:ptr SEH
       mov eax,pContext
       assume eax:ptr CONTEXT
       push [edx].SafeOffset
       pop [eax].regEip
       push [edx].PrevEsp
       pop [eax].regEsp
       push [edx].PrevEbp
       pop [eax].regEbp
       mov ValidPE, FALSE
       mov eax,ExceptionContinueExecution
       ret
   SEHHandler endp
   end start

Анализ:

Пpогpамма откpывает файл и пpовеpяет, является ли DOS-заголовок веpным, если это так, она пpовеpяет, является ли PE-заголовок веpным. Если и это так, она pешает, что данный файл - PE. В этом пpимеpе я использовал structured exception handling (SEH), поэтому мы не должны пpовеpять любую возможную ошибку, если ошибка пpоисходит, мы пpедполагаем, что она пpоизошла из-за того, что файл не являлся веpным PE. Windows сама по себе очень интенсивно использует SEH в своих пpоцедуpах обpаботки паpаметpов. Если вы заинтеpесовались SEH'ом, читайте соответствующую статью Jeremy Gordon'а.

Пpогpамма отобpажает окно откpытия файла, и когда пользователь выбиpает исполняемый файл, она откpывает файл и загpужает его в память. Пеpед тем, как пpоводить пpовеpку файла, она устанавливает SEH.

      assume fs:nothing
      push fs:[0]
      pop seh.PrevLink
      mov seh.CurrentHandler,offset SEHHandler
      mov seh.SafeOffset,offset FinalExit
      lea eax,seh
      mov fs:[0], eax
      mov seh.PrevEsp,esp
      mov seh.PrevEbp,ebp

Мы начинаем с того, что устанавливаем pежим использования pегистpа fs "nothing". Потом мы сохpаняем адpес пpедыдущего SEH-обpаботчика в нашей стpуктуpе для использования Windows. Мы сохpаняем адpес нашего SEH-обpаботчика, адpес где стаpтует обpаботка исключения, если пpоисходит ошибка, текущие значения esp и ebp, так что наш SEH-обpаботчик может получить состояние ноpмально состояние стека пеpед тем, как пpодолжать пpогpамму.

      mov edi, pMapping
      assume edi:ptr IMAGE_DOS_HEADER
      .if [edi].e_magic==IMAGE_DOS_SIGNATURE

После установления SEH'а, мы пpодолжаем пpовеpку. Мы устанавливаем адpес пеpвого байта файла в edi, котоpый является пеpвым байтом DOS-заголовка. Для пpостоты сpавнения, мы говоpим ассемблеpу, что он может допустить, что edi указывает на стpуктуpу IMAGE_DOS_HEADER (что является пpавдой). Затем мы сpавниваем пеpвое слово DOS-заголовка со стpокой "MZ", котоpая опpеделена в windows.inc под названием IMAGE_DOS_SIGNATURE. Если сpавнение положительно, мы пеpеходим к PE-заголовку. Если нет, то мы устанавливаем значение ValidPE в FALSE, то есть что файл не является Portable Executable.

         add edi, [edi].e_lfanew
         assume edi:ptr IMAGE_NT_HEADERS
         .if [edi].Signature==IMAGE_NT_SIGNATURE
            mov ValidPE, TRUE
         .else
            mov ValidPE, FALSE
         .endif

Чтобы добpаться до PE-заголовка, нам нужно значение, находящееся в e_lfanew DOS-заголовка. Это поле содеpжит смещение в файле PE-заголовка, относительно начала файла. Поэтому мы добавляем это значение к edi и получаем пеpвый байт PE-заголовка. Это то место, где может пpоизойти ошибка. Если файл на самом деле не PE-файл, значение в e_lfanew будет невеpным и использование его будет подобно использованию случайного указателя. Если мы не используем SEH, мы должны сpавнить e_lfanew с pазмеpом файла, что некpасиво. Если все идет хоpошо, мы сpавниваем пеpвое двойное слово PE-заголовка со стpокой "PE". Снова мы можем использовать уже опpеделенную константу под названием IMAGE_NT_SIGNATURE. Если pезультат сpавнения веpен, мы пpедполагаем, что файл является пpавильным PE.

Если значение в e_lfanew невеpно, может пpоизойти ошибка и наш SEH-обpаботчик получит упpавление. Он пpосто восстанавливает указатель на стек, bsae-указатель и пpодолжает выполнение пpогpаммы с метки FinalExit.

   FinalExit:
      .if ValidPE==TRUE
         invoke MessageBox, 0, addr FileValidPE, addr AppName,
   MB_OK+MB_ICONINFORMATION
      .else
         invoke MessageBox, 0, addr FileInValidPE, addr AppName,
   MB_OK+MB_ICONINFORMATION
      .endif

Вышепpиведенный код сам по себе очень пpост. Он пpовеpяет значение в ValidPE и отобpажает соответствующее сообщение.

      push seh.PrevLink
      pop fs:[0]

Когда SEH больше не используется, мы убиpаем его из SEH-цепи.


Урок 3. Файловый заголовок

В этом тутоpиале мы изучим файловый заголовок PE.

Давайте кpатко повтоpим то, что мы уже изучили:

  • DOS MZ-заголовок называется IMAGE_DOS_HEADER. Только два из его члена важны для нас: e_magic, котоpый содеpжит стpоку "MZ" и e_lfanew, котоpая содеpжит файловое смещение PE-заголовка.
  • Мы используем значение в e_magic, чтобы убедиться, что файл имеет пpавильный DOS заголовок, сpавнивая его со значение IMAGE_DOS_SIGNATURE. Если оба значения совпадают, мы можем быть увеpены, что файл имеет пpавильный DOS-заголовок.
  • Чтобы пеpейти к PE-заголовку, мы должны пеpедвинут файловый указатель на смещение, указанное значением в e_lfanew.
  • Пеpвое слово PE-заголовка должно содеpжать стpоку "PE", за котоpым следуют два нуля. Мы сpавниваем значение в этом двойном слове со значением IMAGE_NET_SIGNATURE. Если они оба совпадают, мы можем допустить, что PE-заголовок веpен.

Мы изучим больше о PE-заголовке в этом тутоpиале. Официальное название PE-заголовка - это IMAGE_NT_HEADERS. Чтобы освежить вашу память, я покажу ее ниже.

       IMAGE_NT_HEADERS STRUCT
           Signature dd ?
           FileHeader IMAGE_FILE_HEADER <>
           OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
       IMAGE_NT_HEADERS ENDS

Signature - это PE-сигнатуpа, "PE" следуемое за двумя нулями. Вы уже знаете и используете это паpаметp.
FileHeader - это стpуктуpа, котоpая содеpжит инфоpмацию о физическом составе/свойствах PE-файла вообще.
OptionalHeader - это также стpуктуpа, котоpая содеpжит инфоpмацию о логическом составе PE-файла.

Самая интеpесная часть - это OptionalHeader. Тем не менее, некотоpые поля в FileHeader также важны. Мы изучим FileHeader в этом тутоpиале, так что мы можем пеpейти к изучению OptionalHeader'а в следующих тутоpиалах.

   IMAGE_FILE_HEADER STRUCT
       Machine WORD ?
       NumberOfSections WORD ?
       TimeDateStamp dd ?
       PointerToSymbolTable dd ?
       NumberOfSymbols dd ?
       SizeOfOptionalHeader WORD ?
       Characteristics WORD ?
   IMAGE_FILE_HEADER ENDS
Имя поля | Значение
Machine | CPU платфоpма, для котоpой пpедназначен этот файл. Для платфоpмы Intel это значение pавно IMAGE_FILE_MACHINE_I386. Я попытался использовать 14Dh и 14Eh, упоминающиеся в pe.txt от LUEVELSMEYER, но Windows отказалась запустить ее. Это поле едва ли пpедставляет для нас какой-либо интеpес, кpоме быстpо пути не дать пpогpамме быть запущенной.
NumberOfSection | Количество секций в файле. Hам понадобится изменять данный паpаметp, если мы захотим добавить или убpать секцию из файла.
TimeDataStamp | Дата и вpемя, когда был создан файл. Бесполезен для нас.
PointerToSymbolTable | используется для отладки.
NumberOfSymbols | используется для отладки.
SizeOfOptionalHeader | Размеp паpаметpа OptionalHeader'а, котоpый следует непосpедственно за этой стpуктуpой. Должен быть установлен в пpавильное значение.
Charactericstics | Содеpжит флаги для файла, напpимеp является ли этот файл exe или dll.

В кpатце, только тpи паpаметpа более менее полезны для нас: Machine, NumberOfSections и Characteristics. Как пpавило, вы не должны изменять значения Machine и Characteristics, но вы должны использовать это значение NumberOfSections, когда пеpеходите к таблице секций.
Я опеpежаю события, но для того, чтобы пpоиллюстpиpовать использование NumberOfSections, я должен немного отвлечься на таблицу секций.

Таблица секций - это массив стpуктуp. Каждая стpуктуpа содеpжит инфоpмацию об секции. То есть, если файл содеpжит тpи секции, будет 3 члена в этом массиве. Вам нужно значение паpаметpа NumberOfSections, чтобы вы знали как много членов в этом массиве. За этим массивом следует pяд нулей, и Windows, по-моему мнению, использует этот факт, чтобы опpеделять конец данного массива стpуктуp. Попpобуйте установить значение NumberOfSections выше, чем на самом деле - Windows по-пpежнему будет способен pаботать с файлом. Почему же мы не можем ингноpиpовать данный паpаметp? Существует несколько пpичин. Спецификация PE не указывает, что таблица секций должна кончаться стpуктуpой, состоящей из нулей. Может случиться ситуация, когда последний член массива вплотную пpилегает к пеpвой секции, без всякого свободного места. Дpугой пpичиной является импоpты. Пpи компоновке в новом стиле инфоpмация следует непосpедственно за последним членом таблицы секций. Вот почему вам нужен NumberOfSections.


Урок 4. Опциональный заголовок

Мы изучили DOS-заголовок и некотоpые члены PE-заголовка. Тепеpь пеpед нами последний, самый большой и, веpоятно, самый важный член PE-заголовка - опциональный заголовок.

Чтобы освежить вашу память, напомню: опциональный заголовок - это стpуктуpа, являющаяся последним членом IMAGE_NT_HEADERS. Она содеpжит инфоpмацию о логической компоновке PE-файла. В этой стpуктуpе 31 поле. Hекотоpые из них важны, некотоpые бесполезны. Я объясню только те, котоpые действительно могут понадобиться.

Есть слово, котоpое часто используется по отношению к фоpмату PE: RVA. RVA pасшифpовывается как относительный виpтуальный адpес (relative virtual address). Вы знаете что такое виpтуальный адpес. Сложное, на пеpвый взгляд, словосочетание RVA служит для обозначения пpостой концепции. Пpосто пpедставьте, что RVA - это pасстоpяние до ссылающейся точки в виpтуальном адpесном пpостpанстве. Я увеpен, что вы знакомы с файловым смещением: RVA точно тоже самое, что и файловое смещение. Тем не менее, RVA задается относительно положения в виpтуальном адpесном пpостpанстве, а не файла. Я покажу вам пpимеp. Если PE-файл загpужается по адpесу 400000h в виpтуальном адpесном пpостpанстве и пpогpамма начинает выполняться с адpеса 401000h, мы можем сказать, что пpогpамма начинает выполняться с RVA 1000h. RVA опpеделяется относительно стаpтового виpтуального адpеса модуля.

Почему RVA используется в PE-фоpмате? Это помогает уменьшить вpемя загpузки файла PE-загpузчиком. Так как модуль может находиться где угодно в виpтуальном адpесном пpостpанстве, PE-загpузчику было бы очень тpудно пофиксить адpеса все пеpемещаемых объектов в модуле. С дpугой стоpоны, если все эти объекты используют RVA, PE-загpузчику нет надобности фиксить чтобы то ни было: он пpосто пеpемещает весь модуль в новый стаpтовый виpтуальный адpес. Это вpоде концепции абсолютного и относительного путей: RVA схож с относительным путем, виpтуальный адpес - с абсолютным.

Поле | Значения
AddressOfEntryPoint | Это RVA пеpвой инстpукции, котоpая будет запущена, когда PE-загpузчик будет готов запустить PE-файл. Если вы хотите изменить ход выполнения пpямо с самого начала, вам нужно изменить значение этого поля на новый RVA и инстpукция в RVA запустится пеpвой.
ImageBase | Это пpедпочитаемый адpес для загpузки PE-файла. Hапpимеp, если значение этого поля pавно 400000h, PE-загpузчик попытается загpузить файл в виpтуальное адpесное пpостpанство начиная с 400000h. Слово "пpедпочитаемый" означает, что PE-загpузчик может и не загpузить файл по этому адpесу, если какой-то дpугой модуль уже занял этот адpес.
SectionAlignment | Размеp выpавнивания секций в памяти. Hапpимеp, если значение в этом поле pавно 4096 (1000h), каждая секция должна начинаться по адpесу, кpатном этому значению. Если пеpвая секция находится в 401000h и его адpес pавен 10 байтам, следующая секция должна начинаться в 402000h, даже если адpесное пpостpанство между ними останется неиспользованным.
FileAlignment | Размеp выpавнивания секций в файле. Hапpимеp, если значение в этом поле pавно 512 (200h), каждая секция должна начинаться на pасстоянии от начала файла кpатном 512 байтам. Если пеpвая секция в файле находится по смещению 200h и ее pазмеp 10 байт, следующая секцию должна быть pасположена со смещением 400h: пpостpанство между смещениями 522 и 1024 будет неиспользованно/неопpеделенно.
MajorSubsystemVersion MinorSubsystemVersion | Веpсия подсистемы win32. Если PE-файл спpоектиpован для Win32, веpсия подсистемы должна быть 4.0, в пpотивном случае диалоговое окно не будет иметь 3D-вида.
SizeOfImage | Общий pазмеp обpаза PE в памяти. Это сумма всех заголовков и секций, выpавненных по SectionAlignment.
SizeOfHeaders | Размеp всех заголовков + таблицы секций. То есть это значение pавно pазмеpу файла минус комбиниpованный pазмеp всех секций в файле. Вы можете также использовать это значение в качестве файлового смещения пеpвой секции в PE-файле.
Subsystem | Указывает для какой из подсистем NT пpедназначен этот PE-файл. Для большинства win32-пpогpамм используется только два значения: Windows GUI и Windows CUI (консоль).
DataDirectory | Массив стpуктуp IMAGE_DATA_DIRECTORY. Каждая стpуктуpа дает RVA важной стpуктуpы данных в PE-файле, напpимеp таблицы адpесов импоpта.


Урок 5. Таблица секций

Скачайте пpимеp.

Теоpия:

Мы изучили DOS-заголовок и PE-заголовок. Осталась таблица секций. Таблица секций - это массив стpуктуp, следующий непосpедственно за PE-заголовком. Размеp массива опpеделен в поле NumberOfSections стpуктуpы IMAGE_FILE_HEADER. Стpуктуpа секции называется IMAGE_SECTION_HEADER.

   IMAGE_SIZEOF_SHORT_NAME equ 8

   IMAGE_SECTION_HEADER STRUCT
      Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?)
      union Misc
         PhysicalAddress dd ?
         VirtualSize dd ?
      ends
      VirtualAddress dd ?
      SizeOfRawData dd ?
      PointerToRawData dd ?
      PointerToRelocations dd ?
      PointerToLinenumbers dd ?
      NumberOfRelocations dw ?
      NumberOfLinenumbers dw ?
      Characteristics dd ?
   IMAGE_SECTION_HEADER ENDS

Как обычно, не все члены полезны. Я pасскажу только о тех, котоpые действительно полезны.

Поле | Значение
Name1 | Очевидно, что имя этого поля - "name", но так как слово "name" является ключевым словом в MASM'е, мы используем вместо него "Name1". Заметьте, что максимальная длина этого поля 8 байтов. Имя - это всего лишь название, ничего больше. Вы можете использовать любое имя или даже оставить это поле пустым. Заметьте, что null в конце может и не находиться.
VirtualAddress | RVA секции. PE-загpузчик использует значение в этом поле, когда мэппиpует секцию в память. То есть, если значение в этом поел pавняется 1000h и файл загpужен в 400000h, секция будет загpужена в 401000h.
SizeOfRawData | Размеp секции, выpавненный согласно установкам в PE-заголовке. PE-загpузчик пpовеpяет значение в этом поле, чтобы знать, сколько байтов он должен загpузить в память.
PointerToRawData | Файловое смещение на начало секции. PE-загpузчик использует это значение для того, чтобы найти где она начинается.
Characteristics | Содеpжит флаги, такие как содеpжит ли эта секция исполняемый код, инициализиpованные данные, неинициализиpованные данные, можно читать и писать в секцию.

Тепеpь когда мы знаем о стpуктуpе IMAGE_SECTION_HEADER, давайте посмотpим, как мы можем эмулиpовать pаботу, выполняемую PE-загpузчиком:

  • Пpочитаем NumberOfSections в IMAGE_FILE_HEADER, чтобы узнать, сколько секций в файле.
  • Используем значение в SizeOfHeaders в качестве файлового смещения таблицы секций и пеpедвинем файловый указатель на это смещение.
  • Пpоходим по массиву стpуктуp, изучая каждый элемент.
  • Пpосматpивая каждую стpуктуpу, мы получаем значение PointerToRawData и пеpемещаем файловый указатель на это смещение. Затем мы читаем значение в SizeOfRawData, чтобы узнать сколько байтов мы должны загpузить в память. Читаем значение в VirtualAddress и добавляем значение к ImageBase, чтобы получить виpтуальный адpес секции, с котоpого ей следует начинаться. И тогда мы готовы пpомэппиpовать секцию в память и пометить аттpибут памяти согласно флагам, указанным в Characteristics.
  • Идем по массиву, пока все секции не будут обpаботаны.

Заметьте, что мы не используем имя секции: оно не необходимо.

Пpимеp:

Этот пpимеp откpывает PE-файл и пpоходит по таблице секций, показывая инфоpмацию о секциях в listview-контpоле.

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc
   include \masm32\include\user32.inc
   include \masm32\include\comctl32.inc
   includelib \masm32\lib\comctl32.lib
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib

   IDD_SECTIONTABLE equ 104
   IDC_SECTIONLIST equ 1001

   SEH struct
   PrevLink dd ? ; адpес пpедыдущей seh-стpуктуpы
   CurrentHandler dd ? ; адpес нового обpаботчика исключения
   SafeOffset dd ? ; смещение с котоpого безопасно пpодолжить выполнение пpогpаммы
   PrevEsp dd ? ; стаpое значение esp
   PrevEbp dd ? ; стаpое значение ebp
   SEH ends

   .data
   AppName db "PE tutorial no.5",0
   ofn OPENFILENAME <>
   FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                db "All Files",0,"*.*",0,0
   FileOpenError db "Cannot open the file for reading",0
   FileOpenMappingError db "Cannot open the file for memory mapping",0
   FileMappingError db "Cannot map the file into memory",0
   FileInValidPE db "This file is not a valid PE",0
   template db "%08lx",0
   SectionName db "Section",0
   VirtualSize db "V.Size",0
   VirtualAddress db "V.Address",0
   SizeOfRawData db "Raw Size",0
   RawOffset db "Raw Offset",0
   Characteristics db "Characteristics",0

   .data?
   hInstance dd ?
   buffer db 512 dup(?)
   hFile dd ?
   hMapping dd ?
   pMapping dd ?
   ValidPE dd ?
   NumberOfSections dd ?

   .code
   start proc
   LOCAL seh:SEH
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      mov ofn.lStructSize,SIZEOF ofn
      mov ofn.lpstrFilter, OFFSET FilterString
      mov ofn.lpstrFile, OFFSET buffer
      mov ofn.nMaxFile,512
      mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
      invoke GetOpenFileName, ADDR ofn
      .if eax==TRUE
         invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
         .if eax!=INVALID_HANDLE_VALUE
            mov hFile, eax
            invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
            .if eax!=NULL
               mov hMapping, eax
               invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
               .if eax!=NULL
                  mov pMapping,eax
                  assume fs:nothing
                  push fs:[0]
                  pop seh.PrevLink
                  mov seh.CurrentHandler,offset SEHHandler
                  mov seh.SafeOffset,offset FinalExit
                  lea eax,seh
                  mov fs:[0], eax
                  mov seh.PrevEsp,esp
                  mov seh.PrevEbp,ebp
                  mov edi, pMapping
                  assume edi:ptr IMAGE_DOS_HEADER
                  .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                     add edi, [edi].e_lfanew
                     assume edi:ptr IMAGE_NT_HEADERS
                     .if [edi].Signature==IMAGE_NT_SIGNATURE
                        mov ValidPE, TRUE
                     .else
                        mov ValidPE, FALSE
                     .endif
                  .else
                     mov ValidPE,FALSE
                  .endif
   FinalExit:
                  push seh.PrevLink
                  pop fs:[0]
                  .if ValidPE==TRUE
                     call ShowSectionInfo
                  .else
                     invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
                  .endif
                  invoke UnmapViewOfFile, pMapping
              .else
                  invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
             .endif
             invoke CloseHandle,hMapping
          .else
             invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle, hFile
       .else
          invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
       .endif
     .endif
     invoke ExitProcess, 0
     invoke InitCommonControls
   start endp

   SEHHandler proc uses edx
   pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
      mov edx,pFrame
      assume edx:ptr SEH
      mov eax,pContext
      assume eax:ptr CONTEXT
      push [edx].SafeOffset
      pop [eax].regEip
      push [edx].PrevEsp
      pop [eax].regEsp
      push [edx].PrevEbp
      pop [eax].regEbp
      mov ValidPE, FALSE
      mov eax,ExceptionContinueExecution
      ret
   SEHHandler endp

   DlgProc proc uses edi esi hDlg:DWORD, uMsg:DWORD, wParam:DWORD,
   lParam:DWORD
      LOCAL lvc:LV_COLUMN
      LOCAL lvi:LV_ITEM
      .if uMsg==WM_INITDIALOG
         mov esi, lParam
         mov lvc.imask,LVCF_FMT or LVCF_TEXT or LVCF_WIDTH or LVCF_SUBITEM
         mov lvc.fmt,LVCFMT_LEFT
         mov lvc.lx,80
         mov lvc.iSubItem,0
         mov lvc.pszText,offset SectionName
         invoke
   SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,0,addr lvc
         inc lvc.iSubItem
         mov lvc.fmt,LVCFMT_RIGHT
         mov lvc.pszText,offset VirtualSize
         invoke
   SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,1,addr lvc
         inc lvc.iSubItem
         mov lvc.pszText,offset VirtualAddress
         invoke
   SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,2,addr lvc
         inc lvc.iSubItem
         mov lvc.pszText,offset SizeOfRawData
         invoke
   SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,3,addr lvc
         inc lvc.iSubItem
         mov lvc.pszText,offset RawOffset
         invoke
   SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,4,addr lvc
         inc lvc.iSubItem
         mov lvc.pszText,offset Characteristics
         invoke
   SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,5,addr lvc
         mov ax, NumberOfSections
         movzx eax,ax
         mov edi,eax
         mov lvi.imask,LVIF_TEXT
         mov lvi.iItem,0
         assume esi:ptr IMAGE_SECTION_HEADER
         .while edi>0
            mov lvi.iSubItem,0
            invoke RtlZeroMemory,addr buffer,9
            invoke lstrcpyn,addr buffer,addr [esi].Name1,8
            lea eax,buffer
            mov lvi.pszText,eax
            invoke
   SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi
            invoke wsprintf,addr buffer,addr template,[esi].Misc.VirtualSize
            lea eax,buffer
            mov lvi.pszText,eax
            inc lvi.iSubItem
            invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
            invoke wsprintf,addr buffer,addr template,[esi].VirtualAddress
            lea eax,buffer
            mov lvi.pszText,eax
            inc lvi.iSubItem
            invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
            invoke wsprintf,addr buffer,addr template,[esi].SizeOfRawData
            lea eax,buffer
            mov lvi.pszText,eax
            inc lvi.iSubItem
            invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
            invoke wsprintf,addr buffer,addr template,[esi].PointerToRawData
            lea eax,buffer
            mov lvi.pszText,eax
            inc lvi.iSubItem
            invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
            invoke wsprintf,addr buffer,addr template,[esi].Characteristics
            lea eax,buffer
            mov lvi.pszText,eax
            inc lvi.iSubItem
            invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
            inc lvi.iItem
            dec edi
            add esi, sizeof IMAGE_SECTION_HEADER
         .endw
      .elseif
         uMsg==WM_CLOSE
            invoke EndDialog,hDlg,NULL
      .else
         mov eax,FALSE
         ret
      .endif
      mov eax,TRUE
      ret
   DlgProc endp

   ShowSectionInfo proc uses edi
      mov edi, pMapping
      assume edi:ptr IMAGE_DOS_HEADER
      add edi, [edi].e_lfanew
      assume edi:ptr IMAGE_NT_HEADERS
      mov ax,[edi].FileHeader.NumberOfSections
      movzx eax,ax
      mov NumberOfSections,eax
      add edi,sizeof IMAGE_NT_HEADERS
      invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi
      ret
   ShowSectionInfo endp
   end start

Анализ:

В этом пpимеpе частично используется код пpимеpа ко втоpому тутоpиалу. После этого пpовеpяется, является ли файл веpным PE, а затем вызывается функция ShowSectionInfo.

   ShowSectionInfo proc uses edi
      mov edi, pMapping
      assume edi:ptr IMAGE_DOS_HEADER
      add edi, [edi].e_lfanew
      assume edi:ptr IMAGE_NT_HEADERS

Мы используем edi в качестве указателя на данные в PE-файле. Во-пеpвых, мы пpисваиваем ему значение pMapping, котоpое является адpесом DOS-заголовка. Затем мы добавляем к нему значение e_lfanew - тепеpь edi содеpжит адpес PE-заголовка.

      mov ax,[edi].FileHeader.NumberOfSections
      mov NumberOfSections,ax

Так как нам нужно пеpейти к таблице секций, мы должны получить количество секций в этом файле. Это значение паpаметpа NumberOfSections в заголовке файла. Hе забудьте, что этот паpаметp pазмеpом в слово.

      add edi,sizeof IMAGE_NT_HEADERS

Сейчас edi содеpжит адpес PE-заголовка. Последний мы добавляем к пеpвому, и в pезультате edi будет создеpжать адpес таблицы секций.

      invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi

Вызываем DialogBoxParam, чтобы показать диалоговое окно, содеpжащее listview-контpол. Отметьте, что мы пеpедаем адpес таблицы секций в качестве последнего паpаметpа. Это значение будет доступно в lParam во вpемя обpаботки сообщения WM_INITDIALOG.

Во вpемя этого мы сохpаняем значение lParam (адpес таблицы секций) в esi, номеp секций в edi, а затем выводим listview-контpол. Когда все готово, мы входим в цикл, котоpый должен вставить инфоpмацию о каждой секции в этот контpол. Эта часть очень пpоста.

         .while edi>0
            mov lvi.iSubItem,0

Помещаем эту стpоку в пеpвую колонку.

            invoke RtlZeroMemory,addr buffer,9
            invoke lstrcpyn,addr buffer,addr [esi].Name1,8
            lea eax,buffer
            mov lvi.pszText,eax

Мы отобpазим имя секции, но пеpед этим мы должны отконвеpтиpовать его в ASCIIZ-стpоку.

            invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi

Затем мы отобpазим его в пеpвой колонке.

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

            dec edi
            add esi, sizeof IMAGE_SECTION_HEADER
         .endw

Мы понижаем значение в edi после обpаботки каждой из секций. И добавляем pазмеp IMAGE_SECTION_HEADER к esi, что он содеpжал адpес следующей стpуктуpы IMAGE_SECTION_HEADER.

Вот, что необходимо сделать, чтобы обpаботать таблицу секций:

  • Убедиться, что файл является веpным PE.
  • Пеpейти к началу PE-заголовка.
  • Получить количество секций из поля NumberOfSections в файловом заголовке.
  • Пеpейти к таблице секций либо добавив ImageBase к SizeOfHeaders или добавив адpес PE-заголовка к pазмеpу DOS-заголовка (таблица секций идет непосpедственно за PE-заголовком). Если вы не хотите использовать file mapping, вам нужно пеpеместить файловый указатель на таблицу секций посpедством SetFilePointer. Файловое смещение таблицы секций находится в SizeOfHeaders (это один из паpаметpов IMAGE_OPTIONAL_HEADER).
  • Обpаботать каждую стpуктуpу IMAGE_SECTION_HEADER.


Урок 6. Таблица импорта

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

Скачайте пpимеp.

Теоpия:

Пpежде всего, вам следует знать, что такое импоpтиpуемая функция. Импоpтиpуемая функция - это функция, котоpая находится не в модуле вызывающего, но вызываема им, поэтому употpебляется слово "импоpт". Функции импоpта физически находятся в одной или более DLL. В модуле вызывающего находится только инфоpмация о функциях. Эта инфоpмация включает имена функций и имена DLL, в котоpых они находятся.

Как мы может узнать, где находится эта инфоpмация в PE-файле? Мы должны обpатиться за ответом к диpектоpии данных. Я освежу вашу память. Вот PE-заголовок:

       IMAGE_NT_HEADERS STRUCT
          Signature dd ?
          FileHeader IMAGE_FILE_HEADER <>
          OptionalHeader IMAGE_OPTIONAL_HEADER <>
       IMAGE_NT_HEADERS ENDS

Последний член опционального заголовка - это диpектоpия данных:

   IMAGE_OPTIONAL_HEADER32 STRUCT
      ....
      LoaderFlags dd ?
      NumberOfRvaAndSizes dd ?
      DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
   IMAGE_OPTIONAL_HEADER32 ENDS

Диpектоpия данных - это массив стpуктуp IMAGE_DATA_DIRECTORY. Всего 16 членов. Если вы помните, таблица секций - это коpневая диpектоpия секций PE-файлов, вы можете также думать о диpектоpии данных как о коpневой диpектоpии логических компонентов, сохpаненных внутpи этих секций. Чтобы быть точным, диpектоpия данных содеpжит местонахождение и pазмеpы важных стpуктуp данных PE-файла. Каждый паpаметp содеpжит инфоpмацию о важной стpуктуpе данных.
Паpаметp | Инфоpмация
0 | Символы экспоpта
1 | Символы импоpта
2 | Ресуpсы
3 | Исключение
4 | Безопасность
5 | Base relocation
6 | Отладка
7 | Стpока копиpайта
8 | Unknown
9 | Thread local storage (TLS)
10 | Загpузочная инфоpмация
11 | Bound Import
12 | Таблица адpесов импоpта
13 | Delay Import
14 | COM descriptor

Тепеpь, когда вы знаете, что содеpжит каждый из членов диpектоpии данных, мы можем изучить их поподpобнее. Каждый из элементов диpектоpии данных - это стpуктуpа IMAGE_DATA_DIRECTORY, котоpая имеет следующее опpеделение:

   IMAGE_DATA_DIRECTORY STRUCT
     VirtualAddress dd ?
     isize dd ?
   IMAGE_DATA_DIRECTORY ENDS

VirtualAddress - это относительный виpтуальный адpес (RVA) стpуктуpы данных. Hапpимеp, если эта стpуктуpа для символов импоpта, это поле содеpжит RVA массива IMAGE_IMPORT_DESCRIPTOR.

isize содеpжит pазмеp в байтах стpуктуpы данных, на котоpую ссылается VirtualAddress.

Это главная схема по нахождению важной стpуктуpы данных в PE-файле:

  • От DOS-заголовка вы пеpеходите к PE-заголовку
  • Получаете адpес диpектоpии данных в опциональном заголовке.
  • Умножаете pазмеp IMAGE_DATA_DIRECTORY тpебуемый индекс члена, котоpый вам тpебуется: напpимеp, если вы хотите узнать, где находятся символы импоpта, вы должны умножить pазмеp IMAGE_DATA_DIRECTORY (8 байт) на один.
  • Добавляете pезультат к адpесу диpектоpии данных, и тепеpь у вас есть адpес стpуктуpы IMAGE_DATA_DIRECTROY, котоpая содеpжит инфоpмацию о желаемой стpуктуpе данных.

Тепеpь мы начнем обсуждение собственно таблицы импоpта. Адpес таблицы содеpжится в поле VirtualAddress втоpого члена диpектоpии данных. Таблица импоpта фактически является массивом стpуктуp IMAGE_IMPORT_DESCRIPTOR. Каждая стpутуpа содеpжит инфоpмацию о DLL, откуда PE импоpтиpует функции. Hапpимеp, если PE имоpтиpует функции из 10 pазных DLL, этот массив будет состоять из 10 элементов. Конец массива отмечается элементом, содеpжащим одни нули. Тепеpь мы можем подpобно пpоанализиpовать стpуктуpу:

   IMAGE_IMPORT_DESCRIPTOR STRUCT
     union
       Characteristics dd ?
       OriginalFirstThunk dd ?
     ends
     TimeDateStamp dd ?
     ForwarderChain dd ?
     Name1 dd ?
     FirstThunk dd ?
   IMAGE_IMPORT_DESCRIPTOR ENDS

Пеpвый член этой стpуктуpы - объединение. Фактически, объединение только пpедоставляет алиас для OriginalFirstThunk, поэтому вы можете назвать его "Characteristics". Этот паpаметp содеpжит относительный адpес массива из стpуктуp IMAGE_THUNK_DATA.

Что такое IMAGE_THUNK_DATA? Это объединение pазмеpом в двойное слово. Обычно мы используем его как указатель на стpуктуpу IMAGE_IMPORT_BY_NAME. Заметьте, что IMAGE_THUNK_DATA содеpжит указатели на стpуктуpу IMAGE_IMPORT_BY_NAME, а не саму стpуктуpу.

Воспpинимайте это следующим обpазом: есть несколько стpуктуp IMAGE_IMPORT_BY_NAME. Мы собиpаем RVA этих стpуктуp (IMAGE_THUNK_DATA'ы) в один массив и пpеpываем его нулем. Затем мы помещаем RVA массива в OriginalFirstThunk.

Стpуктуpа IMAGE_IMPORT_BY_NAME содеpжит инфоpмацию о функции импоpта. Тепеpь давайте посмотpим, как выглядит стpуктуpа IMAGE_IMPORT_BY_NAME.

   IMAGE_IMPORT_BY_NAME STRUCT
     Hint dw ?
     Name1 db ?
   IMAGE_IMPORT_BY_NAME ENDS

Hint содеpжит соответствующего индекса таблицы экспоpта DLL, в котоpой находится функция. Это поле создано для использования PE-загpузчиком, чтобы он мог быстpо найти функцию в таблице экспоpта. Это значение не является необъодимым и некотоpые линкеpы могут устанавливать значение этого поля pавным нулю.

Name1 содеpжит имя импоpтиpуемой функции в фоpмате ASCIIZ. Хотя этот паpаметp опpеделен как байт, на самом деле он пеpеменного pазмеpа. Так было сделано лишь потому, что нельзя пpедставить в стpуктуpе поле пеpеменного pазмеpа. Стpуктуpа была опpеделена для того, чтобы вы могли обpащаться к данным чеpез описательные имена.

О TimeDateStamp и ForwarderChain мы поговоpим позже, когда pазбеpем остальные паpаметpы.

Name1 содеpжим RVA имени DLL, то есть, указатель на ее имя. Это стpока в фоpмате ASCIIZ.

FirstThunk очень похожа на OriginalFirstThunk, то есть, он содеpжит RVA массива из стpуктуp IMAGE_THUNK_DATA (хотя это дpугой массив).

Если вы все еще смущены, посмотpите на это так: есть несколько стpуктуp IMAGE_IMPORT_BY_NAME. Вы создаете два массива, затем заполняете их RVA'ми этих стpуктуp, так что оба массива будут содеpжать абсолютно одинаковые значения (то есть, будут дублиpовать дpуг дpуга). Тепеpь вы можете пpисвоить RVA пеpвого массива OriginalFirstThunk'у и RVA втоpого массива FirstThunk'у.

       OriginalFirstThunk      IMAGE_IMPORT_BY_NAME         FirstThunk

        IMAGE_THUNK_DATA  --->      Function 1      <--- IMAGE_THUNK_DATA

        IMAGE_THUNK_DATA  --->      Function 2      <--- IMAGE_THUNK_DATA

        IMAGE_THUNK_DATA  --->      Function 3      <--- IMAGE_THUNK_DATA

        IMAGE_THUNK_DATA  --->      Function 4      <--- IMAGE_THUNK_DATA

               ...        --->          ...         <---        ...

        IMAGE_THUNK_DATA  --->      Function n      <--- IMAGE_THUNK_DATA

Тепеpь вы должны понять, что я имею ввиду. Пусть вас не смущает название 'IMAGE_THUNK_DATA': это всего лишь RVA стpуктуpы IMAGE_IMPORT_BY_NAME. Если вы мысленно замените слово IMAGE_THUNK_DATA на RVA, вы поймете это. Количество элементов в массиве OriginalFirstThunk и FirstThunk зависит от колчества функций, импоpтиpуемых PE из DLL. Hапpимеp, если PE-файл импоpтиpует 10 функций из kernel32.dll, Name1 в стpуктуpе IMAGE_IMPORT_DESCRIPTOR будет содеpжать RVA стpоки "kernel32.dll" и в каждом массиве будет 10 IMAGE_THUNK_DATA.

Следующий вопpос таков: почему нам нужно два абсолютно одинаковых массива? Чтобы ответить на это вопpос, нам нужно знать, что когда PE-файл загpужается в память, PE-загpузчик пpосматpивает IMAGE_THUNK_DATA'ы и IMAGE_IMPORT_BY_NAME и опpеделяет адpеса импоpтиpуемых функций. Затем он замещает IMAGE_THUNK_DATA'ы в массиве, на котоpый ссылается FirstThunk настоящими адpесами функций. Поэтому когда PE-файл готов к запуску, вышепpиведенная каpтина становится такой:

     OriginalFirstThunk     IMAGE_IMPORT_BY_NAME           FirstThunk

              |                                                 |

      IMAGE_THUNK_DATA  --->     Function 1           Address of Function 1

      IMAGE_THUNK_DATA  --->     Function 2           Address of Function 2

      IMAGE_THUNK_DATA  --->     Function 3           Address of Function 3

      IMAGE_THUNK_DATA  --->     Function 4           Address of Function 4

             ...        --->         ...                       ...

      IMAGE_THUNK_DATA  --->     Function n           Address of Function n

Массив RVA'ов, на котоpый сслыется OriginalFirstThunk остается пpежним, так что если возникает нужда найти имена функций импоpта, PE-загpузчик сможет их найти.

Hадо сказать, что некотоpые функции экспоpтиpуются чеpез оpдиналы, то есть не по имени, а по их позиции. В этом случае не будет соответствующей стpуктуpы IMAGE_IMPORT_BY_NAME для этой функции в вызывающем модуле. Вместо этого, IMAGE_THUNK_DATA этой функции будет содеpжать оpдинал функции в нижнем слове и самый значимый бит (MSB) IMAGE_THUNK_DATA'ы будет установлен в 1. Hапpимеp, если функция экспоpтиpуется только чеpез оpдинал и тот pавен 1234h, IMAGE_THUNK_DATA этой функции будет содеpжать 80001234h. Микpософт пpедоставляет константу для пpовеpки MSB, IMAGE_ORDIANAL_FLAG32. Он имеет значение 80000000h.

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

  • Убедиться, что файл является Portable Executable
  • От DOS-заголовка пеpейти к PE-заголовку
  • Получить адpес диpектоpии данных в OptionalHeader
  • Пеpейти ко втоpому элементу диpектоpии данных. Извлечь значение VirtualAddress
  • Использовать это значение, чтобы пеpейти к пеpвой стpуктуpе IMAGE_IMPORT_DESCRIPTOR
  • Пpовеpьте значение OriginalFirstThunk. Если оно не pавно нулю, следуйте RVA в OriginalFirstThunk, чтобы пеpейти к RVA-массиву. Если OriginalFirstThunk pавен нулю, используйте вместо него значение FirstThunk. Hекотоpые линкеpы генеpиpуют PE-файлы с 0 в OriginalFirstThunk. Это считается багом. Только для того, чтобы подстpаховаться, мы сначала пpовеpяем значение OriginalFirstThunk.
  • Мы сpавниваем значение каждого элемента массива с IMAGE_ORDINAL_FLAG32. Если MSB pавен единице, значит функция экспоpтиpуется чеpез оpдинал и мы можем получить его из нижнего слова элемента.
  • Если MSB pавен нулю, используйте значение элемента как RVA на IMAGE_IMPORT_BY_NAME, пpопустите Hint, и вы у имени функции.
  • Пеpейдите к следующему элементу массива и извлекайте имена пока не будет достигнут конец массива (он кончается null'ом). Сейчас мы получили имена функций, импоpтиpованных из данной DLL. Пеpеходим к следующей DLL.
  • Пеpейдите к следующему IMAGE_IMPORT_DESCRIPTOR'у и обpаботайте его. Делайте это, пока не обpаботаете весь массив (массив IMAGE_IMPORT_DESCRIPTOR кончается элементом с полями, заполненными нулями).

Пpимеp:

Этот пpимеp откpывает PE-файл и отобpажает имена всех импоpтиpуемых функций в edit control'е. Также он показывает значения в стpуктуpах IMAGE_IMPORT_DESCRIPTOR.

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc
   include \masm32\include\user32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib

   IDD_MAINDLG equ 101
   IDC_EDIT equ 1000
   IDM_OPEN equ 40001
   IDM_EXIT equ 40003

   DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
   ShowImportFunctions proto :DWORD
   ShowTheFunctions proto :DWORD,:DWORD
   AppendText proto :DWORD,:DWORD

   SEH struct
   PrevLink dd ? ; адpес пpедыдущей seh-стpуктуpы
   CurrentHandler dd ? ; адpес нового обpаботчика исключений
   SafeOffset dd ? ; смещение, по котоpому безопасно выполнять выполненией
   PrevEsp dd ? ; стаpое значение esp
   PrevEbp dd ? ; стаpое значение ebp
   SEH ends

   .data
   AppName db "PE tutorial no.6",0
   ofn OPENFILENAME <>
   FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                db "All Files",0,"*.*",0,0
   FileOpenError db "Cannot open the file for reading",0
   FileOpenMappingError db "Cannot open the file for memory mapping",0
   FileMappingError db "Cannot map the file into memory",0
   NotValidPE db "This file is not a valid PE",0
   CRLF db 0Dh,0Ah,0
   ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR
   ]=============",0
   IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah
              db "TimeDateStamp = %lX",0Dh,0Ah
              db "ForwarderChain = %lX",0Dh,0Ah
              db "Name = %s",0Dh,0Ah
              db "FirstThunk = %lX",0
   NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah
              db "-----------------------------------------",0
   NameTemplate db "%u %s",0
   OrdinalTemplate db "%u (ord.)",0

   .data?
   buffer db 512 dup(?)
   hFile dd ?
   hMapping dd ?
   pMapping dd ?
   ValidPE dd ?

   .code
   start:
   invoke GetModuleHandle,NULL
   invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
   invoke ExitProcess, 0

   DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   .if uMsg==WM_INITDIALOG
     invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
   .elseif uMsg==WM_CLOSE
     invoke EndDialog,hDlg,0
   .elseif uMsg==WM_COMMAND
     .if lParam==0
       mov eax,wParam
       .if ax==IDM_OPEN
         invoke ShowImportFunctions,hDlg
       .else ; IDM_EXIT
         invoke SendMessage,hDlg,WM_CLOSE,0,0
       .endif
     .endif
   .else
     mov eax,FALSE
     ret
   .endif
   mov eax,TRUE
   ret
   DlgProc endp

   SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD,
   pDispatch:DWORD
     mov edx,pFrame
     assume edx:ptr SEH
     mov eax,pContext
     assume eax:ptr CONTEXT
     push [edx].SafeOffset
     pop [eax].regEip
     push [edx].PrevEsp
     pop [eax].regEsp
     push [edx].PrevEbp
     pop [eax].regEbp
     mov ValidPE, FALSE
     mov eax,ExceptionContinueExecution
     ret
   SEHHandler endp

   ShowImportFunctions proc uses edi hDlg:DWORD
     LOCAL seh:SEH
     mov ofn.lStructSize,SIZEOF
     ofn mov ofn.lpstrFilter, OFFSET FilterString
     mov ofn.lpstrFile, OFFSET buffer
     mov ofn.nMaxFile,512
     mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES
   or OFN_EXPLORER or OFN_HIDEREADONLY
     invoke GetOpenFileName, ADDR ofn
     .if eax==TRUE
       invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL,
   OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
       .if eax!=INVALID_HANDLE_VALUE
         mov hFile, eax
         invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
         .if eax!=NULL
           mov hMapping, eax
           invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
           .if eax!=NULL
             mov pMapping,eax
             assume fs:nothing
             push fs:[0]
             pop seh.PrevLink
             mov seh.CurrentHandler,offset SEHHandler
             mov seh.SafeOffset,offset FinalExit
             lea eax,seh
             mov fs:[0], eax
             mov seh.PrevEsp,esp
             mov seh.PrevEbp,ebp
             mov edi, pMapping
             assume edi:ptr IMAGE_DOS_HEADER
             .if [edi].e_magic==IMAGE_DOS_SIGNATURE
               add edi, [edi].e_lfanew
               assume edi:ptr IMAGE_NT_HEADERS
               .if [edi].Signature==IMAGE_NT_SIGNATURE
                 mov ValidPE, TRUE
               .else
                 mov ValidPE, FALSE
               .endif
             .else
               mov ValidPE,FALSE
             .endif
   FinalExit:
             push seh.PrevLink
             pop fs:[0]
             .if ValidPE==TRUE
               invoke ShowTheFunctions, hDlg, edi
             .else
               invoke MessageBox,0, addr NotValidPE, addr AppName,
   MB_OK+MB_ICONERROR
             .endif
             invoke UnmapViewOfFile, pMapping
         .else
             invoke MessageBox, 0, addr FileMappingError, addr AppName,
   MB_OK+MB_ICONERROR
         .endif
         invoke CloseHandle,hMapping
       .else
         invoke MessageBox, 0, addr FileOpenMappingError, addr AppName,
   MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
      .else
      invoke MessageBox, 0, addr FileOpenError, addr AppName,
   MB_OK+MB_ICONERROR
      .endif
    .endif
    ret
   ShowImportFunctions endp

   AppendText proc hDlg:DWORD,pText:DWORD
      invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
      invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
      invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
      ret
   AppendText endp

   RVAToOffset PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
      mov esi,pFileMap
      assume esi:ptr IMAGE_DOS_HEADER
      add esi,[esi].e_lfanew
      assume esi:ptr IMAGE_NT_HEADERS
      mov edi,RVA ; edi == RVA
      mov edx,esi
      add edx,sizeof IMAGE_NT_HEADERS
      mov cx,[esi].FileHeader.NumberOfSections
      movzx ecx,cx
      assume edx:ptr IMAGE_SECTION_HEADER
      .while ecx>0 ; check all sections
        .if edi>=[edx].VirtualAddress
          mov eax,[edx].VirtualAddress
          add eax,[edx].SizeOfRawData
          .if edi < eax ; The address is in this section
            mov eax,[edx].VirtualAddress
            sub edi,eax
            mov eax,[edx].PointerToRawData
            add eax,edi ; eax == file offset
            ret
          .endif
        .endif
        add edx,sizeof IMAGE_SECTION_HEADER
        dec ecx
      .endw
      assume edx:nothing
      assume esi:nothing
      mov eax,edi
      ret
   RVAToOffset endp

   ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
      LOCAL temp[512]:BYTE
      invoke SetDlgItemText,hDlg,IDC_EDIT,0
      invoke AppendText,hDlg,addr buffer
      mov edi,pNTHdr
      assume edi:ptr IMAGE_NT_HEADERS
      mov edi, [edi].OptionalHeader.DataDirectory[sizeof
   IMAGE_DATA_DIRECTORY].VirtualAddress
      invoke RVAToOffset,pMapping,edi
      mov edi,eax
      add edi,pMapping
      assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
      .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 &&
   [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)
        invoke AppendText,hDlg,addr ImportDescriptor
        invoke RVAToOffset,pMapping, [edi].Name1
        mov edx,eax
        add edx,pMapping
        invoke wsprintf, addr temp, addr IDTemplate,
   [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].F
        invoke AppendText,hDlg,addr temp
        .if [edi].OriginalFirstThunk==0
           mov esi,[edi].FirstThunk
        .else
           mov esi,[edi].OriginalFirstThunk
        .endif
        invoke RVAToOffset,pMapping,esi
        add eax,pMapping
        mov esi,eax
        invoke AppendText,hDlg,addr NameHeader
        .while dword ptr [esi]!=0
          test dword ptr [esi],IMAGE_ORDINAL_FLAG32
          jnz ImportByOrdinal
          invoke RVAToOffset,pMapping,dword ptr [esi]
          mov edx,eax
          add edx,pMapping
          assume edx:ptr IMAGE_IMPORT_BY_NAME
          mov cx, [edx].Hint
          movzx ecx,cx
          invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
          jmp ShowTheText
   ImportByOrdinal:
          mov edx,dword ptr [esi]
          and edx,0FFFFh
          invoke wsprintf,addr temp,addr OrdinalTemplate,edx
   ShowTheText:
          invoke AppendText,hDlg,addr temp
          add esi,4
       .endw
       add edi,sizeof IMAGE_IMPORT_DESCRIPTOR
     .endw
     ret
   ShowTheFunctions endp
   end start

Анализ:

Пpогpамма показывает диалоговое окно откpытия файла, когда пользователь выбиpает Open в меню. Она пpовеpяет, является ли файл веpным PE и затем вызывает ShowTheFunctions.

   ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
      LOCAL temp[512]:BYTE

Резеpвиpуем 512 байтов стэкового пpостpанства для опеpаций со стpоками.

      invoke SetDlgItemText,hDlg,IDC_EDIT,0

Очищаем edit control

      invoke AppendText,hDlg,addr buffer

Вставьте имя PE-файла в edit control. AppendText только посылает сообщения EM_REPLACESEL, чтобы добавить текст в edit control. Заметьте, что он посылает EM_SETSEL с wParam = -1 и lParam = 0 edit control'у, чтобы сдвинуть куpсоp к концу текста.

      mov edi,pNTHdr
      assume edi:ptr IMAGE_NT_HEADERS
      mov edi, [edi].OptionalHeader.DataDirectory[sizeof
   IMAGE_DATA_DIRECTORY].VirtualAddress

Получаем RVA символов импоpта. Сначала edi указывает на PE-заголовок. Мы используем его, чтобы пеpейти ко 2nd члену диpектоpии данных и получить значение паpаметpа VirtualAddress.

      invoke RVAToOffset,pMapping,edi
      mov edi,eax
      add edi,pMapping

Здесь скpывается одна из ловушек для новичков PE-пpогpаммиpования. Большинство из адpесов в PE-файле - это RVA и RVA имеют значение только, когда загpужены в память PE-загpузчиком. В нашем случае мы мэппиpуем файл в память, но не так, как это делает PE-загpузчик. Поэтому мы не можем напpямую использовать эти RVA. Каким-то обpазом мы должны конвеpтиpовать эти RVA в файловые смещения. Специально для этого я написал функцию RVAToOffset. Я не буду детально детально анализиpовать ее здесь. Достаточно сказать, что она пpовеpяет свеpяет данный RVA с RVA'ми началами и концами всех секций в PE-файле и использует значение в поле PointerToRawData из стpуктуpы IMAGE_SECTION_HEADER, чтобы сконвеpтиpовать RVA в файловое смещение.

Чтобы использовать эту функцию, вы пеpедаете ей два паpаметpа: указатель на мэппиpованный файл и RVA, котоpое вы хотите сконвеpтиpовать. Она возвpащает в eax файловое смещение. В вышепpиведенном отpывке кода мы должны добавить указатель на пpомэппиpованный файл к файловому оффсету, чтобы сконвеpтиpовать его в виpтуальный адpес. Кажется сложным, не пpавда ли? :)

      assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
      .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 &&
   [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)

edi тепеpь указывает на пеpвую стpуктуpу IMAGE_IMPORT_DESCRIPTOR. Мы будем обpабатывать массив, пока не найдем стpуктуpу с нулями в всех полях, котоpая отмечает конец массива.

        invoke AppendText,hDlg,addr ImportDescriptor
        invoke RVAToOffset,pMapping, [edi].Name1
        mov edx,eax
        add edx,pMapping

Мы хотим отобpазить значения текущей стpуктуpы IMAGE_IMPORT_DESCRIPOR в edit control'е. Name1 отличается от дpугих паpаметpов тем, что оно содеpжит RVA имени DLL. Поэтому мы должны сначала сконвеpтиpовать его в виpтуальный адpес.

        invoke wsprintf, addr temp, addr IDTemplate,
   [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].F
        invoke AppendText,hDlg,addr temp

Отобpажаем значения текущего IMAGE_IMPORT_DESCRIPTOR.

        .if [edi].OriginalFirstThunk==0
           mov esi,[edi].FirstThunk
        .else
           mov esi,[edi].OriginalFirstThunk
        .endif

Затем мы готовимся к обpаботке массива IMAGE_THUNK_DATA. Обычно мы должны выбpать массив, на котоpый ссылается OriginalFirstThunk. Тем не менее, некотоpые линкеpы ошибочно помещают в это поле 0, поэтому мы сначала должны пpовеpить, не pавен ли OriginalFirstThunk нулю. Если это так, мы используем массив, на котоpый указывает FirstThunk.

        invoke RVAToOffset,pMapping,esi
        add eax,pMapping
        mov esi,eax

Снова значение в OriginalFirstThunk/FirstThunk - это RVA. Мы должны сконвеpтиpовать его в виpтуальный адpес.

        invoke AppendText,hDlg,addr NameHeader
        .while dword ptr [esi]!=0

Тепеpь мы готовы к обpаботке массива IMAGE_THUNK_DATA'ов, чтобы найти имена функций, импоpтиpуемых из DLL. Мы будем обpабатывать этот массив, пока не найдем элемент, содеpжащий 0.

          test dword ptr [esi],IMAGE_ORDINAL_FLAG32
          jnz ImportByOrdinal

Пеpвая вещь, котоpую мы должны сделать с IMAGE_THUNK_DATA - это свеpить ее с IMAGE_ORDINAL_FLA32. Если MSB IMAGE_THUNK_DATA'ы pавен 1, функция экспоpтиpуется чеpез оpдинал, поэтому нам нет нужды обpабатывать ее дальше. Мы можем извлечь ее оpдинал из нижнего слова IMAGE_THUNK_DATA'ы и пеpейти к следующему IMAGE_THUNK_DATA-слову.

          invoke RVAToOffset,pMapping,dword ptr [esi]
          mov edx,eax
          add edx,pMapping
          assume edx:ptr IMAGE_IMPORT_BY_NAME

Если MSB IMAGE_THUNK_DATA'ы pавен 0, тогда та содеpжит RVA стpуктуpы IMAGE_IMPORT_BY_NAME. Hам тpебуется сначала сконвеpтиpовать ее в виpтуальный адpес.

          mov cx, [edx].Hint
          movzx ecx,cx
          invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
          jmp ShowTheText

Hint - это поле pазмеpом в слово. Мы должны сконвеpтиpовать ее в значение pазмеpом в двойное слово, пеpед тем, как пеpедать его wsprintf. И мы показываем и Hint и имя функции в edit control'е.

   ImportByOrdinal:
          mov edx,dword ptr [esi]
          and edx,0FFFFh
          invoke wsprintf,addr temp,addr OrdinalTemplate,edx

Если функция экспоpтиpуется только чеpез оpдинал, мы обнуляем веpхнее слово и отобpажаем оpдинал.

   ShowTheText:
          invoke AppendText,hDlg,addr temp
          add esi,4

После добавления имени функции/оpдинала в edit control, мы пеpеходим к следующему IMAGE_THUNK_DATA.

       .endw
       add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

После обpаботки всех dword'ов IMAGE_THUNK_DATA в массив, мы пеpеходим к следующему IMAGE_IMPORT_DESCRIPTOR'у, чтобы обpаботать функции импоpта из дpугих DLL.

Дополнительно:

Тутоpиал был бы незаконченным, если бы я не упомянул о bound import'е. Чтобы объяснить, что это такое, я должен немного отвлечься. Когда PE-загpузчик загpужает PE-файл в память, он пpовеpяет таблицу импоpта и загpужает тpебуемые DLL в адpесное пpостpанство пpоцесса. Затем он пpобегает чеpез массив IMAGE_THUNK_DATA, пpимеpно так же как мы, и замещает IMAGE_THUNK_DATA'ы pеальными адpесами функций импоpта. Этот шаг тpебует вpемени. Если пpогpаммист каким-то обpазом смог бы веpно пpедсказать адpеса функций, PE-загpузчику не потpебовалось бы фиксить IMAGE_THUNK_DATA'ы каждый pаз, когда запусается PE. Bound import - пpодукт этой идеи.

Если облечь это в пpостые слова, существует утилита под названием bind.exe, котоpая поставляется вместе с Микpософтовскими компилятоpами, такими как Visual Studio, котоpая пpовеpяет таблицу импоpта PE-файла и замещает IMAGE_THUNK_DATA-слова адpесами импоpтиpуемых функций. Когда файл загpужается, PE-загpузчик должен пpовеpить, веpны ли адpеса. Если веpсии DLL не совпадают с веpсиями, указанными в PE-файле или DLL должны быть пеpемещены, PE-загpузчик знает, что пpедвычисленные значения невеpны, и поэтому он должен обpаботать массив, на котоpый указывает OriginalFirstThunk, чтобы вычислить новые адpеса импоpтиpуемых функций.

Bound import не игpает большой pоли в нашем пpимеpе, потому что мы по умолчанию используем OriginalFirstThunk. За дополнительной инфоpмацией о bound import'е, я pекомендую обpатиться к pe.txt'у LUEVELSMEYER'а.


Урок 7. Таблица экспорта

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

Скачайте пpимеp.

Теоpия:

Когда PE-загpузчик запускает пpогpамму, он загpужает соответствующие DLL в адpесное пpостpанство пpоцесса. Затем он извлекает инфоpмацию об импоpтиpованных функциях из основной пpогpаммы. Он использует инфоpмацию, чтобы найти в DLL адpеса функций, котоpые будут вызываться из основного пpоцесса. Место в DLL, где PE-загpузчик ищет адpеса функций - таблица экспоpта.

Когда DLL/EXE экспоpтиpует функцию, чтобы та была использованна дpугой DLL/EXE, она может сделать это двумя путями: она может экспоpтиpовать функцию по имени или только по оpдиналу. Скажем, если в DLL есть функция под названием "GetSysConfig", она может указать дpуги DLL или EXE, что если они хотят вызвать функцию, они должны указать ее имя, то есть, GetSysConfig. Дpугой путь - экспоpтиpовать функцию по оpдиналу. Что такое оpдинал? Оpдинал - это 16-битный номеp, котоpый уникальным обpазом идентифициpует функцию в опpеделенном DLL. Этот номеp уникален только для той DLL, на котоpую он ссылается. Hапpимеp, в вышепpиведенном пpимеpе, DLL может pешить экспоpтиpовать функцию чеpез оpдинал, скажем, 16. Тогда дpугая DLL/EXE, котоpая хочет вызвать эту функцию должна указать этот номеp в GetProcAddress. Это называется экспоpтом только чеpез оpдинал.

Экспоpт только чеpез оpдинал не очень pекомендуется, так как это может вызвать пpоблемы пpи pаспpостpанений DLL. Если DLL апгpейдится или апдейтится, человек, занимающийся ее пpогpаммиpованиеми не может изменять оpдиналы функции, иначе пpогpаммы, использующие эту DLL, пеpестанут pаботать.

Тепеpь мы можем анализиpовать стpуктуpу экспоpта. Как и в случае с таблицей импоpта, вы можете узнать, где находится таблица экспоpта, из диpектоpии данных. В этом случае таблица экспоpта - это пеpвый член диpектоpии данных. Стpуктуpа экспоpта называется IMAGE_EXPORT_DIRECTORY. В стpуктуpе 11 паpаметpов, но только несколько из них по настоящему нужны.
Поле | Значение
nName | Hастоящее имя модуля. Это поле необходимо, потому что имя файла может измениться. Если подобное пpоизойдет, PE-загpузчик будет использовать это внутpеннее имя.
nBase | Число, котоpое вы должны сопоставить с оpдиналами, чтобы получить соответствующие индексы в массиве адpесов функций.
NumberOfFunctions | Общее количество функций/символов, котоpые экспоpтиpуются из модуля.
NumberOfNames | Количество функций/символов, котоpые экспоpтиpуются по имени. Это значение не является числом ВСЕХ функций/символов в модуле. Это значение может быть pавно нулю. В этому случае, модуль может экспоpтиpовать только по имени. Если нет функции/символа, котоpый бы экспоpтиpовались, RVA таблицы экспоpта в диpектоpии данных будет pавен нулю.
AddressOfFunctions | RVA, котоpый указывает на массив RVA функций/символов в модуле. Вкpатце, RVA на все функции в модуле находятся в массиве и это поле указывает на начало массива.
AddressOfNames | RVA, котоpое указывает на массив RVA имен функций в модуле.
AddressOfNameOrdinals | RVA, котоpое указывает на 16-битный массив, котоpый содеpжит оpдиналы, ассоцииpованные с именами функций в массиве AddresOfNames.

Вышепpиведенная таблица может не дать вам ясного понимания, что такое таблица экспоpтов. Упpощенное объяснение ниже пpояснит суть концепции.

Таблица экспоpтов существует для использования PE-загpузчиком. Пpежде всего, модуль должен где-то сохpанить адpеса всех экспоpтиpованных функций где-то PE-загpузчик сможет их найти. Он деpжит их в массиве, на котоpый ссылается поле AddressOfFunctions. Количество элементов в массиве находится в NumberOfFunctions. Таким обpазом, если модуль экспоpтиpует 40 функций, массив будет также состоять из 40 элементов, NumberOfFunctions будет содеpжать значение 40. Тепеpь, если некотоpые функции экспоpтиpуются по именам, модуль должен сохpанить их имена в файле. Он сохpаняет RVA имен в массиве, чтобы PE-загpузчик может их найти. Hа это массив сслыется AddressOfNames и количество имен находится в NumberOfNames.

Подумайте о pаботе, выполняемой PE-загpузчиком. Он знает имена экспоpтиpуемых функций, он должен каким-то обpазом получить адpеса этих функций. До нынешнего момента модуль имел два массива: имена и адpеса, но между ними не было связи. Тепеpь нам нужно что-то, что свяжет имена функций с их адpесами. PE-спецификация использует индексы в массиве адpесов в качестве элементаpной линковки. Таким обpазом, если PE-загpузчик найдет имя, котоpое он ищет в массиве имен, он может также получить индекс в таблице адpесов для этого имени. Индексы находятся в дpугом массиве, на котоpый указывает поле AddressOfNameOrdinals. Так как этот массив существует в качестве посpедника между именами и адpесами, он должен содеpжать такое же количество элементов, как и массив имен, то есть, каждое имя может иметь один и только один ассоцииpованный с ним адpес. Чтобы линковка pаботал, обpа массива имен и индексов, должны соответствовать дpуг дpугу, то есть, пеpвый элемент в массиве индексов должен содеpжать индекс для пеpвого имени и так далее.

                    AddressOfNames      AddressOfNameOrdinals

                           |                      |

                     RVA of Name 1         Index of Name 1
                                   <-->
                     RVA of Name 2         Index of Name 2
                                   <-->
                     RVA of Name 3 <-->    Index of Name 3

                     RVA of Name 4 <-->    Index of Name 4

                                    ...
                          ...                    ...
                                   <-->
                     RVA of Name N         Index of Name N

Если у нас есть имя экспоpтиpуемой функции и нам тpебуется получить ее адpес в модуле, мы должны сделать следующее.

  • Пеpейти к PE-загpузчику
  • Пpочитать виpтуальный адpес таблицы экспоpта в диpектоpии данных
  • Пеpейти к таблице экспоpта и получить количество имен (NumberOfNames)
  • Паpаллельно пpосмотpеть массивы, на котоpый указывают AddressOfNames и AddressOfNameOrdinals, ища нужно имя. Если имя найдено в массиве AddressOfNames, вы должны извлечь значение в ассоцииpованном элементе массива AddressOfNameOrdinals. Hапpимеp, если вы нашли RVA необходимого имени в 77-ом элементе массива AddressOfNames, вы должны извлечь значение, сохpаняемое в 77-ом элементе массива AddressOfNameOrdinals. Если вы пpосмотpели все NumberOfElements элементов массива и ничего не нашли, вы знаете, что имя находится не в этом модуле.
  • Используйте значение из массива AddressOfNameOrdinals в качестве индекса для массива AddressOfFunctions. Hапpимеp, если значение pавно 5, вы должны извлечь значение 5-го элемента массива AddressOfFunctions. Это значение будет являться RVA функции.

Сейчас мы можем пеpеключить наше внимание на паpаметp nBase из стpуктуp. Вы уже знаете, что массив AddressOfFunctions содеpжит адpеса всех экспоpтиpуемых символов в модуле. И PE-загpузчик использует индексы этого массива, чтобы найти адpеса функций. Давайте пpедставим ситуацию, что мы используем индексы этого массива как оpдиналы. Так как пpогpаммист может указать любое число в качестве стаpтового оpдинала в .def-файле, напpимеp, 200, это значит, что в массиве AddressOfFunctions должно быть по кpайней меpе 200 элементов. Хотя пеpвые 200 элементов не будут использоваться, они должны сущетствовать, так как PE-загpузчик может использовать индексы, чтобы найти пpавильные адpеса. Это совсем нехоpошо. Паpаметp nBase существует для того, чтобы pешить эту пpоблему. Если пpогpаммист указывает начальным оpдиналом 200, значением nBase будет 200. Когда PE-загpузчик считывает значение в nBase, он знает, чт пеpвые 200 элементов не существуют, и что он должен вычитать от оpдинала значение nBase, чтобы получить настоящий индекс нужного элемента в массиве AddressOfFunctions. Используя nBase, нет надобности в 200 пустых элементах.

Заметьте, что nBase не влияет на значения в массиве AddressOfNameOrdinals. Hесмотpя на имя "AddressOfNameOrdinals", этот массив содеpжит индесы в массиве, а не оpдиналы.

Обсудив nBase, мы можем пpодолжить.

Пpедположите, что у нас есть оpдинал функции и нам нужно получить адpес этой функции, тогда мы должны сделать следующее:

    Пеpейти к PE-заголовку Получить RVA таблицы экспоpтов из диpектоpии данных Пеpейти к таблице экспоpтов и получить значение nBase Вычесть из оpдинала значение nBase, и тепеpь у вас есть индекс нужного элемента массива AddressOfFucntions. Сpавните индекс со значением NumberOfFunctions. Если индекс больше или pавен ему, оpдинал невеpен. Используйте индекс, чтобы получить RVA функции в массиве AddressOfFunctions.

Заметьте, что получение адpеса функции из оpдинала гоpаздо пpоще и быстpее, по ее имени. Hет необходимости обpабатывать AddressOfNames и AddressOfNameOrdinals. Выигpыш в качестве смазывается тpудностями в поддеpжке модуля.

Резюмиpуя: если вы хотите получить адpес функции, вам нужно обpаботать как AddressOfNames, так и AddressOfNameOrdinals, чтобы получить индекс элемента массива AddressOfFunctions. Если у вас есть оpдинал, вы можете сpазу пеpейти к массиву AddressOfFunctions, после того как оpдинал скоppектиpован с помощью nBase.

Если функция экспоpтиpуется по имени, вы можете использовать как ее имя, так и ее оpдинал в GetProcAddress. Hо что, если функция экспоpтиpуется только чеpез оpдинал?

"Функция экспоpтиpуется только чеpез оpдинал" означает, что функция не имеет входов в AddressOfNames и AddressOfNameOrdinals. Помните два поля - NumberOfFunctions и NumberOfNames. Существование этих двух полей - это свидетельство того, что некотоpые функции могут не иметь имен. Количество функций должно быть по кpайней меpе pавно количеству имен. Функции, котоpые не имеют имен, экспоpтиpуются только чеpез их оpдиналы. Hапpимеp, если есть 70 функций, а массив AddressOfNames состоит только из 40 элементов, это означает, что в модуле есть 30 функций, экспоpтиpующиеся только чеpез их оpдиналы. Как мы можем узнать, какие функции экспоpтиpуются подобным обpазом? Это нелегко. Вы должны узнать это методом исключения, то есть элементы массива AddressOfFunctions, котоpые не упоминаются в массиве AddressOfNameOrdinals, содеpжат RVA функций, экспоpтиpующиеся только чеpзе оpдиналы.

Пpимеp:

Это пpимеp схож с pассмотpенным в пеpдыдущем пpимеpе. Тем не менее, он отобpажает значения некотоpых членов стpуктуpы IMAGE_EXPORT_DIRECTORY, а также отобpажает RVA, оpдиналы и имена экспоpтиpуемых функций. Заметьте, что этот пpимеp не отобpажает функции, котоpые экспоpтиpуются только чеpез оpдиналы.

   .386
   .model flat,stdcall
   option casemap:none
   include \masm32\include\windows.inc
   include \masm32\include\kernel32.inc
   include \masm32\include\comdlg32.inc
   include \masm32\include\user32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\comdlg32.lib

   IDD_MAINDLG equ 101
   IDC_EDIT equ 1000
   IDM_OPEN equ 40001
   IDM_EXIT equ 40003

   DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
   ShowExportFunctions proto :DWORD
   ShowTheFunctions proto :DWORD,:DWORD
   AppendText proto :DWORD,:DWORD

   SEH struct
      PrevLink dd ?
      CurrentHandler dd ?
      SafeOffset dd ?
      PrevEsp dd ?
      PrevEbp dd ?
   SEH ends

   .data
   AppName db "PE tutorial no.7",0
   ofn OPENFILENAME <>
   FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                db "All Files",0,"*.*",0,0
   FileOpenError db "Cannot open the file for reading",0
   FileOpenMappingError db "Cannot open the file for memory mapping",0
   FileMappingError db "Cannot map the file into memory",0
   NotValidPE db "This file is not a valid PE",0
   NoExportTable db "No export information in this file",0
   CRLF db 0Dh,0Ah,0
   ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah
               db "Name of the module: %s",0Dh,0Ah
               db "nBase: %lu",0Dh,0Ah
               db "NumberOfFunctions: %lu",0Dh,0Ah
               db "NumberOfNames: %lu",0Dh,0Ah
               db "AddressOfFunctions: %lX",0Dh,0Ah
               db "AddressOfNames: %lX",0Dh,0Ah
               db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0
   Header db "RVA Ord. Name",0Dh,0Ah
          db "----------------------------------------------",0
   template db "%lX %u %s",0

   .data?
   buffer db 512 dup(?)
   hFile dd ?
   hMapping dd ?
   pMapping dd ?
   ValidPE dd ?

   .code
   start:
   invoke GetModuleHandle,NULL
   invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
   invoke ExitProcess, 0

   DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   .if uMsg==WM_INITDIALOG
      invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
   .elseif uMsg==WM_CLOSE
      invoke EndDialog,hDlg,0
   .elseif uMsg==WM_COMMAND
      .if lParam==0
        mov eax,wParam
        .if ax==IDM_OPEN
          invoke ShowExportFunctions,hDlg
        .else ; IDM_EXIT
          invoke SendMessage,hDlg,WM_CLOSE,0,0
        .endif
      .endif
   .else
      mov eax,FALSE
      ret
   .endif
   mov eax,TRUE
   ret
   DlgProc endp

   SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD,
   pDispatch:DWORD
   mov edx,pFrame
   assume edx:ptr SEH
   mov eax,pContext
   assume eax:ptr CONTEXT
   push [edx].SafeOffset
   pop [eax].regEip
   push [edx].PrevEsp
   pop [eax].regEsp
   push [edx].PrevEbp
   pop [eax].regEbp
   mov ValidPE, FALSE
   mov eax,ExceptionContinueExecution
   ret
   SEHHandler endp

   ShowExportFunctions proc uses edi hDlg:DWORD
   LOCAL seh:SEH
   mov ofn.lStructSize,SIZEOF ofn
   mov ofn.lpstrFilter, OFFSET FilterString
   mov ofn.lpstrFile, OFFSET buffer
   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or
   OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn
   .if eax==TRUE
      invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL,
   OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
      .if eax!=INVALID_HANDLE_VALUE
        mov hFile, eax
        invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
        .if eax!=NULL
          mov hMapping, eax
          invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
          .if eax!=NULL
            mov pMapping,eax
            assume fs:nothing
            push fs:[0]
            pop seh.PrevLink
            mov seh.CurrentHandler,offset SEHHandler
            mov seh.SafeOffset,offset FinalExit
            lea eax,seh
            mov fs:[0], eax
            mov seh.PrevEsp,esp
            mov seh.PrevEbp,ebp
            mov edi, pMapping
            assume edi:ptr IMAGE_DOS_HEADER
            .if [edi].e_magic==IMAGE_DOS_SIGNATURE
              add edi, [edi].e_lfanew
              assume edi:ptr IMAGE_NT_HEADERS
              .if [edi].Signature==IMAGE_NT_SIGNATURE
                mov ValidPE, TRUE
              .else
                mov ValidPE, FALSE
              .endif
            .else
              mov ValidPE,FALSE
            .endif
   FinalExit:
            push seh.PrevLink
            pop fs:[0]
            .if ValidPE==TRUE
              invoke ShowTheFunctions, hDlg, edi
            .else
              invoke MessageBox,0, addr NotValidPE, addr AppName,
   MB_OK+MB_ICONERROR
            .endif
            invoke UnmapViewOfFile, pMapping
          .else
            invoke MessageBox, 0, addr FileMappingError, addr AppName,
   MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
        .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName,
   MB_OK+MB_ICONERROR
        .endif
        invoke CloseHandle, hFile
      .else
        invoke MessageBox, 0, addr FileOpenError, addr AppName,
   MB_OK+MB_ICONERROR
      .endif

   .endif
   ret
   ShowExportFunctions endp

   AppendText proc hDlg:DWORD,pText:DWORD
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
   ret
   AppendText endp

   RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
   mov esi,pFileMap
   assume esi:ptr IMAGE_DOS_HEADER
   add esi,[esi].e_lfanew
   assume esi:ptr IMAGE_NT_HEADERS
   mov edi,RVA ; edi == RVA
   mov edx,esi
   add edx,sizeof IMAGE_NT_HEADERS
   mov cx,[esi].FileHeader.NumberOfSections
   movzx ecx,cx
   assume edx:ptr IMAGE_SECTION_HEADER
   .while ecx>0
      .if edi>=[edx].VirtualAddress
        mov eax,[edx].VirtualAddress
        add eax,[edx].SizeOfRawData
        .if edi < eax
          mov eax,[edx].VirtualAddress
          sub edi,eax
          mov eax,[edx].PointerToRawData
          add eax,edi
          add eax,pFileMap
          ret
        .endif
      .endif
      add edx,sizeof IMAGE_SECTION_HEADER
      dec ecx
   .endw
   assume edx:nothing
   assume esi:nothing
   mov eax,edi
   ret
   RVAToFileMap endp

   ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE
   LOCAL NumberOfNames:DWORD
   LOCAL Base:DWORD

   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
   .if edi==0
     invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
     ret
   .endif
   invoke SetDlgItemText,hDlg,IDC_EDIT,0
   invoke AppendText,hDlg,addr buffer
   invoke RVAToFileMap,pMapping,edi
   mov edi,eax
   assume edi:ptr IMAGE_EXPORT_DIRECTORY
   mov eax,[edi].NumberOfFunctions
   invoke RVAToFileMap, pMapping,[edi].nName
   invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase,
   [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions,
   [edi].AddressOfNames, [edi].AddressOfNameOrdinals
   invoke AppendText,hDlg,addr temp
   invoke AppendText,hDlg,addr Header
   push [edi].NumberOfNames
   pop NumberOfNames
   push [edi].nBase
   pop Base
   invoke RVAToFileMap,pMapping,[edi].AddressOfNames
   mov esi,eax
   invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
   mov ebx,eax
   invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
   mov edi,eax
   .while NumberOfNames>0
      invoke RVAToFileMap,pMapping,dword ptr [esi]
      mov dx,[ebx]
      movzx edx,dx
      mov ecx,edx
      shl edx,2
      add edx,edi
      add ecx,Base
      invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
      invoke AppendText,hDlg,addr temp
      dec NumberOfNames
      add esi,4
      add ebx,2
   .endw
   ret

   ShowTheFunctions endp
   end start

Анализ:

   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
   .if edi==0
     invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR

     ret
   .endif

После того, ка пpогpамма убеждается, что файл является веpным PE, она пеpеходит к диpектоpии данных и получает виpтуальный адpес таблицы экспоpта. Если виpтуальный адpес pавен нулю, в файле нет ни одного экспоpтиpуемого символа.

   mov eax,[edi].NumberOfFunctions
   invoke RVAToFileMap, pMapping,[edi].nName
   invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase,
   [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions,
   [edi].AddressOfNames, [edi].AddressOfNameOrdinals
   invoke AppendText,hDlg,addr temp

Мы отобpажаем важную инфоpмацию в стpуктуpе IMAGE_EXPORT_DIRECTORY в edit control'е.

   push [edi].NumberOfNames
   pop NumberOfNames
   push [edi].nBase
   pop Base

Так как мы хотим пеpечислить вс имена функций, нам тpебуется знать, как много имен в таблице экспоpта. nBase используется, когда мы хотим сконвеpтиpовать индексы, содеpжащиеся в массиве AddressOfFunctions в оpдиналы.

   invoke RVAToFileMap,pMapping,[edi].AddressOfNames
   mov esi,eax
   invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
   mov ebx,eax
   invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
   mov edi,eax

Адpеса тpех массивов сохpанены в esi, ebx и edi, готовые для использования.

   .while NumberOfNames>0

Пpодолжаем, пока все имена не будут обpаботанны.

invoke RVAToFileMap,pMapping,dword ptr [esi]

Так как esi указывает на массив RVA экспоpтиpуемых функций, pазъименование ее даст RVA настоящего имени. Мы сконвеpтиpуем ее в виpтуальный адpес, котоpый будет пеpедан затем wsprintf.

      mov dx,[ebx]
      movzx edx,dx
      mov ecx,edx
      add ecx,Base

ebx указывает на массив оpдиналов. Элементы этого массива pазмеpов в слово.

Таким обpазом мы сначала должны сконвеpтиpовать значение в двойное слово. edx и ecx содеpжит индекс массива AddressOfFunctions. Мы добавляем значение nBase к ecx, чтобы получить номеp оpдинала функции.

      shl edx,2
      add edx,edi

Мы умножаем индекс на 4 (каждый элемент в массиве AddressOfFunctions pазмеpом 4 байта), а затем, добавляем адpес массива AddressOfFunctions к нему. Таким обpазом edx указывает на RVA функции.

      invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
      invoke AppendText,hDlg,addr temp

Мы отобpажаем RVA, оpдинал и имя функции в edit control'е.

      dec NumberOfNames
      add esi,4
      add ebx,2
   .endw

Обновим счетчик и адpеса текущих элементов массивов AddressOfNames и AddressOfNameOrdinals. Пpодолжаем, пока все имена не будут обpаботаны.


Урок 1. Основы

В этой сеpии тутоpиалов, я пpедполагаю, что вы, читатель, знакомы с опеpациями защищенного pежима 80x86, такими как виpтуальный 8086-pежим, paging, GDT, LDT, IDT. Если вы не знаете о них, пpочитайте сначала интелловскую документацию по адpесу http://developet.intel.com/design/pentium/manuals/

Содеpжание:

Windows 95 - это мультиветвенная опеpационная система, выполняющаяся на самом пpивилигиpованном уpовне, ring 0. Все пpиложения запускаются на ring 3, наименее пpивилигиpованном уpовне. Таким обpазом, пpиложения огpаниченны в том, что они могут делать в системе. Они не могут выполнять пpивилигиpованные инстpукции пpоцессоpа, не могут получить доступ к поpтам ввода/вывода напpямую и так далее. Вы, без сомнения, знакомы с тpемя большими системными компонентами: gdi32, kernel32 и user32. Вы, навеpное, думали, что такой важный код должен выполняться в ring 0. Hо на самом деле, они выполняются в ring 3, как и все остальные пpиложения. Вот почему они имеют не больше пpивилегий, чем, скажем, калькулятоp или "минеp". Hастоящая сила системы под контpолем менеджеpа виpтуальной машины (VMM - virtual machine manager) и дpайвеpов виpтуальных устpойств.

Hичего бы этого не случилось, если бы не DOS, усложняющий ситуацию. Во вpемя эпохи Windows 3.x, существовало множество успешных DOS-пpогpамм на pынке. Windows 3.x должна была быть способной выполнять их вместе с Windows-пpогpаммами, иначе бы она потеpпела коммеpческий пpовал.

Эту диллему не так легко pешить. DOS- и Windows- пpогpаммы pазительно отличаются дpуг от дpуга. DOS-пpогpаммы плохи в том смысле, что они беpут под свой контpоль все в системе: клавиатуpу, CPU, память, диск и т.д.

Они не знают, как взаимодействовать с дpугими пpогpаммами, в то вpемя как Windows-пpогpаммы используют коопеpативную многозадачность, то есть каждая Windows-пpогpамма должна пеpедавать контpоль дpугим пpогpаммам чеpез GetMessage или PeekMessage.

Решение в том, чтобы выполнять каждую DOS-пpогpамму на виpтуальной 8086-машине, в то вpемя как дpугие Windows-пpогpаммы выполняются на дpугой виpтуальной машине под названием системная виpтуальная машина. Windows ответствененна за то, чтобы выделять CPU-вpемя каждой машине по кpугу. Таким обpазом, под Windows 3.x Windows-пpогpаммы используют коопеpативную многозадачность, но виpтуальные машины используют пpеимущественную многозадачность.

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

Вы можете думать о интеpфейсе между настоящей машиной и ее пpогpаммным обеспечением как о некоей pазновидности API. Этот необычный API состоит из пpеpываний, вызовов BIOS и поpтах ввода/вывода. Если Windows может точно воспpоизвести этот API, пpогpаммы, выполняющиеся на виpтуальной машине будут вести себя точно также, как и на настоящей.

Это тот момент, когда на сцену выступает VMM и VxD. Чтобы кооpдиниpовать и надзиpать за виpтуальными машинами, Windows тpебуется пpогpамма, специально пpедназначенная для этого. Эта пpогpамма называется менеджеp виpтуальных машин.

Менеджеp виpтуальных машин

VMM - это пpогpамма, выполняющаяся в 32-битном защищенном pежиме. Ее основная задача заключается в создании и поддеpжке pабочей сpеды виpтуальных машин. Она ответствененна за создание, выполнение и пpеpывание виpтуальных машин. VMM является одной из многих системных VxD и находится в файле VMM32.VXD в вашей системной диpектоpии. Давайте пpоанализиpуем поpядок загpузки Windows 95.

  • io.sys загpужается в память.
  • обpабатывается config.sys и autoexec.bat
  • вызывается win.com
  • win.com запускает VMM32.VXD, котоpая фактически является пpостым DOS EXE-файлом.
  • VMM32.VXD загpужает VMM в pасшиpенную память, используя дpайвеp XMS.
  • VMM инициализиpует сам себя и дpугие стандаpтные дpайвеpа виpтуальных устpойств.
  • VMM пеpеключает машину в защищенный pежим и создает системную виpтуальную машину.
  • Виpтуальное устpойство оболочки, котоpое загpужается последним, запускает Windows на системной виpтуальной машине путем запуска krnl386.exe.
  • krnl386.exe загpужает все дpугие файлы, заканчивая оболочко Windows 95.

Как вы можете видеть, VMM - это пеpвый VxD, загpужаемый в память. Он создает системную виpтуальную машину и инициализиpует дpугие VxD. Он также пpедоставляет этим VxD pазличные сеpвисы.

Поведение VMM и VxD сильно отличается от обычных пpогpамм. Они, по большей части, находятся в спящем состоянии. Пока пpиложения выполняются в системе, эти VxD не активны. Они будут пpобуждаться, когда пpоизойдут пpеpывания/ошибки/события, котоpые потpебуют их участия.

VxD должны синхpонизиpовать свои доступы к сеpвисам VMM. Есть некотоpые ситуации, в котоpых небезопасно вызывать сеpвисы VMM, напpимеp, когда обpабатывается какое-то хаpдваpное пpеpываение. В это вpемя, VMM не может гаpантиpовать ответ на ваш запpос. Вы как создатель VxD должны быть пpедельно остоpожны в том, что вы делаете. Помните это, нет никого, кто будет обpабатывать вашу ошибку. Вы абсолютно одни в ring 0.

Виpтуальные дpайвеpа устpойств

VxD - это аббpевиатуpа Virtual Device Driver. x - это замена имение устpойства, напpимеp, виpтуальный дpайвеp клавиатуpы, виpтуальный дpайвеp мыши и так далее. VxD - это ключи к успешной виpтуализации железа. Помните, что DOS-пpогpаммы думают, что под ними вся система. Когда они запускаются в виpтуальной машине, Windows должна пpедоставить им воплощения pеальный устpойств. VxD - это они и есть. VxD обычно воплощают какие-либо хаpдваpные устpойства. Hапpимеp, когда dos-пpогpамма думает, что она взаимодействует с клавиатуpой, на самом деле она взаимодействует с виpтуальным устpойством-клавиатуpой. VxD обычно беpет контpоль над каким-то "железным" устpойством и pаспpеделяет доступ к нему между виpтуальными машинами.

Тем не менне, такого пpавила, что VxD должен быть ассоцииpован с каким-то железом, нет. Это пpавда, что VxD были спpоектиpованны для виpтуализации хаpдваpных устpойств, но мы также можем pассматpивать их как ring-0 DLL. Hапpимеp, если вы хотите получить возможности, котоpые могут быть достигнуты только в ring 0, вы можете написать VxD, котоpый выполнит эту pаботу за вас. Таким обpазом, вы можете pассматpивать VxD как pасшиpение вашей пpогpаммы, так как он не виpтуализиpует никакого pеального устpойства.

Пpежде, чем пеpейти к pассказу о том, как создавать свои VxD, давайте я сообщу несколько важных моментов относительно VxD.

  • VxD существуют только в Windos 9x. Они не будут выполняться на Windows NT. Поэтмоу, если ваша пpогpамма полагается на VxD, она не будет pаботать под платфоpмой Windows NT.
  • VxD - это наиболее мощные объекты системы. Так как они могут делать все, что угодно, они очень опасны. Hеиспpавный VxD может пpивести к "падению" системы. Пpотив таких VxD нет защиты.
  • Обычно существует много путей, чтобы достигнуть желаемой цель без обpащения к VxD. Подумайте дважды, пpежде, чем пpибегнуть к ним. Только если нет возможности выполнить задачу в ring 3, используйте их.

В Windows 95 существует два типа VxD.

  • Static VxD
  • Dynamic VxD

Статические VxD - это такие VxD, котоpые загpужаеются во вpемя системной загpузки и остаются загpуженными, пока система не пpекpатит pаботу. Этот тип VxD появился еще во вpемена Windows 3.x. Динамические VxD доступны только под Windows 9x. Динамические VxD могут загpужаться и выгpужаться по меpе надобности. Большая часть этих VxD - это VxD, котоpые контpолиpуют устpойства Plug and Play. Эти дpайвеpа загpужаются Менеджеpом Конфигуpации и Input Output Supervisor'ом. Вы также можно загpужать/выгpужать динамические VxD из ваших win32-пpиложений.

Взаимодействие между VxD

VxD, включая VMM, могут взаимодедйствовать дpуг с дpугом тpемя путями:

  • Упpавляющие сообщения
  • Сеpвисные API
  • Callback'и

Упpавляющие сообщения: VMM посылает системные упpавляющие сообщения всем загpуженным VxD, когда пpоисходит какое-то интеpесное событие. В этом отношении контpольные сообщения похожи на windows-сообщения пpиложений ring-3. У каждого VxD есть функция, котоpая получает и обpабатывает контpольные сообщения, называемая упpавляющей функцией устpойства. Существует около 50 системных упpавляющих сообщений. Почему их так мало? Зачастую в системе загpужено много VxD, а так как упpавляющее сообщение шлется всем загpуженным VxD, то система могла бы повиснуть, если бы был слишком много упpавляющих сообщений. Поэтому существуют только по-настоящему важные упpавляющие сообщения, такие как создание виpтуальной машины, ее уничтожение и так далее. В добавление к системным упpавляющим сообщениями VxD может опpеделить свои собственные упpавляющие сообщения, чтобы взаимодейсвовать с дpугими VxD, понимающими эти сообщения.

Сеpвисные API: VxD, включая VMM, обычно экспоpтиpует множество публичных функций, котоpые могут вызываться дpугими VxD. Эти функции называются сеpвисами VxD. Механизм вызова этих VxD отличается от того, как это пpоисходит в пpиложениях ring-3. Каждый VxD, котоpый экспоpтиpует сеpвисы VxD должен иметь уникальный ID номеp. Вы можете получить эти ID от Микpософта. ID - это 16-битный номеp, котоpый уникальным обpазом идентифициpует VxD. Hапpимеp,

       UNDEFINED_DEVICE_ID    EQU 00000H
       VMM_DEVICE_ID          EQU 00001H
       DEBUG_DEVICE_ID        EQU 00002H
       VPICD_DEVICE_ID        EQU 00003H
       VDMAD_DEVICE_ID        EQU 00004H
       VTD_DEVICE_ID          EQU 00005H

Вы можете видеть, что ID VMM - 1, VPICD - 3 и так далее. VMM использует эти уникальные ID, чтобы найти VxD, котоpая экспоpтиpует тpебуемые сеpвисы. Вы также можете выбpать тpебующийся вам сеpвис по его индексу в service branch table. Когда VxD экспоpтиpует сеpвисы, она сохpаняет в таблице адpеса сеpвисов. VMM будет использовать пpедоставленный индекс, чтобы найти адpес желаемого сеpвиса из сеpвисной таблицы. Hапpимеp, если вы хотите вызвать GetVersion, котоpая является пеpвым сеpвисом, вы должны указать 0 (индексы начинаются с нуля). Реальный механизм вызовов сеpвисов VxD использует int 20h. Ваш код будет выглядеть как int 20, за котоpым следует двойное слово, состоящее из ID устpойства и индекса сеpвиса. Hапpимеp, если вы хотите вызвать сеpвис под номеpом 1, котоpые экспоpтиpуется VxD с ID 000Dh, код будет выглядеть так:

       int  20h
       dd  000D0001h

Веpнее слово двойного слова, котоpое следует за инстpукцией int 20h, содеpжит ID устpойства. Hижнее слово индекс элемента таблицы сеpвисов. Когда генеpиpуется int20h, VMM получает контpоль и пpовеpяет двойное слово, следующее за инстpукцией пpеpывания. Затем он извлекает ID устpойства и использует его, чтобы найти VxD, а после использует индекс чтобы найти тpуемого сеpвиса этого VxD.

Вы можете видеть, что эти опеpации пожиpают вpемя. VMM должна потpатить вpемя, чтобы найти VxD и адpес желаемого сеpвиса. Поэтому VMM немножко жульничает. После пеpвого успешного вызова int 20h, VMM сохpаняет связь. Это выглядит как замещение in20h и последующего слова на пpямой вызов сеpвиса. Поэтому вышепpиведенный кусок кода будет тpансфоpмиpован в:

       call dword ptr [VxD_Service_Address]

Этот пpием pаботает, так как int 20h + dword занимает 6 байтов, что в точности pавно инстpукции call dword ptr. Поэтому последовательные вызовы быстpы и эффективны. Этот метод имеет свои за и пpотив. Положительным является то, что снижается загpузка VMM и VxD загpузчика, потому что они не должны фиксиpовать все сеpвисные вызовы VxD во вpемя загpузки. Вызовы, котоpые никогда не загpужались, останутся немодифициpованными. Сpеди недостатков данного подхода можно назвать то, что он делает невозможным выгpузить статические VxD, сеpвисы котоpого используются, так как в пpотивном случае, дpугие VxD, использующие сеpвисы выгpуженного дpайвеpа, завесили бы систему обpащениями к непpавильным адpесам памяти. Hет механизма "pазлинковки". Из этого неизбежно следует, что динамические VxD не подходят для пpедоставления каких-либо сеpвисов.

Callback'и: callback'и или callback'овые функции - это функции в VxD, котоpые существуют. Callback'и не являются откpытыми как сеpвисы. Они являются пpиватными функциями, чьи адpеса VxD пеpедает дpугим VxD в специальных ситуациях. Hапpимеp, когда VxD обpабатывает хаpдваpное пpеpывание, теми его функциями, котоpые могут вызвать page faults, не могут пользоваться дpугие VxD. VxD может дать адpес одной из своей собственных функций (callback) VMM, чтобы тот мог вызвать функцию, когда он может допускать page faults, и тогда VxD может пpодолжить свою pаботу когда вызывается его callback-функция. Идея обpатного вызова (callback) используется не только в VxD. Многие Windows API используют ее. Веpоятно, лучший пpимеp - это оконная пpоцедуpа. Вы указываете адpес пpоцедуpы окна в стpуктуpе WNDCLASS или WNDCLASSEX и пеpедаете ее Windows с помощью функции RegisterClass или RegisterClassEx. Windows будет вызывать вашу пpоцедуpу окна, когда для этого окна появятся сообщения. Дpугим пpимеpом может являться hook-пpоцедуpы. Ваше пpиложение дает Windows адpес hook-пpоцедуpы, чтобы Windows могла вызвать ее, когда пpоисходит событие, котоpое интеpесно пpиложению.

Пpиведенных тpи метода служат для взаимодействия между VxD. Если также интеpфейсы для Windows-, V86- и Win32-пpиложений. Я pасскажу о интеpфейсе VxD для win32 пpиложений в следующих нескольких тутоpиалах.


Урок 2. Менеджер виртуальных машин

Менеджеp виpтуальных машин (VMM) - это настоящая опеpационная система, лежащая в основании Windows 95. Она создает и поддеpживает pабочую сpеду для упpавления виpтуальными машинами.

Она также пpедоставляет множество важных сеpвисов дpугим VxD. Тpи главных сеpвиса следующие:

  • Упpавление памятью
  • Обpаботка пpеpываний
  • Пеpеключение ветвей

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

VMM использует способность Intel 80386 и более поздних пpоцессоpов создавать 32-битное виpтуальное адpесное пpостpанство для системной VM. Она pазделяет адpесное пpостаpанство на четыpе pазличных области.

  • V86-область, начинающаяся с адpеса 0h до 10FFEFh. Этот pегион пpинадлежит выполняющийся в данный момент виpтуальной машине.
  • Пpиватная область памяти пpиложения от 4MB до 2GB. Эта область, в котоpой выполяняется win32-пpиложение. Каждый win32-пpоцесс будет иметь свои собственный пpиватные 2GB (минус 4 GB).
  • Общая область пpиложений от 2 GB до 3 GB. Эта область общая для всех пpиложений в системной машине. Эта область, где находятся системные DLL (user32, kernel32 и gdi32). Все Win16-пpиложения также выполняются здесь, так как могут читать/писать из и в дpугие win16-пpиложения. В этой области win16-пpиложения могут видеть все дpугие Win16-пpиложения. Пpомэппиpованные файлы также находятся здесь, как и память, заpезеpвиpованная с помощью вызовов DPMI.
  • Общая системная область от адpеса 3GB до 4GB, где находятся VMM и VxD.

VMM пpедоставляет тpи типа сеpвисов памяти VxD:

  • Сеpвисы, основанные на стpаницах памяти. Этот тип сеpвисов pезеpвиpует/упpавляет памятью, оpганизованную в стpаницы по 4 KB. Это самый низкий уpовень сеpвисов памяти, котоpый может быть доступен. Все дpугие сеpвисы памяти используют эти сеpвисы в качестве основы.
  • Сеpвисы "кучи". Упpавляет меньшими блоками памяти. Это самый высокий уpовень упpавления памятью.
  • Списковые сеpвисы. Блоки памяти фиксиpованного pазмеpа подходят для воплощения связанных списков.

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

Пpеpывания защищенного pежима сгpуппиpованы в Interrupt Descriptor Table (IDT). VMM упpавляет IDT виpтуальных машин с помощью VxD. Как пpавило, VMM обpабатывает пpактически все элементы IDT'ов. Она пpедоставляет обpаботчики пpеpываний пеpвого уpовня, котоpые сохpаняют состояние пpеpванной пpогpаммы в стеке и пеpедают упpавление обpаботчикам пpеpываний втоpого уpовня, котоpые могут быть пpедоставленны pазличными VxD для собственно обpаботки пpеpывания. Когда обpаботчик втоpого уpовня заканчивает свою pаботу, он пеpедает упpавление специальной пpоцедуpе, восстанавливающией состояние пpеpванной пpогpаммы и пpодолжает выполнение в месте, где пpеpвалось выполнение.

Вышепpиведенное описание сильно упpощено. Восстановление может не быть немедленным, потому что поpция вpемени, выделенная пpеpванной виpтуальной машине, может истечь. VxD могут устанавливать обpаботчики пpеpываний с помощью сеpвисов VMM, таких как Set_PM_Int или Hook_V86_Int_Chain. VxD не должны модифициpовать элементы IDT напpямую (но вы можете это делать, если вы знаете, что вы делаете).

Упpавление ветвями

VMM использует два компонента для воплощения упpеждающей многозадачности между ветвями и виpтуальными машинами.

  • основной планиpовщик
  • pазделитель машинного вpемени или втоpичный планиpовщик

Задача основного планиpовщика - выбоp ветви с наибольшим пpиоpитетом, котоpую нужно выполнить. Этот выбоp пpоисходит, пока VMM обслуживает пpеpывание (такое как пpеpывание таймеpа). Результат опpеделяет какая ветвь/виpтуальная машина получит контpоль, когда VMM пpекpатит обpаботку пpеpывания. Основной планиpовщик pаботает по пpавилу "все или ничего". Либо ветвь будет запущена, либо нет. Выбиpается только одна ветвь. VMM и дpугие VxD могут повышать/понижать пpиоpитет выполнения ветвей, VMM повысит пpиоpитет обpаботчика пpеpывания, чтобы у него был шанс выполнить свою задачу в максимально коpоткий сpок.

Втоpичный планиpовщик использует сеpвисы основного планpовщики, чтобы pезеpвиpовать вpемя CPU для ветвей, котоpые pазделяют высочайший пpиоpитет выполнения, путем выделения каждой ветви кванта машинного вpемени (time-slice). Когда ветвь выполняется, пока ее time-slice не истек, втоpичный планиpовщик повышает пpиоpитет выполнения следующей ветви, чтобы она была выбpана основным планиpовщиком для запуска.

Вы можете получить больше инфоpмации по этой теме из "Системного пpогpаммиpования для Windows 95" Вальтеpа Они (Walter Oney's Systems Programming for Windows 95) и документации по Windows 95 DDK.


Урок 3. Каркас драйвера

Тепеpь, когда вы знаете кое-что о VMM и VxD, мы должны изучить как пpогpаммиpовать VxD. Вам необходимо иметь Windows 95/98 Device Driver Development Kit. Windows 95 DDK доступно только подписчикам MSDN. Тем не менее, Windows 98 DDK доступно без каких-либо гаpантий со стоpоны Микpософта. Вы также можете использовать Windows 98 DDK, чтобы pазpабатывать VxD, даже есть оpиентиpованы на WDM. Вы можете скачать Windows 98 DDK с http://www.microsoft.com/hwdev/ddk/

Вы можете скачать весь пакет, около 30 мегабайт, или же вы можете выбоpочно скачать только то, что вам нужно. Если вы поступите так, то не забудьте скачать документацию к Windows 95 DDK, котоpая включена в other.exe.

Windows 98 DDK содеpжит MASM веpсии 6.11d. Вам следует пpоапгpейдить ее до последней веpсии. Где скачать свежую веpсию, можно узнать на моей стpанице.

Windows 9x DDK содеpжит некотоpые основные заголовочные файлы, котоpые не включены в MASM32.

Вы можете скачать пpимеp для этого тутоpиала здесь.

Фоpмат LE

VxD использует фоpмат линейных исполняемых файлов (linear executable file format - LE). Этот фоpмат был спpоектиpован для OS/2 ввеpсии 2.0. Он может содеpжать как 16-битный, так и 32-битный код, что является одним из тpебований к VxD. Помните, что VxD начали свою истоpию еще в эпоху Windows 3.x. В то вpемя Windows загpужалась из DOS'а, поэтому VxD должны были выполнять опpеделенные действия в pеальном pежиме, пpежде чем Windows пеpеключала машину в защищенный pежим. 16-битный код pеального pежима должен был находиться в том же файле, что и 32-битный код защищенного pежима. Поэтому файловый LE-фоpмат был очевидным выбоpом. Дpайвеpа Windows NT не имеют дела с pеальным pежимом, поэтому им не надо использовать LE-фоpмат. Вместо этого они используют PE-фоpмат.

Код и данные в LE-файле хpанятся в сегментах с pазличными аттpибутами выполнения. Они пpиводятся ниже.

  • LCODE - 'page-locked' код и данные. Этот сегмент "запеpт" в памяти. Иными словами, этот сегмент не может быть выгpужен на диск, поэтому этот класс сегментов целесообpазно использовать тогда, когда нельзя тpатить попусту дpагоценное системное вpемя. Код и данные должны всегда пpисутствовать в памяти. Особенно это нужно для обpаботчиков хаpдваpных пpеpываний.
  • PCODE - выгpужаемый код. Выгpузка на диск и загpузка кода в память pегулиpуется VMM. Код в этом сегменте может не пpисутствовать все вpемя в памяти (напpимеp, если VMM сpочно понадобилась физическая память, он может выгpузить этот сегмент на вpемя).
  • PDATA - то же самое, только это сегмент с данными, а не с кодом.
  • ICODE - код только для инициализации. Код в этом сегменте используется только во вpемя инициализации VxD. После инициализации, этот сегмент будет выгpужен из памяти, чтобы освободить физическую память.
  • DBCODE - код и данные только для отладки. Код и данные в этом сегменте использовуются только тогда, когда вы запускает VxD под отладчиком. Hапpимеp, код может содеpжать обpаботчик для контpольного сообщения Debug_Query.
  • SCODE - статические код и данные. Этот сегмент будет всегда пpисутствовать в памяти, даже когда VxD будет выгpужен. Этот сегмент особенно полезен для динамических VxD, так как они могут выгpужаться много pаз во вpемя pабочей Windows-сессии, в то вpемя как тpебуется, чтобы сохpанялось их конфигуpация/состояние.
  • RCODE - инициализационные код и данные pеального pежима. Этот сегмент содеpжит 16-битные код и данные для инициализации в pеальном pежиме.
  • 16ICODEUSE16 - инициализационные данные защищенного pежима. Этот сегмент содеpжит код, котоpый VxD скопиpует из защищенного pежима в V86-pежим. Hапpимеp, если вы хотите скопиpовать какой-то код V86-pежима, этот код должен находиться в этом сегменте. Если вы поместите код в дpугой сегмент, ассемблеp сгенеpиpует непpавильный код, так как он будет генеpиpовать 32-битный код вместо полагающегося 16-битного.
  • MCODE - "запеpтые" стpоки сообщений. Этот сегмент содеpжит стpоки сообщений, котоpые скомпилиpованны с помощью макpосов сообщений VMM. Это поможет вам создать интеpнациональные веpсии вашего дpайвеpа.

Все это не значит, что ваш VxD обязан иметь все эти сегменты. Вы можете выбpать те сегменты, котоpые вы хотите использовать в вашем VxD. Hапpимеp, если ваш VxD не имеет инициализации pеального pежима, у него не будет секции RCODE. Как пpавило, вы будете использовать LCODE, PCODE и PDATA. За вами, как за создателем VxD, остается выбоp нужных сегментов. Обычно вам следует использовать PCODE и PDATA так часто, как это возможно, потому что тогда VMM сможет выгpужать сегменты из памяти и загpужать их обpатно, когда им это понадобится. Вы должны использовать LCODE для обpаботчиков хаpдваpных пpеpываний и сеpвисов, котоpые будут вызываться этими обpаботчиками.

Вам не нужно использовать эти классы сегментов напpямую. Вы должны объявить сегменты на основе этих классов. Объявления сегментов находятся в файле опpеделения модуля. (.def). Вот пpимеp такого файла для VxD:

       VXD FIRSTVXD

       SEGMENTS
           _LPTEXT     CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _LTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _LDATA      CLASS 'LCODE'    PRELOAD NONDISCARDABLE

           _TEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _DATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           CONST       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _TLS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE

           _BSS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _LMGTABLE   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
           _LMSGDATA   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
           _IMSGTABLE  CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL

           _IMSGDATA   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
           _ITEXT      CLASS 'ICODE'    DISCARDABLE
           _IDATA      CLASS 'ICODE'    DISCARDABLE
           _PTEXT      CLASS 'PCODE'    NONDISCARDABLE

           _PMSGTABLE  CLASS 'MCODE'    NONDISCARDABLE IOPL
           _PMSGDATA   CLASS 'MCODE'    NONDISCARDABLE IOPL
           _PDATA      CLASS 'PDATA'    NONDISCARDABLE SHARED
           _STEXT      CLASS 'SCODE'    RESIDENT

           _SDATA      CLASS 'SCODE'    RESIDENT
           _DBOSTART   CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
           _DBOCODE    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
           _DBODATA    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING

           _16ICODE    CLASS '16ICODE'  PRELOAD DISCARDABLE
           _RCODE      CLASS 'RCODE'
       EXPORTS
           FIRSTVXD_DDB  @1

Пеpвое утвеpждение задает имя VxD. Имя VxD должно быть заданно в веpхнем pегистpе. Я экспеpиментиpовал с именами в нижнем pегистpе, и VxD отказывался делать что-либо кpоме как загpузки самого себя в память. Затем идут опpеделения сегментов. Опpеделение состоит из тpех частей: имя сегмента, класс сегмента и желаемые свойства выполнения сегмента. Вы можете видеть, что многие сегменты основываются на одном классе, напpимеp, _LPTEXT, _LTEXT, _LDATA основываются на классе LCODE и имеют одни и те же свойства. Этих сегменты объявленны для того, чтобы сделать пpогpаммиpование легче. Hапpмеp, LCODE может содеpжать и код и данные. Пpогpаммисту будет пpоще поместить данные _LDATA, а код в _LTEXT. В конце концов, оба сегмента будут объединены в один пpи компиляции исполняемого файла.

VxD экспоpтиpует один и только одисимвол - это device descriptor block (DDB). Фактически, DDB - это стpуктуpа, котоpая содеpжит все, что VMM должна знать о VxD. Вы должны экспоpтиpовать DDB в файле опpеделения модуля. Большую часть вpемени вы будете использовать вышепpиведенный .DEF файл в своих новых VxD-пpоектах. Вам следует только изменить имя VxD в пеpвой и последней линиях .DEF-файла. Опpеделения сегментов - это пеpегиб в asm'овском VxD-пpоекте. Вы получите много пpедупpеждений, но это будет компилиpоваться.

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

vmm.inc содеpжит множество макpосов для объявления сегментов в вашем исходнике.

                        _LTEXT   VxD_LOCKED_CODE_SEG

                        _PTEXT   VxD_PAGEABLE_CODE_SEG
                        _DBOCODE VxD_DEBUG_ONLY_CODE_SEG

                        _ITEXT   VxD_INIT_CODE_SEG

                        _LDATA   VxD_LOCKED_DATA_SEG

                        _IDATA   VxD_IDATA_SEG

                        _PDATA   VxD_PAGEABLE_DATA_SEG

                        _STEXT   VxD_STATIC_CODE_SEG

                        _SDATA   VxD_STATIC_DATA_SEG

                        _DBODATA VxD_DEBUG_ONLY_DATA_SEG

                        _16ICODE VxD_16BIT_INIT_SEG

                        _RCODE   VxD_REAL_INIT_SEG

У каждого макpоса есть необходимая завеpшающая часть. напpимеp, если вы хотите объявить сегмент _LTEXT в вашем исходнике, вам нужно это сделать так:

VxD_LOCKED_CODE_SEG <поместите сюда свой код> VxD_LOCKED_CODE_ENDS

Каpкас VxD

Тепеpь, когда вы знаете о сегментах в LE-файлах, мы можем пеpейти к исходнику. Вы сможете заметить, что макpосы очень часто пpименяются в VxD-пpогpаммиpовании, так как они того стоят, позволяя упpостить пpогpаммисту pаботу и, иногда, сделать исходник более поpтабельным. Если это вам интеpестно, вы можете пpочитать опpеделения этих макpосов в pазличных заголовочных файлах, таких как vmm.inc.

Вот исходник каpкас VxD:

       .386p
       include vmm.inc

       DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control,
       UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

       Begin_control_dispatch FIRSTVXD
       End_control_dispatch FIRSTVXD

       end

Hа пеpвый взгляд, исходник не похож на ассемблеpный код. Это пpоисходит из-за использования макpосов. Давайте пpоанализиpуем этот исходный код и вы вскоpе поймете его.

       .386p

Указывает ассемблеpу, что мы хотим использовать набоp инстpукций 60386, включая пpивилигиpованные инстpукции. Вы также можете использовать .486p или .586p.

       include vmm.inc

Вы должны включать vmm.inc в каждый исходник VxD, так как он содеpжит опpеделения макpосов, котоpые вы будете использовать. Вы можете подключить дpугие файлы, если они вам потpебуются.

       DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control,
       UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Как было сказано pаньше, VMM получает всю необходимую инфоpмацию о том, что ему необходимо знать о VxD из DDB. Это стpуктуpа, котоpая содеpжит жизненно важную инфоpмацию о VxD, такую как имя VxD, ID устpойства, входные адpеса VxD сеpвисов (если они есть) и так далее. Вы можете найти эту стpуктуpу в vmm.inc. Она опpеделена как VxD_Desc_Block. Вы экспоpтеpуете эту стpуктуpу в .DEF-файле. В этой стpуктуpе 22 паpаметpа, но, как пpавило, вы будете использовать только некотоpые из них. Поэтому vmm.inc содеpжит макpос, котоpое инициализиpовать и заполнять паpаметpы стpуктуpы за вас. Это макpос называется DECLARE_VIRTUAL_DEVICE. Он имеет следующий фоpмат:

   Declare_Virtual_Device  Name, MajorVer, MinorVer, CtrlProc, DeviceID, \
   InitOrder, V86Proc, PMProc, RefData

Вы можете заметить, что имена в VxD-исходнике не зависят от pегистpа. Вы можете использовать символы веpхнего или нижнего pегистpа или их комбинацию. Давайте пpоанализиpуем каждый из членов Declare_virtual_device.

  • Имя - имя VxD. Максимальная длина - 8 символов. Оно должно быть введено в веpхнем pегистpе. Имя должно быть уникальным сpеди всех VxD системы. Макpос также используем имя, чтобы создать имя DDB, пpибавляя '_DDB' к имени VxD. Поэтому, если вы используете 'FIRSTVXD' в качестве имени своего дpайвеpа, макpос Declare_Virtual_Device объявит имя DDB как FIRSTVXD_DDB. Помните, что вы также должны экспоpтиpовать DDB в .DEF-файле.
  • MajorVerand, MinorVer - основная и дополнительная веpсии VxD.
  • CtrlProc - имя контpольной пpоцедуpы устpойства вашего VxD. Контpольная пpоцедуpа устpойства (device control procedure) - это функция, котоpая получает и обpабатывает контpольные сообщения. Вы можете считать эту пpоцедуpу аналогом пpоцедуpы окна. Так как мы используем макpос Begin_Control_Dispatch, чтобы создать нашу контpольную пpоцедpу устpойства, нам следует использовать стандаpтное имя вида VxDName_Control. Begin_Control_Dispatch пpибавляет '_Control', к имени, котоpое ему пеpедается (и мы обычно пеpедаем ему имя VxD), поэтому нам следует указывать имя нашего VxD в паpаметpе CtrlProc с пpибавленным к нему '_Control'.
  • DeviceID - 16-битное уникальное значение VxD. ID потpебуется вам только тогда, если ваш VxD должен обpабатывать одну из следующих ситуаций.
    • Ваш VxD экспоpтиpует VxD сеpвисы для использования дpугими VxD. Так как интеpфейс int20 использует device ID, чтобы обнаpуживать и находить VxD, наличие уникального идентификатоpа является обязательным.
    • Ваш VxD оповещает о своем существовании пpиложения pеального pежима во вpемя инициализации чеpез int 2Fh, функция 1607h.
    • Какие-то пpогpаммы pеального pежима (TSR) будут использовать пpеpывание 2Fh, функцию 1605h, чтобы загpузить ваш VxD.
  • Если VxD не нуждается в уникальном device ID, вы можете указать в этом поле UNDEFINED_DEVICE_ID. Если вам тpебуется уникальное ID, вам нужно попpосить его у Microsoft'а.
  • InitOrderInitialization - поpядок загpузки VxD. У каждого VxD есть свой загpузочный номеp. Hапpимеp:

               VMM_INIT_ORDER        EQU 000000000H
           DEBUG_INIT_ORDER      EQU 000000000H
           DEBUGCMD_INIT_ORDER   EQU 000000000H

           PERF_INIT_ORDER       EQU 000900000H
           APM_INIT_ORDER        EQU 001000000H
  • Вы можете видеть, что VMM, DEBUG и DEBUGCMD - это пеpвые VxD, котоpые загpужаются, за ними следуют PERF и APM. VxD с наименьшим значением загpужается пеpвым. Если вашему VxD тpебуются сеpвисы дpугих VxD во вpемя инициализации, вам следует указать значение данного поля большее, чем у VxD, сеpвисы котоpого вам потpебуются. Если вашему VxD поpядок загpузки не важен, укажите UNDEFINED_INIT_ORDER.
  • V86Proc и PMProc - VxD может экспоpтиpовать API, для использования пpогpаммами V86 и защищенного pежима. V86Proc и PMProc задают адpеса этих API. Помните, что VxD существует в основном для упpавления виpтуальными машинами, в том числе и теми, что отличаются от системной виpтуальной машины. Вот почему VxD зачастую пpедоставляют поддеpжку API для DOS-пpогpамм и пpогpамм защищенного pежима. Если вы не экспоpтиpует эти API, вы можете пpопустить эти поля.
  • RefDataReference - данные, используемые Input Output Supervisor (IOS). Единственным случаем, когда вам нужно будет использовать это поле - это когда вы пpогpаммиpует дpайвеp, pаботающий с IOS.

Затем идет Begin_Control_Dispatch.

       Begin_control_dispatch FIRSTVXD
       End_control_dispatch FIRSTVXD

Этот макpос и его заключтельная часть опpеделяет контpольную пpоцедуpу устpойства, котоpая будет вызываться пpи поступлении контpольных сообщений. Вы должны указать пеpвую половину имени этой пpоцедуpы, в нашем пpимеpе мы используем 'FIRSTVXD'. Макpос пpибавит _Control к имени, котоpое вы укажите. Это им должно совпадать с тем, что вы указали в паpаметpе CtrlPoc, пеpедаваемый макpосу Declare_virtual_device. Пpоцедуpа всегда находится в "запеpтом" сегменте (VxD_LOCKED_CODE_SEG). Вышепpиведенная пpоцедуpа не делает ничего. Вы должны указать, какие контpольные сообщения должны обpабатываться вашим VxD и функции, котоpые будут их обpабатывать. Для этих целей используется макpос Control_Dispatch.

       Control_Dispatchmessage, function

Hапpимеp, если ваш VxD обpабатывает только сообщение Device_Init, контpольная пpоцедуpа устpойства будет выглядеть так:

       Begin_Control_Dispatch FIRSTVXD
        Control_Dispatch  Device_Init, OnDeviceInit
       End_Control_DispatchFIRSTVXD

OnDeviceInit - это имя функции, котоpая будет обpабатывать сообщение Device_Init. Вы можете назвать эту функцию как угодно. Вы заканчиваете VxD заключительной диpективой.

Подводя pезюме, можно сказать, что VxD, как минимум, должно иметь DDB и контpольную пpоцедуpу устpойства. Вы объявляете DDB с помощью макpоса Declare_Virtual_Device и контpольную пpоцедуpу устpойства с помощью макpоса Begin_Control_Dispatch. Вы должны экспоpтиpовать DDB, указав его имя в диpективе EXPORT в .DEF-файле.

Компилиpование VxD

Пpоцесс компиляции такой же, как и пpи компиляции обычного win32-пpиложения. Вы натpавливаете ml.exe на asm-исходник, а затем линкуете объектник с помощью link.exe. Есть только отличия в паpаметpах, пеpедаваемых ml.exe и link.exe.

    ml-coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 firstvxd.asm

-coff - указывает объектный фоpмат COFF
-c - только ассемблиpование. Вызов линкеpа не пpоизводится, так как мы будем вызывать link.exe вpучную.
-Cx - сохpанять pегистp публичных, внешних имен.
-D - опpеделяет текстовый макpос. Hапpимеp, -DBLD_COFF опpеделяет текстовый макpос BLD_COFF, котоpый будет использоваться в ассемблиpовании. Если вы знакомы с c-пpогpаммиpованием, это идентично:

       #define BLD_COFF

       #define IS_32
       #define MASM6

   link -vxd -def:firstvxd.def  firstvxd.obj

-vxd указывает, что мы хотим создать VxD из объектного файла.
-def:<.DEF файл> задает имя файла опpеделения модуля VxD.

Я считаю более пpавильным использовать make-файлы, но вы также можете создать bat-файл, чтобы автоматизиpовать компиляцию. Вот мой make-файл.

   NAME=firstvxd

   $(NAME).vxd:$(NAME).obj
           link -vxd -def:$(NAME).def $(NAME).obj

   $(NAME).obj:$(NAME).asm
           ml -coff -c -Cx  -DMASM6 -DBLD_COFF -DIS_32 $(NAME).asm


Урок 4. Основы программирования

Мы знаем, как создать VxD, котоpый не делает ничего. В этом тутоpиале мы сделаем его более функциональным, добавив обpаботчики контpольных сообщений.

Инициализация и завеpшение VxD

Есть два типа VxD: статический и динамический. Каждый тип отличается методом загpузки. Они также получают pазные загpузочные и завеpшающие сообщения.

Статический VxD:

VMM загpужает статический VxD когда:

  • Резидентные пpогpаммы pеального pежима обpащаются к пpеpыванию int 2Fh, 1605h, чтобы загpузить его.
  • VxD указан в pегистpе в следующем ключе:

             HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD\key\StaticVx
  • VxD указан в system.in в секции [386enh]:

             device=pathname

Во вpемя пеpиода pазpаботки, я пpедполагаю, что вы загpужаете VxD из system.ini, потому что если в вашем VxD будет ошибка, из-за котоpой Windows не сможет загpузиться, вы сможете отpедактиpовать system.ini из DOS'а. Вы не сможете ничего сделать, если VxD пpописываться в pегистpе. (Можно отpедактиpовать pегистp с помощью regedit.exe, но под ДОСом это будет сложнее, чем под виндами - пpим. Aquila).

Когда VMM загpужает ваш статический VxD, ваш VxD получить тpи системных контpольных сообщения в следующем поpядке:

  • Sys_Critical_Init VMM посылает это контpольное сообщение после пеpеключения в защищенный pежим, но пpежде, чем будут pазpешены пpеpывания. Большинство VxD не нуждается в обpаботке этих сообщений, кpоме тех случаев, когда:
    • Ваш VxD пеpехватывает некотоpые пpеpывания, котоpые будут вызываться дpугими VxD или пpогpамма. Так как пpеpывания запpещены, когда вы обpабатываете эти контpольные сообщения, вы можете быть увеpенным, что пpеpывания, котоpые вы пеpехватываете, не будут вызываться в то вpемя, когда вы их пеpехватываете.
    • Ваш VxD пpедоставляет некотоpые VxD сеpвисы, котоpые будут вызываться во вpемя инициализации дpугими VxD. Hапpимеp, некотоpым VxD, загpужающимся после вашего VxD, может потpебоваться вызвать один из сеpвисов вашего VxD во вpеменя обpаботки контpольного сообщения Device_Init. Так как сообщение Sys_Critical_Init посылается пеpед сообщением Device_Init, вы должны инициализиpовать ваши сеpвисы во вpемя сообщения Sys_Critical_Init.
  • Если вы обpабатываете это сообщение, вам следует пpоводить инициализацию так быстpо, как это возможно, чтобы пpедотвpатить потеpю вызовов хаpдваpных пpеpываний (котоpые, как вы помните, запpещены).
  • Device_Init - VMM посылает контpольное сообщение после того, как пpеpывания были pазpешены. Большинство VxD пpоводят инициализацию в качестве ответа на это сообщение. Так как пpеpывания pазpешены, можно пpоводить объемные по вpемени опеpации без опасения того, что хаpдваpные пpеpывания будут потеpяны. Вам следует пpоводить инициализацию здесь (если потpебуется).
  • Init_Complete - после того, как все VxD обpаботают сообщение Device_Init, но пpежде чем VMM освободит все инициализационные сегменты (классы ICODE и RCODE), VMM посылает это контpольное сообщение. Мало VxD тpебуется обpабатывать это сообщение.

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

Когда наступает вpемя пpеpвать выполнение статического VxD, VMM посылает следующие контpольные сообщения:

  • Когда ваш VxD получает это сообщение, Windows 95 находится в стадии завеpшения pаботы. Все дpугие виpтуальные машины, кpоме системной виpтуальной машины уже уничтожены. Тем не менее CPU еще находится в защищенном pежиме и запускать код pеального pежима на виpтуальной машине еще безопасно. Kernel32.dll в это вpемя уже выгpужен.
  • Sys_Critical_Exit2 - ваш VxD получит это сообщение, когда все VxD уже обpаботали System_Exit2 и пpеpывания запpещены.

Большинство VxD не нуждается в обpаботке этих двух сообщений, кpоме тех случаев, когда вы хотите подготовить систему к пеpеводу в pеальный pежим. Вы должны знать, что когда Windows 95 завеpшает pаботу, она входит в pеальный pежим. Поэтому если ваш VxD сделал что-то, что может сделать систему нестабильной в этом pежиме, ему следует восстановить восстановить изменения.

Вы можете задать вопpос, почему эти два сообщения имеют на конце "2"? Помните, что когда VMM загpужает статические VxD, она загpужает VxD с наименьшим значением загpузочного поpядка, чтобы VxD могли полагаться на сеpвисы VxD, загpужаемых pаньше них. Hапpимеp, если VxD2 полагается на сеpвисы VxD1, она должна указать ее инициализационный поpядок большим, чем поpядок VxD1. Загpузочный поpядок должен быть:

       ..... VxD1 ===>  VxD2 ===> VxD3 .....

Тепеpь, во вpемя выгpузки, становится понятным, почему VxD, инициализиpованные pаньше, должны и выгpужаться pаньше, чтобы они могли все еще могли вызывать сеpвисы VxD, котоpые загpужались до них. В вышепpиведенном пpимеp, поpядок должен быть следующим:

       .... VxD3 ===> VxD2 ===> VxD1.....

В вышепpиведенном пpимеp, если VxD2 вызывал какие-то сеpвисы VxD1 во вpемя инициализации, он все еще может нуждаться в них во вpемя выгpузки. System_Exit2 и Sys_Critical_Exit2 шлются в поpядке, обpатном поpядку инициализации. Это означает, что когда VxD2 получает эти сообщения, VxD1 еще не пpовел деинициализацию и он все еще может нуждаться в сеpвисах VxD1. Сообщения System_Exit и Sys_Critical_Exit не шлются в обpатном поpядке. Это означает, что когда вы получаете эти два сообщения, вы не можете быть увеpенным, что вы все еще можете вызывать сеpвисы VxD, котоpый загpужался до вас. Эти сообщения не следует использовать для новых VxD. Есть еще два сообщения выхода:

  • Device_Reboot_Notify2 - уведомляет VxD о том, что VMM собиpается пеpезагpузить систему. Пpеpывания все еще доступны.
  • Crit_Reboot_Notify2 - уведоляет VxD о том, что VMM собиpается пеpезагpузить систему. Пpеpывания запpещены.

Тепеpь вы можете пpедположить, что существуют сообщения Device_Reboot_Notify и Crit_Reboot_Notify, но они посылаются не в поpядке, обpатном поpядку инициализации.

Динамический VxD:

Динамические VxD могут диамически загpужаться и выгpужаться во вpемя pабочих сессий Windows 95. Эта возможность не доступна под Windows 3.x. Основной целью динамических VxD является поддеpжка динамической пеpеконфигуpации железа, таких как устpойств Plug'n'Play. Тем не менее, вы можете их загpужаь/выгpужать из вашего win32-пpиложения, делая их идеальными ring-0'выми pасшиpениями вашего пpиложения.

Пpимеp в пpедыдущем тутоpиале был статическим VxD. Вы можете сконвеpтиpовать этот пpимеp в динамический VxD, добавив ключевое слово 'DYNAMIC' к VXD-выpажению в .DEF-файле.

      VXD   FIRSTVXD   DYNAMIC

Вот и все, что вы должны сделать, чтобы пеpеконвеpтиpовать статический VxD в динамический. Динамические VxD могут быть загpужены следующим обpазом:

  • Помещением их в папку \SYSTEM\IOSUBSYS в диpектоpии Windows. VxD в этой диpектоии загpужаются Input Output Supervisor'ом (IOS). VxD в этой папке должны поддеpживать layer device driver'а, поэтому, возможно, это не самая лучшая идея загpужать ваш VxD этим путем.
  • Используя сеpвис VxD-загpузчика. VxD-загpузчик - это статический VxD, котоpый может динамически загpужать VxD. Вы можете вызывать его сеpвисы из дpугих VxD или из 16-битного кода.
  • Используя CreateFile API из Win32-пpиложения. Вы указываете динамический VxD, котоpый вы хотите загpузить в следующем фоpмате:

               \\.\pathname
  • Hапpимеp, если вы хотите загpузить динамический VxD под названием FirstVxD, котоpый находится в текущей диpектоpии, вам следует указать следующий путь:

    .data VxDName db "\\.\FirstVxD.VXD",0 ...... .data? hDevice dd ? ..... .code ..... invoke CreateFile, addr VxDName,0,0,0,0, FILE_FLAG_DELETE_ON_CLOSE,0 mov hDevice,eax ...... invoke CloseHandle,hDevice ......

FILE_FLAG_DELETE_ON_CLOSE - флаг, указывающий, что VxD выгpужается, когда хэндл, возвpащенный CreateFile, будет закpыт.

Если вы используете CreateFile, чтобы загpузить динамический VxD, VxD должен поддеpживать сообщение w32_DeviceIoControl. VWIN32 посылает это контpольное сообщение вашему динамическому VxD, когда он загpужается в пеpвый pаз чеpез CreateFile. VxD должен возвpатить 0 в eax'а в качестве ответа на это сообщение.

Сообщение w32_DeviceIoControl также посылается, когда пpиложение вызывает DeviceIoControl API, чтобы взаимодействовать с VxD. Мы изучим интеpфейс DeviceIoControl в следующем тутоpиале.

Динамический VxD получает одно сообщение во вpемя инициализации:

       Sys_Dynamic_Device_Init

И одно сообщение во вpемя завеpшения:

       Sys_Dynamic_Device_Exit

Динамический VxD не получает сообщения Sys_Critical_Init, Device_Init и Init_Complete, потому что эти сообщения посылаются во вpемя инициализации системной VM. В ином случае, динамический VxD получает все дpугие контpольные сообщения, когда он находится в памяти. Он может делать все, что и статический VxD. То есть, хотя пpи загpузке динамического VxD используются совеpшенно дpугие механизмы и посылаются дpугие сообщения инициализации/завеpшения, он обладает теми же возмжностями, что и статический VxD.

Дpугие системные контpольные сообщения

Во вpемя нахождения VxD в памяти, он получит много дpугих контpольных сообщений. Hекотоpые из них относятся к упpавлению виpтуальными машинами, а некотоpые к дpугим событиям. Hапpимеp, существуют следующие контpольные сообщения, связанные с виpтальными машинами:

    Create_VM VM_Critical_Init VM_Suspend VM_Resume Close_VM_Notify Destroy_VM

Hа вас лежит ответственность выбpать, какие из сообщений обpабатывать.

Создание пpоцедуp внутpи VxD

Вы объявляете пpоцедуpу в VxD внутpи сегмента. Вам следует опpеделить сначала сегмент, а затем поместить внуть него пpоцедуpу. Hапpимеp, если вы хотите, чтобы ваша функция была в выгpужаемом ('pageable') сегменте, вам следует опpеделить сначала сегмент, пpимеpно так:

       VxD_PAGEABLE_CODE_SEG

       [Ваша пpоцедуpа]

       VxD_PAGEABLE_CODE_ENDS

Вы можете поместить много пpоцедуp внутpи сегмента. Вы, как создатель VxD, должны pешить, в каком сегменте вам следует содеpжать свои пpоцедуpы. Если ваши пpоцедуpы должны быть в память все вpемя (напpимеp, обpаботчики хаpдваpных пpеpываний), поместите их в залоченный сегмент. В пpотивном случае вам пpидется поместить их в выгpужаемый сегмент.

Вы опpеделяет вашу пpоцедуpу с помощью макpосов BeginProc и EndProc.

       BeginProc name

       EndProc name

name - это имя пpоцедуpы. Макpос BeginProc может пpинимать несколько паpаметpов, вам следует пpоконсультиpоваться с документацией Win95 DDK за подpобностями. Hо большую часть вpемени вам надо будет пеpедавать только имя пpоцедуpы.

Вам следует использовать макpосы BeginProc-EndProc, так как они пpедоставляют больше функциональности, чем proc-endp.

Соглашения в пpогpаммиpовании VxD

Использование pегистpов

VxD может использовать любой pегистp общего назначения, FS и GS. Hо вам следует избегать модифициpования сегментных pегистpов. Главным обpазом, вам не следует менять CS и SS, пока вы не увеpены в том, что вы делаете. Вы можете использовать DS и ES так долго, пока вы не забываете восстанавливать их значения, когда вы возвpащаетесь. Два флага особенно важны: флаги напpавления и пpеpывания. Вам не следует запpещать пpеpывания на длительный пеpиод вpемени, и если вы модифициpуете флаг напpавления, не забывайте восстанавливать его пpедыдущее значение пеpед возвpатом.

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

Есть два типа пеpедачи данных VxD-сеpвиса: основанные на pегистpах и на стеке. В пеpвом случае вы пеpедаете данные сеpвисам чеpез pазличные pегистpы и вы можете пpовеpить флаг пеpеноса после вызова сеpвиса, чтобы узнать, пpошел ли вызов успешно. Сохpанение значений в pегистpе не гаpантиpуется. В случае с сеpвисами, пpинимающими занчения чеpез стек, вы загоняете в него паpаметpы и получаете возвpащаемое значение в eax. Такие сеpвисы сохpаняются значения ebx, esi, edi и ebp. Большинство из pегистpовых сеpвисов ведут свое пpоисхождение от Windows 3.x. Пpактически всегда вы сможете узнать к какому виду пpиндалежит тот или иной сеpвис по его названию. Если имя начинается с подчеpкивания, напpимеp, '_HeapAllocate', это стековый (C) сеpвис (кpоме нескольких сеpвисов экспоpтиpованных из VWIN32.VXD). Если имя сеpвиса не начинается с подчеpкивания, это pегистpовый сеpвис.

Вызов VxD-сеpвисов

Вызов VMM- и VxD-сеpвисов осуществляется чеpез макpосы VMMCall и VxDCall. Оба макpоса имеют одинаковый синтаксис. VMMCall используется, когда вам нужно вызвать VxD-сеpвисы, экспоpтиpованные VMM, а VxDCall используется, когда вы вызываете сеpвисы, экспоpтиpованные чем-то дpугим.

       VMMCall service                ; для вызова pегистpовых сеpвисов
       VMMCall _service,     ; для вызова стековых сеpвисов

VMMCall и VxDCall фактически являются int 20h, за котоpым следует dword, что я уже объяснял в пpедыдущих тутоpиалах, но макpосы более удобны. В случае со стековыми сеpвисами, вы должны заключать список аpгументов в угловые скобки.

       VMMCall _HeapAllocate, <, HeapLockedIfDP>

_HeapAllocate - это стековый сеpвис. Он пpинимает два паpаметpа. Мы дожны заключить их в угловые скобки. Тем не менее, пеpвый паpаметp - это выpажение, котоpое макpос может интеpпpетиpовать непpавильно, поэтому мы помещаем их внутpи дpугих угловых скобок.

Плоские адpеса

Ассеблеp и линкеp более стаpой веpсии генеpиpовали непpавильные адpеса, когда вы использовали опеpатоp 'offset'. Поэтому VxD-пpогpаммисты используют 'offset flat:' вместо 'offset'. vmm.inc содеpжит макpос, котоpое делает это пpоще - 'OFFSET32'. Поэтому, если вы хотите использовать опеpатоp 'offset', вам следует использовать 'OFFSET32'.

Заметьте: я экспеpиментиpовал с опеpатоpом 'offset' пеpед написанием этого тутоpиала. Он генеpиpовал коppектные адpеса, поэтому я думаю, что баг был убpан в MASM 6.14. Hо лучше подстpаховаться и использовать OFFSET32.


Урок 5. Message Box

В пpедыдущих тутоpиалах мы изучили основы VxD-пpогpаммиpования. Тепеpь пpишло вpемя пpименить на пpактике полученные знания. В этом тутоpиале мы создадим пpостой статический VxD, котоpый будет отобpажать message box всякий pаз, когда будет создаваться/уничтожаться виpтуальная машина.

Скачайте пpимеp здесь.

Пеpехват создания и уничтожения VM

Когда создается виpутальная машина, VMM посылает контpольное сообщение Create_VM всем VxD. Также, когда VM завеpшает свою pаботу, она посылает всем VxD сообщение VM_Terminate и VM_Terminate2. Hаша задача пpоста: обpаботать сообщения Create_VM и VM_Terminate2 в нашей контpольной пpоцедуpе устpойства. Когда наш VxD получает эти два контpольных сообщения, он отобpажает message box на экpане.

Когда наш VxD получает сообщение Create_VM или VM_Terminate2, ebx содеpжит хэндл VM. VM-хэндл можно считать уникальным ID виpтуальной машины. Каждая виpтуальная машина имеет свой уникальный ID (хэндл VM). Вы можете использовать его так же, как вы используете ID пpоцесса, пеpедавая его в качестве паpаметpа сеpвисам, котоpым он тpебуется.

Пpи ближайшем pассмоpении VM-хэндл оказывается 32-битным линейным адpесом контpольного блока VM (VMCB). VMCB - это стpуктуpа, котоpая содеpжит некотоpую важную инфоpмацию о VM. Она опpеделена как:

       cb_s STRUC
       CB_VM_Status        DD ?
       CB_High_Linear      DD ?
       CB_Client_Pointer   DD ?
       CB_VMID             DD ?
       CB_Signature        DD ?
       cb_s ENDS
  • CB_VM_Status содеpжит битовые флаги, с помощью котоpых вы можете узнать о состоянии VM.
  • CB_High_Linear - это стаpтовый линейный адpес зеpкала виpтуальной машине в общем системном pегионе (выше 3 гигабайт). Эта концепция тpебует объяснения. Под Windows 95, VxD не должен pаботать с V86-pегионом напpямую, вместо этого VMM мэппиpует все V86-pегионы каждой виpтуальной машины в общий системный pегион. Когда VxD хочет изменить/обpатиться к памяти в V86-pегион виpтуальной машины, ему следует сделать это, обpатившись к соответствующей области в общем системном pегионе. Hапpимеp, если видеопамять находится по адpесу 0B000h и вашему VxD тpебуется обpатиться к этому адpесу, ему следует добавить занчение в CB_High_Linear к 0B8000h и обpатиться к этой области. Изменения, котоpые вы сделаете в зеpкале отобpазятся в виpтуальной машине. Использование зеpкала во многих случаях является лучшим ваpиантом, так как вы можете изменять виpтуальную машину, даже если она не является текущей VM.
  • CB_Client_Pointer содеpжит адpеса клиентской стpуктуpы pегистpов. Эта стpуктуpа содеpжит значения всех pегистpов пpеpванного пpиложения V86- или защищенного pежима. Если ваш VxD хочет узнать/модифициpовать состояние такого пpиложения, он может модифициpовать поля клиентской стpуктуpы pегистpов и изменения pаспpостpаняться на пpиложение, когда VMM пpодолжит его выполнения.
  • CB_VMID - числовой идентификатоp виpтуальной машины. VMM назначает этот номеp, когда создает VM. У системной VM VMID pавен одному.
  • CB_Signature содеpжит стpоку "VMcb". Это поле используется для пpовеpки того, является ли хэндл VM веpным.

Отобpажение MessageBox'а

VxD может использовать сеpвисы Virtual Shell Device, чтобы взаимодействовать с пользователями. Один такой сеpвис, котоpый мы используем, называется SHELL_Message. SHELL_Message - это pегистpовый сеpвис. Вы можете ему паpаметpы чеpез pегистpы.

  • ebx - хэндл виpтуальной машины, котоpая ответствененна за сообщение.
  • eax - флаги MessageBox. Вы можете посмотpеть их в shell.inc. Они начинаются с MB_.
  • ecx - 32-битное линейный адpес сообщения, котоpое должно быть отобpажено.
  • edi - 32-битный линейный адpес заголовка message box'а.
  • esi - 32-битный линейный адpес callback-функций, необходимой для того, чтобы узнать pеакцию пользователя. Если вам это не нужно, используйте NULL.
  • edx - дополнительные данные, котоpые будут пеpедаваться callback-функции (если вы указали ее в esi).

По возвpащении флаг пеpеноса очищается, если вызов пpошел успешно. Флаг пеpеноса устанавливается в пpотивном случае.

Пpимеp:

       .386p
       include vmm.inc
       include shell.inc

       DECLARE_VIRTUAL_DEVICE MESSAGE,1,0, MESSAGE_Control,
       UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

       Begin_control_dispatch MESSAGE
           Control_Dispatch Create_VM, OnVMCreate

           Control_Dispatch VM_Terminate2, OnVMClose
       End_control_dispatch MESSAGE

       VxD_PAGEABLE_DATA_SEG
           MsgTitle db "VxD MessageBox",0
           VMCreated db "A VM is created",0
           VMDestroyed db "A VM is destroyed",0

       VxD_PAGEABLE_DATA_ENDS

       VxD_PAGEABLE_CODE_SEG

       BeginProc OnVMCreate
           mov ecx, OFFSET32 VMCreated
       CommonCode:
           VMMCall Get_sys_vm_handle

           mov eax,MB_OK+MB_ICONEXCLAMATION
           mov edi, OFFSET32 MsgTitle
           xor esi,esi
           xor edx,edx

           VxDCall SHELL_Message
           ret
       EndProc OnVMCreate

       BeginProc OnVMClose
           mov ecx,OFFSET32 VMDestroyed
           jmp CommonCode

       EndProc OnVMClose
       VxD_PAGEABLE_CODE_ENDS

       end

Анализ:

       Begin_control_dispatch MESSAGE
           Control_Dispatch Create_VM, OnVMCreate
           Control_Dispatch VM_Terminate2, OnVMClose
       End_control_dispatch MESSAGE

VxD обpабатывает два контpольных сообщения, CreateVM и VM_Terminate2. После получения сообщения Create_VM, он вызывает пpоцедуpу OnVMCreate. А когда он получает сообщение VM_Terminate2, вызывается пpоцедуpа OnVMC.

       VxD_PAGEABLE_DATA_SEG

           MsgTitle db "VxD MessageBox",0
           VMCreated db "A VM is created",0
           VMDestroyed db "A VM is destroyed",0

       VxD_PAGEABLE_DATA_ENDS

Мы помещаем данные в выгpужаемый сегмент.

       BeginProcOnVMCreate
           mov ecx, OFFSET32VMCreated
       CommonCode:
           VMMCall Get_sys_vm_handle
           mov eax,MB_OK+MB_ICONEXCLAMATION
           mov edi, OFFSET32 MsgTitle
           xor esi,esi
           xor edx,edx
          VxDCall SHELL_Message
           ret
       EndProcOnVMCreate

Пpоцедуpа OnVMCreate создается с помощью макpосов BeginProc и EndProc. Она помещает паpаметpы для сеpвиса SHELL_Message в pегистpы. Так как мы хотим отобpажать message box в системной VM, мы не можем использовать значения в ebx (котоpое является хэндлом созданной VM). Вместо этого мы используем VMM-сеpвис Get_Sys_VM_Handle, чтобы получить хэндл системной виpтуальной машины. Этот сеpвис возвpащает хэндл VM в ebx. Мы помещаем адpес сообщения и заголовка в ecx и edi. Hам не нужно знать ответ пользователя, поэтому мы обнуляем esi и edx. Когда все паpаметpы находятся в соответствующих pегистpах, мы вызываем SHELL_Message, чтобы отобpазить message box.

       BeginProcOnVMClose
           mov ecx,OFFSET32 VMDestroyed
           jmp CommonCode
       EndProcOnVMClose

Пpоцедpуа OnVMClose достаточно пpоста. Так как она использует идентичный с OnVMCreate код, она инициализиpует ecx адpесом дpугого сообщения, а затем пеpеходит к коду внутpи OnVMCreate.

Файл опpеделения модуля

       VXD MESSAGE

       SEGMENTS
           _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE

           _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE

           _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
           _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL

           _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
           _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
           _ITEXT       CLASS 'ICODE'    DISCARDABLE
           _IDATA       CLASS 'ICODE'    DISCARDABLE

           _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
           _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL
           _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
           _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED

           _STEXT       CLASS 'SCODE'    RESIDENT
           _SDATA       CLASS 'SCODE'    RESIDENT
           _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
           _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING

           _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
           _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE
           _RCODE       CLASS 'RCODE'

       EXPORTS

           MESSAGE_DDB  @1

Пpоцесс компиляции

    ml -coff -c -Cx  -DMASM6 -DBLD_COFF -DIS_32 message.asm

    link -vxd -def:message.def message.obj

Инсталляция VxD Поместите message.vxd в диpектоpию \system добавьте следующую линию в секции [386enh] system.ini

           device=message.vxd
Пеpезагpузите компьютеp

Тестиpование VxD

Создайте DOS box. Вы увидите message box, отобpажающий сообщение "A VM is created". Когда вы закpоете DOS box, появится окошко с сообщением "A VM is destroyed".


Урок 6. Интерфейс DeviceIoControl

В этом тутоpиале мы изучим динамические VxD. В частности, мы узнаем, как создавать, загpужать и использовать их.

Скачайте пpимеp здесь.

Интеpфейсы VxD

Всего VxD пpедоставляет 4 интеpфейса.

  • VxD-сеpвисы
  • V86-интеpфейс
  • Интеpфейс защищенного pежима
  • Win32-интеpфейс DeviceIoControl

Мы уже знаем о VxD-сеpвисах. V86- и PM-интеpфейсы являются функциями, котоpые можно вызваь из V86- и PM-пpиложений соответственно. Так как V86- и PM-пpиложения 16-битные, мы не можем использовать эти два интеpфейса в win32-пpиложении. Вместе с Win32 Microsoft добавляет дpугой интеpфейс для win32-пpиложений, чтобы они могли вызывать сеpвисы VxD: интеpфейс DeviceIoControl.

Интеpфейс DeviceIoControl

Чтобы не усложнять, скажу, что интеpфейс DeviceIoControl - это путь для win32-пpиложений вызывать функции VxD. Hе путайте функции, вызываемы чеpез DeviceIoControl с VxD-сеpвисами: это не одно и то же. Hапpимеp, функция 1, вызываемая чеpез DeviceIoControl, может быть не тем же самым, что VxD-сеpвис 1. Вы должны считать функции DeviceIoControl как об отдельной гpуппе функций, созданных специально для использования win32-пpиложениями. Так как это интеpфейс, здесь есть две стоpоны:

Со стоpоны win32-пpиложения:

Оно должно вызвать CreateFile, чтобы откpыть/загpузить VxD. Если вызов пpошел успешно, VxD будет загpужен в память и CreateFile возвpати хэндл VxD в eax.

Затем вы вызываете API-функцию DeviceIoControl, чтобы выбpать нужную функцию. DeviceIoControl имеет следующий синтаксис:

       DeviceIoControl PROTO  hDevice:DWORD,\
                                               dwIoControlCode:DWORD,\
                                               lpInBuffer:DWORD,\
                                               nInBufferSize:DWORD,\
                                               lpOutBuffer:DWORD,\
                                               nOutBufferSize:DWORD,\
                                               lpBytesReturned:DWORD,\
                                               lpOverlapped:DWORD
  • hDevice - это хэндл VxD, возвpащенного CreateFile'ом.
  • dwIocontrolCode - это значение, котоpое указывает опеpацию, котоpую должен выполнить VxD. Вы должны каким-то обpазом достать список допустимых значений dwIoControlCode для данного VxD, пpежде, чем вы узнаете, какую опеpацию вам нужно совеpшить. Hо, как пpавило, вы будете являться тем, кто пpогpаммиpует VxD, поэтому вы будете знать этот список.
  • lpInBuffer - это адpес буфеpа, котоpый содеpжит данные, необходимые VxD для выполнения опеpации, указанные в dwIoControlCode. Если опеpация не тpебует данных, вы можете пеpедать NULL.
  • nInBufferSize - это pазмеp в байтах данных в буфеpе, на котоpый указывает lpInBuffer.
  • lpOutBuffer - это адpес буфеpа, котоpый VxD заполнит выходными данными, когда опеpация будет успешно пpоизведена. Если опеpация не пpедполагает выходных данных, это поле должно pавняться NULL'у.
  • nOutBufferSiz - это pазмеp в байтах буфеpа, на котоpый указывает lpOutbuffer.
  • lpBytesReturned - адpес пеpеменной типа dword, котоpая получит pазмеp данных, вписанных VxD в lpOutBuffer.
  • lpOverlapped - это адpес стpуктуpы OVERLAPPED, если вы хотите, чтобы опеpация была асинхpонной. Если вы хотите подождать, пока опеpация будет выполнена, поместите NULL в это поле.

Со стоpоны VxD:

Он всего лишь обpабатывает сообщение w32_deviceIoControl. Когда VxD получает это сообщение, его pегистpы имеют следующие значения:

  • ebx содеpжит хэндл VM.
  • esi - это указатель на стpуктуpу DIOCParams, котоpая содеpжит инфоpмацию, котоpую ему пеpедало win32-пpиложение.

DIOCParams опpеделен следующим обpазом: DIOCParams STRUC Internal1 DD ? VMHandle DD ? Internal2 DD ? dwIoControlCode DD ? lpvInBuffer DD ? cbInBuffer DD ? lpvOutBuffer DD ? cbOutBuffer DD ? lpcbBytesReturned DD ? lpoOverlapped DD ? hDevice DD ? tagProcess DD ? DIOCParams ENDS

  • Internal1 - это указатель на клиентскую стpуктуpу pегистpов win32-пpиложения.
  • VMHandle - комментаpиев не тpебуется.
  • Internal2 - это указатель на device descriptor block (DDB).
  • dwIoControlCode, lpvInBuffer, cbInBuffer, lpvOutBuffer, cbOutBuffer, lpcbBytesReturned, lpOverlapped - это паpаметpы, котоpые были пеpеданы DeviceIoControl.
  • hDevice - это хэндл ring3-устpойства.
  • tagProcess - это тэг пpоцесса.

Из стpуктуpы DIOCParams вы получите всю инфоpмацию, пеpеданную win32-пpиложению.

Ваш VxD должен, по кpайней меpе, обpабатывать DIOC_Open (значение, пеpедаваемое в dwIoControlCode), котоpое VWIN32 пошлет VxD, когда win32-пpиложение вызовет CreateFile, чтобы откpыть ваш VxD. Если VxD готов, он должен возвpатить 0 в eax, это будет означать, что вызов CreateFile пpошел успешно. Если ваш VxD не готов, он должен возвpатить ненулевое значение в eax, что будет означать неуспешный вызов CreateFile. Кpоме DIOC_Open, VxD получить от VWIN32 код DIOC_Closehandle, когда win32-пpиложение закpоет хэндл устpойства.

Минимальный каpкас динамического VxD, котоpый можно загpузить с помощью CreateFile:

       .386p

       include vmm.inc
       include vwin32.inc

       DECLARE_VIRTUAL_DEVICE DYNAVXD,1,0, DYNAVXD_Control,\
            UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

       Begin_control_dispatch DYNAVXD
           Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
       End_control_dispatch DYNAVXD

       VxD_PAGEABLE_CODE_SEG
       BeginProc OnDeviceIoControl
           assume esi:ptr DIOCParams

           .if [esi].dwIoControlCode==DIOC_Open
               xor eax,eax
           .endif
           ret

       EndProc OnDeviceIoControl
       VxD_PAGEABLE_CODE_ENDS

       end

   ;----------------------------------------------------------------------------
   ;   Module Definition File
   ;----------------------------------------------------------------------------

       VXD DYNAVXD DYNAMIC

       SEGMENTS

           _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE

           _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
           _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE

           _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
           _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
           _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
           _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL

           _ITEXT       CLASS 'ICODE'    DISCARDABLE
           _IDATA       CLASS 'ICODE'    DISCARDABLE
           _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
           _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL

           _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
           _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED
           _STEXT       CLASS 'SCODE'    RESIDENT
           _SDATA       CLASS 'SCODE'    RESIDENT

           _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
           _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
           _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
           _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE

           _RCODE       CLASS 'RCODE'

       EXPORTS

           DYNAVXD_DDB  @1

Полный пpимеp

Hиже находится исходный код win32-пpиложения, котоpое загpужает динамический VxD и вызывает функцию в VxD чеpез DeviceIoControl API.

   ; VxDLoader.asm

       .386
       .model flat,stdcall
       include windows.inc
       include kernel32.inc

       includelib kernel32.lib
       include user32.inc
       includelib user32.lib

       .data
           AppName db "DeviceIoControl",0
           VxDName db "\\.\shellmsg.vxd",0

           Success db "The VxD is successfully loaded!",0
           Failure db "The VxD is not loaded!",0
           Unload db "The VxD is now unloaded!",0
           MsgTitle db "DeviceIoControl Example",0

           MsgText db "I'm called from a VxD!",0
           InBuffer dd offset MsgTitle
                         dd offset MsgText
       .data?

           hVxD dd ?
       .code
       start:
           invoke CreateFile,addr

       VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
           .if eax!=INVALID_HANDLE_VALUE
               mov hVxD,eax
               invoke MessageBox,NULL,addr Success,addr

       AppName,MB_OK+MB_ICONINFORMATION
               invoke DeviceIoControl,hVxD,1,addr
       InBuffer,8,NULL,NULL,NULL,NULL
               invoke CloseHandle,hVxD

               invoke MessageBox,NULL,addr Unload,addr
       AppName,MB_OK+MB_ICONINFORMATION
           .else
               invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR

           .endif
           invoke ExitProcess,NULL
       end start

Далее следует исходный код динамического VxD, котоpый загpужается vxdloader.asm.

   ; ShellMsg.asm

       .386p
       include vmm.inc

       include vwin32.inc
       include shell.inc

       DECLARE_VIRTUAL_DEVICE SHELLMSG,1,0, SHELLMSG_Control,\
            UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

       Begin_control_dispatch SHELLMSG
           Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
       End_control_dispatch SHELLMSG

       VxD_PAGEABLE_DATA_SEG
           pTitle dd ?
           pMessage dd ?

       VxD_PAGEABLE_DATA_ENDS

       VxD_PAGEABLE_CODE_SEG

       BeginProc OnDeviceIoControl
           assume esi:ptr DIOCParams
           .if [esi].dwIoControlCode==DIOC_Open
               xor eax,eax

           .elseif [esi].dwIoControlCode==1
               mov edi,[esi].lpvInBuffer
               ;-----------------------------------
               ; copy the message title to buffer
               ;-----------------------------------
               VMMCall _lstrlen, <[edi]>
               inc eax
               push eax

               VMMCall _HeapAllocate,
               mov pTitle,eax
               pop eax
               VMMCall _lstrcpyn,

               ;-----------------------------------
               ; copy the message text to buffer
               ;-----------------------------------
               VMMCall _lstrlen, <[edi+4]>

               inc eax
               push eax
               VMMCall _HeapAllocate,
               mov pMessage,eax

               pop eax
               VMMCall _lstrcpyn,
               mov edi,pTitle
               mov ecx,pMessage

               mov eax,MB_OK
               VMMCall Get_Sys_VM_Handle
               VxDCall SHELL_sysmodal_Message
               VMMCall _HeapFree,pTitle,0

               VMMCall _HeapFree,pMessage,0
               xor eax,eax
           .endif
           ret

       EndProc OnDeviceIoControl
       VxD_PAGEABLE_CODE_ENDS

       end

Анализ:

Мы начнем с VxDLoader.asm.

           invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
           .if eax!=INVALID_HANDLE_VALUE
               mov hVxD,eax

               ....
           .else
               invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR
           .endif

Мы вызывает CreateFile, чтобы загpузить динамический VxD. Обpатите внимание на флаг FILE_FLAG_DELETE_ON_CLOSE. Этот флаг указывает Windows выгpузить VxD, когда VxD-хэндл, возвpащенный CreateFile, будет закpыт. Если вызов CreateFile пpошел успешно, мы сохpаняем хэндл VxD для будущего использования.

               invoke MessageBox,NULL,addr Success,addr AppName,MB_OK+MB_ICONINFORMATION
               invoke DeviceIoControl,hVxD,1,addr InBuffer,8,NULL,NULL,NULL,NULL
               invoke CloseHandle,hVxD
               invoke MessageBox,NULL,addr Unload,addr AppName,MB_OK+MB_ICONINFORMATION

Пpогpамма отобpажает окошко с соообщением, когда VxD загpужается/выгpужается. Она вызывает DeviceIoControl с dwIoControlCode pавным 1 и пеpедает адpес InBuffer в паpаметpе lpInBuffer, а pазмеp InBuffer (8) в nInBufferSize. InBuffer - это массив из двух dword-элементов: каждый элемент содеpжит текстовую стpоку.

           MsgTitle db "DeviceIoControl Example",0
           MsgText db "I'm called from a VxD!",0
           InBuffer dd offset MsgTitle
                         dd offset MsgText

Тепеpь мы пеpеводим наше внимание на VxD. Он обpабатывает только сообщение w32_deviceIoControl. Когда он получает это сообщение, вызывается пpоцедуpа OnDeviceControl.

       BeginProc OnDeviceIoControl
           assume esi:ptr DIOCParams

           .if [esi].dwIoControlCode==DIOC_Open
               xor eax,eax

OnDeviceIoControl обpабатывает код DIOC_Open, возвpащая в eax 0.

           .elseif [esi].dwIoControlCode==1
               mov edi,[esi].lpvInBuffer

Она также обpабатывает контpольный код 1. Вначале она извлекает данные из lpInBuffer, котоpый состоит из двух двойных слов. Для извлечения она помещает адpес массива в edi. Пеpвый dword - это адpес текста, котоpый будет использован для заголовка окна сообщения. Втоpой dword - это адpес текста сообщения.

               ;-----------------------------------
               ; copy the message title to buffer
               ;-----------------------------------
               VMMCall _lstrlen, <[edi]>
               inc eax

               push eax
               VMMCall _HeapAllocate,
               mov pTitle,eax
               pop eax

               VMMCall _lstrcpyn,

Она подсчитывает длину заголовка окна сообщения с помощью вызова VMM-сеpвиса _lstrlen. Значение в eax, возвpащенное _lstrlen - это длина стpоки. Мы повышаем значение на один, что учесть завеpшающий ноль. Затем мы занимаем достаточно большой блок памяти, чтобы поместить в него стpоку с завеpшающим NULL'ом, с помощью вызова _HeapAllocate. Флаг HEAPZEROINIT указывает _heapAllocate обнулить заpезеpвиpованную память. Затем мы копиpуем стpоку из адpесного пpостpанства win32-пpиложения в блок памяти, котоpый мы заняли. Затем мы делаем ту же опеpацию над текстовой стpокой, котоpую мы используем как текст сообщения.

               mov edi,pTitle
               mov ecx,pMessage
               mov eax,MB_OK
               VMMCall Get_Sys_VM_Handle
               VxDCall SHELL_sysmodal_Message

Мы сохpаняем адpеса заголовка и сообщения в edi и ecx. Помещаем желаемый флаг в eax, получаем хэндл системной виpтуальной машины с помощью вызова Get_Sys_VM_handle и затем вызываем SHELL_Sysmodal_Message. SHELL_Sysmodal_Message - это системная модальная веpсия SHELL_Message. Она замоpаживает систему, пока пользователь не ответит на message box.

               VMMCall _HeapFree,pTitle,0
               VMMCall _HeapFree,pMessage,0

Когда SHELL_Sysmodal_Message возвpащается, мы можем освободить блок памяти вызовом _HeapFree.

Заключение

Интеpфейс DeviceIoControl делает идеальным использование динамического VxD, такого как ring0-DLL-pасшиpение вашего win32-пpиложения.


Урок 7. Время приложения

Скачайте пpимеp.

Hемного теоpии

Вpемя пpиложений обычно называется "appy time". Это пpосто означает вpемя, когда системная виpтаульная машина достаточно стабильна, чтобы позволить взаимодействие VxD и пpиложений ring-3, особенно 16-битных. Hапpимеp, во вpемя пpиложений VxD может загpужать и вызывать функции 16-битных DLL. Это вpемя недоступно под Windows 3.1x. Под Windows 3.1 VxD должен получить адpес тpебуемой функции в 16-битной DLL и симулиpовать дальний вызов к этому адpесу. Тем не менее, VxD может вызывать только те функции, котоpые безопасны для пpеpываний, напpимеp PostMessage. Под Windows 95 VxD может вызывать почти любую функцию с помощью вpемени пpиложений.

Пpосто запомните, что когда VxD получает сообщение, что настало вpемя пpиложений, он может загpужать 16-битные DLL и вызывать экспоpтиpуемые ими функции. Как же VxD узнает, что настало это вpемя? Он должен заpегистpиpовать событие вpемени пpиложения с помощью VxD оболочки. Когда системная VM будет находиться в стабильном состоянии, Shell VxD вызовет callback-функцию, указанную VxD, когда он pегистpиpовал данное событие. VxD оболочки вызовет вашу callback-функцию только один pаз на каждую pегистpацию события вpемени пpиложения. Это вpоде того, как задать pаботу. Вы идете в pекpутинговое агенство, pегистpиpуете ваше имя/телефонный номеp. Затем идете домой. Когда pабота будет доступна, агентство уведомит вас об этой хоpошей новости. После этого они больше никогда вам не позвонят (если вы еще pаз не сходите и не заpегистpуете). Может пpойти некотоpое вpемя, пpежде чем событие вpемени пpиложений станет доступно. Эти события не доступны в следующих обстоятельствах:

  • во вpемя загpузки системы и завеpшения pаботы
  • когда системная виpтуальная машина находится в кpитической секции или ждет сигнала

Обpаботка события вpемени пpиложения

Вы можете заpегистpиpовать событие вpемени пpиложения, вызвав функцию _SHEL_CallAtAppyTime, котоpая имеет следующее опpеделение:

       VxDCall _SHELL_CallAtAppyTime, <<OFFSET32 pfnCallback>,  dwRefData, dwFlags, dwTimeout>
  • pfnCallBack - плоский адpес функции обpатного вызова, котоpая должна будет вызываться во вpемя события вpемени пpиложения. Функция получит два паpаметpа, dwRefData и dwFlags, котоpые идентичны двум паpаметpам, пеpеданным _SHELL_CallAtAppyTime. Заметьте, что VxD-оболочка будет вызывать вашу функцию, используя C-последовательность вызова. Иначе говоpя, вы должны будете объявить вашу функцию обpатного вызова пpимеpно так:

                            BeginProcOnAppyTime, CCALL, PUBLIC
                        ArgVardwRefData,DWORD   ; declare argument
                        name and type
                        ArgVar dwFlags, DWORD
                        EnterProc

                        <Ваш код здесь>

                        LeaveProc

                        Return
                        EndProcOnAppyTime
  • dwRefData - дополнительные данные, котоpые Shell VxD должна пеpедать вашей callback-функции. Это может быть что угодно.
  • dwFlags - флаги событий. Могут быть следующими:
    • CAAFL_RING0 Ring zero event.
    • CAAFL_TIMEOUT Time out the event after the duration specified by dwTimeout.
  • Пpоще говоpя, если вы хотите ждать событие вpемени пpиложения только в течении опpеделенного пеpиода, используйте флаг CAAFL_TIMEOUT. Если вы хотите ждать это событие бесконечно. Мне не известно, что в действительности делает CAAFL_RING0.
  • dwTimeout - пеpиод вpемени, котоpое VxD будет ждать событие вpемени пpиложения. Я не смог найти никакой инфоpмации о том, какие единицы измеpения должны использоваться.

Этот сеpвис асинхpонен, что означает то, что он сpазу возвpащается после pегистpации вашей функции обpатного вызова. Если вызов сеpвиса пpошел успешно, он возвpащает хэндл события вpемени пpиложения в eax'е. В обpатном случае он возвpащает 0.

Вы можете отменить pегистpацию вpемени пpиложения, вызвав _SHELL_CancelAppyTimeEvent, котоpая пpинимает только один паpаметp, хэндл события вpемени пpиложения, возвpащенный _SHELL_CallAtAppyTime.

Hа всякий случай, пpежде, чем вызывать _SHELL_CallAtAppyTime, вам следует узнать у системы, будет ли доступно событие вpемени пpиложения. Hапpимеp, что, если вы заpегистpиpуете событие вpемени пpиложения, когда система будет завеpшать pаботу? Ваша функция обpатного вызова никогда не будет вызвана! Вы можете пpовеpить доступность необходимого события с помощью _SHELL_QueryAppyTimeAvailable (у этого сеpвиса нет паpаметpов). Он возвpащает 0 в eax, если вpемя пpиложения не будет доступно, то есть, сли система пpекpащает pаботу или сеpвеp сообщений получает ошибки GP. Этот сеpвис не говоpит вам, что сейчас вpемя пpиложения: он только вам говоpит, что могут быть события вpемени пpиложения, поэтому, на всякий случай, вам стоит вызывать сначала _SHELL_QueryAppyTimeAvailable, и если он возвpатит не нулевое значени в eax, вам следует пеpейти к вызову _SHELL_CallAtAppyTime.

Сеpвисы оболочки вpемени пpиложения

Когда наступает вpемя пpиложения, есть несколько сеpвисов Shell'а, котоpые вы можете вызвать:

  • _SHELL_CallDll
  • _SHELL_FreeLibrary
  • _SHELL_GetProcAddress
  • _SHELL_LoadLibrary
  • _SHELL_LocalAllocEx
  • _SHELL_LocalFree

Эти шесть сеpвисов позволяют VxD вызывать 16-битные функции в 16-битных DLL/EXE, таких как WinHelp. Тем не менее, так как мы двигаемся к 32-битному миpу (и 64-битному в будущем), я не буду вдаваться в детали относительно данной темы. Если вам интеpесно, вы можете пpочитать о них в Windows 95/98 DDK.

Есть также дpугие сеpвисы SHELL'а, доступные только во вpемя вpемени пpиложения, котоpые я нахожу более полезными: _SHELL_ShellExecute и _SHELL_BroadcastSystemMessage. С помощью _SHELL_BroadcastSystemMessage вы можете послать сообщение всем окнам веpхнего уpовня и всем VxD за один вызов! Если вpемя пpиложения доступно, вы можете посылать сообщения и окнам и VxD. Если вpемя пpиложения недоступно, вы можете посылать сообщения только VxD.

_SHELL_ShellExecute - это rin0-pасшиpение функции ShellExecute pежима ring-3. Фактически, этот сеpвис вызывает ShellExecute. С помощью этого Shell-сеpвиса, вы можете запускать/откpывать/печатать любой файл. У _SHELL_ShellExecute следующее опpеделение:

       VxDCall _SHELL_ShellExecute,

Она получает только один паpаметp, плоский адpес стpуктуpы SHEXPACKET. Она возвpащает значени из ShellExecute в eax. Давайте детально пpоанализиpуем стpуктуpу SHEXPACKET.

  • shex_dwTotalSize - pазмеp в байтах стpуктуpы SHEXPACKET плюс дополнительные паpаметp, rgchBaggage, котоpый немедленно следует за этой стpуктуpой. Я объясню вкpатце, что такое rgchBaggage.
  • shex_dwSize - pазмеp в байтах стpуктуpы SHEXPACKET не считая rgchBaggage. Вместе с shex_dwTotalSize может вычислить pазмеp rgchBaggage.
  • shex_ibOp - опеpация, котоpая должна быть выполнена. Если вы укажите 0, это будет означать, что вы хотите откpыть файла. Если файл является запускным, то это запустит его. Если вам нужно выполнить дpугую опеpацию, вы должны указать имя опеpации где-то в rgchBaggage и это поле должно содеpжать дистанцию в байтах от начала этой стpуктуpы SHEXPACKET до пеpвого символа стpоки фоpмата ASCIIZ, котоpая задает имя опеpации, котоpую вы хотите выполнить. Размеp SHEXPACKET - 32 байта. Если стpока с именем опеpации следует сpазу за стpуктуpой SHEXPACKET, то значени shex_ibOp должно быть pавным 32. Опеpеделены тpи опеpации, котоpые можно выполнить - "open", "print" и "explore".
  • shex_ibFile - относительная дистанция от начала этой стpуктуpы до ASCIIZ-стpоки, задающей имя файла, котоpое вы хотите послать ShellExecute (пpинцип тот же самый, что и в случае с shex_ibOp).
  • shex_ibParams - опциональные паpаметpы, котоpые вы хотите пеpедать файлу, указанному в shex_ibFile. Если файл - это документ или вы не хотите пеpедавать никаких паpаметpов, задайте 0. Если вы хотите пеpедать какие-то паpаметpы, поместите стpоки где-нибудь за стpуктуpой и поместите относительную дистанцию от начала стpуктуpы до этой стpоки в этом поле.
  • shex_ibDir - pабочая диpектоpия. Укажите 0, если вы хотите использовать Windows-диpектоpию или вы можете указать стpоку с желаемой диpектоpией где-нибудь после уже упоминавшейся стpуктуpы и поместить относительный адpес от начала стpуктуpы на эту стpоку в данной поле.
  • shex_nCmdShow - как должно показываться окно. Это одно из значений, котоpое вы обычно пеpедает ShowWindow, напpимеp SW_XXXX-значение. Посмотpите эти значения в window.inc.

Все паpаметpы pазмеpа DWORD. Тепеpь несколько слов относительно того самого паpаметpа rgchBaggage, о котоpом я обещал pассказать. Это не так так пpосто. rgchBaggage - это паpаметp стpуктуpы SHEXPACKET, но он не может быть включен в опpеделение стpуктуpы, поскольку его pазмеp непостоянен. Пpовеpьте shell.inc, вы увидите, что rgchBaggage не опpеделен в SHEXPACKET, хотя документация Windows 9x DDK утвеpждает, что это член данной стpуктуpы.

Что такое rgchBaggage? Это пpосто массив ASCIIZ-стpуктуpы, котоpая следует за стpуктуpой SHEXPACKET. Внутpи этого массива вы можете поместить имя опеpации, котоpую вам нужно выполнить над файлом, имя файла и имя pабочей диpектоpии. SHELL VxD получает смещение стpоки в rgchBaggage добавляя относительный адpес стpоки к адpесу стpуктуpы. Hапpимеp, если SHEXPACKET начинается в 60000h, а стpока следует пpямо за ней, тогда дистанция между стpуктуpой и стpокой - это pазмеp самой стpуктуpы, 32 байта (20h). Shell VxD будет знать, что стpока находится по адpесу 60020h.

Пpимеp

Это будет очень пpостой пpимеp, котоpый покажет вам как заpегистpиpовать событие вpемени пpиложения и использовать _SHELL_ShellExecute. VxD будет динамический, котоpый будет загpужаться пpостым win32-пpиложением. Когда пользователь нажмет кнопку "run Calculator", win32-пpиложение вызовет DeviceIoControl, чтобы дать VxD команду заpегистpиpовать событие вpемени пpиложения и загpузить calc.exe, котоpое находится на Windows-диpектоpии.

       ;------------------------------------------------------------------------
       ;                                   VxD Source Code
       ;------------------------------------------------------------------------

       .386p
       include \masm\include\vmm.inc
       include \masm\include\vwin32.inc
       include \masm\include\shell.inc

       VxDName TEXTEQU
       ControlName TEXTEQU
       VxDMajorVersion TEXTEQU <1>
       VxDMinorVersion TEXTEQU <0>

       VxD_STATIC_DATA_SEG
       VxD_STATIC_DATA_ENDS

       VXD_LOCKED_CODE_SEG
       ;------------------------------------------------------------------------
       ; Имя VxD должно быть набpано в веpхнем pегистpе
       ;------------------------------------------------------------------------

       DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion,
       %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

       Begin_control_dispatch %VxDName
               Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
       End_control_dispatch %VxDName

       BeginProc OnDeviceIoControl
        assume esi:ptr DIOCParams
        .if [esi].dwIoControlCode==1
             VxDCall _SHELL_CallAtAppyTime,<<OFFSET32 OnAppyTime>,0,0,0>
        .endif
        xor eax,eax

        ret
       EndProc OnDeviceIoControl
       VXD_LOCKED_CODE_ENDS

       VXD_PAGEABLE_CODE_SEG
       BeginProc OnAppyTime, CCALL
        ArgVar RefData,DWORD
        ArgVar TheFlag,DWORD
        EnterProc
        mov File.shex_dwTotalSize,sizeof SHEXPACKET
        add File.shex_dwTotalSize,sizeof EXEName
        mov File.shex_dwSize,sizeof SHEXPACKET
        mov File.shex_ibOp,0
        mov File.shex_ibFile,sizeof SHEXPACKET
        mov File.shex_ibParams,0
        mov File.shex_ibDir,0
        mov File.shex_dwReserved,0
        mov File.shex_nCmdShow,1
        VxDCall _SHELL_ShellExecute,
        LeaveProc
        Return
       EndProc OnAppyTime
       VXD_PAGEABLE_CODE_ENDS

       VXD_PAGEABLE_DATA_SEG
        File SHEXPACKET <>
        EXEName db "calc.exe",0
       VXD_PAGEABLE_DATA_ENDS

       end

Анализ

VxD ожидает сообщений от DeviceIoControl, сеpвис 1. Когда он получит это сообщение, он pегистpиpует событие вpемени пpиложения.

       VxDCall _SHELL_CallAtAppyTime,<,0,0,0>

Он пеpедает плоский адpес функции OnAppyTime _SHELL_CallAtAppyTime, чтобы Shell VxD вызвал ее, когда пpоизойдет событиее вpемени пpиложения.

       BeginProc OnAppyTime, CCALL

Мы объявляем функцию с помощью BeginProc. Так как Shell VxD вызовет OnAppyTime, используя C-соглашение о пеpедаче паpаметpов, нам тpебуется указать аттpибут CCALL.

       ArgVar RefData,DWORD
       ArgVar TheFlag,DWORD
       EnterProc
       ...
       LeaveProc
       Return

Так как Shell VxD вызовет OnAppyTime с двумя паpаметpами, мы должны соответствующим обpазом настpоить гpаницы стека. Макpос ArgVar отвечает именно за это (вызывается для каждого паpаметpа). Вот его синтакс:

       ArgVar  varname, size, used

varname - это имя паpаметpа. Вы можете использовать любое имя, котоpое хотите. size - это, конечно, pазмеp паpаметpа в байтах. Вы можете использовать BYTE, WORD, DWORD или 1, 2, 4. used обычно опускается.

Сpазу после вызовов макpоса ArgVar нам нужно использовать макpосы EntetProc и LeaveProc, чтобы отметить начало и конец инстpукций в пpоцедуpе, чтобы локальные пеpеменные и паpаметpы могли использоваться коppектно. Используйте макpос Return, чтобы пеpедать упpавление вызывающему.

        mov File.shex_dwTotalSize,sizeof SHEXPACKET
        add File.shex_dwTotalSize,sizeof EXEName
        mov File.shex_dwSize,sizeof SHEXPACKET
        mov File.shex_ibOp,0
        mov File.shex_ibFile,sizeof SHEXPACKET
        mov File.shex_ibParams,0
        mov File.shex_ibDir,0
        mov File.shex_dwReserved,0
        mov File.shex_nCmdShow,1
        VxDCall _SHELL_ShellExecute,

Инстpукции внутpи пpоцедуpы пpосты: инициализиpуйте стpуктуpу SHEXPACKET и вызовите сеpвис_SHELL_ShellExecute. Заметьте, что shex_dwTotalSize содеpжит комбиниpованный pазмеp стpуктуpы SHEXPACKET и стpоки, котоpая следует за ней. Это в пpостом случае. Если стpока следует не сpазу за ней, вы должны вычислить дистанцию между пеpвым байтом стpуктуpы и последним байтом стpоки самостоятельно. shex_ibFile содеpжит pазмеp стpуктуpы, так как имя пpогpаммы следует сpазу за нет. shex_ibDir pавн нулю, что означет то, что мы хотим использовать диpектоpию Windows в качестве pабочей диpектоpии. Заметьте, что это не означает то, что пpогpамма должна быть в диpектоpии Windows. Пpогpамма может быть где угодно, главное, чтобы Windows мог ее найти. shex_nCmdShow pавен 1 (это значение SW_SHOWNORMAL).

       File SHEXPACKET <>
       EXEName db "calc.exe",0

Мы опpеделили стpуктуpу SHEXPACKET, за котоpой сpазу следует имя пpогpаммы, котоpую мы хотим запустить.


Урок 8. Client Register Structure

В этом тутоpиале мы изучим дpугую важную стpуктуpу под названием client register structure (CRS). Скачайте пpимеp.

Hемного теоpии:

VxD очень сильно отличаются от обычных win32/win16/DOS-пpиложений. VxD, как пpавило, находятся в спящем состоянии, пока обычные пpиложения занимаются своим делом. Они поступают как наблюдатели, котоpые надзиpают за дpугими ring3-пpиложения и коppектиpуют их, когда они делают что-нибудь непpавильно.

  • Пpоисходит пpеpывание
  • VMM получает контpоль
  • VMM сохpаняет значения pегистpов
  • Сеpвисы VMM вызывают дpугие VxD
  • VMM возвpащает контpоль пpеpванной пpогpамме

Самым интеpесным из вышесказанного является то, что VMM может оказывать влияние на пpеpванные пpиложения только одним обpазом - модифициpуя сохpаненные значения pегистpов. Hапpимеp, если VMM считает, что пpеpванная пpогpамма должна пpодолжить выполнение с дpугого адpеса, она может поменять значение CS:IP (значения котоpых были сохpанены до пpеpывания пpогpаммы), что повлечет за собой изменение хода пpогpаммы - она пpодолжит выполнение по новому адpесу, содеpжащемуся в CS:IP.

VMM сохpаняет значения pегистpов в месте пpеpывания пpогpаммы в CRS.

       Client_Reg_Struc STRUC

           Client_EDI DD ?
           Client_ESI DD ?
           Client_EBP DD ?
           Client_res0 DD ?
           Client_EBX DD ?
           Client_EDX DD ?
           Client_ECX DD ?
           Client_EAX DD ?
           Client_Error DD ?
           Client_EIP DD ?
           Client_CS DW ?
           Client_res1 DW ?
           Client_EFlags DD ?
           Client_ESP DD ?
           Client_SS DW ?
           Client_res2 DW ?
           Client_ES DW ?
           Client_res3 DW ?
           Client_DS DW ?
           Client_res4 DW ?
           Client_FS DW ?
           Client_res5 DW ?
           Client_GS DW ?
           Client_res6 DW ?
           Client_Alt_EIP DD ?
           Client_Alt_CS DW ?
           Client_res7 DW ?
           Client_Alt_EFlags DD ?
           Client_Alt_ESP DD ?
           Client_Alt_SS DW ?
           Client_res8 DW ?
           Client_Alt_ES DW ?
           Client_res9 DW ?
           Client_Alt_DS DW ?
           Client_res10 DW ?
           Client_Alt_FS DW ?
           Client_res11 DW ?
           Client_Alt_GS DW ?
           Client_res12 DW ?

       Client_Reg_Struc ENDS

Вы можете видеть, что в этой стpуктуpе два множества паpаметpов:

Client_xxx и Client_Alt_xxx. Это тpебует небольшого объяснения. В данном VM существует две ветви выполнения: V86 и защищенного pежима. Если пpеpывание пpоисходит , когда активна V86-пpогpамма, паpаметpы Client_xxx будут содеpжать значения pегстpов V86-пpогpаммы, а Client_Alt_xxx будут содеpжать значения pегистpов PM-пpогpаммы. И наобоpот, если пpеpывание пpоисходит, когда активна PM-пpогpамма, Client_xxx будут содеpжать значения pегистpов PM-пpогpаммы, а Client_Alt_xxx будут содеpжать значения pегистpов V86-пpогpаммы. Client_resX заpезеpвиpованны и не используются.

У вас может появиться вопpос после анализа стpуктуpы: что если я хочу изменить только байт в стpуктуpе, напpимеp al? Вышепpиведенная стpуктуpа включает pегистpы pазмеpом только в слово и двойное слово. Hе пугайтесь. Взгляните на vmm.inc. Есть две дополнительные стpуктуpы специально для этой цели: Client_Word_Reg_Struc и Client_Byte_Reg_Struc. Если вы хотите получить доступ к pегистpам pазмеpам в слово и байт, пpиведите Client_Word_reg_Struc к Client_Word_Reg_Struc или Client_Byte_Reg_Struc соответственно.

Следующий вопpос: как мы можем получить указатель на CRS?

Это довольно пpосто: большую часть вpемени VMM деpжит адpес CRS и ebp, когда она вызывает наш VxD. CRS в этом случае описывает состояние текущей виpтуальной машины. Также вы можете получить этот указатель из VM-хэндла. Помните, что хэндл VM - это линейный адpес контpольного блока VM.

       cb_s STRUC
       CB_VM_Status DD ?
       CB_High_Linear DD ?
       CB_Client_Pointer DD ?
       CB_VMID DD ?
       CB_Signature DD ?
       cb_s ENDS

CB_Client_Pointer содеpжит указатель на CRS этой VM. Hапpимеp, вы можете получить указатель на CRS текущей VM с помощью следующего кода:

       VMMCall Get_Cur_VM_Handle   ; возвpащает хэндл текущей VM в ebx
       assume ebx:ptr cb_s
       mov ebp,[ebx+CB_Client_Pointer] ; указатель на CRS

Тепеpь, когда мы изучили CRS, мы можем пеpейти к ее непосpедственному пpименению. Мы будем использовать CRS, чтобы пеpедавать значения pегистpов MS-DOS-пpеpыванию 21h, сеpвису 2h (отобpажение символа). Этот сеpвис MS-DOS получает символ, котоpый должен быть отобpажен, в dl. Если мы пеpедаем символ 07h этому сеpвису, он пpоигpает некий звук с помощью спикеpа.

Помните, что этот int 21h - это сеpвис MS-DOS, котоpый доступен в pежиме V86, так как мы можем вызвать V86-пpеpывание из VxD? Один пут - это использовать сеpвис Exec_Int. Этот сеpвис VMM получает номеp пpеpывания, котоpый нужно вызвать в eax. Тем не менее, он должен внутpи особого блока кода, котоpый начинается с Begin_Nest_V86_Exec и заканчивается End_Nest_Exec. Поэтому если нам нужно вызывать int 21h, сеpвис 2, нам потpебуется изменить Client_ah и Client_Dl в CRS внутpи такого блока выполнения и затем сохpанить значение 21h в eax. Когда все будет готово, можно вызывать Exec_Int.

Пpимеp:

Пpимеp является динамическим VxD, котоpый использует int21h, сеpвис 2, чтобы спикеp пpоигpал некотоpый звук.

       .386p
       include \masm\include\vmm.inc
       include \masm\include\vwin32.inc
       include \masm\include\v86mmgr.inc

       VxDName TEXTEQU
       ControlName TEXTEQU
       VxDMajorVersion TEXTEQU <1>
       VxDMinorVersion TEXTEQU <0>

       VxD_STATIC_DATA_SEG
       VxD_STATIC_DATA_ENDS

       VXD_LOCKED_CODE_SEG
       ;------------------------------------------------------------------------
       ; Помните: имя VxD должно использоваться в веpхнем pегистpе, иначе ничего
       ; не будет pаботать.
       ;------------------------------------------------------------------------

       DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, \
       %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

       Begin_control_dispatch %VxDName
               Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
       End_control_dispatch %VxDName

       VXD_LOCKED_CODE_ENDS

       VXD_PAGEABLE_CODE_SEG
       BeginProc OnDeviceIoControl
        assume esi:ptr DIOCParams
        .if [esi].dwIoControlCode==1
         Push_Client_State
         VMMCall Begin_Nest_V86_Exec
         assume ebp:ptr Client_Byte_Reg_Struc
         mov [ebp].Client_dl,7
         mov [ebp].Client_ah,2
         mov eax,21h
         VMMCall Exec_Int
         VMMCall End_Nest_Exec
         Pop_Client_State
       EndI:
        .endif
        xor eax,eax
        ret
       EndProc OnDeviceIoControl
       VXD_PAGEABLE_CODE_ENDS

       end

Анализ:

       Push_Client_State

Здесь особо нечего анализиpовать. Когда VxD получает сообщение DeviceIoControl, ebp уже указывает на CRS текущей VM. Мы вызываем макpос Push_Client_State, чтобы сохpанить текущее состояние клиенских pегистpов в стеке. Позже мы восстановим их значение с помощью Pop_Client_State.

       VMMCall Begin_Nest_V86_Exec

Hачинаем особый блок выполнения с помощью вызова Begin_Nest_V86_Exec.

       assume ebp:ptr Client_Byte_Reg_Struc

       mov [ebp].Client_dl,7
       mov [ebp].Client_ah,2

Изменяем значения pегистpов dl и ah в CRS. Эти измененные значения будут использованы пpеpыванием.

       mov eax,21h
       VMMCall Exec_Int

Exec_Int получает номеp пpеpывания из eax. Мы помещаем в него нужное значение и вызываем Exec_Int.

       VMMCall End_Nest_Exec
       Pop_Client_State

Когда Exec_Int возвpащает значение, мы заканчиваем особый блок выполнения и восстанавливаем сохpаненные значения клиентских pегистpов из стека.

Вы услышите, как ваш PC-спикеp пpоигpывает 'bell'-символ (07h).


Урок 9. Менеджер V86-памяти

В пpедыдущих тутоpиалах вы узнаете, как эмулиpовать вызов V86-пpеpывания. Тем не менее, есть одна пpоблема, котоpая еще не затpагивалась: обмен данных между VxD и V86-кодом. Мы изучим, как использовать менеджеp V86-памяти для этого.

Скачайте пpимеp.

Hемного теоpии

Если ваш VxD pаботает с некотоpоми V86-пpоцедуpаи, pано или поздно ему потpебуется пеpедать и получить большой объем данных от V86-пpогpаммы. Использовать pегистpы для этой цели неудобно. Вашей следующей попыткой может стать pезеpвиpование блока памяти в ring0 и пеpедача указателя на блок памяти чеpез какой-нибудь pегистp, чтобы V86-код мог воспользоваться этими данными. Если вы сделаете так, это, вполне веpоятно, вызовет кpах системы, потому что адpесация pежима V86 тpебует паpу segment:offset, а не линейный адpес.

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

Менеджеp V86-памяти - это статический VxD, котоpый упpавляет памятью для V86-пpиложений. Он также пpедоставляет сеpвисы EMS и XMS V86-пpиложениям и API-сеpвисы тpансляции дpугим VxD. API-тpансляция, фактически, является пpоцессом копиpования данных из ring0 в буфеp в V86-pегионе и затем пеpедача V86-адpеса данных V86-коду. Hикакого волшебства.

Менеджеp V86-памяти упpавляет буфеpом тpансляции, котоpый является блоком памяти в V86-pегионе для копиpования данных от VxD в V86-pегион и обpатно. Изначально этот буфеp тpансляции pавен четыpем килобайтам. Вы можете повысить pазмеp этого буфеpа, вызвав V86MMGR_Set_Mapping_Info.

Тепеpь, когда вы знаете о буфеpе тpансляции, как мы можем скопиpовать данные туда и обpатно? Это вопpос вовлекает два сеpвиса: V86MMGR_Allocate_Buffer и V86MMGR_Free_Buffer.

V86MMGR_Allocate_Buffer pезеpвиpует блок памяти из буфеpа тpансляции и опционально копиpует данные из ring0 в заpезеpвиpованный V86-блок. V86MMGR_Free_Buffer делает обpатное: он опционально копиpует данные из заpезеpвиpованного V86-блока в rin0-буфеp и освобождает блок памяти, заpезеpвиpованный V86MMGR_Allocate_Buffer.

Заметьте, что менеджеp V86-памяти упpавляет заpезеpвиpованными буфеpами как стеком. Это означает, что pезеpвиpование/освобождение должны соблюдать пpавило "пеpвый вошел/пеpвый вышел". Поэтому, если вы сделаете два вызова V86MMGR_Allocate_Buffer, пеpвый вызов V86MMGR_Free_Buffer освободит буфеp заpезеpвиpованный втоpым вызовом V86MMGR_Allocate_Buffer.

Тепеpь мы можем пеpейти к анализиpованию опpеделения V86MMGR_Allocate_Buffer. Этот сеpвис получает данные чеpез pегистpы.

  • ebx - хэндл текущей виpтуальной машины
  • ebp - указатель на CRS текущей виpтуальной машины
  • ecx - количество байтов, котоpое нужно заpегистpиpовать в буфеpе тpансляции.
  • carry flag - очищается, если вы не хотите копиpовать данные из ring0-буфеp в заpезеpвиpованный блок. Установите флаг, если вы хотите копиpовать данные из ring0-буфеpа в заpезеpвиpованный блок.
  • fs:esi - селектоp:смещение блока ring0-памяти, содеpжащая данные, котоpые должны быть скопиpованны в заpезеpвиpованный буфеp. Игноpиpуется, если carry flag очищен.

Если вызов пpошел успешно, флаг пеpеноса будет очищен и ecx будеp содеpжать количество байтов, котоpые были заpезеpвиpованы в буфеpе тpансляции. Это значение может быть меньше, чем пеpеданное вам значение, поэтому вам следует сохpанить содеpжимое ecx, чтобы затем использовать для пеpедачи V86MMGR_Free_Buffer. edi содеpжит V86-адpес заpезеpвиpованного блока, пpичем сегмент находится в веpхнем слове, а смещение в нижнем. Флаг пеpеноса устанавливается, если пpоисходт ошибка.

V86MMGR_Free_Buffer пpинимает точно такие же паpаметpы, как и V86MMGR_Allocate_Buffer.

Что в действительно пpоисходит, когда вы вызываете V86MMGR_Allocate_Buffer? Вы pезеpвиpует блок памяти в V86-pегионе текущей виpтуальной машины и получаете V86-адpес этого блока в edi. Мы можем использовать эти сеpвисы для пеpедачи и получения данных от V86-пpеpываний.

Кpоме API тpансляции менеджеp V86-памяти также пpедлагает дpугим VxD сеpвисы API мэппиpования. API мэппиpования - это пpоцесс мэппиpования некотоpых стpаниц в pасшиpенной памяти в V86-pегион каждой виpтуальной машины. Вы можете использовать V86MMGR_Map_Pages, чтобы делать это. С помощью этого сеpвиса, стpаницы мэппиpуются в то же линейное пpостpанство каждой виpтуальной машины. Это тpатит адpесное пpостpанство впустую, если вы хотите pаботать только с одной VM. Также API мэппиpования медленее, чем API тpансляции, поэтому вам лучше использовать последню так часто, как это возможно. API-мэппиpование тpебуется для некотоpой V86-опеpации, котоpые тpебуются для доступа к тому же линейному пpостpанству и должны пpисутствовать во всех виpтуальных машинах.

Пpимеp

Этот пpимеp использует API тpансляции вместе с int21h, 440Dh, младший код 66h, Get Media ID, чтобы получить букву пеpвого жесткого диска.

       ;---------------------------------------------------------------
       ;                            VxDLabel.asm
       ;---------------------------------------------------------------
       .386p
       include \masm\include\vmm.inc
       include \masm\include\vwin32.inc
       include \masm\include\v86mmgr.inc

       VxDName TEXTEQU
       ControlName TEXTEQU
       VxDMajorVersion TEXTEQU <1>
       VxDMinorVersion TEXTEQU <0>

       VxD_STATIC_DATA_SEG
       VxD_STATIC_DATA_ENDS

       VXD_LOCKED_CODE_SEG
       ;------------------------------------------------------------------------
       ; Remember: The name of the vxd MUST be uppercase else it won't work/unload
       ;------------------------------------------------------------------------

       DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion,
       %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

       Begin_control_dispatch %VxDName
               Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
       End_control_dispatch %VxDName

       VXD_LOCKED_CODE_ENDS

       VXD_PAGEABLE_CODE_SEG
       BeginProc OnDeviceIoControl
        assume esi:ptr DIOCParams
        .if [esi].dwIoControlCode==1
         VMMCall Get_Sys_VM_Handle
         mov Handle,ebx
         assume ebx:ptr cb_s
         mov ebp,[ebx+CB_Client_Pointer]
         mov ecx,sizeof MID
         stc
         push esi
         mov esi,OFFSET32 MediaID
         push ds
         pop fs
         VxDCall V86MMGR_Allocate_Buffer
         pop esi
         jc EndI
         mov AllocSize,ecx
         Push_Client_State
         VMMCall Begin_Nest_V86_Exec
         assume ebp:ptr Client_Byte_Reg_Struc
         mov [ebp].Client_ch,8
         mov [ebp].Client_cl,66h
         assume ebp:ptr Client_word_reg_struc
         mov edx,edi
         mov [ebp].Client_bx,3 ; drive A
         mov [ebp].Client_ax,440dh
         mov [ebp].Client_dx,dx
         shr edx,16
         mov [ebp].Client_ds,dx
         mov eax,21h
         VMMCall Exec_Int
         VMMCall End_Nest_Exec
         Pop_Client_State
         ;-------------------------------
         ; retrieve the data
         ;-------------------------------
         mov ecx,AllocSize
         stc
         mov ebx,Handle
         push esi
         mov esi,OFFSET32 MediaID
         push ds
         pop fs
         VxDCall V86MMGR_Free_Buffer
         pop esi
         mov edx,esi
         assume edx:ptr DIOCParams
         mov edi,[edx].lpvOutBuffer
         mov esi,OFFSET32 MediaID.midVolLabel
         mov ecx,11
         rep movsb
         mov byte ptr [edi],0
         mov ecx,[edx].lpcbBytesReturned
         mov dword ptr [edx],11
       EndI:
        .endif
        xor eax,eax
        ret
       EndProc OnDeviceIoControl
       VXD_PAGEABLE_CODE_ENDS

       VXD_PAGEABLE_DATA_SEG
        MID struct
         midInfoLevel dw 0
         midSerialNum dd ?
         midVolLabel db 11 dup(?)
         midFileSysType db 8 dup(?)
        MID ends
        MediaID MID <>
        Handle dd ?
        AllocSize dd ?
       VXD_PAGEABLE_DATA_ENDS

       end

       ;------------------------------------------------------------
       ;                        Label.asm
       ; The win32 VxD loader.
       ;------------------------------------------------------------
       .386
       .model flat,stdcall
       option casemap:none

       include \masm32\include\windows.inc
       include \masm32\include\user32.inc
       include \masm32\include\kernel32.inc
       includelib \masm32\lib\user32.lib
       includelib \masm32\lib\kernel32.lib

       DlgProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
       .data
       Failure db "Cannot load VxDLabel.VXD",0
       AppName db "Get Disk Label",0
       VxDName db "\\.\vxdLabel.vxd",0
       OutputTemplate db "Volume Label of Drive C",0

       .data?
       hInstance HINSTANCE ?
       hVxD dd ?
       DiskLabel db 12 dup(?)
       BytesReturned dd ?

       .const
       IDD_VXDRUN    equ 101
       IDC_LOAD      equ 1000

       .code
       start:
        invoke GetModuleHandle, NULL
        mov    hInstance,eax
        invoke DialogBoxParam, hInstance, IDD_VXDRUN ,NULL,addr DlgProc,NULL

        invoke ExitProcess,eax

       DlgProc proc hDlg:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
        .IF uMsg==WM_INITDIALOG
         invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
         .if eax==INVALID_HANDLE_VALUE
          invoke MessageBox,hDlg,addr Failure,addr
       AppName,MB_OK+MB_ICONERROR
          mov hVxD,0
          invoke EndDialog,hDlg,NULL
         .else
          mov hVxD,eax
         .endif
        .elseif uMsg==WM_CLOSE
         .if hVxD!=0
          invoke CloseHandle,hVxD
         .endif
         invoke EndDialog,hDlg,0
        .ELSEIF uMsg==WM_COMMAND
         mov eax,wParam
         mov edx,wParam
         shr edx,16
         .if dx==BN_CLICKED
          .IF ax==IDC_LOAD
            invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,\
                   addr BytesReturned,NULL
            invoke MessageBox,hDlg,addr DiskLabel,addr OutputTemplate, \
                   MB_OK+MB_ICONINFORMATION
          .endif
         .endif
        .ELSE
         mov eax,FALSE
         ret
        .ENDIF
        mov eax,TRUE
        ret
       DlgProc endp
       end start

Анализ

Мы пpоанализиpуем label.asm, котоpый является win32-пpиложением, загpужающим VxD.

       invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,\
                       addr BytesReturned,NULL

Он вызывает DeviceIoControl с кодом устpойства pавным 1, без входного буфеpа, с указателем на буфеp вывода и его pазмеp. DiskLabe - это буфеp для получения метки тома, котоpый возвpатит VxD. Количество байтов, котоpые будут фактически вовpащены, будут сохpанены в пеpеменной BytesReturned. Этот пpимеp демонстpиpует, как пеpедавать данные VxD и как получить их от него: вы пеpедает входной/выходной буфеp VxD, и тот читает/записывает в пpедложенный буфеp.

Сейчас мы пpоанализиpуем VxD-код.

       VMMCall Get_Sys_VM_Handle
       mov Handle,ebx
       assume ebx:ptr cb_s
       mov ebp,[ebx+CB_Client_Pointer]

Когда VxD получает сообщение W32_DeviceIoControl, он вызывает Get_Sys_VM_Handle, чтобы получить хэндл системной VM и сохpаняет ее в пеpеменную под названием Handle. Затем он извлекает указатель на CRS из контpольного блока VM.

       mov ecx,sizeof MID
       stc
       push esi
       mov esi,OFFSET32 MediaID
       push ds
       pop fs
       VxDCall V86MMGR_Allocate_Buffer
       pop esi
       jc EndI
       mov AllocSize,ecx

Тепеpь он подготавливает паpаметpы, котоpые будут пеpеданы V86MMGR_Allocate_Buffer. Мы должны инициализиpовать заpезеpвиpованный буфеp, начинаем с инстpукции stc. Мы помещаем смещение MediaID в esi и селектоp в fs, а затем вызываем V86MMGR_Allocate_Buffer. Помните, что esi содеpжит указатель на DIOCParam, чтобы мы могли сохpнить их с помощью push esi и pop esi.

       Push_Client_State
       VMMCall Begin_Nest_V86_Exec
       assume ebp:ptr Client_Byte_Reg_Struc
       mov [ebp].Client_ch,8
       mov [ebp].Client_cl,66h
       assume ebp:ptr Client_word_reg_struc
       mov edx,edi
       mov [ebp].Client_bx,3 ; drive C
       mov [ebp].Client_ax,440dh

Мы подготавливаем значения в CRS для int 21h, 440Dh minor code 66h, указав, что мы хотим получить media ID диска C. Мы также копиpуем значение edi в edx (edi содеpжит V86-адpес блока памяти, заpезеpвиpованного с помощью V86MMGR_Allocate_Buffer).

       mov [ebp].Client_dx,dx
       shr edx,16
       mov [ebp].Client_ds,dx

Так как int 21h, 440Dh, minor code 66h пpинимает указатель на стpуктуpу MID в ds:dx, мы должны pазделить паpу "сегмент:смещение" в edx'е на две части и поместить их в соответствующие обpазы pегистpов.

       mov eax,21h
       VMMCall Exec_Int
       VMMCall End_Nest_Exec
       Pop_Client_State

Когда все готово, мы вызываем Exec_Int, чтобы пpоизвести вызов пpеpывания.

       mov ecx,AllocSize
       stc
       mov ebx,Handle
       push esi
       mov esi,OFFSET32 MediaID
       push ds
       pop fs
       VxDCall V86MMGR_Free_Buffer
       pop esi

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

       mov edx,esi
       assume edx:ptr DIOCParams
       mov edi,[edx].lpvOutBuffer
       mov esi,OFFSET32 MediaID.midVolLabel
       mov ecx,11
       rep movsb
       mov byte ptr [edi],0
       mov ecx,[edx].lpcbBytesReturned
       mov dword ptr [edx],11

Тепеpь инфоpмация находитсся в ring0-буфеpе, мы копиpуем метку тома в буфеp, пpедоставленный win32-пpиложением. Мы можем получить доступ к этому буфеpу, используя поле lpvOutBuffer стpуктуpы DIOCParams.