Превращаем SoftModalMessageBox при помощи хуков в окно диалога Теория ― МАТЬ СКЛЕРОЗА Все, что нужно – это установить локальный хук, вызвать SoftModalMessageBox, выполнить в обработчике хука все необходимые действия и снять хук по завершении SoftModalMessageBox первое оповещение типа HCBT_CREATEWND, пришедшее в наш обработчик, даст нам HWND окна сообщения, которое мы и будем использовать в дальнейшем Функции Windows для работы с хуками Приложения Windows используют функции SetWindowsHookEx, UnhookWindowsHookEx, и CallNextHookEx для управления очередью функций-фильтров хука. SetWindowsHookEx Функция SetWindowsHookEx добавляет функцию-фильтр к хуку. Эта функция принимает четыре аргумента: Целочисленный код, описывающий хук, к которому будет прикреплена фильтрующая функция. Эти коды определены в WINUSER.H Адрес функции-фильтра. Эта функция должна быть описана как экспортируемая включением ее в секцию EXPORTS файла определения приложения или библиотеки динамической линковки (DLL), или использованием соответствующих опций компилятора. Хэндл модуля, содержащего фильтрующую функцию. В Win32 этот параметр должен быть NULL при установке хука на поток, но данное требование не является обязательным. При установке хука для всей системы или для потока в другом процессе, нужно использовать описатель DLL, содержащей функцию-фильтр. Идентификатор потока, для которого устанавливается хук. Если этот идентификатор ненулевой, установленная фильтрующая функция будет вызываться только в контексте указанного потока. Если идентификатор равен нулю, установленная функция имеет системную область видимости и может быть вызвана в контексте любого потока в системе. Приложение или библиотека могут использовать функцию GetCurrentThreadId для получения идентификатора текущего потока. Некоторые хуки могут быть установлены только с системной областью видимости, некоторые можно устанавливать как для всей системы, так и для одного потока, как показано в следующей таблице. ХукОбласть видимостиWH_CALLWNDPROCПоток или вся системаWH_CBTWH_DEBUGWH_GETMESSAGEWH_FOREGROUNDIDLEWH_SHELLWH_KEYBOARDWH_MOUSEWH_MSGFILTERWH_SYSMSGFILTERТолько системаWH_JOURNALRECORDWH_JOURNALPLAYBACKДля любого данного типа хука, первыми вызываются хуки потоков, и только затем системные хуки. Хуки потоков: Не создают лишней работы приложениям, которые не заинтересованы в вызове хука. Не помещают все события, относящиеся к хуку, в очередь (так, чтобы они поступали не одновременно, а одно за другим). Например, если приложение установит клавиатурный хук для всей системы, то все клавиатурные сообщения будут пропущены через фильтрующую функцию этого хука, оставляя неиспользованными системные возможности многопотоковой обработки ввода. Если эта функция прекратит обрабатывать клавиатурные события, система будет выглядеть зависшей, хотя на самом деле и не зависнет. Пользователь всегда сможет использовать комбинацию CTRL+ALT+DEL для того, чтобы выйти из системы (log-out) и решить проблему, но ему это вряд ли понравится. К тому же, пользователь может не знать, что подобную ситуацию можно решить, войдя в систему под другим именем (log-out/log-in). Не требуют нахождения функции-фильтра в отдельной DLL. Все системные хуки и хуки для потоков в другом приложении должны находиться в DLL. Им не нужно разделять данные между DLL, загруженными в разные процессы. Фильтрующие функции с системной областью видимости, которые обязаны находиться в DLL, должны к тому же разделять необходимые данные с другими процессами. Так как такое поведение не является типичным для DLL, вы должны принимать специальные меры предосторожности при реализации системных фильтрующих функций. Если функция-фильтр не умеет разделять данные и неправильно использует данные в другом процессе, этот процесс может рухнуть. SetWindowsHookEx возвращает описатель установленного хука. Приложение или библиотека должны использовать этот описатель для вызова функции UnhookWindowsHookEx. SetWindowsHookEx возвращает NULL если она не смогла добавить функцию к хуку. SetWindowsHookEx также устанавливает код последней ошибки в одно из следующих значений для индикации неудачного завершения функции. ERROR_INVALID_HOOK_FILTER: Неверный код хука. ERROR_INVALID_FILTER_PROC: Неверная фильтрующая функция. ERROR_HOOK_NEEDS_HMOD: Глобальный хук устанавливается с параметром hInstance, равным NULL либо локальный хук устанавливается для потока, который не принадлежит данному приложению. ERROR_GLOBAL_ONLY_HOOK: Хук, который может быть только системным, устанавливается как потоковый. ERROR_INVALID_PARAMETER: Неверный идентификатор потока. ERROR_JOURNAL_HOOK_SET: Для регистрационного хука (journal hook) уже установлена фильтрующая функция. В любой момент времени может быть установлен только один записывающий или воспроизводящий хук. Этот код ошибки может также означать, что приложение пытается установить регистрационный хук в то время, как запущен хранитель экрана. ERROR_MOD_NOT_FOUND: Параметр hInstance в случае, когда хук является глобальным, не ссылался на библиотеку. (На самом деле, это значение означает лишь, что модуль User не смог обнаружить данный описатель в списке модулей.) Любое другое значение: Система безопасности не позволяет установить данный хук, либо в системе закончилась память. UnhookWindowsHookEx Для удаления функции-фильтра из очереди хука вызовите функцию UnhookWindowsHookEx. Эта функция принимает описатель хука, полученный от SetWindowsHookEx и возвращает логическое значение, показывающее успех операции. На данный момент UnhookWindowsHookEx всегда возвращает TRUE. WH_CBT Для написания подчиненного приложения, разработчик должен координировать его работу с работой главного приложения, для которого оно разрабатывается. Для достижения этой цели Windows предоставляет разработчикам хук WH_CBT. Windows передает фильтрующей функции код хука, показывающий, какое произошло событие, и соответствующие этому событию данные. Фильтр для хука WH_CBT должен знать о десяти хуковых кодах: HCBT_ACTIVATE HCBT_CREATEWND HCBT_DESTROYWND HCBT_MINMAX HCBT_MOVESIZE HCBT_SYSCOMMAND HCBT_CLICKSKIPPED HCBT_KEYSKIPPED HCBT_SETFOCUS HCBT_QS HCBT_ACTIVATE Windows вызывает хук WH_CBT с этим кодом при активации какого-нибудь окна. Когда хук WH_CBT установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр в ответ на это событие вернет TRUE, окно не будет активизировано. Параметр wParam содержит описатель активизируемого окна. В lParam содержится указатель на структуру CBTACTIVATESTRUCT, которая описана следующим образом: Код (C): typedef struct tagCBTACTIVATESTRUCT { BOOL fMouse; // TRUE, если активация наступила в результате // мышиного клика; иначе FALSE. HWND hWndActive; // Содержит описатель окна, активного // в настоящий момент. } CBTACTIVATESTRUCT, *LPCBTACTIVATESTRUCT; HCBT_CREATEWND Windows вызывает хук WH_CBT с этим при создании окна. Когда хук установлен как локальный, это окно должно создаваться потоком, на который установлен хук. Хук WH_CBT вызывается до того, как Windows пошлет новому окну сообщения WM_GETMINMAXINFO, WM_NCCREATE, или WM_CREATE. Таким образом, фильтрующая функция может запретить создание окна, вернув TRUE. В параметре wParam содержится описатель создаваемого окна. В lParam ― указатель на следующую структуру. Код (C): // данные для HCBT_CREATEWND, на которые указывает lParam struct CBT_CREATEWND { struct tagCREATESTRUCT *lpcs; // Данные для создания нового окна. HWND hwndInsertAfter; // Описатель окна, после которого будет // добавлено это окно (Z-order). } CBT_CREATEWND, *LPCBT_CREATEWND; Функция-фильтр может изменить значение hwndInsertAfter или значения в lpcs. HCBT_DESTROYWND Windows вызывает хук WH_CBT с этим кодом перед уничтожением какого-либо окна. Если хук является локальным, это окно должно принадлежать потоку, на который установлен хук. Windows вызывает хук WH_CBT до посылки сообщения WM_DESTROY. Если функция-фильтр вернет TRUE, окно не будет уничтожено. Параметр wParam содержит описатель уничтожаемого окна. В lParam находится 0. HCBT_MINMAX Windows вызывает хук WH_CBT с этим кодом перед минимизацией или максимизацией окна. Когда хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр вернет TRUE, действие будет отменено. В wParam передается описатель окна, которое готовится к максимизации/минимизации. lParam содержит одну из SW_-констант, определенных в WINUSER.H и описывающих операцию над окном. HCBT_MOVESIZE Windows вызывает хук WH_CBT с этим кодом перед перемещением или изменением размеров окна, сразу после того, как пользователь закончил выбор новой позиции или размеров окна. Если хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр вернет TRUE, действие будет отменено. В wParam передается описатель перемещаемого/изменяемого окна. lParam содержит LPRECT, который указывает на новые координаты окна. HCBT_SYSCOMMAND Windows вызывает хук WH_CBT с этим кодом во время обработки системной команды. Если хук установлен как локальный, окно, чье системное меню вызвало данное событие, должно принадлежать потоку, на который установлен хук. Хук WH_CBT вызывается из функции DefWindowsProc. Если приложение не передает сообщение WH_SYSCOMMAND функции DefWindowsProc, это хук не получит управление. Если функция-фильтр вернет TRUE, системная команда не будет выполнена. В wParam содержится системная команда (SC_TASKLIST, SC_HOTKEY, и так далее), готовая к выполнению. Если в wParam передается SC_HOTKEY, в младшем слове lParam содержится описатель окна, к которому относится горячая клавиша. Если в wParam передается любое другое значение и если команда системного меню была выбрана мышью, в младшем слове lParam будет находиться горизонтальная позиция, а в старшем слове ― вертикальная позиция указателя мыши. Следующие системные команды приводят к срабатыванию этого хука изнутри DefWindowProc: SC_CLOSEЗакрыть окноSC_HOTKEYАктивировать окно, связанное с определенной горячей клавишейSC_HSCROLLГоризонтальная прокруткаSC_KEYMENUВыполнить команду меню по комбинации клавишSC_MAXIMIZEРазвернуть окно на весь экранSC_MINIMIZEСвернуть окноSC_MOUSEMENUВыполнить команду меню по щелчку мышиSC_MOVEПереместить окноSC_NEXTWINDOWПерейти к следующему окнуSC_PREVWINDOWПерейти к предыдущему окнуSC_RESTOREСохранить предыдущие координаты (контрольная точка ― checkpoint)SC_SCREENSAVEЗапустить хранитель экранаSC_SIZEИзменить размер окнаHCBT_CLICKSKIPPED Windows вызывает хук WH_CBT с этим кодом при удалении события от мыши из входной очереди потока, в случае, если установлен хук мыши. Windows вызовет системный хук, когда из какой-либо входной очереди будет удалено событие от мыши и в системе установлен либо глобальный, либо локальный хук мыши. Данный код передается только в том случае, если к хуку WH_MOUSE прикреплена фильтрующая функция. Несмотря на свое название, HCBT_CLICKSKIPPED генерируется не только для пропущенных событий от мыши, но и в случае, когда событие от мыши удаляется из системной очереди. Его главное назначение ― установить хук WH_JOURNALPLAYBACK в ответ на событие мыши. В wParam передается идентификатор сообщения мыши ― например, WM_LBUTTONDOWN или любое из сообщений WM_*BUTTON*. lParam содержит указатель на структуру MOUSEHOOKSTRUCT, которая описана следующим образом: Код (C): typedef struct tagMOUSEHOOKSTRUCT { POINT pt; // Позиция курсора мыши в координатах экрана HWND hwnd; // Окно, получающее сообщение UINT wHitTestCode; // Результат проверки координат (hit-testing) DWORD dwExtraInfo; // Доп.информация о сообщении } MOUSEHOOKSTRUCT, FAR *LPMOUSEHOOKSTRUCT, *PMOUSEHOOKSTRUCT; HCBT_KEYSKIPPED Windows вызывает хук WH_CBT с этим кодом при удалении клавиатурного события из системной очереди, в случае, если установлен клавиатурный хук. Windows вызовет системный хук, когда из какой-либо входной очереди будет удалено событие от клавиатуры и в системе установлен либо глобальный, либо локальный клавиатурный хук. Данный код передается только в том случае, если к хуку WH_KEYBOARD прикреплена фильтрующая функция. Несмотря на свое название, HCBT_KEYSKIPPED генерируется не только для пропущенных клавиатурных событий, но и в случае, когда клавиатурное событие удаляется из системной очереди. Его главное назначение ― установить хук WH_JOURNALPLAYBACK в ответ на клавиатурное событие. В wParam передается виртуальный код клавиши ― то же самое значение, что и в wParam функций GetMessage или PeekMessage для сообщений WM_KEY*. lParam содержит то же значение, что и lParam функций GetMessage или PeekMessage для сообщений WM_KEY*. WM_QUEUESYNC Часто приложение должно реагировать на события в процессе, для которого оно разработано. Обычно такими событиями являются события от клавиатуры или мыши. К примеру, пользователь нажимает на кнопку OK в диалоговом окне, после чего приложение желает послать главному приложению серию клавиатурных нажатий. Приложение может использовать хук мыши для определения момента нажатия кнопки OK. После этого, приложение должно выждать некоторое время, пока главное приложение не закончит обработку нажатия кнопки OK. Приложение может использовать сообщение WM_QUEUESYNC для определения момента окончания нужного действия. Слежение производится с помощью клавиатурного или мышиного хуков. Наблюдая за главным приложением с помощью хуков, приложение узнает о наступлении необходимого события. После этого приложение должно подождать окончания этого события, прежде чем приступать к выполнению ответных действий. Для определения момента окончания обработки события, приложение делает следующее: Ждет от Windows вызова хука WH_CBT с кодом HCBT_CLICKSKIPPED или HCBT_KEYSKIPPED. Это происходит при удалении из системной очереди события, которое приводит к срабатыванию обработчика в главном приложении. Устанавливает хук WH_JOURNALPLAYBACK. Приложение не может установить этот хук, пока не получит код HCBT_CLICKSKIPPED или HCBT_KEYSKIPPED. Хук WH_JOURNALPLAYBACK посылает приложению сообщение WM_QUEUESYNC. Когда приложение получает такое сообщение, оно может выполнить необходимые действия, например, послать главному приложению серию клавиатурных нажатий. HCBT_SETFOCUS Windows вызывает хук WH_CBT с таким кодом, когда Windows собирается передать фокус ввода какому-либо окну. Когда хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Если фильтр вернет TRUE, фокус ввода не изменится. В wParam передается описатель окна, получающего фокус ввода. lParam содержит описатель окна, теряющего фокус ввода. HCBT_QS Windows вызывает хук WH_CBT с этим кодом когда из системной очереди удаляется сообщение WM_QUEUESYNC, в то время как происходит изменение размеров или перемещение окна. Ни в каком другом случае этот хук не вызывается. Если хук установлен как локальный, это окно должно принадлежать потоку, на который установлен хук. Оба параметра ― и wParam, и lParam ― содержат ноль. ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИ файл tut_10m-0.asm Код (ASM): ; GUI # IDC_IMG equ 1001 IDC_ICON1 equ 1000 include win64a.inc includelib tut_10m-2.lib UninstallHook proto InstallHook proto :QWORD MSGBOXDATA struct params MSGBOXPARAMS <> pwndOwner QWORD ? wLanguageId DWORD ?,? pidButton QWORD ? ; // Array of button IDs ppszButtonText QWORD ? ; // Array of button text strings cButtons DWORD ? DefButton DWORD ? CancelId DWORD ? Timeout DWORD ? MSGBOXDATA ends .code MsgBoxText1 dw 0 MsgCaption8: du <Tutorial 10m: SoftModalMessageBox+Hook> sBTN1: du <Say Hello> sBTN2: du <Exit> dwBtnIds dd 1,2 dwTxtTbl dq sBTN1,sBTN2 aEdit db "EDIT",0 expTxt db "Hello!",0 align 16 WinMain proc local mb:MSGBOXPARAMS local mbxData:MSGBOXDATA mov mbxData.params.cbSize,sizeof MSGBOXPARAMS mov mbxData.params.hwndOwner,0 mov mbxData.params.hInstance,IMAGE_BASE movr mbxData.params.lpszText,MsgBoxText1 ;адрес текста в окне сообщений movr mbxData.params.lpszCaption,MsgCaption8 ;адрес заголовка в окне сообщений mov mbxData.params.dwStyle,MB_OK or MB_USERICON mov mbxData.params.lpszIcon,IDC_ICON1 mov mbxData.params.dwContextHelpId,0 movr mbxData.params.lpfnMsgBoxCallback,MsgBoxCallback ;адрес процедуры если нажмут кнопку "Say Hello!" mov mbxData.params.dwLanguageId,0 and mbxData.pwndOwner,0 and mbxData.wLanguageId,0 movr mbxData.pidButton,dwBtnIds movr mbxData.ppszButtonText,dwTxtTbl mov mbxData.cButtons,2 ;две кнопки mov mbxData.DefButton,0 ;кнопка "по умолчанию" -- "Say Hello!" mov mbxData.CancelId,2 or mbxData.Timeout,-1 ;---------------------------------- invoke InstallHook ;----------------------------------- lea ecx,mbxData invoke SoftModalMessageBox ;---------------------------------------- invoke UninstallHook invoke RtlExitUserProcess,NULL WinMain endp align 16 MsgBoxCallback proc lpHelpInfo:qword;(LPHELPINFO lpHelpInfo) invoke GetActiveWindow invoke FindWindowEx,eax,0,&aEdit,0 invoke SendMessage,eax,WM_SETTEXT,0,&expTxt leave xor eax,eax retn MsgBoxCallback endp end файл tut_10m-0.rc (ресурсы для EXE-файла) Код (C): #define IDC_ICON1 1000 IDC_ICON1 ICON DISCARDABLE "br_Fox1.ico" файл tut_10m-2.asm для создания DLL, которая будет использована сперва для установки, а потом для снятия локального хука Код (ASM): ; DLL # include win64a.inc IDC_IMG equ 1001 ;OPTION PROLOGUE:none .data hInstance dq ? hHook dq ? aStatic db "STATIC",0 aButton db "BUTTON",0 aEdit db "EDIT" aNull db 0 EditBoxString db "Wow! I'm in an edit box now",0 hwndMessageBox dq 0 m_hwndEditBox dq 0 image dd ? FileBmp db "03.bmp",0 .code DllMain proc ;hInstDLL:QWORD, reason:QWORD, unused:QWORD mov hInstance,rcx mov eax,TRUE retn DllMain Endp MouseProc proc nCode:QWORD,wParam:QWORD,lParam:QWORD mov nCode,rcx mov wParam,rdx mov lParam,r8 cmp ecx, HCBT_CREATEWND jz hcbtCREATEWND cmp ecx, HCBT_ACTIVATE jnz hcbtBYE hcbtACTIVATE: mov rax,m_hwndEditBox or eax,eax jnz hcbtBYE mov rax,wParam cmp hwndMessageBox,rax jnz hcbtBYE invoke InsetEditBox jmp hcbtBYE hcbtCREATEWND: mov rax,hwndMessageBox or eax, eax jnz hcbtBYE mov rax,wParam mov hwndMessageBox,rax ;hwndMessageBox = hwnd hcbtBYE:invoke CallNextHookEx,hHook,nCode,wParam,lParam leave ret MouseProc endp InstallHook proc hwnd:QWORD lea edx,MouseProc invoke SetWindowsHookEx,WH_CBT,,hInstance,NULL mov hHook,rax leave ret InstallHook endp UninstallHook proc enter 20h,0 invoke UnhookWindowsHookEx,hHook leave ret UninstallHook endp CalcEditBoxRect proc rectEditBox:qword,nGap:qword local rectTmp:RECT local hwndTextOrIcon:qword local hdcMessageBox:qword local old_rdi:qword mov nGap,rdx mov rectEditBox,rcx mov old_rdi,rdi ;Ищем иконку или текст, если иконки нет invoke FindWindowEx,hwndMessageBox,0,&aStatic,0 mov hwndTextOrIcon,rax or eax, eax jz locret lea edx, rectTmp invoke GetWindowRect,eax;hwndTextOrIcon or eax, eax jz locret ;Тут мы получили .left, отступ по вертикали, и, возможно, .bottom mov edx, rectTmp.left mov rdi, rectEditBox mov [rdi].RECT.left, edx ;rectEditBox->left = rectTmp.left invoke MapWindowPoints,0,hwndMessageBox,&rectTmp,1 mov rdx, nGap mov eax, rectTmp.top mov [rdx], eax mov ecx, rectTmp.bottom mov [rdi].RECT.bottom, ecx ;Ищем текст (если до этого нашли иконку) invoke FindWindowEx,hwndMessageBox,hwndTextOrIcon,&aStatic,0 or eax, eax jz @f lea edx,rectTmp invoke GetWindowRect,eax or eax, eax jz locret ;получили .right и .bottom @@: mov edx, rectTmp.right mov [rdi].RECT.right,edx ;rectEditBox->right = rectTmp.right mov eax,rectTmp.bottom ;if( rectTmp.bottom > rectEditBox->bottom ) cmp eax,[rdi].RECT.bottom jbe @f mov [rdi].RECT.bottom, eax ;rectEditBox->bottom = rectTmp.bottom ;Теперь нужно рассчитать размер текста и галочки @@: invoke GetWindowDC,hwndMessageBox mov hdcMessageBox,rax or eax, eax jz locret @@: invoke GetSystemMetrics,SM_CXMENUCHECK mov rectTmp.left,eax mov ecx,[rdi].RECT.left sub rectTmp.right,ecx ;rectTmp.right -= rectEditBox->left mov rectTmp.top, 0 mov rectTmp.bottom,4000h mov qword ptr [rsp+20h],DT_CALCRECT or DT_WORDBREAK or DT_NOPREFIX; format lea edx,EditBoxString ; m_lpEditBoxString invoke DrawText,hdcMessageBox,,-1,&rectTmp invoke ReleaseDC,hwndMessageBox,hdcMessageBox ;Получаем rectEditBox.top mov eax,[rdi].RECT.bottom ;rectEditBox->bottom sub eax,rectTmp.bottom mov [rdi].RECT.top,eax ;rectEditBox->top = rectEditBox->bottom - rectTmp.bottom invoke MapWindowPoints,0,hwndMessageBox,rdi,2 locret: mov rdi,old_rdi leave retn CalcEditBoxRect endp MoveButtonsDown proc nDistance:qword local rectButton:RECT local hwndButton:qword mov nDistance,rcx ;1A invoke FindWindowEx,hwndMessageBox,0,&aButton,0 mov hwndButton,rax @@: lea edx, rectButton invoke GetWindowRect,hwndButton ;3EBh 24Dh 443h 267h invoke MapWindowPoints,0,hwndMessageBox,&rectButton,2 mov qword ptr [rsp+30h],SWP_NOSIZE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_NOREDRAW and qword ptr [rsp+28h],0;cy and qword ptr [rsp+20h],0 mov r9d,20; rectButton.top mov r8d,325;rectButton.left ; X invoke SetWindowPos,hwndButton,0 invoke FindWindowEx,hwndMessageBox,hwndButton,&aButton,0 mov hwndButton,rax @@: lea edx, rectButton invoke GetWindowRect,hwndButton ;3EBh 24Dh 443h 267h invoke MapWindowPoints,0,hwndMessageBox,&rectButton,2 mov qword ptr [rsp+30h],SWP_NOSIZE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_NOREDRAW and qword ptr [rsp+28h],0;cy and qword ptr [rsp+20h],0 mov r9d,52; rectButton.top mov r8d,325;left ; X invoke SetWindowPos,hwndButton,0 locret: leave retn MoveButtonsDown endp InsetEditBox proc local nHeightGrow:qword local rectEditBox:RECT local rectWindow:RECT local hwndEditBox:qword sub rsp,100h lea edx,nHeightGrow lea ecx,rectEditBox invoke CalcEditBoxRect or eax, eax jz locret ;Создаем EditBox ;bmp-файл из ресурсов mov edx,IDC_IMG invoke LoadImage,hInstance,,IMAGE_BITMAP,0,0,0;LR_LOADFROMFILE mov image,eax and qword ptr [rsp+58h],0 ; lpParam mov rax,hInstance mov [rsp+50h],rax ; hInstance and qword ptr [rsp+48h],IDC_IMG mov rax,hwndMessageBox mov [rsp+40h],rax mov qword ptr [rsp+38h],101;rax and qword ptr [rsp+30h],128 and qword ptr [rsp+28h],0 and qword ptr [rsp+20h],0 lea edx,aStatic lea r8d,aNull invoke CreateWindowEx,0,,,\ WS_CHILD or WS_VISIBLE or SS_BITMAP; or SS_CENTERIMAGE;BITMAP or SS_CENTERIMAGE;REALSIZEIMAGE invoke SendMessage,eax,STM_SETIMAGE,IMAGE_BITMAP,image and qword ptr [rsp+58h],0 ; lpParam ; and qword ptr [rsp+50h],0 ; hInstance mov rax,hInstance mov [rsp+50h],rax ; hInstance and qword ptr [rsp+48h],0 ; hMenu mov rax,hwndMessageBox ; hWndParent mov [rsp+40h],rax mov qword ptr [rsp+38h],26 ; nHeight = rectEditBox.bottom - rectEditBox.top mov qword ptr [rsp+30h],190 ; nWidth = rectEditBox.right - rectEditBox.left mov qword ptr [rsp+28h],35;Y mov qword ptr [rsp+20h],130;X lea edx,aEdit invoke CreateWindowEx,WS_EX_CLIENTEDGE,,&EditBoxString,\ ES_LEFT+WS_TABSTOP+WS_CHILD+WS_VISIBLE;+BS_AUTOCHECKBOX+BS_MULTILINE ; mov hwndEditBox,rax ; invoke SetFocus,eax ; lea edx,aEdit ; invoke CreateWindowEx,WS_EX_CLIENTEDGE,,&EditBoxString,\ ; ES_LEFT+WS_TABSTOP+WS_CHILD+WS_VISIBLE;+BS_AUTOCHECKBOX+BS_MULTILINE ; mov hwndEditBox,rax ;HWND hStatic1 = CreateWindowEx( NULL, "STATIC", "", WS_CHILD | WS_VISIBLE | SS_ICON | SS_WHITERECT, 250,70, 50, 50, hwnd, NULL, GetModuleHandle(NULL), NULL); ; invoke GetLastError ;hwndLabel = CreateWindow( ; TEXT("STATIC"), /*The name of the static control's class*/ ; TEXT("Label 1"), /*Label's Text*/ ; WS_CHILD | WS_VISIBLE | SS_LEFT, /*Styles (continued)*/ ; 0, /*X co-ordinates*/ ; 0, /*Y co-ordinates*/ ; 50, /*Width*/ ; 25, /*Height*/ ; hwnd, /*Parent HWND*/ ; (HMENU) ID_MYSTATIC, /*The Label's ID*/ ; hInstance, /*The HINSTANCE of your program*/ ; NULL); /*Parameters for main window*/ ;Увеличиваем окно и сдвигаем все кнопки вниз @@: lea edx,rectWindow ;2F1 1E6 4C0 287h invoke GetWindowRect,hwndMessageBox or eax, eax jz @f mov qword ptr [rsp+30h],SWP_NOMOVE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_NOREDRAW; uFlags mov qword ptr [rsp+28h],175 ; cy = rectWindow.bottom - rectWindow.top + nHeightGrow mov qword ptr [rsp+20h],450 ; cx = rectWindow.right - rectWindow.left invoke SetWindowPos,hwndMessageBox,0,0,0 invoke MoveButtonsDown,nHeightGrow;=1A @@: lea eax,rectEditBox mov m_hwndEditBox,rax locret: leave retn InsetEditBox endp end файл tut_10m-2.rc (ресурсы для DLL) Код (ASM): #define IDC_IMG 1001 IDC_IMG BITMAP MOVEABLE PURE LOADONCALL DISCARDABLE "03.bmp" файл tut_10m-2.def для создания tut_10m-2.lib с помощью которого будет создан tut_10m-2.dll Код (ASM): LIBRARY tut_10m-2 EXPORTS MouseProc InstallHook UninstallHook Скачайте файл примера здесь. Первоначальный вид MessageBox'а MessageBox после установки хука MessageBox после нажатия на кнопку «Say Hello»
Глава тридцать первая. Братец Кролик разбирается с памятью и файлами В этой главе Братец Кролик изучал основы управления памятью и файловые операции ввода/вывода. Для этого он использовал обычные диалоговые окна как устройства ввода/вывода. Скачайте пример здесь.ТЕОРИЯ ― МАТЬ СКЛЕРОЗАУправление памятью под Win64 с точки зрения приложения достаточно просто и прямолинейно. Используемая модель памяти называется плоской моделью памяти. В этой модели все сегментные регистры (или селекторы) указывают на один и тот же стартовый адрес и используется 32-битное смещение, так что приложение может обратиться к любой точке памяти своего адресного пространства без необходимости изменять значения селекторов. Это очень упрощает управление памятью. Под Win16 существует две основные категории функций API памяти: глобальные и локальные. Функции глобального типа взаимодействуют с памятью в других сегментах, поэтому они функции "дальней" памяти. Функции локального типа взаимодействуют с локальной кучей процессов, поэтому они функции "ближней" памяти. Под Win64 оба этих типа идентичны. Используете ли вы GlobalAlloc или LocalAlloc, вы получите одинаковый результат. Выделите блок памяти с помощью вызова GlobalAlloc. Эта функция возвращает дескриптор на запрошенный блок памяти. "Закройте" блок памяти, вызвав GlobalLock. Эта функция принимает дескриптор на блок памяти и возвращает указатель на блок памяти. Вы можете использовать указатель, чтобы читать или писать в память. "Откройте" блок памяти с помощью вызова GlobalUnlock. Эта функция возвращает указатель на блок памяти. Освободите блок памяти с помощью GlobalFree. Эта функции принимает дескриптор на блок памяти. Стандартный менеджер памяти ― это не единственный диспетчер кучи в вашей программе. Вам также доступны: HeapAlloc/HeapFree LocalAlloc/LocalFree GlobalAlloc/GlobalFree IMalloc.Alloc/IMalloc.Free CoTaskMemAlloc/CoTaskMemFree SHAlloc/SHFree Чем они отличаются? Это разные реализации одной идеи. В системе есть несколько менеджеров памяти, которые имеют разные цели и используются в разных случаях. Вышеуказанные функции ― это точки доступа к различным менеджерам памяти. Когда их надо использовать? Когда вам нужно «поговорить» с кодом, который понимает только их. К примеру, буфер обмена Windows работает с GlobalAlloc/GlobalFree. Самое главное, нужно помнить правило: «кто девушку ужинает ― тот ее и танцует». Иными словами, если память выделили через GetMem ― то освобождать ее должны через FreeMem, выделили через VirtualAlloc ― освобождать нужно через VirtualFree, выделили через LocalAlloc ― тогда LocalFree... Вы также можете заменить "Global" на "Local", то есть LocalAlloc, LocalLock и так далее. Вышеуказанный метод может быть упрощен использованием флага GMEM_FIXED при вызове GlobalAlloc. Если вы используете этот флаг, возвращаемое значение от Global/LocalAlloc будет указателем на зарезервированный блок памяти, а не дескриптор этого блока. Вам не надо будет вызывать Global/LocalLock вы сможете передать указатель Global/LocalFree без предварительного вызова Global/LocalUnlock. Hо в этой главе я использую "традиционный" подход, так как вы можете столкнуться с ним при изучении исходников других программ. Файловый ввод/вывод по Win64 имеет значительное сходство с тем, как это делалось под DOS. Все требуемые шаги точно такие же. Вам только нужно заменить прерывания на вызовы API функций. Откройте или создайте файл функцией CreateFile. Эта функция очень универсальна: не считая файла, она может открывать LPT/COM-порты, пайпы, дисковые приводы и консоли. В случае успеха она возвращает дескриптор файла или устройства. Затем вы можете использовать этот дескриптор, чтобы выполнить определенные действия над файлом или устройством. Переместите файловый указатель в желаемое местоположение функцией SetFilePointer. Проведите операцию чтения или записи с помощью вызова ReadFile или WriteFile. Перед этим вы должны зарезервировать достаточно большой блок памяти для данных. Закройте файл с помощью CloseHandle. Эта функции принимает дескриптор файла. GlobalAllocРаспределяет указанное число байтов из кучи. Примечание Глобальные функции имеют большую нагрузку и обеспечивают меньше возможностей , чем другие функции управления памятью. Новые приложения должны использовать функции кучи, если документация не утверждает, что глобальная функция должна быть использована. Код (C): HGLOBAL WINAPI GlobalAlloc( _In_ UINT uFlags, /* Выделение памяти атрибутов. Если задано нулевое значение по умолчанию является GMEM_FIXED. Этот параметр может быть одним или более из следующих значений, за исключением несовместимых комбинаций */ _In_ SIZE_T dwBytes /* Количество необходимых байт. Если этот параметр равен нулю и параметр определен как GMEM_MOVEABLE, функция возвращает дескриптор объекта памяти, который помечен как перемещаемый */ ); КонстантаhexЗначениеGMEM_FIXED0Выделяет фиксированную память. Возвращаемое значение является указателем.GMEM_MOVEABLE2Выделяет перемещаемую память. Блоки памяти не перемещаются в физической памяти, но могут быть перемещаемы в пределах кучи. Возвращаемое значение представляет собой дескриптор объекта памяти. Для того, чтобы перевести дескриптор в указатель, используйте функцию GlobalLock. Это значение не может быть объединено с GMEM_FIXEDGMEM_ZEROINIT40Заполняет выделенный фрагмент памяти нулями.GPTR40Сочетание GMEM_FIXED и GMEM_ZEROINITGHND42Сочетание GMEM_MOVEABLE и GMEM_ZEROINIT.Следующие значения являются устаревшими, но предназначены для совместимости с 16-битной Windows. Они игнорируются. GMEM_DDESHARE GMEM_DISCARDABLE GMEM_LOWER GMEM_NOCOMPACT GMEM_NODISCARD GMEM_NOT_BANKED GMEM_NOTIFY GMEM_SHARE Возвращаемое значениеЕсли функция завершается успешно, возвращаемое значение является дескриптор вновь выделенного объекта памяти. Если функция завершается ошибкой, возвращаемое значение ― NULL. Выделив блок, его можно в любой момент зафиксировать при помощи GlobalLock и после этого работать с ним. Функция GlobalUnlock снимает фиксацию и разрешает системе переупорядочивать блоки. При использовании флага GMEM_MOVEABLE возвращается не адрес, а дескриптор. Аргументом функции GlobalLock является дескриптор. Функция GlobalLock возвращает адрес. Если флаг GMEM_DISCARDABLE используется вместе с GMEM_MOVEABLE, тогда блок памяти может быть удален из памяти системой, если он не был предварительно зафиксирован. Если блок был удален системой, тогда функция GlobalLock возвратит NULL и придется снова выделять блок и загружать данные. Для удаления блока памяти используют GlobalFree. В случае выделения фиксированного блока аргумент функции адрес блока памяти, в случае перемещаемого блока ― дескриптор. Для освобождения удаляемого блока используют функцию GlobalDiscard.
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИПриведенная ниже программа отображает открытый файловое диалоговое окно. Оно позволяет пользователю использовать текстовый файл, чтобы открыть и показать содержимое файла в клиентской области окна ввода. Пользователь может изменять текст в окне ввода по своему усмотрению, а затем может сохранить содержимое в файл.Управление памятью, файловый ввод/вывод, GlobalAlloc/Lock/Unlock и GlobalFreerc-файл Код (ASM): #define ZZZ_OPEN 1 #define ZZZ_SAVE 2 #define ZZZ_EXIT 3 #define IDR_MAINMENU 30 IDR_MAINMENU MENU { POPUP "&File" { MENUITEM "&Open",ZZZ_OPEN MENUITEM "&Save As",ZZZ_SAVE MENUITEM SEPARATOR MENUITEM "&Exit",ZZZ_EXIT } MENUITEM "&Exit",ZZZ_EXIT } asm-файл Код (ASM): ; GUI # include win64a.inc ZZZ_OPEN equ 1 ZZZ_SAVE equ 2 ZZZ_EXIT equ 3 MAXSIZE equ 256 MEM_SIZE equ 65535 EditID equ 1 IDR_MAINMENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov edi,offset ClassName mov esi,IMAGE_BASE mov eax,10029h push rax ;hIconSm push rdi ;lpszClassName push IDR_MAINMENU;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push rsi push rsi push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,WS_OVERLAPPEDWINDOW or WS_VISIBLE invoke SetFocus,hwndEdit lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke TranslateMessage,edi invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM local hMem:QWORD ;handle to the allocated memory block local pMem:QWORD ;pointer to the allocated memory block local szReadWrite:QWORD ;number of bytes actually read or write mov edi,offset dlgOpenOfn mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_CREATE je wmCREATE cmp edx,WM_SIZE je wmSIZE leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,NULL wmSIZE: mov qword ptr [rbp-28h],TRUE movzx rax,word ptr lParam+2 mov [rbp-30h],rax movzx r9,word ptr lParam invoke MoveWindow,hwndEdit,0,0 jmp wmBYE OPEN: mov [rdi+OPENFILENAME.Flags],OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or\ OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName,edi test eax,eax je @0 mov [rbp-20h],rbx mov qword ptr [rbp-28h],FILE_ATTRIBUTE_ARCHIVE mov qword ptr [rbp-30h],OPEN_EXISTING mov ecx,offset dOpenBuffer invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or FILE_SHARE_WRITE,0 mov edi,eax;handle to file invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEM_SIZE mov hMem,rax invoke GlobalLock,eax mov pMem,rax mov [rbp-30h],rbx lea r9d,szReadWrite invoke ReadFile,edi,eax,MEM_SIZE-1 invoke SendMessage,hwndEdit,WM_SETTEXT,0,pMem jmp @1 SAVE: mov [rdi+OPENFILENAME.Flags],OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetSaveFileName,edi;&dOpenOfn test eax,eax je @0 mov [rbp-20h],rbx mov qword ptr [rbp-28h],FILE_ATTRIBUTE_ARCHIVE mov qword ptr [rbp-30h],CREATE_NEW mov ecx,offset dOpenBuffer invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or FILE_SHARE_WRITE,0 mov edi,eax;hFile invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEM_SIZE mov hMem,rax invoke GlobalLock,eax mov pMem,rax invoke SendMessage,hwndEdit,WM_GETTEXT,MEM_SIZE-1,rax ;pMem mov [rbp-30h],rbx lea r9,szReadWrite invoke WriteFile,edi,pMem,rax @1: invoke CloseHandle,edi;hFile invoke GlobalUnlock,pMem invoke GlobalFree,hMem @0: invoke SetFocus,hwndEdit jmp wmBYE wmCREATE:mov [rdi+OPENFILENAME.hwndOwner],rcx push rbx push IMAGE_BASE push EditID push rcx;hWnd push rbx push rbx push rbx push rbx mov edx,offset EditClass sub esp,20h invoke CreateWindowEx,0,,0,WS_VISIBLE or WS_CHILD or \ ES_LEFT or WS_VSCROLL or WS_HSCROLL or \ ES_AUTOHSCROLL or ES_AUTOVSCROLL or ES_MULTILINE mov hwndEdit,rax jmp wmBYE wmCOMMAND:movzx rax,word ptr wParam or r9d,r9d ;cmp lParam,0 jnz wmBYE cmp rax,ZZZ_EXIT ja wmBYE jmp [menu_handlers+rax*8] EXIT: invoke DestroyWindow wmBYE:leave retn menu_handlers dq wmBYE,OPEN,SAVE,EXIT WndProc endp ;data ClassName db 'Win64 Iczelion''s lesson #12: Memory Management, File I/O, GlobalAlloc/Lock/Unlock and GlobalFree',0 dlgOpenTitle db 'Open File',0 align 8 dlgOpenOfn label OPENFILENAME dd sizeof OPENFILENAME,? dq ? dq IMAGE_BASE dq FilterString dq ?,? dq dOpenBuffer dd MAXSIZE dd 19 dup(?) FilterString db 'All Files (*.*)',0,'*.*',0 db 'Text Files (*.txt)',0,'*.txt',0,0 dOpenBuffer db MAXSIZE dup(?) hwndEdit dq ? ;handle for edit control EditClass db "edit",0 end Разбор полётов Код (ASM): push rbx push IMAGE_BASE push EditID push rcx;hWnd push rbx push rbx push rbx push rbx mov edx,offset EditClass sub esp,20h invoke CreateWindowEx,0,,0,WS_VISIBLE or WS_CHILD or \ ES_LEFT or WS_VSCROLL or WS_HSCROLL or \ ES_AUTOHSCROLL or ES_AUTOVSCROLL or ES_MULTILINE mov hwndEdit,rax В секции WM_CREATE мы создаем окно ввода. Параметры, которые определяют координаты, ширину, высоту элемента управления равны нулю, поскольку мы изменим размер элемента управления позже, чтобы покрыть всю клиентскую область родительского окна. Заметьте, что в этом случае мы не вызываем ShowWindow, чтобы заставить появиться элемент управления на экране, так как указан стиль WS_VISIBLE. Код (ASM): mov edi,offset dlgOpenOfn . . . . ; Инициализируем структуру mov [rdi+OPENFILENAME.hwndOwner],rcx; rcx=hWnd . . . . ;data dlgOpenTitle db 'Open File',0 align 8 dlgOpenOfn label OPENFILENAME dd sizeof OPENFILENAME,? dq ? dq IMAGE_BASE dq FilterString dq ?,? dq dOpenBuffer dd MAXSIZE dd 19 dup(?) После создания окна ввода, мы доинициализируем структуру ofn. Основные поля структуры заполнены в секции данных. Так как мы хотим использовать структуру ofn повторно в диалоговом окне, мы заполняем только общие члены, которые используются и GetOpenFileName и GetSaveFileName. Секция WM_CREATE ― это прекрасное место для одноразовой инициализации. Код (ASM): wmSIZE: mov qword ptr [rbp-28h],TRUE movzx rax,word ptr lParam+2 mov [rbp-30h],rax movzx r9,word ptr lParam invoke MoveWindow,hwndEdit,0,0 Мы получаем сообщения WM_SIZE, когда размер клиентской области нашего основного окна изменяется. Мы также получаем его, когда окно создается. Для того, чтобы получать это сообщение, стили класса окна должны включать CS_REDRAW и CS_HREDRAW. Мы используем эту возможность для того, чтобы сделать размер нашего окна ввода равным клиентской области окна. Для начала мы должны узнать текущую ширину и высоту клиентской области родительского окна. Мы получаем эту информацию из lParam. Старшее слово lParam содержит высоту, а младшее слово ― ширину клиентской области. Затем мы используем эту информацию для того, чтобы изменить размер окна ввода с помощью вызова функции MoveWindow, которая может изменять позицию и размер окна на экране. Код (ASM): OPEN: mov [rdi+OPENFILENAME.Flags],OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or\ OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName,edi test eax,eax je @0 mov [rbp-20h],rbx mov qword ptr [rbp-28h],FILE_ATTRIBUTE_ARCHIVE mov qword ptr [rbp-30h],OPEN_EXISTING mov ecx,offset dOpenBuffer invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or FILE_SHARE_WRITE,0 mov edi,eax;handle to file invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEM_SIZE mov hMem,rax invoke GlobalLock,eax mov pMem,rax mov [rbp-30h],rbx lea r9d,szReadWrite invoke ReadFile,edi,eax,MEM_SIZE-1 invoke SendMessage,hwndEdit,WM_SETTEXT,0,pMem jmp @1 . . . . @1: invoke CloseHandle,edi;hFile invoke GlobalUnlock,pMem invoke GlobalFree,hMem @0: invoke SetFocus,hwndEdit Когда пользователь выбирает пункт меню File/Open, мы заполняем в структуре параметр Flags и вызываем функцию GetOpenFileName, чтобы отобразить окно открытия файла.
После того, как пользователь выберет файл для открытия, мы вызываем CreateFile, чтобы открыть файл. Мы указываем, что функция должна попробовать открыть файл для чтения и записи. После того, как файл открыт, функция возвращает дескриптор на открытый файл, который мы сохраняем в глобальной переменной для будущего использования. Эта функция имеет следующий синтаксис: Код (C): HANDLE WINAPI CreateFile( _In_ LPCTSTR lpFileName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _In_ DWORD dwCreationDisposition, _In_ DWORD dwFlagsAndAttributes, _In_opt_ HANDLE hTemplateFile ); dwDesireAccess указывает, какую операцию вы хотите выполнить над файлом. Открыть файл для проверки его атрибутов. Вы можете писать и читать из файла. GENERIC_READ Открыть файл для чтения. GENERIC_WRITE Открыть файл для записи. dwShareMode указывает, какие операции вы хотите позволить выполнять вашим процессам над открытыми файлами. 0 ― не разделять файл с другими процессами. FILE_SHARE_READ ― позволяет другим процессам прочитать информацию из файла, который был открыт FILE_SHARE_WRITE ― позволяет другим процессам записывать информацию в открытый файл. lpSecurityAttributes не имеет значения под Windows 95. dwCreationDistribution указывает действие, которое будет выполнено над файлом при его открытии. CREATE_NEW ― создание нового файла, если файла не существует. CREATE_ALWAYS ― создание нового файла. Функция перезаписывает файл, если он существует. OPEN_EXISTING ― открытие существующего файла. OPEN_ALWAYS ― открытие файла, если он существует, в противном случае, функция создает новый файл. TRUNCATE_EXISTING ― открытие файла и обрезание его до нуля байтов. Вызывающий функцию процесс должен открывать файл, по крайней мере, с доступом GENERIC_WRITE. Если файл не существует, функция не срабатывает. dwFlagsAndAttributes указывает атрибуты файла FILE_ATTRIBUTE_ARCHIVE ― файл является архивным. Приложения используют этот атрибут для бэкапа или удаления. FILE_ATTRIBUTE_COMPRESSED ― файл или каталог сжаты. Для файла это означает, что вся информация в файле обработана архиватором. Для директории это означает, что сжатие подразумевается по умолчанию для создаваемых вновь файлов и поддиректорий. FILE_ATTRIBUTE_NORMAL ― у файла нет других атрибутов. Этот атрибут действителен, только если используется один. FILE_ATTRIBUTE_HIDDEN ― файл спрятан. Он не включается в обычные листинги директорий. FILE_ATTRIBUTE_READONLY ― файл только для чтения. Приложения могут читать из файла, но не могут писать в него или удалить его. FILE_ATTRIBUTE_SYSTEM ― файл часть операционной системы или используется только ей. Код (ASM): invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEM_SIZE mov hMem,rax invoke GlobalLock,eax mov pMem,rax Когда файл открыт, мы резервируем блок память для использования функциями ReadFile и WriteFile. Мы указываем флаг GMEM_MOVEABLE, чтобы позволить Windows перемещать блок памяти, чтобы уплотнять последнюю. Когда GlobalAlloc возвращает положительный результат, rax содержит дескриптор зарезервированного блока памяти. Мы передаем этот дескриптор функции GlobalLock, который возвращает указатель на блок памяти. Код (ASM): mov [rbp-30h],rbx lea r9d,szReadWrite invoke ReadFile,edi,eax,MEM_SIZE-1 invoke SendMessage,hwndEdit,WM_SETTEXT,0,pMem Когда блок памяти готов к использованию, мы вызываем функцию ReadFile для чтения данных из файла. Когда файл только что открыт или создан, указатель на смещение равен нулю. В этом случае, мы начинаем чтение с первого байта. Первый параметр ReadFile ― это дескриптор файла, из которого необходимо произвести чтение, второй ― это указатель на блок памяти, затем ― количество байтов, которое нужно считать из файла, четвертый параметр ― это адрес переменной размера QWORD, который будет заполнен количеством байтов, в реальности считанных из файла. После заполнения блока памяти данными, мы помещаем данные в окно ввода, посылая сообщение WM_SETTEXT элементу управления, причем lParam содержит указатель на блок памяти. После этого вызова окно ввода отображает данные в его клиентской области. Код (ASM): invoke CloseHandle,edi;hFile invoke GlobalUnlock,pMem invoke GlobalFree,hMem @0: invoke SetFocus,hwndEdit В этом месте у нас нет необходимости держать файл открытым, так как нашей целью является запись модифицированных данных из окна ввода в другой файл, а не в оригинальный. Поэтому мы закрываем файл функцией CloseHandle, передав ей в качестве параметра дескриптор файла. Затем мы открываем блок памяти и освобождаем его. В действительности, вам не нужно освобождать ее сейчас, вы можете использовать этот же блок во время операции сохранения. Hо в целях обучения ее освобождают сразу. Код (ASM): invoke SetFocus, hwndEdit Когда на экране отображается окно открытия файла, фокус ввода сдвигается на него. Поэтому, когда это окно закрывается, мы должны передвинуть фокус ввода обратно на окно ввода. На этом операцию чтения из файла заканчивается. В этом месте пользователь должен отредактировать содержимое окна ввода. И когда он хочет сохранить данные в другой файла, он должен выбрать File/Save, после чего отобразиться диалоговое окно. Создание окна сохранения файла не слишком отличается от создание окна открытия файла. Фактически, они отличаются только именем функций. Вы можете снова использовать большинство из параметров структуры OPENFILENAME, кроме параметра Flags. Код (ASM): SAVE: mov [rdi+OPENFILENAME.Flags],OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetSaveFileName,edi;&dOpenOfn test eax,eax je @0 mov [rbp-20h],rbx mov qword ptr [rbp-28h],FILE_ATTRIBUTE_ARCHIVE mov qword ptr [rbp-30h],CREATE_NEW mov ecx,offset dOpenBuffer invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or FILE_SHARE_WRITE,0 mov edi,eax;hFile invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEM_SIZE mov hMem,rax invoke GlobalLock,eax mov pMem,rax invoke SendMessage,hwndEdit,WM_GETTEXT,MEM_SIZE-1,rax ;pMem mov [rbp-30h],rbx lea r9,szReadWrite invoke WriteFile,edi,pMem,rax @1: invoke CloseHandle,edi;hFile invoke GlobalUnlock,pMem invoke GlobalFree,hMem @0: invoke SetFocus,hwndEdit В нашем случае, мы хотим создать новый файл, так чтобы OFN_FILEMUSTEXIST и OFN_PATHMUSTEXIST должны быть убраны, иначе диалоговое окно не позволит нам создать файл, который уже не существует. Параметр dwCreationDistribution функции CreateFile должен быть установлен в CREATE_NEW, так как мы хотим создать новый файл. Оставшийся код практически одинаков с тем, что используется при создании окна открытия файла, за исключением следующего фрагмента: Код (ASM): invoke SendMessage,hwndEdit, WM_GETTEXT, MEM_SIZE-1,rax ;pMem mov [rbp-30h],rbx lea r9,szReadWrite invoke WriteFile,edi,pMem,rax Мы посылаем сообщение WM_GETTEXT окну ввода, чтобы скопировать данные из него в блок памяти, возвращаемое значение в rax ― это длина данных внутри буфера. После того, как данные оказываются в блоке памяти, мы записываем их в новый файл.
Управление памятью, файловый ввод/вывод, GetProcessHeap, HeapAlloc и HeapFree HeapAllocВыделяет блок памяти из кучи. Выделенная память неперемещаема. Код (C): LPVOID WINAPI HeapAlloc( _In_ HANDLE hHeap, /* Дескриптор кучи, из которой будет выделена память. Этот дескриптор возвращается функцией HeapCreate или GetProcessHeap function. */ _In_ DWORD dwFlags, /* Параметры распределения кучи. Указание любого из этих значений отменит соответствующее значение , указанное при куче была создана с HeapCreate. Этот параметр может быть одним или более из следующих значений */ _In_ SIZE_T dwBytes /* Число байтов , которые будут выделены Если куча заданное параметром hHeap является "не расширяемым" кучи, dwBytes должен быть меньше , чем 0x7FFF8. Вы создаете не расширяемым кучу, вызвав HeapCreate функционировать с ненулевым значением.*/ ); dwFlags ЗначениеНазначениеhexHEAP_NO_SERIALIZE1Сериализованный доступа не будет использоваться для этого распределения. Для того, чтобы гарантировать , что сериализованный доступ отключен для всех вызовов этой функции, указать HEAP_NO_SERIALIZE в вызове HeapCreate. В этом случае нет необходимости дополнительно указывать HEAP_NO_SERIALIZE в этом вызове функции. Это значение не должно быть указано при обращении к кучи процесса по умолчанию.Система может создавать дополнительные потоки внутри процесса приложения, такие как обработчик CTRL+C, что одновременно доступ по умолчанию кучи процесса.HEAP_GROWABLE2Specifies that the heap is growable. Must be specified if HeapBase is NULLHEAP_GENERATE_EXCEPTIONS4Система поднимет исключение , чтобы указать сбой функции, такие как состояние вне-памяти, вместо возврата NULL. Для того, чтобы убедиться , что исключения генерируются для всех вызовов этой функции, указать HEAP_GENERATE_EXCEPTIONS в вызове HeapCreate. В этом случае нет необходимости дополнительно указать HEAP_GENERATE_EXCEPTIONS в этом вызове функции.HEAP_ZERO_MEMORY8Выделенная память будет инициализирована нулями.HEAP_REALLOC_IN_PLACE_ONLY10There can be no movement when reallocating a memory block. If this value is not specified, the function may move the block to a new location. If this value is specified and the block cannot be resized without moving, the function fails, leaving the original memory block unchanged.HEAP_TAG_SHIFT12HEAP_TAIL_CHECKING_ENABLED20HEAP_FREE_CHECKING_ENABLED40HEAP_DISABLE_COALESCE_ON_FREE80HEAP_MAXIMUM_TAG0FFFHEAP_PSEUDO_TAG_FLAG8000HEAP_CREATE_ALIGN_1610000HEAP_CREATE_ENABLE_TRACING20000HEAP_CREATE_ENABLE_EXECUTE40000HEAP_SKIP_VALIDATION_CHECKS10000000HEAP_VALIDATE_PARAMETERS_ENABLED40000000HeapFreeОсвобождает блок памяти , который выделяется из кучи с помощью функции HeapAlloc или HeapReAlloc. Код (C): BOOL WINAPI HeapFree( _In_ HANDLE hHeap, /* Дескриптор кучи блока памяти который должен быть освобожден. Дескриптор возвращается либо функцией HeapCreate, либо функцией GetProcessHeap */ _In_ DWORD dwFlags,/* Куча свободных вариантов. Задание следующих значений отменяет соответствующее значение , указанное в параметре flOptions когда эта куча была создана с помощью функции HeapCreate */ _In_ LPVOID lpMem /* Указатель на блок памяти будет освобожден. Этот указатель возвращается функцией HeapAlloc или функцией HeapReAlloc. Если этот указатель равен NULL, поведение не определено */ ); asm-файл Код (ASM): ; GUI # include win64a.inc ZZZ_OPEN equ 1 ZZZ_SAVE equ 2 ZZZ_EXIT equ 3 MAXSIZE equ 256 MEM_SIZE equ 65535 EditID equ 1 IDR_MAINMENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov edi,offset ClassName mov esi,IMAGE_BASE mov eax,10029h push rax ;hIconSm push rdi ;lpszClassName push IDR_MAINMENU;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra mov eax,offset WndProc push rax ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push rsi push rsi push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,WS_OVERLAPPEDWINDOW or WS_VISIBLE invoke GetMenu,eax mov hMenu,rax lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke TranslateMessage,edi invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM local hFile:QWORD ;handle of file local szReadWrite:QWORD ;number of bytes actually read or write mov edi,offset dlgOpenOfn mov hWnd,rcx mov lParam,r9 mov wParam,r8 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_CREATE je wmCREATE cmp edx,WM_SIZE je wmSIZE leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,NULL wmSIZE:movzx rax,word ptr lParam+2 movzx r9,word ptr lParam invoke MoveWindow,hwndEdit,0,0,,rax,TRUE jmp wmBYE OPEN: mov [edi+OPENFILENAME.Flags],OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or\ OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName,edi test eax,eax je wmBYE movzx edx,word ptr [edi+OPENFILENAME.nFileOffset] add edx,offset dOpenBuffer invoke SetWindowText,hWnd mov ecx,offset dOpenBuffer invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or \ FILE_SHARE_WRITE,0,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,rbx mov hFile,rax;handle to file invoke GetFileSize,eax,0 mov FileSize,rax invoke GetProcessHeap mov hHeap,rax invoke HeapAlloc,eax,HEAP_ZERO_MEMORY,MEM_SIZE mov pMem,rax lea r9d,szReadWrite invoke ReadFile,hFile,eax,MEM_SIZE-1,,rbx invoke SendMessage,hwndEdit,WM_SETTEXT,0,pMem xor r9d,r9d mov r8d,MF_GRAYED jmp @f SAVE: mov [edi+OPENFILENAME.Flags],OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetSaveFileName,edi;&dOpenOfn test eax,eax je wmBYE movzx edx,word ptr [edi+OPENFILENAME.nFileOffset] add edx,offset dOpenBuffer invoke SetWindowText,hWnd mov ecx,offset dOpenBuffer invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or \ FILE_SHARE_WRITE,0,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,rbx mov hFile,rax invoke SendMessage,hwndEdit,WM_GETTEXT,FileSize,pMem lea r9,szReadWrite invoke WriteFile,hFile,pMem,rax,,rbx invoke HeapFree,hHeap,0,pMem mov r9d,MF_GRAYED xor r8d,r8d;MF_ENABLED=0 @@: invoke EnableMenuItem,hMenu,0;IDM_OPEN=0 invoke EnableMenuItem,hMenu,ZZZ_SAVE invoke CloseHandle,hFile jmp wmBYE wmCREATE:mov [dlgOpenOfn.hwndOwner],rcx push rbx push IMAGE_BASE push EditID push rcx;hWnd push rbx push rbx push rbx push rbx mov edx,offset EditClass sub esp,20h invoke CreateWindowEx,0,,0,WS_VISIBLE or WS_CHILD or \ ES_LEFT or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or \ ES_AUTOHSCROLL or ES_AUTOVSCROLL mov hwndEdit,rax jmp wmBYE wmCOMMAND:movzx rax,word ptr wParam or r9,r9 ;cmp lParam,0 jnz wmBYE cmp rax,ZZZ_EXIT ja wmBYE jmp [menu_handlers+eax*8] EXIT: ;mov rcx,hWnd ;ax=MI_EXIT invoke DestroyWindow wmBYE:leave retn menu_handlers dq wmBYE,OPEN,SAVE,EXIT WndProc endp ;--------------------------------------- ClassName db 'Win64 Iczelion''s lesson #12b: Memory Management, File I/O, GetProcessHeap, HeapAlloc and HeapFree',0 dlgOpenTitle db 'Open File',0 dlgOpenOfn label OPENFILENAME dd sizeof OPENFILENAME,? dq ? dq IMAGE_BASE dq FilterString dq ?,? dq dOpenBuffer dd MAXSIZE,? dq 9 dup(?) ;data FilterString db 'All Files (*.*)',0,'*.*',0 db 'Text Files (*.txt)',0,'*.txt',0,0 dOpenBuffer db MAXSIZE dup(?) hwndEdit dq ? ;handle for edit control EditClass db "edit",0 hHeap dq ? pMem dq ? FileSize dq ? hMenu dq ? end rc-файл Код (C): #define ZZZ_OPEN 1 #define ZZZ_SAVE 2 #define ZZZ_EXIT 3 #define IDR_MAINMENU 30 IDR_MAINMENU MENU { POPUP "&File" { MENUITEM "&Open",ZZZ_OPEN MENUITEM "&Save As",ZZZ_SAVE MENUITEM SEPARATOR MENUITEM "&Exit",ZZZ_EXIT } MENUITEM "&Exit",ZZZ_EXIT }
Управление памятью, файловый ввод/вывод, CoTaskMemAlloc и CoTaskMemFreerc-файл Код (C): #define ZZZ_OPEN 1 #define ZZZ_SAVE 2 #define ZZZ_EXIT 3 #define IDR_MAINMENU 30 IDR_MAINMENU MENU { POPUP "&File" { MENUITEM "&Open",ZZZ_OPEN MENUITEM "&Save As",ZZZ_SAVE MENUITEM SEPARATOR MENUITEM "&Exit",ZZZ_EXIT } MENUITEM "&Exit",ZZZ_EXIT } asm-файл Код (ASM): ; GUI # include win64a.inc ZZZ_OPEN equ 1 ZZZ_SAVE equ 2 ZZZ_EXIT equ 3 MAXSIZE equ 256 MEM_SIZE equ 65535 EditID equ 1 IDR_MAINMENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov edi,offset ClassName mov esi,IMAGE_BASE mov eax,10029h push rax ;hIconSm push rdi ;lpszClassName push IDR_MAINMENU;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra mov eax,offset WndProc push rax ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push rsi push rsi push rsi push rsi sub esp,20h invoke CreateWindowEx, WS_EX_CLIENTEDGE,edi,edi,WS_OVERLAPPEDWINDOW or WS_VISIBLE invoke GetMenu,eax mov hMenu,rax lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke TranslateMessage,edi invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM local hFile:QWORD ;handle of file local szReadWrite:QWORD ;number of bytes actually read or write mov edi,offset dlgOpenOfn mov hWnd,rcx mov lParam,r9 mov wParam,r8 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_CREATE je wmCREATE cmp edx,WM_SIZE je wmSIZE leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,NULL wmSIZE:movzx rax,word ptr lParam+2 movzx r9,word ptr lParam invoke MoveWindow,hwndEdit,0,0,,rax,TRUE jmp wmBYE OPEN: mov [edi+OPENFILENAME.Flags],OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or\ OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName,edi test eax,eax je wmBYE movzx edx,word ptr [edi+OPENFILENAME.nFileOffset] add edx,offset dOpenBuffer invoke SetWindowText,hWnd mov ecx,offset dOpenBuffer invoke CreateFile,,\ GENERIC_READ or GENERIC_WRITE,\ FILE_SHARE_READ or FILE_SHARE_WRITE,\ 0,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,0 mov hFile,rax;handle to file invoke GetFileSize,eax,0 mov FileSize,rax invoke CoTaskMemAlloc,rax mov buff,rax invoke ReadFile,hFile,eax,MEM_SIZE-1,&szReadWrite,rbx invoke SendMessage,hwndEdit,WM_SETTEXT,0,buff xor r9d,r9d mov r8d,MF_GRAYED jmp @f SAVE: mov [edi+OPENFILENAME.Flags],OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetSaveFileName,edi;&dOpenOfn test eax,eax je wmBYE movzx edx,word ptr [edi+OPENFILENAME.nFileOffset] add edx,offset dOpenBuffer invoke SetWindowText,hWnd mov ecx,offset dOpenBuffer invoke CreateFile,,\ GENERIC_READ or GENERIC_WRITE,\ FILE_SHARE_READ or FILE_SHARE_WRITE,\ 0,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,0 mov hFile,rax invoke SendMessage,hwndEdit,WM_GETTEXT,FileSize,buff invoke WriteFile,hFile,buff,rax,&szReadWrite,rbx invoke CoTaskMemFree,buff mov r9d,MF_GRAYED xor r8d,r8d;MF_ENABLED=0 @@: invoke EnableMenuItem,hMenu,0;IDM_OPEN=0 invoke EnableMenuItem,hMenu,ZZZ_SAVE invoke CloseHandle,hFile jmp wmBYE wmCREATE:mov [dlgOpenOfn.hwndOwner],rcx push rbx push IMAGE_BASE push EditID push rcx;hWnd push rbx push rbx push rbx push rbx mov edx,offset EditClass sub esp,20h invoke CreateWindowEx,0,,0,WS_VISIBLE or WS_CHILD or \ ES_LEFT or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or \ ES_AUTOHSCROLL or ES_AUTOVSCROLL mov hwndEdit,rax jmp wmBYE wmCOMMAND:movzx rax,word ptr wParam or r9,r9 ;cmp lParam,0 jnz wmBYE cmp rax,ZZZ_EXIT ja wmBYE jmp [menu_handlers+eax*8] EXIT: invoke DestroyWindow wmBYE: leave retn menu_handlers dq wmBYE,OPEN,SAVE,EXIT WndProc endp ;data ClassName db 'Win64 Iczelion''s lesson #12c: Memory Management, File I/O, CoTaskMemAlloc and CoTaskMemFree',0 dlgOpenTitle db 'Open File',0 dlgOpenOfn label OPENFILENAME dd sizeof OPENFILENAME,? dq ? dq IMAGE_BASE dq FilterString dq ?,? dq dOpenBuffer dd MAXSIZE,? dq 9 dup(?) FilterString db 'All Files (*.*)',0,'*.*',0 db 'Text Files (*.txt)',0,'*.txt',0,0 dOpenBuffer db MAXSIZE dup(?) hwndEdit dq ? ;handle for edit control EditClass db "edit",0 buff dq ? FileSize dq ? hMenu dq ? end
Глава тридцать вторая. Братец Кролик и файлы, проецируемые в память Что такое файлы, проецируемые в память и как их использовать? Процессор не может работать с содержимым файла непосредственно на диске. Для обращения к какому-либо участку файла этот участок необходимо сначала прочитать в память. Если же требуется не просто обработать данные из файла, а модифицировать его содержимое, то к операции чтения файла добавляется еще и операция его записи. Поэтому процедура работы с дисковым файлом включает 4 этапа: открытие файла на диске; чтение требуемого участка файла в созданную заранее переменную; модификация этой переменной; запись модифицированной переменной на то же место в файле на диске. Скачайте пример здесь.ТЕОРИЯ ― МАТЬ СКЛЕРОЗАОбычно при работе с файлом требуется обращение не к одному его участку, а к нескольким. Так как программа как правило запрашивает не смежные, а разнесенные по всему файлу участки, поэтому каждое такое обращение к файлу существенно замедляет скорость выполнения программы в целом. Для эффективности файл читается целиком в память. Поэтому для работы с файлом требуется только одна операция чтения в начале и одна операция записи в конце. Это возможно только с файлами небольшого размера и сильно снижает надежность системы, так как модифицированные данные записываются на диск только в конце сеанса и любой сбой приведет к потере модифицированных данных. Windows использует механизм объединивший работу с файлом и чтение/запись в память. Конкретный файл включается в состав физической памяти, в адресном пространстве приложения резервируется участок достаточного размера и часть физической памяти, включающая в себя файл, отображается на этот участок. Дальнейшие операции с файлом заменяются операциями с его отображением в адресном пространстве приложения. Если вы хорошо изучили пример из прошлой главы, вы увидите, что у него есть несколько серьезных недостатков: файл, который вы хотите прочитать окажется больше, чем зарезервированный блок памяти строка, которую вы хотите найти будет обрезана посередине, потому что кончился блок памяти Традиционный ответ на первый вопрос ― вам нужно последовательно читать данные из файла, пока он не закончится. Ответом на второй вопрос ― вы должны обрабатывать подобную возможность. Она называется проблемой пограничного значения, представляет собой головную большую для программистов и вызывает ошибки в программе. Было бы неплохо, если бы мы могли зарезервировать очень большой блок памяти, достаточный для того, чтобы сохранить весь файл, но наша программа стала бы очень прожорливой в плане ресурсов. Проекция файлов в память ― это спасение. Используя его, вы можете считать весь файл уже загруженным в память и использовать указатель на память, чтобы читать или писать данные в файл. Отпадает необходимость одновременного использования функций памяти и файловых функций. Проекция файлов в память также используется для обмена данными между процессами. При использовании проекции файлов в память таким образом, реально не используется никакой файл. Это больше похоже на блок памяти, который могут видеть все процессы. Hо обмен данными между процессами ― весьма деликатный предмет. Вы должны будете обеспечить синхронизацию между процессами и ветвями, иначе ваше приложение очень скоро повиснет. Мы не будем касаться того, как использовать проекцию файлов в память для создания общего региона памяти в этой главе. Мы сконцентрируемся на том, как использовать проекцию файлов в память для "загрузки" файла в память. Фактически, PE-загрузчик использует проекцию файлов в память для загрузки исполняемых файлов в память. Это очень удобно, так как только необходимые порции файла будут считываться с диска. Под Win64 вам следует использовать проекция файлов в память так часто, как это возможно. Правда, существует несколько ограничений при использовании проекции файлов в память. Как только вы создали такой файл, его размер не может изменяться до закрытия сессии. Поэтому проекция файлов в память прекрасно подходит для файлов из которых нужно только читать или файловых операций, которые не изменяют размер файла. Это не значит, что вы не можете использовать проекцию файлов в память, если хотите увеличить размер файла. Вы можете установить новый размер и создать файл спроецированный в память нового размера и файл увеличится до этого размер. Это просто неудобно, вот и все. Достаточно объяснений. Давайте перейдем к реализации проекции файлов в память. Для того, чтобы использовать этот механизм, должны быть выполнены следующие шаги: Вызов CreateFile для создания или открытия файла. Вызов CreateFileMaping, которой передается дескриптор файла, возвращенный CreateFile. Эта функция создает объект "проекция файла" из файла, созданного или открытого функцией CreateFile. Вызов MapViewOfFile, чтобы загрузить выбранный файловый регион или весь файл в память. Эта функция возвращает указатель на первый байт спроецированного в память файлового региона. Используется указатель, чтобы писать или читать по адресам памяти, на которую отображена проекция, так же как это бы происходило с файлом расположенным на диске. Вызывается UnmapViewOfFile, чтобы выгрузить спроецированный в памяти файл в файл на диске. Вызывается CloseHandle с передачей ему дескриптора спроецированного в память файла в качестве одного из параметра, чтобы закрыть его. Вызывается CloseHandle с передачей ему дескриптор файла, возвращенного CreateFile, чтобы закрыть файл на диске. ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИПрограмма, листинг которой приведен ниже, позволит вам открыть файл с помощью диалога открытия файла. Программа откроет файл, используя проекцию файла в память и, если это удастся, заголовок окна изменится на имя открытого файла. Можно сохранить файл под другим именем, выбрав пункт меню File/Save as. Программа скопирует все содержимое открытого файла в новый файл. При этом вы не вызываете функцию GlobalAlloc для резервирования блока памяти.rc-файл Код (C): #define ZZZ_OPEN 1 #define ZZZ_SAVE 2 #define ZZZ_EXIT 3 #define IDR_MAINMENU 30 IDR_MAINMENU MENU { POPUP "&File" { MENUITEM "&Open",ZZZ_OPEN MENUITEM "&Save As",ZZZ_SAVE MENUITEM SEPARATOR MENUITEM "&Exit",ZZZ_EXIT } MENUITEM "&Exit",ZZZ_EXIT }
asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h ZZZ_OPEN equ 1 ZZZ_SAVE equ 2 ZZZ_EXIT equ 3 MAXSIZE equ 256 MEM_SIZE equ 65535 EditID equ 1 IDR_MAINMENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov edi,offset ClassName mov esi,IMAGE_BASE mov eax,10029h push rax ;hIconSm push rdi ;lpszClassName push IDR_MAINMENU;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push rsi push rsi push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,WS_OVERLAPPEDWINDOW or WS_VISIBLE invoke GetMenu,eax mov hMenu,rax invoke SetFocus,hwndEdit lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke TranslateMessage,edi invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM local hFile:QWORD ;handle of file local szReadWrite:QWORD ;number of bytes actually read or write mov edi,offset dlgOpenOfn mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_CREATE je wmCREATE cmp edx,WM_SIZE je wmSIZE leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,0 wmSIZE: push TRUE movzx rax,word ptr lParam+2 mov [rsp+20h],rax movzx r9,word ptr lParam invoke MoveWindow,hwndEdit,0,0 jmp wmBYE OPEN: mov [rdi+OPENFILENAME.Flags],OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or\ OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName,edi test eax,eax je wmBYE push rbx ;<- align stack 16h push rbx push FILE_ATTRIBUTE_ARCHIVE push OPEN_EXISTING mov ecx,offset dOpenBuffer sub esp,20h invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or \ FILE_SHARE_WRITE,0 mov hFile,rax;handle to file invoke GetFileSize,eax,0 mov FileSize,rax movzx edx,word ptr [rdi+OPENFILENAME.nFileOffset] add edx,offset dOpenBuffer invoke SetWindowText,hWnd ;создание в памяти объекта "проекция файла" mov [rsp+28h],rbx mov [rsp+20h],rbx invoke CreateFileMapping,hFile,0,PAGE_READWRITE,0 mov hMapping,rax ;отображение проекции файла на адресное пространство процесса mov [rsp+20h],rbx invoke MapViewOfFile,eax,FILE_MAP_WRITE,0,0 mov pMapping,rax invoke SendMessage,hwndEdit,WM_SETTEXT,0,rax mov r9,rbx mov r8d,MF_GRAYED jmp @f SAVE: mov [rdi+OPENFILENAME.Flags],OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetSaveFileName,edi;&dOpenOfn test eax,eax je wmBYE push rbx ;<- align stack 16h push rbx push FILE_ATTRIBUTE_ARCHIVE push CREATE_NEW mov ecx,offset dOpenBuffer sub esp,20h invoke CreateFile,,GENERIC_READ or GENERIC_WRITE,\ FILE_SHARE_READ or FILE_SHARE_WRITE,0 mov hFile,rax movzx edx,word ptr [rdi+OPENFILENAME.nFileOffset] add edx,offset dOpenBuffer invoke SetWindowText,hWnd push rbx ;<- align stack 16h push rbx lea r9,szReadWrite sub esp,20h invoke WriteFile,hFile,pMapping,FileSize invoke UnmapViewOfFile,pMapping invoke CloseHandle,pMapping invoke CloseHandle,hMapping mov r9d,MF_GRAYED mov r8,rbx;MF_ENABLED=0 @@: invoke EnableMenuItem,hMenu,0;IDM_OPEN=0 invoke EnableMenuItem,hMenu,ZZZ_SAVE invoke CloseHandle,hFile jmp wmBYE wmCREATE:mov [rdi+OPENFILENAME.hwndOwner],rcx push rbx push IMAGE_BASE push EditID push rcx;hWnd push rbx push rbx push rbx push rbx mov edx,offset EditClass sub esp,20h invoke CreateWindowEx,0,,0,WS_VISIBLE or \ WS_CHILD or ES_LEFT or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or\ ES_AUTOHSCROLL or ES_AUTOVSCROLL mov hwndEdit,rax jmp wmBYE wmCOMMAND:movzx rax,word ptr wParam or r9,r9 ;cmp lParam,0 jnz wmBYE cmp rax,ZZZ_EXIT ja wmBYE jmp [menu_handlers+rax*8] EXIT: invoke DestroyWindow wmBYE: leave retn menu_handlers dq wmBYE,OPEN,SAVE,EXIT WndProc endp ;data ClassName db 'Win64 Iczelion''s lesson #13: Memory Mapped File',0 dlgOpenTitle db 'Open File',0 align 8 dlgOpenOfn OPENFILENAME <{sizeof OPENFILENAME},,IMAGE_BASE,\ FilterString,,,,dOpenBuffer,{MAXSIZE},,,,,,,,,,,,,,> FilterString db 'All Files (*.*)',0,'*.*',0 db 'Text Files (*.txt)',0,'*.txt',0,0 dOpenBuffer db MAXSIZE dup(?) hwndEdit dq ? ;handle for edit control EditClass db "edit",0 hMapping dq ? ;file mapped handle pMapping dq ? FileSize dq ? hMenu dq ? end Разбор полётов Код (ASM): invoke CreateFile,ADDR buffer,GENERIC_READ,0,\ NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL Когда пользователь выбирает файл в окне открытия файла, мы вызываем CreateFile, чтобы открыть его. Заметьте, что мы указываем GENERIC_READ, чтобы открыть этот файл в режиме read-only, потому что мы не хотим, чтобы какие-либо другие процессы изменяли файл во время нашей работы с ним. Код (ASM): invoke CreateFileMapping, hFileRead, NULL, PAGE_READONLY,0,0,NULL Затем мы вызываем CreateFileMaping, чтобы создать файл проецируемый в память из открытого файла. CreateFileMapping имеет следующий синтаксис: Код (C): HANDLE WINAPI CreateFileMapping( _In_ HANDLE hFile, //дескриптор файла, проецируемого на адресное пространство процесса _In_opt_ LPSECURITY_ATTRIBUTES lpAttributes,//указатель на структуру SECURITY_ATTRIBUTES, которая относится к объекту "проекция файла", для установки защиты по умолчанию ему присваивается NULL _In_ DWORD flProtect,//желательные атрибуты защиты _In_ DWORD dwMaximumSizeHigh, _In_ DWORD dwMaximumSizeLow, _In_opt_ LPCTSTR lpName ); Вам следует знать, что CreateFileMaping не обязана проецировать весь файл в память. Вы можете спроецировать только часть файла. Размер проецируемого файла вы задаете параметрами dwMaximumSizeHigh и dwMaximumSizeLow. Если вы зададите размер больше, чем его действительный размер, файл будет увеличен до нового размера. Если вы хотите, чтобы проецируемый файл был такого же размера, как и исходный файл, сделайте оба параметра равными нулю. Вы можете использовать NULL в lpFileMappingAttributes, чтобы Windows создала проецируемый файл со значениями безопасности по умолчанию. flProtect ― определяет желаемую защиту для проецируемого файла. В нашем примере, мы используем PAGE_READONLY, чтобы разрешить только операции чтения над проецируемым файлом. Заметьте, что этот атрибут не должен входить в противоречие с атрибутами, указанными в CreateFile, иначе CreateFileMaping возвратит ошибку. lpName ― указывает на имя проецируемого файла. Если вы хотите разделять этот файл с другими процессами, вы должны присвоить ему имя. Hо в нашем примере другие процессы не будут его использовать, поэтому мы игнорируем этот параметр. Код (ASM): mov eax,OFFSET buffer movzx edx,ofn.nFileOffset add eax,edx invoke SetWindowText,hWnd,eax Если CreateFileMapping выполнилась успешно, мы изменяем название окна на имя открытого файла. Имя файла с полным путем сохраняется в буфере, мы же хотим отобразить только собственно имя файла, поэтому мы должны добавить значение параметра nFileOffset структуры OPENFILENAME к адресу буфера. Код (ASM): invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED В качестве меры предосторожности, мы не хотим чтобы пользователь мог открыть несколько файлов за pаз, поэтому делаем пункт меню Open недоступным для выбора и делаем доступным пункт Save. EnableMenuItem используется для изменения атрибутов пункта меню. После этого, мы ждем, пока пользователь выберет File/Save или закроет программу. Код (ASM): .ELSEIF uMsg==WM_DESTROY .if hMapFile!=0 call CloseMapFile .endif invoke PostQuitMessage,NULL В выше приведенном коде, когда процедура окна получает сообщение WM_DESTROY, она сначала проверяет значение hMapFile ― равно ли то нулю или нет. Если оно не равно нулю, она вызывает функцию CloseMapFile, которая содержит следующий код: Код (ASM): CloseMapFile PROC invoke CloseHandle,hMapFile mov hMapFile,0 invoke CloseHandle,hFileRead ret CloseMapFile endp CloseMapFile закрывает проецируемый файл и сам файл, так что наша программа не оставляет за собой следов при выходе из Windows. Если пользователь выберет сохранение информации в другой файл, программа покажет ему окно сохранения файла. После он сможет напечатать имя нового файла, который и будет создать функцией CreateFile. Код (ASM): invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0 mov pMemory,eax Когда объект "проекция файла" создан, нужно, чтобы система, зарезервировав регион адресного пространства под данные файла, передала их как физическую память, отображаемую на регион. Это делает функция MapViewOfFile. Эта функция имеет следующий синтаксис:
Код (C): LPVOID WINAPI MapViewOfFile( _In_ HANDLE hFileMappingObject, _In_ DWORD dwDesiredAccess, _In_ DWORD dwFileOffsetHigh, _In_ DWORD dwFileOffsetLow, _In_ SIZE_T dwNumberOfBytesToMap ); dwDesiredAccess определяет, какую операцию мы хотим совершить над файлом. В нашем примере мы хотим только прочитать данные, поэтому мы используем FILE_MAP_READ. dwFileOffsetHigh и dwFileOffsetLow задают стартовый файловое смещение файловой порции, которую вы хотите загрузить в память. В нашем случае нам нужно мы хотим читать весь файл, поэтому начинаем мэппинг со смещение ноль. dwNumberOfBytesToMap задает количество байтов, которое нужно промэппировать в память. Чтобы сделать это со всем файлом, передайте ноль MapViewOfFile. После вызова MapViewOfFile, желаемое количество загружается в память. Вы получите указатель на блок памяти, который содержит данные из файла. Код (ASM): invoke GetFileSize,hFileRead,NULL Теперь узнаем, какого размера наш файл. Размер файла возвращается в rax. Если файл больше, чем 4 GB, то верхнее двойное слово размера файла сохраняется в FileSizeHighWord. Так как мы не ожидаем встретить таких больших файлов, мы можем проигнорировать это. Код (ASM): invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL Запишем данные в выходной файл. Код (ASM): invoke UnmapViewOfFile,pMemory Когда мы заканчиваем со входным файлом, вызываем UnmapViewOfFile. Код (ASM): call CloseMapFile invoke CloseHandle,hFileWrite И закрываем все файлы. Код (ASM): invoke SetWindowText,hWnd,ADDR AppName Восстанавливаем оригинальное название окна. Код (ASM): invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED разрешаем доступ к пункту меню Open и запрещаем к Save As.
Глава тридцать третья. Братец Кролик изучает процессы Здесь мы изучим, что такое процесс, как его создать и как прервать. Скачайте пример здесь.ТЕОРИЯ ― МАТЬ СКЛЕРОЗАЧто такое процесс? "Процесс ― это выполняющееся приложение, которое состоит из личного виртуального адресного пространства, кода, данных и других ресурсов операционной системы, таких как файлы, пайпы и синхронизационные объекты, видимые для процесса." Как вы можете видеть из вышеприведенного определения, у процесса есть несколько объектов: адресное пространство, выполняемый модуль (модули) и все, что эти модули создают или открывают. Как минимум, процесс должен состоять из выполняющегося модуля, личного адресного пространства и ветви. У каждого процесса по крайней мере одна ветвь. Что такое ветвь? Фактически, ветвь ― это выполняющаяся очередь. Когда Windows впервые создает процесс, она делает только одну ветвь на процесс. Эта ветвь обычно начинает выполнение с первой инструкции в модуле. Если в дальнейшем понадобится больше ветвей, он может сам создать их. Когда Windows получает команду для создания процесса, она создает личное адресное пространство для процесса, а затем она загружает исполняемый файл в пространство. После этого она создает основную ветвь для процесса. Под Win64 вы также можете создать процессы из своих программ с помощью функции CreateProcess. Она имеет следующих синтаксис: Код (C): BOOL WINAPI CreateProcess( _In_opt_ LPCTSTR lpApplicationName, _Inout_opt_ LPTSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCTSTR lpCurrentDirectory, _In_ LPSTARTUPINFO lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation ); Не пугайтесь количества параметров. Большую их часть мы можем игнорировать. lpAplicationName ― имя исполняемого файла с или без пути, который вы хотите запустить. Если параметр равен нулю, вы должны предоставить имя исполняемого файла в параметре lpCommandLine. lpCommandLine ― Аргументы командной строки к программе, которую вам требуется запустить. Заметьте, что если lpAplicationName равен нулю, этот параметр должен содержать также имя исполняемого файла. Например так: "notepad.exe readme.txt". lpProcessAttributes и lpThreadAttributes ― укажите атрибуты безопасности для процесса и основной ветви. Если они равны NULL, то используются атрибуты безопасности по умолчанию. bInheritHandles ― флаг, который указывает, хотите ли вы, чтобы новый процесс наследовал все открытые дескрипторы из вашего процесса. dwCreationFlags ― несколько флагов, которые определяют поведение процесса, который вы хотите создать, например, хотите ли вы, чтобы процесс был создан, но тут же приостановлен, чтобы вы могли проверить его или изменить, прежде, чем он запустится. Вы также можете указать класс приоритета ветви(ей) в новом процессе. Этот класс приоритета используется, чтобы определить планируемый приоритет ветвей внутри процесса. Обычно мы используем флаг NORMAL_PRIORITY_CLASS. lpEnviroment ― указатель на блок памяти, который содержит несколько переменных окружения для нового процесса. Если этот параметр равен NULL, новый процесс наследует их от родительского процесса. lpCurrentDirectory ― указатель на строку, которая указывает текущий диск и директорию для дочернего процесса. NULL ― если вы хотите, чтобы дочерний процесс унаследовал их от родительского процесса. lpStartupInfo ― указывает на структуру STARTUPINFO, которая определяет, как должно появиться основное окно нового процесса. Эта структура содержит много членов, которые определяют появление главного окна дочернего процесса. Если вы не хотите ничего особенного, вы можете заполнить данную структуру значениями родительского процесса, вызвав функцию GetStartupInfo. lpProcessInformation ― указывает на структуру PROCESS_INFORMATION, которая получает идентификационную информацию о новом процессе. Структура PROCESS_INFORMATION имеет следующие параметры: Код (ASM): PROCESS_INFORMATION STRUCT hProcess HANDLE ? ;дескриптор дочернего процесса hThread HANDLE ?;дескриптор основной ветви дочернего процесса dwProcessId DWORD ? ;ID дочернего процесса dwThreadId DWORD ? ;ID основной ветви PROCESS_INFORMATION ENDS Дескриптор процесса и ID процесса ― это две разные вещи. ID процесса ― это уникальный идентификатор процесса в системе. Дескриптор процесса ― это значение, возвращаемое Windows для использования другими API-функциями, связанными с процессами. Дескриптор процесса не может использоваться для идентификации процесса, так как он не уникален. После вызова функции CreateProcess, создается новый процесс и функция сразу же возвращается. Вы можете проверить, является ли еще процесс активным, вызвав функцию GetExitCodeProcess, которая имеет следующий синтаксис: Код (C): BOOL WINAPI GetExitCodeProcess( _In_ HANDLE hProcess, _Out_ LPDWORD lpExitCode ); Если вызов этой функции успешен, lpExitcode будет содержать код выхода запрашиваемого процесса. Если значение в lpExitCode равно STILL_ACTIVE, тогда это означает, что процесс по-прежнему запущен. Вы можете принудительно прервать процесс, вызвав функцию TerminateProcess. У нее следующий синтаксис: Код (C): BOOL WINAPI TerminateProcess( _In_ HANDLE hProcess, _In_ UINT uExitCode ); Вы можете указать желаемый код выхода для процесса, любое значение, какое захотите. TerminateProcess ― не лучший путь прервать процесс, так как любые используемые им dll не будут уведомлены о том, что процесс был прерван.Практика ― сестра шизофренииСледующий пример создаст новый процесс, когда пользователь выберет пункт меню "create process". Он попытается запустить "msgbox.exe". Если пользователь захочет прервать новый процесс, он может выбрать пункт меню "terminate process". Программа будет сначала проверять, уничтожен ли уже новый процесс, если нет, программ вызовет TerminateProcess для этого.rc-файл Код (C): #define MI_PROCESS_CREATE 1 #define MI_PROCESS_TERMINATE 2 #define MI_EXIT 3 #define IDR_MAINMENU 30 IDR_MAINMENU MENU { POPUP "&Process" { MENUITEM "&Create Process",MI_PROCESS_CREATE MENUITEM "&Terminate Process",MI_PROCESS_TERMINATE,GRAYED MENUITEM SEPARATOR MENUITEM "E&xit",MI_EXIT } }
asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h MI_PROCESS_CREATE equ 1 MI_PROCESS_TERMINATE equ 2 MI_EXIT equ 3 IDR_MAINMENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov edi,offset ClassName mov esi,IMAGE_BASE mov eax,10029h push rax ;hIconSm push rdi ;lpszClassName push IDR_MAINMENU;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 200 push 400 push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,\ WS_OVERLAPPEDWINDOW or WS_VISIBLE invoke GetMenu,eax mov hMenu,rax lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM local progStartInfo:STARTUPINFO mov edi,offset processInfo mov esi,offset proExitCode mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_INITMENUPOPUP je wmINITMENUPOPUP leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,0 wmINITMENUPOPUP:invoke GetExitCodeProcess,[rdi+PROCESS_INFORMATION.hProcess],esi or eax,eax jz @f;GetExitCodeProcess_TRUE cmp dword ptr [rsi],STILL_ACTIVE;cmp [proExitCode],STILL_ACTIVE jne @f; GetExitCodeProcess_STILL_ACTIVE xor r9,r9;MF_ENABLED mov r8d,MF_GRAYED jmp @0 @@: mov r9d,MF_GRAYED xor r8,r8;MF_ENABLED @0: invoke EnableMenuItem,hMenu,0;MI_PROCESS_CREATE invoke EnableMenuItem,hMenu, MI_PROCESS_TERMINATE, MF_ENABLED,MF_GRAYED jmp wmBYE wmCOMMAND:movzx rax,word ptr wParam or r9,r9 ;cmp lParam,0 jnz wmBYE cmp rax,MI_EXIT ja wmBYE jmp [menu_handlers+eax*8] PROCESS_CREATE:cmp [rdi+PROCESS_INFORMATION.hProcess],rbx je pi_hProcess_IS_0 invoke CloseHandle,[edi+PROCESS_INFORMATION.hProcess] mov [rdi+PROCESS_INFORMATION.hProcess],rbx pi_hProcess_IS_0: lea esi,progStartInfo invoke GetStartupInfo,esi push rdi push rsi push rbx push rbx push NORMAL_PRIORITY_CLASS push rbx mov ecx,offset progName sub esp,20h invoke CreateProcess,,0,0,0 mov rcx,[rdi+PROCESS_INFORMATION.hThread] call CloseHandle jmp wmBYE TERMINATE:invoke GetExitCodeProcess,[rdi+PROCESS_INFORMATION.hProcess], esi;proExitCode cmp dword ptr [rsi],STILL_ACTIVE jne proExitCode_NOT_STILL_ACTIVE;a4; invoke TerminateProcess,[rdi+PROCESS_INFORMATION.hProcess],0 proExitCode_NOT_STILL_ACTIVE: invoke CloseHandle,[rdi+PROCESS_INFORMATION.hProcess] mov [rdi+PROCESS_INFORMATION.hProcess],rbx;0 jmp wmBYE EXIT: ;mov rcx,hWnd ;ax=MI_EXIT invoke DestroyWindow wmBYE: leave retn menu_handlers dq wmBYE,PROCESS_CREATE, TERMINATE, EXIT WndProc endp ;data ClassName db 'Win64 Iczelion''s lesson #14: Process',0 hMenu dq ? proExitCode dq ?;process exit code progName db 'msgbox.exe',0 processInfo PROCESS_INFORMATION <> end Разбор полетов:Программа создает основное окно и получает дескриптор меню для последующего использования. Затем она ждет, пока пользователь выберет команду в меню. Когда пользователь выберет "process", мы обрабатываем сообщение WM_INITMENUPOPUP, чтобы изменить пункты меню. Код (ASM): .ELSEIF uMsg==WM_INITMENUPOPUP invoke GetExitCodeprocess,processInfo.hProcess,ADDR ExitCode .if eax==TRUE .if ExitCode==STILL_ACTIVE invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED .else invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED .endif .else invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED .endif Почему мы хотим обработать это сообщение? Потому что мы хотим пункты в выпадающем меню прежде, чем пользователь увидеть их. В нашем примере, если новый процесс еще не стартовал, мы хотим разрешить "start process" и запретить доступ к пункту "terminate process". Мы делаем обратное, если программа уже запущена. Вначале мы проверяем, активен ли еще новый процесс, вызывая функцию GetExitCodeProcess и передавая ей дескриптор процесса, полученный при вызове CreateProcess. Если GetExitCodeprocess возвращает FALSE, это значит, что процесс еще не был запущен, поэтому запрещаем пункт "terminate process". Если GetExitCodeProcess возвращает TRUE, мы знаем, что новый процесс уже стартовал, мы должны проверить, выполняется ли он еще. Поэтому мы сравниваем значение в ExitCode со значением STILL_ACTIVE, если они равны, процесс еще выполняется: мы должны запретить пункт меню "start process", так как мы не хотим, чтобы запустилось несколько совпадающих процессов. Код (ASM): .if ax==IDM_CREATE_PROCESS .if processInfo.hProcess != 0 invoke CloseHandle,processInfo.hProcess mov processInfo.hProcess,0 .endif invoke GetStartupInfo,ADDR startInfo invoke Createprocess,ADDR programname,NULL,NULL,NULL,FALSE,\ NORMAL_PRIORITY_CLASS,\ NULL,NULL,ADDR startInfo,ADDR processInfo invoke CloseHandle,processInfo.hThread Когда пользователь выбирает пункт "start process", мы вначале проверяем, закрыт ли уже параметр hProcess структуры PROCESS_INFORMATION. Если это в первый раз, значение hProcess будет всегда равно нулю, так как мы определяем структуру PROCESS_INFORMATION в секции .data. Если значение параметра hProcess не равно нулю, это означает, что дочерний процесс вышел, но мы не закрыли его дескриптор. Поэтому пришло время сделать это. Мы вызываем функцию GetSturtupInfo, чтобы заполнить структуру sturtupinfo, которую передаем функцию CreateProcess. После этого мы вызываем функцию CreateProcess. Заметьте, что я не проверил возвращаемое ей значение, потому что это усложнило бы пример. Вам следует проверять это значение. Сразу же после CreateProcess, мы закрываем дескриптор основной ветви, возвращаемой в структуре processInfo. Закрытие дескриптора не означает, что мы прерываем ветвь, только то, что мы не хотим использовать дескриптор для обращения к ветви из нашей программы. Если мы не закроем его, это вызовет потерю ресурсов. Код (ASM): .elseif ax == IDM_TERMINATE invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode .if ExitCode==STILL_ACTIVE invoke TerminateProcess,processInfo.hProcess,0 .endif invoke CloseHandle,processInfo.hProcess mov processInfo.hProcess,0 Когда пользователь выберет пункт меню "terminate process", мы проверяем, активен ли еще новый процесс, вызвав функцию GetExitCodeProcess. Если он еще активен, мы вызываем функцию TerminateProcess, чтобы убить его. Также мы закрываем дескриптор дочернего процесса, так как он больше нам не нужен.
Глава тридцать четвертая. Братец Кролик и треды (ветви) Мы узнаем, как создать мультитредную программу. Мы также изучим методы, с помощью которых треды могут общаться друг с другом. Скачайте пример здесь.Теория ― мать склерозаВ предыдущей главе, вы изучили процесс, состоящий по крайней мере из одного треда: основного. Тред ― это цепь инструкций. Вы также можете создавать дополнительные треды в вашей программе. Вы можете считать мультитрединг как многозадачность внутри одной программы. Если говорить в терминах непосредственной реализации, тред ― это функция, которая выполняется параллельно с основной программой. Вы можете запустить несколько экземпляров одной и той же функции или вы можете запустить несколько функций одновременно, в зависимости от ваших требований. Мультитрединг свойственен Win32 и Win64, под Win16 аналогов не существует. Треды выполняются в том же процесс, поэтому они имеют доступ ко всем ресурсам процесса: глобальным переменным, дескрипторам и так далее. Тем не менее, каждый тред имеет свой собственный стек, так что локальные переменные в каждом треде приватны. Каждый тред также имеет свой собственный набор регистров, поэтому когда Windows переключается на другой тред, предыдущий "запоминает" свое состояние и может "восстановить" его, когда он снова получает контроль. Это обеспечивается внутренними средствами Windows. Мы можем поделить треды на две категории: Тред интерфейса пользователя: тред такого типа создает свое собственное окно, поэтому он получает оконные сообщения. Он может отвечать пользователю с помощью своего окна. Этот тип тредов действуют согласно Win16 Mutex правилу, которое позволяет только один тред пользовательского интерфейсав 16-битном пользовательском и gdi-ядре. Пока один подобный тред выполняет код 16-битного пользовательского и gdi-ядра, другие UI треды не могут использовать сервисы этого ядра. Заметьте, что этот Win16 Mutex свойственен Windows 9x, так как его функции обращаются к 16-битному коду. В Windows NT нет Win16 Mutex'а, поэтому треды пользовательского интерфейса под NT работают более плавно, чем под Windows 95. рабочий тред: Этот тип тредов не создает окно, поэтому он не может принимать какие-либо windows-сообщения. Он существует только для того, чтобы делать предназначенную ему работу на заднем фоне (согласно своему названию). Я советую следующую стратегию при использовании мультитредовых способностей Win32: позвольте основному треду делать все, что связанно с пользовательским интерфейсом, а остальным делать тяжелую работу в фоновом режиме. В этому случае, основной тред ― Правитель, другие треды ― его помощники. Правитель поручает им определенные задания, в то время как сам общается с публикой. Его помощники послушно выполняют работу и докладывают об этом Правителю. Если бы Правитель делал всю работу сам, он бы не смог уделять достаточно внимания народу или прессе. Это похоже на окно, которое занято продолжительной работой в основном треде: оно не отвечает пользователю, пока работа не будет выполнена. Такая программа может быть улучшена созданием дополнительного треда, который возьмет часть работы на себя и позволит основной ветви отвечать на команды пользователя. Мы можем создать тред с помощью вызова функции CreateThread, которая имеет следующий синтаксис: Код (C): HANDLE WINAPI CreateThread( _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_opt_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_opt_ LPDWORD lpThreadId ); Функция CreateThread похожа на CreateProcess. lpThreadAttributes ― Вы можете использовать NULL, если хотите, чтобы у треда были установки безопасности по умолчанию. dwStackSize ― укажите размер стека треда. Если вы хотите, чтобы тред имел такой же размер стека, как и у основного, используйте NULL в качестве параметра. lpStartAddress ― адрес функции треда. Эта функция будет выполнять предназначенную для треда работу. Эта функция должна получать один и только один 32-битный параметр и возвращать 32-битное значение. lpParametr ― Параметр, который вы хотите передать функции треда. dwCreationFlags ― 0 означает, что тред начинает выполняться сразу же после его создания. Для обратного можно использовать флаг CREATE_SUSPEND. lpThreadId ― CreateThread поместит сюда ID созданного треда. Если вызов CreateThread прошел успешно, она возвращает дескриптор созданного треда, в противном случае она возвращает NULL.
Функция треда запускается так скоро, как только заканчивается вызов CreateThread, если только вы не указали флаг CREATE_SUSPENDED. В этом случае тред будет заморожен до вызова функции ResumThread. Когда функция треда возвращается (с помощью инструкции ret) Windows косвенно вызывает ExitThread для функции треда. Вы можете сами вызвать ExitThread, но в этом немного смысла. Вы можете получить код выхода треда с помощью функции GetExitCodeThread. Если вы хотите прервать тред из другого треда, вы можете вызвать функцию TerminateThread. Hо вы должны использовать эту функцию только в экстремальных условиях, так как эта функция немедленно прерывать тред, не давая ему шанса произвести необходимую чистку за собой. Теперь давайте рассмотрим методы коммуникации между тредами. Вот три из них: Использование глобальных переменных Windows-сообщения События Треды разделяют ресурсы процесса, включая глобальные переменные, поэтому треды могут использовать их для того, чтобы взаимодействовать друг с другом. Тем не менее, этот метод должен использоваться осторожно. Синхронизацию нужно внимательно спланировать. Например, если два треда используют одну и ту же структуру из 10 членов, что произойдет, если Windows вдруг передаст управление от одного треда другому, когда структура обновлена еще только наполовину. Другой тред получит неправильную информацию! Не сделайте никакой ошибки, мультитредовые программы тяжелее отлаживать и поддерживать. Этот тип ошибок появляется непредсказуемо и их очень трудно отловить. Вы также можете использовать windows-сообщения, чтобы осуществлять взаимодействие между тредами. Если все треды имеют пользовательский интерфейс, то нет проблем: этот метод может использоваться для двухсторонней коммуникации. Все, что вам нужно сделать ― это определить один или более дополнительных windows-сообщений, которые будут использоваться тредами. Вы определяете сообщение, используя значение WM_USER как базовое, например так: Код (ASM): WM_MYCUSTOMMSG equ WM_USER+100h Windows не использует сообщения с номером выше WM_USER, поэтому мы можем использовать значение WM_USER и выше для наших собственных сообщений. Если один из тредов имеет пользовательский интерфейс, а другой является рабочим, вы не можете использовать данный метод для двухстороннего общения, так как у рабочего треда нет своего окна, а следовательно и очереди сообщений. Вы можете использовать следующие схемы: Тред с пользовательским интерфейсом [math]\to[/math] глобальная переменная [math]\to[/math] рабочий тред рабочий тред [math]\to[/math] windows-сообщение [math]\to[/math] Тред с пользовательским интерфейсом Фактически, мы будем использовать этот метод в нашем примере. Последний метод, используемый для коммуникации ― это объект события. Вы можете рассматривать его как своего рода флаг. Если объект события "не установлен", значит тред спит. Когда объект события "установлен", Windows "пробуждает" тред и он начинает выполнять свою работу.Практика ― сестра шизофренииВам следует скачать zip-файл с примером запустить thread1.exe. Нажмите на пункт меню "Savage Calculation". Это даст команду программе выполнить "add eax,eax" 600.000.000 раз. Заметьте, что во время этого времени вы не сможете ничего сделать с главным окном: вы не сможете его двигать, активировать меню и так далее. Когда вычисление закончится, появится окно с сообщением. После этого окно будет нормально реагировать на ваши команды. Чтобы избежать подобного неудобства для пользователя, мы должны поместить процедуру вычисления в отдельный рабочий тред и позволить основному треду продолжать взаимодействие с пользователем. Вы можете видеть, что хотя основное окно отвечает медленнее, чем обычно, оно все же делает это.asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h MI_THREAD_CREATE equ 0 MI_EXIT equ 1 WMU_THREAD_FINISH equ WM_USER + 100h IDR_MAINMENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov edi,offset ClassName mov esi,IMAGE_BASE mov eax,10029h push rax ;hIconSm push rdi ;lpszClassName push IDR_MAINMENU;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra mov eax,offset WndProc push rax ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 200 push 500 push rsi push rsi invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,\ WS_OVERLAPPEDWINDOW or WS_VISIBLE mov hWindow,rax invoke GetMenu,eax mov hMenu,rax lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM local progStartInfo:STARTUPINFO mov hWnd,rcx cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WMU_THREAD_FINISH je wmu_THREAD_FINISH leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,0 wmu_THREAD_FINISH:mov edx,offset ClassName invoke MessageBox,0,,edx,MB_OK jmp wmBYE wmCOMMAND:cmp r8w,MI_THREAD_CREATE je wmCOMMAND_MI_THREAD_CREATE cmp r8w,MI_EXIT jne wmBYE wmCOMMAND_MI_EXIT:call DestroyWindow;,[hWnd] jmp wmBYE wmCOMMAND_MI_THREAD_CREATE: mov eax,offset tId push rax push NORMAL_PRIORITY_CLASS mov r8d,offset thread_procedure sub esp,20h invoke CreateThread,NULL,0,,NULL invoke CloseHandle,eax wmBYE: leave retn WndProc endp thread_procedure proc sub rsp,5*8 mov rcx,3FFFFFFFh @@: add eax,eax loop @b loopEND:invoke SendMessage,hWindow,WMU_THREAD_FINISH,NULL,NULL add rsp,5*8 retn thread_procedure endp ;data ClassName db 'Win64 Iczelion''s lesson #15: Multithreading Programming',0 hMenu dq ? hWindow dq ? tId dd ? end rc-файл Код (C): #define MI_THREAD_CREATE 0 #define MI_EXIT 1 #define IDR_MAINMENU 30 IDR_MAINMENU MENU { POPUP "&Thread" { MENUITEM "&Create Thread",MI_THREAD_CREATE MENUITEM SEPARATOR MENUITEM "E&xit",MI_EXIT } } Разбор полетовОсновную программу пользователь воспринимает как обычное окно с меню. Если пользователь выбирает в последнем пункт "Создать тред", программа создает тред: Код (ASM): .if ax==IDM_CREATE_THREAD mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ NULL,0,ADDR ThreadID invoke CloseHandle,eax Вышеприведенная функция создает тред, который запустит процедуру под названием ThreadProc параллельно с основным тредом. Если вызов функции прошел успешно, CreateThread немедленно возвращается и ThreadProc начинает выполняться. Так как мы не используем дескриптор треда, нам следует закрыть его, чтобы не допустить бессмысленное расходование памяти. Закрытие дескриптора не прерывает сам тред. Единственным эффектом будет то, что мы не сможем больше использовать его дескриптор. Код (ASM): ThreadProc proc uses ecx param:DWORD mov ecx,600000000 @@: add eax,eax loop @b Get_out: invoke postMessage,hwnd,WM_FINISH,NULL,NULL ret ThreadProc endp Как вы можете видеть ThreadProc выполняет подсчет, требующий некоторого времени, и когда она заканчивает его, она отправляет сообщение WM_FINISH основному окну. WM_FINISH ― это наше собственное сообщение, определенное следующим образом: Код (ASM): WM_FINISH equ WM_USER+100h Вам не обязательно добавлять к WM_USER 100h, но будет лучше сделать это. Сообщение WM_FINISH имеет значение только в пределах нашей программы. Когда основное окно получает WM_FINISH, она реагирует на это показом окна с сообщением о том, что подсчет закончен. Вы можете создать несколько тредов, выбрав "Create Thread" несколько раз. В этом примере применяется односторонняя коммуникация, то есть только тред может уведомлять основное окно о чем-либо. Если вы хотите, что основной тред слал команды рабочему, вы должны сделать следующее: добавить пункт меню "Kill Thread". добавить глобальную переменную, используемую в качестве флага. TRUE=остановить тред, FALSE=продолжить тред. Изменить ThreadProc так, чтобы та проверяла в цикле значение флага. Когда пользователь выберет "Kill Thread", основная программа установит флаг в TRUE. Когда ThreadProc видит, что значение флага равно TRUE, она выходит из цикла и возвращается, что заканчивает действие треда.
Глава тридцать пятая. Братец Кролик и объект события Мы изучим, что такое объект события и как использовать его в мультитредной программе. Скачайте пример здесь.Теория ― мать склерозаВ предыдущей главе демонстрировалось, как треды взаимодействуют друг с другом через собственные windows-сообщения. Пропущено два других метода: глобальная переменная объект события В этой главе будут использованы оба метода. Объект события ― это что-то вроде переключателя: у него есть только два состояния: вкл и выкл. Вы создаете объект события и помещаете его в коде соответствующего треда, где наблюдаете за состояние объекта. Если объект события выключен, ждущие его треды будут "спать". В подобном состоянии треды мало загружают CPU. Вы можете создать объект события, вызвав функцию CreateEvent, которая имеет следующий синтаксис: Код (C): HANDLE WINAPI CreateEvent( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, _In_ BOOL bManualReset, _In_ BOOL bInitialState, _In_opt_ LPCTSTR lpName ); lpEventAttribute ― Если вы укажете значение NULL, у создаваемого объекта будут установки безопасности по умолчанию. bManualReset ― Если вы хотите, чтобы Windows автоматически переключал объект события в "выключено", вы должны присвоить этому параметру значение FALSE. Иначе вам надо будет выключить объект вручную с помощью вызова ResetEvent. bInitialStae ― Если вы хотите, чтобы объект события при создании был установлен в положение "включено", укажите TRUE в качестве данного параметра, в противном случае объект события будет установлен в положение "выключен". lpName ― Указатель на ASCIIZ-строку, которая будет именем объекта события. Это имя будет использоваться, когда вы захотите вызвать OpenEvent. Если вызов прошел успешно, CreateEvent возвратит дескриптор на созданный объект события. В противном случае она возвратит NULL. Вы можете изменять состояние объекта события с помощью двух API-функций: SetEvent ResetEvent Функция SetEvent устанавливает объект события в положение "включено". ResetEvent делает обратное. Когда объект события создан, вы должны поместить вызов функции WaitForSingleObject в тред, который должен следить за состоянием объекта события. Эта функция имеет следующий синтаксис: Код (C): DWORD WINAPI WaitForSingleObject( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds ); hObject ― дескриптор одного из синхронизационных объектов. Объект события ― это вид синхронизационного события. dwTimeout ― Указывает в миллисекундах время, которое эта функция будет ждать, пока объект события не перейдет во включенное состояние. Если указанное время пройдет, а объект события все еще выключен, WaitForSingleObject вернет управление. Если вы хотите, чтобы функция наблюдала за объектом бесконечно, вы должны указать значение INFINITE в качестве этого параметра. Практика ― сестра шизофренииНижеприведенный пример отображает окно, ожидающее пока пользователь не выберет какую-либо команду из меню. Если пользователь нажмет на "run thread", тред начнет подсчет. Когда счет закончится, появится сообщение, информирующее пользователя о том, что работа выполнена. Во время того, как проводится подсчет, пользователь может выбрать команду "stop thread", чтобы остановить тред.asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h IDM_START_THREAD equ 1 IDM_STOP_THREAD equ 2 IDM_EXIT equ 3 WM_FINISH equ WM_USER+100h IDR_MAINMENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov edi,offset ClassName mov esi,IMAGE_BASE mov eax,10029h push rax ;hIconSm push rdi ;lpszClassName push IDR_MAINMENU;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra mov eax,offset WndProc push rax ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 200 push 400 push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,\ WS_OVERLAPPEDWINDOW or WS_VISIBLE mov hwnd,rax invoke GetMenu,eax mov hMenu,rax lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM push rbp mov ebp,esp sub esp,20h mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_CREATE je wmCREATE cmp edx,WM_FINISH je wmFINISH leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,0 wmCREATE:invoke CreateEvent,NULL,FALSE,FALSE,NULL mov hEventStart,rax db 68h dd ThreadID push NORMAL_PRIORITY_CLASS mov r8d,offset ThrdProc sub esp,20h invoke CreateThread, NULL, NULL,,NULL mov hThread,rax invoke CloseHandle,eax jmp wmBYE wmCOMMAND:movzx rax,word ptr wParam cmp r9,rbx ;cmp lParam,0 jnz wmBYE cmp rax,IDM_EXIT ja wmBYE jmp [menu_handlers+eax*8] START_THREAD:invoke SetEvent,hEventStart invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED jmp wmBYE STOP_THREAD:mov EventStop,TRUE invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED jmp wmBYE EXIT: ;mov rcx,hWnd ;ax=MI_EXIT invoke DestroyWindow jmp wmBYE wmFINISH:mov r8d,offset ClassName mov edx,offset SuccessString invoke MessageBox,NULL,,,MB_OK wmBYE: leave retn menu_handlers dq wmBYE, START_THREAD, STOP_THREAD, EXIT WndProc endp ;--------------------------------------- ThrdProc: sub rsp,5*8 invoke WaitForSingleObject,hEventStart,INFINITE or ecx,-1 @0: cmp EventStop,FALSE jz @f mov r8d,offset SuccessString mov edx,offset StopString invoke MessageBox,hwnd,,,MB_OK mov EventStop,FALSE add rsp,5*8 jmp ThrdProc @@: add eax,eax dec rcx jnz @0 invoke PostMessage,hwnd,WM_FINISH,NULL,NULL invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED add rsp,5*8 jmp ThrdProc ; ret ;--------------------------------------- ClassName db 'Win64 Iczelion''s lesson #16: Event Example',0 SuccessString db "The calculation is completed!",0 StopString db "The thread is stopped",0 EventStop BOOL FALSE hwnd HANDLE ? hMenu HANDLE ? ThreadID dq ? ExitCode dq ? hThread dq ? hEventStart HANDLE ? end rc-файл Код (C): #define IDM_START_THREAD 1 #define IDM_STOP_THREAD 2 #define IDM_EXIT 3 #define IDR_MAINMENU 30 IDR_MAINMENU MENU { POPUP "&Thread" { MENUITEM "&Run Thread",IDM_START_THREAD MENUITEM "&Stop Thread",IDM_STOP_THREAD,GRAYED MENUITEM SEPARATOR MENUITEM "E&xit",IDM_EXIT } }
Анализ:В этом примере будет демонстрироваться другая техника работы с тредами. Код (ASM): .IF uMsg==WM_CREATE invoke CreateEvent,NULL,FALSE,FALSE,NULL mov hEventStart,eax mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ NULL,0,ADDR ThreadID invoke CloseHandle,eax Вы можете видеть, что создан объект события и тред во время обработки сообщения WM_CREATE. Я создаю объект события, установленного в состояние "выключено" и обладающего свойством автоматического выключения. После того, как объект события создан, я создаю тред. Тем не менее, тред не начинает выполняться немедленно, так как он ждет, пока не включится объект события: Код (ASM): ThreadProc proc uses ecx param:DWORD invoke WaitForSingleObject,hEventStart,INFINITE mov ecx,600000000 Первая строка процедуры треда ― это вызов WainForSingleObject. Она ждет, пока не включится объект события, а затем возвращается. Это означает, что даже если тред создан, мы помещаем его в спящее состояние. Когда пользователь выбирает в меню команду "run thread", мы включаем объект события: Код (ASM): .if ax==IDM_START_THREAD invoke SetEvent,hEventStart Вызов SetEvent включает объект события, после чего WainForSingleObject возвращается и тред начинает выполняться. Когда пользователь выбирает команду "stoр thread", мы устанавливаем значение глобальной переменной в TRUE. Код (ASM): .if EventStop==FALSE add eax,eax dec ecx .else invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK mov EventStop,FALSE jmp Threadproc .endif Это останавливает тред и снова передает управление функции WaitForSingleObject. Заметьте, что мы не должны вручную выключать объект, так как мы указали при вызове функции CreateEvent, что значение bManualReset равно FALSE.
Глава тридцать шестая. Братец Кролик и динамические библиотеки В этой главе мы узнаем о dll, что это такое и как их создавать. Вы можете скачать пример здесь.ТЕОРИЯ ― МАТЬ СКЛЕРОЗАЕсли вы программируете достаточно долго, вы заметите, что программы, которые вы пишете, зачастую используют один и те же общие процедуры. Из-за того, что вам приходиться переписывать их снова и снова, вы теряете время. Во времена DOS'а программисты сохраняли эти общие процедуры в одной или более библиотеках. Когда они хотели использовать эти функции, они всего лишь прилинковывали библиотеку к объектному файлу и линкер извлекал функции прямо из библиотек и вставлял их в финальный файл. Этот процесс называется статической линковкой. Хорошим примером являются стандартные библиотеки в C. У этого метода есть изъян ― то, что в каждой программе у вас находятся абсолютно одинаковые копии функций. Впрочем, для DOS-овских программ это не очень большой недостаток, так как только одна программа могла быть активной в памяти, поэтому не происходила трата драгоценной памяти. Под Windows ситуация стала более критичной, так как у вас может быть несколько программ, выполняющихся одновременно. Память будет быстро пожираться, если ваша программа достаточно велика. У Windows есть решение этой проблемы: динамические библиотеки (dynamic link libraries ― dll). Динамическая библиотека ― это что-то вроде сборника общих функций. Windows не будет загружать несколько копий DLL в память; даже если одновременно выполняются несколько экземпляров вашей программы, будет только одна копия DLL в памяти. Здесь я должен остановиться и разъяснить чуть поподробнее. В реальности, у всех процессов, использующих одну и ту же dll есть своя копия этой библиотеки, однако Windows делает так, чтобы все процессы разделяли один и тот же код этой dll. Впрочем, секция данных копируется для каждого процесса. Программа линкуется к DLL во время выполнения в отличии от того, как это осуществлялось в старых статических библиотеках. Вы также можете выгрузить DLL во время выполнения, если она вам больше не нужна. Если программа одна использует эту DLL, тогда та будет выгружена немедленно. Hо если ее еще используют какие-то другие программы, DLL останется в памяти, пока ее не выгрузит последняя из использующих ее программ. Как бы то ни было, перед линкером стоит сложная задача, когда он проводит фиксирование адресов в конечном исполняемом файле. Так как он не может "извлечь" функции и вставить их в финальный исполняемый файл, он должен каким-то образом сохранить достаточно информации о DLL и используемых функциях в выходном файле, чтобы тот смог найти и загрузить верную DLL во время выполнения. И тут в дело вступают библиотеки импорта. Библиотека импорта содержит информацию о DLL, которую она представляет. Линкер может получить из нее необходимую информацию и вставить ее в исполняемый файл. Когда Windows загружает программу в память, она видит, что программа требует DLL, поэтому ищет библиотеку и проецирует ее в адресное пространство процесса и выполняет фиксацию адресов для вызовов функций в DLL. Вы можете загрузить DLL самостоятельно, не полагаясь на Windows-загрузчик. В этом случае вам не потребуется библиотека импорта, поэтому вы сможете загружать и использовать любую DLL, даже если к ней не прилагается библиотеки импорта. Тем не менее, вам все равно нужно знать какие функции находятся внутри нее, сколько параметров они принимают и тому подобную информацию. Когда вы поручаете Windows загружать DLL, если та отсутствует, Windows выдаст сообщение "Требуемый .DLL-файл, xxxxx.dll отсутствует" и все! Ваша программ не может сделать ничего, что изменить это, даже если ваша dll не является необходимой. Если же вы будете загружать DLL самостоятельно и библиотека не будет найдена, ваша программа может выдать пользователю сообщение, уведомляющее об этом, и продолжить работу. Вы можете вызывать недокументированные функции, которые не включены в библиотеки импорта, главное, чтобы у вас было достаточно информации об этих функциях. Если вы используете LoadLibrary, вам придется вызывать GetProcAddress для каждой функции, которую вы заходите вызвать. GetProcAddress получает адрес входной точки функции в определенной DLL. Поэтому ваш код будет чуть-чуть больше и медленнее, но не намного. Теперь, рассмотрев преимущества и недостатки использования LoadLibrary, мы подробно рассмотрим как создать DLL. Следующий код является каркасом DLL. tut_17a.asm Код (ASM): include win64a.inc .code DllMain proc hInstDLL:QWORD, reason:QWORD, unused:QWORD sub esp,28h .if edx==DLL_PROCESS_ATTACH lea rdx,LoadMsg .elseif edx==DLL_PROCESS_DETACH lea rdx,UnloadMsg .endif mov r9d,MB_OK jmp exit DllMain Endp TestHello proc sub esp,28h lea rdx,HelloMsg mov r9d,MB_OK + MB_ICONERROR exit:: lea r8,AppName invoke MessageBox,0 add esp,28h mov eax,TRUE ret TestHello endp AppName db "DLL Skeleton",0 HelloMsg db "Hello, you're calling a function in this DLL",0 LoadMsg db "The DLL is loaded",0 UnloadMsg db "The DLL is unloaded",0 end Вышеприведенная программа ― это каркас DLL. Каждая DLL должна иметь стартовую функцию. Windows вызывает эту функцию каждый pаз, когда: DLL загружена в первый раз DLL выгружена Создается тред в том же процессе Тред разрушен в том же процессе Код (ASM): DllMain proc hInstDLL:QWORD, reason:QWORD, unused:QWORD . . . . mov eax,TRUE ret DllMain Endp Вы можете назвать стартовую функцию как пожелаете, главное чтобы она упоминалась в bat-файле ("/ENTRY: DllMain"). Эта функция получает три параметра, только первые два из них важны. hInstDLL ― это дескриптор модуля DLL. Это не тоже самое, что дескриптор процесса. Вам следует сохранить это значение, так как оно понадобится вам позже. Вы не сможете ее получить в дальнейшем легко. reason ― может иметь одно из следующих четырех значений: DLL_PROCESS_ATTACH ― DLL получает это значение, когда впервые загружается в адресное пространство процесса. Вы можете использовать эту возможность для того, чтобы осуществить инициализацию. DLL_PROCESS_DETACK ― DLL получает это значение, когда выгружается из адресного пространства процесса. Вы можете использовать эту возможность для того, чтобы "почистить" за собой: освободить память и так далее. DLL_THREAD_ATTACK ― DLL получает это значение, когда процесс создает новую ветвь. DLL_THREAD_DETACK ― DLL получает это значение, когда ветвь в процессе уничтожена. Вы возвращаете TRUE в eax, если вы хотите, чтобы DLL продолжала выполняться. Если вы возвратите FALSE, DLL не будет загружена. Например, если ваш инициализационный код должен зарезервировать память и он не может это сделать, стартовой функции следует возвратить FALSE, чтобы показать, что DLL не может запуститься. Вы можете поместить ваши функции в DLL следом за стартовой функцией или до нее. Hо если вы хотите, чтобы их можно было вызвать из других программ, вы должны поместить их имена в списке экспортируемых функций в файле установок модуля. DLL требуется DEF-файл на стадии разработки. файла tut_17a.def Код (Text): LIBRARY tut_17a EXPORTS DllMain EXPORTS TestHello Обычно у вас должна быть первая строка. Ключевое слово LIBRARY определяет внутреннее имя модуля DLL. Желательно, чтобы оно совпадало с именем файла. EXPORTS говорит линкеру, какие функции в DLL экспортируются, то есть, могут вызываться из других программ. В прилагающемся примере нам нужно, чтобы другие модули могли вызывать TestFunction, поэтому мы указываем здесь ее имя. Другое отличие заключается в параметрах, передаваемых линкеру. Вы должны указать ключи /DLL и /DEF: Код (Text): cls set masm64_path=\masm64\ set filename=tut_17a if exist %filename%.dll del %filename%.dll %masm64_path%bin\ml64.exe /c /Cp /I %masm64_path%include %filename%.asm || exit %masm64_path%bin\Link.exe %filename%.obj /LIBPATH:%masm64_path%lib ^ /SUBSYSTEM:WINDOWS /ENTRY:DllMain /DLL /ALIGN:16 ^ /stub:%masm64_path%bin\stubby.exe /SECTION:.text,W /DEF:%filename%.def || exit del %filename%.obj del %filename%.exp Параметры компилятора те же самые, обычно /c /Cp. После компиляции вы получите dll-файл и lib-файл. Последний файл ― это библиотека импорта, которую вы можете использовать, чтобы прилинковать к другим программам функции из соответствующего dll-файла.
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИ Динамическую библиотеку можно вызывать двумя способами ― явным и неявным связыванием. Неявное связывание ― наиболее широко используемый способ. Здесь с самого начала компилятору строго говорится, что вот так-то и так-то, вот это вот считать dll и поступать с ней вот так, а не иначе. Явное связывание ― это вызов подпрограммы LoadLibrary для загрузки конкретной библиотеки (например, kernel32.dll), а потом подпрограмма GetProcAddress возвращает адрес конкретной функции (например, IsDebuggerPresent), а далее что-то вроде call [адрес, возвращенный GetProcAddress]. Неявное связывание оставляет следы в таблице импорта, которые легко обнаружить. Явное связывание в таблице импорта не отображается.Два варианта EXE-файлов, которые будут работать с tut17a.dll Вариант первый ― загрузка адреса функции TestHello из tut17a.dll операционной системой в секцию импорта. Код (ASM): include win64a.inc includelib tut_17a.lib TestHello PROTO .code WinMain proc sub esp,28h call [TestHello] invoke ExitProcess,0 WinMain endp end Вариант второй ― «динамическая загрузка» tut17a.dll при помощи функции LoadLibrary. Далее определяется адрес процедуры TestHello с помощью GetProcessAddress, после чего осуществляется вызов TestHello. После того как tut17a.dll стала ненужной ― ее выгружают из памяти функцией FreeLibrary (если вы этого не сделаете ― система выгрузит dll автоматически после закрытия приложения ― использование FreeLibrary это программисткая традиция с целью освободить оперативную память от «лишних» приложений ) Код (ASM): include win64a.inc .code WinMain proc sub esp,28h mov ecx,offset LibName invoke LoadLibrary ; Вызываем LoadLibrary и передаем имя желаемой DLL. Если вызов проходит успешно, ; будет возвращен дескриптор библиотеки (DLL). Если нет, то будет возвращен NULL. ; Вы можете передать дескриптор библиотеки функции GetProcAddress или любой другой ; функции, которая требует его в качестве одного из параметров. or eax,eax;.if eax==NULL ;the dll not found jnz @f mov r8d,offset AppName mov edx,offset DllNotFound invoke MessageBox,0,,,MB_OK jmp exit;.else @@: mov hLib,rax mov edx,offset FunctionName invoke GetProcAddress,eax ; Когда вы получаете дескриптор библиотеки, вы передаете его GetрrocAddress вместе ; с именем функции в этой dll, которую вы хотите вызвать. Она возвратит адрес ; функции, если вызов пройдет успешно. В противном случае, она возвратит NULL. ; Адреса функций не изменятся, пока вы не перезагрузите библиотеку. Поэтому ; их можно поместить в глобальные переменные для будущего использования or eax,eax;.if eax==NULL ;requested function not found jnz @f mov r8d,offset AppName mov edx,offset FunctionNotFound invoke MessageBox,0,,,MB_OK jmp @0 @@: call rax ; вызываем функцию с помощью call @0: invoke FreeLibrary,hLib ; вам больше не требуется библиотека, выгружаете ее с помощью FreeLibrary. exit: invoke ExitProcess,0 WinMain endp LibName db "tut_17a.dll",0 FunctionName db "TestHello",0 DllNotFound db "Cannot load library",0 AppName db "Load Library",0 FunctionNotFound db "TestHello function not found",0 hLib dq ? ; дескриптор библиотеки (DLL) end Как вы можете видеть, использование LoadLibrary чуть сложнее, но гораздо гибче.
В данном пример, мы обновляем progress bar, а затем проверяем, было ли достигнуто максимальное значение. Если это так, мы убиваем таймеp, после чего устанавливаем текст статус-окна с помощью сообщения SB_SETTEXT. Вызывается MessageBox, и когда пользователь нажмет на OK, мы очищаем текст в status bar'е и progress bar'е.
Глава тридцать восьмая. Братец Кролик и окно просмотра деревьев В этой главе мы изучим как использовать окно просмотра деревьев. Более того, мы также узнаем как реализовать drag and drop для этого элемента управления и как использовать список изображений. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАОкно просмотра деревьев ― это особый вид окна, который представляет объекты в иерархическом порядке. В качестве примера может случить левая панель Windows Explorer'а. Вы можете использовать этот элемент управления, чтобы показать отношения между объектами. Создание окон просмотра деревьевВы можете создать окно просмотра деревьев, вызвав CreateWindowEx и передав ей в качестве имени класса "SysTreeView32" или "WC_TREEVIEW". Желательно чтобы родительское окно включало в свой стиль значения WS_VISIBLE и WS_TABSTOP. Не забудьте поместить вызов InitCommonControls в ваш код. Есть несколько стилей присущих только окну просмотра деревьев. Вот наиболее часто используемые: TVS_HASBUTTONS ― отображает слева от изображения кнопки разворачивания (+) и сворачивания (-). Пользователь щелкает мышкой по кнопкам, чтобы открыть или закрыть список дочерних элементов. Чтобы вставить кнопки с пунктами в корень окна просмотра деревьев, также должен быть указан TVS_LINESATROOT. TVS_HASLINES ― ветви древовидных структур соединяются линиями. TVS_LINESATROOT ― корневые узлы соединяются с ветвями дерева линиями. Этот стиль игнорируется, если не указан TVS_HASLINES. При задании стилей TVS_HASLINES и TVS_LINESATROOT узлы дерева в окне соединяются линиями. Это придает содержимому окна просмотра деревьев вид древовидной структуры. Применение стиля TVS_HASBUTTONS влечет за собой добавление стандартной кнопки раскрытия/сворачивания слева от каждого элемента дерева, для которого допустимы эти операции. Кнопки содержат знак "+", если элемент дерева можно раскрыть по крайней мере на один уровень, и знак "-", если ветвь дерева полностью развернута. Для разворачивания или сворачивания ветви дерева достаточно щелкнуть мышью на этой кнопке. Обычно при создании окна используют все три стиля. Сразу после создания в окне просмотра деревьев не отражается никаких элементов. Окно заполняется путем последовательного добавления отдельных элементов.Посылка управляющих сообщений окну просмотра деревьевОкно просмотра деревьев, как и любой другой элемент управления, взаимодействует с родительским окном с помощью сообщений. Родительское окно может посылать различные сообщения окну просмотра деревьев, а тот может посылать "уведомительные" сообщения своему родительскому окну. В этом отношении окно просмотра деревьев ничем не отличается от других окон. Когда с окном просмотра деревьев происходит что-нибудь, оно посылает сообщение WM_NOTIFY родительскому окну вместе с дополнительной информацией. wParam ― ID элемент управления, но то, что оно будет уникальным не гарантируется, поэтому не используйте его. Вместо этого мы будет использовать hwndFrom или IDFrom из структуры NMHDR, на которую указывает lParam. lParam ― указатель на структуру NMHDR. Некоторые элементы управления могут передавать указатель на большую структуру, но они должны иметь в качестве первого поля структуру NMHDR. Поэтому вы можете быть уверены, что lParam по крайней мере указывает на NMHDR. Затем мы проанализируем структуру NMHDR. Код (ASM): NMHDR STRUCT hwndFrom QWORD ? ;дескриптор окна элемента управления, ;который послал это сообщение. idFrom QWORD ? ;ID элемента управления _code DWORD ? ;настоящее сообщение, которое элемент ;управления хотел послать родительскому окну NMHDR ENDS Уведомления от окна просмотра деревьев начинаются с префикса TVN_ (Tree View Notify). Сообщения для окно просмотра деревьев начинаются с TVM_ (Tree View Message), например TVM_CREATEDRAGIMAGE. Окно просмотра деревьев посылает TVN_xxxx в поле code структуры NMHDR. родительское окно может посылать TVM_xxxx элементу управления.Добавление пунктов в окно просмотра деревьевПосле того, как вы создадите окно просмотра деревьев, вы можете добавить в него пункты. Вы можете сделать это, послав элементу управления TVM_INSERTITEM. wParam=0; lParam=указатель на структуру типа TV_INSERTSTRUCT; СообщениеДействиеTVM_DELETEITEMУдаление элемента из окна просмотра деревьев. При успешном удалении возвращается ненулевое значение. wParam должен быть равен 0. lParam содержит дескриптор удаляемого элементаTVM_EXPANDРазворачивание или сворачивание ветви дерева на один уровень. При успешном завершении операции возвращается ненулевое значение. wParam определяет тип операции. Может быть равным TVE_COLAPSE (сворачивание ветви дерева), TVE_COLAPSERESET (сворачивание ветви и удаление порожденных элементов), TVE_EXPAND (раскрытие ветви) TVE_TOGGLE (переключение состояния узла). lParam содержит дескриптор родительского узла ветвиTVM_GETITEMПолучение атрибутов отдельного элемента дерева. При успешном завершении возвращается ненулевое значение. wParam должен быть равен 0. lParam содержит указатель на структуру типа TV_ITEM, в которую записывается информация об элементеTVM_INSERTITEMВставка в дерево нового элемента. Возвращается дескриптор нового элемента либо 0 при ошибке. wParam должен быть равен 0. lParam содержит указатель на структуру типа TV_INSERTSTRUCT описывающую новый элементTVM_SELECTITEMВыборка элемента окна просмотра деревьев. При успешном завершении возвращается ненулевое значение. wParam задает тип операции. TVGN_CARET (элемент выбирается), TVGN_DROPHILITE (элемент подсвечивается для операции перетаскивания (drag-and-drop)), TVGN_FIRSTVISIBLE (содержимое окна просмотра деревьев прокручивается (сдвигается) так, чтобы заданный элемент стал первым видимым элементом). lParam содержит дескриптор элемента дереваВам следует знать кое-какую терминологию, касающуюся взаимоотношений между элементами в окне просмотра деревьев. Элемент может быть родительским, дочерним или тем и другим одновременно. Родительский элемент ― это такой элемент, с которым ассоциированы подэлементы. В то же время, родительский элемент может быть дочерним по отношению к какому-то другому. Элемент, у которого нет родителя, называется корнем (root). В окне просмотра деревьев может быть много корневых элементов. Теперь мы проанализируем структуру TV_INSERTSTRUCT. Код (ASM): TV_INSERTSTRUCT STRUCT hParent QWORD ? ;дескриптор родительского элемента. Если этот параметр ;равен TVI_ROOT или NULL, тогда элемент вставляется в корень окна просмотра деревьев hInsertAfter QWORD ?;значение определяющее каким образом новый элемент должен быть ;добавлен в дерево. Если это поле содержит дескриптор элемента, новый элемент будет вставлен ;после этого элемента item TV_ITEM <>;описывает сам элемент TV_INSERTSTRUCT ENDS hInsertAfter ― дескриптор элемента, после которого будет вставляться новый элемент, или одно из следующих значений: TVI_FIRSTвставка элемента в начало спискаTVI_LASTвставка элемента в конец спискаTVI_SORTвставка элемента в список согласно алфавитному порядку Код (ASM): TV_ITEM STRUCT imask DWORD ?,? hItem QWORD ? state DWORD ? stateMask DWORD ? pszText QWORD ? cchTextMax DWORD ? iImage DWORD ? iSelectedImage DWORD ? cChildren DWORD ? lParam DWORD ? TV_ITEM ENDS Эта структура используется для отсылки и получения информации об элементе окно просмотра деревьев (в зависимости от сообщений). Например, с помощью TVM_INSERTITEM, она используется для указания атрибутов элемента, который должен быть вставлен в окно просмотра деревьев. С помощью TVM_GETITEM, она будет заполнена информацией о выбранном элементе окна просмотра деревьев. imask ― используется для указания, какой член структуры TV_ITEM верен. Например, если значение в imask равно TVIF_TEXT, оно означает, что только pszText верно. Вы можете комбинировать несколько флагов вместе. содержимое поляназначениеTVIF_HANDLEИнформация содержится в поле hItemTVIF_STATEИнформация содержится в полях state и stateMaskTVIF_TEXTИнформация содержится в полях pszText и cchTextMaxTVIF_IMAGEИнформация содержится в поле iImageTVIF_SELECTEDIMAGEИнформация содержится в поле iSelectedImageTVIF_CHILDRENИнформация содержится в поле cChildrenTVIF_LPARAMИнформация содержится в поле lParam