Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
25_11_Fasm__33__33__33__33.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
289.63 Кб
Скачать

Idi_main,lang_neutral,main_icon

resource icons,\

1,Lang_neutral,main_icon_data

resource versions,\

1,Lang_neutral,version

menu main_menu

menuitem '&Файл',0,MFR_POPUP

menuitem <'Созд&ать',9,'Ctrl+N'>,IDM_NEW,0,MFS_GRAYED

menuitem <'&Открыть…',9,'Ctrl+O'>,IDM_OPEN,0,MFS_GRAYED

menuitem <'&Сохранить',9,'Ctrl+S'>,IDM_SAVE,0,MFS_GRAYED

menuitem 'Сохранить &как…',IDM_SAVEAS,0,MFS_GRAYED

menuseparator

menuitem <'В&ыход',9,'Ctrl+Q'>,IDM_EXIT,MFR_END

menuitem '&Правка',0,MFR_POPUP

menuitem <'&Отменить',9,'Ctrl+Z'>,IDM_UNDO

menuseparator

menuitem <'&Вырезать',9,'Ctrl+X'>,IDM_CUT

menuitem <'&Копировать',9,'Ctrl+C'>,IDM_COPY

menuitem <'Вст&авить',9,'Ctrl+V'>,IDM_PASTE

menuitem <'&Удалить',9,'Del'>,IDM_DELETE

menuseparator

menuitem <'Выделить в&се',9,'Ctrl+A'>,IDM_SELECTALL,MFR_END

menuitem '&Вид',0

menuitem '&Справка',0,MFR_POPUP+MFR_END

menuitem '&О программе',IDM_ABOUT,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,\

FVIRTKEY+FNOINVERT+FCONTROL,'Z',IDM_UNDO,\

FVIRTKEY+FNOINVERT+FCONTROL,'X',IDM_CUT,\

FVIRTKEY+FNOINVERT+FCONTROL,'C',IDM_COPY,\

FVIRTKEY+FNOINVERT+FCONTROL,'V',IDM_PASTE,\

FVIRTKEY+FNOINVERT+FCONTROL,'A',IDM_SELECTALL

dialog about_dialog,'О программе',40,40,172,60,WS_CAPTION+WS_POPUP+WS_SYSMENU+DS_MODALFRAME,\

dialogitem 'STATIC',<'Мой Первый Текстовый Редактор',0Dh,0Ah,'Copyright ',0A9h,' BarMentaLisk 2008.'>,-

1,27,10,144,40,WS_VISIBLE+SS_CENTER

dialogitem 'STATIC',IDI_MAIN,-1,8,8,32,32,WS_VISIBLE+SS_ICON

dialogitem 'STATIC','',-1,4,34,164,11,WS_VISIBLE+SS_ETCHEDHORZ

dialogitem 'STATIC','Написан при помощи FASM',-1,12,42,100,20,WS_VISIBLE+SS_LEFT

dialogitem 'BUTTON','OK',IDOK,124,40,42,14,WS_VISIBLE+ WS_TABSTOP+BS_DEFPUSHBUTTON

enddialog

Icon main_icon,main_icon_data,'1.Ico'

versioninfo version,VOS_NT_WINDOWS32,VFT_APP,VFT2_UNKNOWN,LANG_RUSSIAN+ SUBLANG_DEFAULT,0,\

'Comments','Написан при помощи FASM',\

'CompanyName','BarMentaLisk',\

'FileDescription','Текстовый редактор',\

'ProductName',<'Мой Первый',0Dh,0Ah,'Текстовый Редактор'>,\

'LegalCopyright',<'Copyright ',0A9h, 'BarMentaLisk 2008'>,\

'FileVersion','0.1.0.0',\

'OriginalFilename','editor1.EXE'

Видали, сколько текста? Я бы на вашем месте не рискнул все это набирать вручную, а воспользовался бы электронной копией с форума. Хотя при вводе кода вручную он намного лучше понимается и запоминается. Действует такой же принцип, как и при конспектировании услышанного материала. В секции данных вам все должно быть понятно, кроме rb $-errtxt+10. У символа $ есть несколько значений в зависимости от контекста. К примеру, он может использоваться для обозначения шестнадцатеричного числа, как в паскале: $FACE8D. Он также является специальным символом, обозначающим текущее относительное смещение. Подробно о смещениях я планирую рассказать вам в следующих статьях, но сейчас тем из вас, кто вообще не имеет представления о смещениях, скажу "по-колхозному": смещение — это номер байта в программе, причем отсчет начинается с нулевого байта, затем идет первый, второй и так далее. Наши данные укладываются в секцию данных друг за дружкой в каждый байт. Мы знаем, что title является указателем на первый символ строки заголовка, а class — на первый символ строки класса, причем class идет следом за title. Значит, class-title=30, потому что ровно 30 байт занимают данные title (29 символов + завершающий ноль). Под буфер errbuf необходимо выделить ровно на 10 байт больше памяти, чем занимает строка errtxt. В таком случае мы могли бы использовать инструкцию rb 15+10 или rb 25. Но тогда каждый раз при изменении длины строки придется изменять и размер буфера. А если подобных строк и буферов много, то очень легко запутаться. Так что, если размер буфера относителен, то надежнее выражать его таким образом. $ — это относительный номер текущего байта — значит, $-errtxt (при условии, что выражение следует сразу за errtxt) при компиляции будет всегда замещаться размером этой строки в байтах. Такие вот дела. Напоминаю, что LoadIcon загружает указанную иконку из ресурсов исполняемого файла. Первый параметр — идентификатор исполняемого файла или ноль для загрузки стандартной иконки. Второй параметр — строка — имя иконки из ресурсов либо идентификатор стандартной иконки, если первый параметр ноль. Теперь мы указываем наше приложение в качестве первого параметра и идентификатор нашей иконки (для удобства используется псевдоним IDI_MAIN=401) в качестве второго. Курсор оставляем стандартный. Меню и акселераторы ставим свои. Акселераторы — это горячие клавиши или комбинации клавиш для быстрого выполнения тех или иных действий в программе. Они также являются одним из типов ресурсов, тип — RT_ACCELERATOR. Описываются макроинструкцией accelerator, за которой после имени ресурса тройками через запятую следуют параметры: флаги акселератора; код виртуальной клавиши или символ ASCII; идентификатор. Список флагов (Accelerator flags) и кодов виртуальных клавиш (Virtual key codes), как обычно, смотрим в EQUATES\USER32.INC. Атрибуты FALT, FCONTROL и FSHIFT означают, что вместе с указанной клавишей нажаты Alt, Ctrl, Shift или их комбинация. Атрибут FVIRTKEY означает, что событие происходит при нажатии соответствующей виртуальной клавиши (символ клавиши в кавычках должен быть в верхнем регистре). При отсутствии этого атрибута горячей клавишей будет считаться указанный символ — в этом случае Alt, Ctrl, Shift не учитываются, а также клавиша получится зависимой от регистра и раскладки. FNOINVERT означает, что выбранный при помощи акселератора пункт не будет подсвечиваться — этот атрибут является устаревшим и используется лишь для обратной совместимости с 16-битными версиями Windows. Функция LoadAccelerators используется аналогично функции LoadMenu, только возвращаемое ею значение — дескриптор загруженной таблицы акселераторов. В цикл обработки сообщений включена функция TranslateAccelerator. Она обрабатывает сообщения о нажатии горячих клавиш — акселераторов нашего меню, переводя их в сообщения WM_COMMAND или WM_SYSCOMMAND. Чтобы переведенные функцией сообщения можно было отличить от сообщений меню или элементов управления, старший байт параметра wparam сообщения устанавливается в единицу (табл. 1). Параметры: дескриптор обрабатываемого окна; дескриптор таблицы акселераторов; указатель на структуру MSG. При ошибке функция возвращает ноль. Если результат отличен от нуля — значит, сообщение обработано и не нуждается в дополнительной обработке функцией TranslateMessage.

Сообщение от:

wParam (старшее слово)

wParam (младшее слово)

lParam

Меню

0

идентификатор меню (IDM_*)

0

Акселератора

1

идентификатор акселератора(IDM_*)

0

Элемента управления

событие

идентификатор элемента

дескриптор элемента (handle)

Для дополнительной обработки ошибок использована функция GetLastError. Эта функция возвращает код последней ошибки (если была ошибка), произошедшей при вызове API-Функции. При помощи функции wsprintf мы приводим код ошибки к символьному виду и выводим на экран функцией MessageBox. Список кодов ошибок можно найти в MSDN или в каком-либо справочнике по Windows, хотя даже у самой Microsoft отсутствует полный список кодов ошибок. Обработчики сообщений меню файл за исключением пункта Выход в ресурсах установлены как недоступные (MFS_GRAYED), но на случай их разблокировки каким-нибудь взломщиком обрабатываются выводом сообщения о недоступности функции. Обработку этих пунктов мы добавим и изучим в следующей статье. Выход обрабатывается вызовом функции DestroyWindow. Эта функция предназначена для корректного уничтожения окна со всеми его элементами, зависимыми окнами. Она посылает окну сообщения WM_DESTROY и WM_NCDESTROY. Единственный ее параметр — дескриптор уничтожаемого окна. При обработке пунктов "отменить", "вырезать", "копировать", "вставить", "удалить" производится отправка соответствующего сообщения окну редактирования. Параметры всех сообщений, кроме EM_SETSEL (выделение текста), не используются и должны быть нулями. Параметры EM_SETSEL: номер символа начала выделения; номер символа конца выделения. Если первый параметр равен нулю, а второй — минус единице, то выделяется весь текст в окне. Если первый параметр равен минус единице — все выделения снимаются. О бработчик пункта О программе вызывает функцию DialogBoxParam. Эта функция создает диалоговое окно (dialog box) — окно, разработанное специально для быстрого и удобного создания окон с дочерними элементами. Все его элементы описываются в одном блоке ресурсов и автоматически создаются средствами операционной системы при создании окна. Параметры: идентификатор исполняемого модуля программы; идентификатор ресурса, содержащего шаблон диалогового окна; дескриптор окна-владельца; указатель на процедуру диалогового окна; значение, передаваемое диалоговому окну в параметре lParam сообщения WM_INITDIALOG. Описание элементов окна в шаблоне интуитивно понятно и схоже с параметрами функции CreateWindowEx. Процедура диалогового окна аналогична процедуре обычного окна. Более подробно о диалоговых окнах мы поговорим в одной из следующих статей. Функция GetClientRect возвращает координаты клиентской области указанного окна в указанную структуру RECT. Параметры: дескриптор окна; указатель на структуру RECT. Причем в элементы структуры left и top возвращаются нули, а в элементы right и bottom — соответственно ширина и высота клиентской области окна. Эти данные необходимы нам, чтобы правильно задать размеры окна для редактирования. Нижеследующие команды имеют смысл лишь при успешном создании окна, поэтому, если в eax возвращается ноль (ошибка), мы пропускаем остальные команды. CreateFont создает или изменяет шрифт с указанными параметрами: высота шрифта, ноль для высоты по умолчанию; ширина шрифта, ноль для оптимальной пропорции; угол наклона текста в десятых долях градуса; угол наклона символов в десятых долях градуса; жирность шрифта (0-1000, 0 — по умолчанию, 400 — нормальный, 700 — жирный); false либо true = 0 либо 1 = обычный либо курсив; обычный либо подчеркнутый; обычный либо зачеркнутый; набор символов — например, ANSI_CHARSET или DEFAULT_CHARSET (список ищем в INCLUDE\EQUATES\GDI32.INC); флаг степени соответствия шрифта заданным параметрам — например, OUT_DEFAULT_PRECIS (список ищем там же); флаг степени сглаживания шрифта — например, CLIP_DEFAULT_PRECIS (список там же); флаг качества шрифта — например, DEFAULT_QUALITY (список там же); сдвиг и семейство шрифта — например, DEFAULT_PITCH+FF_DONTCARE; указатель на строку с названием шрифта или ноль для оптимального выбора. Возвращаемое в eax значение — дескриптор созданного шрифта либо ноль при ошибке. Сообщение WM_SETFONT устанавливает указанный шрифт. Параметры: дескриптор шрифта; флаг перерисовки окна с новым шрифтом (0 либо 1). При изменении размеров основного окна (сообщение WM_SIZE) необходимо изменять размеры и дочернего окна редактирования. Для этого мы снова узнаем размер главного окна и перемещаем дочернее — MoveWindow. Эта функция изменяет позицию окна и его размеры. Параметры: дескриптор окна; позиция X; позиция Y; ширина; высота; перерисовки окна (0 либо 1). На сегодня, думаю, хватит. Продолжение, естественно, следует.

Заждались, наверное, продолжения? Что ж, ожидание только усиливает радость встречи. На сегодня у нас запланирован выход бета-версии 0.2 нашего первого текстового редактора. Он, к сожалению, по возможностям пока что немного уступает даже стандартному "Блокноту", но это ведь только во благо вашему обучению. В дальнейшем у вас будет шанс для усовершенствования программы в меру своих возможностей и, самое главное, знаний. Вы же не ждете, что я всю жизнь буду программировать за вас?  Надеюсь, что нет, потому что основная цель этих занятий — развить интерес к программированию на ассемблере под Windows и изучить его основные моменты. Ну а время покажет, кому что ближе. Кого-то потянет в низкоуровневое программирование, кто-то, привязавшись к макросам, подастся в высокий уровень, забросив ассемблер… Но это все в будущем. А сейчас мы открываем привычный для нас FASMW.EXE и вводим код, электронную копию которого, как обычно, можно скачать на форуме. Код программы частично соответствует коду, приведенному в 6-й части цикла, но только частично. Так что если решите сэкономить время и скопировать набранные куски из прошлой версии — будьте предельно внимательны и сверяйте каждую строчку. format PE GUI 4.0 entry start include 'win32a.inc' include 'encoding\WIN1251.INC' MAXSIZE equ 260 ; максимальное имя файла в байтах MEMSIZE equ 65537 ; размер временного буфера section '.data' data readable writeable title db 'Мой Первый Текстовый Редактор',0 class db 'FASMWIN32',0 edit db 'EDIT',0 saveq db 'Сохранить изменения ?',0 filter db 'Text Files',0,'*.txt',0 db 'All formats',0,'*.txt;*.asm;*.inc;*.ini',0 db 'All files',0,'*.*',0,0 fnsaved db 0 ;флаг имени файла 1=сохранено, 0=нет errtxt db 'Код ошибки: %u',0 errbuf rb $-errtxt+10 fname rb MAXSIZE hfile dd ? hheap dd ? pmem dd ? sbuf dd ? hwnd dd ? hmenu dd ? hedit dd ? hacc dd ? font dd ? 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 msg MSG client RECT menuinfo MENUITEMINFO sizeof.MENUITEMINFO,MIIM_STATE 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] proc WindowProc hwnd,wmsg,wparam,lparam push ebx esi edi cmp [wmsg],WM_COMMAND je .wmcommand cmp [wmsg],WM_CREATE je .wmcreate cmp [wmsg],WM_SIZE je .wmsize cmp [wmsg],WM_SETFOCUS je .wmsetfocus 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_NEW je .NEW cmp ax,IDM_OPEN je .OPEN cmp ax,IDM_SAVE je .SAVE cmp ax,IDM_SAVEAS je .SAVEAS cmp ax,IDM_EXIT je .EXIT cmp ax,IDM_UNDO je .UNDO cmp ax,IDM_CUT je .CUT cmp ax,IDM_COPY je .COPY cmp ax,IDM_PASTE je .PASTE cmp ax,IDM_DELETE je .DELETE cmp ax,IDM_SELECTALL je .SELECTALL cmp ax,IDM_ABOUT je .ABOUT jmp .finish ; обработчики сообщений меню файл: .NEW: call get_modified invoke SendMessage,[hedit],WM_SETTEXT,0,0 mov [fnsaved],0 jmp .finish .OPEN: call get_modified call open_file jmp .finish .SAVE: call save_file jmp .finish .SAVEAS: call get_save jmp .finish .EXIT: call get_modified invoke DestroyWindow,[hwnd] jmp .finish ; обработчики сообщений меню правка: .UNDO: mov eax,EM_UNDO jmp .send2editbox .CUT: mov eax,WM_CUT jmp .send2editbox .COPY: mov eax,WM_COPY jmp .send2editbox .PASTE: mov eax,WM_PASTE jmp .send2editbox .DELETE: mov eax,WM_CLEAR .send2editbox: invoke SendMessage,[hedit],eax,0,0 jmp .finish .SELECTALL: invoke SendMessage,[hedit],EM_SETSEL,0,-1 jmp .finish .ABOUT: invoke DialogBoxParam,[wc.hInstance],IDD_ABOUT,[hwnd],AboutDialog,0 jmp .finish .wmcreate: invoke GetClientRect,[hwnd],client invoke CreateWindowEx,WS_EX_CLIENTEDGE,edit,0,WS_VISIBLE+WS_CHILD+WS_HSCROLL+ WS_VSCROLL+ES_AUTOHSCROLL+ES_AUTOVSCROLL+ES_MULTILINE,[client.left], [client.top], [client.right],[client.bottom],[hwnd],0,[wc.hInstance],NULL cmp eax,0 je .failed mov [hedit],eax invoke SendMessage,[hedit],EM_LIMITTEXT,MEMSIZE-1,0 invoke CreateFont,16,0,0,0,0,FALSE,FALSE,FALSE,RUSSIAN_CHARSET,OUT_RASTER_PRECIS, CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,FIXED_PITCH+FF_DONTCARE, NULL cmp eax,0 je .failed mov [font],eax invoke SendMessage,[hedit],WM_SETFONT,eax,FALSE mov eax,0 jmp .finish .failed: mov eax,-1 jmp .finish .wmsize: invoke GetClientRect,[hwnd],client invoke MoveWindow,[hedit],[client.left],[client.top],[client.right],[client.bottom],TRUE mov eax,0 jmp .finish .wmsetfocus: invoke SetFocus,[hedit] mov eax,0 jmp .finish .wmdestroy: invoke PostQuitMessage,0 mov eax,0 .finish: pop edi esi ebx ret get_modified: invoke SendMessage,[hedit],EM_GETMODIFY,0,0 cmp eax,0 je .not_modified invoke MessageBox,[hwnd],saveq,title,MB_YESNO + MB_ICONWARNING cmp eax,IDYES jne .not_modified call save_file .not_modified: retn save_file: cmp [fnsaved],1 je create_file get_save: mov [ofn.Flags],OFN_EXPLORER + OFN_OVERWRITEPROMPT invoke GetSaveFileName,ofn cmp eax,0 je failed create_file: invoke CreateFile,fname,GENERIC_READ + GENERIC_WRITE,FILE_SHARE_READ + FILE_SHARE_WRITE,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0 mov [hfile],eax invoke GetProcessHeap mov [hheap],eax invoke HeapAlloc,[hheap],HEAP_ZERO_MEMORY,MEMSIZE mov [pmem],eax invoke SendMessage,[hedit],WM_GETTEXT,MEMSIZE,[pmem] invoke lstrlen,[pmem] invoke WriteFile,[hfile],[pmem],eax,sbuf,0 mov [fnsaved],1 invoke SendMessage,[hedit],EM_SETMODIFY,0,0 invoke HeapFree,[hheap],0,[pmem] invoke CloseHandle,[hfile] retn open_file: mov [ofn.Flags], OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST + OFN_EXPLORER invoke GetOpenFileName,ofn cmp eax,0 je failed invoke CreateFile,fname,GENERIC_READ + GENERIC_WRITE,FILE_SHARE_READ + FILE_SHARE_WRITE,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0 mov [hfile],eax invoke GetProcessHeap mov [hheap],eax invoke HeapAlloc,[hheap],HEAP_ZERO_MEMORY,MEMSIZE mov [pmem],eax invoke ReadFile,[hfile],[pmem],MEMSIZE-1,sbuf,0 invoke SendMessage,[hedit],WM_SETTEXT,0,[pmem] mov [fnsaved],1 invoke HeapFree,[hheap],0,[pmem] invoke CloseHandle,[hfile] retn failed: retn endp proc AboutDialog hwnd,msg,wparam,lparam push ebx esi edi cmp [msg],WM_COMMAND je .close cmp [msg],WM_CLOSE je .close mov eax,0 jmp .finish .close: invoke EndDialog,[hwnd],0 .processed: mov eax,1 .finish: pop edi esi ebx 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 IDM_UNDO = 1201 IDM_CUT = 1202 IDM_COPY = 1203 IDM_PASTE = 1204 IDM_DELETE = 1205 IDM_SELECTALL = 1206 IDM_ABOUT = 1401 directory RT_MENU,menus,\ RT_ACCELERATOR,accelerators,\ RT_DIALOG,dialogs,\ RT_GROUP_ICON,group_icons,\ RT_ICON,icons,\ RT_VERSION,versions resource menus,\ IDM_MAIN,LANG_RUSSIAN+SUBLANG_DEFAULT,main_menu resource accelerators,\ IDA_MAIN,LANG_ENGLISH+SUBLANG_DEFAULT,main_keys resource dialogs,\ IDD_ABOUT,LANG_RUSSIAN+SUBLANG_DEFAULT,about_dialog resource group_icons,\ IDI_MAIN,LANG_NEUTRAL,main_icon resource icons,\ 1,LANG_NEUTRAL,main_icon_data resource versions,\ 1,LANG_NEUTRAL,version menu main_menu menuitem '&Файл',0,MFR_POPUP menuitem <'Созд&ать',9,'Ctrl+N'>,IDM_NEW,0 menuitem <'&Открыть…',9,'Ctrl+O'>,IDM_OPEN,0 menuitem <'&Сохранить',9,'Ctrl+S'>,IDM_SAVE,0 menuitem 'Сохранить &как…',IDM_SAVEAS,0 menuseparator menuitem <'В&ыход',9,'Ctrl+Q'>,IDM_EXIT,MFR_END menuitem '&Правка',0,MFR_POPUP menuitem <'&Отменить',9,'Ctrl+Z'>,IDM_UNDO menuseparator menuitem <'&Вырезать',9,'Ctrl+X'>,IDM_CUT menuitem <'&Копировать',9,'Ctrl+C'>,IDM_COPY menuitem <'Вст&авить',9,'Ctrl+V'>,IDM_PASTE menuitem <'&Удалить',9,'Del'>,IDM_DELETE menuseparator menuitem <'Выделить в&се',9,'Ctrl+A'>,IDM_SELECTALL,MFR_END menuitem '&Вид',0 menuitem '&Справка',0,MFR_POPUP+MFR_END menuitem '&О программе',IDM_ABOUT,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,\ FVIRTKEY+FNOINVERT+FCONTROL,'Z',IDM_UNDO,\ FVIRTKEY+FNOINVERT+FCONTROL,'X',IDM_CUT,\ FVIRTKEY+FNOINVERT+FCONTROL,'C',IDM_COPY,\ FVIRTKEY+FNOINVERT+FCONTROL,'V',IDM_PASTE,\ FVIRTKEY+FNOINVERT+FCONTROL,'A',IDM_SELECTALL dialog about_dialog,'О программе',40,40,172,60,WS_CAPTION+WS_POPUP+WS_SYSMENU+DS_MODALFRAME dialogitem 'STATIC',<'Мой Первый Текстовый Редактор',0Dh,0Ah,'Copyright ',0A9h,' BarMentaLisk 2008.'>,- 1,27,10,144,40,WS_VISIBLE+SS_CENTER dialogitem 'STATIC',IDI_MAIN,-1,8,8,32,32,WS_VISIBLE+SS_ICON dialogitem 'STATIC','',-1,4,34,164,11,WS_VISIBLE+SS_ETCHEDHORZ dialogitem 'STATIC','Написан при помощи FASM',-1,12,42,100,20,WS_VISIBLE+SS_LEFT dialogitem 'BUTTON','OK',IDOK,124,40,42,14,WS_VISIBLE+WS_TABSTOP+ BS_DEFPUSHBUTTON enddialog icon main_icon,main_icon_data,'1.ico' versioninfo version,VOS_NT_WINDOWS32,VFT_APP,VFT2_UNKNOWN, LANG_RUSSIAN+SUBLANG_DEFAULT,0,\ 'Comments','Написан при помощи FASM',\ 'CompanyName','BarMentaLisk',\ 'FileDescription','Текстовый редактор',\ 'ProductName',<'Мой Первый',0Dh,0Ah,'Текстовый Редактор'>,\ 'LegalCopyright',<'Copyright ',0A9h, 'BarMentaLisk 2008'>,\ 'FileVersion','0.2.0.0',\ 'OriginalFilename','editor1.EXE' В самом начале объявляются две константы: MAXSIZE и MEMSIZE. Первая — это максимальное количество байт под полный путь и имя файла, вторая — размер памяти, выделяемой под буфер для текста, включая завершающий строку ноль. В данной ситуации максимальное количество символов в тексте у нас не должно превышать 65 536 (64 Кб). Памяти выделяется на один байт больше, но в окне редактирования и в файле на жестком диске завершающий ноль записан не будет. При необходимости вы всегда можете изменить данную константу по своему усмотрению в пределах от 2 до 2 147 483 647 (7FFFFFFFh). Потому что максимальное количество символов, которое может вместить стандартный элемент EDIT = 7FFFFFFEh, а минимальное = 1. В секции данных мало нового. Фильтр допустимых файлов (filter) состоит из одной или нескольких пар строк, завершающихся нулем. Каждая пара — это отображаемое имя фильтра и собственно сам фильтр. Последняя пара должна завершаться двумя нулями. Переменную fnsaved мы будем использовать для того, чтобы определять, сохранено ли текущее имя файла в переменной fname. Еще у нас появилась структура OPENFILENAME. Список ее элементов вы можете увидеть в \INCLUDE\EQUATES\ COMDLG32.INC. Чтобы не париться с нудным описанием каждого элемента, опишу только те, которые могут нам понадобиться в ближайшем будущем, тем более что последние 11 элементов я даже не инициализировал какими-либо значениями (последние элементы структуры обычно автоматически инициализируются нулями, если опущены). Хотя по правилам хорошего тона необходимо было бы дописать после MAXSIZE еще 11 ноликов через запятую, но это уж кому как нравится. lStructSize — размер структуры в байтах; hwndOwner — дескриптор окна-владельца диалогового окна открытия файла; hInstance — дескриптор исполняемого модуля; lpstrFilter — указатель на фильтр; lpstrCustomFilter — указатель на пользовательский фильтр; nMaxCustFilter — размер пользовательского фильтра; nFilterIndex — номер фильтра, выбираемого по умолчанию; lpstrFile — указатель на полное имя файла; nMaxFile — размер буфера для полного имени файла; lpstrFileTitle — короткое имя файла (только имя, без пути к файлу); nMaxFileTitle — размер буфера для короткого имени; lpstrInitialDir — открываемая по умолчанию директория; lpstrTitle — заголовок диалогового окна открытия/сохранения файла. Переходим к секции кода. Начало похоже на предыдущую версию, только надо не забыть скопировать дескриптор исполняемого модуля еще и в ofn.hInstance. Затем добавляем обработку сообщения WM_CLOSE, чтобы по команде закрытия можно было проверить, изменилось ли содержимое окна, и предложить сохранение, если изменилось. Обработчики сообщений от элементов меню "Файл" теперь доступны (параметры MFS_GRAYED сняты с этих элементов в ресурсе menu) и готовы обрабатывать свои сообщения. При выборе пункта "Создать" (IDM_NEW -> .NEW), происходит вызов подпрограммы (метка get_modified). Команда call (от англ. call — вызывать) очень похожа на команду jmp. Только команда call сохраняет при этом в стек адрес возврата — адрес следующей за call команды, чтобы потом команда ret (от англ. return — возвращаться) могла вернуть управление на команду, следующую за call. Пара команд call — ret используется для возможности вызова какой-либо подпрограммы из разных мест программы. Например, макрос invoke, к которому мы уже так привыкли, тоже использует команду call, только предварительно запихивает в стек параметры вызываемой функции. На "том конце провода" функция API выполняет необходимые операции и при помощи команды ret возвращает управление следующей команде нашей программы. Сложно? Ерунда! Это я вам еще только в общих чертах обрисовал картину. Ну да ладно, не стоит на этом загоняться — это уже почти хакерский уровень, а вы пока что как бы "чайники". Запомните пока просто, что call вызывает, а ret возвращает. Вот и в нашей ситуации выполнение команд временно перескакивает на метку get_modified. Там посылкой сообщения EM_GETMODIFY нашему окну редактирования мы узнаем, был ли изменен текст окна: если изменен, вернется единица, иначе — ноль. Параметры у сообщения отсутствуют, а потому выставляются в ноли. Если изменений нет, то значит и сохранять нечего — возвращаемся. Retn (Return Near) — это тот же ret, только для близких возвратов — мы же находимся внутри процедуры WindowProc. Если же изменения имели место, то переспрашиваем у пользователя, следует ли их сохранять. Если следует (IDYES), то вызываем save_file, нет — возвращаемся. На метке save_file проверяется, сохранен ли путь к нашему файлу (мы сами будем устанавливать переменную fnsaved в единицу, когда путь сохранен или файл открыт, и в ноль при создании нового файла, путь которого еще не определен). Если путь сохранен, сразу переходим к метке create_file, если нет — вызываем диалог сохранения файла функцией GetSaveFileName. Эта функция имеет лишь один параметр (но зато какой!) — указатель на огромную структуру OPENFILENAME. Предварительно устанавливаем флаги OFN_EXPLORER для открытия диалога в стиле проводника windows и OFN_OVERWRITEPROMPT для подтверждения перезаписи файла, если он существует. GetSaveFileName возвращает ноль в случае, если пользователь отменил сохранение. Иначе в буферы помещается имя файла, а возвращаемое значение отлично от ноля. Таким образом, на метке create_file, в fname содержится полное имя файла. Функция CreateFile создает или открывает объект ввода/вывода. Параметры: 1. Имя объекта. 2. Права доступа: GENERIC_READ — чтение, GENERIC_WRITE — запись или оба сразу. 3. Права на совместный одновременный доступ к файлу несколькими процессами: 0 — доступ другим процессам запрещен до закрытия файла, FILE_SHARE_READ — разрешено чтение, FILE_SHARE_WRITE — разрешена запись, FILE_SHARE_DELETE — разрешено удаление. 4. Атрибуты безопасности: 0 для значений по умолчанию. 5. Способ открытия файла: CREATE_NEW — создать новый файл, ошибка, если файл существует, CREATE_ALWAYS — создать новый файл, перезаписывает старый, если файл существует, OPEN_EXISTING — открыть файл, ошибка, если файл не существует, OPEN_ALWAYS — открыть файл, если файл не существует, он будет создан, TRUNCATE_EXISTING — открыть файл и очистить его содержимое, ошибка, если файл не существует. 6. Набор атрибутов (скрытый, системный и т.д.). 7. Файл-шаблон атрибутов. При успешном создании/открытии файла возвращается его дескриптор, если файл отсутствует — возвращается 0, в случае ошибки возвращается -1. Функция GetProcessHeap возвращает дескриптор кучи вызывающего функцию процесса. Куча — это область виртуальной памяти, под которую не выделяется реальная физическая память, но по мере заполнения кучи данными диспетчер, управляющий кучами (heap manager), выделяет под нее физическую память. Функция HeapAlloc выделяет блок памяти в куче и возвращает указатель на выделенный блок. Параметры: дескриптор кучи; флаги способа выделения памяти (HEAP_ZERO_MEMORY — проинициализировать блок нулевыми значениями); размер выделяемого блока в байтах. После выделения блока мы копируем текст, завершающийся нолем, из окна редактирования в этот блок, отсылкой окну редактирования сообщения WM_GETTEXT. Функция lstrlen (параметр — адрес строки) возвращает длину строки в символах без учета завершающего ноля. Функция WriteFile записывает данные в файл. Ее параметры: дескриптор файла; указатель на данные; размер данных в байтах; буфер для ответа, сколько байт удалось записать; указатель на структуру OVERLAPPED для дополнительных сведений. В случае ошибки возвращается ноль. Записываем в fnsaved единицу — файл сохранен и его имя хранится в переменной fname. Отсылаем окну редактирования сообщение EM_SETMODIFY — первый параметр 0 означает, что содержимое окна не изменялось, второй параметр не используется. Функция HeapFree освобождает указанный блок памяти в куче. Параметры: дескриптор кучи; флаги; указатель на блок памяти. Функция CloseHandle закрывает объект (в нашем случае — открытый файл), чей дескриптор указан в единственном параметре. Теперь возвращаемся к следующей после call команде. При создании файла после вызова подпрограммы get_modified мы очищаем содержимое окна редактирования и устанавливаем fnsaved в ноль, так как имя нового файла пока неизвестно. При открытии файла мы опять же вызываем подпрограмму сохранения старого файла (get_modified), а затем вызываем подпрограмму открытия (open_file). Там все почти аналогично подпрограмме get_modified. Устанавливаем флаги OFN_FILEMUSTEXIST (открываемый файл должен существовать), OFN_PATHMUSTEXIST (открываемый путь к файлу должен существовать) и OFN_EXPLORER. Функция GetOpenFileName вызывает диалог открытия файла и возвращает его имя в буфер fname. Открываем выбранный файл функцией CreateFile. Выделяем память. Функция ReadFile аналогична функции WriteFile, только не пишет данные в файл, а читает их. Размер данных для чтения указываем MEMSIZE-1, потому что последний байт в памяти необходимо оставить нулевым — это завершающий строку ноль. Устанавливаем прочитанные данные текстом окна редактирования (WM_SETTEXT). Задвигаем в fnsaved единицу — файл открыт и его имя нам известно. Освобождаем память. Закрываем файл. При выборе пункта "Сохранить" подпрограмма сохранения вызывается с метки save_file, дабы избежать вопроса о необходимости сохранения. При выборе пункта "Сохранить как…" вызываем сохранение с метки выбора файла для сохранения. Выбор пункта "Выход" или закрытие программы приведет к выполнению get_modified и вызову функции DestroyWindow с дескриптором окна в качестве единственного параметра. На метке failed ничего не происходит — возврат без выполнения каких-либо действий. Здесь вы самостоятельно можете добавить дополнительную обработку ошибок и вывод соответствующего сообщения. Также можете на свое усмотрение внести проверку успешного завершения после каждой важной функции подпрограмм чтения и записи файла. Согласен, что некоторые моменты разъяснены не так подробно, как хотелось бы, но тут я уповаю на вашу сообразительность. Если что-то непонятно — возможно стоит еще раз прочесть предыдущие уроки. В любом случае не падайте духом и не теряйте уверенности в своих силах и возможностях.

В шестой части я обещал вам подробнее рассказать о применении диалоговых окон. О них сегодня и пойдет речь. Напишем простейший шифровальщик данных на основе диалогового окна. Прошу заметить, что "простейший" не всегда означает "быстрейший". Ввиду этого наша сегодняшняя программа вряд ли претендует на роль в криптографии огромных массивов или видеофайлов, однако, думаю, вполне справится с шифрованием небольших текстовых документов с паролями и секретными данными на ожидаемом уровне. include 'win32ax.inc' include 'encoding\WIN1251.INC' MAXSIZE = 260 PASSIZE = 8 .data ernopass db 'Введите пароль!',0 erdifpass db 'Пароль и подтверждение не совпадают!',0 erfsize db 'Файл слишком большой!',0 erfname db 'Файл не выбран!',0 bigfile db 'Файл довольно большой!',13,10 db 'Шифрование может занять много времени.',0 success db 'Успешно завершено!',0 extname db '.crpt',0 errtxt db 'Код ошибки: %u',0 errbuf rb $-errtxt+10 fname1 rb MAXSIZE fname2 rb MAXSIZE pass1 rb PASSIZE+1 pass2 rb PASSIZE+1 buf db ? passlen dd ? hfile1 dd ? hfile2 dd ? temp dd ? ofn OPENFILENAME sizeof.OPENFILENAME,,,,,,,fname1,MAXSIZE,,,,, OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST + OFN_EXPLORER .code start: invoke GetModuleHandle,0 mov [ofn.hInstance],eax invoke DialogBoxParam,eax,IDD_MAIN,HWND_DESKTOP,DialogProc,0 invoke ExitProcess,0 proc DialogProc hwnddlg,msg,wparam,lparam push ebx esi edi cmp [msg],WM_INITDIALOG je .wminitdialog cmp [msg],WM_COMMAND je .wmcommand cmp [msg],WM_DROPFILES je .drop cmp [msg],WM_CLOSE je .wmclose xor eax,eax jmp .finish .wminitdialog: mov eax,[hwnddlg] mov [ofn.hwndOwner],eax invoke SendDlgItemMessage,[hwnddlg],ID_PASS1,EM_LIMITTEXT,PASSIZE,0 invoke SendDlgItemMessage,[hwnddlg],ID_PASS2,EM_LIMITTEXT,PASSIZE,0 jmp .processed .wmcommand: cmp [wparam],ID_OPEN je .open cmp [wparam],ID_EXIT je .wmclose cmp [wparam],ID_CRYPT jne .finish ;=crypt= cmp [fname1],0 je .erfname invoke GetDlgItemText,[hwnddlg],ID_PASS1,pass1,PASSIZE+1 cmp [pass1],0 je .ernopass invoke GetDlgItemText,[hwnddlg],ID_PASS2,pass2,PASSIZE+1 invoke lstrcmp,pass1,pass2 test eax,eax jne .erdifpass invoke lstrlen,pass1 mov [passlen],eax invoke CreateFile,fname1,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,0 cmp eax,-1 je .erfname mov [hfile1],eax invoke GetFileSize,eax,0 cmp eax,-1 je .erfilesize test eax,eax je .erfilesize mov ebx,eax cmp eax,500000h ;5MB jb .size_ok invoke MessageBox,[hwnddlg],bigfile,0,MB_OKCANCEL cmp eax,IDOK jne .hclose .size_ok: invoke lstrcpy,fname2,fname1 invoke lstrcat,fname2,extname test eax,eax je .erfname invoke CreateFile,fname2,GENERIC_WRITE,FILE_SHARE_READ,0,CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,0 cmp eax,-1 je .erfname mov [hfile2],eax .crypt_cycle: invoke ReadFile,[hfile1],buf,1,temp,0 test eax,eax je .error xor edx,edx mov eax,ebx div [passlen] add edx,pass1 mov al,[edx] xor [buf],al invoke WriteFile,[hfile2],buf,1,temp,0 test eax,eax je .error dec ebx test ebx,ebx jne .crypt_cycle invoke CloseHandle,[hfile1] invoke CloseHandle,[hfile2] invoke SetFileAttributes,fname1,FILE_ATTRIBUTE_NORMAL invoke DeleteFile,fname1 test eax,eax je .error invoke MoveFile,fname2,fname1 test eax,eax je .error invoke MessageBox,[hwnddlg],success,0,0 jmp .processed .ernopass: invoke MessageBox,[hwnddlg],ernopass,0,0 jmp .processed .erdifpass: invoke MessageBox,[hwnddlg],erdifpass,0,0 jmp .processed .erfname: invoke MessageBox,[hwnddlg],erfname,0,0 jmp .processed .erfilesize: invoke MessageBox,[hwnddlg],erfsize,0,0 jmp .hclose .error: invoke GetLastError invoke wsprintf,errbuf,errtxt,eax invoke MessageBox,[hwnddlg],errbuf,0,0 .hclose: invoke CloseHandle,[hfile1] invoke CloseHandle,[hfile2] jmp .processed .drop: invoke DragQueryFile,[wparam],0,fname1,MAXSIZE jmp .new_file .open: invoke GetOpenFileName,ofn test eax,eax je .processed .new_file: invoke SetDlgItemText,[hwnddlg],ID_NAMESTRING,fname1 jmp .processed .wmclose: invoke EndDialog,[hwnddlg],0 .processed: mov eax,1 .finish: pop edi esi ebx ret endp .end start section '.rsrc' resource data readable IDD_MAIN = 100 ID_NAMESTRING = 101 ID_PASS1 = 102 ID_PASS2 = 103 ID_OPEN = 104 ID_CRYPT = 105 ID_EXIT = 109 directory RT_DIALOG,dialogs resource dialogs,\ IDD_MAIN,LANG_RUSSIAN+SUBLANG_DEFAULT,main_dialog dialog main_dialog,'Простейший Шифровальщик',240,120,190,175,WS_CAPTION+WS_POPUP+WS_SYSMENU+ DS_MODALFRAME,WS_EX_ACCEPTFILES dialogitem 'STATIC','&Файл:',-1,10,10,70,8,WS_VISIBLE dialogitem 'EDIT','',ID_NAMESTRING,10,20,170,13,WS_VISIBLE+WS_BORDER+ ES_AUTOHSCROLL+ES_READONLY dialogitem 'STATIC','&Пароль:',-1,10,50,70,8,WS_VISIBLE dialogitem 'EDIT','',ID_PASS1,10,60,60,13,WS_VISIBLE+WS_BORDER+ WS_TABSTOP+ES_AUTOHSCROLL dialogitem 'STATIC','&Подтверждение:',-1,10,80,70,8,WS_VISIBLE dialogitem 'EDIT','',ID_PASS2,10,90,60,13,WS_VISIBLE+WS_BORDER+ WS_TABSTOP+ES_AUTOHSCROLL dialogitem 'BUTTON','&Открыть',ID_OPEN,10,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_DEFPUSHBUTTON dialogitem 'BUTTON','&Шифровать',ID_CRYPT,85,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_DEFPUSHBUTTON dialogitem 'BUTTON','В&ыход',ID_EXIT,135,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_PUSHBUTTON enddialog П робежимся взглядом по секции данных. MAXSIZE — максимальное количество символов в полном имени файла вместе с путем. PASSIZE — максимальная длина пароля без учета нуль-терминатора. Содержимое extname добавляется к полному имени для создания временного файла. В описании структуры ofn опущенные параметры отсутствуют — это допустимо, необходимо лишь обозначить их запятыми, если за ними следуют инициализируемые параметры. Диалоговое окно, как можно понять из кода, является упрощенным вариантом создания окна. Напомню параметры функции DialogBoxParam:  1) идентификатор исполняемого модуля программы; 2) идентификатор ресурса, содержащего шаблон диалогового окна; 3) дескриптор окна-владельца; 4) указатель на процедуру диалогового окна; 5) значение, передаваемое диалоговому окну в параметре lParam сообщения WM_INITDIALOG. Для вызова DialogBoxParam нам достаточно указать в первом параметре значение, возвращаемое функцией GetModuleHandle, во втором — шаблон окна, в четвертом — процедуру обработки сообщений. Третий параметр у нас — константа HWND_DESKTOP, которая равна нулю и указана лишь для наглядности. Шаблон окна содержится в секции ресурсов и описывается аналогично прочим ресурсам. Тип ресурса — RT_DIALOG. Объявляется макроинструкцией dialog, за которой следует некоторое количество элементов, обозначаемых макросом dialogitem, завершаемое инструкцией enddialog. Макроинструкция dialog может иметь до 11 параметров, первые 7 из которых обязательные: 1. Имя ресурса. 2. Заголовок окна в кавычках. 3, 4, 5, 6 — соответственно X, Y — ширина, высота. 7. Стиль окна. 8. Расширенный стиль (необязательный параметр). 9. Идентификатор меню окна (необязательный параметр). 10. Имя шрифта в кавычках (необязательный параметр). 11. Размер шрифта (необязательный параметр). Инструкция dialogitem имеет 8 обязательных параметров и один допустимый: 1. Имя класса элемента в кавычках. 2. Текст элемента в кавычках, или идентификатор ресурса (например, ID картинки для элемента класса STATIC со стилем SS_BITMAP). 3. Идентификатор элемента (используется в функциях API). 4, 5, 6, 7 — соответственно X, Y, ширина и высота элемента. 8. Стиль. 9. Расширенный стиль (необязательный параметр). Шаблон диалога может содержать любое количество элементов или вообще ни одного, но обязательно должен завершаться макросом enddialog. Вызов функции DialogBoxParam надолго передает управление процедуре DialogProc. В данном случае нам не надо организовывать цикл получения сообщений от окна. Операционная система будет крутить этот цикл самостоятельно, а нашей процедуре останется лишь обеспечить обработку входящих сообщений. Если на каком-то этапе нам понадобится закрыть диалоговое окно и продолжить выполнение команд, следующих за вызовом функции DialogBoxParam, то мы прямо из процедуры обработки сообщений вызовем функцию EndDialog для завершения функции и передачи управления следующей команде. Она имеет 2 параметра: дескриптор приговоренного диалогового окна; значение, которое вернет функция DialogBoxParam при успешном завершении (в случае неудачи DialogBoxParam вернет -1, поэтому во избежание двусмысленности сами не отправляйте -1 в этом параметре). После обработки сообщения наша процедура должна поместить в eax единицу и выполнить команду возврата из процедуры (ret). Если сообщение не было обработано процедурой, то в eax должен быть помещен ноль — тогда операционная система сама выполнит требуемые действия по обработке сообщения. Функция DialogBoxParam создает окно по указанному шаблону и перед самой публикацией его на экране отправляет процедуре диалога сообщение WM_INITDIALOG, содержащее в lParam значение, переданное нами в пятом параметре DialogBoxParam. Это значение может использоваться программистом для передачи каких-либо данных диалоговому окну в момент его инициализации. В нашем примере мы ничего не передаем, но по получении сообщения WM_INITDIALOG копируем дескриптор окна в элемент hwndOwner структуры ofn (сразу оба операнда команды MOV не могут являться указателями на память, поэтому копируем через привилегированный регистр EAX за две команды). Также отправляем обоим полям для ввода пароля сообщения EM_LIMITTEXT. Для отправки сообщений элементам диалогового окна служит функция SendDlgItemMessage. В первых двух параметрах этой функции указываются дескриптор диалогового окна и идентификатор элемента-получателя. В остальном функция аналогична функции SendMessage. Сообщение EM_LIMITTEXT устанавливает максимально допустимое число вводимых символов для элемента класса EDIT. Первый параметр сообщения — максимальное число символов, второй не используется. Сообщение WM_DROPFILES было рассмотрено в четвертой статье. По приходу сообщения от элемента ID_OPEN (кнопка "Открыть"), вызывается диалог открытия файла. Если после него возвращается ноль — значит, файл не был выбран. В этом случае — прыжок на метку .processed и возврат. Если же пользователь выбрал файл — сползаем на метку .new_file. Метка здесь для того, чтобы функция SetDlgItemText выполнялась и при открытии файла через диалог открытия, и при дропе файла на окно. Данная функция похожа на сообщение WM_SETTEXT. Она предназначена для установки нового текста элемента диалогового окна. Параметры: дескриптор диалога; идентификатор элемента; указатель на текстовую строку с нулем на конце. На метку .wmclose мы попадаем при получении сообщения WM_CLOSE или при получении сообщения WM_COMMAND от элемента ID_EXIT (кнопка "Выход"). Тут все предельно ясно: получили приказ на закрытие — вызываем упоминавшуюся выше функцию EndDialog. Теперь переходим, так сказать, к ядру нашей программы. Тут нас и поджидает масса новых неизвестностей. Когда мы проверяем последнее из возможных сообщений от элементов диалога — ID_CRYPT, — то помещаем ноль в EAX и прыгаем на .finish, если это не оно, чтобы функция операционной системы самостоятельно обрабатывала не предусмотренные нашей программой сообщения. Иначе — делаем вывод, что сообщение пришло от кнопки "Шифровать", и начинаем процедуру шифрования. Сразу мы сравниваем первый байт содержимого fname1 с нулем. Если файл был выбран, то там должен находиться первый символ его полного имени, ну, а если нет — там будет ноль, и мы переходим на сообщение об ошибке "Файл не выбран!" Функция GetDlgItemText аналогична сообщению WM_SETTEXT и является противоположностью функции SetDlgItemText. Она возвращает текст заданного элемента диалога в указанный буфер. Ее параметры: дескриптор диалога; идентификатор элемента; указатель на буфер для размещения текста; максимальное количество символов включая завершающий ноль. Максимальная длина пароля у нас равна константе PASSIZE, поэтому в качестве последнего параметра мы указываем PASSIZE+1 с учетом завершающего строку нуля. Сравнивая первый байт содержимого pass1 с нулем, мы проверяем, был ли введен хотя бы один символ пароля. Если пароль был введен — получаем содержимое поля для подтверждения пароля в переменную pass2. При помощи функции lstrcmp (ее мы проходили еще на втором занятии) сравниваем строки pass1 и pass2. Если возвращается не ноль — значит, пароль и подтверждение не совпадают, и мы прыгаем на соответствующую ошибку. В противном случае, используя функцию lstrlen, мы помещаем в переменную passlen количество введенных символов пароля: нам необходимо будет знать, из скольких символов состоит введенный пользователем пароль. Как же будет происходить шифрование выбранного файла? Самый простой способ шифрования — использование команды xor. Эта команда изначально предназначена для других вещей, но как нельзя лучше подходит для нашей сегодняшней задачи. Для начала давайте познакомимся поближе с основными арифметическими и логическими командами ассемблера: add — складывает два операнда и помещает результат в первый операнд. Синтаксис: add op1,op2 inc — прибавляет к операнду единицу. Синтаксис: inc op1 sub — вычитает второй операнд из первого. Результат сохраняется в первом операнде. Синтаксис: sub op1,op2 dec — вычитает из операнда единицу. Синтаксис: dec op1 mul — умножает операнд на регистр AL/AX/EAX в зависимости от размерности операнда (байт/слово/двойное слово). Результат помещается соответственно в AX/DX:AX/EDX:EAX. То есть, если вы хотите умножить 3 на 5, то задвигаете пятерку в AL и умножаете на тройку: mov al,5 mul 3 div — делит AX/DX:AX/EDX:EAX (в зависимости от размерности операнда) на операнд. Частное помещается соответственно в AL/AX/EAX, а остаток — в AH/DX/EDX. Пример деления мы разберем ниже по тексту. Думаю, вы знаете, что каждый байт данных в ЭВМ представлен в виде блока из восьми бит — восьми элементов, каждый из которых имеет лишь два состояния: выключен и включен (0 и 1). Логические команды выполняют логические операции над битами операндов. and — логическое "И": соответствующие биты первого операнда устанавливаются в единицу только в том случае, если в обоих операндах они равны единице. Иначе — в ноль. or — логическое "ИЛИ": соответствующие биты первого операнда устанавливаются в ноль только в том случае, если в обоих операндах они равны нолю. Иначе — в единицу. xor — логическое "ЛИБО" (логическое исключающее ИЛИ): соответствующие биты первого операнда устанавливаются в единицу только в том случае, если бит одного из операндов равен единице, а другого — нолю. Если соответствующие биты операндов равны, то бит первого операнда устанавливается в ноль. not — логическое "НЕ": биты операнда инвертируются. Ноль меняется на единицу, единица — на ноль. Второй операнд не используется. test — логическое "И" без изменения операндов: выполняется операция and, но результат не записывается в операнд, а выставляет флаг нуля (ZF) в ноль, если хотя бы одна пара соответствующих битов обоих операндов совпала в значении единицы. Команду cmp eax,0 удобно и выгодно заменять командой test eax,eax: результат тот же, зато выполняется за три такта процессора (против четырех в случае использования cmp) и занимает на один байт меньше памяти.

op1

op2

and

or

xor

not

0

0

0

0

0

1

0

1

0

1

1

1

1

0

0

1

1

0

1

1

1

1

0

0

С флагом нуля есть небольшая путаница: если ZF=0 — значит, флаг нуля не установлен, значит, результат не ноль. Если же результат ноль — флаг ZF установлен, то есть ZF=1. Все становится более понятным при использовании команд перехода JZ (100% синоним JE) — перейти, если ноль (флаг нуля установлен); JNZ (100% синоним JNE) — перейти, если не ноль (флаг нуля не установлен). Команда cmp после сравнения двух операндов методом вычитания выставляет соответствующие флаги. По флагу нуля мы и определяем, равны операнды или нет. Если операнды равны — флаг нуля устанавливается (ZF=1), и мы можем осуществить условный переход — например, je или jz — переход, если флаг нуля установлен, то есть если операнды равны. По состоянию флага переноса (CF) после выполнения команды cmp можно определить, меньше ли первый операнд, чем второй. Если флаг переноса установлен (CF=1) — значит, первый операнд меньше второго. Если не установлен (CF=0) — значит, первый операнд больше или равен второму. JB (синоним JC JNAE) осуществляет переход, если первый операнд команды cmp меньше второго, то есть если установлен флаг переноса CF. JAE (JNB, JNC) делает прыг, когда первый операнд больше или равен второму, то есть не меньше второго. JBE (JNA) — переход, если меньше или равно, то есть когда CF=1 или ZF=1. JA (JNBE) — переход, если больше, — CF=0 и ZF=0. Много информации? Перечитайте и отдохните. Сейчас будет еще веселей. Я говорил, что шифровать мы будем командой xor. Как же это работает? Команду xor можно представлять как переключатель выбранных битов в противоположное значение. То есть те биты, которые установлены в единицы во втором операнде (второй операнд в таком случае называют маской), будут инвертированы в первом. Те, которые во втором операнде установлены в ноль, в первом останутся нетронутыми. xor очень удобно использовать, к примеру, для быстрого обнуления регистра: xor eax,eax. Получается, что все установленные в единицу биты содержимого eax будут инвертированы, а нули останутся как есть. В итоге имеем в регистре набор исключительно нулевых битов, он же ноль. И экономим память и процессорное время. Когда мы применяем команду xor к какому-либо значению, второй операнд как бы является ключом шифра. Он указывает, значение каких битов изменить на противоположное, а какие не трогать. Если зашифровать какие-либо данные командой xor с определенным ключом, то повторное шифрование тем же ключом произведет расшифровку данных. Поэтому для расшифровки нам даже не придется менять порядок действий. Возвращаемся к коду. Функцией CreateFile открываем исходный файл на чтение. Проверяем на ошибку и сохраняем дескриптор в hfile1. Функция GetFileSize возвращает размер файла, дескриптор которого передается в функцию в первом параметре. Второй параметр — указатель на 32-битную переменную, в которую вернется старшее двойное слово размера файла. То есть, если число, обозначающее размер файла в байтах, будет настолько велико, что не вместится в 32 бита регистра для ответа, то функция запихнет его в 64 бита по частям: левые 32 бита — во второй параметр, правую часть — в привычный eax. Такое хитрое оформление ответа сделано из соображений совместимости со старыми программами, которые не предполагают, что размер файла может равняться 4 гигабайтам и выше. На самом деле я тоже совершенно не рассчитываю, что этим простейшим шифратором кто-то станет шифровать образы DVD. Программа может работать некорректно, если размер файла окажется более четырех гиг, так что будьте осторожны. Просто -1 (он же 0xFFFFFFFF в 32-битном варианте), который функция возвращает в случае ошибки, может трактоваться и как ошибка, и как размер в 4 гигабайта, а, к примеру, ноль в ответе функции может оказаться и нулевым размером файла, и 4 гигабайта + один байт. В любом случае этот пример не оптимизирован для работы с большими файлами, о чем выводится предупреждающее сообщение в случае, если размер файла равен или больше 0x500.000 (5 мегабайт). Если размер файла в норме (или программа думает, что в норме, а на самом деле размер превышает 4 гигабайта), мы имеем в ebx размер файла и начинаем подготовку к циклу шифрования на метке .size_ok. Копируем строку с именем файла из fname1 в fname2. Добавляем к строке fname2 строку extname, чтобы получилось имя временного файла, в который мы будем записывать шифрованные данные, отличное от имени оригинала. Создаем файл с именем fname2 и правами на запись. Помещаем его дескриптор в hfile2. Цикл шифрования .crypt_cycle. Функцией ReadFile читаем 1 байт в переменную buf. Указатель на текущую позицию для чтения/записи в файле автоматически сдвигается каждый раз на число прочитанных/записанных байт. Поэтому каждый следующий раз мы будем автоматически читать следующий байт. Далее нам надо поместить куда-нибудь очередной символ пароля, чтобы шифровать каждый следующий байт следующим символом пароля. Проще всего вычислить очередной символ, поделив номер текущего шифруемого байта нацело на количество символов указанного пароля. Остаток от деления каждый раз будет необходимым смещением относительно начала пароля. То есть, если длина введенного пароля три символа, а текущий шифруемый байт — седьмой, то 6/3=2 и 0 в остатке, 7 — 1 в остатке, 8 — 2 в остатке, 9 — снова 0 в остатке и т.д. В ebx мы сохранили размер файла в байтах и на каждом витке цикла отнимаем единичку (dec ebx). Значит, ebx у нас хранит число байт, оставшихся для шифрования, а когда становится нулем, мы понимаем, что все байты обработаны, и выходим из цикла. Значит, отсчет байтов в ebx у нас идет как бы обратный, и символы пароля тоже будут выбираться в обратном порядке, что немного повышает устойчивость шифра к взлому. Для деления нам необходимо поместить делимое в EDX:EAX. Старшую часть — EDX — мы просто обнуляем (xor edx,edx), а в младшую копируем наш обратный счетчик (mov eax,ebx). Делим на количество введенных символов пароля (passlen). Имеем остаток от деления в EDX. Теперь, чтобы обратиться именно к выбранному таким методом символу пароля, необходимо обратиться к байту по адресу [pass1 + полученное смещение]. От перемены мест слагаемых сумма не меняется, поэтому к полученному смещению прибавляем pass1 и имеем в EDX адрес искомого символа пароля. Оба операнда команды xor не могут одновременно являться указателями на память, поэтому одним из операндов у нас будет самый младшенький байт регистра EAX — AL. В него мы и скопируем необходимый символ (mov al,[edx]). Вас, наверное, уже давно мучает вопрос, что означают квадратные скобки в FASM? Почему al мы записали просто так, а edx заключили в них? Все дело в том, что, если операнд записывается без скобок, то это значит, что процессор оперирует непосредственно со значением операнда. Взятие операнда в квадратные скобки дает команду работать с данными, находящимися по адресу, указанному в операнде. В данном случае EDX содержит лишь адрес в виртуальной памяти нашей программы, а не само значение необходимого символа пароля. А вот в регистр AL будет помещено именно значение — конкретный символ пароля. Шифруем содержимое buf этим символом. Записываем зашифрованное значение в hfile2. Уменьшаем EBX и повторяем цикл, если EBX еще не равен нулю. По завершению цикла шифрования закрываем дескрипторы открытых файлов. Устанавливаем атрибуты файла-оригинала в значение FILE_ATTRIBUTE_NORMAL. Просто нам надо его теперь удалить — а вдруг он имеет атрибут "только для чтения", и удаление не удастся? Поэтому удаляем оригинал. Переименовываем зашифрованный дубликат в имя оригинала. Вызываем окошко с сообщением об успешном завершении. Стоит сказать, что чтение и запись большого файла по одному байту за раз является не самым лучшим вариантом с точки зрения скорости. Каждый вызов функции занимает достаточно много процессорного времени, что будет заметно невооруженным глазом при шифровании файлов размером в несколько мегабайт. Для ускорения процесса шифрования можно было бы шифровать данные сразу по 4 байта за проход, но тогда придется немного усложнить процедуру выбора текущих символов пароля для шифрования. Также можно заметно ускорить процесс шифрования, если помещать зашифрованные данные во временный буфер в оперативной памяти и только по заполнению буфера скидывать их на жесткий диск. Попробуйте самостоятельно осуществить эти задачи. На самом деле, имеющихся у вас знаний уже вполне достаточно для этого. Если хотите, чтобы вводимые символы пароля отображались в виде звездочек — добавьте к стилю полей для ввода паролей ES_PASSWORD.

Приветствую постоянных читателей, а также тех, кто только решил влиться в ряды изучающих ассемблер под Windows. Сегодня весьма полезная тема и для первых, и для вторых. Потому что сегодня мы подробно рассмотрим основные команды ассемблера в теории. Обычно я строил наши занятия по большей части с практическим уклоном. Но теперь, когда вы уже получили достаточно примеров использования тех или иных команд ассемблера, пришла пора четко и безо всякой двусмысленности зафиксировать их в вашем понимании. Обычно ассемблер начинают изучать именно с теории. Но без представления возможностей практического применения тех или иных команд такое обучение, скорее всего, покажется скучным и малопонятным. Добавьте к этому сложность предмета программирования самого по себе, и получите вполне ожидаемый результат — потерю желания изучать ассемблер вообще. Надеюсь, что вы уже имеете определенное положительное мнение об ассемблере и твердо решили идти до конца. Если же вы все еще не верите в свои силы, то попытаюсь вас успокоить: основных команд в ассемблере немногим более сотни, учитывая то, что многие из них — синонимы (близкие или одинаковые по значению) или антонимы (прямо противоположные). Команд, которые необходимо запомнить для каждодневного использования — вообще не более двух-трех десятков. Причем запоминать их все и сразу вам не обязательно. Просто сохраните эту статью, и всегда сможете быстро вспомнить название и назначение той или иной команды. Не забывайте, что данный цикл статей опирается на синтаксис, используемый в компиляторе FASM. Прежде всего, придется помучить вас маленьким, но очень важным моментом — регистром флагов. По общим правилам изучения ассемблера с этим регистром принято знакомиться на первом или втором занятии, сразу же после знакомства с регистрами общего назначения и сегментными регистрами. Однако, дабы не сильно пугать вас сразу, я отложил знакомство с ним аж до сегодняшнего занятия, а сегментные регистры пока что вообще не трогал. Пишут же люди программы на языках высокого уровня, даже не подозревая о таких "мелочах", так зачем вам, все еще местами сомневающимся в собственных силах касательно освоения ассемблера, изначально забивать голову пугающей теорией? И все же азы надо знать, так что соберитесь: регистр флагов! Регистр флагов в 32-битных процессорах имеет размер 32 бита и называется EFLAGS. Как вы могли заметить, приставка "E" в названии 32-битного регистра обычно свидетельствует о том, что он имеет 16-битное происхождение (64-битные регистры имеют приставку R). Значит, из соображений обратной совместимости со старыми 16-битными программами должен существовать и 16-битный предок — FLAGS. На его примере мы и начнем знакомство с данным регистром:

Регистр FLAGS

Бит

Флаг

Описание

0

CF

Флаг переноса (Carry flag)

1

1

Зарезервирован

2

PF

Флаг четности (Parity flag)

3

0

Зарезервирован

4

AF

Вспомогательный флаг переноса (Auxiliary flag)

5

0

Зарезервирован

6

ZF

Флаг нуля (Zero flag)

7

SF

Флаг знака (Sign flag)

8

TF

Флаг трассировки (Trace flag)

9

IF

Флаг разрешения прерываний (Interrupt enable flag)

10

DF

Флаг направления (Direction flag)

11

OF

Флаг переполнения (Overflow flag)

12,13

IOPL

Уровень приоритета ввода-вывода (I/O privilege level)

14

NT

Флаг вложенной задачи (Nested Task)

15

0

Зарезервирован

На данном этапе вам понадобится запомнить лишь 3-4 флага, поэтому не пугайтесь этой таблицы: ее цель — лишь получение вами общего представления о регистре флагов 16-битного процессора, который, тем не менее, сохранился и в 32-, и в 64-битных потомках. CF устанавливается (1), если в результате операции из старшего бита происходит перенос при сложении или заем при вычитании, иначе CF сбрасывается (0). Кроме того, CF используется в логических и унарных операциях. ZF устанавливается (1), если результат операции равен нулю, иначе ZF сбрасывается (0). Главное — впоследствии не запутаться в том, что ZF=0, если результат отличен от ноля, но ZF=1 при нулевом результате. SF отражает состояние старшего (знакового) бита результата. SF=1, если старший бит результата равен единице, и ZF=0 — если старший бит результата нулевой. OF фиксирует факт потери старшего (знакового) бита результата при арифметических операциях. OF=1, если произошел перенос в старший бит результата или заем из старшего бита, ZF=0 при отсутствии переноса или заема. PF устанавливается (1), если младшие 8 битов результата содержат четное число единиц. Теперь мы можем перейти к знакомству с часто применяемыми командами. Команды обмена данными: MOV, XCHG. MOV приемник,источник Копирует байт(byte)/слово(word)/двойное слово(dword) из операнда источник в операнд приемник. Позволяет копировать данные из одного регистра общего назначения (РОН) в другой, из РОН в память, из памяти в РОН. Командой MOV нельзя напрямую переслать данные из одной области памяти в другую — для этого придется использовать в качестве посредника один из РОН. Копирование данных в таком случае осуществляется за две команды: сначала из исходной области памяти в РОН, а затем — из РОН в целевую область памяти. При помощи команды MOV также можно помещать в регистр или память непосредственное значение, копировать содержимое сегментного регистра в РОН или память, содержимое РОН или памяти в сегментный регистр, содержимое регистра управления или отладки в РОН, содержимое РОН в регистр управления или регистр отладки. Команда MOV может быть обработана компилятором только если размеры источника и приемника совпадают. Ниже следуют примеры возможных вариантов использования команды MOV: mov bx,ax ;копировать содержимое РОН в РОН mov [char],al ;РОН в память mov bl,[char] ;память в РОН mov dl,32 ;значение в РОН mov [char],32 ;значение в память mov ax,ds ;сегментный регистр в РОН mov [bx],ds ;сегментный регистр в память mov ds,ax ;РОН в сегментный регистр mov ds,[bx] ;память в сегментный регистр mov eax,cr0 ;регистр управления в РОН mov cr3,ebx ;РОН в регистр управления XCHG операнд1,операнд2 Используется для двунаправленной пересылки данных между операндами. Размер обоих операндов может быть байтом, словом или двойным словом, но оба операнда должны быть одинакового размера. Команда XCHG помещает содержимое первого операнда во второй, а второго — в первый. Один из операндов всегда должен быть РОН, а другой может быть областью памяти или также РОН. xchg ax,bx ;обменять содержимое РОН и РОН xchg al,[char] ;обменять содержимое РОН и памяти Обмен данными через стек: PUSH, POP. Стек — область памяти, специально выделяемая каждой программе для временного хранения промежуточных данных. Обычно адреса в памяти растут от ноля к максимальному адресу. В стеке все наоборот: он растет от дна (максимальный адрес сегмента стека) к нолю. Для того, чтобы поместить данные в стек, применяется команда PUSH, ее синтаксис следующий: PUSH источник Эта команда уменьшает указатель на текущий кадр стека (регистр ESP) и копирует содержимое операнда-источника по адресу вершины стека, содержащемуся в ESP. В роли операнда может выступать РОН, память, сегментный регистр, значение размером в слово или двойное слово. Если в качестве операнда указано непосредственное значение, то в 16-битном режиме оно по умолчанию воспринимается компилятором как слово, а в 32-битном — как двойное слово. Мнемоники PUSHW и PUSHD указывают компилятору, что значение необходимо сохранить как слово или как двойное слово соответственно независимо от режима, в котором работает компилятор. Если за командой PUSH следуют несколько операндов, разделенных пробелами, то они будут обработаны компилятором как последовательность из нескольких команд PUSH с этими операндами по отдельности. PUSHA сохраняет в стеке содержимое всех восьми регистров общего назначения. Примеры использования команды PUSH: push ax ;сохранить РОН push es ;сохранить сегментный регистр pushw [bx] ;сохранить память push 1000h ;сохранить значение push ebx,esi,edi ;сохранить по очереди три регистра pusha ;сохранить все 8 РОН POP приемник Команда POP копирует слово или двойное слово, содержащееся на вершине стека, в указанный операнд-приемник, затем увеличивает ESP так, чтобы он указывал на новую вершину стека. Операндом может быть РОН, память, сегментный регистр. Эта команда предназначена для извлечения из стека данных, сохраненных командой PUSH. Следует помнить, что извлекаются данные в обратном порядке, так что, если вы сохранили в стеке EAX, EBX и потом ECX, то извлекать надо сперва ECX, затем EBX и EAX. Команда POP является полной противоположностью команды PUSH, поэтому мнемоники POPW, POPD и POPA работают по аналогии с описанными выше мнемониками PUSH, но выполняют обратные действия.  Примеры: pop bx ;восстановить РОН pop ds ;восстановить сегментный регистр popw [si] ;восстановить память pop edi,esi,ebx ;восстановить по очереди три регистра popa ;восстановить все 8 РОН Арифметические команды. INC операнд Команда увеличивает значение операнда на единицу. Операнд может быть РОН или памятью. Размер операнда — байт, слово или двойное слово. Примеры: inc ax ;увеличить значение в регистре inc byte[bx] ;увеличить значение в памяти ADD приемник,источник Складывает оба операнда и помещает результат в приемник. Если результат превысил размер приемника, устанавливает флаг переноса (CF). Размером операндов может быть байт, слово или двойное слово. Приемник может быть РОН или памятью. Источник может быть РОН или значением. Источник может также быть памятью при условии, что приемник является регистром. Примеры: add ax,bx ;прибавить к регистру регистр add ax,[si] ;прибавить к регистру память add [di],al ;прибавить к памяти регистр add al,48 ;прибавить к регистру значение add [char],48 ;прибавить к памяти значение ADC приемник,источник Складывает оба операнда и добавляет единицу в случае, если флаг переноса установлен. Помещает результат в приемник. Правила для операндов те же, что и у ADD. Команда ADD в связке с ADC может использоваться для сложения чисел, не помещающихся целиком в регистр процессора.  DEC операнд Команда уменьшает значение операнда на единицу. Правила для операнда те же, что и у INC. SUB приемник,источник Вычитает источник из приемника, помещает результат в приемник. Если источник был больше приемника, то устанавливается флаг переноса CF. Если источник был равен приемнику (результат 0), то устанавливается флаг ноля ZF. Правила для операндов те же, что и у ADD. SBB приемник,источник Вычитает источник из приемника, вычитает единицу в случае, если флаг переноса установлен. Помещает результат в приемник. Правила для операндов те же, что и у ADD. Команда SUB в связке с SBB может использоваться для вычитания чисел, не помещающихся целиком в регистр процессора.  CMP приемник,источник Команда осуществляет сравнение приемника с источником способом вычитания источника из приемника, но, в отличие от SUB, результат никуда не сохраняет. По установленным командой флагам можно отследить результат такого сравнения и выполнить условный переход (JZ, JO, JC и т.п.).  NEG приемник Изменяет знак операнда на противоположный, вычитая операнд из нуля. На практике команда применяется не только для смены знака, но и для вычитания из константы. Допустим, нам надо вычесть содержимое AX из 300. Очень хочется написать: sub 300,ax, но команда SUB не допускает возможности вычитания из непосредственного значения, потому что приемник должен являться РОН или памятью. Значит, мы могли бы предварительно поместить значение 300 в какой-то РОН, а затем вычесть из него AX, однако более простым вариантом с точки зрения процессора будет такой:  neg ax add ax,300 Мы прибавили 300 к отрицательному значению ax, что по законам математики дает такой же результат, что и вычитание ax из 300. XADD приемник,источник Еще одна редкая, но иногда очень полезная команда. Она похожа на ADD, только перед тем, как поместить сумму операндов в приемник, производит обмен значениями между операндами (как команда XCHG). Эта команда одним махом выполняет сразу 2 действия, а значит, может помочь сэкономить процессорное время. Все вышеперечисленные арифметические операции изменяют флаги SF, ZF, PF, OF в соответствии с результатом. Команды MUL(умножение) и DIV(деление) были достаточно подробно описаны в предыдущей статье, поэтому не будем повторяться, и идем дальше. Преобразование типов Размеры операндов арифметических команд должны быть одинаковыми. Поэтому, если необходимо произвести арифметическое действие над операндами, имеющими разные размеры, следует сначала преобразовать один из них так, чтобы оба операнда имели одинаковый размер. Для этого в системе команд процессора предусмотрены команды преобразования типов. Они служат для преобразования байтов в слова, слов — в двойные слова, двойных слов — в учетверенные (qword). Эти преобразования могут выполняться способом знакового расширения — увеличение размера операнда с учетом знака (заполнение старших разрядов увеличенного операнда значением старшего бита исходного операнда) или нулевого расширения (заполнение старших разрядов увеличенного операнда нолями). Команды преобразования со знаковым расширением без операндов: CBW преобразовывает байт, содержащийся в регистре AL, в слово, помещаемое в регистр AX. CWD преобразовывает слово, содержащееся в регистре AX, в двойное слово, помещаемое в регистры DX:AX. Старшая часть значения разместится в DX, а младшая — в AX. CWDE преобразовывает слово, содержащееся в регистре AX, в двойное слово, помещаемое в регистр EAX. CDQ преобразовывает двойное слово, содержащееся в EAX, в учетверенное слово, помещаемое в регистры EDX:EAX. Еще раз напомню, что все перечисленные преобразования по сути своей — лишь распространение значения старшего (знакового) бита исходного операнда на все биты добавляемой части. Эти команды работают с конкретными регистрами и поэтому не имеют операндов. MOVSX приемник,источник Преобразовывает с учетом знакового расширения байт в слово или двойное слово; слово — в двойное слово. Операнд-источник может быть памятью или РОН, приемник всегда должен быть РОН. MOVZX приемник,источник Работает так же, как и MOVSX, только производит расширение без учета знака, то есть заполняет добавляемую часть нулями, а не знаковым старшим битом источника. Десятичная арифметика Десятичные числа могут быть представлены в так называемом двоично-десятичном коде (Binary Coded Decimal — BCD). Этот способ предполагает хранение каждой десятичной цифры в четырех битах. Различают два формата хранения BCD-чисел: — упакованный, когда каждый байт (8 бит) может содержать две десятичные цифры (по 4 бита каждая). В таком случае каждый байт содержит десятичное число в диапазоне от 00 до 99. — неупакованный, когда каждый байт содержит лишь одну десятичную цифру в младших четырех битах, а старшие четыре бита имеют нулевое значение. Диапазон представления неупакованного BCD-числа в отдельно взятом байте составляет от 0 до 9. Десятичная арифметика осуществляется методом комбинирования вышеописанных команд двоичной арифметики с командами, специально предназначенными для десятичных операций. Команды десятичной арифметики используются для приведения результата предыдущих двоичных вычислений к упакованному/неупакованному формату BCD-числа или, наоборот, для подготовки введенных данных к двоичным арифметическим операциям. DAA корректирует результат сложения двух упакованных BCD-чисел в регистре AL. Эта команда должна следовать за командой сложения (ADD или ADC), если в сложении участвовали два упакованных BCD, и результат находится в AL. Если откорректированный результат превысит 99, то будет установлен флаг CF, а в AL останутся лишь две младшие цифры. Пример: mov al,49h mov bl,52h add al,bl daa В данном примере наглядно показано, что упакованные BCD можно записывать просто как шестнадцатеричные числа (буковка h в конце числа означает HEX), только без использования символов ABCDEF. Когда будет произведено сложение 49h и 52h, результат будет неверным. Точнее, он будет верным, но лишь применительно к шестнадцатеричным числам, потому что процессор, складывая эти числа, считает их не упакованными десятичными, а стандартными двоичными или шестнадцатеричными, если хотите. Однако команда DAA все расставляет по своим местам, и в результате в AL получается 01 (две младшие цифры от 101), а установленный флаг CF позволяет определить, что единичка для следующего третьего разряда "в уме". DAS работает аналогично DAA, только используется для корректировки результата вычитания. У команды также отсутствует операнд, потому что действие производится над регистром AL. Флаг CF (если установлен) указывает на то, что вычитаемое оказалось больше уменьшаемого, и необходимо это дело обработать, например, уменьшением третьего разряда на единицу. AAA предназначена для корректировки результата сложения НЕупакованных BCD-чисел размером в байт, то есть одноразрядных. Операнды отсутствуют, действие производится над регистром AL. Если результат превышает 9, то в AL помещается лишь младший разряд, а AH увеличивается на единицу, и устанавливается флаг CF. AAS аналогична AAA за исключением того, что применяется для корректировки вычитания неупакованных одноразрядных BCD-чисел. При необходимости заема устанавливается флаг CF, а содержимое AH уменьшается на единицу. AAM корректирует результат умножения двух неупакованных BCD, находящийся в AL. Данная команда просто делит содержимое AL на 10 и помещает частное в AH, а остаток — в AL. Таким образом в AX помещается двухразрядное неупакованное BCD. Стандартная версия команды не имеет операндов, однако существует расширенная версия AAM, в которой в роли операнда выступает непосредственное значение — база, на которую будет производиться деление. Так что, выполнив, к примеру, команду AAM 5, вы поделите нацело содержимое AL, получив частное в AH, а остаток — в AL. Очень удобный способ для деления нацело небольших (до 8 бит) чисел. AAD подготавливает к делению неупакованное BCD-число, находящееся в AX. Команда просто добавляет к AL содержимое AH, умноженное на 10, потом AH обнуляется. Несмотря на свое основное назначение, команда отлично справляется не только с подготовкой к делению, но и с простым преобразованием неупакованного двузначного BCD в двоичный эквивалент. Также команда может оказать неоценимую помощь в преобразовании символьного кода цифр сразу в двоичный код. Например, в таблице ASCII цифра "1" имеет код 31h, цифра "2" — 32h и т.д. Значит, для того, чтобы из символьного кода получить неупакованное BCD, нам достаточно обнулить старший шестнадцатеричный разряд (старшие 4 бита) ASCII-кода. Поместим число 15 в символьном виде в AX и переведем его в двоичное значение: mov AX,3135h and AX,0F0Fh ;обнуляем левые половинки байтов aad После выполнения этих команд в AX у нас будет 0Fh, то есть число 15 в нормальном двоичном (шестнадцатеричном) виде, привычном для процессора. В расширенном варианте команды AAD есть возможность указать в качестве операнда непосредственное значение, на которое будет умножаться AH, прежде чем прибавиться к AL. Это дает еще один вариант использования команды: быстрое умножение небольших чисел.

Сегодня мы продолжим знакомство с основными командами ассемблера. Программирование под Windows — это, конечно, замечательно, но чтобы программировать на ассемблере, необходимо знать команды ассемблера. Те, кому не по душе теория, могут не волноваться: она скоро закончится, и мы снова вернемся к практике. Помню, как мне не нравилась вся эта скучная теория, особенно когда самые непонятные моменты приходилось читать на английском. Но искусство требует жертв. Если вы хотите научиться искусству программирования на ассемблере — вам придется пожертвовать своей нетерпеливостью и, если не убить ее в себе, то хотя бы покалечить. Покажите ей, кто из вас хозяин: она, жаждущая тотчас же получать свой кусок "бесплатного сыра", или вы, знающий истинную цену жемчужин знания, и желающий на этот раз насобирать их целый мешок? Надеюсь, что хозяином своего ума оказались вы же, поэтому продолжим искать жемчуг на дне океана теории ассемблера. Логические команды AND, OR, XOR, NOT были рассмотрены нами в 8-й части цикла. Добавлю лишь, что NOT не оказывает влияния на флаги, а AND, OR и XOR изменяют флаги SF, ZF, PF в соответствии с результатом. BT источник,индекс Команды BT, BTS, BTR, BTC оперируют с отдельным битом в памяти или регистре общего назначения (РОН). Эти команды сначала передают значение указанного бита флагу CF, чтобы далее можно было организовать условный переход посредством команд JC (перейти если CF=1) или JNC (перейти если CF=0). BT больше ничего и не делает, BTS устанавливает заданный бит в единицу, BTR сбрасывает бит в ноль, BTC изменяет значение бита на противоположное. Операндом-источником этих команд может быть слово или двойное слово. Индекс может быть РОН или непосредственным значением. Биты отсчитываются от младшего к старшему, то есть справа налево начиная с нулевого. Примеры: bt ax,15 ;проверить старший бит в регистре bts word[bx],15 ;проверить и установить бит в единицу btr ax,cx ;проверить бит в регистре и сбросить его в ноль btc word[bx],cx ;проверить бит в памяти и переключить его BSF приемник,источник BSF и BSR сканируют источник (слово или двойное слово) в поисках бита, установленного в единицу. Индекс первого найденного бита заносится в операнд-приемник, который должен быть РОН. Сканируемое битовое поле определяется операндом-источником и может быть РОН или памятью. Если все биты источника оказались нулевыми, устанавливается флаг ZF, иначе ZF сбрасывается в ноль. BSF сканирует биты от младшего к старшему, а BSR производит поиск в обратном порядке. SHL источник,индекс SHL сдвигает содержимое источника влево на количество бит, указанное в индексе. Источник может иметь размер в байт, слово, двойное слово и являться РОН или памятью. Индекс может быть непосредственным значением или регистром CL. С правой стороны вдвигаются нулевые биты, а последний выдвигаемый из источника бит становится значением флага CF. Команда SAL является синонимом SHL. Команды SHL, SAL являются удобным средством для быстрого умножения числа на степень двойки: mov ax,17 shl ax,3 ;умножить 17 на 8 (2 в степени 3) SHR и SAR сдвигают содержимое источника вправо на количество бит, указанное в индексе. Команды выполняют действие, аналогичное SHL/SAL, только при использовании SHR, с левой стороны вдвигаются нули, а SAR вдвигает слева знаковый старший бит операнда: ноль для положительного значения либо единицу для отрицательного. Поэтому команда SHR широко используется для деления целочисленных операндов на степень двойки без учета знака, а SAL — с учетом знака: mov cl,2 shr ax,cl ;разделить содержимое ax на 4 (2 в степени 2) SHLD приемник,источник,индекс SHLD сдвигает влево биты операнда приемника, вдвигая справа биты операнда источника. Содержимое источника при этом не изменяется. Приемник может быть РОН или памятью размером в слово или двойное слово, источник должен быть РОН. Индекс, в котором указывается количество битов для сдвига, может быть непосредственным значением или регистром CL. SHRD сдвигает биты приемника вправо, вдвигая в него слева младшие биты из источника. Правила те же, что и для команды SHLD. ROL и RCL осуществляют циклический сдвиг битов операнда влево. Синтаксис и правила для операндов такие же, как у SHL. Отличия в том, что при каждом сдвиге выдвигаемый слева бит операнда вдвигается в него же справа (команда ROL). При использовании команды RCL выдвигаемый бит прежде, чем быть вдвинутым с другой стороны, попадает в CF и вдвигается обратно в операнд лишь на следующем шаге цикла. ROR и RCR производят циклический сдвиг байта, слова или двойного слова вправо. Команды аналогичны командам ROL и RCL за исключением направления сдвига. BSWAP изменяет порядок следования байтов в операнде, который должен быть 32-битным РОН. Биты 0-7 меняются местами с битами 24-31, а биты 8- 15 — с битами 16-23: mov edx,1A2B3C4Dh bswap edx ;edx=4D3C2B1Ah Команды передачи управления Хотя я и касался в прошлых выпусках нескольких команд условного и безусловного перехода, теперь необходимо эту информацию закрепить в уме окончательно и бесповоротно. JMP передает управление в указанную точку программы безусловно. Целевой адрес может быть определен в команде непосредственно либо косвенно через регистр или память. На самом деле процессор узнает о том, какая команда должна выполняться следующей, по содержимому пары регистров CS:(E)IP, где CS — это адрес текущего сегмента кода, а EIP/IP — смещение относительно текущего сегмента. Для 16-битного режима используется 16- битный регистр IP, а для 32-битного — 32-битный EIP. Обычно при выполнении процессором какой-либо команды содержимое EIP/IP автоматически увеличивается на число байтов, занимаемое командой. Таким образом, после выполнения одной команды регистр EIP/IP будет содержать адрес следующей за ней команды. Команды передачи управления просто изменяют содержимое EIP/IP, а также содержимое CS, если переход выполняется в другой сегмент кода. Команды передачи управления могут быть разного размера в зависимости от дальности перехода. Обычно компилятор самостоятельно выбирает тип перехода near (близкий внутрисегментный) или far (дальний межсегментный), но можно и принудительно определить его выбор, вставив между командой и операндом префикс near либо far. Следует помнить, что для 32-битного режима размер адреса для близкого перехода равен двойному слову (32 бита), так как для внутрисегментного перехода используется лишь EIP. Для дальнего перехода размер адреса удваивается, так как необходимо указывать не только смещение, но и базовый адрес сегмента кода. В 16-битном режиме размеры адресов для ближнего и дальнего переходов — соответственно 16 и 32 бита. Также может быть использован короткий тип перехода (short). Целевой адрес короткого перехода может находиться в пределах -128 — +127 байт относительно следующей за JMP команды. Зато команда короткого перехода занимает всего 2 байта: 1-й байт — сама команда, 2-й — значение смещения перехода. Поэтому компилятор старается выбирать короткий вариант перехода везде, где это возможно. Все это сразу может показаться очень сложным и непонятным, но для использования команд перехода вам вовсе не обязательно высчитывать адрес самостоятельно. Достаточно вставить метку в текст программы и при использовании команды перехода указать в качестве операнда имя этой метки. Каждая метка может быть объявлена лишь один раз в программе. Простейший способ объявления метки — поставить двоеточие сразу после ее имени — например, "metka1:". За таким объявлением метки может даже следовать очередная команда в той же строке, хотя для удобства чтения текста программы лучше объявлять метки в отдельных строках кода. Метка, имя которой начинается с точки, считается локальной меткой — например, ".local1:". Ее имя автоматически присоединяется к имени последней глобальной метки (без точки в начале имени), поэтому вы можете использовать короткое имя такой метки лишь до объявления следующей глобальной метки. Для доступа к локальной метке из других мест придется использовать полное имя: "jmp metka1.local1". Метка, имя которой начинается с двух точек, является исключением из правил — такая метка имеет свойства глобальной метки, но следующие за ней локальные метки к ней не привязываются. Существуют также и безымянные метки. Их в тексте программы может быть сколько угодно. Безымянная метка обозначается двумя собаками: "@@:". Переход к безымянной метке может быть осуществлен при помощи специальных символов. @B или @R указывают на ближайшую предшествующую безымянную метку. @F является указателем на ближайшую нижеследующую безымянную метку. Команда CALL схожа с командой JMP, но позволяет впоследствии осуществить возврат из вызываемой подпрограммы в точку вызова. CALL передает управление процедуре (подпрограмме), сохраняя в стек адрес следующей за CALL команды. Позже командой RET можно вернуть управление на команду, следующую за командой CALL. RET, RETN и RETF прекращают исполнение процедуры и возвращают управление в точку, откуда была вызвана процедура. RETN и RETF являются соответственно командами ближнего (внутрисегментного) и дальнего (межсегментного) возврата. Условные переходы Команды условного перехода осуществляют переход, если соблюдено условие: — отношения между операндами со знаком (больше или меньше); — отношения операндов без знака (выше или ниже); — состояния флагов ZF, SF, CF, OF, PF. Синтаксис команд условного перехода всегда одинаковый: Jcc адрес_перехода cc означает условие перехода — например, JZ передает управление на указанный адрес, если установлен флаг нуля ZF. В связи с тем, что процессор не различает числа с учетом знака и без его учета, условия "больше" и "меньше" относятся к сравнению чисел с учетом знакового бита, а условия "выше" и "ниже" являются аналогичными условиями для сравнения чисел без учета знака. Если условие команды соблюдается, то производится переход в указанную операндом точку программы, иначе выполняется следующая команда. Существующие команды условного перехода описаны в таблице. 

Команда

Флаги

Условие перехода

JO

OF=1

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

JNO

OF=0

Нет переполнения

JB, JC, JNAE

CF=1

Ниже, перенос

JAE, JNB, JNC

CF=0

Выше или равно, нет переноса

JE, JZ

ZF=1

Равно, ноль

JNE, JNZ

ZF=0

Не равно, не ноль

JBE, JNA

CF=1 или ZF=1

Ниже или равно

JA, JNBE

CF=0 и ZF=0

Выше

JS

SF=1

Отрицательное

JNS

SF=0

Положительное

JP, JPE

PF=1

Четное количество единичных битов

JNP, JPO

PF=0

Нечетное количество единичных битов

JL, JNGE

SF<>OF

Меньше

JGE, JNL

SF=OF

Больше или равно

JLE, JNG

ZF=1 или SF<>OF

Меньше или равно

JG, JNLE

ZF=0 и SF=OF

Больше

JCXZ/JECXZ

CX/ECX = 0

Команды условного перехода могут выполнять короткий или близкий переход внутри сегмента, но не могут быть использованы для дальнего межсегментного перехода. При необходимости условного перехода в другой сегмент команду условного перехода комбинируют с командой JMP. Команды JCXZ/JECXZ, в отличие от других приведенных команд, выполняют переход в зависимости от содержимого регистра CX/ECX. Переход осуществляется только в случае, когда содержимое равно нолю. Еще одно отличие этих команд в том, что они могут выполнять только короткий переход. Для увеличения дальности перехода команды JCXZ/JECXZ комбинируют с командой JMP. Раз уж мы рассмотрели команды условного перехода, скажу пару слов о команде условной установки байта SETcc. Здесь "cc" — то же самое, что и в командах Jcc, — условие. Если выполняется условие, то в операнд помещается единица, иначе — ноль. Условия могут быть такими же, что и в командах Jcc: SETO, SETNO, SETB и т.д., кроме CXZ/ECXZ. Операнд имеет размер в байт и может быть любым 8-битным РОН (AL, AH, BL, BH, CL, CH, DL, DH) или памятью. Например, команда SETZ AL поместит в AL единицу, если ZF=1, либо обнулит AL, если ZF=0. Команда LOOP тоже является в какой-то мере командой условного перехода. Хотя изначально эта команда предназначена для организации циклов. LOOP уменьшает на единицу содержимое CX/ECX (в зависимости от разрядности режима 16/32 бит) и производит переход, если содержимое не равно нулю. Иначе, если значение в CX/ECX равно нулю, выполняется команда, следующая за LOOP. Таким образом, для организации цикла необходимо обозначить начало цикла меткой, а в конце цикла выполнить команду LOOP метка. Естественно, поместить число повторений цикла в CX/ECX необходимо до его начала. Команду LOOP можно заставить работать с 16-битным регистром CX даже в 32-битном режиме, если использовать мнемонику LOOPW. Для принудительного использования ECX независимо от режима существует мнемоника LOOPD. LOOPE и ее синоним LOOPZ отличаются от обычной LOOP лишь тем, что прекращают цикл не только при нулевом значении CX/ECX, но и при ZF=0. LOOPEW и LOOPZW служат для принудительного использования CX, а LOOPED и LOOPZD — для ECX. LOOPNE и LOOPNZ завершают цикл при установленном флаге ZF. Соответственно, LOOPNEW и LOOPNZW форсируют использование 16- битного CX, а LOOPNED и LOOPNZD — 32-битного ECX. Все вышеописанные подвиды команды LOOP могут осуществлять только короткий переход не более 128 байт назад либо 127 байт вперед от адреса команды, следующей за командой LOOP. Ввиду особой сложности восприятия принципов работы команды LOOP новичками постараюсь объяснить это дело простым языком на примере небольшой программки. Допустим, нам необходимо найти в строке первый пробел. Логика программы заключается в следующем: сравнить первый символ в строке с пробелом, если это не пробел и если это не последний символ, сравнить второй символ с пробелом и т.д. В таком случае программа будет выглядеть примерно так: include 'win32ax.inc' .data stroka db 'Ищемпробелвэ тойстроке',0 dlina_stroki = $-stroka msg_no_spc db 'Нет пробелов',0 msg_spc db 'Первый пробел после символа № ' msg_spc_2 db 0,0,0 .code start: mov esi,-1 mov ecx,dlina_stroki cycl: inc esi cmp [stroka+esi],' ' loopne cycl jecxz net_probelov mov eax,esi aam or ax,3030h xchg al,ah mov word [msg_spc_2],ax invoke MessageBox,0,msg_spc,0,0 jmp exit net_probelov: invoke MessageBox,0,msg_no_spc,0,0 exit: invoke ExitProcess,0 .end start В начале программы мы помещаем в ECX длину проверяемой строки. А в ESI необходимо поместить смещение первого символа относительно начала строки. Смещение первого символа — 0, но ввиду того, что в начале цикла перед сравнением мы будем каждый раз увеличивать это смещение на единицу для обработки очередного символа, необходимо заранее позаботиться о том, чтобы изначальное смещение было на единицу меньше, поэтому mov esi,-1. Далее следует собственно цикл проверки. Увеличиваем ESI, сравниваем содержимое ячейки памяти по адресу stroke+esi с непосредственным значением символа "пробел". Если сравнение выявит равенство, то будет установлен флаг ZF, иначе ZF=0. Поэтому мы используем команду LOOPNE, чтобы повторять команды на метке cycl до тех пор, пока результат сравнения NE (Not Equal — Не Равно) и, разумеется, до тех пор, пока ECX не будет равно нулю. Конечно, кажется более разумным запись команды inc esi после команды сравнения. Но команда inc влияет на флаг ZF, а нам нельзя допускать изменения флага ZF после команды сравнения до тех пор, пока результат сравнения не будет обработан командой условного перехода. Цикл поиска пробела может быть прекращен в двух случаях: 1 — найден пробел, и ESI содержит его смещение относительно начала строки; 2 — пробел не найден, ECX=0, ESI содержит смещение последнего символа — нуль-терминатора в нашем примере. Поэтому по завершению цикла мы совершаем условный переход командой JECXZ в случае равенства ECX нулю. Иначе — выполняем преобразование значения смещения в символьный вид, копируем его в два заранее приготовленных байта на метке msg_spc_2 (третий байт — нуль-терминатор для строки вывода) и выводим сообщение. Команда AAM, если вы помните, преобразует двоичное содержимое AX в формат неупакованного BCD-числа. Если у нас пробел, к примеру, после 12-го символа, то неупакованный BCD-эквивалент результата в AX будет 0102h. Для того, чтобы получить символьный формат (3132h), мы используем команду OR AX,3030h. Теперь остается только обменять местами AL и AH, потому что записанное в AX 3132h будет читаться из регистра с младшего байта — справа налево, а записываться в память — слева направо. Иначе в сообщении вместо 12 мы увидим 21. Оператор word после команды mov необходим потому, что метка msg_spc_2 у нас объявляет данные в байтах (db). Если мы не заставим компилятор сгенерировать инструкцию для копирования слова (word), он будет пытаться сгенерировать инструкцию копирования байта (byte) и сообщать нам об ошибке несоответствия размерности операндов, потому что AX явно не является однобайтовым регистром. Компилятор FASM всегда старается генерировать инструкции минимального размера, поэтому иногда необходимо указывать после команды требуемый размер операндов операторами word или dword. Возможно, самые забывчивые из вас зададутся вопросом, почему пробел является 13-м символом, а в ESI получилось число 12? Напоминаю: люди нумеруют элементы начиная с единицы, а компьютеры отсчитывают смещение от нулевого. Поэтому первый "человеческий" символ с компьютерной точки зрения всегда является нулевым, второй — первым… тринадцатый — двенадцатым. Таков закон процессоров: ноль — положительное число, и именно с него начинается отсчет. Поэтому мы можем после цикла добавить к ESI единичку и написать, что пробел является символом №_, или, ничего не прибавляя, написать, что пробел следует за символом №_, как это сделано в моем примере. Но никогда не забывайте про эту разницу в единицу между человеческим представлением и компьютерным. Многие начинающие программисты долго не могут обнаружить ошибку в своих программах, если забудут, что процессор "загибает свои пальцы", начиная с нулевого. Стоит признать, что это не самый лучший вариант программы поиска пробела. А дело в том, что мы еще не знакомы с командами обработки строк. Эти команды также обычно называют цепочечными командами, но познакомимся мы с ними в следующий раз. Пока что впитывайте этот материал. Помните: "Тяжело в учении — легко в бою!" Не надейтесь, что после беглого просмотра материала вам все сразу станет ясно. Ассемблер — король всех языков программирования. Чтобы его изучить, надо приложить усилие.

Продолжим изучение наидревнейшего и бессменного языка электронных машин. Сегодня пришла пора познакомиться поближе с командами обработки строк. Эти команды также обычно называют цепочечными командами, потому что с их помощью можно обрабатывать не только строки символов, но и цепочки любых данных. Цепочечные команды незаменимы при работе с массивами. Несмотря на их кажущуюся ограниченность, большинство высокоуровневых функций обработки массивов в конечном итоге преобразуются в машинные команды обработки строк. Если они преобразуются высокоуровневым компилятором автоматически, вы рискуете получить не самый лучший с точки зрения производительности код. Если же вы оформляете обработку массивов вручную, то имеете все шансы добиться максимального быстродействия обработки данных. Именно поэтому в наш век сверхскоростных процессоров и высокоуровневых языков движки самых крутых современных игр до сих пор частично или целиком пишутся на ассемблере. Цепочечные команды работают с элементами строк размером в байт, слово или двойное слово. Адреса строковых элементов для таких команд всегда указываются косвенно. Адрес источника (от англ. Source Index) предварительно помещается в SI/ESI (16/32-битный режим). Адрес приемника (Destination Index) должен находиться в DI/EDI. Такая косвенная адресация разработана для того, чтобы строковые команды могли автоматически изменять адрес текущего элемента (после его обработки) на адрес следующего элемента, увеличивая адрес или уменьшая его на 1, 2 или 4 в зависимости от размера элемента строки (байт, слово или двойное слово). Будет ли адрес увеличен или уменьшен, зависит от состояния флага направления DF. Состоянием флага DF можно управлять с помощью команд CLD (сброс DF в ноль) и STD (установка DF в единицу). Если DF=0, то цепочечные команды будут обрабатывать строки слева направо, автоматически увеличивая индексные регистры SI/ESI и DI/EDI. Если DF=1, то обработка будет происходить в обратном порядке — содержимое индексных регистров будет каждый раз уменьшаться. Всего существует 7 основных команд обработки строк: MOVS, CMPS, SCAS, LODS, STOS, INS и OUTS. Каждая цепочечная команда имеет краткую форму, которая не нуждается в операндах и использует SI и/или DI в 16-битном режиме, а в 32-битном — ESI и/или EDI. При этом адрес сегмента, на который ссылаются SI или ESI (источник), по умолчанию считывается из DS, а DI или EDI (приемник) всегда ссылаются на данные сегмента, адрес которого хранится в ES. Обычно после загрузки приложения Windows регистры DS и ES и так указывают на сегмент данных, поэтому на практике зачастую достаточно поместить лишь эффективный адрес строки- источника в SI/ESI, а строки-приемника — в DI/EDI. Краткая форма цепочечной команды образуется добавлением к мнемонике команды буквы, определяющей размер элемента строки: B — байт, W — слово, D — двойное слово. При использовании полной формы команд необходимо указать размер элемента строки (byte/word/dword), а также операнды приемник и источник: ES:(E)DI, DS:(E)SI. Перед SI/ESI может быть указан любой сегментный префикс, но перед DI/EDI — исключительно ES. Команда MOVS копирует элемент строки, адрес которого задан в SI/ESI в ячейку памяти, указанную в DI/EDI. Размер элемента может быть задан как byte, word или dword: MOVS BYTE [DI],[SI] MOVS WORD [ES:DI],[SS:SI] MOVSD Команда CMPS сравнивает элементы строк приемника и источника путем вычитания элемента строки приемника из элемента строки источника и устанавливает флаги AF, SF, ZF, PF, CF, OF в соответствии с результатом. Сами элементы при этом не изменяются. Если сравниваемые элементы равны, то устанавливается флаг ZF, иначе ZF сбрасывается в ноль. Заметьте, что первым операндом команды CMPS должен быть источник (SI/ESI), а вторым — приемник (DI/EDI): CMPSB CMPS WORD [DS:SI],[ES,DI] CMPS DWORD [FS:ESI],[EDI] Команда SCAS производит сканирование строки с целью поиска заданного значения. Искомое значение необходимо предварительно поместить в AL/AX/EAX в зависимости от размера искомого элемента. SCAS вычитает элемент строки приемника из AL/AX/EAX и устанавливает флаги AF, SF, ZF, PF, CF, OF в соответствии с результатом. Если сравниваемые элементы равны, то устанавливается флаг ZF, иначе он сбрасывается в ноль. Единственный операнд — элемент сканируемой строки, указанный при помощи DI/EDI: SCAS BYTE [ES:DI] SCASW SCAS DWORD [ES:EDI] Команда LODS загружает элемент строки источника в регистр AL/AX/EAX. Операнд, содержащий адрес загружаемого элемента, может быть регистром SI/ESI с любым сегментным префиксом: LODS BYTE [DS:SI] LODS WORD [CS:SI] LODSD Команда STOS сохраняет содержимое AL/AX/EAX в элемент строки приемника. Правила для операнда такие же, как и в команде SCAS. Команда INS вводит элемент строки из порта. Номер порта должен содержаться в регистре DX. Операнд-приемник указывается в DI/EDI: INSB INS WORD [ES:DI],DX INS DWORD [EDI],DX Команда OUTS передает содержимое элемента строки источника в порт вывода, указанный в DX. Операнд-приемник — DX, операнд-источник — SI/ESI: OUTS DX,BYTE [SI] OUTSW OUTS DX,DWORD [GS:ESI] Для автоматической обработки строк всеми вышеперечисленными командами используются префиксы повторения: REP, REPE/REPZ и REPNE/REPNZ. Перед использованием команды с префиксом повторения необходимо поместить в CX/ECX число повторений команды. Префикс повторения автоматически уменьшает регистр CX/ECX и повторяет выполняемую команду до тех пор, пока CX/ECX не будет равен нулю. REPE/REPZ и REPNE/REPNZ прекращают повторение команды не только при CX/ECX=0, но и в зависимости от состояния флага ZF: REPE/REPZ может повторяться лишь до тех пор, пока ZF установлен, а REPNE/REPNZ — пока ZF=0. Повторяемая команда обработки строк будет обрабатывать каждый раз следующий элемент строки. Что ж, посмотрим как можно использовать некоторые цепочечные команды на практике. В прошлой части мы искали пробел в строке, циклически выполняя команду cmp при помощи команды loop. Сейчас мы можем пойти более правильным путем: include 'win32ax.inc' .data stroka db 'Ищем пробел в этой строке',0 dlina_stroki = $-stroka msg_no_spc db 'Нет пробелов',0 msg_spc db 'Первый пробел — символ № ' msg_spc_2 db 0,0,0 .code start: cld mov ecx,dlina_stroki lea edi,[stroka] mov al,' ' repne scasb jecxz net_probelov mov eax,edi sub eax,stroka aam or ax,3030h xchg al,ah mov word [msg_spc_2],ax invoke MessageBox,0,msg_spc,0,0 jmp exit net_probelov: invoke MessageBox,0,msg_no_spc,0,0 exit: invoke ExitProcess,0 .end start Командой CLD обнуляем флаг DF, чтобы обрабатывать элементы строки слева направо. Задвигаем в ECX длину строки. Загружаем в EDI эффективный адрес строки. В AL помещаем значение символа "пробел". Префикс REPNE заставит команду SCASB повторяться, пока результатом сравнения элемента строки с содержимым AL будет Not Equal (Не Равно). Или пока автоматически уменьшаемое содержимое ECX не сравняется с нулем после проверки последнего символа строки. Если пробел будет найден, то в EDI будет содержаться смещение следующего за ним символа. Однако это будет смещение относительно начала сегмента, а не относительно начала строки. В нашем случае начало строки и начало сегмента совпадают, потому что наша строка самая первая в секции данных. Но мы должны предусматривать все варианты, и потому после копирования в EAX значения из EDI необходимо отнять от этого значения относительный адрес начала строки: sub eax,stroka. Тонкости приведения результата к символьному виду были описаны в прошлый раз. Теперь посмотрим, что мы сможем сделать интересного с этими командами. Как вам идея бегущей строки в заголовке окна? Раз строка — значит, обрабатывать ее надо командой обработки строк. Как проще всего представить реализацию бегущей строки? Правильно: это повторяющийся цикл, на каждом повторении которого происходит перенос одного крайнего символа в другой конец строки. Как будто вы сложили текст из детских кубиков и по одному переставляете кубики из одного конца строки в другой. Памяти под содержимое строки у нас выделяется ровно столько, сколько символов в строке. Поэтому представим, что кубики лежат в узкой длинной коробке, длина которой совпадает с длиной строки кубиков. В таком случае для циклического сдвига строки влево последовательность наших действий будет следующая: взять крайний кубик, сдвинуть кубики влево, положить взятый кубик на освободившееся место справа. Вот и весь алгоритм. Берем кубик — тьфу, символ — в регистр командой mov, сдвигаем строку влево командой movsb с префиксом rep, кладем символ обратно с другого конца строки: … stroka db 'Бегущая строка !!! ',0 strlen = $-stroka-1 … cld mov ecx,strlen-1 lea esi,[stroka+1] lea edi,[stroka] mov byte al,[edi] rep movsb mov byte [edi],al … Этот кусок кода сдвигает строку влево на один символ, перемещая первый символ строки на место последнего. Чтобы не сдвигать завершающий строку нуль-терминатор, при определении strlen добавляем минус один. А в ECX помещаем еще на единицу меньше, так как один символ будет обработан отдельно через регистр AL. В приемник загружаем адрес начала строки, а в источник — адрес второго символа (stroka+1). Перед запуском копирования цепочки байтов сохраняем первый символ в AL. После копирования извлекаем его по адресу, содержащемуся в EDI, — это будет уже адрес последнего символа строки. Для реализации бегущей строки в заголовке окна нам недостает организации цикличного повторения кода сдвига строки, установки измененной строки новым текстом заголовка окна и, наконец, кода самого окна. С последним проще: в качестве основы программы возьмите TEMPLATE.ASM из папки ..\FASM\EXAMPLES\TEMPLATE\. Желательно работать с копией файла, потому что оригинал еще может пригодиться. Вставьте в секцию данных первые две строчки вышеприведенного кода. А вот со вставкой остальной части в секцию кода пока повремените. Еще раз вспомним наши условия. Требуется, чтобы код, производящий циклический сдвиг строки, повторялся через определенный промежуток времени, а после каждого сдвига строка устанавливалась новым текстом заголовка. Для выполнения каких-либо действий в оконном приложении Windows через определенные промежутки времени в операционной системе предусмотрена достаточно полезная API-функция SetTimer. Параметры у нее следующие: 1. Дескриптор окна, которое будет получать сообщения таймера. Окно должно принадлежать вызывающему функцию процессу. 2. Отличный от нуля идентификатор таймера. Если первый параметр не указан, то и этот параметр игнорируется. 3. Время задержки в миллисекундах, через которое будут приходить сообщения. 4. Указатель на процедуру обработки сообщений таймера в приложении. Если указать ноль, то сообщения WM_TIMER будут приходить в общую процедуру обработки сообщений. Короче, чтобы кинуть таймер на нашу прогу, дописываем сразу после создания основного окна строку запуска таймера. После успешного создания окна его дескриптор должен быть в EAX, поэтому сие будет выглядеть примерно так: … invoke CreateWindowEx,0,_class,_title,WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU, 128,128,256,192,NULL,NULL,[wc.hInstance],NULL test eax,eax jz error invoke SetTimer,eax,1,100,0 … Добавляем обработку сообщений от таймера в процедуру обработки сообщений, посланных окну, и обработчик: … cmp [wmsg],WM_TIMER je .wmtimer … .wmtimer: ;сюда вставляем код, который будет выполняться по получению сообщения от таймера jmp .finish … Теперь остается вставить код сдвига строки в обработчик сообщений от таймера, добавив после сдвига строку: invoke SetWindowText,[hwnd],stroka для установки измененной строки новым текстом заголовка окна. Ах, да, чуть не забыл: в самом начале шаблона программы замените include 'win32w.inc' на include 'win32a.inc'. W — это юникод, A — это ANSI. Мы же считали, что каждый символ будет занимать один байт, а не два, как в юникоде. Хотя при желании можно внести некоторые изменения и корректно обрабатывать строку в юникоде: … strlen = ($-stroka)/2-1 … cld mov ecx,strlen-1 lea esi,[stroka+2] lea edi,[stroka] mov word ax,[edi] rep movsw mov word [edi],ax … В этом случае не забудьте подключить кодировку 1251: include 'encoding\WIN1251.INC' Чтобы получить длину строки в символах, а не в байтах, делим длину в байтах на два. В ECX помещается количество символов для обработки, поэтому ничего не меняем. В приемник, как и в примере с ANSI, загружается тот же адрес. А вот в источник грузим stroke+2, потому что каждый символ занимает 2 байта. Первый символ, естественно, помещаем в двухбайтный AX. Сдвиг производим командой MOVSW. Ну что, тяжело в учении? Согласен, тяжело. А кому сейчас легко? Перечитайте материал еще раз, и двигаемся дальше. Сделаем нашу строку не только бегущей, но и прыгающей. Добавьте в ANSI-версию программы бегущей строки перед вызовом функции SetWindowText следущие строки:  … mov ecx,strlen lea edi,[stroka] .cycl: .if byte [edi]>='A' & byte [edi]<='Z' add byte [edi],('a'-'A') .elseif byte [edi]>='a' & byte [edi]<='z' add byte [edi],('A'-'a') .elseif byte [edi]>='А' & byte [edi]<='Я' add byte [edi],('а'-'А') .elseif byte [edi]>='а' & byte [edi]<='я' add byte [edi],('А'-'а') .endif add edi,1 loop .cycl … Не забудьте подключить описания макросов IF: include 'macro/if.inc' Теперь объясню, что делает этот код. Макрос .if (если) генерирует код для проверки простых условий и выполнения следующих за макроинструкцией команд в зависимости от результата. Блок условно выполняющихся команд обязательно должен заканчиваться инструкцией .endif (конец блока если). Перед ней могут быть использованы инструкции .elseif (иначе если) для обозначения кода, выполняемого при истинности условия, если ранее уже не была встречена истина. Также последний блок условно выполняемого кода перед .endif может быть обозначен инструкцией .else. Этот блок будет исполнен, если ни одно из условий не выполнено. Итак, для того, чтобы строка "прыгала", необходимо менять буквы верхнего регистра на нижний, а нижнего — на верхний. Числовые значения, обозначающие буквы в ANSI-кодировке ASCII идут по порядку от латинского 'A'(41h) до 'Z'(5Ah), от латинского 'a'(61h) до 'z'(7Ah). Подобным образом дело обстоит и с русскими буквами. Поэтому, если значение символа больше или равно латинскому 'A' и при этом меньше или равно 'Z', то мы можем утверждать, что это заглавная буква латинского алфавита. По аналогии мы определяем прописные латинские буквы и кириллические символы. Числовое значение символа 'a' больше значения 'A' ровно настолько же, насколько значение 'b' больше 'B', и т.д. Так что для изменения регистра латинской буквы с верхнего на нижний надо прибавить к значению эту разность. Для изменения регистра с нижнего на верхний — отнять или прибавить отрицательную разность. Теперь, думаю, вам стало ясно, каким образом можно изменить регистр символа. Заметьте, что, если символ не является буквой, то он и не подвергнется изменению. Ну и, естественно, проверка и изменение регистра производится столько раз, сколько мы указали через ECX. Чтобы сделать только прыгающую строку (без бегущей составляющей), вам надо удалить из программы код сдвига строки. Для получения более привлекательного эффекта напишите строку буквами с чередующимся регистром — например: "ПрЫгАюЩаЯ СтРоКа!!!". Можно оформить заголовок эффектом "бегущая буква". Для этого нам придется ввести переменную, которая будет хранить смещение "бегущей буквы" относительно начала строки. Чтобы обозначить 32-битную переменную и инициализировать ее значение нулем, допишите в секцию данных: k dd 0 Теперь полностью замените обработчик сообщения таймера: … .wmtimer: lea edi,[stroka] add edi,[k] mov byte bl,[edi] .if byte [edi]>='A' & byte [edi]<='Z' add byte [edi],('a'-'A') .elseif byte [edi]>='a' & byte [edi]<='z' add byte [edi],('A'-'a') .elseif byte [edi]>='А' & byte [edi]<='Я' add byte [edi],('а'-'А') .elseif byte [edi]>='а' & byte [edi]<='я' add byte [edi],('А'-'а') .endif invoke SetWindowText,[hwnd],stroka mov byte [edi],bl .if [k] < strlen-1 inc [k] .else mov [k],0 .endif jmp .finish … У нас получился достаточно простой алгоритм. При каждом сообщении таймера в EDI загружается адрес начала строки и увеличивается на содержимое K. Теперь, когда EDI содержит адрес обрабатываемого символа, мы временно копируем значение этого символа в BL. Далее — знакомая нам проверка и смена регистра буквы. Вывод измененной строки в заголовок. Возвращаем символ из BL, чтобы привести строку в памяти к изначальному виду. Увеличиваем K на единицу при условии, что K<strlen-1. Иначе — обнуление K. Почему сравниваем K со strlen-1? Потому, что K — это смещение, а не порядковый номер символа. Для первого символа K=0, для второго — K=1, для энного символа K будет равняться n-1. Здесь есть еще один важный момент, на котором я хотел бы заострить ваше внимание. После сохранения символа в регистр BL, но перед его восстановлением происходит вызов API- функции SetWindowText. Часто в подобных ситуациях начинающий программист забывает, что вызываемая API-функция обычно использует содержимое некоторых регистров под собственные нужды и изменяет их содержимое. Например, если бы для временного хранения значения символа мы использовали AL или CL вместо BL, результат был бы далек от требуемого. Следует помнить, что после использования API-функции windows сохраняются лишь регистры EBX, EBP, ESP, ESI, EDI. Остальные регистры придется сохранять вручную (команда PUSH) и восстанавливать (команда POP) после использования API-функции, если, конечно, их содержимое вас интересует в дальнейшем. Теперь можно немного упростить предыдущий эффект и получить эффект ручного ввода символов. Задача: отображать сначала один символ строки, потом — два, потом — три и т.д. Решение: … lea edi,[stroka] add edi,[k] mov byte bl,[edi] mov byte [edi],0 invoke SetWindowText,[hwnd],stroka mov byte [edi],bl .if [k] < strlen inc [k] .else mov [k],0 .endif jmp .finish … Этот вариант похож на предыдущий, только вместо замены регистра символа мы просто временно записываем ноль на его место. Как известно, строка в windows завершается нулем. Строка, у которой первый символ имеет нулевое значение, считается и отображается пустой. Если мы поставим ноль, к примеру, вместо четвертого символа, то отображаемая строка будет состоять из трех символов. Поэтому на этот раз мы ставим условие [k] < strlen: последним обрабатываемым символом в этом алгоритме должен являться ноль-терминатор. Хотя он и так содержит ноль, но только так мы сможем в конце цикла отобразить строку целиком. Для большего эффекта можно добавить звуковое оформление: … .if [k] < strlen inc [k] invoke Beep,37,10 .else mov [k],0 invoke Beep,370,50 .endif … Функция Beep генерирует звук на системном динамике. Первый параметр — частота воспроизводимого сигнала в герцах — может находиться в пределах от 37 до 32767. Второй пареметр — время звучания в миллисекундах. Будьте осторожны со вторым параметром, потому что функция не возвращает управление программе до завершения воспроизводимого звука. Если вы установите слишком большое время звучания сигнала, то не сможете закрыть программу до прекращения звука.

Сегодня мы поговорим о массивах данных. Работая за компьютером, с массивами данных мы сталкиваемся практически повсеместно: сортировка файлов, индексированный поиск, электронные таблицы, списки и многое, многое другое. Теперь, когда мы изучили столько важных команд ассемблера, можем всерьез заняться изучением алгоритмов работы с массивами. Тема эта весьма сложная, но без умения управляться с массивами данных нельзя далеко продвинуться в программировании. Так что собирайтесь с силами, и приступим к изучению массивов. Массив — это структурированный тип данных, состоящий из нескольких элементов одного типа. В высокоуровневых языках существуют специальные средства для описания массивов. В ассемблере такие средства отсутствуют, а потому массив обычно обозначается просто как линейная область данных. Например, чтобы обозначить одномерный массив однобайтовых элементов и выделить под него память, обычно достаточно директивы RB (Reserve Bytes — Зарезервировать Байты), за которой следует число резервируемых байт. Если элемент массива должен иметь размер в два байта, будем использовать директиву RW (Reserve Words) и число резервируемых слов. Для четырехбайтовых элементов — соответственно RD и количество двойных слов и так далее. Если мы хотим не только выделить память, но и сразу внести в массив значения элементов, можно описать элементы при помощи стандартных директив описания данных — таких, как DB, DW, DD и т.д. За директивой определения данных должно следовать одно или несколько числовых выражений, разделенных запятыми. Эти выражения определяют значение элементов массива. Если вместо значения указан символ вопроса, то значение считается неопределенным, то есть таким же, как если бы мы использовали директиву резервирования данных. Размер элемента зависит от того, какая директива используется. Табл. 1. Директивы определения и резервирования данных

Размер  элемента  в байтах

Директивы  определения  данных

Директивы  резервирования  данных

1

db file

rb

2

dw du

rw

4

dd

 

6

dp df

rprf

8

dq

rq

10

dt

rt

Для инициализации элементов массива одним и тем же значением или повторяющейся цепочкой значений можно использовать специальный оператор DUP. Количество дубликатов указывается перед DUP, а дублируемое значение или цепочка значений, разделенных запятыми, указывается после оператора DUP и заключается в скобки. Например, db 5 dup (1,2) означает, что необходимо создать 5 копий указанной последовательности из двух байт. Массивы бывают статические и динамические. Динамические массивы отложим на потом, а пока попробуем разобраться хотя бы со статическими. Размер статического массива не меняется на протяжении времени работы программы. Поэтому с ними работать несколько проще. Достаточно выделить область памяти под статический массив лишь один раз, и потом остается лишь работать с содержимым массива — его элементами.  Перечислим основные операции, которые мы можем производить над массивами: — ввод данных в массив; — вывод данных из массива; — поиск значения в массиве; — сортировка элементов. Начнем мы, пожалуй, с операции вывода, чтобы позже у нас было на чем проверить правильность наших операций ввода, поиска и сортировки. Создадим массив из пяти элементов размером в один байт и выведем значение каждого элемента в отдельном поле окна: format PE GUI 4.0 entry start include 'win32a.inc' ;константы MASSIZE=5 ;количество элементов массива BUFSIZE=3 ;макс.кол-во знаков в элементе ;(для буфера вывода десятичных значений) section '.data' data readable writeable _class db 'FASMWIN32',0 _cedit db 'EDIT',0 _title db 'Работа с массивами',0 _error db 'Ошибка запуска.',0 buf rb BUFSIZE+1 ;+1 для нуль-терминатора mas db 123,23,3,4,5 hmas dd MASSIZE dup (?) wc WNDCLASS 0,WindowProc,0,0,NULL,NULL,NULL,COLOR_BTNFACE+1,NULL,_class msg MSG section '.code' code readable executable start: invoke GetModuleHandle,0 mov [wc.hInstance],eax invoke LoadIcon,0,IDI_APPLICATION mov [wc.hIcon],eax invoke LoadCursor,0,IDC_ARROW mov [wc.hCursor],eax invoke RegisterClass,wc test eax,eax jz error invoke CreateWindowEx,0,_class,_title,WS_VISIBLE+WS_DLGFRAME+ WS_SYSMENU,128,128,256,192,NULL,NULL,[wc.hInstance],NULL test eax,eax jz error msg_loop: invoke GetMessage,msg,NULL,0,0 cmp eax,1 jb end_loop jne msg_loop invoke TranslateMessage,msg invoke DispatchMessage,msg jmp msg_loop error: invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK end_loop: invoke ExitProcess,[msg.wParam] proc WindowProc hwnd,wmsg,wparam,lparam push ebx esi edi cmp [wmsg],WM_CREATE je .wmcreate cmp [wmsg],WM_DESTROY je .wmdestroy .defwndproc: invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam] jmp .finish .wmcreate: ;создаем поле под каждый элемент массива mas ;и помещаем дескриптор каждого поля в массив hmas mov esi,5 mov edi,5 xor ebx,ebx .create_cycl: invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ES_RIGHT,esi,edi,40,20,[hwnd],ebx,[wc.hInstance],0 test eax,eax jz error mov [hmas+ebx*4],eax add esi,50 inc ebx cmp ebx,MASSIZE jne .create_cycl .out_mas: ;заполняем поля значениями элементов массива xor ebx,ebx .masout_cycl: mov ah,[mas+ebx] lea edi,[buf+BUFSIZE] .dec: movzx ax,ah aam or al,30h dec edi mov byte [edi],al test ah,ah jnz .dec invoke SendMessage,[hmas+ebx*4],WM_SETTEXT,0,edi inc ebx cmp ebx,MASSIZE jne .masout_cycl jmp .finish .wmdestroy: invoke PostQuitMessage,0 xor eax,eax .finish: pop edi esi ebx ret endp section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL' include 'api\kernel32.inc' include 'api\user32.inc' Для верного отображения значений элементов они должны находиться в диапазоне от 0 до 255 включительно, то есть являться однобайтовыми беззнаковыми целыми. Позже, научимся отображать и числа со знаком, но сейчас нам это только осложнит задачу и отвлечет от понимания принципов работы с массивами. Раз самое длинное число у нас состоит из трех знаков, то и буфер для его вывода (buf) будет иметь размер 3+1 байта. Четвертый байт выделяется специально под нолик, обозначающий конец строки. Значения всех пяти элементов массива (mas) мы на данном этапе определим сразу в секции данных, поскольку функция ввода значений массива пока что отсутствует в нашей программе. Массив hmas (h означает handles — дескрипторы), состоящий из пяти двойных слов (MASSIZE=5), будет содержать дескрипторы полей для вывода значений элементов массива. Так нам на первых порах будет проще: берем значение первого элемента mas, преобразовываем его к символьному виду и выводим в поле, дескриптор которого хранится в первом элементе hmas. Потом выводим значение второго элемента mas в поле, дескриптор которого берем соответственно из второго элемента hmas. Аналогично поступаем с третьим элементом, четвертым, пятым… да хоть миллионным, лишь бы все поля уместились на экране, а массивы значений и дескрипторов — в памяти компьютера. Разумеется, весь этот вывод нам выгоднее оформить в повторяющемся цикле. Этот цикл и будет скелетом функции вывода. Не будем останавливаться на участках подготовки и создания окна, так как это было подробно описано еще в первых частях данного курса. Обратим внимание сразу на обработчик сообщения WM_CREATE. Здесь мы создаем поле под каждый элемент массива mas и помещаем дескриптор каждого созданного поля в соответствующий элемент hmas. Это еще не сам вывод, но создание полей для вывода значений. Перед циклом мы помещаем в esi и edi соответственно X- и Y-координаты расположения первого поля относительно левого верхнего угла клиентской области нашего основного окна. На каждом шаге цикла мы будем увеличивать esi (координата X) на 50, исходя из того, что ширина самого поля равна 40, а 10 — зазор между соседними полями. Ввиду того, что пять полей прекрасно умещаются в одну строку, содержимое edi мы сейчас изменять не станем. Его на данном этапе можно было бы и не использовать вообще, но это — так сказать, задел на будущее. Подробно разберем, что происходит в цикле ".create_cycl:". Первым делом мы создаем окно стандартного класса "EDIT" с указанными параметрами. Содержимое EBX выступает в качестве идентификатора каждого нового окна, поэтому поля будут иметь идентификаторы 0, 1, 2 и т.д. Это, конечно, не самый красивый вариант, но в данном примере мы не будем обращаться к окнам по их идентификаторам, а потому имеем полное право указать даже один и тот же идентификатор для всех создаваемых полей. Но я решил их все же сделать отличными друг от друга без каких-либо дополнительных действий. Впрочем, это мелочи. Главное назначение регистра EBX в этом цикле — счетчик. Сразу после создания окна мы убеждаемся в отсутствии ошибки и помещаем содержимое EAX (дескриптор созданного окна) в очередной элемент массива hmas. Вам понятно, как это происходит? Элементы считаются в программировании с нулевого (а не с первого) именно потому, что первый элемент никуда не смещен относительно начала строки или массива. Он первый, он находится по адресу начала массива, и его смещение — нулевое. А вот второй элемент уже имеет смещение на размер одного (первого) элемента, третий — на размер двух элементов, четвертый — трех и т.д. Размер дескриптора, как и большинства других элементов в 32-битной системе, равняется 32 битам или 4 байтам. Поэтому мы умножаем 4 на EBX, чтобы получить смещение очередного элемента от начала массива. А затем прибавляем к нему еще смещение самого hmas относительно начала сегмента. Таким образом, для первого элемента смещение получится равным hmas+0, для второго — hmas+4, для третьего — hmas+8 и т.д. Обычно в качестве счетчика используют регистр ECX, который изначально и задуман для этих целей. Однако API-функции (в нашем случае функция CreateWindowEx) используют некоторые регистры в своих целях и могут изменить содержимое этих регистров за время своего исполнения. Гарантированно сохраняется лишь содержимое регистров EBX, EBP, ESP, ESI, EDI. Поэтому содержимое остальных регистров по необходимости следует сохранить командой PUSH, а потом восстановить командой POP. В нашем случае такой необходимости нет, так как в данном цикле мы вполне можем обойтись регистрами EBX, ESI, EDI. Итак, мы сохраняем дескриптор очередного созданного поля EDIT в массив hmas. Увеличиваем ESI на 50, чтобы следующее поле было создано на 50 точек правее. Увеличиваем EBX на единицу. И повторяем цикл при условии, что EBX еще не равен количеству элементов массива. Если EBX=MASSIZE — значит, мы создали поля для всех элементов массива и можем переходить к выводу значений в эти поля. Тут же прописана метка ".out_mas:" для того, чтобы можно было впоследствии осуществлять повторный вывод текущих значений без повторного создания полей. Перед циклом вывода обнуляем наш счетчик EBX. Задвигаем в AH значение текущего элемента (mas+счетчик*размер_элемента). Так как размер элемента у нас 1 байт, то есть единица, пишем просто mas+EBX. Загружаем в EDI эффективный адрес последнего символа буфера — нуль-терминатора. В данном примере буфер у нас состоит из трех символов + нуль-терминатор, потому что константа BUFSIZE у нас равна трем. Адрес первого символа буфера — buf, адрес второго — buf+1, …, адрес нуль-терминатора — buf+3 или buf+BUFSIZE. Адрес нуль-терминатора нам, конечно, не нужен. Но для корректного перевода числа из машинного представления в символьное нам необходимо записывать символы, начиная с конца. Поэтому мы сразу поместим в EDI адрес последнего символа, а потом будем уменьшать EDI на единицу перед записью каждого следующего символа. Таким образом мы запишем сначала число единиц, затем — число десятков, затем — сотен. Для приведения числа к символьному виду можно было бы воспользоваться и API-функцией ОС — например, wsprintf. Но давайте на всякий случай научимся делать такие вещи вручную. Это может сильно помочь вам в понимании низкоуровневых алгоритмов преобразования типов.  Табл. 2. Размещение разрядов в буфере

Смещение

buf+0

buf+1

buf+2

buf+3

Назначение

сотни

десятки

единицы

нуль-терминатор

Цикл ".masout_cycl:" повторяется для каждого элемента массива, а цикл ".dec:" — для каждого десятичного разряда выводимого элемента. На первом шаге цикла ".dec:" в AH находится значение текущего элемента массива. Чтобы выделить из него число единиц, мы копируем его в AL. Команда AAM, как вы уже знаете, делит содержимое AL на 10 и помещает частное в AH, а остаток — в AL. Остаток от деления числа на 10 и будет числом единиц в нем. Частное на следующем шаге цикла можно будет снова поделить на 10 и получить в остатке уже число десятков, а потом — и сотен. Но сейчас, чтобы привести число единиц в AL к символьному виду, мы выполняем команду or al,30h. Табл. 3. Символьное представление чисел в ASCII

Символ

Шестнадцатеричное значение (ASCII-код)

Двоичное  значение (ASCII-код)

0

30h

00110000b

1

31h

00110001b

2

32h

00110010b

3

33h

00110011b

9

39h

00111001b

Как видно из третьей таблицы, шестнадцатеричное значение символа отличается от машинного представления числа ровно на 30h. То есть, чтобы число от нуля до девяти преобразовать в значение соответствующего ему символа, достаточно прибавить к числу 30h. Двоичные значения я тоже привел неслучайно. Из них понятно, что 30h — это, по сути, два установленных бита — пятый и шестой. То есть, если мы к двоичному значению числа от 0 до 9 (от 0b до 1001b) применим команду OR со вторым операндом 30h (00110000b), то получим соответствующий этому числу ASCII-код символа. Стало быть, в таких случаях можно на равных правах использовать как команду ADD, так и команду OR. Привели число единиц к символьному виду, уменьшаем EDI (а то он у нас еще на нуль-терминатор указывает, а нам уже единицы в буфер вписать требуется). Копируем байт из AL по адресу, содержащемуся в EDI. Тестируем AH на предмет нулевого содержимого. Если в AH (ведь это частное нашего последнего деления) содержится ноль — значит, десятков в данном элементе нет (или нет сотен на втором шаге цикла), и мы не станем повторять цикл ".dec:", а перейдем к выводу полного значения данного элемента в соответствующее поле. Если же AH нулю не равен — значит, еще остались старшие разряды, которые необходимо обработать аналогичным способом. Функцией SendMessage выводим значение элемента в текущее поле. Дескриптор берем по адресу hmas+ebx*4. Выводимый текст — по адресу, хранящемуся в EDI. Обратите внимание, что не по адресу buf, а именно по адресу в EDI! Буфер целиком у нас содержит неопределенное значение. Если, к примеру, первый элемент состоял из трех десятичных знаков, а второй — из двух, то на момент вывода второго элемента в окно буфер будет содержать в первом байте сотни, оставшиеся от первого элемента, и только со второго байта пойдут десятки текущего элемента. Поэтому верное значение текущего элемента у нас будет только с адреса в EDI. Именно по данному адресу в буфер был прописан последний старший разряд текущего элемента. Увеличиваем EBX на единицу, и, если он еще не равен MASSIZE, переходим в начало цикла ".masout_cycl:" для вывода следующего элемента в следующее поле. Иначе прыгаем на финиш ввиду того, что все элементы выведены на экран, то есть в окно на экране. Ну, а теперь тем, кому "деление на десять" показалось слишком сложным циклом, покажу упрощенный вариант с использованием функции wsprintf. Преобразование значения в символьный вид выполнит за нас эта функция, поэтому мы сможем избавиться от цикла ".dec:". Мы обсудили возможности данной функции еще в четвертой части курса. В первом параметре указывается буфер для результата, во втором — задается формат, а в остальных указываются данные, которые необходимо преобразовать. Так что нам остается лишь добавить в секцию данных строку с образцом формата: form db '%u',0 Управляющий символ '%u' означает тип "unsigned integer" — беззнаковое целое. И теперь мы можем цикл вывода упростить до следующего вида:  .out_mas: xor ebx,ebx .masout_cycl: movzx eax,[mas+ebx] invoke wsprintf,buf,form,eax invoke SendMessage,[hmas+ebx*4],WM_SETTEXT,0,buf inc ebx cmp ebx,MASSIZE jne .masout_cycl jmp .finish Функции 32-битных ОС обычно нуждаются в том, чтобы параметры были 32-битными. Поэтому командой movzx мы расширяем 8-битное значение из ячейки [mas+ebx] до 32-битного в регистре eax. Вызываем wsprintf и получаем в буфере искомое значение в символьном виде. Далее все так же, как и в первом варианте программы, но… Помните, друзья мои, что бесплатный сыр бывает только в мышеловке! Используя для такого простого преобразования всю мощь универсальной функции wsprintf, мы совершаем кучу лишних действий и слегка теряем в скорости исполнения. Конечно, на такой простенькой программе вы этого не заметите. На современных мощных компьютерах вы также можете не увидеть разницу невооруженным глазом даже в более прожорливой программе. Но вы же планируете в дальнейшем писать намного более серьезные вещи, не так ли? А значит, всегда следуйте хакерским путем: сперва используйте наиболее простой для понимания метод, а затем оптимизируйте его, преобразуя в более простой для исполнения процессором. Сейчас часто можно услышать, что экономия в несколько тактов или пару байт памяти ничего не дает. Даже и не пытайтесь с этим спорить. Но для себя не забывайте, что вычислительные мощности с каждым днем растут, а большинство программ почему-то становятся все более тормознутыми. Весьма странная закономерность! Да не будем обращать внимание на ошибки окружающих, а вместо этого давайте все же не лениться экономить такты и байты. На сегодня все. В следующий раз продолжим о массивах.

В прошлый раз мы познакомились с массивами и научились выводить простейший одномерный массив в окне программы. Сегодня — научимся производить поиск в массиве и сортировку элементов по их значению.  Сортировка элементов массива — достаточно сложная тема даже если речь идет о высокоуровневом языке. А уж если говорить об ассемблере, то здесь новички боятся сортировки как огня. Но лишь в самом начале. Потом приходит понимание вопроса, и выясняется, что бояться, собственно, и нечего. Остается лишь вопрос: что же было сложного в изучении сортировки? Основная сложность понимания сортировки — это цикл в цикле. Существует несколько основных способов сортировки, но все они состоят из вложенных один в один циклов. И вот, когда в учебнике или в ВУЗе вам ни с того ни с сего представляют к изучению такой двойной цикл, у вас сначала очень быстро глаза разбегаются, а потом достаточно медленно, но верно пропадает желание что-либо понимать. Чтобы такого не произошло, мы наглядно разобьем сортировку на два этапа, и, лишь детально изучив их по отдельности, соединим воедино. А до этого давайте пока в качестве развлечения быстренько научимся производить поиск! Что искать-то будем? Просто искать заданное значение неинтересно — мы этим уже занимались в 10-й и 11-й частях. Ну и что, что мы искали пробел в текстовой строке? Пробел — это вполне конкретное значение (20h), как и любой другой символ, а строка состоит как раз из нескольких элементов одного типа, так что представляет собой не что иное, как одномерный массив. Так что увольте, но искать определенное значение надо было раньше. Сейчас мы будем искать максимальное и минимальное значения в массиве. Стратегия поиска такова:  1. Определяем две ячейки размером с элемент массива: одну под минимальное значение, вторую — под максимальное. 2. Помещаем в обе этих ячейки значение первого элемента массива. 3. Сравниваем в цикле каждый элемент массива (начиная уже со второго) с нашими минимумом и максимумом, и, если элемент больше максимума, заменяем им значение максимума, а если элемент окажется меньше минимума, заменяем им значение минимума. Возьмем за шаблон программу вывода массива из прошлого занятия и немного ее доработаем: … ;секция данных _cstatic db 'STATIC',0 _tmin db 'MIN:',0 _tmax db 'MAX:',0 mas db 123,23,3,4,5,0,0 hmas rd MASSIZE hmin rd 1 hmax rd 1 … ;секция кода … .wmcreate: ;создаем поле под каждый элемент массива mas ;и помещаем дескриптор каждого поля в массив hmas mov esi,5 mov edi,5 xor ebx,ebx .create_cycl: invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ ES_RIGHT,esi,edi,40,20,[hwnd],ebx,[wc.hInstance],0 test eax,eax jz error mov [hmas+ebx*4],eax add esi,50 inc ebx cmp ebx,MASSIZE jne .create_cycl ;создаем другие элементы invoke CreateWindowEx,0,_cstatic,_tmin,WS_VISIBLE+WS_CHILD+ES_RIGHT, 5,30,40,20,[hwnd],ebx,[wc.hInstance],0 invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ES_RIGHT,55,30,40,20,[hwnd],ebx,[wc.hInstance],0 test eax,eax jz error mov [hmin],eax invoke CreateWindowEx,0,_cstatic,_tmax,WS_VISIBLE+WS_CHILD+ES_RIGHT, 105,30,40,20,[hwnd],ebx,[wc.hInstance],0 invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ES_RIGHT,155,30,40,20,[hwnd],ebx,[wc.hInstance],0 test eax,eax jz error mov [hmax],eax .find_minmax: mov al,[mas] mov ah,al mov ebx,1 .find_cycl: cmp [mas+ebx],ah jna @f mov ah,[mas+ebx] @@: cmp [mas+ebx],al jnb @f mov al,[mas+ebx] @@: inc ebx cmp ebx,MASSIZE jne .find_cycl mov word [mas+MASSIZE],ax .out_mas: ;заполняем поля значениями элементов массива xor ebx,ebx .masout_cycl: mov ah,[mas+ebx] lea edi,[buf+BUFSIZE] .dec: mov al,ah aam or al,30h ;можно заменить на add al,30h dec edi mov byte [edi],al test ah,ah jnz .dec invoke SendMessage,[hmas+ebx*4],WM_SETTEXT,0,edi inc ebx cmp ebx,MASSIZE+2 jne .masout_cycl jmp .finish … Как вы могли заметить, и в mas, и в hmas внесено два дополнительных элемента. Причем в mas они добавлены явно, а в hmas — неявно, а как отдельные переменные hmin и hmax. Тем не менее, они определены сразу следом за hmas и условно считаются продолжением массива. Такое решение является оптимальным для вывода в окно значений минимума и максимума. Нам не придется отдельно вызывать преобразование значений элементов min и max — оно будет выполнено сразу в цикле вывода элементов массива. Только для этого следует при проверке окончания вывода указать значение MASSIZE+2, чтобы были выведены не только значения массива, но и два дополнительных элемента. Естественно, следует предварительно еще при создании окна создать поля для этих элементов и поместить их дескрипторы в соответствующие ячейки hmas, имеющие для удобства чтения кода личные псевдонимы — hmin и hmas. Это проще сделать сразу после цикла создания полей под элементы массива. Минимальное значение будет у нас сохраняться в AL, а максимальное — в AH. Поэтому перед началом поиска мы копируем значение первого элемента в AL и в AH. EBX мы будем использовать в качестве счетчика, поэтому загружаем в него единицу: mas+1 укажет как раз на второй элемент, с которого мы начнем сравнение. В цикле сравниваем очередной элемент с AH, и, если он не больше текущего максимума, то переходим к сравнению с минимумом. Иначе, если значение оказалось больше, помещаем его в AH. Подобным же образом поступаем с минимумом, только минимум заменяем значением текущего элемента лишь в случае, если элемент окажется меньше. Увеличиваем EBX, сравниваем его с количеством элементов, повторяем цикл, если еще не все элементы были проверены. По окончании поиска минимума и максимума нам необходимо сохранить их значения в два дополнительных байта за массивом: AL — по адресу mas+MASSIZE, AH — по адресу mas+MASSIZE+1. А лучше это сделать за один раз, сохранив двухбайтовый AX по адресу mas+MASSIZE. В таком случае содержимое AL уляжется по младшему адресу, а AH — по старшему точно так, как требуется. Вот и все. Остается только вывести все значения, а это мы уже умеем. Только не забудьте про MASSIZE+2. Сделайте передышку. Если чего-то не поняли — перечитайте разок-другой. Теперь приступим к покорению алгоритмов сортировки. Существует много способов сортировки. Не знаю, имеет ли смысл сейчас рассматривать их все, но три базовых мы уж точно осилим. Начнем с метода сортировки прямым обменом. В народе его еще называют методом пузырьковой сортировки, потому что на каждом этапе наибольшее из еще несортированных значений как бы "всплывает" над меньшими. Это, конечно, в том случае, если сортировка производится в порядке возрастания значений, с коей мы и начнем. На первом этапе самое большое число занимает свое крайнее правое место, на втором — второе по значимости занимает второе место справа, на третьем — третье и т.д. Причем на каждом этапе не только самое большое из несортированных чисел становится на свое место, но и могут "всплывать" некоторые "пузырьки" чуть меньшего размера. Начнем с реализации первого этапа, а остальные потом просто повторим в цикле. Не волнуйтесь: все разберем детально и разложим по полочкам! Для того, чтобы самое большое число массива заняло крайнее правое место, нам необходимо будет выполнить небольшой цикл операций: 1. Обнуляем счетчик элементов, чтобы адрес [mas+счетчик] указывал на первый элемент массива. 2. Сравниваем текущий элемент (по адресу mas+счетчик) со следующим за ним, и, если выясняется, что текущий элемент имеет большее значение, чем следующий, то меняем местами текущий и следующий элементы. Если же текущий элемент не больше следующего (меньше или равен), то сразу переходим к третьему пункту. 3. Увеличиваем счетчик, и, если он еще не указывает на последний элемент (за последним элементом уже не будет следующего, с которым можно сравнивать текущий), повторяем цикл со второго пункта. Если мы возьмем за шаблон программу, написанную в предыдущей части статьи, то для реализации первого этапа сортировки нам будет достаточно добавить кнопку "сортировка по возрастанию" и преобразовать вышеуказанный алгоритм в ассемблерный код обработки нажатия этой кнопки: … ID_SV =1001 … _cbutton db 'BUTTON',0 _tsvbut db 'Сортировать по возрастанию',0 … cmp [wmsg],WM_COMMAND je .wmcommand … .wmcommand: cmp [wparam],ID_SV je .sv jmp .finish .sv: call Sortirovka jmp .out_mas … proc Sortirovka xor ebx,ebx .cycl: mov ah,[mas+ebx] cmp ah,[mas+ebx+1] jna .next ;если текущий элемент больше следующего ;меняем местами текущий и следующий mov al,[mas+ebx+1] mov word [mas+ebx],ax .next: inc ebx cmp ebx,MASSIZE-1 jne .cycl ret endp … Здесь я вынес этап сортировки в отдельную процедуру (функцию), чтобы заодно напомнить вам, что в ассемблере также можно использовать вызовы функций, как и в высокоуровневых языках. Вставьте тело функции Sortirovka сразу за окончанием (endp) процедуры обработки сообщений WindowProc. Теперь при каждом нажатии на кнопку сортировки наибольший из еще несортированных элементов будет становиться на свое место. Убедитесь в этом, запустив программу и нажав несколько раз на кнопку сортировки. Для того, чтобы окончательно автоматизировать процесс сортировки, нам необходимо заставить этот цикл исполниться столько раз, сколько у нас элементов в массиве. Даже на один раз меньше ввиду того, что, когда все элементы, кроме последнего, будут стоять на своих местах, последний оставшийся уж точно будет стоять на своем месте. Для этого мы организуем внешний цикл, который будет повторять внутренний цикл на один раз меньше, чем количество элементов в массиве: … proc Sortirovka mov ecx,MASSIZE-1 .cycl1: xor ebx,ebx .cycl2: mov ah,[mas+ebx] cmp ah,[mas+ebx+1] jna .next ;если текущий элемент больше следующего ;меняем местами текущий и следующий mov al,[mas+ebx+1] mov word [mas+ebx],ax .next: inc ebx cmp ebx,MASSIZE-1 jne .cycl2 loop .cycl1 ret endp … Теперь наш массив будет верно отсортирован от начала до конца. Но наш способ еще далек от оптимального. Если после первого этапа сортировки крайний правый элемент заведомо находится на своем месте, а после второго — два крайних справа и т.д., то зачем нам каждый раз проверять весь массив полностью? Заменим команду cmp ebx,MASSIZE-1 на команду cmp ebx,ecx. Тогда получится, что в первый проход мы, как и раньше, остановимся на MASSIZE-1, затем команда loop уменьшит ECX, и на второй этап сортировки завершится при EBX=MASSIZE-2 и т.д. В последний раз будет проверена только одна пара (mas+0 и mas+1). Однако даже этот код все еще нуждается в оптимизации. Вдруг наш массив будет уже упорядочен или почти упорядочен? Например, если взять значения массива из прошлого занятия: 123,23,3,4,5. После первых двух проходов массив уже будет упорядочен, однако программа сделает после этого еще два прохода. Хорошо, когда массив небольшой, как в нашем примере, а если в нем будет тысяча элементов, и только два из них будут стоять не на своих местах — зачем нам делать лишних 997 циклических проходов? Надо что-то придумать, чтобы программа не совершала так много лишних действий. Мы можем с уверенностью сказать, что, если на одном из этапов не было сделано ни одной перестановки соседних элементов, то и на остальных этапах их не будет. Значит, если мы введем в ".cycl2" счетчик перестановок для каждого этапа, то после очередного прохода по нулевому значению этого счетчика мы сможем определить, что массив уже отсортирован. … proc Sortirovka mov ecx,MASSIZE-1 .cycl1: xor ebx,ebx xor edx,edx .cycl2: mov ah,[mas+ebx] cmp ah,[mas+ebx+1] jna .next mov al,[mas+ebx+1] mov word [mas+ebx],ax inc edx .next: inc ebx cmp ebx,ecx jne .cycl2 test edx,edx loopnz .cycl1 ret endp … Теперь наша процедура сортировки методом прямого обмена достаточно оптимизирована. Для выполнения сортировки по убыванию значений достаточно заменить jna .next на jnb .next. Как уже было сказано, кроме метода прямого обмена, существуют еще два базовых метода сортировки: метод прямого выбора и метод прямого включения. Суть метода сортировки прямым включением состоит в том, что на каждом этапе берется очередной элемент массива начиная со второго, и его местоположение ищется в левой части массива, которая считается уже отсортированной. Взятое значение включается в массив на свое законное место в отсортированной левой части. Если взятое значение и так находится на своем месте, то ничего никуда не сдвигаем, а берем следующее значение и т.д. … proc Sortirovka mov esi,1 .cycl1: push esi .cycl2: mov al,[mas+esi-1] cmp al,[mas+esi] jb .next xchg al,[mas+esi] dec esi ;двигаемся к началу массива mov [mas+esi],al jnz .cycl2 .next: pop esi inc esi ;двигаемся к концу массива cmp esi,MASSIZE jb .cycl1 ret endp … По адресу [mas+esi] у нас будет числиться текущий "взятый" элемент, место включения которого мы ищем. Поэтому перед началом первого цикла мы поместим в ESI единицу, чтобы [mas+esi] изначально указывал на второй элемент массива. В первом цикле мы временно сохраняем очередную позицию текущего элемента — ESI, дабы не использовать под это дело два регистра, и сразу переходим к циклу номер два для включения элемента в отсортированную правую часть массива. Задвигаем в AL предыдущий элемент [mas+esi-1], сравниваем его с текущим. Если предыдущий оказался меньше текущего, значит, текущий элемент находится на своем месте, и можно переходить к поиску места следующего элемента. Иначе меняем местами текущий и предыдущий элементы, одновременно сдвигая указатель на текущий элемент на позицию влево (dec esi), чтобы продолжить поиск верного места для текущего элемента. На метке ".next:" мы восстанавливаем указатель на первый из несортированных элементов. Сдвигаем его вправо, так как после второго цикла он уже указывает на последний элемент отсортированной части. Проверяем, не весь ли массив мы уже отсортировали, и повторяем первый цикл, если сортировка еще не окончена. При сортировке методом прямого выбора на каждом этапе в правой несортированной части массива производится поиск минимального элемента, и его перемещение на место крайнего левого элемента несортированной части. То есть на первом этапе находим минимальный элемент во всем массиве и перемещаем его на первую позицию. На втором этапе ищем минимум в последовательности от второго элемента включительно до конца, найденное значение перемещаем на вторую позицию и т.д. В этот раз указатель на первый элемент несортированной части массива хранится в EDI, но это, как вы понимаете, не принципиально. … proc Sortirovka mov edi,mas mov ecx,MASSIZE .cycl1: lea ebx,[edi+ecx] mov al,[edi] .cycl2: dec ebx cmp al,[ebx] jbe .next xchg al,[ebx] .next: cmp ebx,edi jnz .cycl2 mov [edi],al inc edi loop .cycl1 ret endp … Перед циклом ".cycl2:" мы сохраняем в AL значение первого элемента текущей несортированной последовательности. В цикле мы сравниваем его со всеми элементами несортированной части начиная с последнего и, если находим элемент с еще меньшим значением, меняем местами AL и текущий элемент. После цикла мы сохраняем уже абсолютный минимум несортированной части, находящийся в AL, на место ее первого элемента (по адресу в EDI). Увеличиваем EDI, чтобы он указывал уже на следующий элемент, и повторяем ".cycl1:". Каждый из вышеупомянутых методов имеет свои плюсы и минусы, поэтому необходимо четко понять принцип действия каждого из них. В любом случае эти три метода являются лишь базовыми методами сортировки и не дают максимального быстродействия. Кроме базовых, существуют усовершенствованные методы сортировки. Но ввиду того, что наш курс и так может показаться достаточно сложным для чайников, я не стану здесь приводить эти методы. Однако я надеюсь, что, когда вы в достаточной мере освоите базу, вам не составит особого труда и самостоятельно разобраться с более навороченными методами, используя интернет или классические учебники по программированию. Теперь давайте подготовимся к изучению динамических массивов, специфику которых мы рассмотрим подробнее в следующей части курса. Основное отличие динамического массива от статического в том, что в динамическом массиве число элементов может изменяться. Поэтому нам в первую очередь необходимо константу MASSIZE заменить переменной: n dd MASSIZE Теперь надо будет при обращении вместо константы MASSIZE к этой переменной не забывать брать ее в квадратные скобки, чтобы работать именно с ее значением, а не с адресом, по которому оно хранится. Учтите, что резервирование места под массив hmas следует оставить как есть. Мы не можем в качестве параметра директивы db или rd указать переменную. Также учтите, что, если мы, к примеру, будем переделывать процедуру сортировки "пузырек" для работы с динамическим массивом, то инициализацию счетчика ECX значением MASSIZE-1 придется заменить на: mov ecx,[n] dec ecx Попробуйте теперь самостоятельно добавить в программу функцию удаления заданного элемента. Сделайте отдельное поле для ввода порядкового номера элемента. Только не забудьте убрать параметр ES_READONLY, иначе ввод будет запрещен. Создайте кнопку "удалить элемент", обработчик которой будет посылать этому полю сообщение WM_SETTEXT (первый параметр — количество символов, второй — буфер для текста), чтобы получить номер удаляемого элемента и преобразовывать символ из буфера в двоичное значение. И, конечно, напишите процедуру, которая будет удалять указанный элемент из массива со сдвигом остальных значений влево на позицию удаленного элемента. Попробуйте также удалить поле вывода удаляемого элемента и его дескриптор из массива hmas. Для удаления поля следует послать ему сообщение WM_CLOSE (параметры отсутствуют, так что пишем два нуля). Предусмотрите обработку ошибок, например, если введен неверный номер элемента или вообще не номер, а другой символ. На этом я с вами прощаюсь. Удачи в разработке программы!

Массивы, массивы, кругом одни массивы. Понимаю, как вы устали их учить, но позже, оглядываясь назад, вы не сможете недооценить важность этой темы. Не зря же говорят: тяжело в учении — легко в бою. Ни одна серьезная программа на сегодняшний день не обходится без функций обработки массивов. Конечно, для использования этих функций вам, скорее всего, не понадобится каждый раз писать их самостоятельно. В интернете вы всегда сможете найти готовые примеры этих функций и просто вставлять их в текст своей программы. Но для того, чтобы правильно ими пользоваться, необходимо понимать принципы их действия. А лучшего пути, чем на практике разобраться с этими принципами, пока что не придумано. В прошлый раз я дал вам весьма трудное задание: самостоятельно запрограммировать удаление элемента из массива. Что ж, сегодня давайте разберемся, чего там было сложного для вас. Начнем, пожалуй, с функции удаления элемента из массива. Для того, чтобы удалить элемент из массива, нам необходимо всего лишь сдвинуть следующий за ним элемент на его место, затем сдвинуть следующий элемент на место предыдущего и т.д. Например, имеем массив из шести букв алфавита: A,B,C,D,E,F. Чтобы удалить из него элемент D, необходимо сдвинуть E на место D, затем сдвинуть F на место E. Получим массив: A,B,C,E,F,F, потому что мы не на самом деле сдвигаем, а лишь копируем (команда mov). Последняя F — лишняя, поэтому для правильного дальнейшего восприятия нашего массива программой необходимо уменьшить переменную, содержащую размер массива, на число удаленных элементов. То есть на единицу. Итак, допустим, что размер массива у нас содержится в переменной [n], а сам массив находится по адресу [mas]. Тогда программа удаления элемента из массива будет выглядеть примерно так: section '.data' data readable writeable mas db 'ABCDEF' n db 6 section '.code' code readable executable start: mov esi,4 mov ecx,[n] sub ecx,esi add esi,mas mov edi,esi dec edi cld rep movsb dec [n] Смещение первого сдвигаемого элемента мы поместим в регистр ESI. Напомню, что смещение A=0, смещение B=1, смещение E=4, а этот элемент, как вы понимаете, будет помещен на место удаляемого элемента D. Далее мы помещаем в ECX общее количество элементов массива из переменной [n]. ECX будет отсчитывать количество сдвигаемых элементов, поэтому, чтобы он содержал лишь число элементов, подлежащих сдвигу, мы вычитаем ESI из ECX. Теперь в ECX содержится двойка, ведь именно два элемента нам надо сдвинуть влево: E и F. Команда MOVS требует, чтобы в ESI находился адрес источника, а в EDI — адрес приемника. Поэтому мы прибавляем к ESI адрес массива и получаем в ESI адрес элемента E. Копируем этот адрес в EDI и уменьшаем EDI на единицу, чтобы получить адрес символа D. Командой CLD сбрасываем флаг DF в ноль, чтобы определить направление обработки элементов слева направо. Выполняем сдвиг: rep movsb, который будет повторен дважды, ввиду того, что в ECX содержалось число 2. Уменьшаем переменную [n] на единицу. Этот код прост и понятен, но он — лишь скелет функции удаления элемента из массива. В нем не производится никаких проверок и отсутствует возможность выбора удаляемого элемента. Давайте теперь увеличим возможности этой функции и оформим ее соответственно в виде полноценной функции к нашей программе обработки массива из прошлого занятия. Полноценная функция должна осуществлять поиск заданного элемента в массиве и его удаление. Если элемент был найден и удален, функция возвращает в EAX нулевое значение. Если элемент отсутствует, функция выдаст соответствующее сообщение об ошибке и вернет в EAX значение -1 (0FFFFFFFFh). На входе в функцию, то есть непосредственно перед ее вызовом, в AL должно содержаться искомое значение. Вот и все условия. Помещаем в AL значение, которое требуется удалить из массива, вызываем функцию удаления командой CALL, проверяем EAX. Если он не равен -1, значит, элемент был успешно удален, и обновленный массив готов к повторному выводу на экран. Это наши планы. Осталось их реализовать: proc DeleteElement cld mov ecx,[n] lea edi,[mas] repne scasb je .del_el ;Элемент не найден invoke MessageBox,0,_terror3,0,0 mov eax,-1 ret ;Удаляем элемент ;по адресу EDI-1 .del_el: mov esi,edi dec edi rep movsb dec [n] ;Удаляем последнее поле mov ebx,[n] invoke SendMessage,[hmas+ebx*4],WM_CLOSE,0,0 xor eax,eax ret endp Вот такая вот загогулина! Неужели что-то непонятно? Ну да ладно, сейчас разберемся. Итак, как мы условились, считаем, что на момент начала исполнения функции в AL уже лежит значение от 0 до 255, которое нужно найти и удалить. Поэтому устанавливаем флаг направления поиска в положение "слева направо", то есть сбрасываем DF ноль. Задвигаем в счетчик ECX текущее количество элементов массива. Загружаем в EDI эффективный адрес массива [mas]. В данной ситуации команда lea edi,[mas] дает такой же результат, как и mov edi,mas, но правильнее будет использовать все-таки LEA. Сканируем массив до тех пор, пока ECX не уменьшится до нуля или пока результат сравнения — NE (Not Equal — не равно). По окончании сканирования прыг на удаление элемента (je .del_el), если E (равно). Иначе, если сравнение последнего элемента завершилось результатом NE, значит, заданный элемент найден не был. В таком случае выводим "устрашающий" MessageBox (text error я сократил как t error, и получился террор3=), помещаем в EAX минус единицу — условный код ошибки — и выходим из функции (ret). На метку ".del_el:" мы попадаем лишь в случае, если значение было найдено в массиве. При таком раскладе EDI уже будет содержать адрес следующего за найденным элемента. Чтобы корректно удалить элемент, нам надо, чтобы адрес следующего элемента хранился в ESI, а в EDI был адрес удаляемого элемента. Поэтому мы копируем EDI в ESI, после чего уменьшаем EDI на размер элемента, чтобы он указывал на удаляемый элемент. ECX у нас и так хранит число элементов, оставшихся до конца массива, поэтому смело производим сдвиг: rep movsb. По окончании сдвига уменьшаем на единицу число элементов массива. С этого момента наш массив уже можно считать обновленным. Из него удален один элемент, и переменная, обозначающая количество действительных элементов, тоже обновлена и соответствует действительности. Но ведь полей, в которых мы отображаем значения наших элементов, не убавилось. Если не решить эту проблему, то при выводе обновленный массив будет неверно отображен. Последнее поле будет отображать значение бывшего последнего элемента, который к этому моменту будет уже находиться на бывшем месте предпоследнего. В общем, чтобы этого не допустить, нам надо, удалив элемент из массива, удалить и поле. Будем сдвигать дескрипторы полей в массиве hmas, а затем в цикле подвинем все окошки на место удаленного? Нет, не угадали. Все намного проще. Нам достаточно удалить последнее поле, и все готово. Ведь после того, как мы сдвинули оставшиеся элементы массива на место удаляемого, именно последний элемент остался лишним и перестал числиться в массиве. С учетом того, что переменную n мы уже уменьшили, дескриптор поля, подлежащего удалению, будет находиться по адресу [hmas+[n]*4]. Только значение переменной [n] нам придется передать через регистр, потому что мы не можем записать в вызове функции, что дескриптор окна находится по адресу, значение которого находится по адресу. Кстати, вы еще не забыли, что в FASM'е квадратные скобки означают? Ну и как вам не стыдно такое забыть? Эдак вы половину материала не понимаете и думаете, что умом не вышли. А на самом деле просто невнимательно читали первые статьи. Второй и последний раз объясняю, но в этот раз запомните уж раз и навсегда! Квадратные скобки указывают на то, что значение, над которым производится действие, находится в памяти по адресу, указанному в квадратных скобках. Например, в нашем случае n — это константа, значением которой является адрес ячейки в памяти, выделенной под хранение значения переменной [n]. Компилятор при сборке программы заменяет повсеместно символ n конкретным значением — адресом, по которому находится наша переменная [n]. Поэтому значение n во время работы программы изменять нельзя. Изменять можно лишь значение, находящееся по адресу n, то есть в ячейке [n]. Регистры — это другое дело. Они могут содержать как непосредственное значение, так и адрес в памяти. Все зависит только от программиста. Командой mov ebx,[n] мы помещаем в ebx значение ячейки по адресу n — количество элементов массива. Если бы мы написали mov ebx,n, то в регистр было бы помещено не значение переменной, а адрес, по которому она хранится. Однако далее при посылке сообщения окну мы указываем в качестве дескриптора окна [hmas+ebx*4]. Это значит, что дескриптор будет прочитан из ячейки по адресу учетверенного количества элементов + адрес массива дескрипторов hmas. По адресу hmas у нас хранится четырехбайтовый дескриптор первого поля, по адресу hmas+4 — дескриптор второго поля и т.д. По адресу hmas+([n]-1)*4 у нас хранится дескриптор последнего поля. Но, так как мы только что уменьшили значение [n] на единицу, то по адресу hmas+[n]*4 у нас находится дескриптор бывшего последнего, все еще не удаленного окошка. И поэтому сейчас самое время его удалить. Посылаем окну сообщение WM_CLOSE, не имеющее параметров. И с этих пор его дескриптор считаем недействительным. Помещаем в EAX ноль как показатель того, что удаление прошло успешно, и возвращаемся из функции. Двигаемся в обратном направлении. Мы условились, что функция, удаляющая элемент, получает его значение через регистр AL. Значит, надо сделать так, чтобы введенное пользователем значение туда как-то попало перед вызовом функции удаления. Для этого создаем поле для ввода удаляемого значения и кнопку удаления. Также сразу привожу то, что нам понадобится добавить в секцию данных в шаблон из предыдущего занятия: … n dd MASSIZE _terror1 db 'Ошибка запуска.',0 _terror2 db 'Введите число 0-255 !',0 _terror3 db 'Элемент не найден.',0 _tdelbut db 'Удалить элемент',0 hedit dd ? buf rb BUFSIZE+1 … ;обработчик кнопки "Удалить элемент" .de: call ASCII2BIN cmp eax,-1 je .finish call DeleteElement cmp eax,-1 je .finish jmp .out_mas … proc ASCII2BIN invoke SendMessage,[hedit],WM_GETTEXT,BUFSIZE+1,buf test eax,eax jz .error lea ebx,[buf] xor eax,eax .dec_check_loop: cmp byte [ebx],0 ;if end of string je .finish mov edx,eax ;умножаем shl edx,3 ;EAX shl eax,1 ;на add eax,edx ;10 xor edx,edx mov dl,byte [ebx] sub edx,'0' cmp edx,9 ja .error add eax,edx inc ebx jmp .dec_check_loop .error: invoke MessageBox,0,_terror2,0,0 mov eax,-1 ret .finish: cmp eax,255 ja .error ret endp … При каких обстоятельствах будет выполнен код обработчика кнопки "Удалить элемент", думаю, объяснять не надо. Для удобства понимания я разделил удаление элемента на 2 процедуры: получение значения из текстового поля и удаление элемента с указанным значением. После каждого вызова производится проверка корректного выполнения функции. Процедуру удаления мы уже рассмотрели. Теперь рассмотрим процедуру получения значения из текстового поля, преобразование его в двоичный вид из символьного. Процедура ASCII2BIN осуществляет именно это. Отправляем сообщение WM_GETTEXT окну, в котором предположительно находится удаляемое значение. BUFSIZE+1 указывает, что максимум нам понадобится получить 3 символа + нуль- терминатор в память по адресу buf. После получения текста в EAX должно находиться число полученных символов. Поэтому, если EAX=0, мы прыгаем на ошибку с текстом "Введите число 0-255 !". Иначе производим преобразование символьного вида в двоичный. Загружаем в EBX адрес буфера, который мы будем преобразовывать, а EAX обнуляем — его мы будем использовать для сохранения результата преобразования. Действуем по тому же принципу, что и при преобразовании двоичного вида в символьный, только, естественно, производим обратные действия в обратном порядке. В цикле ".dec_check_loop" проверяем очередной текущий символ в буфере на равенство нулю. Такое равенство будет означать, что мы дошли до завершающего нуль-терминатора, и преобразование окончено. Понятно, что это произойдет никак не на первом шаге цикла, но проверку придется выполнить один лишний раз. Таков алгоритм. Следующее действие также лишнее на первом шаге цикла, но может понадобиться на остальных повторениях. Это умножение содержимого EAX на 10. Оно необходимо для того, чтобы правильно преобразовать десятки и сотни. Допустим, первый символ числа 123 был единицей. На втором кругу мы умножим 1 на 10 и прибавим к нему 2, и получится уже 12. На третьем кругу 12 умножим на 10 и прибавим 3 — получится требуемое число 123. На первом же кругу мы пока еще не поместили в EAX ничего, кроме нуля, поэтому от умножения на 10 перед преобразованием первого символа нам ни холодно ни жарко. Кстати, вы заметили, как извращенно мы производим умножение EAX на 10 по формуле X*8+X*2=X*10? Такова традиция.  На старых процессорах операция умножения могла занимать 10 и более тактов. Зато мы знаем, что умножить число на степень двойки (2,4,8,16 и т.д.) можно за один такт командой битового сдвига SHL. Сдвигая двоичное число на одну позицию влево, мы получаем результат умножения его на два в первой степени, то есть просто на 2. На две позиции — на два во второй степени, то есть умножение на 4. На три позиции — умножение на 8. Таким образом, за четыре однотактных команды, мы получим в EAX то же самое, что и за одну десятитактную команду. Тем самым сэкономим 6 тактов процессорного времени. Хотя на современных 64-битных процессорах команда умножения занимает уже три такта + один-два такта на помещение множителей в требуемые регистры. Так что, если вы уверены, что ваша программа не будет использоваться на старых компьютерах, то можете пользоваться командой MUL. Но все же на данный момент считается более правильным использовать приведенный мною способ. Итак, умножили на 10, что дальше? Дальше обнуляем EDX и помещаем в него наш очередной преобразуемый символ. Помещаем в DL, так как символ у нас размером в байт. Хотя можно использовать команду movzx edx,byte [ebx]. Результат будет тот же. Вычитаем из преобразуемого символа значение '0', чтобы преобразовать в двоичный вид. Теперь сравниваем уже преобразованное значение с девяткой. Значение больше девяти укажет на то, что символ изначально не являлся символом от 0 до 9, а значит — ошибка ввода. Если ошибки нет, добавляем данное значение к общему результату в EAX и переходим к обработке следующего символа, не забыв увеличить EBX, чтобы он указывал на очередной непреобразованный символ. При возникновении ошибки мы не только выводим окошко с сообщением, но и помещаем в EAX код ошибки (-1). По окончанию процедуры мы должны не забыть сравнить результат с числом 255. Если результат больше, значит, введенное значение не влезает в один байт, и мы прыгаем на ошибку. Иначе в AL лежит подлежащее удалению значение, и мы преспокойно возвращаемся из функции. Чтобы на этапе вывода не возникло проблем, добавляем следующий текст: … .out_mas: cmp [n],0 je .finish … Так мы пропустим цикл вывода, если вдруг окажется, что все элементы удалены. Также, чтобы не вводить в заблуждение процедуру упорядочивания элементов, следует предусмотреть вариант, когда элементов будет меньше двух. Нам незачем даже пытаться сортировать массив, если в нем лишь один элемент или элементы вообще отсутствуют. … .sv: cmp [n],2 jb .finish call Sortirovka jmp .out_mas … Теперь давайте оформим вставку элемента в массив. Вставка происходит следующим образом: — проверяем наличие свободной ячейки и, если она отсутствует, выводим сообщение и ничего не делаем; — иначе преобразуем значение из символьного представления в двоичный вид; — вставляем значение в массив; — добавляем поле для вывода; — переходим к выводу значений. Помимо самой процедуры удаления элемента, нам понадобится внести в код следующие изменения и дополнения: … ;константы ID_IN=1003 … ;данные _tdelbut db 'Удалить',0 _tinsbut db 'Вставить',0 _terror4 db 'Нет свободной ячейки.',0 … ;код .wmcommand: … cmp [wparam],ID_IN je .in … .in: cmp [n],MASSIZE jb @f invoke MessageBox,0,_terror4,0,0 jmp .finish @@: call ASCII2BIN cmp eax,-1 je .finish push [hwnd] call InsertElement cmp eax,-1 je .finish jmp .out_mas … .wmcreate: … invoke CreateWindowEx,0,_cbutton,_tdelbut,WS_VISIBLE+ WS_CHILD+BS_PUSHBUTTON+WS_TABSTOP,60,120,80,20,[hwnd],ID_DE,[wc.hInstance],0 invoke CreateWindowEx,0,_cbutton,_tinsbut,WS_VISIBLE+ WS_CHILD+BS_PUSHBUTTON+WS_TABSTOP,150,120,80,20,[hwnd],ID_IN,[wc.hInstance],0 … proc InsertElement hwnd ;вставляем элемент mov edi,[n] mov [mas+edi],al .sort_cycl: mov al,[mas+edi-1] cmp al,[mas+edi] jbe @f xchg al,[mas+edi] dec edi ;двигаемся к началу массива mov [mas+edi],al jnz .sort_cycl @@: ;добавляем поле mov esi,[n] mov edi,esi;Умножаем mov eax,esi; shl esi,5 ;esi shl edi,4 ; shl eax,1 ;на add esi,edi; add esi,eax;50 add esi,5 mov edi,5 invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+ WS_BORDER+ES_READONLY+ES_RIGHT,esi,edi,40,20,[hwnd],ebx,[wc.hInstance],0 test eax,eax jz @f mov ebx,[n] mov [hmas+ebx*4],eax inc [n] ret @@: mov eax,-1 ret endp … Думаю, все, кроме самой процедуры, не вызывает вопросов. Процедуру удаления рассмотрим подробнее. Первое ее отличие от написанных ранее процедур в том, что она имеет параметр hwnd. Его мы передаем в процедуру через стек, выполняя команду push [hwnd] перед вызовом процедуры. Дело в том, что hwnd — это локальная переменная процедуры WindowProc. К ней нельзя обратиться из другого места, кроме как из этой процедуры. Вообще это не совсем корректно — использовать в данном случае переменную hwnd, потому что она не обязательно должна содержать дескриптор главного окна, а содержит дескриптор окна, сообщение которому в данный момент обрабатывается. Но ввиду того, что сообщение WM_COMMAND в нашем случае приходит только главному окну (сообщение от дочерних элементов — кнопок), мы можем не создавать отдельную глобальную переменную под дескриптор главного окна, а передать значение локальной переменной через стек. Что мы и делаем. Дескриптор главного окна нам нужен лишь для указания родительского окна при создании дочернего поля. Локальные переменные процедуры, значения которых содержатся в стеке к моменту ее вызова, должны быть указаны после имени процедуры и разделяются запятыми. Новый элемент мы вставляем по адресу mas+[n]. Ввиду того, что mas — это прямой указатель, а n — указатель на место, где хранится значение, мы передаем n через регистр. Когда мы будем работать с полноправными динамическими массивами, и адрес массива будет храниться в переменной, а не являться константой, как сейчас, нам придется передавать через регистр и адрес массива. На случай, если массив уже был отсортирован, мы попробуем поставить новое значение сразу же на его место в упорядоченной последовательности. Для этого пишем короткий цикл, в котором текущий элемент сравнивается с предыдущим и меняется с ним местами, если предыдущий оказался большим по значению (jbe — прыг если меньше или равно). Таким образом, в цикле наш элемент будет сдвигаться влево до тех пор, пока очередной предыдущий элемент не окажется меньшим или равным по значению. Здесь все по аналогии с методом сортировки прямым включением, только поиск места производится лишь для единственного элемента. Чтобы добавленное поле было на позицию правее текущего последнего поля или на первой позиции, если поля вообще отсутствуют, нам надо умножить [n] на 50 (50 точек у нас — расстояние между левыми краями соседних полей) и прибавить 5 (отступ для первого поля). Умножаем по тому же принципу, что и на 10, только для умножения на 50 используем формулу: X*50=X*32+X*16+X*2. Если поле было создано, помещаем его дескриптор в следующий за последним текущим элемент массива [hmas], увеличиваем [n] и возвращаемся из функции. Иначе оставляем [n] как есть, помещаем в eax код ошибки, чтобы не выводить повторно неизмененный массив, и возвращаемся. В следующий раз постараюсь закончить с массивами, но в программировании вы с ними столкнетесь еще много-много раз. А на сегодня все. Желаю вам понять все то, что я вам здесь объяснял, и даже то, чего не объяснил. До новых встреч!

Сегодня мы попробуем закончить с азами управления массивами, а также познакомимся поближе с методикой выделения памяти в славном семействе Windows NT/2K/XP/Vista. В предыдущих примерах наш массив располагался в секции данных исполняемого модуля. Такой подход вполне удобен при работе со статическим массивом. Но статические массивы используются достаточно редко. Чаще возникает необходимость использования динамического массива, количество элементов в котором не определено, а следовательно, размер памяти, занимаемой массивом, может изменяться за время работы программы.  Ранее, на рубеже перехода от семейства 9x к семейству NT, для выделения небольших объемов памяти рекомендовалось пользоваться функциями GlobalAlloc и LocalAlloc. Сейчас это по ряду причин считается нецелесообразным, а для выделения небольших объемов памяти (до нескольких мегабайт) рекомендуется использовать функции работы с кучами. Куча (от англ. Heap) — это область зарезервированного процессом адресного пространства, при помощи которой реализуется динамическое выделение памяти. При создании кучи под нее выделяется лишь виртуальная память, а физическая память выделяется специальным диспетчером куч (heap manager) уже по мере заполнения кучи данными. Физическая память выделяется определенными системой блоками — страницами, а по мере освобождения страниц возвращается системе. У каждого процесса есть стандартная куча, дескриптор которой можно получить вызовом функции GetProcessHeap. Параметры у этой функции отсутствуют, так как каждый процесс имеет лишь одну стандартную кучу. Также можно создавать дополнительные кучи при помощи функции HeapCreate. Ее параметры: 1. опции кучи: HEAP_CREATE_ENABLE_EXECUTE — разрешение на исполнение кода, содержащегося в куче, HEAP_GENERATE_EXCEPTIONS — системные уведомления при переполнении кучи и т.п., HEAP_NO_SERIALIZE — отказ от одновременного доступа к куче несколькими потоками немного увеличивает производительность, но может вызвать сбой при попытке одновременного доступа. 2. Начальный размер кучи в байтах. Значение округляется в большую сторону до ближайшего значения, кратного размеру страницы, поэтому, если указать ноль, будет зарезервирована одна страница. Размер страницы можно узнать при помощи функции GetSystemInfo. 3. Максимальный размер кучи в байтах. Если при выполнении функций HeapAlloc и HeapReAlloc (выделение памяти в куче) запрашиваемый размер превышает начальный размер кучи, то система выделяет новые страницы физической памяти, но при этом суммарный размер кучи не может превысить значение данного параметра. Если максимальный размер кучи не равен нолю, то полученная куча не является "растущей", и существует ограничение на максимальный размер выделяемого блока, который не должен превышать 0x7FFF8 байт. Функция выделения памяти для такой кучи автоматически вернет ошибку при попытке выделить блок памяти большего размера. Если же в качестве максимального размера кучи указать ноль, то куча считается "растущей". Общий размер такой кучи ограничен лишь доступной системе памятью. Для такой кучи попытка выделения блока памяти размером более 0x7FFF8 байт уже не будет считаться ошибкой, — система сама произведет вызов функции VirtualAlloc для выделения памяти под большой блок данных. Приложения, которым требуется выделять большие блоки памяти, всегда должны устанавливать значение данного параметра в ноль. При успешном выполнении функция возвращает в EAX-дескриптор созданной кучи. В случае ошибки возвращается ноль. Причем функция работает так, что точный максимальный размер блока в "нерастущей" куче никогда не известен. В официальной документации MSDN сказано лишь, что максимальный размер блока должен быть немного меньше 0x7FFF8 байт. Например, у меня не получилось выделить в "нерастущей" куче блок более 0x7EFF8. Так что старайтесь не использовать кучи фиксированного размера для выделения блоков памяти размером, близким к загадочной цифре 0x7FFF8. Для выделения блоков памяти в куче используется функция HeapAlloc, а для изменения размера выделенного блока — HeapReAlloc. Параметры первой из них:  1. Дескриптор кучи, в которой будет выделен блок (возвращается функциями GetProcessHeap и HeapCreate). 2. Опции блока: HEAP_GENERATE_EXCEPTIONS — системные уведомления при переполнении и т.п. (лучше указывать эту опцию сразу в HeapCreate для всех блоков кучи), HEAP_NO_SERIALIZE (тоже можно указать в HeapCreate для всех блоков кучи, а здесь не указывать), HEAP_ZERO_MEMORY — проинициализировать блок нулевыми значениями (занимает время, использовать по необходимости). 3. Размер выделяемого блока в байтах (если куча не является "растущей", то размер блока не должен превышать 0x7FFF8 байт). В случае успешного выполнения функция возвращает указатель на выделенный блок памяти. При возникновении ошибки функция возвращает ноль, если не был указан параметр HEAP_GENERATE_EXCEPTIONS. Иначе функция может вернуть значения STATUS_NO_MEMORY (нехватка памяти) или STATUS_ACCESS_VIOLATION (неверные параметры) в зависимости от произошедшей ошибки. В отличие от большинства функций, функции HeapAlloc и HeapReAlloc не вызывают SetLastError в случае ошибки, поэтому обращение к GetLastError не даст более подробных сведений об ошибке. Параметры функции HeapReAlloc: 1. дескриптор кучи, в которой находится перераспределяемый блок (возвращается функциями GetProcessHeap и HeapCreate); 2. опции перераспределения: HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, HEAP_REALLOC_IN_PLACE_ONLY — запрещает перемещение блока: в случае недостатка свободной памяти для расширения блока по месту его расположения, функция не станет искать подходящее место в памяти, а вернет ошибку, оставив блок без изменений, HEAP_ZERO_MEMORY; 3. указатель на перераспределяемый блок памяти (возвращается функциями HeapAlloc или HeapReAlloc); 4. новый размер блока в байтах (если куча не является "растущей", то размер блока не должен превышать 0x7FFF8 байт). Возвращаемые значения такие же, как и у HeapAlloc. Обе вышеописанные функции могут выделить блок памяти как указанного размера, так и чуть больше требуемого. Точный размер выделенного блока можно узнать, обратившись к функции HeapSize. Ее параметры: 1. дескриптор кучи, в которой находится блок; 2. опции: HEAP_NO_SERIALIZE; 3. указатель на заданный блок. В случае успешного выполнения возвращается размер заданного блока. В случае ошибки — минус единица. Данная функция также не указывает подробности о произошедшей ошибке через SetLastError. Если выделенный в куче блок памяти вам больше не требуется, его следует удалить функцией HeapFree, чтобы освободить системную память. Параметры этой функции такие же, как и у HeapSize. В случае успешного освобождения памяти от заданного блока возвращается ненулевое значение. В случае ошибки возвращается ноль. Подробности можно узнать при помощи функции GetLastError. Для удаления кучи целиком используется функция HeapDestroy. Она имеет всего один параметр — дескриптор удаляемой кучи, возвращаемый функцией HeapCreate. Только не пытайтесь удалить кучу, дескриптор которой получен при помощи функции GetProcessHeap. Не вы ее создавали, не вам ее удалять. В случае успешного удаления кучи возвращается ненулевое значение. В случае ошибки возвращается ноль. Подробности можно узнать при помощи функции GetLastError. Н у вот теперь, когда мы изучили функции управления кучами, можем приступать к апгрейду нашего примера управления массивом. Первым делом не забудьте сменить надпись в заголовке окна на гордое словосочетание: "Динамический массив". Пусть все знают о том, что это скромное окошко способно сожрать всю системную память машины, если допустить пару нелепых ошибок в коде обработчиков его событий. Итак, приступим к изучению кода. Чтобы сэкономить газетную площадь, постараюсь приводить лишь новые и измененные участки кода. Массив у нас теперь динамический, поэтому константами задаем начальное количество элементов отдельно от максимального количества, под которое будет выделена память. Переменные [mas] и [hmas] теперь будут хранить лишь адреса массивов в выделенной системой памяти: … ;константы MASSIZE=3 ;начальное количество элементов MAXMASSIZE=5 ;макс.кол-во элементов BUFSIZE=3 ;макс.кол-во знаков в элементе(для буфера вывода десятичных значений) … ;секция данных _title db 'Динамический массив',0 hheap dd ? mas dd ? hmas dd ? … Перед попыткой удаления элемента введем проверку количества элементов на ноль. Теперь, если элементы в массиве отсутствуют, нажатие кнопки удаления элемента не приведет ни к каким действиям: ;секция кода … .de: cmp [n],0 je .finish call ASCII2BIN cmp eax,-1 je .finish call DeleteElement cmp eax,-1 je .finish jmp .out_mas … Перед вставкой элемента в массив сравниваем текущее количество элементов с максимальным и производим вставку только при условии, что текущее количество элементов меньше количества выделенных в памяти ячеек: … .in: cmp [n],MAXMASSIZE jb @f invoke MessageBox,0,_terror4,0,0 jmp .finish @@: call ASCII2BIN cmp eax,-1 je .finish push [hwnd] call InsertElement cmp eax,-1 je .finish jmp .out_mas … По приходу сообщения WM_CREATE, то есть первым делом при создании окна, создаем растущую кучу, не обеспечивающую возможность одновременного доступа нескольких потоков. Если бы в нашем коде существовала вероятность одновременного доступа к куче, мы бы указали ноль вместо HEAP_NO_SERIALIZE. В этой куче выделяем два блока памяти — под массив элементов и под массив дескрипторов полей вывода: … .wmcreate: ;выделяем память под массивы invoke HeapCreate,HEAP_NO_SERIALIZE,0,0 test eax,eax jz error mov [hheap],eax invoke HeapAlloc,[hheap],HEAP_NO_SERIALIZE,MAXMASSIZE test eax,eax jz error mov [mas],eax invoke HeapAlloc,[hheap],HEAP_NO_SERIALIZE,MAXMASSIZE*4 test eax,eax jz error mov [hmas],eax … Здесь начинаются главные отличия от примеров из предыдущих статей. Ранее наш массив располагался по заранее известному адресу, поэтому мы могли с уверенностью утверждать, что по смещению mas хранится первый элемент массива. Теперь по смещению mas у нас хранится адрес первого элемента массива. Заметили разницу? Ранее, например, командой mov [mas+2],al мы бы поместили значение регистра al в третий элемент массива. Теперь для этого нам бы потребовалось что-то вроде mov [[mas]+2],al. Но ввиду того, что синтаксис ассемблера не позволяет использовать двойную адресацию (поместить содержимое al в память по адресу, который находится по адресу…), нам придется сначала поместить значение [mas] в регистр, а потом уже обращаться к элементам, используя адресацию вида [регистр+смещение]. Если здесь вам что-либо неясно, возможно, вы невнимательно читали предыдущую статью. … ;создаем поле под каждый элемент массива mas ;и помещаем дескриптор каждого поля в массив hmas cmp [n],0 je @f mov esi,5 mov edi,5 xor ebx,ebx mov edx,[hmas] .create_cycl: push edx invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ES_RIGHT,esi,edi,40,20,[hwnd],ebx,[wc.hInstance],0 test eax,eax jz error pop edx mov [edx+ebx*4],eax add esi,50 inc ebx cmp ebx,[n] jne .create_cycl @@: ;создаем другие элементы … Здесь у нас добавлена проверка количества элементов массива на ноль. Если элементов нет — незачем создавать поля для их вывода. Адрес, по которому расположен массив hmas, мы копируем в регистр edx перед циклом создания полей. Таким образом, mov [edx],eax приведет к копированию значения eax в память по адресу edx. Ввиду того, что API-функции windows не гарантируют сохранение содержимого регистров EAX, ECX и EDX, нам придется перед вызовом функции сохранить содержимое EDX в стеке (push edx), а после выполнения функции — восстановить его (pop edx). При выводе элементов массива придерживаемся тех же правил. В esi помещаем адрес массива элементов, в edx — массива дескрипторов полей. Перед вызовом API- функции сохраняем edx, после вызова — восстанавливаем: … .out_mas: cmp [n],0 je .finish ;заполняем поля значениями элементов массива xor ebx,ebx mov esi,[mas] mov edx,[hmas] .masout_cycl: mov ah,byte [esi+ebx] lea edi,[buf+BUFSIZE] .dec: mov al,ah aam or al,30h ;можно заменить на add al,30h dec edi mov byte [edi],al test ah,ah jnz .dec push edx invoke SendMessage,[edx+ebx*4],WM_SETTEXT,0,edi pop edx inc ebx cmp ebx,[n] jne .masout_cycl ;test heap size invoke HeapSize,[hheap],0,[mas] mov ah,al lea edi,[buf+BUFSIZE] .dec1: mov al,ah aam or al,30h dec edi mov byte [edi],al test ah,ah jnz .dec1 push edx invoke SendMessage,[hedit],WM_SETTEXT,0,edi jmp .finish … Для наглядности я добавил вывод текущего размера памяти, выделенного под массив (;test heap size). Чтобы не создавать лишних полей, размер выводится прямо в поле ввода удаляемого/добавляемого элемента. По завершении программы не забываем освободить память от нашей кучи: … .wmdestroy: invoke HeapDestroy,[hheap] invoke PostQuitMessage,0 xor eax,eax .finish: pop edi esi ebx ret endp … Процедуры сортировки, удаления и вставки элементов также претерпели изменения. И, если в процедуре сортировки изменилось лишь размещение адреса массива (ранее адрес был константой mas, а теперь стал значением переменной [mas], которое мы по ранее указанным причинам будем во время работы с массивом хранить в регистре), то процедуры удаления и вставки подверглись более значительным изменениям: … proc Sortirovka mov esi,[mas] mov ecx,[n] dec ecx .cycl1: xor ebx,ebx xor edx,edx .cycl2: mov ah,[esi+ebx] cmp ah,[esi+ebx+1] jna .next mov al,[esi+ebx+1] mov [esi+ebx],ax inc edx .next: inc ebx cmp ebx,ecx jne .cycl2 test edx,edx loopnz .cycl1 ret endp … proc DeleteElement cld xor bl,bl mov ecx,[n] mov edi,[mas] .del_next: repne scasb je .del_el test bl,bl jne .finish ;Элемент не найден invoke MessageBox,0,_terror3,0,0 mov eax,-1 ret ;Удаляем элемент ;по адресу EDI-1 .del_el: mov esi,edi dec edi push eax ecx edi rep movsb dec [n] ;Удаляем последнее поле mov edx,[n] shl edx,2 add edx,[hmas] invoke SendMessage,[edx],WM_CLOSE,0,0 mov bl,1 pop edi ecx eax test ecx,ecx jne .del_next .finish: xor eax,eax ret endp proc InsertElement hwnd ;вставляем элемент mov edi,[n] mov esi,[mas] mov [esi+edi],al .sort_cycl: mov al,[esi+edi-1] cmp al,[esi+edi] jbe @f xchg al,[esi+edi] dec edi ;двигаемся к началу массива mov [esi+edi],al jnz .sort_cycl @@: ;добавляем поле mov esi,[n] mov edi,esi;Умножаем mov eax,esi; shl esi,5 ;esi shl edi,4 ; shl eax,1 ;на add esi,edi; add esi,eax;50 add esi,5 invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ES_RIGHT,esi,5,40,20,[hwnd],ebx,[wc.hInstance],0 test eax,eax jz @f mov edx,[n] shl edx,2 add edx,[hmas] mov [edx],eax inc [n] ret @@: mov eax,-1 ret endp … Процедура удаления элемента теперь удаляет все элементы с указанным значением, а не один из них, как было раньше. Для того, чтобы это корректно реализовать, потребовалось ввести счетчик удаленных элементов. Точнее, даже не счетчик, а своеобразный флаг, который поможет избежать вывода ошибки "Элемент не найден", когда мы уже удалили один или несколько элементов, но при очередном поиске таковых больше не нашли. Если содержимое bl, которое мы обнуляем перед циклом удаления, равно нулю, значит, еще ни один элемент не был удален. В таком случае, когда поиск по массиву завершится (ecx сравняется с нулем), мы будем знать, что в массиве отсутствовали элементы с заданным значением, и после вывода сообщения об ошибке поместим в eax код ошибки (минус единицу) и вернем управление на точку вызова процедуры. Иначе, если bl не равен нулю, мы прыгнем на метку ".finish", потому что bl, отличный от нуля, означает, что удаление заданных элементов было произведено. Теперь подробнее о цикле удаления. Мы, как и прежде, командой repne scasb сканируем массив в поисках элемента, равного содержимому AL. Сканирование может быть прекращено либо если найдено равенство, либо если ECX сравнялся с нулем. Второй случай мы только что рассмотрели. Если же найден элемент, равный AL, то мы прыгаем на метку ".del_el", чтобы выполнить удаление элемента. В edi к этому моменту находится адрес следующего за удаляемым элементом. А нам для удаления при помощи команды rep movsb необходимо, чтобы в edi был адрес удаляемого элемента, а в esi — следующего за ним. Поэтому мы копируем содержимое edi в esi, а edi уменьшаем на единицу. Теперь мы сохраним регистры eax, ecx и edi, потому что текущие значения ecx и edi нам еще понадобятся для продолжения поиска других кандидатов на удаление с текущей позиции. На текущую позицию будет сдвинут следующий за удаляемым элементом, а он ведь тоже может оказаться подлежащим удалению, хотя пока мы об этом не знаем, а лишь готовимся к удалению текущего элемента, но предусматриваем все варианты. Содержимое EAX мы сохраняем по другой причине: после вызова функции удаления лишнего окошка в EAX вернется результат ее выполнения. А у нас в AL, который является частью EAX, если вы помните, хранится значение искомых элементов. Поэтому обязательно сохраняем.  Элемент удалили, уменьшили текущее количество элементов в переменной [n], удаляем последнее поле. Его дескриптор сейчас находится по адресу [hmas]+[n]*4. Но ввиду невозможности двойной адресации мы поместим значение [n] в edx, умножим на 4 (shl edx,2) и добавим к edx значение [hmas]. Теперь, когда edx хранит адрес дескриптора удаляемого окна, мы смело можем указать [edx] в качестве параметра функции SendMessage. После удаления окошка задвигаем в bl единицу в знак того, что хотя бы один элемент мы уже удалили. Восстанавливаем сохраненные регистры, соответственно, в обратном порядке. На всякий случай проверяем ecx на ноль — вдруг это был последний элемент массива. Если не ноль, то переходим на метку ".del_next", чтобы продолжить сканирование с того элемента, который мы еще не проверяли. Иначе обнуляем eax — успешное выполнение — и возвращаемся из процедуры. В процедуре вставки элемента так же, как и в остальных измененных процедурах, и по тем же причинам адрес массива копируется в регистр. Ну и для доступа к ячейке [hmas]+[n]*4 используется такой же прием, как и в процедуре удаления. Однако и сейчас, несмотря на то, что массив динамический, ему чего-то не хватает. Мы не можем вставить больше элементов, чем MAXMASSIZE. Для того, чтобы обойти это ограничение, заменим данную константу переменной. Например, икс: … x dd MAXMASSIZE … Теперь на метке ".in:" сравнивайте [n] с [x], естественно, через регистр. Если памяти не хватает для вставки очередного элемента — выделите ее функцией HeapReAlloc. Только постарайтесь сделать так, чтобы память выделялась сразу под несколько элементов, а не под один каждый раз — это уменьшит фрагментацию и общее время на выделение памяти. А еще лучше — чтобы память выделялась заранее: например, когда текущей выделенной памяти осталось меньше чем на 10 элементов, блоки увеличиваются на 20 элементов. Попробуйте сделать это сами — ваш уровень это позволяет. Вот, собственно, и все, что я вам хотел рассказать о динамических массивах. Понятно, что существуют массивы, в которых каждый элемент может быть строкой неопределенного размера, существуют двумерные и многомерные массивы, но общие принципы работы с ними остаются такими же. Просто усложняется алгоритм обработки. Однако, если вы разберетесь с простым типом массивов, то и более сложные типы без проблем поймете по мере надобности. Желаю вам успехов в этом, и до новых встреч!

16

Привет, сегодня мы немного отойдем от сложных математических расчетов и изучим основные функции работы с реестром Windows. Обычно реестр Windows или системный реестр используется для хранения информации и настроек оборудования или программного обеспечения. Надеюсь, вы уже имеете представление о реестре и знакомы с программой regedit.exe. Если нет, то советую вам почитать об этих вещах в интернете или учебниках, прежде чем приступать к прочтению данной статьи. Обычно при работе с реестром возникает необходимость производить следующие основные действия: — создавать или открывать ключи реестра; — устанавливать их значения; — читать значения; — изменять и удалять значения; — удалять ключи. Собственно, этому мы сегодня и научимся. Для создания ключа реестра используется функция RegCreateKeyEx. Если указанный ключ уже имеется в реестре, то функция его откроет. Параметры функции следующие: 1. Дескриптор открытого ключа, полученный при помощи RegCreateKeyEx или RegOpenKeyEx, либо одно из зарезервированных значений: HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS. 2. Указатель на завершающуюся нулем строку, содержащую имя создаваемого подраздела. Строка не должна начинаться с символа "\". Параметр не может быть нулем. 3. Зарезервированный параметр, должен быть нулем; 4. Указатель на завершающуюся нолем строку, содержащую имя класса создаваемого ключа. Данный параметр может быть проигнорирован и может быть нулем. 5. Специальные опции — одно из следующих значений: REG_OPTION_BACKUP_RESTORE, REG_OPTION_NON_VOLATILE, REG_OPTION_VOLATILE; 6. Права доступа: KEY_ALL_ACCESS — полный доступ (комбинация значений: STANDARD_RIGHTS_REQUIRED, KEY_QUERY_VALUE, KEY_SET_VALUE, KEY_CREATE_SUB_KEY, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY, KEY_CREATE_LINK), KEY_CREATE_LINK — зарезервировано для системного доступа, KEY_CREATE_SUB_KEY — разрешение создавать подразделы, KEY_ENUMERATE_SUB_KEYS — разрешение перечислять подразделы, KEY_EXECUTE — эквивалент значения KEY_READ, KEY_NOTIFY — разрешение на получение уведомлений об изменениях ключа или подраздела ключа, KEY_QUERY_VALUE — разрешение получать значения ключа, KEY_READ — разрешение на чтение (комбинация значений: STANDARD_RIGHTS_READ, KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY), KEY_SET_VALUE — разрешение удалять или изменять значения ключа, KEY_WRITE — разрешение на запись (комбинация значений: STANDARD_RIGHTS_WRITE, KEY_SET_VALUE, KEY_CREATE_SUB_KEY). 7. Указатель на структуру атрибутов безопасности SECURITY_ATTRIBUTES, которая определяет возможность наследования дескриптора созданного ключа дочерними процессами. Если параметр ноль, дескриптор не может использоваться дочерними процессами. 8. Указатель на переменную, в которой будет сохранен дескриптор созданного или открытого ключа. 9. Указатель на переменную, в которой будет сохранено значение результата функции: REG_CREATED_NEW_KEY — ключ отсутствовал, но был создан, REG_OPENED_EXISTING_KEY — ключ существовал и был открыт. Если в параметре указать ноль, то данная информация не будет получена. В случае успешного выполнения функция возвращает значение ERROR_SUCCESS. В случае ошибки возвращается код системной ошибки. Следует отметить, что ключ, создаваемый данной функцией, не содержит значений. Для установки значений ключа следует использовать функцию RegSetValueEx. Приложение не должно создавать новые подразделы прямо в HKEY_USERS или HKEY_LOCAL_MACHINE — только уровнем ниже, в уже имеющихся подразделах. Открытие уже существующих в реестре ключей производится функцией RegOpenKeyEx. Ее параметры: 1. Дескриптор открытого ключа, полученный при помощи RegCreateKeyEx или RegOpenKeyEx, либо одно из зарезервированных значений: HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS. 2. Указатель на строку, завершающуюся нулем, содержащую имя подраздела. Если указан ноль, или пустая строка, функция откроет новый дескриптор для указанного в первом параметре ключа. 3. Зарезервированный параметр, должен быть нулем. 4. Права доступа (смотрите 6-й параметр функции RegCreateKeyEx). 5. Указатель на переменную, в которой будет сохранен дескриптор открытого ключа. В случае успешного выполнения функция возвращает значение ERROR_SUCCESS. В случае ошибки возвращается код системной ошибки. Для удаления ключа реестра используется функция RegDeleteKey. Параметры: 1. Дескриптор открытого ключа, полученный при помощи RegCreateKeyEx или RegOpenKeyEx, либо одно из зарезервированных значений: HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS. 2. Указатель на строку завершающуюся нулем, содержащую имя удаляемого подраздела. Удаляемый подраздел не должен сам иметь подразделы. В случае успешного выполнения функция возвращает значение ERROR_SUCCESS. В случае ошибки возвращается код системной ошибки. Помните, что удаляемый ключ не будет удален до тех пор, пока существуют его открытые дескрипторы. Удаляемый ключ сам не должен содержать еще ключи. Чтобы удалить ключ, внутри которого имеются другие ключи, следует предварительно удалить каждый из них по отдельности. Функция RegCloseKey закрывает открытый ранее ключ реестра. Единственный параметр этой функции — дескриптор открытого ключа. Не забывайте применять эту функцию, когда более не требуется обращаться к открытому или созданному ключу реестра. Возвращаемые значения такие же, как и в ранее перечисленных функциях. Значения в реестре могут храниться в различных форматах. Сохраняя данные в реестре, вам следует выбрать подходящий тип значения из доступных типов: REG_BINARY — бинарные данные в любой форме. REG_DWORD — 32-битное значение. REG_DWORD_LITTLE_ENDIAN — 32-битное значение в формате little-endian (то же самое, что и REG_DWORD). Little-endian, называемый в народе интеловским порядком следования байтов, — это порядок, при котором младший байт располагается по младшему адресу. От младшего к старшему. То есть число 0x01020304 будет записано как 0x04030201. Этот порядок наиболее распространен в процессорах Intel и AMD, и, естественно, в операционных системах, предназначенных для работы с ними. REG_DWORD_BIG_ENDIAN — 32-битное значение в формате big-endian. Порядок следования байтов — старший байт по младшему адресу. В таком порядке записываются привычные для нас арабские числа, поэтому такой порядок более удобен для восприятия человеком, но менее удобен для процессора, поскольку арифметические операции над длинными числами производятся начиная с младшего байта. REG_EXPAND_SZ — завершающаяся нулем строка, содержащая неразвернутую сноску на системную переменную (например "%PATH%"). REG_LINK — зарезервировано для системного использования. REG_MULTI_SZ — массив завершающихся нулем строк, завершающийся двумя нолями. REG_NONE — значение неопределенного типа. REG_SZ — завершающаяся нулем строка. Чтобы создать в открытом ключе значение, а также чтобы изменить имеющееся значение, используется функция RegSetValueEx. Параметры: 1. Дескриптор открытого ключа, полученный при помощи RegCreateKeyEx или RegOpenKeyEx, либо одно из зарезервированных значений: HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS. 2. Указатель на завершающуюся нулем строку, содержащую имя создаваемого или изменяемого значения. Если указатель — ноль или указывает на пустую строку, то функция обработает безымянное значение (значение по умолчанию). По сути, ключи реестра не имеют "значений по умолчанию", но могут содержать одно безымянное значение любого типа. 3. Зарезервирован. Пишем ноль. 4. Тип создаваемого значения. Допустимые типы приведены выше. 5. Указатель на создаваемое значение. 6. Размер создаваемого значения в байтах включая завершающий строку ноль для строковых значений. Возвращаемые значения такие же, как и в ранее перечисленных функциях. Для получения типа и содержимого определенного значения ключа используется функция RegQueryValueEx. Параметры функции: 1. Дескриптор открытого ключа, полученный при помощи RegCreateKeyEx или RegOpenKeyEx, либо одно из зарезервированных значений: HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS. 2. Указатель на завершающуюся нулем строку, содержащую имя значения. Нулевой указатель, или указатель на пустую строку, позволяет получить тип и содержимое безымянного значения, если таковое существует. 3. Зарезервирован. 4. Указатель на переменную, в которую будет сообщен тип указанного значения. Если тип значения не требуется, можно указать ноль. 5. Указатель на буфер для получения содержимого указанного значения. Если содержимое не требуется, можно указать ноль. 6. Указатель на переменную, содержащую размер буфера, определенного для получения содержимого значения. Размер должен включать нуль- терминаторы, если таковые присутствуют в содержимом значения. При успешном выполнении функции в данной переменной будет сохранен размер полученного содержимого. Если размер буфера не позволяет вместить содержимое, в данной переменной будет сохранен требуемый размер, а функция вернет ошибку ERROR_MORE_DATA. Данный параметр может быть нулем только в случае, если содержимое не требуется, и предыдущий параметр — тоже ноль. Если предыдущий параметр — ноль, а данный параметр отличен от нуля, то функция вернет ERROR_SUCCESS, а в переменной, указанной в данном параметре, сохранит текущий размер содержимого значения. Таким образом, можно корректно определить размер требуемого буфера под содержимое значения. В остальном возвращаемые данной функцией значения не отличаются от возвращаемых значений описанных ранее функций. Для удаления значения из ключа реестра применяют функцию RegDeleteValue. Функция имеет лишь два параметра, аналогичных первым двум параметрам функций RegSetValueEx и RegQueryValueEx. Возвращаемые значения те же. Для получения информации обо всех значениях ключа можно использовать функцию RegEnumValue. Она перечисляет все значения указанного открытого ключа реестра. Причем вызывать ее придется в цикле; для каждого очередного значения — отдельный вызов функции. Подробности — ниже по тексту, а параметры функции следующие: 1. Дескриптор открытого ключа. 2. Индекс запрашиваемого значения. Для первого вызова функции он должен быть нулем, а при следующих вызовах увеличивается на единицу. Ввиду того, что значения в ключах не упорядочены по каким либо свойствам, кроме индекса, функция будет возвращать их в произвольном порядке. 3. Указатель на буфер, в который будет помещено имя значения в виде завершающейся нулем строки. 4. Указатель на переменную, содержащую размер буфера под имя значения в символах. По возвращению из функции в данную переменную будет помещено количество записанных в буфер символов без учета завершающего строку нуля. 5. Зарезервирован. 6. Указатель на переменную, в которую будет сообщен тип указанного значения. Если тип значения не требуется, можно указать ноль. 7. Указатель на буфер для получения содержимого указанного значения. Если содержимое не требуется, можно указать ноль. 8. Указатель на переменную, содержащую размер буфера, определенного для получения содержимого значения. По возвращению из функции в данную переменную будет помещено количество записанных в буфер байтов. Данный параметр может быть нулем только в случае, если содержимое не требуется, и предыдущий параметр — тоже ноль. Размер должен включать нуль-терминаторы, если таковые присутствуют в содержимом значения. Если размер буфера не позволяет вместить содержимое, в данной переменной будет сохранен требуемый размер, а функция вернет ошибку ERROR_MORE_DATA. Возвращаемые функцией значения такие же, как и в перечисленных ранее функциях за небольшим исключением. Если в ключе больше не осталось неперечисленных значений, функция возвращает ошибку ERROR_NO_MORE_ITEMS. По получению такой ошибки можно определить, что все имеющиеся в ключе значения были перечислены. Аналогичным образом используется функция RegEnumKeyEx для получения информации обо всех подразделах ключа, то есть о ключах, находящихся в данном ключе. Ее параметры: 1. Дескриптор открытого ключа. 2. Индекс запрашиваемого подраздела. 3. Указатель на буфер, в который будет помещено имя подраздела в виде завершающейся нулем строки. Функция сообщает только имя подраздела, а не полное его имя в иерархии ключей реестра. 4. Указатель на переменную, содержащую размер буфера под имя подраздела в символах, с учетом завершающего нуля. По возвращению из функции в данную переменную будет помещено количество записанных в буфер символов без учета завершающего строку нуля. 5. Зарезервирован. 6. Указатель на буфер, в который будет помещен класс подраздела в виде завершающейся нулем строки. Можно указать ноль. 7. Указатель на переменную, содержащую размер буфера под класс подраздела в символах, с учетом завершающего ноля. По возвращению из функции в данную переменную будет помещено количество записанных в буфер символов без учета завершающего строку нуля. Данный параметр может быть нулем только если предыдущий параметр тоже ноль. 8. Указатель на переменную, в которую будет помещено время последней записи подраздела. Параметр может быть нулем. Если в ключе больше не осталось неперечисленных подразделов, функция возвращает ошибку ERROR_NO_MORE_ITEMS. В остальном возвращаемые значения аналогичны значениям описанных ранее функций. Для получения значений или подразделов ключа при помощи функций RegEnumValue или RegEnumKeyEx мы можем в цикле увеличивать второй параметр функции до получения ошибки ERROR_NO_MORE_ITEMS. Или же, наоборот, двигаться от последнего индекса до нуля. Индекс последнего значения или подраздела ключа можно узнать при помощи функции RegQueryInfoKey. Функция предназначена для получения общей информации об открытом ключе реестра. Ее параметры: 1. Дескриптор открытого ключа. 2. Указатель на буфер, в который будет помещен класс ключа в виде завершающейся нулем строки. Можно указать ноль. 3. Указатель на переменную, содержащую размер буфера под класс ключа в символах, с учетом завершающего нуля. По возвращению из функции в данную переменную будет помещено количество записанных в буфер символов без учета завершающего строку нуля. В случае недостаточного размера буфера будет выдана ошибка ERROR_MORE_DATA, а в данную переменную будет помещен требуемый размер буфера в символах без учета завершающего нуля. Данный параметр может быть нулем только если предыдущий параметр тоже ноль. 4. Зарезервирован. 5. Указатель на переменную, в которую будет помещено количество подразделов данного ключа. Параметр может быть нулем. 6. Указатель на переменную, в которую будет помещен размер самого длинного имени подраздела данного ключа в символах Юникода без учета завершающего нуля. Параметр может быть нулем. 7. Указатель на переменную, в которую будет помещен размер самого длинного имени класса подразделов данного ключа в символах Юникода без учета завершающего нуля. Параметр может быть нулем. 8. Указатель на переменную, в которую будет помещено количество значений данного ключа. Параметр может быть нулем. 9. Указатель на переменную, в которую будет помещен размер самого длинного имени значения данного ключа в символах Юникода без учета завершающего нуля. Параметр может быть нулем. 10. Указатель на переменную, в которую будет помещен размер самого длинного содержимого значений данного ключа в байтах. Параметр может быть нулем. 11. Указатель на переменную, в которую будет помещен размер дескриптора прав доступа к данному ключу в байтах. Параметр может быть нулем. 12. Указатель на 64-битную структуру FILETIME, в которую будет помещено время последнего изменения ключа или его значения. Параметр может быть нулем. При успешном выполнении функция возвращает значение ERROR_SUCCESS, в случае ошибки — код системной ошибки. Вот и все, что касается основных функций работы с реестром. Примеров их использования сегодня не будет, зато их будет предостаточно в следующей части. А до ее выхода попробуйте самостоятельно разобраться с этим делом. Только учтите, что реестр очень легко повредить, а исправить намного сложнее. Так что, если в чем-то сильно сомневаетесь, лучше подождите следующего выпуска. Ну, а если вы уверены в себе, то вам больше и объяснять ничего не требуется. Скажу лишь, что функции работы с реестром находятся в отдельной библиотеке — advapi32. Поэтому не забудьте добавить в секцию импортируемых данных строки: advapi32,'ADVAPI32.DLL',\ … include 'api\advapi32.inc'

17

В предыдущей части мы познакомились с основными функциями для работы с реестром Windows. Сегодня научимся их использовать на примерах простейших программ. Не забывайте, что реестр — достаточно серьезная вещь, поэтому будьте предельно внимательны: ошибка при работе с реестром может привести к отказу всей операционной системы. Тем не менее, волков бояться — в лес не ходить. Так что приступим! Для начала создадим ключ реестра в ветке HKEY_CURRENT_USER: format PE GUI 4.0 entry start include 'win32a.inc' include 'macro/if.inc' ERROR_SUCCESS = 0 section '.data' data readable writeable szREGSZ db 'REG_SZ',0 szTestKey db 'Test Key',0 szOK db 'Ключ был создан или уже существовал.',0 section '.data?' readable writeable hKey dd ? lpdwDisp dd ? section '.code' code readable executable start: ;Открываем или создаем ключ реестра invoke RegCreateKeyEx,HKEY_CURRENT_USER,szTestKey,0,szREGSZ,0,KEY_WRITE or KEY_READ,0,hKey,lpdwDisp .if eax = ERROR_SUCCESS invoke MessageBox,0,szOK,szTestKey,MB_OK + MB_ICONASTERISK ;Закрываем ключ реестра invoke RegCloseKey,hKey.else invoke MessageBox,0,0,0,0 .endif exit: invoke ExitProcess,0 section '.idata' import data readable writeable library advapi32,'ADVAPI32.DLL',\ kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL' include 'api\advapi32.inc' include 'api\kernel32.inc' include 'api\user32.inc' Если ключ 'Test Key' уже присутствует в ветке HKEY_CURRENT_USER, например, если вы запустите программу повторно, то данный ключ не будет создан заново, а просто будет открыт. Если такого ключа нет, то программа создаст его. В случае невозможности по каким-либо причинам создать или открыть ключ программа выдаст ошибку. После запуска программы убедитесь, что ключ действительно был создан. Для этого запустите regedit.exe: выберите в меню Пуск пункт Выполнить, введите команду regedit и подтвердите нажатием кнопки OK или Enter на клавиатуре. В редакторе реестра раскройте дерево ключей в ветке HKEY_CURRENT_USER и найдите в нем ключ Test Key. Если вы еще не в курсе: ключ в regedit отображается в виде желтой папочки. Если вы открыли редактор реестра еще до запуска программы, вам может понадобиться нажать F5 для обновления выведенной информации. Для того, чтобы программно удалить ключ из реестра, нам необходимо открыть ключ, удалить его, закрыть дескриптор ключа. Создадим копию нашей программы создания ключа и внесем в нее необходимые изменения: … szOK db 'Ключ успешно удален.',0 szError db 'Ключ не существует.',0 … start: ;Открываем ключ реестра invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_WRITE or KEY_READ,hKey .if eax = ERROR_SUCCESS invoke RegDeleteKey,HKEY_CURRENT_USER,szTestKey .if eax = ERROR_SUCCESS invoke MessageBox,0,szOK,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif ;Закрываем ключ реестра invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … Здесь мы пытаемся открыть ключ, и если это нам удается — пытаемся его удалить. Если попытка открытия или удаления открытого ключа не завершилась значением ERROR_SUCCESS, выводим сообщение об ошибке. Причем, если открытие ключа удалось произвести, то закрытие ключа необходимо произвести в любом случае независимо от результата его удаления. Если же не удалось произвести открытие ключа, то и закрывать нечего. Просто выводим сообщение и выходим из программы. Следующий пункт — создание значения в ключе реестра. Необходимо всегда перед созданием значения, его чтением, изменением или удалением убедиться, что в реестре присутствует ключ, значение которого будет создаваться, читаться, изменяться или удаляться. В любом случае в первом параметре функции RegSetValueEx необходимо указать дескриптор открытого ключа, полученный при помощи RegCreateKeyEx или RegOpenKeyEx. Мы уже знаем, как создавать и открывать ключи, поэтому следует просто не забывать делать это перед добавлением или изменением значения. Внесем необходимые изменения в нашу программу: … szOK db 'Значение успешно создано или изменено.',0 ddValue dd 01020304h ValSize dd 4 … start: ;Открываем или создаем ключ реестра invoke RegCreateKeyEx,HKEY_CURRENT_USER,szTestKey,0,szREGSZ,0,KEY_WRITE or KEY_READ,0,hKey,lpdwDisp .if eax = ERROR_SUCCESS ;Создаем значение invoke RegSetValueEx,[hKey],szValueName,0,REG_DWORD,ddValue,[ValSize] .if eax = ERROR_SUCCESS invoke MessageBox,0,szOK,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif ;Закрываем ключ реестра invoke RegCloseKey,hKey .else invoke MessageBox,0,0,0,0 .endif exit: invoke ExitProcess,0 … Данная программа создает значение в заданном ключе, а также может создать и сам ключ в случае его отсутствия. Если бы ключ создавать не требовалось, а необходимо было создать значение в уже существующем ключе, следовало бы использовать функцию RegOpenKeyEx. Она не станет создавать ключ, а выдаст ошибку, если не удастся его открыть. Откройте regedit и убедитесь, что программа создала значение в заданном ключе реестра. Вы можете увидеть, что в том же ключе существует еще и безымянное значение (по умолчанию). Для того, чтобы изменить его тип и содержимое, необходимо лишь опустить второй параметр функции RegSetValueEx, то есть указать ноль вместо имени значения. Все значения в ключах реестра создаются для того, чтобы когда-нибудь быть прочитанными. Следующий шаг — научиться получать содержимое заданного значения. Для этого используется функция RegQueryValueEx. Следующие изменения в программе позволят нам получить содержимое значения и его тип: … szError db 'Ключ не существует.',0 form db 'Тип: %u Значение: %08X',0 … buffer rb 256 ddValue dd ? lpType dd ? … start: ;Открываем ключ реестра invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_READ,hKey .if eax = ERROR_SUCCESS ;Получаем значение invoke RegQueryValueEx,[hKey],szValueName,0,lpType,ddValue,ValSize .if eax = ERROR_SUCCESS invoke wsprintf,buffer,form,[lpType],[ddValue] invoke MessageBox,0,buffer,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif ;Закрываем ключ реестра invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … Здесь мы только открываем ключ, потому что, если он не существует, то его значение мы уж точно не узнаем. Если ключ открыт, и значение получено — преобразуем его содержимое и тип при помощи функции wsprintf. %u означает беззнаковое целое (unsigned), а %08X — шестнадцатеричное целое, выводимое в восьми знаках, чтобы не потерялись нолики в начале содержимого, если они там есть. Тип мы выводим в цифровом виде. Четверка соответствует значению REG_DWORD или REG_DWORD_LITTLE_ENDIAN. Остальные значения вы можете найти в файле KERNEL32.INC в папке ..\FASM\INCLUDE\EQUATES\ вашего компилятора. Можно было бы организовать вывод типа значения и в текстовом виде, но в данном случае это лишняя работа, да и сейчас разговор не об этом. Работа со строковыми типами значений не сильно отличается от вышеприведенных примеров, поэтому на них мы тоже останавливаться не будем. Возникнут вопросы — пишите, отвечу. Рассмотрим лучше способ получения сводной информации об открытом ключе и его значениях: … form db 'Подразделов: %u',13,'Значений: %u',0 … SubKeysNumber dd ? ValuesNumber dd ? … start: invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_READ,hKey .if eax = ERROR_SUCCESS ;Получаем информацию о ключе invoke RegQueryInfoKey,[hKey],0,0,0,SubKeysNumber,0,0,ValuesNumber,0,0,0,0 .if eax = ERROR_SUCCESS invoke wsprintf,buffer,form,[SubKeysNumber],[ValuesNumber] invoke MessageBox,0,buffer,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … В данном примере мы получаем информацию о количестве подразделов открытого ключа и количестве значений в данном ключе. Полное описание параметров функции RegQueryInfoKey можно прочитать в предыдущей части статьи. Добавьте в тестовый ключ вручную или при помощи измененной программы несколько подразделов и несколько значений и убедитесь, что выдаваемые данной программой сведения о ключе верны. Я не зря попросил вас создать в тестовом ключе несколько подразделов, потому что сейчас мы попробуем программно перечислить подразделы ключа. Надеюсь, вы еще не забыли азы организации программного цикла. Если вдруг забыли, то сейчас заодно и вспомните: … ERROR_NO_MORE_ITEMS = 259 … bufsize dd 256 index dd 0 … start: invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_READ,hKey .if eax = ERROR_SUCCESS ;Перечислим подразделы ключа cycl: mov [bufsize],256 invoke RegEnumKeyEx,[hKey],[index],buffer,bufsize,0,0,0,0 .if eax = ERROR_SUCCESS inc [index] invoke MessageBox,0,buffer,szTestKey,MB_OK + MB_ICONASTERISK jmp cycl .elseif eax = ERROR_NO_MORE_ITEMS .else invoke MessageBox,0,0,0,0 .endif invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … После того, как ключ открыт, начинается цикл. В цикле нам необходимо каждый раз устанавливать значение переменной [bufsize] в размер буфера в байтах. Этим нельзя пренебрегать, потому что после вызова функции в этой переменной уже будет находиться количество скопированных символов без учета завершающего нуля, и при следующем вызове функция может подумать, что в буфере не хватает места, опираясь на возвращенное в предыдущем вызове значение. Если после вызова функции мы получаем ответ ERROR_SUCCESS, то увеличиваем индекс на единицу, выводим сообщение с именем подраздела и повторяем цикл. Если ответ функции оказался ERROR_NO_MORE_ITEMS, то мы ничего не делаем, а просто вываливаемся из второго макроса ".if" прямо на функцию RegCloseKey. В остальных случаях — выводим сообщение об ошибке и, опять же, на функцию RegCloseKey. Здесь, правда, не совсем корректно учтена ситуация, когда подразделов изначально не окажется: программа не выдаст вообще никаких сообщений. Но это ведь всего лишь учебный пример, который предполагает определенные исходные условия. Кроме того, я думаю, вам и самим не составит труда включить в этот пример корректную обработку отсутствия подразделов в заданном ключе. Перечисление значений ключа осуществляется аналогично. Для того, чтобы перечислить только имена значений, в вышеприведенной программе перечисления подразделов потребуется лишь заменить RegEnumKeyEx на RegEnumValue. Если же понадобится получить еще и типы значений, и их содержимое, то придется немного доработать программку. Это не сложно, и при необходимости вы легко справитесь с этой задачей. В принципе, про работу с реестром можно говорить долго и нудно, но про основные методы я вам все рассказал и показал. Теперь в этой области вы имеете хорошую точку опоры, оттолкнувшись от которой, можно продолжать плавание в любом разумном направлении. Подробно расписывать способы удаления целых разделов я не стану, чтобы такой "халявой" не соблазнить вас на написание вредоносных программ. Тот, кому это действительно понадобится на деле, всегда сможет додуматься сам или спросить у более опытных товарищей по коду. Тот, кто хочет отомстить соседу, пусть лучше поразит его своими полезными программами. На этом и распрощаемся. Всех вам благ, и успехов в понимании собственноручно написанного кода!

18

В предыдущей части мы познакомились с основными функциями для работы с реестром Windows. Сегодня научимся их использовать на примерах простейших программ. Не забывайте, что реестр — достаточно серьезная вещь, поэтому будьте предельно внимательны: ошибка при работе с реестром может привести к отказу всей операционной системы. Тем не менее, волков бояться — в лес не ходить. Так что приступим! Для начала создадим ключ реестра в ветке HKEY_CURRENT_USER: format PE GUI 4.0 entry start include 'win32a.inc' include 'macro/if.inc' ERROR_SUCCESS = 0 section '.data' data readable writeable szREGSZ db 'REG_SZ',0 szTestKey db 'Test Key',0 szOK db 'Ключ был создан или уже существовал.',0 section '.data?' readable writeable hKey dd ? lpdwDisp dd ? section '.code' code readable executable start: ;Открываем или создаем ключ реестра invoke RegCreateKeyEx,HKEY_CURRENT_USER,szTestKey,0,szREGSZ,0,KEY_WRITE or KEY_READ,0,hKey,lpdwDisp .if eax = ERROR_SUCCESS invoke MessageBox,0,szOK,szTestKey,MB_OK + MB_ICONASTERISK ;Закрываем ключ реестра invoke RegCloseKey,hKey.else invoke MessageBox,0,0,0,0 .endif exit: invoke ExitProcess,0 section '.idata' import data readable writeable library advapi32,'ADVAPI32.DLL',\ kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL' include 'api\advapi32.inc' include 'api\kernel32.inc' include 'api\user32.inc' Если ключ 'Test Key' уже присутствует в ветке HKEY_CURRENT_USER, например, если вы запустите программу повторно, то данный ключ не будет создан заново, а просто будет открыт. Если такого ключа нет, то программа создаст его. В случае невозможности по каким-либо причинам создать или открыть ключ программа выдаст ошибку. После запуска программы убедитесь, что ключ действительно был создан. Для этого запустите regedit.exe: выберите в меню Пуск пункт Выполнить, введите команду regedit и подтвердите нажатием кнопки OK или Enter на клавиатуре. В редакторе реестра раскройте дерево ключей в ветке HKEY_CURRENT_USER и найдите в нем ключ Test Key. Если вы еще не в курсе: ключ в regedit отображается в виде желтой папочки. Если вы открыли редактор реестра еще до запуска программы, вам может понадобиться нажать F5 для обновления выведенной информации. Для того, чтобы программно удалить ключ из реестра, нам необходимо открыть ключ, удалить его, закрыть дескриптор ключа. Создадим копию нашей программы создания ключа и внесем в нее необходимые изменения: … szOK db 'Ключ успешно удален.',0 szError db 'Ключ не существует.',0 … start: ;Открываем ключ реестра invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_WRITE or KEY_READ,hKey .if eax = ERROR_SUCCESS invoke RegDeleteKey,HKEY_CURRENT_USER,szTestKey .if eax = ERROR_SUCCESS invoke MessageBox,0,szOK,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif ;Закрываем ключ реестра invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … Здесь мы пытаемся открыть ключ, и если это нам удается — пытаемся его удалить. Если попытка открытия или удаления открытого ключа не завершилась значением ERROR_SUCCESS, выводим сообщение об ошибке. Причем, если открытие ключа удалось произвести, то закрытие ключа необходимо произвести в любом случае независимо от результата его удаления. Если же не удалось произвести открытие ключа, то и закрывать нечего. Просто выводим сообщение и выходим из программы. Следующий пункт — создание значения в ключе реестра. Необходимо всегда перед созданием значения, его чтением, изменением или удалением убедиться, что в реестре присутствует ключ, значение которого будет создаваться, читаться, изменяться или удаляться. В любом случае в первом параметре функции RegSetValueEx необходимо указать дескриптор открытого ключа, полученный при помощи RegCreateKeyEx или RegOpenKeyEx. Мы уже знаем, как создавать и открывать ключи, поэтому следует просто не забывать делать это перед добавлением или изменением значения. Внесем необходимые изменения в нашу программу: … szOK db 'Значение успешно создано или изменено.',0 ddValue dd 01020304h ValSize dd 4 … start: ;Открываем или создаем ключ реестра invoke RegCreateKeyEx,HKEY_CURRENT_USER,szTestKey,0,szREGSZ,0,KEY_WRITE or KEY_READ,0,hKey,lpdwDisp .if eax = ERROR_SUCCESS ;Создаем значение invoke RegSetValueEx,[hKey],szValueName,0,REG_DWORD,ddValue,[ValSize] .if eax = ERROR_SUCCESS invoke MessageBox,0,szOK,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif ;Закрываем ключ реестра invoke RegCloseKey,hKey .else invoke MessageBox,0,0,0,0 .endif exit: invoke ExitProcess,0 … Данная программа создает значение в заданном ключе, а также может создать и сам ключ в случае его отсутствия. Если бы ключ создавать не требовалось, а необходимо было создать значение в уже существующем ключе, следовало бы использовать функцию RegOpenKeyEx. Она не станет создавать ключ, а выдаст ошибку, если не удастся его открыть. Откройте regedit и убедитесь, что программа создала значение в заданном ключе реестра. Вы можете увидеть, что в том же ключе существует еще и безымянное значение (по умолчанию). Для того, чтобы изменить его тип и содержимое, необходимо лишь опустить второй параметр функции RegSetValueEx, то есть указать ноль вместо имени значения. Все значения в ключах реестра создаются для того, чтобы когда-нибудь быть прочитанными. Следующий шаг — научиться получать содержимое заданного значения. Для этого используется функция RegQueryValueEx. Следующие изменения в программе позволят нам получить содержимое значения и его тип: … szError db 'Ключ не существует.',0 form db 'Тип: %u Значение: %08X',0 … buffer rb 256 ddValue dd ? lpType dd ? … start: ;Открываем ключ реестра invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_READ,hKey .if eax = ERROR_SUCCESS ;Получаем значение invoke RegQueryValueEx,[hKey],szValueName,0,lpType,ddValue,ValSize .if eax = ERROR_SUCCESS invoke wsprintf,buffer,form,[lpType],[ddValue] invoke MessageBox,0,buffer,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif ;Закрываем ключ реестра invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … Здесь мы только открываем ключ, потому что, если он не существует, то его значение мы уж точно не узнаем. Если ключ открыт, и значение получено — преобразуем его содержимое и тип при помощи функции wsprintf. %u означает беззнаковое целое (unsigned), а %08X — шестнадцатеричное целое, выводимое в восьми знаках, чтобы не потерялись нолики в начале содержимого, если они там есть. Тип мы выводим в цифровом виде. Четверка соответствует значению REG_DWORD или REG_DWORD_LITTLE_ENDIAN. Остальные значения вы можете найти в файле KERNEL32.INC в папке ..\FASM\INCLUDE\EQUATES\ вашего компилятора. Можно было бы организовать вывод типа значения и в текстовом виде, но в данном случае это лишняя работа, да и сейчас разговор не об этом. Работа со строковыми типами значений не сильно отличается от вышеприведенных примеров, поэтому на них мы тоже останавливаться не будем. Возникнут вопросы — пишите, отвечу. Рассмотрим лучше способ получения сводной информации об открытом ключе и его значениях: … form db 'Подразделов: %u',13,'Значений: %u',0 … SubKeysNumber dd ? ValuesNumber dd ? … start: invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_READ,hKey .if eax = ERROR_SUCCESS ;Получаем информацию о ключе invoke RegQueryInfoKey,[hKey],0,0,0,SubKeysNumber,0,0,ValuesNumber,0,0,0,0 .if eax = ERROR_SUCCESS invoke wsprintf,buffer,form,[SubKeysNumber],[ValuesNumber] invoke MessageBox,0,buffer,szTestKey,MB_OK + MB_ICONASTERISK .else invoke MessageBox,0,0,0,0 .endif invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … В данном примере мы получаем информацию о количестве подразделов открытого ключа и количестве значений в данном ключе. Полное описание параметров функции RegQueryInfoKey можно прочитать в предыдущей части статьи. Добавьте в тестовый ключ вручную или при помощи измененной программы несколько подразделов и несколько значений и убедитесь, что выдаваемые данной программой сведения о ключе верны. Я не зря попросил вас создать в тестовом ключе несколько подразделов, потому что сейчас мы попробуем программно перечислить подразделы ключа. Надеюсь, вы еще не забыли азы организации программного цикла. Если вдруг забыли, то сейчас заодно и вспомните: … ERROR_NO_MORE_ITEMS = 259 … bufsize dd 256 index dd 0 … start: invoke RegOpenKeyEx,HKEY_CURRENT_USER,szTestKey,0,KEY_READ,hKey .if eax = ERROR_SUCCESS ;Перечислим подразделы ключа cycl: mov [bufsize],256 invoke RegEnumKeyEx,[hKey],[index],buffer,bufsize,0,0,0,0 .if eax = ERROR_SUCCESS inc [index] invoke MessageBox,0,buffer,szTestKey,MB_OK + MB_ICONASTERISK jmp cycl .elseif eax = ERROR_NO_MORE_ITEMS .else invoke MessageBox,0,0,0,0 .endif invoke RegCloseKey,hKey .else invoke MessageBox,0,szError,0,0 .endif exit: invoke ExitProcess,0 … После того, как ключ открыт, начинается цикл. В цикле нам необходимо каждый раз устанавливать значение переменной [bufsize] в размер буфера в байтах. Этим нельзя пренебрегать, потому что после вызова функции в этой переменной уже будет находиться количество скопированных символов без учета завершающего нуля, и при следующем вызове функция может подумать, что в буфере не хватает места, опираясь на возвращенное в предыдущем вызове значение. Если после вызова функции мы получаем ответ ERROR_SUCCESS, то увеличиваем индекс на единицу, выводим сообщение с именем подраздела и повторяем цикл. Если ответ функции оказался ERROR_NO_MORE_ITEMS, то мы ничего не делаем, а просто вываливаемся из второго макроса ".if" прямо на функцию RegCloseKey. В остальных случаях — выводим сообщение об ошибке и, опять же, на функцию RegCloseKey. Здесь, правда, не совсем корректно учтена ситуация, когда подразделов изначально не окажется: программа не выдаст вообще никаких сообщений. Но это ведь всего лишь учебный пример, который предполагает определенные исходные условия. Кроме того, я думаю, вам и самим не составит труда включить в этот пример корректную обработку отсутствия подразделов в заданном ключе. Перечисление значений ключа осуществляется аналогично. Для того, чтобы перечислить только имена значений, в вышеприведенной программе перечисления подразделов потребуется лишь заменить RegEnumKeyEx на RegEnumValue. Если же понадобится получить еще и типы значений, и их содержимое, то придется немного доработать программку. Это не сложно, и при необходимости вы легко справитесь с этой задачей. В принципе, про работу с реестром можно говорить долго и нудно, но про основные методы я вам все рассказал и показал. Теперь в этой области вы имеете хорошую точку опоры, оттолкнувшись от которой, можно продолжать плавание в любом разумном направлении. Подробно расписывать способы удаления целых разделов я не стану, чтобы такой "халявой" не соблазнить вас на написание вредоносных программ. Тот, кому это действительно понадобится на деле, всегда сможет додуматься сам или спросить у более опытных товарищей по коду. Тот, кто хочет отомстить соседу, пусть лучше поразит его своими полезными программами. На этом и распрощаемся. Всех вам благ, и успехов в понимании собственноручно написанного кода!

19

В прошлый раз мы научились выводить bitmap в качестве фона диалогового окна. Сегодня продолжим разговор об использовании графических элементов в окне программы. Рассмотрим отображение графики в обычном окне. Этот способ несколько сложнее, чем отображение картинки в диалоговом окне, ввиду того, что некоторые вещи, которые выполнялись системой автоматически, нам придется выполнить самостоятельно. Зато мы сможем более подробно рассмотреть механизм отрисовки графики в окне и лучше понять, как это реализуется в окнах. Итак, если для отображения картинки в диалоговом окне нам было достаточно указать в статическом элементе идентификатор ресурса с картинкой и размеры отображаемого рисунка, то теперь надо будет при создании окна загрузить картинку из ресурсов в память функцией LoadBitmap. Она работает аналогично функциям LoadIcon и LoadCursor и имеет те же два параметра: дескриптор исполняемого модуля, из ресурсов которого будет загружена картинка, и идентификатор ресурса с картинкой. При указании нуля в первом параметре можно указать во втором параметре значение стандартного системного изображения (от 32734 до 32767), некоторые из которых могут отсутствовать в современных ОС, да и вообще это уже достаточно редко используемый вариант. После загрузки изображения мы, как и в случаях с иконками и курсорами, получаем его дескриптор. Однако если дескриптор загруженной иконки или курсора мы можем сразу поместить в соответствующий элемент структуры WNDCLASS непосредственно перед регистрацией класса окна, то дескриптор загруженного изображения является промежуточным идентификатором отображаемой картинки. Картинка выводится в окно достаточно долгими окольными путями. Включите мозги на максимальное восприятие — сейчас будет нехило! Если в окне требуется отрисовка какого-либо изображения или чего-либо еще, то она производится по получению окном сообщения WM_PAINT. Получили сообщение — начинаем рисовать. Для этого вызываем функцию BeginPaint, которая подготавливает окно к перерисовке и заполняет структуру PAINTSTRUCT необходимыми данными. Функция имеет два параметра: дескриптор перерисовываемого окна и указатель на структуру PAINTSTRUCT. А возвращает она дескриптор контекста графического устройства (handle to a display device context) перерисовываемого окна. Этот труднопроизносимый "контекст графического устройства" в народе называют не иначе как DC, а дескриптор (handle) на него — HDC. DC представляет собой сложную структуру данных, которая в нашем случае используется для хранения текущего графического содержимого окна. Чтобы что-либо отрисовать в окне, мы должны получить доступ к DC его клиентской области. Функция BeginPaint как раз и предоставляет нам этот доступ. Главное после отрисовки — не забыть освободить DC окна функцией EndPaint. Рисовать прямо в DC не принято. Обычно для вывода изображения на экран используется метод двойной буферизации. Создается невидимая на экране копия DC перерисовываемого окна (вторичный буфер), в ней рисуют все, что душе угодно, а потом этот вторичный буфер быстренько копируют в первичный. Таким образом реализуется быстрый вывод изображения на экран. Допустим, мы получили HDC нашего окна. Теперь при помощи функции CreateCompatibleDC мы создадим совместимый DC, он же вторичный буфер. Эта функция имеет всего один параметр — дескриптор DC, для которого будет создана совместимая копия. В случае успеха функция возвращает дескриптор на область памяти вторичного буфера. Сохраним его в переменную "hMemDC" и идем дальше. Функция SelectObject выбирает объект для отображения в указанном DC. Она имеет два параметра: дескриптор DC и дескриптор отображаемого объекта. Картинки могут быть отображены только на виртуальном холсте, то есть только на вторичном буфере. Это еще раз объясняет, почему мы используем двойную буферизацию и рисуем картинку сначала в копию DC. Еще один момент, который важно запомнить сейчас, чтобы потом часами не отлавливать глюки программы, — это то, что каждая картинка одновременно может быть выбрана лишь для одного буфера DC. Хотите выбрать одну и ту же картинку сразу в несколько DC — загрузите ее из ресурсов несколько раз или скопируйте из одного DC в другой. Возвращаемые функцией SelectObject значения рассматривать будем при более благоприятных обстоятельствах вместе с ее остальными возможностями — такими, как выбор кисти, шрифта, пера и региона. А пока не забиваем переполненный мозг второстепенной информацией и двигаемся вперед. Ф ункция GetClientRect сообщает в указанную во втором параметре структуру RECT координаты клиентской области окна. В первом параметре, как вы могли догадаться, указывается дескриптор этого окна. Координаты left и top (левый верхний угол) будут равны нулю ввиду того, что они отсчитываются как раз относительно левого верхнего угла клиентской области окна. А вот right и bottom (правый нижний угол) будут соответствовать ширине и высоте клиентской области. Собственно, они нам и понадобятся для использования в следующей функции. Сейчас поймете, почему. Функция BitBlt (bit-block transfer) осуществляет передачу битовых блоков данных о цветах пикселей из одного DC в другой. Ее параметры следующие: 1. Дескриптор DC приемника. 2,3. Координаты X и Y левого верхнего угла прямоугольника, в который будет передано изображение. 4,5. Ширина и высота передаваемой прямоугольной области. 6. Дескриптор DC источника. 7,8. Координаты X и Y левого верхнего угла прямоугольника, из которого будет передано изображение. 9. Значение, определяющее способ передачи битовых блоков. Способы передачи битовых блоков могут быть следующими: BLACKNESS — закрасить выбранный прямоугольник DC приемника в черный цвет. DSTINVERT — инвертировать выбранный прямоугольник DC приемника. MERGECOPY — смешать посредством оператора AND цвета прямоугольника DC источника с цветом кисти, выбранной для DC приемника. MERGEPAINT — смешать посредством оператора OR цвета инвертированного прямоугольника DC источника с цветами прямоугольника DC приемника. NOTSRCCOPY — скопировать инвертированный прямоугольник DC источника в приемник. NOTSRCERASE — объединить цвета прямоугольников DC источника и приемника посредством оператора OR, а затем инвертировать цвета результата. PATCOPY — закрасить прямоугольник DC приемника цветом кисти, выбранной для DC приемника. PATINVERT — объединить посредством оператора XOR цвета выбранного прямоугольника DC приемника с цветом кисти, выбранной для DC приемника. PATPAINT — объединить посредством оператора OR цвета инвертированного прямоугольника DC источника с цветом кисти, выбранной для DC приемника. А полученный результат еще объединить с цветами прямоугольника DC приемника посредством того же OR. SRCAND — объединить цвета прямоугольников DC источника и приемника посредством оператора AND. SRCCOPY — скопировать прямоугольник источника в прямоугольник приемника. SRCERASE — объединить посредством оператора AND инвертированные цвета прямоугольника DC приемника с цветами прямоугольника DC источника. SRCINVERT — объединить цвета прямоугольников DC источника и приемника посредством оператора XOR. SRCPAINT — объединить цвета прямоугольников DC источника и приемника посредством оператора OR. WHITENESS — закрасить выбранный прямоугольник DC приемника в белый цвет. Для начала нам бы хватило и способа SRCCOPY — простого копирования битовых блоков, но вдруг кто-то захочет самостоятельно попробовать другие способы? Тем более, что скоро некоторые из них нам уже смогут пригодиться. После копирования прямоугольника с картинкой размером с клиентскую область нашего окна из вторичного буфера в первичный мы уже сможем видеть ее на экране. Теперь нам останется лишь удалить наш вторичный буфер при помощи функции DeleteDC, чтобы он не занимал лишнюю память. И закончим отрисовку вызовом EndPaint, которая обязательно должна вызываться каждый раз после BeginPaint. Параметры функции: дескриптор перерисованного окна и указатель на структуру PAINTSTRUCT, использованную в BeginPaint. Теперь можете ознакомиться с примером программы: format PE GUI 4.0 entry start include 'win32a.inc' HTCAPTION = 2 section '.data' data readable writeable _class TCHAR 'FASMWIN32',0 _title TCHAR 'Картинка в окне',0 _error TCHAR 'Ошибка запуска.',0 wc WNDCLASS 0,WindowProc,0,0,NULL,NULL,NULL,COLOR_BTNFACE+1,NULL,_class msg MSG ps PAINTSTRUCT rect RECT hBitmap dd ? hdc dd ? hMemDC dd ? section '.code' code readable executable start: invoke GetModuleHandle,0 mov [wc.hInstance],eax invoke LoadIcon,[wc.hInstance],17 mov [wc.hIcon],eax invoke LoadCursor,[wc.hInstance],27 mov [wc.hCursor],eax invoke RegisterClass,wc test eax,eax jz error invoke CreateWindowEx,0,_class,_title,WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU,128,128,326,271,NULL,NULL,[wc.hInstance],NULL test eax,eax jz error msg_loop: invoke GetMessage,msg,NULL,0,0 cmp eax,1 jb end_loop jne msg_loop invoke TranslateMessage,msg invoke DispatchMessage,msg jmp msg_loop error: invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK end_loop: invoke ExitProcess,[msg.wParam] proc WindowProc hwnd,wmsg,wparam,lparam push ebx esi edi mov eax,[wmsg] cmp eax,WM_CREATE je .wmcreate cmp eax,WM_PAINT je .wmpaint cmp eax,WM_DESTROY je .wmdestroy cmp eax,WM_LBUTTONDOWN je .wmlbuttondown .defwndproc: invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam] jmp .finish .wmcreate: invoke LoadBitmap,[wc.hInstance],37 mov [hBitmap],eax jmp .finish .wmpaint: invoke BeginPaint,[hwnd],ps mov [hdc],eax invoke CreateCompatibleDC,[hdc] mov [hMemDC],eax invoke SelectObject,[hMemDC],[hBitmap] invoke GetClientRect,[hwnd],rect invoke BitBlt,[hdc],0,0,[rect.right],[rect.bottom],[hMemDC],0,0,SRCCOPY invoke DeleteDC,[hMemDC] invoke EndPaint,[hwnd],ps jmp .finish .wmlbuttondown: invoke ReleaseCapture invoke SendMessage,[hwnd],WM_NCLBUTTONDOWN,HTCAPTION,0 jmp .finish .wmdestroy: invoke PostQuitMessage,0 xor eax,eax .finish: pop edi esi ebx ret endp section '.idata' import data readable writeable library gdi32,'GDI32.DLL',\ kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL' include 'api\gdi32.inc' include 'api\kernel32.inc' include 'api\user32.inc' section '.rsrc' resource data readable … RT_CURSOR,cursors,\ RT_GROUP_CURSOR,group_cursors,\ … resource cursors,\ 2,LANG_NEUTRAL,cursor_data resource group_cursors,\ 27,LANG_NEUTRAL,main_cursor … cursor main_cursor,cursor_data,'cursor.cur' … Секция ресурсов отличается от примера из прошлой программы отсутствием шаблона диалогового окна и наличием ресурса описания курсора, так что я не стал приводить ее полностью — думаю, догадаетесь, что к чему. Заметьте, что для использования некоторых графических функций мы подключаем в секцию импорта библиотеку GDI32.DLL. Для возможности перетаскивания окна за его клиентскую область оставлен обработчик сообщения WM_LBUTTONDOWN. На сегодня все. Экспериментируйте с новыми функциями, изучайте новые возможности. В следующий раз продолжим разговор про отображение графики в окошках.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]