Программирование на ассемблере под windows

В этой статье я покажу как написать приложение для 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, знаменитого Спектрума и его многочисленных клонов. Писать пусть и очень простую, но полноценную игру на ассемблере мне по-настоящему понравилось. Надеюсь читателям тоже было интересно!

http://www.sklyaroff.ru

149

программисты на ассемблере под Windows в основном используют только MASM32. Поэтому для продолжения вам необходимо скачать с сайта http://www.masm32.com и установить к себе на компьютер этот ассемблер.

Кроме документации, примеров программ, полезных утилит и пр. пакет MASM32 предоставляет специальные программные функции, которые через особые библиотеки входящие в пакет MASM32 программист может задействовать в своей программе. Эти функции имеют синтаксис подобный API-функциям Windows, но не являются API функциями. Следовательно, описание функций MASM32 невозможно найти в MSDN. Но их описание на английском языке можно найти в chm-файлах, которые входят в состав пакета MASM32, и расположены по следующему относительному пути: \masm32\help\.

В качестве примера в листинге 7.3 мы используем MASM32-функцию FpuFLtoA, которая предназначена для вывода вещественных чисел на экран. Эта функция не является API-функцией.

А теперь посмотрим, как написать простейшее приложение под Windows и на его примере рассмотрим все принципы вызовов API-функций на ассемблере. Все, что будет делать наша программа это выводить окно с сообщением, показанное на рис. 7.1.

Рис. 7.1. Вид окна с сообщением, отображаемого программой

Для вывода таких сообщений используется API-функция MessageBox, ниже приведено описание, взятое из MSDN в переводе на русский:

int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);

Параметры функции:

hWnd дескриптор родительского окна. Если родительского окна нет, то используется нулевое значение;

lpText текст, отображаемый внутри окна;

lpCaption текст в заголовке окна;

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

MB_ABORTRETRYIGNORE — окно сообщений будет содержать три кнопки: «Abort», «Retry», и «Ignore»;

MB_OK — окно сообщений будет содержать только одну кнопку: «OK»;

MB_OKCANCEL — окно сообщений будет содержать две кнопки: «OK» и «Cancel»;

MB_YESNO — окно сообщений будет содержать две кнопки: «Yes» и «No»;

MB_ICONEXCLAMATION — в окне сообщений появится иконка с восклицательным знаком;

MB_ICONHAND — в окне сообщений появится иконка с изображением знака «Stop»;

http://www.sklyaroff.ru

150

MB_ICONQUESTION — в окне сообщений появится иконка с изображением вопросительного знака;

MB_ICONASTERISK — в окне сообщений появится иконка с изображением буквы «i»;

MB_DEFBUTTON1 — фокус находится на первой кнопке;

MB_DEFBUTTON2 — фокус находится на второй кнопке;

MB_DEFBUTTON3 — фокус находится на третьей кнопке.

Все флаги и вообще любые строковые идентификаторы, которые вы можете встретить в описаниях функций API, на самом деле являются именами определенных числовых значений (кодов). Вы не можете в программе на ассемблере просто использовать, скажем, флаг MB_OK, т. к. на самом деле требуется указать число, которое заменяет этот флаг. Узнать числовые значения флагов и всех прочих идентификаторов API-функций, всегда можно в так называемых заголовочных файлах (имеют расширение .h). В описании каждой API-функции в MSDN обычно указывается, в каком заголовочном файле следует искать такие числовые значения и сразу предоставляется возможность просмотреть этот заголовочный файл. Для функции MessageBox все значения флагов содержатся в заголовочном файле winuser.h. Вот нужный нам отрывок из winuser.h:

#define MB_OK

0x00000000L

#define MB_OKCANCEL

0x00000001L

#define MB_ABORTRETRYIGNORE

0x00000002L

#define MB_YESNOCANCEL

0x00000003L

#define MB_YESNO

0x00000004L

#define MB_RETRYCANCEL

0x00000005L

Оператор языка Си #define является аналогом ассемблерной директивы эквивалентности EQU, которая присваивает имени значение операнда (см.

разд. 2.4.2).

Как видим, нужному нам флагу MB_OKCANCEL присвоено значение 0x00000001L.

Буква L на конце числа означает в языке Си длинное целое (long). В ассемблере эта буква не используется, поэтому значение просто будет выглядеть как 1.

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

MB_OKCANCEL EQU 1

К счастью нам не придется это делать самим постоянно, т. к. в пакет MASM32 входит специальный файл WINDOWS.INC (расположен в \MASM32\include\) в котором определены директивы эквивалентности для всевозможных идентификаторов и флагов, которые используют API-функции, в том числе и MB_OKCANCEL. Откройте в любом текстовом редакторе этот файл и посмотрите его содержимое. Можно (и нужно) подключать файл WINDOWS.INC в своих программах и использовать любые идентификаторы из него, как это сделать мы узнаем ниже.

Прежде чем мы начнем составлять программу на ассемблере, вам нужно узнать еще одну важную тонкость, связанную с API-функциями. Все API-функции работающие со строками (как, например, MessageBox) существуют в двух версиях:

ANSI-версия, для работы со строками в кодировке ANSI (один байт на символ). В этом случае к имени функции добавляется суффикс A. Например, для функции

MessageBox имя ANSI-версии будет MessageBoxA.

Unicode-версия, для работы со строками в кодировке Unicode (два байта на символ). К имени функции добавляется суффикс W. Например, для функции

MessageBox имя Unicode-версии будет MessageBoxW.

API-функции без суффиксов (например MessageBox), на самом деле является лишь обертками для этих двух функций. Компилятор высокоуровневых языков сам решает,

http://www.sklyaroff.ru

151

какую из двух версий API-функции нужно использовать, однако программист на ассемблере должен точно указывать нужную версию функции ANSI или Unicode в своих программах.

ANSI-версию стоит выбирать когда в функцию передаются строки только из 256 символов кодировки ASCII, а Unicode-версию, когда в функцию требуется передавать строки из расширенного набора (65536 символов) кодировки Unicode.

Строки для API-функций ANSI-версий (MessageBoxA) задаются также, как мы это делали в DOS-программах, к примеру:

str DB «Ivan Sklyaroff», 0

А для Unicode-версий (MessageBoxW) строки необходимо определять следующим образом:

str DW ‘I’, ‘v’, ‘a’, ‘n’, ‘ ‘, ‘S’, ‘k’, ‘l’, ‘y’, ‘a’, ‘r’, ‘o’, ‘f’, ‘f’, 0

Разумеется, можно просто указывать коды непечатаемых символов, например, в следующей строке заданы четыре символа в кодировке Unicode в шестнадцатеричном виде (значок «евро» – код 020ACh, математическая «бесконечность» – код 0221Eh, «больше или равно» – код 02265h, буква арабского языка – код 0642h):

str DW 020ACh, 0221Eh, 02265h, 0642h, 0

Мы далее будем использовать строки, а, следовательно, и функции только ANSIверсий.

Обратите также внимание, что в Windows строки заканчиваются нулем, а не символом $ как в DOS-программах. Это правило вытекает из языка Си, так как в нем строки должны заканчиваться нулевым символом. Однако помните о том, что часто один из параметров API-функции задает длину строки (в документации это часто называется буфером), определяемой другим параметром, из-за этого возвращаемая из функции строка может не иметь нуля на конце. При использовании в дальнейшем строки без нуля на конце, в других функциях могут возникнуть ошибки.

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

Прототип создается с помощью директивы PROTO. Например, для функции MessageBoxA прототип будет выглядеть следующим образом:

MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD

Это означает, что в функцию MessageBoxA должны быть переданы четыре 32разрядных параметра.

В прототипах всех API-функций для указания типа параметров можно использовать DWORD, т. к. в API-функциях все параметры только 32-разрядные (мы говорили об этом выше).

Теперь мы можем написать предварительный вызов функции MessageBoxA на ассемблере:

MB_OKCANCEL EQU 1

.386P

http://www.sklyaroff.ru

152

.model flat,stdcall

MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data

hello_mess

db «Первая программа под Windows на ассемблере», 0

hello_title

db «Hello, World!», 0

.code

start:

push MB_OKCANCEL

push offset hello_title push offset hello_mess push 0

call MessageBoxA

end start

Мы верно составили вызов API-функции, однако как мы говорили в начале этого дня, функции API находятся в dll-библиотеках, и пока не присоединить нужные библиотеки к программе никакие даже правильно составленные вызовы APIфункций работать не будут. Но как присоединить динамические библиотеки к программе на ассемблере?

Напрямую это сделать нельзя и нужно использовать так называемые библиотеки импорта, которые обычно имеют тоже имя, что и соответствующие dll-файлы, но с расширением .lib. В MSDN Library в описании функции MessageBox можно увидеть следующую информацию (табл. 7.1).

Как видите, здесь указано, что функция содержится в файле user32.dll, а библиотекой импорта является файл User32.lib.

Таблица 7.1. Информация из MSDN Library к описанию функции MessageBox

Minimum DLL Version

user32.dll

Header

Declared in Winuser.h, include Windows.h

Import library

User32.lib

Minimum operating systems

Windows 95, Windows NT 3.1

Unicode

Implemented as ANSI and Unicode versions.

Файлы библиотек импорта входят в дистрибутивы любых средств разработки для Windows, например их можно взять из Visual Studio. Однако пакет MASM32 облегчает нам задачу тем, что в нем уже содержатся все необходимые библиотеки импорта в разделе \MASM32\lib\.

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

includelib \masm32\lib\user32.lib

Кроме того, в пакет MASM32 входят INC-файлы (расположены в разделе \MASM32\include\) в которых содержатся все прототипы API-функций. Эти INCфайлы имеют имена такие же, как имена соответствующих lib-файлов. Поэтому, подключив к программе INC-файл, вам не придется самостоятельно определять прототипы функций.

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

include \masm32\include\user32.inc

Посмотрите содержимое файла user32.inc, вы найдете в нем прототип MessageBoxA.

Кроме того, как мы уже говорили, в разделе MASM32\include\ существует файл WINDOWS.INC, в котором определены директивы эквивалентности для

http://www.sklyaroff.ru

153

всевозможных идентификаторов и флагов, которые используют API-функции, как,

например MB_OKCANCEL.

Подключается файл WINDOWS.INC также как и любой INC-файл:

include \masm32\include\windows.inc

Файл WINDOWS.INC является большим подспорьем для программистов, т. к. в больших программах число всевозможных флагов, идентификаторов и структур, необходимых функциям API достигает многие десятки. Подключив этот файл к своей программе, вам не придется больше определять их самостоятельно.

Мы далее будем включать этот файл во все наши программы на ассемблере. В листинге 7.1 показана готовая программа, которая выводи окно сообщения.

Вы видите, мы задействовали еще одну API-функцию ExitProcess для выхода из программы. Описание функции:

VOID WINAPI ExitProcess(UINT uExitCode);

Она расположена в kernel32.dll, поэтому мы добавляем в программу включения файлов:

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

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

Функцией ExitProcess мы будем завершать все наши программы на ассемблере под

Windows.

Компиляция программы осуществляется следующей строкой: ml /c /coff /Cp hello.asm

Назначение параметров:

/c компиляция без компоновки.

/coff создать .obj файл в формате COFF (Common Object File Format). Применение этого параметра обязательно.

/Cр сохранять регистр имен, заданных пользователем. Вы можете вставить строку «option casemaр:none» в начале исходного кода вашей программы, сразу после директивы .model, чтобы добиться того же эффекта.

После успешной компиляции hello.asm, вы получите hello.obj. Это объектный файл, который содержит инструкции и данные в двоичной форме. Отсутствуют только необходимая корректировка адресов, которая проводится линкером.

link.exe /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib hello.obj

/SUBSYSTEM:WINDOWS информирует линкер о том, что приложение является оконным приложением. Для консольных приложений необходимо использовать параметр /SUBSYSTEM:CONSOLE.

/LIBPATH:<путь к библиотекам импорта> указывает линкеру местоположение библиотек импорта. Если вы используете MASM32, они будут в \MASM32\lib\.

После окончания линковки вы получите файл hello.exe.

Листинг 7.1. Простейшая программа под Windows (hello.asm)

.386P

.model flat,stdcall

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

includelib

\masm32\lib\user32.lib

; Подключаем библиотеки

includelib

\masm32\lib\kernel32.lib

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

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

Последнее обновление: 01.07.2023

Установка MASM

Для работы с MASM надо установить для Visual Studio инструменты разработки для C/C++. Поэтому после загрузки программы установщика Visual Studio запустим ее и в окне устанавливаемых
опций выберем пункт Разработка классических приложений на C++:

Установка MASM 64 в Windows

Visual Studio включает как 32-разрядные, так и 64-разрядные версии MASM. 32-раздяная версия представляет файл ml.exe,
а 64-разрядная — файл ml64.exe. Точное расположение файлов может варьироваться от версии Visual Studio. Например, в моем случае это папка
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64

Ассемблер MASM64 в Windows

Для использования MASM64 перейдем к меню Пуск и в списке программ найдем пункт Visual Studio и подпункт
x64 Native Tools Command Prompt for VS 2022

Build Tools for Visual Studio 2022 и MASM64 в Windows

Нам должна открыться консоль. Введем в нее ml64, и нам отобразится версия ассемблера и некоторая дополнительная информация:

**********************************************************************
** Visual Studio 2022 Developer Command Prompt v17.5.5
** Copyright (c) 2022 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'

C:\Program Files\Microsoft Visual Studio\2022\Community>ml64
Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1
Copyright (C) Microsoft Corporation.  All rights reserved.

usage: ML64 [ options ] filelist [ /link linkoptions]
Run "ML64 /help" or "ML64 /?" for more info

C:\Program Files\Microsoft Visual Studio\2022\Community>

Стоит отметить, что запуск этой этой утилиты фактически представляет запуск файла C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat —
он по сути вызывает другой файл — vcvarsall.bat, который собственно и настраивает окружение для выполнения ассемблера.

Структура программы на ассемблере MASM

Типичная программа на MASM содержит одну или несколько секций, которые определяют, как содержимое программы будет располагаться в памяти. Эти секции
начинаются с таких директив MASM, как .code или .data. Данные, используемые в программе, обычно определяются в секции .data.
Инструкции ассембра определяются в секции .code.

В общем случае программа на ассемблере MASM имеет следующий вид:

.code

main proc
 
  ret
main endp

end

Директива .code указывает MASM сгруппировать операторы, следующие за ней, в специальный раздел памяти, зарезервированный для машинных инструкций.

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

Далее с помощью операторов main proc определяется процедура main. Операторы main endp указывают на конец функции main.
Между main proc и main endp располагаются выполняемые инструкции ассемблера. Причем в самом конце функции идет инструкция ret,
с помощью которой выполнение возвращается в окружение, в котором была вызвана даннуа процедура.

В конце файла кода идет инструкция end

Программа может содержать комментарии, которые располагаются после точки с запятой:

.code   ; начало секции с кодом программы

main proc   ; Функция main
 
 ret ; возвращаемся в вызывающий код
 
main endp ; окончание функции main

end ; конец файла кода

Комментарии на работу программы никак не влияют и при компиляции не учитываются.

Компиляция программы

Компиляция программы на MASM обычно происходит в командной строке. Например, воспользуемся кодом выше и напишем простейшую программу на ассемблере, которая ничего
не делает. Для этого определим на жестком диске папку для файлов с исходным кодом. Допустим, она будет называться
C:\asm. И в этой папке создадим новый файл, который назовем hello.asm и в котором определим следующий код:

.code   ; начало секции с кодом программы

main PROC   ; Функция main
 
 ret ; возвращаемся в вызывающий код
 
main ENDP

END ; конец файла кода

Откроем программу x64 Native Tools Command Prompt for VS 2022 и перейдем в ней к папке, где располагается файл hello.asm. Затем выполним следующую команду

ml64 hello.asm /link /entry:main

В данном случае вызываем приложение ml64.exe и передаем ему для компиляции файл hello.asm. А флаг /link указывает MASM
скомпоновать скомпилированный файл в файл приложения exe, а все дальнейшие параметры (в частности, параметр /entry:main) передаются компоновщику.
Параметр /entry:main передает компоновщику имя основной процедуры/функции, с которой начинается выполнение программы.
Компоновщик сохраняет этот адрес этой процедуры/функции в специальном месте исполняемого файла, чтобы Windows могла определить начальный адрес основной программы после загрузки исполняемого файла в память.

В результате ассемблер скомпилирует ряд файлов

**********************************************************************
** Visual Studio 2022 Developer Command Prompt v17.5.5
** Copyright (c) 2022 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'

C:\Program Files\Microsoft Visual Studio\2022\Community>cd c:\asm

c:\asm>ml64 hello.asm /link /entry:main
Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: hello.asm
Microsoft (R) Incremental Linker Version 14.35.32217.1
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:hello.exe
hello.obj
/entry:main

c:\asm>

В итоге в каталоге программы будут сгенерированы объектный файл hello.obj и собственно файл программы — hello.exe.

  • Программное обеспечение ocr для brother скачать windows 10
  • Программирование на python под windows
  • Программное обеспечение microsoft windows vista
  • Программирование драйверов для windows комиссаров
  • Программирование для детей приложение для windows