Для взаимодействия с ресурсами системы на Windows ARM, также как и на других платформах, можно использовать системные вызовы или syscalls. Но в armasm64 обращение к системным вызовам имеет свои особенности. Прежде всего, как и в общем случае для выполнения системного вызова применяется инструкция svc:
SVC #imm
Но в качестве операнда инструкции передается числовой номер системного вызова. В этом отличие от других платформ, например, на Linux ARM64 код вызова передается в регистр Х8, а на MacOS ARM64 - в регистр Х16.
Минусом ОС Windows является то, что она не ориентирована на использование системных вызовов. Так, официальной документации по этому поводу нет, более того о некоторых системных функциях нет вообще никакого упоминания в документации а сами номера системных вызовах могут меняться в зависимости от номера билда ОС. Хотя в целом они стабильны для большинства выпусков. Более менее полную таблицу системных вызовов для Windows можно найти на страницу https://hfiref0x.github.io/NT10_syscalls.html.
Возьмем простейший системный вызов - завершение процесса, который представлен функцией NtTerminateProcess. Если мы обратимся к вышеуказанной таблице, то увидим, что эта функция имеет номер 44. Мы можем найти в документации определение этой функции:
NTSYSAPI NTSTATUS ZwTerminateProcess( [in, optional] HANDLE ProcessHandle, [in] NTSTATUS ExitStatus );
Хотя здесь указана функция ZwTerminateProcess, а не NtTerminateProcess, но в целом Zw-версии функций и Nt-версии аналогичны.
И из определения функции мы видим, что она принимает два параметра:
ProcessHandle
: дескриптор процесса, который надо закрыть. Это необязательный параметр. Если он равен 0, то закрываем текущий процесс.
ExitStatus
: статус завершения процесса, в качестве которого выступает числовой код и который обычно возвращается данной функцией.
Применим данную функцию в программе:
global __main area main, CODE ; начало раздела CODE __main mov x0, 0 ; первый параметр - ProcessHandle не указываем mov x1, 17 ; второй параметр - код статуса - произвольное число svc #44 ; вызываем системную функцию с номером 44 - NtTerminateProcess ret end
Передача значений в системную функцию идет так же, как и в Linux/MacOS: первые 7 целочисленных параметров передаются последовательно через регистры Х0-Х7, а числа с плавающей точкой - через регистры v0-v7. Результат функции помещается в регистр Х0 (целочисленный) или в V0 (число с плавющей точкой). Поскольку первый параметр необязательный, и мы хотим завершить текущую программу, то первому параметру через регистр Х0 передаем значение 0. Второму параметру через регистр Х1 передается произвольный числовой код статуса, в нашем случае число 17.
Для вызова системной функции инструкции svc передается числовой код функции - 44. Результат компиляции и вызова программы
(допустим, код программы расположен в файле hello.asm
и компилируется в файл hello.obj
):
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 /entry:__main /subsystem:console Microsoft (R) Incremental Linker Version 14.36.32537.0 Copyright (C) Microsoft Corporation. All rights reserved. c:\arm64>hello c:\arm64>echo %ERRORLEVEL% 17 c:\arm64>
Таким образом, функция NtTerminateProcess возвратила число 17, которое передавалось через второй параметр функции.
Рассмотрим другой пример - запись данных в файл, и как частный случай, вывод строки на консоль. За это отвечает системная функция NtWriteFile, которая для последних версий Windows имеет номер 8 и которая имеет следующий заголовок:
__kernel_entry NTSYSCALLAPI NTSTATUS NtWriteFile( [in] HANDLE FileHandle, [in, optional] HANDLE Event, [in, optional] PIO_APC_ROUTINE ApcRoutine, [in, optional] PVOID ApcContext, [out] PIO_STATUS_BLOCK IoStatusBlock, [in] PVOID Buffer, [in] ULONG Length, [in, optional] PLARGE_INTEGER ByteOffset, [in, optional] PULONG Key );
Функция принимает аж 9 параметров, из которых отметим наболее важные.
1-й параметр - FileHandle
представляет дескриптор файла (в нашем случае дискриптор консольного вывода).
5-й пареметр IoStatusBlock
представляет блок байтов, в который записывается статус операции.
6-й параметр - Buffer
- указатель на данные для записи (в нашем случае это будет адрес строки для вывода на экран)
7-й параметр - Length
хранит размер записываемых данных (размер строки для вывода)
Все остальные параметры необязательные, и вместо них будет использоваться значение по умолчанию - 0. Но при желении про эти параметры можно прочитать в документации. Тепеь определим программу для вывода строки на консоль:
global __main import GetStdHandle ; импортируем функцию GetStdHandle для получения дескриптора файла area main, CODE __main stp fp, lr, [sp, #-16]! ; Сохраняем регистры FR и LR в стек mov x0, #-11 ; Аргумент для GetStdHandle - STD_OUTPUT bl GetStdHandle ; вызываем функцию GetStdHandle mov x1, #0 ; Второй аргумент mov x2, #0 ; Третий аргумент mov x3, #0 ; Четвертый аргумент ldr x4, =IoStatusBlock ; Пятый аргумент ldr x5, =message ; Шестой аргумент - строка для вывода mov x6, len ; Седьмой аргумент - длина строки mov x7, #0 ; Восьмой аргумент str xzr, [sp, #-16]! ; Восьмой аргумент - для него выделяем память в стеке svc #8 ; вызов системной функции NtWriteFile add sp, sp, #16 ; очищаем ранее выделенные в стеке 16 байт, где располагался 8-й аргумент ldp fp, lr, [sp], #16 ; Сохраняем регистры FR и LR в стек ret area DATA message dcb "Hello METANIT.COM!\n" ; строка для вывода len equ 19 ; размер строки align 4 IoStatusBlock space 16 ; буфер для получения статуса end
Для упрощения для получения дескриптора консольного вывода используем функцию GetStdHandle. Эта функция возвращает через регистр Х0 дескриптор. Так как эта функция изменяет регистр LR, то необходимо сохранить этот регистр в стек, а после вызова функции восстановить.
После получения дескриптора устанавливаем все параметры для системного вызова NtWriteFile. Стоит отметить, что поскольку он принимает 9 параметров, то аргумент для последнего параметра передается через стек. Однако поскольку стек должен быть выровнен по 16 байтам, то фактически в стеке выделяется 16 байт:
str xzr, [sp, #-16]!
Большинство параметров необязательные, и данном случае не имеют значения, поэтому передаем им значение 0. Ключевые параметры помещаются в регистр Х5 (адрес строки) и Х6 (длина строки)
ldr x5, =message ; Шестой аргумент - строка для вывода mov x6, len ; Седьмой аргумент - длина строки
После установки параметров вызываем системную функцию:
Поскольку в программе используется функция GetStdHandle, то при компоновке программы необходимо передать компоновщику системную библиотеку kernel32.lib. Полный консольный вывод программы с компиляцией:
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>