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

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

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

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