Для взаимодействия с ресурсами системы на Windows, также как и на Linux, теоретически можно использовать системные вызовы или syscalls. Однако в Windows обращение к системным вызовам имеет свои особенности. Прежде всего, надо установить номер вызываемой системной функции в регистре RAX. И как и в общем случае для выполнения системного вызова применяется инструкция syscall:
movq НОМЕР_СИСТЕМНОГО_ВЫЗОВА, %rax syscall
Если системный вызов принимает параметры, то их можно передать, как и в общую функцию C/C++: для передачи в функцию первых четырех параметров используются регистры, но первый параметр передается через регистр R10, а не через RCX, как в случае с функциями C/C++. То есть первые четыре параметра передаются через R10, RDX, R8 и R9 соответственно. Результат вызова возвращается через регистр RAX.
Минусом ОС 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
: статус завершения процесса, в качестве которого выступает числовой код и который обычно возвращается данной функцией.
Применим данную функцию в программе:
.globl main .text main: movq $0, %r10 # первый параметр - ProcessHandle не указываем movq $17, %rdx # второй параметр - код статуса - произвольное число movq $44, %rax # в rax номер вызываемой системной функции - 44 - NtTerminateProcess syscall # вызываем системную функцию ret
Поскольку первый параметр необязательный, и мы хотим завершить текущую программу, то первому параметру через регистр R10 передаем значение 0. Второму параметру через регистр RDХ передается произвольный числовой код статуса, в нашем случае число 17.
Для вызова системной функции инструкции в регистр %rax передается числовой код функции - 44, и выполняется инструкция syscall.
Результат компиляции и вызова программы (допустим, код программы расположен в файле hello.s
и компилируется в файл hello.exe
):
c:\asm>as hello.s -o hello.o c:\asm>ld hello.o -o hello.exe c:\asm>hello.exe c:\asm>echo %ERRORLEVEL% 17 c:\asm>
Таким образом, функция 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. Но при желении про эти параметры можно прочитать в документации. Тепеь определим программу для вывода строки на консоль:
.globl main .data message: .asciz "Hello METANIT.COM\n" .equ message_len, . - message .balign 8 IoStatusBlock: .space 16 # буфер для получения статуса .text main: subq $88, %rsp movq $-11, %rcx # Аргумент для GetStdHandle - STD_OUTPUT call GetStdHandle # вызываем функцию GetStdHandle movq %rax, %r10 # первый аргумент movq $0, %rdx # Второй аргумент movq $0, %r8 # Третий аргумент movq $0, %r9 # Четвертый аргумент leaq IoStatusBlock(%rip), %rax # Пятый аргумент movq %rax, 40(%rsp) leaq message(%rip), %rax movq %rax, 48(%rsp) # Шестой аргумент - строка для вывода movq $message_len, 56(%rsp) # # Седьмой аргумент - длина строки movq $0, 64(%rsp) # Восьмой аргумент - для него выделяем память в стеке movq $0, 72(%rsp) # Девятый аргумент - для него выделяем память в стеке movq $8, %rax # вызов системной функции NtWriteFile syscall movq $0, %r10 # первый параметр - ProcessHandle не указываем movq $message_len, %rdx # второй параметр - код статуса - произвольное число movq $44, %rax # в rax номер вызываемой системной функции - 44 - NtTerminateProcess syscall # вызываем системную функцию addq $88, %rsp # очищаем стек ret
Для упрощения для получения дескриптора консольного вывода используем функцию GetStdHandle. Эта функция возвращает через регистр
RAX дескриптор, который помещаем в регистр R10. Стоит отметить, что поскольку системный вызов NtWriteFile принимает 9 параметров, соответственно все параметры
не поместятся в 4 стандартных регистра, поэтому выделяем достаточное местов в стеке. Причем пятый параметр (он же первый параметр, который помещается в стек) должен начинаться в стеке со смещения
40(%rsp)
. Большинство параметров необязательные, и данном случае не имеют значения, поэтому передаем им значение 0.:
movq %rax, %r10 # первый аргумент movq $0, %rdx # Второй аргумент movq $0, %r8 # Третий аргумент movq $0, %r9 # Четвертый аргумент leaq IoStatusBlock(%rip), %rax # Пятый аргумент movq %rax, 40(%rsp) leaq message(%rip), %rax movq %rax, 48(%rsp) # Шестой аргумент - строка для вывода movq $message_len, 56(%rsp) # # Седьмой аргумент - длина строки movq $0, 64(%rsp) # Восьмой аргумент - для него выделяем память в стеке movq $0, 72(%rsp) # Девятый аргумент - для него выделяем память в стеке
После установки параметров вызываем системную функцию:
movq $8, %rax # вызов системной функции NtWriteFile syscall
Поскольку в программе используется функция GetStdHandle, то при компоновке программы необходимо передать компоновщику системную библиотеку kernel32. Полный консольный вывод программы с компиляцией:
c:\asm>as hello.s -o hello.o c:\asm>ld hello.o -o hello.exe -lkernel32 c:\asm>hello.exe Hello METANIT.COM c:\asm>echo %ERRORLEVEL% 19 c:\asm>