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
Первые четыре целочисленных или указательных аргумента:
| Порядок | Регистр |
|---|---|
| 1 | RCX |
| 2 | RDX |
| 3 | R8 |
| 4 | R9 |
Далее — стек (справа налево при подготовке). Вещественные аргументы 1–4 — в XMM0–XMM3.
Shadow space (home space): вызывающий резервирует 32 байта на стеке перед call, даже если функция принимает ≤4 аргументов. Это место, куда callee может временно сохранить регистровые аргументы.
Выравнивание: перед call RSP кратен 16 (с учётом того, что call кладёт 8 байт return address).
Возврат:
- целое / указатель —
RAX(иRDXдля 128 бит); float/double—XMM0.
Callee-saved — RBX, RBP, RDI, RSI, R12–R15, XMM6–XMM15 (нижние 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
Типичная цепочка (логика, не полный листинг):
CreateFileA/W— открыть или создать (RCX= имя,RDX= access,R8= share,R9= security; остальное на стеке).ReadFile/WriteFile— handle вRCX, буфер вRDX, размер вR8, указатель наDWORD"сколько прочитано" вR9.CloseHandle—RCX= 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-bit | 64-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.
Связанные материалы
- Несколько модулей —
global/externте же, меняется-f win64и библиотеки линкера. - Взаимодействие с C — на Windows используйте
extern "C"и соглашение компилятора MSVC. - Вещественные аргументы в API — SIMD и float.