Для обращения к некоторым возможностям операционной системы Windows предоставляет некоторый функционал в виде функций WinAPI, которые можно вызывать в том числе и из программы на ассемблере ARM64. Для работы с WinAPI требуется подключить в программу одну и из библиотек Windows, которые содержать необходимые функции.
Так, за работу с файлами отвечает библиотека kernel32.lib. Рассмотрим на примере записи в файл, а точнее на его частном случае - выводе на консоль. Для записи в файл применяется функция WriteFile, которая имеет следующее определение на языке C++:
BOOL WriteFile( [in] HANDLE hFile, [in] LPCVOID lpBuffer, [in] DWORD nNumberOfBytesToWrite, [out, optional] LPDWORD lpNumberOfBytesWritten, [in, out, optional] LPOVERLAPPED lpOverlapped );
WriteFile
имеет следующие параметры:
hFile
: дескриптор файла, передается через регистр X0
lpBuffer
: адрес буфера для записи, передается через регистр X1
nNumberOfBytesToWrite
: размер буфера, передается через регистр Х2
lpNumberOfBytesWritten
: адрес переменной типа dword для получения количества байтов, записанных в файл. Равно размеру буфера, если операция записи прошла успешно.
Передается через регистр Х3
lpOverlapped
: обычно имеет значение NULL (0), передается через регистр Х4
После выполнения функция WriteFile
возвращает ненулевое значение, если запись прошла успешно, и ноль, если произошла ошибка.
Но чтобы записать данные в файл, нам необходим дескриптор этого файла. В зависимости от того, куда мы хотим записать, отличается способ получения дескриптора. В данном же случае мы будем выводить данные на консоль, и для этого нам потребуется встроенная функция GetStdHandle
HANDLE WINAPI GetStdHandle( _In_ DWORD nStdHandle );
Она принимает один параметр - число, которое указывает, для какого устройства надо получить дескриптор. Этому параметру можно передать следующие значения:
-10: устройство стандартного ввода (для ввода с консоли)
-11: устройство стандартного вывода (для вывода на консоль)
-12: для вывода ошибок на консоль
В качестве результата функция возвращает дескриптор консоли.
Используем функцию WriteFile
для записи (вывода) данных на консоль, а точнее в стандартный поток вывода:
global __main import WriteFile ; импортируем функцию WriteFile import GetStdHandle ; импортируем функцию GetStdHandle area main, CODE __main stp fp, lr, [sp, #-16]! ; Сохраняем регистры FR и LR в стек mov x0, #-11 ; Аргумент для GetStdHandle - STD_OUTPUT bl GetStdHandle ; вызываем функцию GetStdHandle ; После вызова GetStdHandle Х0 хранит дескриптор консоли ; Устанавливаем остальные параметры ldr x1, =message ; Второй параметр - строка для вывода mov x2, len ; Третий параметр - длина строки ldr x3, =bytesWritten ; Четвертый параметр - количество записанных байтов mov x4, #0 ; Пятый параметр bl WriteFile ; вызываем функцию WriteFile ldp fp, lr, [sp], #16 ; Сохраняем регистры FR и LR в стек ret area DATA message dcb "Hello METANIT.COM!\n" ; строка для вывода len equ 19 ; размер строки bytesWritten dcd 16 ; для получения количество записанных байтов end
Для использования функций их надо импортировать с помощью директивы import:
import WriteFile ; импортируем функцию WriteFile import GetStdHandle ; импортируем функцию GetStdHandle
Вместо import
можно было бы использовать синонимичную директиву extern
extern WriteFile ; импортируем функцию WriteFile extern GetStdHandle ; импортируем функцию GetStdHandle
Передача параметров в функции и возвращение результатов применяет те же соглашения, что действуют в целом в ARM64 на других платформах: первые 8 целочисленных параметров передаются через регистры Х0-Х7, а числа с плавающей точкой - через регистры V0-V7. Девятый и последующие параметры передаются через стек. Результат функции помещается в регистр Х0.
Здесь функции GetStdHandle
в качестве параметра через регистр X0 передается значение -11, которое указывает, что надо получить
дескриптор стандартного вывода.
Вызов функций WinAPI выполняется также как и вызов любых других функций - с помощью инструкции bl
mov x0, #-11 ; Аргумент для GetStdHandle - STD_OUTPUT bl GetStdHandle ; вызываем функцию GetStdHandle
Полученный через регистр X0 дескриптор будет передаваться первому параметру функции WriteFile. Через регистр X1 передается адрес выводимой строки - переменной message, а через регистр X2 - длина строки - константа len. В регистр Х3 загружаем адрес переменной bytesWritten, через которую получим реальное число записанных байтов.
ldr x1, =message ; Второй параметр - строка для вывода mov x2, len ; Третий параметр - длина строки ldr x3, =bytesWritten ; Четвертый параметр - количество записанных байтов mov x4, #0 ; Пятый параметр bl WriteFile ; вызываем функцию WriteFile
Поскольку функции изменяют состояние регистра LR/X30, то перед вызовом функций необходимо его сохранить в стек, а после выполнения функций восстановить из стека
stp fp, lr, [sp, #-16]! ; Сохраняем регистры FR и LR в стек ; ..................... ldp fp, lr, [sp], #16 ; Сохраняем регистры FR и LR в стек
Поскольку эти функции располагаются в библиотеке kernel32.lib, то эту библиотеку необходимо передать компоновщику при компоновке программы. Допустим, исходный код рограммы определен в файле hello.asm, то процесс компиляции/компоновки и вызов программы будет выглядеть следующим образом :
c:\arm64>armasm64 hello.asm -o hello.obj Microsoft (R) ARM Macro Assembler Version 14.36.32537.0 for 64 bits Copyright (C) Microsoft Corporation. All rights reserved. c:\arm64>link hello.obj kernel32.lib /entry:__main /subsystem:console Microsoft (R) Incremental Linker Version 14.36.32537.0 Copyright (C) Microsoft Corporation. All rights reserved. c:\arm64>hello Hello METANIT.COM! c:\arm64>
Рассмотрим еще один примере взаимодействия с WinAPI на примере создания простейшего графического приложения на примере диалогового окна. Для создания графического приложения в Windows применяется библиотека user32.lib, а непосредственно для создания диалогового окна в WinAPI можно использовать функцию MessageBoxA, которая имеет следующее определение на языке C++:
int MessageBoxA( [in, optional] HWND hWnd, [in, optional] LPCSTR lpText, [in, optional] LPCSTR lpCaption, [in] UINT uType );
Первый параметр - hWnd
представляет дескриптор родительского окна. Если родительское окно не применяется, то этот параметр имеет значение NULL
. Второй параметр
представляет текст отображаемого в окне сообщения, а третий параметр - текст заголовка окна. Четвертый параметр - числовое значение, которое указывает, какие кнопки будут использоваться на окне.
Рассмотрим создание графического окна на примере. Пусть у нас есть файл hello.asm со следующим кодом:
global __main extern MessageBoxA ; импортируем функцию MessageBoxA area main, CODE __main stp fp, lr, [sp, #-16]! ; Сохраняем регистры FR и LR в стек mov x0, #0 ; HWin = NULL ldr x1, =message ; отображаемое сообщение ldr x2, =win_title ; Заголовок диалогового окна mov x3, #0 ; 0 - кнопка "OK" bl MessageBoxA ; вызываем функцию MessageBoxA ldp fp, lr, [sp], #16 ; Восстанавливаем регистры FR и LR из стека ret area DATA message dcb "Hello METANIT.COM!",0 ; сообщение в окне win_title dcb "Window ARM", 0 ; заголовок окна end
Вначале импортируем внешнюю функцию MessageBoxA
:
extern MessageBoxA ; импортируем функцию MessageBoxA
Поскольку родительское окно не используется, то для первого параметра в регистр X0 передаем значение 0:
mov x0, #0
Второй и третий параметры, которые передаются соответственно через регистры X1 и Х2, принимают в качестве теста сообщения и заголовка адреса переменных message и win_title:
ldr x1, =message ; отображаемое сообщение ldr x2, =win_title ; Заголовок диалогового окна
В качестве кнопки последнему параметру через регистр Х3 передаем число 0. Это значит, что окно будет иметь одну кнопку "OK":
mov x3, #0 ; 0 - кнопка "OK"
Можно передать и другие значения. Например, число 1 устанавливает кнопки OK и Cancel, а число 4 - кнопки "Yes" и "No". Полный список значений можно посмотреть в документации.
При компоновке программные компоновщику надо передать библиотеку user32.lib, а также параметр /subsystem:windows. Данный параметр указывает, что будет создаваться графическое приложение. Так, скомпилируем приложение с помощью следующей команды:
armasm64 hello.asm -o hello.obj link hello.obj user32.lib /entry:__main /subsystem:windows
В итоге после запуска скомпилированного приложения мы увидим следующее окно: