Перейти к основному содержимому

Windows x64, WinAPI и отличия от Linux

Разработчику

Контекст: Windows x64, NASM, Intel. Первый запуск и MessageBox (32-bit) — Первая программа. Linux syscall — архитектура программ и справочник.


Два способа попросить ОС о работе

Linux (user mode)Windows (user mode)
syscall с номером в RAXвызов функций из DLL (kernel32.dll, ntdll.dll, …)
аргументы в RDI, RSI, …Microsoft x64 ABI + таблицы импорта PE
исполняемый ELFисполняемый PE (Portable Executable)

Программа на ассемблере под Windows не вызывает ядро напрямую: она зовёт WinAPI (или CRT), а те уже обращаются к системе.

Из-за этого основная сложность на практике — аккуратное соблюдение ABI и правил линковки PE-модуля. Как только эти правила фиксируются, остальной код становится довольно прямолинейным.


Microsoft x64 calling convention

Первые четыре целочисленных или указательных аргумента:

ПорядокРегистр
1RCX
2RDX
3R8
4R9

Далее — стек (справа налево при подготовке). Вещественные аргументы 1–4 — в XMM0XMM3.

Shadow space (home space): вызывающий резервирует 32 байта на стеке перед call, даже если функция принимает ≤4 аргументов. Это место, куда callee может временно сохранить регистровые аргументы.

Выравнивание: перед call RSP кратен 16 (с учётом того, что call кладёт 8 байт return address).

Возврат:

  • целое / указатель — RAXRDX для 128 бит);
  • float/doubleXMM0.

Callee-saved — RBX, RBP, RDI, RSI, R12R15, XMM6XMM15 (нижние 128 бит XMM6–15).

Это не System V AMD64 из статьи про C на Linux: порядок регистров другой, есть shadow space.


Импорт функций из DLL

В 32-битном stdcall имена декорированы: _MessageBoxA@16 (16 байт аргументов). В 64-битном C-стиле — без суффикса @N, с ведущим подчёркиванием в зависимости от линкера.

Код ITЗагрузка примера кода…

Разбор:

  • default rel включает относительную адресацию по умолчанию, поэтому lea rdx, [msg] формирует корректный адрес данных для позиционно-независимого кода.
  • В прологе push rbp / mov rbp, rsp / sub rsp, 48 функция создаёт стековый кадр, резервирует shadow space и сохраняет выравнивание стека по ABI Windows x64.
  • GetStdHandle получает дескриптор стандартного вывода: в RCX передаётся -11 (STD_OUTPUT_HANDLE), а результат возвращается в RAX.
  • Полученный дескриптор сохраняется в RBX, потому что это callee-saved регистр, его значение безопасно переносить между вызовами WinAPI.
  • Вызов WriteFile подготавливает 5 аргументов — первые четыре в RCX, RDX, R8, R9, пятый (lpOverlapped = NULL) кладётся в стек по адресу [rsp+32].
  • msg_len equ $ - msg вычисляет длину строки на этапе ассемблирования, поэтому в рантайме длину считать не нужно.
  • bytes_written dq 0 выделяет 8 байт под переменную, куда API запишет фактическое число записанных байтов (через указатель в R9).
  • ExitProcess завершает процесс с кодом из ECX; xor ecx, ecx передаёт код выхода 0.

Сборка (пример с Microsoft toolchain):

nasm -f win64 hello.asm -o hello.obj
link hello.obj kernel32.lib /subsystem:console /entry:main

Разбор:

  • nasm -f win64 hello.asm -o hello.obj ассемблирует исходник в 64-битный COFF-объект, пригодный для линковки в экосистеме MSVC.
  • Формат win64 важен: он определяет правила имён символов, релокаций и совместимость с Microsoft linker.
  • link hello.obj kernel32.lib ... связывает объект с import-библиотекой kernel32, где описаны GetStdHandle, WriteFile, ExitProcess.
  • /subsystem:console сообщает, что приложение консольное, поэтому у него есть стандартные потоки ввода/вывода.
  • /entry:main явно задаёт точку входа и обход стандартного CRT-стартапа в минимальном примере.

Точка входа может быть main (с CRT) или _start при -nostdlib — как в первой программе.


Сравнение с Linux syscall

Linux sys_write:

mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, len
syscall

Разбор:

  • RAX = 1 выбирает системный вызов sys_write в Linux x86-64.
  • RDI = 1 задаёт файловый дескриптор stdout, то есть вывод в стандартный поток.
  • RSI = msg передаёт адрес буфера, а RDX = len — количество байт для записи.
  • syscall передаёт управление ядру; ядро читает аргументы из регистров по Linux ABI и выполняет запись.
  • Этот пример показывает контракт Linux напрямую с ядром, в отличие от Windows-фрагмента выше, где используется слой WinAPI (WriteFile).

Windows WriteFile — больше параметров, handle консоли через GetStdHandle, другой ABI. Номера и регистры не переносятся между ОС.


Частые ошибки при переносе Linux -> Windows

ОшибкаЧто происходит
Использование RDI/RSI/... как в System Vаргументы уезжают не в те регистры
Нет shadow space перед callслучайные падения внутри WinAPI
Нарушено выравнивание RSPнестабильное поведение в вызовах DLL
Ожидание прямого syscall как в Linuxкод не соответствует пользовательскому контракту Windows

Файлы — CreateFile, ReadFile, CloseHandle

Типичная цепочка (логика, не полный листинг):

  1. CreateFileA/W — открыть или создать (RCX = имя, RDX = access, R8 = share, R9 = security; остальное на стеке).
  2. ReadFile / WriteFile — handle в RCX, буфер в RDX, размер в R8, указатель на DWORD "сколько прочитано" в R9.
  3. CloseHandleRCX = handle.

Ошибки — через GetLastError (возвращает код в RAX после вызова API в некоторых обёртках; уточняйте документацию вызываемой функции).

Скелет чтения файла (Win64, упрощённо):

; после CreateFile: handle в RBX
mov rcx, rbx ; hFile
lea rdx, [buffer]
mov r8, buffer_size
lea r9, [bytes_read]
mov qword [rsp+32], 0 ; lpOverlapped = NULL
call ReadFile
; bytes_read — сколько реально прочитано

mov rcx, rbx
call CloseHandle

Разбор:

  • CreateFile (не показан) должен вернуть валидный handle в регистр, здесь предполагается RBX.
  • ReadFile повторяет тот же контракт, что и WriteFile: 4 регистра + 5-й аргумент в shadow/stack.
  • lea r9, [bytes_read] передаёт адрес переменной, куда API запишет фактический размер чтения.
  • call CloseHandle освобождает дескриптор; утечка handle — типичная ошибка в WinAPI-коде.
  • Без проверки возвращаемого значения (test eax, eax / jz error) код хрупкий, но схема вызова видна явно.

PE и линковка

  • Секции .text, .data, .rdata аналогичны ELF по роли.
  • Таблица импорта подключает kernel32.dll автоматически при линковке с kernel32.lib.
  • Дизассемблирование: objdump из MinGW или dumpbin из MSVC — см. чтение листинга, формат другой, идея та же.

32-бит и 64-бит на Windows

32-bit64-bit
Вызов APIчаще stdcall, аргументы на стекерегистры + shadow
Имена@N суффиксбез байтового суффикса
РегистрыEAX, EBX, …RAX, RBX, …

Учебный MessageBox в Первая программа на ассемблереwin32 stdcall. Портирование на x64 требует перекладки аргументов в RCX, RDX, R8, R9 и shadow space.


Когда выбирать Windows-трек

  • драйверы и Win32-утилиты под заказчиков на Windows;
  • отладка чужих .exe / DLL;
  • сравнение с курсом на Linux в одной энциклопедии.

Для кроссплатформенного ядра знаний сначала закрепите Linux x86-64 + NASM (Первая программа на ассемблере в варианте syscall, Взаимодействие с C и C++), затем переносите идеи на WinAPI с заменой ABI.


Связанные материалы