Ассемблер под Windows для чайников. Часть 22

Если вы помните, в последних выпусках по ассемблеру (см. КГ №47 за 2008 год), мы изучали разные оконные и графические функции Windows: отображали картинку из ресурсов в окне программы, осуществляли поочередное воспроизведение кадров для реализации анимации, создавали окна нестандартной формы. Чтобы покончить с азами графики, нам осталось написать собственный графический редактор. Редактор, естественно, будет простеньким, и на первых порах даже не будет иметь возможностей редактирования. Ну да ладно, для начала попробуем осилить хотя бы принципы создания простейшего "вьювера" - программы просмотра изображений. Тем более что даже просмотр изображений придется разбирать на протяжении нескольких статей.

С чего начинается "вьювер"? С написания основы любого оконного приложения. Писать это дело с ноля мне было лень, и я взял за основу код текстового редактора, который был написан еще для 6-й части данного цикла (КГ №22 за 2008 г.). Что-то пришлось изменить, что-то – удалить, много чего добавилось, но суть осталась примерно та же – открытие файла для просмотра. Правда, там файлы были текстовые, а здесь – битовые карты (BitMap). Вот эти отличия вы и найдете в сегодняшней статье, но все по порядку - смотрим новые и измененные данные:

MAXSIZE equ 260
VERSION equ '0.0.0.1'

section '.data' data readable writeable

title db 'Мой Первый Графический Редактор',0
class db 'FASMWIN32',0
filter db 'Bitmaps',0,'*.bmp',0
db 'All files',0,'*.*',0,0
errtxt db 'Код ошибки: %u',0
errbuf rb $-errtxt+10

wc WNDCLASS 0,WindowProc,0,0,0,0,0,COLOR_BTNFACE+1,0,class
ofn OPENFILENAME sizeof.OPENFILENAME,0,0,filter,0,0,0,fname,MAXSIZE
menuinfo MENUITEMINFO sizeof.MENUITEMINFO,MIIM_STATE

fname rb MAXSIZE
hfile dd ?
hheap dd ?
pmem dd ?
BytesRead dd ?
FileSize dd ?
hwnd dd ?
hmenu dd ?
hacc dd ?
hBitmap dd ?
BmpPS PAINTSTRUCT
client RECT
hDC dd ?
hMemDC dd ?
pBMInfo dd ?
pBits dd ?
bWidth dd ?
bHeight dd ?
msg MSG

Константа MAXSIZE служит не только для наглядности. Старайтесь всегда использовать константы, если какое-либо значение используется в коде более одного раза. Здесь MAXSIZE – максимальная длина имени файла в байтах – используется и для резервирования буфера под имя файла, и для указания размера этого буфера в структуре OPENFILENAME. Быстрее было бы написать "260" в обоих случаях, но тогда повышается риск того, что когда-нибудь, решив изменить это значение, вы измените его в одной строке, забыв про другую. И потом долго будете отлавливать причину случайных ошибок. Так что константы, хотя и не самое главное в сегодняшнем занятии, но и не являются чем-то излишним.

Переменные не сильно изменились, но все же, на мой взгляд, нуждаются в кратком описании. Заголовок и класс окна традиционно обозначены "title" и "class". Фильтр допустимых файлов "filter" состоит из одной или нескольких пар строк, завершающихся нолем. Каждая пара – это отображаемое имя фильтра и собственно сам фильтр. Последняя пара должна завершаться двумя нолями. Когда-нибудь вы сможете честно написать в первой паре: 'Imagess',0, '*.bmp;*.gif;*.jpg;*.png',0, но пока что давайте разберемся с несжатым BMP-форматом. Строка "errtxt" необходима для вывода системной ошибки, а "errbuf" – буфер, в который будет копироваться отформатированная строка с номером ошибки вместо ключа "%u", указывающего, что значение ошибки следует понимать, как беззнаковое целое. Структуры "wc", "ofn" и "menuinfo" – стандартные структуры, некоторые элементы которых мы инициализируем сразу во время выделения памяти под структуру.

Дескриптор открываемого файла будем сохранять в двойном слове по адресу "hfile". Еще раз напомню для тех, кто недавно влился в ряды постоянных читателей данного цикла, что "hfile", как и в случаях с остальными переменными, является по сути лишь константой, значение которой – адрес первого байта переменной. Поэтому, обращаясь к содержимому переменной, мы должны заключать ее имя в квадратные скобки: [hfile]. Команда "mov [hfile],eax" верна, так как выполняет сохранение содержимого регистра eax в переменной по адресу hfile. А вот команда "mov hfile,eax" является грубой синтаксической ошибкой (по правилам FASM), так как пытается сохранить содержимое eax в значении константы, словно вы попробуете запихнуть чемодан не в камеру хранения с определенным номером, а непосредственно в номер, нанесенный на дверцу этой камеры.

В двойном слове по адресу "hheap" мы поместим дескриптор стандартной кучи (default heap) нашего процесса. В Windows 2K/XP/Vista (из соображений безопасности) выделять память принято через функции диспетчера куч. Кучу можно создать, или воспользоваться стандартной кучей, которая автоматически создается при создании любого процесса, имеет начальный размер в 1 Мб и автоматически увеличивается по мере необходимости. Чтобы выделить блок памяти в куче, изменить его размер или освободить этот блок, необходимо указать дескриптор кучи, который мы и сохраним в [hheap]. А в [pmem] мы запишем указатель (pointer) на выделенный блок памяти.

Остальные переменные обсудим по мере разбора кода программы:

section '.code' code readable executable

start:
invoke GetModuleHandle,0
mov [wc.hInstance],eax
mov [ofn.hInstance],eax
invoke LoadIcon,[wc.hInstance],IDI_MAIN
mov [wc.hIcon],eax
invoke LoadCursor,0,IDC_ARROW
mov [wc.hCursor],eax
invoke RegisterClass,wc
cmp eax,0
je error
invoke LoadAccelerators,[wc.hInstance],IDA_MAIN
mov [hacc],eax
invoke LoadMenu,[wc.hInstance],IDM_MAIN
mov [hmenu],eax
invoke
CreateWindowEx,0,class,title,WS_VISIBLE+WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,0,eax,[wc.hInstance],0 cmp eax,0
je error
mov [hwnd],eax
msg_loop:
invoke GetMessage,msg,0,0,0
cmp eax,0
je end_loop
invoke TranslateAccelerator,[hwnd],[hacc],msg
cmp eax,0
jne msg_loop
invoke TranslateMessage,msg
invoke DispatchMessage,msg
jmp msg_loop

error:
invoke GetLastError
invoke wsprintf,errbuf,errtxt,eax
invoke MessageBox,0,errbuf,0,MB_OK

end_loop:
invoke ExitProcess,[msg.wParam]


Получаем дескриптор нашего исполняемого модуля и сохраняем его в соответствующих элементах hInstance структур wc и ofn. В общем, неплохо было бы сохранить этот дескриптор еще и в отдельную самостоятельную переменную, так как он нам необходим для загрузки иконки, меню из ресурсов модуля, а также для создания окна и диалогового окна "О программе", но можно для этого использовать и элемент структуры [wc.hInstance] – процессор отличий не заметит. Загружаем нашу иконку, записываем полученный дескриптор в соответствующий элемент структуры wc. То же самое проделываем с курсором, только курсор загружаем не из ресурсов нашего файла, а стандартный системный. Регистрируем класс окна, загружаем меню и акселераторы из ресурсов модуля. Создаем окно (CreateWindowEx), и сохраняем возвращенный дескриптор в переменной [hwnd], если не произошло ошибки.

Теперь любые действия пользователя, примененные к нашему окну, будут обрабатываться стандартным способом в цикле "msg_loop", и, в случае какого- либо события, данные о нем будут переданы процедуре WindowProc в виде четырех 32-битных параметров: дескриптор окна, сообщение, два параметра сообщения. Заметьте еще раз, что переменная hwnd в теле процедуры является локальной, и не путайте ее с глобальной одноименной переменной вне процедуры. Глобальная hwnd хранит в себе дескриптор главного окна программы, а вот локальная будет содержать дескриптор того окна, от которого пришло сообщение – может, главного, а может, и какого-нибудь дочернего. Давайте теперь рассмотрим код процедуры обработки этих сообщений:

proc WindowProc hwnd,wmsg,wparam,lparam
push ebx esi edi
cmp [wmsg],WM_COMMAND
je .wmcommand
cmp [wmsg],WM_PAINT
je .wmpaint
cmp [wmsg],WM_CLOSE
je .EXIT
cmp [wmsg],WM_DESTROY
je .wmdestroy
.defwndproc:
invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam]
jmp .finish
.wmcommand:
mov eax,[wparam]
cmp ax,IDM_OPEN
je .OPEN
cmp ax,IDM_EXIT
je .EXIT
jmp .finish
; обработчики сообщений меню:
.OPEN:
call open_file
jmp .finish
.EXIT:
invoke DestroyWindow,[hwnd]
mov eax,0
jmp .finish
.wmpaint:
invoke BeginPaint,[hwnd],BmpPS
mov [hDC],eax

cmp [pmem],0
je @f
invoke SetDIBitsToDevice,[hDC],0,0,[bWidth],[bHeight],0,0,0,[bHeight],[pBits],[pBMInfo],0
@@:
invoke EndPaint,[hwnd],BmpPS
mov eax,0
jmp .finish
.wmdestroy:
invoke PostQuitMessage,0
mov eax,0
.finish:
pop edi esi ebx
ret
endp

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

В общем, не забываем, что константы сообщений – это всего лишь числовые значения. Сравниваем значение полученного сообщения [wmsg] с теми сообщениями, которые мы готовы обработать средствами нашей программы. Если совпало – отправляемся на соответствующую метку для обработки сообщения, иначе – вызываем DefWindowProc для обработки любого другого сообщения средствами операционной системы. После полной обработки большинства сообщений необходимо обнулить регистр eax, чтобы показать системе, что сообщение обработано. На метке ".wmcommand" у нас идет еще сравнение младших шестнадцати бит [wparam] со значением обрабатываемых идентификаторов пунктов меню или назначенных им акселераторов. Значения константам этих идентификаторов присвоены ниже по тексту программы в секции ресурсов (section '.rsrc'):

proc open_file
ret
endp

section '.idata' import data readable writeable

library kernel32,'KERNEL32.DLL',user32,'USER32.DLL',gdi32,'GDI32.DLL', comdlg32,'COMDLG32.DLL'

include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\gdi32.inc'
include 'api\comdlg32.inc'

section '.rsrc' resource data readable

IDM_MAIN = 101
IDA_MAIN = 201
IDD_ABOUT = 301
IDI_MAIN = 401

IDM_NEW = 1101
IDM_OPEN = 1102
IDM_SAVE = 1103
IDM_SAVEAS = 1104
IDM_EXIT = 1109

directory RT_MENU,menus,RT_ACCELERATOR,accelerators,RT_GROUP_ICON, group_icons,RT_ICON,icons

resource menus,IDM_MAIN,LANG_RUSSIAN+SUBLANG_DEFAULT,main_menu

resource accelerators,IDA_MAIN,LANG_ENGLISH+SUBLANG_DEFAULT,main_keys

resource group_icons,IDI_MAIN,LANG_NEUTRAL,main_icon

resource icons,1,LANG_NEUTRAL,main_icon_data

menu main_menu
menuitem '&Файл',0,MFR_POPUP+MFR_END
menuitem <'Созд&ать',9,'Ctrl+N'>,IDM_NEW,0,MFS_GRAYED
menuitem <'&Открыть…',9,'Ctrl+O'>,IDM_OPEN,0
menuitem <'&Сохранить',9,'Ctrl+S'>,IDM_SAVE,0,MFS_GRAYED
menuitem 'Сохранить &как…',IDM_SAVEAS,0,MFS_GRAYED
menuseparator
menuitem <'В&ыход',9,'Ctrl+Q'>,IDM_EXIT,MFR_END

accelerator main_keys, FVIRTKEY+FNOINVERT+FCONTROL,'N',IDM_NEW, FVIRTKEY+FNOINVERT+FCONTROL,'O',IDM_OPEN,
FVIRTKEY+FNOINVERT+FCONTROL,'S',IDM_SAVE, FVIRTKEY+FNOINVERT+FCONTROL,'Q',IDM_EXIT

icon main_icon,main_icon_data,'1.ico'

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

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

BITMAPFILEHEADER
ЭлементРазмерОписание
bfTypeWORDТип файла – символы "BM"
bfSizeDWORDРазмер файла в байтах
bfReserved1WORDЗарезервирован, содержит нули
bfReserved2WORDЗарезервирован, содержит нули
bfOffBitsDWORDСмещение в байтах от начала структуры BITMAPFILEHEADER до начала битовой карты изображения
BITMAPINFOHEADER
biSizeDWORDРазмер структуры в байтах
biWidthDWORDШирина изображения в пикселях
biHeightDWORDВысота изображения в пикселях
biPlanesWORDЧисло плоскостей, должно быть единицей
biBitCountWORDКоличество бит на пиксель
biCompressionDWORDТип сжатия для сжатых изображений
biSizeImageDWORDРазмер изображения в байтах
biXPelsPerMeterDWORDГоризонтальное разрешение в пикселях на метр
biYPelsPerMeterDWORDВертикальное разрешение в пикселях на метр
biClrUsedDWORDКоличество используемых цветов палитры
biClrImportantDWORDКоличество цветов, необходимых для отображения изображения
Палитра
У полноцветных изображений палитра может отсутствовать. При наличии палитры, она содержит массив из четырехбайтовых полей - по четыре байта на каждый цвет палитры. Первый из четырех байт определяет интенсивность синей составляющей цвета, второй – зеленой, третий – красной, четвертый байт не используется.
Битовая карта
Содержит массив битовых полей, каждое из которых описывает очередной пиксель. Размер битового поля зависит от цветности изображения. Например, в монохромном (черно-белом) изображении в одном байте можно описать восемь пикселей, так как для описания черной либо белой точки достаточно одного бита.


Итак, все это в общем, а теперь – некоторые пояснения. Во-первых, вы должны знать, что структура BITMAPINFOHEADER для повышения функциональности может быть заменена структурой BITMAPV4HEADER (поддерживается начиная с Widows 95 и NT 4.0) либо BITMAPV5HEADER (поддерживается начиная с Widows 98 и 2000). Однако подробности этих структур нам пока не понадобятся, так как в них добавлены всякие маски, гаммы и т.п., а нам бы с обычными BMP разобраться. Просто имейте это в виду, чтобы не испугались какого-либо несоответствия, когда начнете в шестнадцатеричном редакторе просматривать BMP-файлы для лучшего понимания формата.

Иногда вы можете встретить структуру BITMAPINFO. Она на самом деле является лишь совокупностью BITMAPINFOHEADER (либо BITMAPV4HEADER, либо BITMAPV5HEADER) и палитры. То есть, физически BITMAPINFO начинается с того же байта, что и BITMAPINFOHEADER – сразу после BITMAPFILEHEADER, но заканчивается там, где заканчивается палитра изображения или как раз перед началом битовой карты. Такое объединение двух структур придумали для того, чтобы в одной структуре можно было бы сразу получить всю информацию об изображении – и размеры, и используемые цвета.

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

Есть еще одна мелочь, которую надо запомнить. В зависимости от цветности изображения (количество бит на пиксель), один байт битовой карты может содержать информацию сразу о нескольких пикселях, об одном (8-битные изображения), а может быть и так, что каждый пиксель описывается в нескольких байтах (16-, 24-, 32-битные). Но каждая строка изображения должна иметь размер кратный четырем байтам, поэтому в конце строк битовой карты могут присутствовать выравнивающие ноли. Их задача – дополнить размер строки до кратного 32 битам. К слову, файлы, созданные в фотошопе, почему-то всегда содержат в конце каждого BMP еще два лишних нулевых байта, хотя они и не мешают нормальному открытию файла в других программах.

Напоследок определимся, как правильно находить смещения (относительные адреса) секций BMP-файла. BITMAPFILEHEADER, естественно, находится по нулевому смещению, так что искать его не придется: где начинается файл – там начинается BITMAPFILEHEADER. А где начинается BITMAPINFOHEADER? Там, где заканчивается BITMAPFILEHEADER. BITMAPINFOHEADER и BITMAPINFO находятся по адресу BITMAPFILEHEADER + размер структуры BITMAPFILEHEADER. Относительный адрес палитры отсчитывается от смещения BITMAPINFO и равен адресу BITMAPINFO + значение biSize (размер структуры
BITMAPINFOHEADER).

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

Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт sec.org/index.php?showtopic=766. q@sa-sec.org – ящик для ваших вопросов.

BarMentaLisk, SASecurity gr.


Компьютерная газета. Статья была опубликована в номере 09 за 2009 год в рубрике программирование

©1997-2024 Компьютерная газета