Системные вызовы Windows

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

Для взаимодействия с ресурсами системы на 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

Рассмотрим другой пример - запись данных в файл, и как частный случай, вывод строки на консоль. За это отвечает системная функция 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>
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850