Первая программа на MASM

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

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

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

includelib kernel32.lib   ; подключаем библиотеку kernel32.lib

; подключаем функции WriteFile и GetStdHandle
extrn WriteFile: PROC
extrn GetStdHandle: PROC
.code
text byte "Hello METANIT.COM!"    ; выводимая строка

main proc
  sub  rsp, 40   ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт)
  mov  rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
  call GetStdHandle ; вызываем функцию GetStdHandle
  mov  rcx, rax     ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли
  lea  rdx, text    ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX
  mov  r8d, 18      ; Третий параметр WriteFile - длина строки для записи в регистре R8D 
  xor  r9, r9       ; Четвертый параметр WriteFile - адрес для получения записанных байтов
  mov  qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
  call WriteFile ; вызываем функцию WriteFile
  add  rsp, 40
  ret
main endp
end

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

ml64 hello.asm /link /entry:main

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

**********************************************************************
** 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. Запустим этот файл, и консоль выведет строку:

**********************************************************************
** 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
Hello METANIT.COM!
c:\asm>

Вкратце разеберем программу. Вначале идет инструкция

includelib kernel32.lib

Эта инструкция подключает библиотеку kernel32.lib. Это библиотека Windows включает нативные функции GetStdHandle и WriteFile, которые использует программа для вывода строки на консоль. Установка Visual Studio включает этот файл, а файл vcvars64.bat при запуске должным образом установить пути к нему, чтобы компоновщик мог его найти при сборке файла программы смог бы его найти.

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

extrn WriteFile: PROC
extrn GetStdHandle: PROC

С помощью директивы .code открываем секцию кода и затем определяем используемые в программе данные - выводимую строку

.code
text byte "Hello METANIT.COM!"    ; выводимая строка

Строка называется text, а каждый ее элемент (символ) имеет тип byte (8-битное число).

Далее идут собственно выполняемые программой действия. Они помещаются между инструкциями main proc и main endp

main proc
    ; действия программы
main endp

Далее нам надо должным образом настроить стек. В частности, резервируем в стеке 40 байт для параметров функций GetStdHandle и WriteFile и при этом учитываем выравнивание стека по 16-байтной границе. Указатель на верхушку стека хранится в регистре rsp. Поэтому вычитаем с помощью инструкции sub из значения в регистре rsp 40 байт

sub rsp, 40

Почему 40? Прежде всего при вызове функций WinAPI (как в данном случае функций GetStdHandle и WriteFile) необходимо зарезервировать в стеке как минимум 32 байта - так называемое "shadow storage" (теневое хранилище). Далее нам надо учитывать количество параметров функции. Пеовые 4 параметра функций передаются через регистры, а параметры начиная с 5-го передаются через стек. Соответственно для 5-го и последующих параметров надо выделить в стеке область. Для каждого параметра вне зависимости от его размера выделяется 8 байт. Функция WriteFile как раз принимает 5 параметров, поэтому для нее надо выделить дополнительные 8 байт в стеке. Поэтому получаем 32 байта + 8 байт (5-й параметр WriteLine) = 40 байт. Количество параметров смотрим по функции с наибольшим количеством параметров. Третий момент - нам надо учитывать, что перед вызовом функций WinAPI стек имел выравнивание по 16 байтовой границе, то есть значение в RSP должно быть кратно 16. По умолчанию при вызове функции в стек помещается адрес возврата функии размером 8 байт. Поэтому наши 40 байт + 8 байт (адрес возврата из функции) дадут 48 байт - число кратное 16.

Вначале нам надо использовать встроенную функцию GetStdHandle(), которая позволяет получить дескриптор на устройство ввода-вывода. Она имеет следующее определение на C:

HANDLE WINAPI GetStdHandle(
  _In_ DWORD nStdHandle
);

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

 mov  rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT

После установки параметра этой функции вызываем ее с помощью инструкции call:

call GetStdHandle

В результате выполнения функция GetStdHandle возвращает дескриптор - объект, через который мы можем взаимодействовать с консолью. Этот дескриптор помещается в регистр rax. Получив этот дескриптор, используем его для вывода на консоль строки с помощью функции WriteFile. Для справки ее определение на С++

BOOL WriteFile(
  [in]                HANDLE       hFile,
  [in]                LPCVOID      lpBuffer,
  [in]                DWORD        nNumberOfBytesToWrite,
  [out, optional]     LPDWORD      lpNumberOfBytesWritten,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

Вызов функции GetStdHandle помещает в регистр rax дескриптор консоли. И для вывода строкии на консоль с помощью функции WriteFile нам надо поместить этот дескриптор в регистр rcx

mov rcx, rax

Затем с помощью инструкции lea загружаем в регистр rdx адрес выводимой строки

lea  rdx, text

Далее в регистр r8d помещаем длину выводимой строки в байтах - в данном случае это 18 байт:

mov  r8d, 18

Затем в регистре r9 устанавливаем адрес четвертого параметра функции WriteFile:

xor  r9, r9

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

Последний - пятый параметр функции WriteFile должен иметь значение NULL, по сути 0. Поэтому устанавливаем для него значение 0, смещаясь в стеке вперед на 32 байта (4 параметра * 8):

mov qword ptr [rsp + 32], 0

Инструкция mov помещает значение в определенное место. Здесь в качестве значения служит число 0. А место определяется выражением qword ptr [rsp + 32]. qword ptr указывает, что этот операнд описывает адрес размером в четыре слова, что означает 8 байтов (слово имеет длину 2 байта). ptr указывает, что значение операнда следует рассматривать как адрес. То есть число 0 представляет 8-байтное значение и помещается по адресу rsp + 32.

И далее собственно вызываем функцию WriteFile:

call WriteFile

Этот вызов должен привести к выводу строки на консоль. После этого восстанавливаем значение верхушки стека. Для этого с помощью инструкции add прибавляем к значению в регстре rsp ранее отнятые 40 байт:

add rsp, 40

И с помощью инструкции ret выходим из программы.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850