Windows 10 terminal прозрачность

Новое приложение Windows Terminal, может похвастаться вкладками, запускать Cmd, PowerShell, Bash и Python в одном окне. Новый терминал легко настроить, вы даже можете установить анимированные GIF-изображения в качестве фона. Вы можете использовать Командную строку, PowerShell или Linux с прозрачным фоном в терминале Windows, вот как включить эту функцию.

Наряду со способностью устанавливать собственные темы и изменять цвет фона, вы также можете применять акриловый фон для добавления эффектов прозрачности и размытия с помощью Microsoft Fluent Design System.

В этом руководстве для Windows 10, вы узнаете, как включить и настроить эффект прозрачности в терминале Windows.

Как включить Прозрачность в Windows Terminal

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

Шаг 1: Откройте Windows Terminal.

Шаг 2: Нажмите кнопку меню (стрелка вниз) рядом с вкладкой.

Нажмите кнопку меню (стрелка вниз) рядом с вкладкой.

Шаг 3: Нажмите «Параметры».

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

Рекомендуется использовать редактор который включает в себя схему JSON

Шаг 4: В разделе «defaults» скопируйте и вставьте следующую команду, чтобы включить прозрачность во всех приложениях командной строки:

"useAcrylic": true,

"acrylicOpacity": 0.5

В разделе «defaults» скопируйте и вставьте следующую команду

Шаг 5: И сохраните JSON файл.

Терминал Windows с прозрачным фоном для всех приложений

Если вы хотите настроить прозрачный фон, только для одного приложения, например powershell

В разделе «list» выберите профиль (приложение), чтобы установить акриловый фон. Например, после:

«commandline»: «powershell.exe»

вставьте запятую ( ,) в конце строки, скопируйте и вставьте следующие строки:

"useAcrylic": true,

"acrylicOpacity": 0.5

розрачный фон, только для одного приложения, например powershell

 Вы можете это повторить для Командной строки и др.

 (Необязательно) В настройках «acrylicOpacity» измените значение выбранного уровня прозрачности. Например, 0.6 сделает фон менее прозрачным, а 0.4 сделает фон более прозрачным.

Сохраните файл, чтобы применить изменения.

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

В этой статье я покажу как написать приложение для windows на ассемблере. В качестве IDE будет привычная Visual Studio 2019 со своими плюшками — подсветка кода, отладка и привычный просмотр локальных переменных и регистров. Собирать приложение будет MASM, а значит, у нас будут и чисто масмовские плюшки. Приложением будет игра в пятнашки. С одной стороны это просто, но не так чтобы совсем хелловорлд (впрочем хелловорлд мы тоже увидим во время настройки VS). С другой стороны это будет полноценное оконное приложение с меню, иконкой, отрисовкой, выводом текста и обработкой мыши с клавиатурой. Изначально у меня была мысль сделать что-нибудь поинтереснее пятнашек, но я быстро передумал. Сложность и размер статьи увеличивается значительно, а она и так получилась немаленькая. Новой же информации сильно больше не становится. Статья рассчитана на тех, кто имеет хотя бы начальные знания ассемблера, поэтому здесь не будет всяких мелочей из разряда как поделить одно число на другое или что делает команда mov. Иначе объем пришлось бы увеличить раза в три.

Заранее постараюсь ответить на вопрос — зачем это нужно, если на ассемблере сейчас уже никто не пишет? Есть пара объективных причин и одна субъективная. Из объективного — написание подобных программ позволяет понять внутреннее устройство windows и как в конечном итоге наш код исполняется на процессоре. Далеко не всем это действительно надо, но для некоторых вещей это обязательное знание. Вторая причина это то, что позволяет взглянуть на разработку немного под другим углом. Примерно так же как попробовать функциональное программирование полезно даже если не писать ничего в функциональном стиле. К примеру я слушал лекции Мартина Одерски вовсе не потому что решил перейти с C# на Scala. Полезно посмотреть на привычную разработку под другим углом. Субъективная же причина — для меня это было просто интересно, отойти от коммерческой разработки, этого цикла задач, спринтов, митингов, сроков и заняться тем, что интересно именно тебе.

Так получилось что у меня появилось много свободного времени, часть из которого я потратил на то, что называется пет-проектами. Это не стало какими-то production-ready вещами, скорее какие-то идеи интересные лично мне, что-то на что вечно не хватало времени. Одна из этих идей это ассемблер в современной IDE. Давно хотел этим заняться, но все не было времени. Мне было очень интересно со всем этим разбираться, надеюсь читателям тоже понравится.

Шаг первый — настраиваем VS

Тут я немного схитрил. Точнее так уж получилось, что здесь все уже сделано за нас. Есть пошаговая инструкция и даже готовый пустой проект. Можно воспользоваться пошаговой инструкцией, а я просто скачал пустой проект и переименовал SampleASM в FifteenAsm. Единственное, что надо сделать помимо переименования, это установить SubSystem : Windows в свойствах проекта (properties > Linker > System > SubSystem : Windows). Далее выбираем x86, нажимаем F5 (либо кликаем мышкой) и видим вот такое сообщение:

Hello world asm

Hello world asm

Теперь о подсветке синтаксиса. Тут есть разные пути, и я решил поискать что есть готового. Готового оказалось немного, я установил Asm-Dude. Также попробовал ChAsm, но внешний вид меня не порадовал. Впрочем внешний вид это дело вкуса, я остановился на Asm-Dude. Тут правда есть такой нюанс — Asm-Dude не поддерживает VS 2022, самая старшая версия VS 2019. Вот так выглядит все в сборе — дебаг, просмотр переменных, в т.ч. нормальное отображение структур, мнемоника для ассемблера.

Update: AsmDude для 2022 существует, но его нет в студийном менеджере расширений (автор не стал публиковать), доступен по ссылке. Вторая версия расширения в процессе разработки, и 1 октября автор расширения ее опубликовал. Во второй версии заявлено много всяких фич, не все они готовы, но пользоваться уже можно. Спасибо @NN1 и @aroman313 за уточнение.

Теперь еще одна вещь, о которой хочется рассказать, прежде чем приступить к основной части. Это MASM SDK. Это совсем необязательная вещь, но очень полезная. Там есть готовые inc файлы для WinAPI, а еще есть много примеров самых разных приложений на ассемблере. Но проект из этой статьи будет работать и без него.

Шаг второй — оконное приложение

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

О вызове функций вообще и WinAPI в частности

Чтобы вызвать функцию, ее надо объявить. Ниже разные способы это сделать.

extern MessageBoxA@16 : PROC
MessageBoxA PROTO, hWnd:DWORD, pText:PTR BYTE, pCaption:PTR BYTE, style:DWORD
MessageBoxW PROTO, :DWORD,:DWORD,:DWORD,:DWORD

Объявление со списком параметров более понятно. Хотя именовать параметры и необязательно. Объявлением с extern я пользоваться не буду, оставим это для любителей разгадывать ребусы. Что такое A(или W) в имени функции? Это указание на тип строк, A — ANSI, W — Unicode. Для простоты дела я решил не связываться с юникодом и везде использовал ANSI версии. Обычно же применяют дефайн примерно такого вида:

#ifdef UNICODE
#define SetWindowText SetWindowTextW
#else
#define SetWindowText SetWindowTextA
#endif

Теперь о вызовах функций, «стандартный» для ассемблера вызов выглядит так

push 0
push offset caption
push offset text
push 0
call MessageBoxA

Существует мнемоническое правило для порядка аргументов — слева направо — снизу вверх. Иными словами первый аргумент в объявлении функции (здесь это хендл окна hWnd:DWORD) будет в самом нижнем push. К счастью в MASM есть очень удобная вещь — invoke. Вот так выглядит вызов той же самой функции.

invoke MessageBoxA, 0, offset howToText, offset caption, 0

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

int MessageBoxA(
  [in, optional] HWND   hWnd,
  [in, optional] LPCSTR lpText,
  [in, optional] LPCSTR lpCaption,
  [in]           UINT   uType
);

Осталось перевести все эти HWND и LPCSTR в соответствующие типы для ассемблера. Тип данных LPCSTR будет DWORD, ведь это просто ссылка. Олдфаги с легкостью узнают венгерскую нотацию, а название типа расшифровывается как Long Pointer Const String. HWND тоже будет просто DWORD, ведь HWND, как и LPCSTR по своей сути просто ссылка. Ну а UINT это DWORD просто по определению. В некотором роде сигнатура функций на ассемблере даже проще, ссылка здесь это просто ссылка, нет кучи разных типов.
Отсюда следует важный вывод — нет никаких специальных «ассемблерных» функций, это то же самое WinAPI !. Нам достаточно знать как решается нужная нам задача средствами WinAPI, неважно на каком языке они будут вызываться. Поэтому задача «вывести текст в окно средствами ассемблера» на самом деле будет «вывести текст в окно средствами WinAPI», а уж информации по WinAPI полно. Обратное тоже верно, зная как что-то сделать средствами WinAPI это можно сделать на практически любом языке. А это уже часто бывает полезно при написании скриптов.

Создаем простое окно

Перед созданием окна я сделал три inc-файла. Один с прототипами WinAPI, другой с константами приложения (ширина окна, заголовок, цвет заливки и все в таком же духе) и третий, со структурами WinAPI и целой кучей винапишных констант. Теперь можно писать NULL или TRUE/FALSE. Или MB_OK вместо 0, как в примере выше с MessageBoxA. Никаких специфических действий не нужно, просто Add — New Item — Text File и не забываем include filename. Файлики назвал WinApiProto.inc, WinApiConstants.inc, AppConstants.inc. Пример содержимого показан ниже.

Вот так теперь выглядит наш код

.386
.model flat, stdcall
.stack 4096

include WinApiConstants.inc
include WinApiProto.inc

.data

include AppConstants.inc

.code
main PROC
;...more code

Небольшое отступление про строки. Вот пример строковых констант

szClassName             db "Fifteen_Class", 0
howToText               db "The first line", CR , LF , "The second.", 0

Запятая означает конкатенацию, db это define byte, CR LF определены в WinApiConstants.inc (13 и 10 соответственно), ноль на конце это null-terminated строка. В итоге строки это никакой не специальный тип данных, а просто массивы байт с нулем на конце. В случае с юникодом возни было бы больше, но я решил не усложнять себе жизнь и использовать везде ANSI строки.

Вот мы и добрались до создания окна. Для этого нам надо

  • заполнить структуру WNDCLASSEX (объявлена в WinApiConstants)

  • зарегистрировать класс окна

  • создать процедуру главного окна

  • создать окно

Кода вышло уже почти на 200 строк, поэтому я покажу самые интересные куски, целиком можно посмотреть на гитхабе.
Объявление и заполнение WNDCLASSEX, как видим все как в языках высокого уровня. Ну, почти все — автодополнения со списком полей структуры нет.

WNDCLASSEX STRUCT
  cbSize            DWORD ?
  style             DWORD ?
  lpfnWndProc       DWORD ?
WNDCLASSEX ENDS
mov wc.cbSize,         sizeof WNDCLASSEX
mov wc.style,          CS_BYTEALIGNWINDOW
mov wc.lpfnWndProc,    offset WndProc

При создании окна весьма важный параметр WS_EX_COMPOSITED. Без него при перерисовке будет мерзкий flickering. Очень хорошо что это работает — реализовывать двойную буферизацию самостоятельно желания не было.

push WS_EX_OVERLAPPEDWINDOW or WS_EX_COMPOSITED
call CreateWindowExA

Теперь немного чудесных директив MASM. Вот так вот просто организован цикл обработки сообщений

; Loop until PostQuitMessage is sent
.WHILE TRUE
    invoke GetMessageA, ADDR msg, NULL, 0, 0
    .BREAK .IF (!eax)
    invoke TranslateMessage, ADDR msg
    invoke DispatchMessageA, ADDR msg
.ENDW

А вот так без них

StartLoop:
  push 0
  push 0
  push 0
  lea eax, msg
  push eax
  call GetMessageA

  cmp eax, 0
  je ExitLoop

  lea eax, msg
  push eax
  call TranslateMessage

  lea eax, msg
  push eax
  call DispatchMessageA

  jmp StartLoop
ExitLoop:

А вот как все просто в оконной процедуре. Никаких тебе cmp uMsg, WM_DESTROY, кучи меток, простой IF

.IF uMsg == WM_DESTROY
    invoke PostQuitMessage, NULL
    xor eax, eax
    ret
.ENDIF

Вот как делается подтверждение на закрытие окна

.IF uMsg == WM_CLOSE
    invoke MessageBoxA, hwin, ADDR exitConfirmationText, ADDR caption, MB_YESNO
    .IF eax == IDNO
        xor eax, eax
        ret
    .ENDIF
.ENDIF

Обещанный хелловорлд готов.

Добавляем иконку и меню

Иконка и меню в мире windows относятся к ресурсам. Поэтому добавляем к нашему проекту файл ресурсов — Add — Resource — Menu. Дальше можно воспользоваться встроенным редактором VS, я просто взял и отредактировал свежий файл FifteenAsm.rc в блокноте. Получилось вот так

500 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "FIFTEENICON.ICO"

600 MENUEX MOVEABLE IMPURE LOADONCALL DISCARDABLE
BEGIN
    POPUP "&File", , , 0
        BEGIN
        MENUITEM "&New Game", 1100
        MENUITEM "&Exit", 1000
    END
    POPUP "&Help", , , 0
    BEGIN
        MENUITEM "&How to play", 1800
        MENUITEM "&About", 1900
    END
END

Обратите внимание на магические числа 500 и 600. Это идентификаторы ресурсов, совсем скоро мы увидим зачем они нужны. Также обратите внимание на магические числа 1000, 1100, 1800, 1900. Это идентификаторы команд, мы тоже увидим зачем они нужны, но чуть позже. Чуть не забыл про сам файл иконки, нарисовал я ее в каком-то онлайн редакторе. Дизайнер из меня так себе, поэтому что получилось, то получилось. Добавляем в проект под именем Fifteenicon.ico, тут главное назвать точно как в файле ресурсов. Дальше все просто. Иконка добавляется на этапе заполнения структуры WNDCLASSEX, тут у нас магическое число 500

push 500
push hInst
call LoadIconA
mov wc.hIcon, eax

Меню добавляется после создания окна, здесь магическое число 600

call CreateWindowExA

mov hWnd,eax

push 600
push hInst
call LoadMenuA

push eax
push hWnd
call SetMenu

А вот так обрабатываются команды меню, тут остальные магические числа 1000, 1100, 1800, 1900. Вообще с использованием MASM код не особо отличается от кода на тех же плюсах.

.IF uMsg == WM_COMMAND

    .IF wParam == 1000
        invoke SendMessageA, hwin, WM_SYSCOMMAND, SC_CLOSE, NULL
    .ENDIF

    .IF wParam == 1100
        invoke MessageBoxA, hwin, ADDR newGameConfirmationText, ADDR caption, MB_YESNO
        .IF eax == IDYES
            ;call InitTilesData
        .ELSEIF eax == IDNO
            xor eax, eax
            ret
        .ENDIF
    .ENDIF

    .IF wParam == 1800
        invoke MessageBoxA, hwin, ADDR howToText, ADDR caption, MB_OK
    .ENDIF

    .IF wParam == 1900
        invoke MessageBoxA, hwin, ADDR aboutText, ADDR caption, MB_OK
    .ENDIF

.ENDIF

У приложения появилась иконка и есть меню. Ради интереса посмотрел на размер исполняемого файла, всего 6656 байт.

Нажали на New Game

Нажали на New Game

Шаг третий — игра

Создали окно, пора заняться самой игрой. Здесь я тоже покажу только самые интересные места.

Инициализация данных и начальная перетасовка

Данные о положении тайлов будут храниться в массиве из 16 байт. Ноль будет положением пустого тайла, значения от 1 до 15 соответствующие тайлы. Нумерация индексов тайлов слева направо, сверху вниз. Теперь надо их перетасовать и тут встает вопрос, откуда брать случайные числа? RDRAND и RDSEED появились достаточно поздно, а мне хотелось сделать код в «классическом» стиле. Сгоряча я даже думал реализовать Вихрь Мерсенна, но потом решил что это перебор. Поэтому честно нашел простенький ГПСЧ буквально в десяток команд, для seed использовал системное время. Идея начальной перетасовки простая, сначала заполняем массив по порядку (приводим в конечное состояние), а потом случайным образом двигаем тайлы. Тайлы двигаются по правилам, значит их всегда можно будет собрать в конечное положение. Если заполнять тайлы совсем рандомно, то надо проверять можно ли вообще собрать такую комбинацию. По опыту уже 100 итераций перемешивает тайлы вполне нормально.

    local randSeed : DWORD

    invoke GetTickCount
    mov randSeed, eax

    xor eax, eax
    xor ebx, ebx

    xor ebx, ebx
    .WHILE ebx < initialSwapCount
        
        mov eax, 4; random numbers count, i.e. from 0 to 3
        push edx
        imul edx, randSeed, prndMagicNumber
        inc edx
        mov randSeed, edx
        mul edx
        mov eax, edx
        pop edx

        add al, VK_LEFT
        push ebx
        invoke ProcessArrow, NULL, al; move a tile
        pop ebx

        inc ebx
    .ENDW

    ret

Отрисовка

Добавляем обработку WM_PAINT в оконной процедуре

LOCAL Ps     :PAINTSTRUCT
LOCAL hDC    :DWORD

.IF uMsg == WM_PAINT
    invoke BeginPaint, hWin, ADDR Ps
      mov hDC, eax
      invoke PaintProc, hWin, hDC
    invoke EndPaint, hWin, ADDR Ps
.ENDIF

Отрисовка тайлов. Из интересного здесь организация двойного цикла с использованием директив MASM WHILE и передача указателя на RECT в процедуре CalculateTileRect.

    LOCAL Rct       : RECT
    invoke CreateSolidBrush, tileBackgroundColor
    mov hBrush, eax
    invoke SelectObject, hDC, hBrush

    ;fill tiles with background color
    mov vert, 0
    .WHILE vert < 4
        mov hor, 0
        .WHILE hor < 4
            
            invoke CalculateTileRect, ADDR Rct, hor, vert
            invoke RoundRect, hDC, Rct.left, Rct.top, Rct.right, Rct.bottom, 
tileRoundedEllipseSize, tileRoundedEllipseSize
            
        inc hor
        .ENDW
        inc vert
    .ENDW

    invoke DeleteObject, hBrush

CalculateTileRect proc rct :DWORD, hor:BYTE, vert:BYTE

    mov edx, rct

    invoke CalculateTileRectPos, hor, 0
    mov (RECT PTR [edx]).left, eax

    ret
CalculateTileRect endp

Обратите внимание на эту строчку. Структура передана по ссылке, смещение на left вычисляется автоматически.

mov (RECT PTR [edx]).left, eax

А вот как работает IntToStr (почти что честный) на ассемблере. Писать честный IntToStr мне не хотелось, поэтому я тут схитрил. Завел массив из 3 байт под строку, второй и третий байты сразу обнуляются. Числа бывают от 1 до 15, поэтому если число было меньше 10, то к значению прибавляем магическое число 48 (ASCII код для нуля) и получаем нужный первый байт буфера. Получается тоже самое что и на Си, когда пишем c = ‘0’ + i. Поскольку второй байт уже нулевой у нас получается готовая null-terminated строка, неважно что буфер из 3 байт. Если число больше 9, то первая цифра всегда 1, а вторая это остаток от деления на 10. Тут уже третий байт играет роль конца строки.

mov [buffer+1], 0
mov [buffer+2], 0

.IF bl < 10
  add bl, asciiShift
  mov [buffer], bl
  sub bl, asciiShift
.ELSEIF bl > 9
  mov al, asciiShift
  inc al
  mov [buffer], al

  xor ax, ax
  mov al, bl
  mov cl, 10
  div cl
  add ah, asciiShift
  mov [buffer+1], ah
.ENDIF

Вот так выглядит игровое поле

Добавляем интерактив

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

.IF uMsg == WM_KEYDOWN
    .if wParam == VK_LEFT
        invoke ProcessArrow, hWin, wParam
    .elseif wParam == VK_RIGHT
        invoke ProcessArrow, hWin, wParam
    .elseif wParam == VK_UP
        invoke ProcessArrow, hWin, wParam
    .elseif wParam == VK_DOWN
        invoke ProcessArrow, hWin, wParam
    .endif
.ENDIF

.IF uMsg == WM_LBUTTONUP
    invoke ProcessClick, hWin, lParam
.ENDIF

Сначала посмотрим как реализовано перемещение тайлов курсором. Вот немного укороченная версия процедуры ProcessArrow. FindEmptyTileIndex возвращает в регистре eax индекс пустого тайла . В зависимости от нажатой клавиши проверяем выход за границы диапазона, т.е. можно ли переместить тайл в данной позиции в данном направлении. Если нельзя, уходим на метку pass в конец процедуры, если можно, то вызываем последовательно SwapTiles, RedrawWindow и ProcessPossibleWin.

ProcessArrow proc hWin:DWORD, key:DWORD

    call FindEmptyTileIndex

    .IF key == VK_UP
        cmp eax, 12
        ja pass

        ;when tile goes up, new empty tile index (ETI) will be ETI+4,
        mov ebx, eax
        add ebx, 4
    .ENDIF

    .IF key == VK_RIGHT
        ;empty tile shouldnt be on 0, 4, 8, 12 indexes
        cmp eax, 0
        je pass
        cmp eax, 4
        je pass
        cmp eax, 8
        je pass
        cmp eax, 12
        je pass

        ;when tile goes right, new empty tile index (ETI) will be ETI-1,
        mov ebx, eax
        dec ebx
    .ENDIF

    invoke SwapTiles, eax, ebx
    .IF hWin != NULL ;little trick to simplify initial random data
        invoke RedrawWindow, hWin, NULL, NULL, RDW_INVALIDATE
        invoke ProcessPossibleWin, hWin
    .ENDIF

    pass:
    ret
ProcessArrow endp

Для перемещения тайла от кликов мышью нужно понять по какому тайлу кликнули и проверить, можно ли его перемещать. Для этого в цикле (двойной цикл организован через директиву MASM .WHILE) вызываем CalculateTileRect и проверяем находится ли курсор мыши внутри прямоугольника. Принцип проверки тот же, что и в ProcessArrow — cmp в ряд, только команды условного перехода другие. Внутри ProcessArrow je (jump equal), а тут ja jb (jump above jump below). Дальше все тоже самое что и с курсором, только наоборот. Смотрим разницу между индексами пустого и кликнутого тайла и вызываем процедуру ProcessArrow (наверное не самое удачное название) с нужными аргументами. Сокращенная версия процедуры.

ProcessClick proc hWin:DWORD, lParam:DWORD
    local rct : RECT

    movsx ebx, WORD PTR [ebp+12] ; x coordinate
    movsx ecx, WORD PTR [ebp+14] ; y coordinate

    mov vert, 0
    .WHILE vert < 4
        mov hor, 0
        .WHILE hor < 4
            
            invoke CalculateTileRect, ADDR Rct, hor, vert
            
            cmp ebx, Rct.left
            jb next
            cmp ebx, Rct.right
            ja next
            cmp ecx, Rct.top
            jb next
            cmp ecx, Rct.bottom
            ja next
                
; the idea is that tile can be moved only if there is a particular diff
; between its index and empty tile index
; -1, +1 ,-4, +4 for different directions, similar to ProcessArrow proc
                call FindEmptyTileIndex

                .IF index > al
                    sub index, al
                    .IF index == 1
                        invoke ProcessArrow, hWin, VK_LEFT
                    .ELSEIF index == 4
                        invoke ProcessArrow, hWin, VK_UP
                    .ENDIF
                .ENDIF

        next:

        inc hor
        .ENDW
        inc vert
    .ENDW

    ret
ProcessClick endp

Вспомогательные процедуры типа проверки на окончание игры, или смены местами значений в массиве я приводить не буду, т.к. они банальны, а статья и так разрослась. Теперь, когда все готово, в итоге получилось 587 строк в Main.asm и 8192 байта исполняемый файл. Размер екзешника меня приятно порадовал — 8 килобайт это и для прежних времен немного, а сейчас и подавно. Полный код приложения можно увидеть в гитхабе.

В итоге получилась вот такая красота

В итоге получилась вот такая красота

Заключение

Наша игра готова. Мы увидели как это делается в привычной IDE, узнали откуда брать сигнатуры и как вызывать функции WinAPI, поняли что надо сделать чтобы создать полноценное оконное приложение, использовали директивы MASM для упрощения кода. Хоть я никогда и не использовал ассемблер в коммерческой разработке, но интерес к нему был с юных лет. Начиная с изучения ассемблера для Z80, знаменитого Спектрума и его многочисленных клонов. Писать пусть и очень простую, но полноценную игру на ассемблере мне по-настоящему понравилось. Надеюсь читателям тоже было интересно!


Рейтинг 4.63/163:

2 / 2 / 1

Регистрация: 20.10.2015

Сообщений: 244

12.04.2016, 15:50. Показов 30890. Ответов 15


Посоветуйте редактор и компилятор для Assembler на Windows 10. Дали задание в универе, а где работать даже не объяснили. Буду очень благодарен за помощь!



0



Programming

Эксперт

39485 / 9562 / 3019

Регистрация: 12.04.2006

Сообщений: 41,671

Блог

12.04.2016, 15:50

Ответы с готовыми решениями:

Assembler и Windows 7
Столкнулся с проблемой: мы начали изучать асемблер на основе i8086, но при написании своей проги у себя вдома столкнулся с проблемой.
На…

Assembler и Windows 7
Заголовок уже говорит о вопросе — Assembler и Windows 7 на 64-х разрядной шине они вообще совместимы? Если да, то тыкните, пожалуйста,…

Assembler на 64-разрядной Windows 7
Я хочу изучить Assembler, но у меня на компе 64-разрядная винда. само-собой асс не идет, требует 32-разрядную систему. Что можно сделать,…

15

Хитрая блондиночка $)

1472 / 988 / 399

Регистрация: 21.12.2015

Сообщений: 3,785

12.04.2016, 15:58

Смотря что в задании написано.
Вообще FASM хорош. Но мало ли что тебе задали. В Турбодебугере заставят работать и сиди пляши с DosBox



0



2 / 2 / 1

Регистрация: 20.10.2015

Сообщений: 244

12.04.2016, 16:14

 [ТС]

Hikari, В лабе сказано про DosBox, его возможно настроить для виндовс 10?



0



Хитрая блондиночка $)

1472 / 988 / 399

Регистрация: 21.12.2015

Сообщений: 3,785

12.04.2016, 16:20

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

его возможно настроить для виндовс 10?

Сама не пробовала, но думаю проблем не будет.
И раз уж тебе ДОС впарили то настраивайся на Turbo Assembler



0



2 / 2 / 1

Регистрация: 20.10.2015

Сообщений: 244

12.04.2016, 16:35

 [ТС]

Hikari, Вставил код что дан в лабе в Turbo Assembler, выбивает такую ошибку:



0



Хитрая блондиночка $)

1472 / 988 / 399

Регистрация: 21.12.2015

Сообщений: 3,785

12.04.2016, 19:39

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

Вставил код что дан в лабе в Turbo Assemble

Тебе под ДОС нужен ассемблер. А это… под виндовс.
У тебя в методичке написано как в DosBox настраивать ассемблер? Или преподавателям твоим все равно?



0



2 / 2 / 1

Регистрация: 20.10.2015

Сообщений: 244

12.04.2016, 19:42

 [ТС]

Hikari, В методичке ничего не понятно, как и через что запускать нету, вот и не могу разобраться, дос бокс установил, там пишет диск z, как перейти на свой локальный диск не пойму.
Вот, если украинский понимаете моя методичка (4 лаба):
AK_MetLab_2.pdf



0



Max00766

2 / 2 / 1

Регистрация: 20.10.2015

Сообщений: 244

12.04.2016, 19:45

 [ТС]

Hikari, И в задании дано два кода, com и exe, сколько не пробовал никак не получается их скомпилировать
Вот эти кода:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
STACK_SEG   SEGMENT  PARA STACK  'STACK'
DB 100 DUP (?)  
STACK_SEG ENDS  
DATA_SEG SEGMENT 
HELLO DB "Hello world!!!$"
DATA_SEG ENDS  
TEXT_SEG  SEGMENT PARA 'CODE' 
ASSUME CS:TEXT_SEG, SS:STACK_SEG, DS:DATA_SEG, ES:DATA_SEG 
NEW_PROC  PROC NEAR  
BEG_:   
MOV AX, DATA_SEG  
MOV DS, AX  
LEA DX, HELLO  
MOV AH, 09h  
INT 2lh   
MOV AH, 4CH 
INT 21H ; exit to MS-DOS 
NEW_PROG ENDP 
TEXT_SEG  ENDS 
END  BEG_
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CODE_SEG SEGMENT 
ASSUME CS:CODE_SEG, DS:CODE_SEG, SS:CODE_SEG, ES:CODE_SEG 
ORG 100h 
START_:  
JMP BEG_ 
HELLO DB " Здравствуй мир!!!!$" 
NEW_PROC   PROC   NEAR  
BEG_:    
LEA DX, HELLO ; завантажити адресу рядка до DX   
MOV AH, 09h ; занести до АХ номер функції MS-DOS        
; (09h - виведення на екран)  
INT 2lh ; викликати MS-DOS: вивести рядок на екран  
RET  ; вихід до MS-DOS 
NEW_PROC ENDP  
CODE_SEG ENDS  
END START_



0



183 / 121 / 26

Регистрация: 18.05.2015

Сообщений: 509

12.04.2016, 20:52

Max00766, берем: tasm.exe , tlink.exe, rtm.exe, td.exe, dpmi16bi.ovl, vc.com (Volkov Commander)
кидаем все в одну папку + ваши исходники.
Берем мышку за VC.COM и накладываем на DOS-BOX.
Открывается командер в эмуляции дос, пишем лабу и радуемся.

Ну надо понимать что это не чистый дос, например обращения к жесткому диску (int 13h) не прокатят.



1



Хитрая блондиночка $)

1472 / 988 / 399

Регистрация: 21.12.2015

Сообщений: 3,785

13.04.2016, 08:21

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

вот и не могу разобраться, дос бокс установил, там пишет диск z, как перейти на свой локальный диск не пойму.

Как запустить программу с DosBox?



0



3408 / 1827 / 489

Регистрация: 28.02.2015

Сообщений: 3,696

13.04.2016, 09:21

Max00766,
— Качаете tasm,
— распаковываете его в папку, к примеру D:\my\tasm\,
— в эту же папку кидаете свои программы,
— запускаете DOSBox, набираете команду mount d d:\my\tasm,
— набираете D:, теперь Вы уже в нужной папке,
— компилятор будет в папке BIN,
— набираете bin\tasm name
— набираете bin\tlink name
— после этого у Вас должна появится программа ЕХЕ, если нет ошибок в коде,
— для программ СОМ, нужно компилировать с ключом /t
все.



0



7 / 3 / 3

Регистрация: 07.11.2014

Сообщений: 218

Записей в блоге: 1

26.03.2017, 08:50

А можно как-то без досбокса а чисто на винде? Я слышал, что есть нормальный полноценный компилятор с отладчиком, который нормально работает на современных системах, где даже ввод/вывод занесен в подключаемые директивы как процедуры. Работает с 16-32 битным кодом, нормально запускается на 64битных системах. Но я найти такой не могу.



0



Модератор

8603 / 4446 / 1661

Регистрация: 01.02.2015

Сообщений: 13,801

Записей в блоге: 9

26.03.2017, 09:05

Скорее всего речь о Emu8086. http://www.emu8086.com/ Стоимость лицензии ~300руб(российск).

В теме Заданы 2 числа. Определить является ли первое число кубом второго пользователь приводит программу с неизвестными, но работающими процедурами ввода и вывода.

Но сам я никогда не пользовался этой программой.

Добавлено через 1 минуту
Вообще — в вашем учебном заведении должно регламентироваться использование компилятора. В методичке.



1



7 / 3 / 3

Регистрация: 07.11.2014

Сообщений: 218

Записей в блоге: 1

22.04.2017, 10:46

ФедосеевПавел, Спасибо огромное. Хоть готовых функций ввода и вывода нет, но эмулятор очень удобный, все необходимые инструменты под рукой, и только в нем я начал чего-то понимать в программировании на чистом ассемблере. У tasm32 и своего текстового процессора нет с выделением соответствующими цветами участки кода и нумирации строк, и работает там не все, и ошибки непонятные. А тут все понятно, даже таблица ascii и калькулятор есть. За такое мне не жалко 300р отдать. Единственно печалит что это всего лишь эмулятор и на Win10 программы работать не будут



0



Ушел с форума

16355 / 7671 / 1078

Регистрация: 11.11.2010

Сообщений: 13,737

22.04.2017, 10:48

Test video tutorial for Quick Editor



1



Модератор

8603 / 4446 / 1661

Регистрация: 01.02.2015

Сообщений: 13,801

Записей в блоге: 9

22.04.2017, 11:38

Gubila_2000, освоив команды, вы вполне можете взять компилятор для Windows, и создавать рабочие приложения. В разделах есть закреплённые темы, в которых хорошо освещено создание программ для Windows.
Сам себе Iczelion
Создание консольных приложений в 64-разрядной Windows Seven
Уроки Iczelion’a на FASM
Ещё для программирования в Windows отлично помогает книга Чарльза Петзольда «Программирование в Windows». Она для языка C, но для понимания Win32 API — просто идеальна.

За исключением других шаблонов исходников (структуры программы), другого способа вызова API (DOS или Windows), больших размеров регистров — отличий почти нет.

В masm32 есть множество примеров, которые можно использовать, как заготовки.

Не вижу смысла асм программ с GUI интерфейсом, но в консоль вывод не сложнее, чем в DOS.

Редактор. Много копий сломано по этому поводу. Лично я для учебных asm программ на форум использую или SciTE или идущий в комплекте с masm32 редактор qeditor (основная проблема с настройкой путей ко включаемым файлам и библиотекам). Отладку выполняю в OllyDbg, но последнее время на форуме пропагандируют «более новый и лучший» x64dbg.



1



inter-admin

Эксперт

29715 / 6470 / 2152

Регистрация: 06.03.2009

Сообщений: 28,500

Блог

22.04.2017, 11:38

Помогаю со студенческими работами здесь

Блокиратор Windows на Assembler
Короче, задали написать блокиратор виндовс. Код не мой, компилируется, но вылетает. Что делать не знаю. Компилирую масм32. Какие могут быть…

Обучение Assembler на Windows х64
С чего начать? Большинство учебников вводят в assembler с 16-разрядного программирования, но коль скоро оно не доступно на х64……

Assembler/windows/dos/tasm/emu8086
Записывает в текстовый файл символ, воспринимая информацию введенную в него как аски код того символа. Что делать?

компилятор assembler для windows 7 и работает ли visual C++ Express 2010 c asembler-ом
Какие компиляторы asembler будут работать на windows 7 x64?Так же в инете я нашол, что visual C++ поддерживает asmbler, но в примере был…

Assembler для Windows
Ребят, таков вопрос, раньше немного писал на ассемблере на win.xp, сейчас есть возможность писать только на win.8.1 64-bit, но насколько я…

Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:

16

Новые блоги и статьи

Все статьи  

Все блоги / 

POCO, ACE, Loki и другие продвинутые C++ библиотеки

NullReferenced 13.05.2025

В C++ разработки существует такое обилие библиотек, что порой кажется, будто ты заблудился в дремучем лесу. И среди этого многообразия POCO (Portable Components) – как маяк для тех, кто ищет. . .

Паттерны проектирования GoF на C#

UnmanagedCoder 13.05.2025

Вы наверняка сталкивались с ситуациями, когда код разрастается до неприличных размеров, а его поддержка становится настоящим испытанием. Именно в такие моменты на помощь приходят паттерны Gang of. . .

Создаем CLI приложение на Python с Prompt Toolkit

py-thonny 13.05.2025

Современные командные интерфейсы давно перестали быть черно-белыми текстовыми программами, которые многие помнят по старым операционным системам. CLI сегодня – это мощные, интуитивные и даже. . .

Конвейеры ETL с Apache Airflow и Python

AI_Generated 13.05.2025

ETL-конвейеры – это набор процессов, отвечающих за извлечение данных из различных источников (Extract), их преобразование в нужный формат (Transform) и загрузку в целевое хранилище (Load). . . .

Выполнение асинхронных задач в Python с asyncio

py-thonny 12.05.2025

Современный мир программирования похож на оживлённый мегаполис – тысячи процессов одновременно требуют внимания, ресурсов и времени. В этих джунглях операций возникают ситуации, когда программа. . .

Работа с gRPC сервисами на C#

UnmanagedCoder 12.05.2025

gRPC (Google Remote Procedure Call) — открытый высокопроизводительный RPC-фреймворк, изначально разработанный компанией Google. Он отличается от традиционых REST-сервисов как минимум тем, что. . .

CQRS (Command Query Responsibility Segregation) на Java

Javaican 12.05.2025

CQRS — Command Query Responsibility Segregation, или разделение ответственности команд и запросов. Суть этого архитектурного паттерна проста: операции чтения данных (запросы) отделяются от операций. . .

Шаблоны и приёмы реализации DDD на C#

stackOverflow 12.05.2025

Когда я впервые погрузился в мир Domain-Driven Design, мне показалось, что это очередная модная методология, которая скоро канет в лету. Однако годы практики убедили меня в обратном. DDD — не просто. . .

Исследование рантаймов контейнеров Docker, containerd и rkt

Mr. Docker 11.05.2025

Когда мы говорим о контейнерных рантаймах, мы обсуждаем программные компоненты, отвечающие за исполнение контейнеризованных приложений. Это тот слой, который берет образ контейнера и превращает его в. . .

Micronaut и GraalVM — будущее микросервисов на Java?

Javaican 11.05.2025

Облачные вычисления безжалостно обнажили ахиллесову пяту Java — прожорливость к ресурсам и медлительный старт приложений. Традиционные фреймворки, годами радовавшие корпоративных разработчиков своей. . .

Наверх

Надумал я тут написать небольшую утилиту.. и понял, что писать-то я и не умею. Смеялись всем селом!! :) Если рассматривать вопрос по существу, то сегодня мы раскроем некоторые особенности синтаксиса языка ассемблер в свете использования компилятора FASM, и приведем типовой шаблон оконного приложения на ассемблере, а так же выполним разбор структуры для дальнейшего использования в качестве базиса в различного рода проектах. Быть может, когда-то статья и станет звеном в цикле по изучению программирования на языке Ассемблер под Windows, но на данный момент она представляет собой обособленный материал.

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

Я попытался до определенной степени детализировать небольшой накопленный опыт, дабы читатель любого уровня подготовки смог увидеть весь диапазон направлений, требуемых для более глубокого изучения особенностей языка Ассемблера при разработке приложений под операционную систему Windows, если появится желание дальнейшего продвижения. Будут рассмотрены основные (базовые) директивы ассемблера FASM, которые позволяют существенно влиять на структуру исполняемого файла программы. Некоторые из приведенных разделов вполне могли бы дорасти до размера самостоятельной статьи, однако пока подобная структура не создана, информация будет приводиться здесь. Темой данной статьи станет создание простейшего графического оконного приложения на ассемблере для Windows, потому как данная категория приложений является наиболее распространенной, соответственно и востребованной.

Оконное приложение Windows — это класс приложений, использующих для взаимодействия с пользователем элементы графического пользовательского интерфейса, то есть объекты типа: окна, кнопки, поля ввода, элементы контроля и многие другие. При помощи устройств ввода (клавиатуру/мышь/тачпад и прочие), пользователь имеет возможность взаимодействовать с объектами оконного приложения: перемещать, активировать, прокручивать. Примером данного класса являются классические графические приложения, работающие с окнами.

За основу для изучения я взял стандартный шаблон 32-битного оконного приложения на ассемблере с именем template.asm, поставляемый в составе пакета FASM и размещающийся в поддиректории \EXAMPLES\TEMPLATE\, и слегка модифицировал его для некоторой наглядности. Для начала представим исходный код программы:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

format          PE GUI 4.0                                                      ; Формат PE. Версия GUI 4.0.

entry           start                                                           ; Точка входа

include         ‘%fasminc%\win32a.inc’                                          ; Делаем стандартное включение описателей.

_style           equ             WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU              ; Стили окна. ЭКВИВАЛЕНТЫ должны задаваться ДО основного кода

;=== сегмент кода ============================================================

section         ‘.text’ code readable executable

  start:

                invoke          GetModuleHandle,0                               ; Получим дескриптор приложения.

                mov             [wc.hInstance],eax                              ; Сохраним дескриптор приложения в поле структуры окна (wc)

                invoke          LoadIcon,0,IDI_ASTERISK                         ; Загружаем стандартную иконку IDI_ASTERISK

                mov             [wc.hIcon],eax                                  ; Сохраним дескриптор иконки в поле структуры окна (wc)

                invoke          LoadCursor,0,IDC_ARROW                          ; Загружаем стандартный курсор IDC_ARROW

                mov             [wc.hCursor],eax                                ; Сохраним дескриптор курсора в поле структуры окна (wc)

                mov             [wc.lpfnWndProc],WindowProc                     ; Зададим указатель на нашу процедуру обработки окна

                mov             [wc.lpszClassName],_class                       ; Зададим имя класса окна

                mov             [wc.hbrBackground],COLOR_WINDOW+1               ; Зададим цвет кисти

                invoke          RegisterClass,wc                                ; Зарегистрируем наш класс окна

                test            eax,eax                                         ; Проверим на ошибку (eax=0).

                jz              error                                           ; Если 0, то ошибка — прыгаем на error.

                invoke          CreateWindowEx,0,_class,_title,_style,128,128,256,192,NULL,NULL,[wc.hInstance],NULL  ; Создадим экземпляр окна на основе зарегистрированного класса. в eax возвращает дескриптор окна.

                test            eax,eax                                         ; Проверим на ошибку (eax=0).

                jz              error                                           ; Если 0, то ошибка — прыгаем на error.

                mov             [wHMain],eax                                    ; сохраним дескриптор созданного окна

;— цикл обработки сообщений ————————————————

  msg_loop:

                invoke          GetMessage,msg,NULL,0,0                         ; Получаем сообщение из очереди сообщений приложения

                or              eax,eax                                         ; Сравнивает eax с 0

                jz              end_loop                                        ; Если 0 то пришло сообщение WM_QUIT — выходим из цикла ожидания сообщений, если не 0 — продолжаем обрабатывать очередь

  msg_loop_2:

                invoke          TranslateMessage,msg                            ; Дополнительная функция обработки сообщения. Конвертирует сообщения клавиатуры отправляет их обратно в очередь.

                invoke          DispatchMessage,msg                             ; Пересылает сообщения соответствующим процедурам обработки сообщений (WindowProc …).

                jmp             short msg_loop                                  ; Зацикливаемся

  error:

                invoke          MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK  ; Выводим окно с ошибкой

  end_loop:

                invoke          ExitProcess,[msg.wParam]                        ; Выход из программы.

;— процедура обработки сообщений окна (функция окна, оконная процедура, оконная функция)

proc            WindowProc      hWnd,wMsg,wParam,lParam

                push            ebx esi edi                                     ; сохраним все регистры

                cmp             [wMsg],WM_DESTROY                               ; Проверим на WM_DESTROY

                je              .wmdestroy                                      ; на обработчик wmdestroy

                cmp             [wMsg],WM_CREATE                                ; Проверим на WM_CREATE

                je              .wmcreate                                       ; на обработчик wmcreate

  .defwndproc:

                invoke          DefWindowProc,[hWnd],[wMsg],[wParam],[lParam]   ; Функция по умолчанию. Обрабатывает все сообщения, которые обрабатывает наш цикл.

                jmp             .finish

  .wmcreate:

                xor             eax,eax

                jmp             .finish

  .wmdestroy:                                                                   ; Обработчик сообщения WM_DESTROY. Обязателен.

                invoke          PostQuitMessage,0                               ; Посылает сообщение WM_QUIT в очередь сообщений, что вынуждает GetMessage вернуть 0. Посылается для выхода из программы. Посылается только основным окном.

                xor             eax,eax                                         ; Если наша процедура окна обрабатывает какое-либо сообщение, то она должна вернуть в eax 0. Иначе программа поведет себя непредсказуемо.

  .finish:

                pop             edi esi ebx                                     ; восстановим все регистры

                ret

endp

;=== сегмент данных ==========================================================

section         ‘.data’ data readable writeable

_class          db              ‘FASMWIN32’,0                                   ; Название собственного класса.

_title          db              ‘Win32 program template’,0                      ; Текст в заголовке окна.

_error          db              ‘Startup failed.’,0                             ; Текст ошибки

wHMain          dd              ?                                               ; дескриптор окна

wc              WNDCLASS                                                        ; Структура окна. Для функции RegisterClass

msg             MSG                                                             ; Структура системного сообщения, которое система посылает нашей программе.

;=== таблица импорта =========================================================

section         ‘.idata’ import data readable writeable

library         kernel32,‘KERNEL32.DLL’,user32,‘USER32.DLL’

import kernel32,\

GetModuleHandle,‘GetModuleHandleA’,\

ExitProcess,‘ExitProcess’

import user32,\

LoadIcon,‘LoadIconA’,\

LoadCursor,‘LoadCursorA’,\

RegisterClass,‘RegisterClassA’,\

CreateWindowEx,‘CreateWindowExA’,\

GetMessage,‘GetMessageA’,\

TranslateMessage,‘TranslateMessage’,\

DispatchMessage,‘DispatchMessageA’,\

MessageBox,‘MessageBoxA’,\

DefWindowProc,‘DefWindowProcA’,\

PostQuitMessage,‘PostQuitMessage’

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

Идеология программирования под Windows

В операционной системе MS-DOS исполняемая программа (читать: код) имела монопольный (бесконтрольный) доступ к большинству аппаратных ресурсов системы, поэтому говорить о полноценной многозадачности не приходилось. Другой особенностью программ, работающих под MS-DOS, была необходимость самостоятельно инициировать взаимодействие с операционной системой: делать вызовы функций [программных прерываний] с целью организации обмена данными с пользователем/операционной системой/устройствами (например: ожидание ввода с клавиатуры). Но вот в Windows все изменилось, мало того, что в системе могут уже одновременно функционировать несколько процессов (читать: программ), но и для взаимодействия с пользователем (обмена данными), коду пользовательского приложения уже вовсе не обязательно делать вызовов каких бы то ни было специализированных функций, ожидающих ввода (нажатия клавиш клавиатуры/мыши, ввод символов в поле ввода). Объясняется всё это сменой парадигмы программирования на событийно-ориентированное программирование.

Событиийно-ориентиированное программиирование (англ.: event-driven programming) — подход к программированию, при котором ход выполнения программы определяется [внешними/внутренними] событиями: действиями пользователя (клавиатура, мышь, сенсорный экран и прч.), сообщениями других программ и потоков, событиями операционной системы (например, поступлением сетевого пакета).

Соответственно:

Приложение в Windows «пассивно», поскольку в ходе функционирования оно ждет когда операционная система уделит ей внимание.

Подход к программированию в среде Windows существенно изменился и теперь основная концепция программирования ориентирована на так называемые события. Это означает, что ядро системы постоянно следит за аппаратной/программной активностью, и в ответ [на эту активность] генерирует специальные сообщения, которые затем, в зависимости от необходимости, передает (через специальные системные механизмы) приложениям, ожидающим наступления этих событий. Таким образом, можно утверждать что приложения в Windows управляются событиями. С событиями ассоциируется любая пользовательская/системная активность: перемещение окон, нажатие клавиш клавиатуры/мыши, изменения состояния буфера обмена, изменения в аппаратурной конфигурации, изменение статуса энергопотребления, изменение значений таймеров и другая. Поэтому, когда происходит какое-либо событие (к коим относятся и любые действия пользователя), система сама «предоставляет» для пользовательского приложения входные данные, и делает она это посредством передачи сообщений. А прикладная программа, в свою очередь, должна содержать обработчики этих самых поступающих в неё сообщений, которые требуемым образом реагируют на данные сообщения.

Таким образом приложение в Windows должно обеспечивать ту или иную обработку при поступлении разного рода сообщений. Какие именно события обрабатывать и как именно — решает разработчик приложения.

При завершении программой обработки события, управление возвращается [обратно] ядру системы. Вы уже поняли, что все это коренным образом меняет подход к написанию программ, поскольку в MSDOS программа контролировала собственные действия/события, теперь же сама операционная система Windows проводит большинство работы, а пользовательской программе передает лишь управляющие сообщения, которые могут активизировать те или иные процедуры обработки (которые посчитал нужным добавить в код автор) в программе.

Программирование под Windows — это, в основе своей, программирование обработчиков сообщений, то есть реакции приложения на необходимый набор внешних событий.

Но помимо обработки ввода или реакции на иные входящие системные сообщения, прикладной программе требуется выполнять и некоторые другие действия над объектами операционной системы. С целью обеспечения доступа пользовательских программ ко всему спектру исполняемых компонентов Windows, предоставляется так называемый программный интерфейс приложений (API). А это означает, что весь функционал операционной системы доступен через функции, и чтобы программисту что-либо сделать — надо вызвать функцию соответствующего назначения.

API (Интерфейс прикладного программирования, Application Programming Interface) — многообразие системных функций, посредством которых приложение (процесс) может взаимодействовать с операционной системой Windows.

Заголовок

Рассмотрение исходного кода начнем мы с заголовка, или, если выразиться более точно — «так называемого заголовка». Я возьму на себя смелость подобным образом именовать область исходного кода, начинающуюся непосредственно с первого символа и идущую до директивы объявления первой секции. Начало области не обозначается специальными директивами, это просто начальная часть листинга программы. В этом месте (у нас строка 1) может использоваться директива format, которая предназначается для указания формата результирующего исполняемого файла, получаемого на выходе после компиляции. Форматы можно указать следующие:

Имя Расшифровка Описание
MZ Mark Zbikowski формат 16-битных исполняемых файлов с расширением .exe для ОС MSDOS
PE PE64 Portable Executable формат 32/64-битных исполняемых файлов с расширением .exe для ОС Windows
COFF MS COFF MS64 COFF Common Object File Format формат объектного файла, содержащий промежуточное представление кода программы, предназначенный для объединения с другими объектными файлами (проектами/ресурсами) с целью получения готового исполнимого модуля.
ELF ELF64 Executable and Linkable Format формат исполняемых файлов систем семейства UNIX. Объектный файл (.obj) для компилятора gcc.
ARM Advanced RISC Machine формат исполняемых файлов под архитектуру ARM (?)
Binary файлы бинарной структуры. Что зададите, то и соберется. Например, выставив смещение 100h (org 100h) от начала, можно получить старый-добрый .com-файл под MSDOS. Формат имеет ряд аналогичных применений для создания произвольных бинарных приложений или файлов данных.

Вторым параметром (после указания формата исполняемого файла) директивы format может указываться тип подсистемы для создаваемого приложения:

Имя Описание
GUI Графическое (оконное) приложение. Выходной исполняемый файл, который подразумевает создание типовых оконных приложений и инициализацию на начальной стадии всех соответствующих библиотек Win32 API. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значение поля Subsystem = 2 (оно же IMAGE_SUBSYSTEM_WINDOWS_GUI).
console Консольное приложение. Выходной исполняемый файл, подразумевающий выполнения кода в консоли, без участия оконного интерфейса. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значение поля Subsystem = 3 (оно же IMAGE_SUBSYSTEM_WINDOWS_CUI).
native Родное/нативное приложение. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значение поля Subsystem = 1 (оно же IMAGE_SUBSYSTEM_NATIVE). Подобное значение поля обычно характерно для драйверов, библиотек и приложений режима ядра, которым не требуется инициализация подсистемы Win32 на стадии подготовки образа к выполнению.
DLL Динамическая библиотека. Особый формат выходного исполняемого файла, предназначающийся для экспорта (предоставления) функций сторонним приложениям, у которого в структуре PE-заголовка IMAGE_NT_HEADERS, подструктуре IMAGE_FILE_HEADER, в поле Characteristics включен флаг IMAGE_FILE_DLL (2000h).
WDM Системный драйвер, построенный на основе модели WDM (Windows Driver Model).
EFI EFIboot EFIruntime UEFI-приложение. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значение поля Subsystem = 10 | 11 | 12 | 13 (оно же IMAGE_SUBSYSTEM_EFI_APPLICATION, IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER, IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER, IMAGE_SUBSYSTEM_EFI_ROM). Подобное значение поля требуется для создания UEFI-приложений различных стадии/типа: загрузки, выполнения и драйвера.

Роль данного параметра достаточно велика, поскольку именно он определяет, какая именно подсистема будет вызываться для запуска исполняемого файла, то есть фактически определяет программное окружение при запуске процесса. Если используется тип приложения GUI, необходимо уточнять минимальную версию системы (у нас: 4.0), под которую создается наш исполняемый модуль.
Затем, в строке под номером 2 в нашем исходном коде располагается директива с именем entry, которая определяет точку входа в программу.

Точка входа — адрес первой инструкции (в адресном пространстве процесса), с которой начинается выполнение кода приложения.

В качестве аргумента директивы entry указывается метка в коде, с которой у нас начнется выполнение кода скомпилированной программы. Становится очевидным, что именно на основе данной директивы компилятор формирует значения соответствующих полей результирующего исполняемого PE-файла. При запуске .exe-файла, загрузчик образов (динамический компоновщик) создаст адресное пространство процесса нашего приложения, подгрузит и разберет исполняемый образ, сопоставив все необходимые сегменты с регионами памяти, сформировав иные необходимые структуры, передаст управление именно по адресу, где будет располагаться инструкция, описанная в исходном коде меткой, указанной в директиве entry. В нашем случае точку входа определяет метка start, располагающаяся в сегменте кода в строке 11.
В строке 3 мы обнаруживаем директиву компилятора include, при помощи которой в исходный код нашей программы (в позицию нахождения директивы) включается текст внешнего модуля (файла), указанного в ней в качестве параметра.

Включение позволяет подключать необходимые программе структуры данных из внешних файлов. Основной файл у нас содержит код программы, а специфичные внешние данные, такие как системные константы, переменные и определения макросов, размещаются в отдельных заголовочных файлах. Заголовочные файлы FASM несколько отличаются от привычных нам по языку C/C++ тем, что не описывают прототипов процедур/функций.

В нашем случае подключается файл %fasminc%\win32a.inc, который, в свою очередь, содержит ссылки на другие подключаемые файлы, содержащие определения ключевых структур, требуемых для компиляции нашей программы: макросов, типов данных, констант, системных структур. Без включения этого файла у нас попросту не пройдет процесс компиляции нашего исходного кода, то есть исполняемый файл не будет создан (не соберется).

Как Вы уже наверное заметили, в параметре директивы include используется переменная пути %fasminc%. В общем случае она создается для удобства указания пути к поддиректории \INCLUDE основной директории FASM в исходниках. Вам тоже необходимо задать полный путь к каталогу дистрибутива в настройках операционной системы: окно Свойства системы -> вкладка Дополнительно -> раздел Переменные среды -> добавить новый параметр с именем FASMINC, имеющий значение пути (например: C:\YandexDisk\_Project\ASM\FASM\INCLUDE) в области Переменные среды пользователя или Системные переменные.

Непосредственно за подключением внешнего файла, в строке 5 у нас располагается объявление внутренней константы _style, которая используется в нашем коде и принимает значение WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU, определяющее внешний вид окна. Ключевые слова WS_VISIBLE, WS_DLGFRAME, WS_SYSMENU являются не чем иным, как символическими именами глобальных констант, или битовых флагов (содержащихся во внешних файлах включений, подключаемых на этапе компиляции), определенных в системе Windows и иначе именуемых стилями окна.

Заметьте, что константы объединяются операцией + (логическое/побитовое ИЛИ, or), с целью получить сумму значений нескольких свойств, то есть применить их совокупность.

С возможными вариантами стилей можно ознакомиться на соответствующей странице, описывающей стили окна.

Секции

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

Секция — область (блок) в структуре исполняемого PE-файла, предназначающаяся для разделения всего массива данных приложения на логические части, подразумевающие хранение кода/данных, объединенных единым назначением/методом доступа. В исполняемом файле каждая секция характеризуется собственным именем, смещением в исполняемом файле, виртуальным адресом для копирования содержимого, размером и атрибутами. Все эти параметры определяют способ загрузки секции, способ формирования страниц виртуальной памяти и управления другими параметрами данных на стадии загрузки образа.

Использование секций регламентировано структурой формата исполняемых PE-файлов, используемых в системе Windows. Именно спецификация формата PE определяет требования к наличию определенных структур в исполняемых файлах и предписывает использование тех или иных секции для разделения информационных блоков. Сразу после директивы section в одинарных кавычках (апостроф) задается имя (название) секции и ряд параметров: тип секции, флаги (атрибуты) секции.

Наименование секции может быть произвольным (но не более 8 символов) либо отсутствовать вовсе, это никак не сказывается на процессе компиляции и исполнения программы, поскольку ключевым для компилятора является тип секции. Исключением является, разве что, секция ресурсов с именем .rsrc.

Флаги могут принимать следующие значения: code, data, readable, writeable, executable, shareable, discardable, notpageable, в дополнение к ним могут использоваться спецификаторы секции данных, такие как export, import, resource, fixups, которые определяют структуру (строение) секции. Типы секций, флаги и их комбинации я свел в таблицу:

Наименование Обозначение FASM Описание
Секция кода code Секция, в которой предписывается размещать исполняемый код приложения. Обычно в данную секцию включается весь ассемблерный код, фактически реализующий логику работы приложения.
Секция данных data В данной секции предписывается размещать все динамические (изменяемые) данные (локальные/глобальные переменные, строки, структуры и т.п.), которые активно используются в коде приложения.
Секция импорта import Расхожее название: Таблица импорта. В данной секции размещаются строковые литералы (наименования) библиотек и таблицы подключаемых (импортируемых) из этих библиотек виртуальных функций, которые требуются нашей программе для работы. Функции могут импортировать по наименованию (символическое имя) или по ординалу (числовой идентификатор).
Секция ресурсов resource Данная секция содержит данные, которые преобразуются в исполняемом файле в многоуровневое двоичное дерево (индексированный массив), построенное определенным образом для ускорения доступа к данным. Эти данные называются ресурсами, доступны из кода через специальные идентификаторы, статичны, описывают различные используемые в программе объекты: меню, диалоги, иконки, курсоры, картинки, звуки и прочее.
Таблица перемещений (таблица настроек адресов, релокации) fixups Релокации — набор таблиц (fixup blocks) со смещениями (Relevant Virtual Addresses, RVA) от базового адреса загрузки образа (фактически указателями на абсолютные адреса в коде), которые загрузчик образа должен скорректировать (исправить) в памяти процесса, если образ загружается по адресу, отличному от предпочитаемого. Иначе (проще) можно представить как список ячеек памяти, которые нуждаются в корректировке при загрузке образа в памяти процесса по произвольному адресу. Таблица перемещений применяется только для фиксированных адресов в коде приложения, то есть адресов тех инструкций, которые компилятор задал в явном виде (например: mov al, [01698745]).
Таблица экспорта export Секция описывает экспортируемые нашей программой функции. Обычно используется при создании библиотек DLL.

Обычно тип секции предписывает размещать внутри неё код/данные требуемого назначения.

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

Секция кода (code)

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

  • Получаем дескриптор экземпляра текущего процесса (в контексте которого и выполняется наш код);
  • Регистрируем класс окна. Регистрация собственного класса требуется во всех случаях за исключением тех, когда Вы используете стандартные (предопределенные, предоставляемые системой) типы окон;
  • Создаем главное (и единственное) окно на основе только что зарегистрированного класса и сообщаем Windows адрес функции обработки событий для этого окна;
  • В нашем примере не используется: отображение основного окна (функция ShowWindow) и обновление клиентской области окна (функция UpdateWindow) в промежутке между созданием окна и началом очереди обработки сообщений программы. В примерах кода от MS часто можно встретить связку данных функций, вероятно они используются в случаях, когда: 1) требуется дополнительная перерисовка (в случае манипуляций с видимой частью окна) клиентской области в промежутке между созданием окна и началом обработки очереди сообщений программы, 2) когда окно создается с классическими стилями, не отображающими окно. В нашем же случае мы используем дополнительные стили, которые сразу делают окно видимым.
  • Входим в бесконечный цикл обработки сообщений для всех окон, принадлежащих нашему процессу. В данном примере обрабатываются сообщения к единственному [основному] окну;
  • (специальной функцией) обрабатываем сообщения, поступающие для любого из контролируемых нами окон;
  • Выходим из программы по нажатию пользователем кнопки Закрыть [X] или комбинации клавиш Alt+F4;

Визуальным результатом работы нашей программы является вывод на рабочий стол обычного окна с единственной системной кнопкой (закрыть) в правом верхнем углу.

Соответственно, вся логика нашей программы укладывается в создание окна и обработку нажатия в нем одной-единственной кнопки: выход. Так же, в окне можно увидеть выбранную нами типовую иконку (левый верхний угол) и окно имеет заданные нами размеры.
Ну а теперь самое время разобраться с алгоритмом работы. Перво-наперво мы получаем дескриптор (handle) нашего модуля при помощи вызова функции GetModuleHandle. Немного оторвемся от изучения логики и обратим внимание на строку 12 вызова данной функции, тут мы впервые встречаемся с ключевым словом invoke. Во с этого самого момента для новичков начинается знакомство с реалиями современного программирования под Windows на языке ассемблер. Для людей, которые разбираются с языком даже на начальном уровне, очевидно, что такой команды в ассемблере нет, но это и не команда, это макрос. Макрос invoke содержится в файлах определения макросов \INCLUDE\MACRO\PROC32.INC и \INCLUDE\MACRO\PROC64.INC пакета FASM и вот его объявление:

macro invoke proc,[arg] ; indirectly call STDCALL procedure

{ common

    if ~ arg eq

   reverse

     pushd arg

   common

    end if

    call [proc] }

Из алгоритма макроса видно, что при наличии аргументов функции, выполняется помещение их в стек в обратном порядке, следом уже вызывается сама функция. И зачем нам это всё нужно? Для облегчения процесса разработки приложения! В принципе, Вы можете и не использовать макрос invoke, но тогда от Вас потребуется масса дополнительной работы: Вам придется самостоятельно заносить входные аргументы в стек в порядке, определенном для той или иной функции. Дело в том, что порядок занесения определяется так называемым соглашением о вызовах, которое повсеместно применяется в программах Windows и которое предписывает заносить параметры в стек строго в требуемом порядке.
Вернемся обратно к основному алгоритму. В официальной документации сказано, что функция GetModuleHandle возвращает дескриптор приложения. Если в качестве входного параметра функции GetModuleHandle используется значение 0, то функция возвращает дескриптор для исполняемого образа, участвующего в создании вызывающего её процесса, то есть (проще) для того процесса, из которого она вызвана. Возвращаемый функцией дескриптор не глобальный и не наследуемый, он актуален в контексте только текущего процесса.

Дескриптор (описатель, handle) — косвенный (абстрактный, виртуальный, локальный) указатель на системный ресурс (окно, элемент управления, курсор, иконка, адрес памяти, открытый файл, канал и тому подобное). В большинстве случаев это всего-лишь абстракция, позволяющая предотвратить непосредственный доступ приложений к системным структурам данных. Она скрывает от разработчика некий физический ресурс, позволяя ядру реорганизовать аппаратные ресурсы удобным ему образом, абсолютно прозрачно для приложений. Фактически это своеобразный индекс (используемый на входе/выходе функций), преобразуемый внутри ядра на основе специальных системных таблиц соответствия во внутреннее представление — указатель непосредственно на физический объект. Поэтому дескриптор должен рассматриваться исключительно в качестве локального значения, имеющего смысл в контексте API в пределах текущего приложения. Не использовать подобный механизм мы не можем, поскольку тогда лишимся связывания различных структур системы друг с другом. Все объекты (окна, файлы, процессы, потоки, события и т.д.) и системные ресурсы в Windows описываются с помощью дескрипторов.

Далее у нас следует блок кода, который отвечает за регистрацию класса окна. Тут у нас впервые в коде появляется структура wc (будет подробно описана в секции данных), которая описывает все необходимые параметры будущего окна, поэтому все члены (поля) этой структуры должны быть предварительно инициализированы. Например, функция GetModuleHandle возвращает дескриптор приложения (процесса) в регистре eax, и мы сохраняем его в wc.hInstance (строка 13), тем самым инициализируя член hInstance структуры wc. Затем мы загружаем иконку при помощи функции LoadIcon и инициализируем дескриптор иконки для будущего окна wc.hIcon. Можно использовать пользовательскую иконку, определённую в секции ресурсов, но обычно, для сокращения кода и упрощения логики, используют один из типовых значков, именно так и сделано в нашей программе. Затем загружаем курсор при помощи функции LoadCursor и инициализируем дескриптор курсора wc.hCursor будущего окна. Опять же, тут каждый волен задавать собственный пользовательский курсор, либо использовать стандартный предопределенный. Обратите внимание на то, что в структуре wc имеется член lpfnWndProc, в который мы записываем адрес начала процедуры обработки событий окна WindowProc, о которой будет рассказано далее. Еще инициализируется дескриптор кисти фона окна hbrBackground. Вызовы всех этих процедур нам необходимы для заполнения структуры wc, используемой в дальнейшем для регистрации класса нашего окна при помощи функции RegisterClass.

Класс окна — набор свойств и методов (спецификация), определяющий требуемые параметры (куpсоp, иконка, адрес функции обработки сообщений окна и прочие), в соответствии с которыми будут создаваться окна в нашем приложении.

В нашем примере используется собственный (пользовательский) класс окна. Однако, в операционной системе Windows для разработчика предлагается и несколько предопределенных классов окон, которые могут быть использованы в приложении через определенные ключевые слова. Для чего вообще нужна регистрация некоего класса окна, почему нельзя создать сразу непосредственно само окно? Ответ дает концепция объектно-ориентированного программирования, и класс в рамках концепции существует для того, чтобы задать все параметры будущего окна в одной точке кода, ведь если Вы на основе одного-единственного класса будете создавать множество окон, то подобная стратегия полностью себя оправдает. Отсюда следует вывод:

Операционная система выводит на экран и обслуживает окна только зарегистрированных классов. Соответственно, что бы система узнала о вашем собственном пользовательском классе — его необходимо зарегистрировать.

После регистрации класса окна, при помощи системной функции CreateWindowEx, мы создаём экземпляр окна зарегистрированного ранее класса. Как Вы уже заметили, в нашей программе вместо типовой функции CreateWindow используется расширенная версия функции по имени CreateWindowEx, отличающаяся от типовой поддержкой дополнительных стилей окна (параметр dwExStyle). В качестве входных параметров для функции Вы должны указывать некоторое множество параметров:

Наименование Тип в ASM Тип в C Описание
dwExStyle DD DWORD Расширенный стиль описания создаваемого окна. Содержит разнообразные украшательства, которые не входят в основное описание стиля dwStyle.Список возможных значений можно посмотреть в Extended Window Styles.
lpClassName DD LPCTSTR Указатель (адрес) на строку с именем класса окна. В нашем случае используется строка _class.
lpWindowName DD LPCTSTR Указатель на строку с именем окна, отображаемым в заголовке окна. В нашем случае используется строка _title.
dwStyle DD DWORD Константа, определяющая стиль окна. В нашем случае используется константа _style, содержащая битовые флаги.
x DD int Горизонтальная координата (X) левого верхнего угла окна. В координатах экрана.
y DD int Вертикальная координата (Y) левого верхнего угла окна. В координатах экрана.
nWidth DD int Ширина окна в пикселях.
nHeight DD HCURSOR Высота окна в пикселях.
hWndParent DD HWND Дескриптор родительского (порождающего) окна.
hMenu DD HMENU Дескриптор меню, используемого окном. В случае, если окно основано на предопределенном системном классе окна, оно не может содержать меню, тогда параметр используется как идентификатор дочернего элемента управления.
hInstance DD HINSTANCE Дескриптор приложения (модуля), создающего данное окно.
lpParam DD LPVOID Опциональный указатель на дополнительную структуру данных (CREATESTRUCT), посылаемых окну. Если параметр задан, то в первом сообщении WM_CREATE параметр lParam указывает на требуемую структуру. Или же данный указатель может принимать значение NULL, сообщая, что никаких данных посредством функции CreateWindow не передается.

Непосредственно после вызова функции CreateWindowEx наше окно появляется на экране. Всё, окно выведено. Но сам по себе факт отрисовки окна нам мало что дает, окно должно функционировать. Для этого нам необходимо обеспечить получение информации от пользователя/системы (пользовательский/системный ввод) и делается это при помощи очереди сообщений.

Очередь сообщений

Давайте сделаем небольшое отступление и поговорим об одной из фундаментальных основ большинства приложений операционной системы Windows: очереди сообщений. Дело в том, что событийно-ориентированное программирование, о котором мы упоминали выше, предполагает, что главный цикл программы (приложения) должен состоять как минимум из двух частей: процедуры выборки событий и процедуры обработки событий (события/данные поступают в виде сообщений).

Сообщения — один из основных механизмов операционной системы Windows, обеспечивающий взаимодействие между различными процессами и объектами в пределах операционной системы.

Как уже отмечалось, процессы в операционной системе обмениваются между собой сообщениями, которые представляют из себя предопределенные константы, однозначно характеризующие произошедшее событие. Каждый раз, когда для нашего процесса имеются входные данные (информация, действия пользователя: сообщения от клавиатуры/мыши и прч.), ядро передает ей эти данные в виде сообщений, помещая их в очередь сообщений (message queue) того или иного потока (зачастую единственного) в рамках процесса.

Операционная система Windows поддерживает очередь сообщений для каждой программы (функционирующей в данный момент в виде процесса).

То есть ядро передает события приложению в форме сообщения, помещая их в очередь того программного потока, которому принадлежит окно, над которым в данный момент проводятся те или иные действия, являющиеся источниками событий. Ядро может генерировать сообщения не только в ответ на какие-то глобальные изменения в системе, инициированные данным приложением (изменение пула ресурсов системных шрифтов, изменение размера одного из своих окон и прч), но и по действиям над объектами оконного интерфейса (окна, дочерние элементы), принадлежащими процессу приложения. Чтобы не было путаницы с пониманием типов и целей сообщений, надо уяснить, что сообщения бывают:

  • Синхронные (поставленные в очередь, queued): сообщения, поступающие в основную очередь сообщений потока (которому принадлежит окно), а затем уже, в зависимости от назначения, обслуживаемые в основной очереди, либо диспетчеризируемые в соответствующую процедуру обработки сообщений окна (оконную процедуру);
  • Асинхронные (не поставленные в очередь, nonqueued): сообщения, поступающие напрямую в процедуру WindowProc соответствующего окна, минуя очередь сообщений потока;

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

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

Например, существует сообщение WM_PAINT, которое предписывает целевому окну отрисовать собственное содержимое. Как видно из наименования WM_PAINT, символическое значение содержит в своем имени префикс (WM), указывающий на его категорию. Символьные значения сообщений (WM_CREATE, WM_DESTROY, WM_PAINT и прч.) определены в файлах стандартных включений \include\equates\user32.inc / \include\equates\user64.inc пакета FASM. Это сделано по аналогии со стандартными файлах заголовков Windows (windows.h и прочие), которые включаются в программы C/C++.
В каждой программе определяется цикл обработки сообщений (message loop), который призван обрабатывать (выбирать/транслировать/диспетчеризировать) поступающие приложению сообщения. В нашем примере, в начале этого цикла функция GetMessage проверяет, есть ли какие-либо сообщения от операционной системы. Особенностью данной функции является то, что она не возвращает управление в вызывающую программу, пока не появится какое-нибудь сообщение, затем извлекает сообщение из очереди сообщений потока и помещает его в структуру с именем msg типа MSG. Как мы видим, параметр hWnd (второй параметр) для функции установлен в ноль (NULL), поэтому извлекаются все сообщения, адресованные любому окну, ассоциированного с текущим потоком, и любые сообщения для текущего потока, чьи hwnd равны нулю (NULL). Таким образом, если hWnd равно нулю, и оконные сообщения и сообщения потока обрабатываются.

Традиционно для Windows-функций, результат выполнения кода функции возвращается в регистре eax.

Поэтому в нашем коде (строка 35) мы анализируем содержимое данного регистра и если оно равно 0, то это значит, что пришло сообщение WM_QUIT, в случае чего переходим на метку end_loop с последующим выходом. Во всех остальных случаях подразумевается, что пришло сообщение, отличное от WM_QUIT, и его требуется обработать. Обработка начинается с вызова вспомогательной функции TranslateMessage (с аргументом в виде структуры msg), которая предназначена для дополнения (расширения) сообщений клавиатуры WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN и WM_SYSKEYUP сообщениями WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, WM_SYSDEADCHAR, содержащими ASCII-значения нажатых клавиш. Согласитесь, что иметь дело с ASCII-значениями проще, нежели со scan-кодами. Если её исключить из цикла, то вероятно мы не получим символических значений, а будем довольствоваться лишь скан-кодами нажатых в окне клавиш. Затем у нас вызывается функция DispatchMessage (аргументом которой все так же является ссылка на нашу структуру msg), которая отправляет сообщение в процедуру окна, поскольку главное её предназначение разбирать сообщения, извлеченные функцией GetMessage.

Функция DispatchMessage в коде обработки очереди сообщений потока проверяет, для какого именно класса окна предназначено сообщение и вызывает соответствующую оконную процедуру.

Обратите внимание, что тут возникает один тонкий момент: зачем нам фактически две логики разбора очереди через GetMessage и через WindowProc, ведь можно было обойтись одной, зачем нам нужно вызывать еще отдельную процедура обработки сообщений окна, когда можно обработать сообщение в основном цикле? Так то оно так, но как мы уже упоминали, сообщения могут быть синхронными и асинхронными. Синхронные сообщения помещаются в очередь сообщений потока, соответственно извлекаются и диспетчеризируются они в основном цикле обработки сообщений: при помощи GetMessage, а затем могут быть отправлены в оконную процедуру через связку DispatchMessage+WindowProc. Асинхронные сообщения передаются непосредственно окну путем прямого вызова оконной процедуры. Это как, неужели ядро что-то напрямую вызывает в пользовательском коде? Я думаю, что тут всё несколько иначе и «прямой» передачей (асинхронных сообщений) занимается исключительно функция DispatchMessage, потому как данные сообщения не извлекаются из очереди функцией GetMessage? В любом случае, оконная процедура принимает все типы сообщений, адресованные заданному окну: синхронные и асинхронные. Именно поэтому цикл обработки сообщений у нас выглядит так а не иначе.
Ну и, наконец, вся эта логика обработки сообщений завершается в строке 40 командой jmp, которая зацикливает прием и обработку сообщений. Таким образом, если сообщений на данный момент нет, то функция все равно ожидает появления сообщения в бесконечном цикле, выходом из которого является лишь действие по закрытию окна.

Основная логика работы большинства Windows-программы с оконным интерфейсом сводится к работе с сообщениями окна.

Код под меткой error, на которую осуществляет переход из нескольких мест обработки ошибочных ситуаций в нашей программе, служит для обработки критической ситуации, когда функции у нас по каким-либо причинам возвращают ошибку и дальнейшее выполнение кода программы становится бессмысленным. В этом случае мы выдаем окно с ошибкой при помощи функции MessageBox, а затем вызывается функция ExitProcess с аргументом msg.wParam, который содержит код выхода.

В случае [внезапного] завершения цикла обработки сообщений, код выхода хранится в члене wParam структуры msg. Этот код выхода необходимо вернуть ядру операционной системы посредством вызова функции ExitProcess с входным параметром, равным значению msg.wParam.

Тут возникает резонный вопрос, почему метка error располагается в основном цикле обработки сообщений, ведь логичнее было бы её вообще вынести в другое место кода. Да, это действительно так, но тогда бы нам пришлось дублировать выход из программы при помощи функции ExitProcess, а так мы можем использовать уже существующую точку выхода (используемую в цикле), не прибегая к дублированию кода. Исключительно с этой целью логика обработки ошибки встроена в цикл обработки сообщений.

Процедура обработки сообщений окна

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

Оконная (процедура) функция обеспечивает непосредственную обработку нужных нам сообщений: сравнивает идентификаторы и если они относятся к обрабатываемым её классам, то выполняет необходимую работу.

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

Оконная (процедура) функция – функция обратного вызова. Ядро Windows само посылает сообщения окну, что в свою очередь, инициирует вызов сопоставленной оконной процедуры.

Фактически, вызов (опосредовано или напрямую) кодом ядра системы оконной процедуры соответствующего окна, называют «обратным вызовом» или (по-английски) callback’ом. И что такое функция обратного вызова? Это функция, которая вызывается ядром при наступлении определенных условий. В действительности система не может так вот запросто взять и вызвать любую произвольную функцию вашего приложения, вместо этого она предоставляет специальный механизм, посредством которого может вызывать только заранее определенную функцию в вашем пользовательском коде. Вот именно эта функция обратного вызова называется оконной (функцией) процедурой (обычно носящей имя WndProc, в нашем случае WindowProc) и ассоциируется со всеми графическими окнами процесса. Задается адрес функции обратного вызова через специальный член структуры класса окна lpfnWndProc, на этапе регистрации класса. Каждый раз, когда для какого-либо окна нашего процесса или его дочерних элементов (элементы меню, поля, кнопки, радиокнопки, элементы управления и прочее) имеются входные данные (информация, действия пользователя: сообщения от клавиатуры/мыши и прч.), ядро сначала передает сообщение в цикл обработки сообщений, а затем через функцию DispatchMessage опосредованно вызывает соответствующую оконную процедуру и передает ей данные в виде сообщений, которые поступают через входные параметры процедуры. На каждое событие (например, набор пользователем символов с клавиатуры, движение курсора мыши в пределах границ окна, щелканье по элементам управления (кнопка, скролл-бар и прч.)), относящееся к окну, ядро генерирует определенное сообщение. В конкретном примере алгоритм процедуры обрабатывает всего два сообщения: WM_DESTROY и WM_CREATE. Процедура WindowProc предваряется у нас в коде неким ключевым словом proc и получает четыре входных параметра (hWnd,wMsg,wParam,lParam), но что это за proc? А это ни что иное как, опять же, макрос, настраивающий пролог/эпилог вызываемой процедуры. Давайте немного отступим от основной линии повествования и познакомимся с макросом proc поближе:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

macro proc [args] ; define procedure

{ common

    match name params, args>

    \{ define@proc name,<params \} }

prologue@proc equ prologuedef

macro prologuedef procname,flag,parmbytes,localbytes,reglist

{ local loc

   loc = (localbytes+3) and (not 3)

   parmbase@proc equ ebp+8

   localbase@proc equ ebploc

   if parmbytes | localbytes

    push ebp

    mov ebp,esp

    if localbytes

     sub esp,loc

    end if

   end if

   irps reg, reglist \{ push reg \} }

epilogue@proc equ epiloguedef

macro epiloguedef procname,flag,parmbytes,localbytes,reglist

{ irps reg, reglist \{ reverse pop reg \}

   if parmbytes | localbytes

    leave

   end if

   if flag and 10000b

    retn

   else

    retn parmbytes

   end if }

close@proc equ

Структура данного макроса лишний раз говорит за то, что в FASM реализован очень продвинутый макроязык. Ведь данным макросом, компилятор фактически «настраивает» любую процедуру, описываемую при помощи ключевого слова proc. И настройка эта состоит в автоматическом создании пролога и эпилога процедуры, настройке стекового фрейма (кадра), резервации места в стеке под локальные переменные, восстановлении стека в эпилоге, сохранении/восстановление регистров общего назначения. Все эти подготовительные действия при входе в процедуру и выходе из неё, считаются типовыми и используются уже давно в компиляторах языков различных уровней. В отсутствии данного макроса программисту пришлось бы писать весь «обвес» процедуры самостоятельно, затрачивая на это драгоценное время, либо тратя его на то, чтобы самостоятельно создавать подобные облегчающие программирование макросы. Поверьте, совокупность подобных (с виду незначительных) автоматизаций серьёзно облегчает работу разработчика. Поэтому, хочу отдельно отметить неоспоримые достоинства макроязыка FASM, поскольку именно с его помощью Вы можете создавать поистине грандиозные конструкции, которые могут кардинально изменить синтаксис языка.
Оконная процедура занимает в исходном тексте строки с 48 по 66. В начале процедуры проверяем идентификатор входящего сообщения wMsg на стандартную константу WM_DESTROY. Данное сообщение посылается окну в случае его закрытия.

WM_DESTROY — единственное сообщение, которое непременно (всегда, в любом случае) должно быть обработано в вашей оконной процедуре!

Если сообщение WM_DESTROY поступило, то прыгаем на локальную метку .wmdestroy, в которой у нас располагается функция PostQuitMessage, фактически посылающая в очередь сообщений сообщение WM_QUIT, которое затем обрабатывается уже в основном цикле функцией GetMessage и предписывает ей вернуть 0 (в регистре eax), что в итоге ведет к выходу из приложения (строка 36).

В строках 58 и 62 у нас присутствует команда xor eax,eax, которая обнуляет регистр eax. Интересно, для чего нам вдруг понадобилось его обнулять? Дело в том, что это регламентируется общим правилом API: функция должна возвращать в регистре eax либо код завершения, либо один из результатов своей работы. Соответственно, если оконная процедура WindowProc обрабатывает какое-либо сообщение, то она должна возвратить 0 в случае успешного завершения, либо любое другое значение в случае ошибочного. Вот именно по этой причине у нас тут и располагается команда обнуления регистра, поскольку подразумевается, что все обрабатываемые нашей процедурой сообщения обрабатываются успешно.

Затем в строке 52 сравниваем значение поля wMsg со значением WM_CREATE, фактически этим мы проверяем, не поступило ли сообщение о создании окна? У нас данная ветка кода пустует, мы как бы реагируем на это сообщение в очереди, но в действительности ничего не делаем. Далее, для всех сообщений, которые не обрабатываются нашей оконной процедурой, мы должны вызвать функцию DefWindowProc, как того предписывает Microsoft. Фактически функция DefWindowProc является функцией обработки по умолчанию и гарантирует, что каждое поступающее в очередь сообщение (даже то, которое нас не интересует), будет обработано. Далее следует локальная метка .finish, которая восстанавливает сохраненные на входе оконной процедуры регистры и выходит из неё.

Секция данных (data)

Как уже следовало из описания секции, в данном разделе помещаются данные, необходимые исполняемому коду, отсюда происходит и название секции. Ключевыми данными тут являются две структуры: wc и msg, на которых стоит остановиться подробнее. Структура wc имеет прототип структуры WNDCLASS. А уже сама структура WNDCLASS является стандартной для библиотек Win32 API и описана в файлах \INCLUDE\EQUATES\USER32.INC и \INCLUDE\EQUATES\USER64.INC пакета FASM. Давайте подробнее её изучим, я просто скопирую содержимое:

struct WNDCLASS

  style          dd ?

  lpfnWndProc    dd ?

  cbClsExtra     dd ?

  cbWndExtra     dd ?

  hInstance      dd ?

  hIcon          dd ?

  hCursor        dd ?

  hbrBackground  dd ?

  lpszMenuName   dd ?

  lpszClassName  dd ?

ends

Поля требуют пояснения, поэтому я свел их в небольшую таблицу:

Наименование Тип в ASM Тип в C Описание
style DD UINT Определяет стиль окна.
lpfnWndProc DD WNDPROC Адрес процедуры обработки событий окна.
cbClsExtra DD int Информация о дополнительных байтах для структуры класса окна.
cbWndExtra DD int Информация о дополнительных байтах для структуры экземпляра окна.
hInstance DD HINSTANCE Дескриптор экземпляра приложения, которое содержит оконную процедуру для класса окна.
hIcon DD HICON Дескриптор загруженной иконки окна. Может принимать значение дескриптора иконки приложения, отображаемой в верхнем левом углу окна.
hCursor DD HCURSOR Дескриптор загруженного курсора окна. Может принимать значение дескриптора курсора, используемого в пределах окна.
hbrBackground DD HBRUSH Дескриптор загруженной кисти фона окна. Этот член может принимать значение дескриптора кисти, используемой для отрисовки фона, или принимать значение цвета. Цвет должен быть одним из определенных стандартных цветов (правило: к значению цвета должна добавляться 1).
lpszMenuName DD LPCTSTR Имя (идентификатор) ресурса для класса меню. Это имя должно быть определено в секции/файле ресурсов. 0 означает, что меню отсутствует.
lpszClassName DD LPCTSTR Определяет имя класса окна. Это обычная текстовая строка с завершающим нулем. Имеются предопределенные классы, однако можно задавать и собственный класс.

Непосредственно перед регистрацией класса, структура WNDCLASS должна быть заполнена. Наиболее важные значениями являются: дескриптор приложения hInstance, дескриптор иконки для окна hIcon, дескриптор курсора окна hCursor, дескриптор кисти фона окна hbrBackground. В случае с кистью можно оставить значение по умолчанию, либо использовать значение стандартного системного цвета? Во многих исходниках это значение не используется, но я думаю, что шаблон будет более универсальным и готовым к широкому применению, если в нем описать наибольшее количество параметров.
Затем у нас описывается еще одна структура msg, которая имеет прототип системной структуры MSG. Это критически важная структура, поскольку сообщения, передаваемые в приложение, имеют структуру MSG, включающую 6 полей. Описание этой структуры содержится в файлах \INCLUDE\EQUATES\USER32.INC и \INCLUDE\EQUATES\USER64.INC пакета FASM. Проведем детальное рассмотрение данной структуры:

struct MSG

  hwnd     dd ?

  message  dd ?

  wParam   dd ?

  lParam   dd ?

  time     dd ?

  pt       POINT

ends

Так же, при помощи сообщений подобной структуры, система посылает сообщения в процедуру окна. Как мы видим, в структуре сообщения используется несколько параметров, которые хорошо бы разобрать подробнее:

Наименование Тип в ASM Тип в C Описание
hwnd DD HWND Дескриптор окна, оконная процедура которого получила данное сообщение. Поле равно нулю (NULL) когда сообщение принадлежит потоку (thread message).
message DD UINT Идентификатор сообщения. Представляет из себя именованную (символическую) константу, которая определяет назначение сообщения. Когда оконная процедура получает сообщение, код использует данный идентификатор для определения перехода на конкретный обработчик. В нашем примере, идентификатор WM_DESTROY определяет переход на метку .wmdestroy, код которой инициирует закрытие окна. Приложения могут использовать только младшее слово двойного слова message, поскольку старшее слово зарезервировано за системой.
wParam DD WPARAM Дополнительные данные сообщения. Параметр, уточняющий смысл сообщения: сами данные или расположение данных. Смысл и значение этого параметра зависит исключительно от типа сообщения, в совокупности с которым этот параметр рассматривается. Параметр может содержать целочисленное значение, флаги, указатель на структуру, содержащую дополнительные данные и тому подобное. Когда сообщение не использует дополнительных параметров, значение обычно устанавливается в 0 (NULL). Код оконной процедуры обычно проверяет идентификатор сообщения message для уточнения, как именно интерпретировать этот параметр.
lParam DD LPARAM Дополнительные данные сообщения. Параметр, уточняющий смысл сообщения: сами данные или расположение данных. Смысл и значение этого параметра зависит исключительно от типа сообщения, в совокупности с которым этот параметр рассматривается. Параметр может содержать целочисленное значение, флаги, указатель на структуру, содержащую дополнительные данные и тому подобное. Когда сообщение не использует дополнительных параметров, значение обычно устанавливается в 0 (NULL). Код оконной процедуры обычно проверяет идентификатор сообщения message для уточнения, как именно интерпретировать этот параметр.
time DD DWORD Время, в которое данное сообщение было помещено в очередь. Формат?
pt DD,DD POINT Позиция курсора, где сообщение было опубликовано. В координатах экрана.

Секция импорта (import data)

Для чего вообще нужна секция импорта? Если вы заметили, наша программа в своей работе использует разнообразные системные функции Win32 API. Все эти функции распределены в системе по различным библиотекам, которые располагаются в соответствующих .DLL-файлах. Например, наша программа использует несколько системных функций (GetModuleHandle, LoadIcon, LoadCursor, RegisterClass, CreateWindowEx и прочие), размещенных во внешних системных библиотеках (KERNEL32.DLL, USER32.DLL). Все эти функции вызываются в коде нашего приложения через таблицу импорта: к примеру, когда в процессе выполнения кода встречается вызов функции GetModuleHandle, на самом деле в исходном коде исполняемого файла вызов выглядит как:

call [адрес_в_таблице_импорта]

То есть производится вызов функции через адрес, указанный в конкретном поле таблицы импорта. Так вот, чтобы функции, используемые в нашем коде, могли корректно вызываться в процессе исполнения приложения, динамический компоновщик на этапе подготовки образа к выполнению, производит связывание действительных адресов функций (по которым расположены функции библиотек в виртуальном адресном пространстве процесса нашего приложения) с полями таблицы импорта. Только после выполнения вышеописанной процедуры внешние функции доступны для вызова.
Секцию импорта в нашем примере компилятор создает при помощи макроса library, который имеется в стандартных библиотечных файлах пакета FASM. На деле, макрос ответственен за создание в выходном исполняемом .exe-файле специального блока данных (секции), в самом начале которого записываются смещения имен библиотек (DLL), а затем смещения, по которым располагаются имена (или ординалы) импортируемых из этих библиотек функций. Директива import (строки 86,90) предписывает компилятору подключить перечисленные функции (используемые в нашей программе) из библиотек. Зачем на этом? Дело в том, что в диалекте FASM использование одного лишь макроса library в секции импорта недостаточно, в дополнение к нему есть два основных подхода:

  • Описание имен библиотек через макрос library + указание через директиву import списка используемых функций. Этот метод более универсальный, поскольку позволяет работать со всеми имеющимися в системе библиотеками.
  • Описание имен библиотек через макрос library + указание через директиву include файлов-включений для используемых библиотек. Это метод более простой, однако имеет существенный недостаток: .inc-файлы в комплекте FASM имеются только к основным системным библиотекам. Поэтому, если у вас нет .inc-файла к какой-либо используемой вами библиотеке — используйте первый метод!!

Как вы видите, у меня используется первый подход. Однако никто не запрещает пользоваться и вторым подходом, в этом случае окончание секции будет выглядеть (к примеру) вот так:

. . .

include         ‘%fasminc%\api\kernel32.inc’

include         ‘%fasminc%\api\user32.inc’

. . .

О плюсах и минусах того или иного подхода в процессе построения секции импорта можно дискутировать бесконечно, тем не менее их можно и комбинировать: для библиотек без инклюдов использовать первый метод, для библиотек с инклюдами — второй.

Written on . Posted in Assembler

Страница 40 из 47

   Для получения изображения  из  массива  и  вывода его на экран
используйте оператор PUT. Этот оператор требует только координаты
левого верхнего угла области  экрана,  в которую будет выводиться
изображение.   За  координатами должно быть указано имя  массива.
Например, PUT (40,30),ARRAY1 помещает  изображение, левый верхний
угол которого будет находиться в столбце 40 и строке 30. Оператор
PUT может иметь еще и необязательный параметр, определяющий цвет,
которым будет выводиться изображение.  Если этот параметр опущен,
то изображение будет выводиться  точно  в том виде, в котором оно
было  записано  оператором  GET.   Это  эквивалентно  записи  PUT
(40,30),ARRAY1,PSET.  В противном случае имеются некоторые другие
возможности. Если Вы вместо PSET укажете PRESET, то цвет 0 палет-
ты будет заменен на цвет 3 и наоборот, а цвет 1 палетты — на цвет
2 и наоборот.
   Имеются  еще три случая, использующие логические операции AND,
OR или XOR.  Как и PRESET эти  слова могут заменять PSET в приве-
денном примере. Обсуждение этих трех операций смотрите в приложе-
нии Б.  Каждая  операция  включает  сравнение  битов существующей
точки  на  экране с битами точки накладываемого  изображения.   В
режиме высокого разрешения, когда на точку отводится только 1 бит
операция простая. Но в режиме умеренного разрешения, в котором на
каждую точку отводится 2 бита, могут происходить различные транс-
формации цветов.
   AND устанавливает бит только если он был установлен и у  точки
экрана и у точки изображения (взятой из массива).  В режиме высо-
кого  разрешения это означает, что точка изображения появится  на
экране только если  соответствующая  точка экрана уже «включена».
Все остальные точки области будут выключены.  В режиме умеренного
разрешения  операция  производится  над  обоими  битами. Если для
точки  экрана  установка битов 01, а  для  соответствующей  точки
изображения — 10, то оба бита будут сброшены и точка экрана полу-
чит код 00, что соответствует фоновому цвету.
   OR  устанавливает  бит, если он был установлен либо для  точки
экрана, либо для точки изображения.  В черно-белом режиме OR нак-
ладывает  изображение  на существующее изображение на экране.   В
цветном режиме для определения эффекта Вы опять должны прибегнуть
к вычислениям. Комбинация кодов палетты 1(01) и 2(10) дает 3(11),
также как и комбинация 0(00) и 3(11).

   И, наконец, XOR устанавливает  бит,  если из двух сравниваемых
только  один был установлен.  Применение этой операции  для  чер-
но-белого экрана с массивом единиц дает негативное изображение (1
и 1 дает 0, а 1 и 0 — дает 1). В режиме умеренного разрешения эта
операция меняет все цвета. В  результате  получаем наложение двух
изображений.   Но  более важно, что при повторении этой  операции
экран принимает в точности такой же вид, который он имел первона-
чально.  При этом изображение стирается.  Эта техника полезна для
мультипликации, когда над изображением дважды производится опера-
ция XOR в одной позиции, затем в соседней и т.д.

   Низкий уровень.

   Имеется много подходов к написанию процедур заполнения  графи-
ческих объектов. Ни один из них  не является идеальным, поскольку
всегда  имеется конфликт между скоростью работы процедуры и слож-
ностью фигур, которые она может  обрабатывать.  Любая  процедура,
которая заполняет область точку за точкой будет медленной,  неза-
висимо от того, насколько элегантно она реализована.  Имейте вви-
ду,  что почти каждая модифицируемая точка  расположена в  байте,
все точки которого будут изменяться в тот же самый цвет.  Получе-
ние  доступа  к одному и тому же байту с  использованием  сложных
процедур требует существенно больше времени, чем установка целого
байта  за один доступ к ячейке видеобуфера.  Например, поточечная
очистка экрана требует на IBM PC нескольких  секунд при использо-
вании функции BIOS, в то время как прямой доступ в память  произ-
водит эту операцию мгновенно:

      MOV  AX,0B800H     ;ES указывает на буфер экрана
      MOV  ES,AX         ;
      MOV  CX,8192       ;заполняем все байты
      MOV  AX,0          ;в каждый байт пишем 0
      MOV  DI,0          ;DI поочередно указывает на все байты
REP   STOSW              ;повторяем запись 8192 раза

   Многие процедуры заполняют  по  одной  горизонтальной  строке,
проверяя на цвет границы справа и слева. Поскольку строки состоят
из смежных байтов данных, то  надо  поочередно брать байты из ви-
деобуфера  и проверять присутствует ли в них цвет границы.   Если
цвет границы отсутствует, то  можно  заменить  сразу весь байт на
цвет  заполнения.  В противном случае к данному байту применяется
поточечный подход.
   Имеется очень быстрый способ определения  присутствует ли гра-
ничный цвет в данном байте видеобуфера. Предположим, что процеду-
ра ищет цвет 1 палетты в режиме умеренного  разрешения с четырьмя
цветами. Этому цвету соответствует код 01, поэтому сначала запол-
ним весь байт этим кодом: 01010101. Затем используем операцию NOT
для обращения каждого бита, после чего байт примет вид  10101010.
Проделаем операцию  XOR  со  значением  взятым  из видеобуфера; в
результате получим байт, у которого оба бита, относящиеся к одной
точке равны 1 только для  точек,  имеющих  граничный цвет.  Затем
снова используем операцию NOT с тем, чтобы пара битов, относящих-
ся к точке граничного цвета имела  код 00. После этого используем
операцию  TEST для нахождения полей со значением 00.  Если  такое

поле найдено, то граничный цвет обнаружен и процедура переходит к
обычному поточечному анализу данного байта.  Эту процедуру  можно
еще убыстрить, если использовать словные данные.

   MOV  AL,ES:[BX]    ;берем байт из видеобуфера
   XOR  AL,10101010B  ;устанавливаем биты для цвета границы
   NOT  AL            ;обращаем биты
   TEST AL,11000000B  ;проверяем биты 7-6
   JZ   FOUND_BOUND   ;переход если граничный цвет
   TEST AL,00110000B  ;проверяем биты 5-4
   JZ   FOUND_BOUND   ;переход если граничный цвет
   TEST AL,00001100B  ;проверяем биты 3-2
   JZ   FOUND_BOUND   ;переход если граничный цвет
   TEST AL,00000011B  ;проверяем биты 1-0
   JZ   FOUND_BOUND   ;переход если граничный цвет
   MOV  AL,FILL_COLOR ;граничного цвета нет, заполняем байт
   MOV  ES:[BX],AL    ;возвращаем байт в видеобуфер
    .
    .
FOUND_BOUND:

   Когда это возможно,  постарайтесь, чтобы границы прямоугольных
областей  Ваших картинок были выравнены на границу двух,  четырех
или восьми точек, с тем чтобы  прямое  отображение в память имело
дело с целыми байтами.  Другая возможность, хотя и не столь быст-
рая, состоит в создании  определяемых  пользователем псевдографи-
ческих  символов [4.3.4] и выводе их на границе области  заполне-
ния.  Короче, в данной области Вы имеете все возможности проявить
сообразительность,  а  зачастую  стоит подумать, а нужна  ли  Вам
столь сложная графика в данной задаче.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Vmware horizon client download windows
  • Счетчики производительности не запущены windows server 2019
  • Как убрать название папки в windows 10 без нумпада
  • Защитник windows 10 этим параметром управляет ваш администратор как исправить
  • Драйвер для defender game racer turbo rs3 драйвер windows