При обращении к функциями C/C++ на Windows в коде ассемблера GAS необходимо следовать некоторым соглашениям, которые известны как Microsoft Windows ABI (Application Binary Interface). Эти соглашения определяют, как данные передеются в функции и как результат возвращается из функции. И стоит учитывать, что они отличаются от тех, которые действуют в Linux (System V ABI). Рассмотрим основные принципы Microsoft Windows ABI.
Вызывающий код передает первые четыре параметра в регистры. Для передачи в функцию первых четырех параметров (целочисленных) используются регистры RCX, RDX, R8 и R9 соответственно. Если параметры представляют числа с плавающей точкой, то они передаются через регистры XMM0, XMM1, XMM2 и XMM3 и дублируются в соответствующих регистрах RCX/RDX/R8/R9. То есть например, если второй параметр представляет число с плавающей точкой, то оно помещается в XMM1 и дублируется в RDX.
Параметры начиная с 5-го передаются через стек. Так, 5-й параметр должен занимать в стеке место по адресу RSP+32, 6-й параметр - по адресу RSP+40 и так далее.
Если процедура имеет смешанные параметры - одновременно и целочисленные, и с плавающей запятой, то каждый параметр помещается в соответствующий его номеру регистр RCX/RDX/R8/R9 или XMM0/XMM1/XMM2/XMM3. Например, если у нас есть следующая функция C/C++
void someFunc(int a, double b, char *c, double d)
то параметры будут размещаться следуюшим образом
RCX | a |
XMM1 и RDX | b |
R8 | c |
XMM3 и R9 | d |
Параметры всегда представляют собой 8-байтовые значения.
Вызывающий код должен зарезервировать для параметров в стеке как минимум 32 байта, даже если параметров меньше четырех - это так называемое shadow storage или теневое хранилище. Условно можно сказать, что первый параметр занимает в стеке память по адресу RSP, второй - по адресу RSP+8, третий - RSP+16, четвертный - RSP+24. Но в реальности это хранидище можно использовать для своих целей, например, как место хранения локальных переменных
Указатель стека RSP должен быть выровнен по 16 байтам непосредственно перед тем, как инструкция call поместит адрес возврата в стек.
Регистры в Microsot Windows ABI делятся на две группы:
Volatile-регистры (изменяемые регистры) - они могут изменять свои значения, и функции/процедуре не нужно сохранять значения этих регистров при вызове другой функции/процедуры. Это такие регистры как RAX, RCX, RDX, R8, R9, R10 и R11, а также XMM0/YMM0 – XMM5/YMM5
Nonvolatile-регистры (неизменяемые регистры) - они должны сохранять информацию при вызове функций/процедур. Поэтому процедура/функция должна сохранять значения этих регистров во время вызова. Если процедура изменяет один из этих регистров, она должна сохранить значение регистра перед первой такой модификацией и восстановить значение регистра из сохраненного местоположения перед возвратом. Это такие регистры как RBX, RBP, RDI, RSI, RSP, R12, R13, R14 и R15, а также XMM6/YMM6 - XMM15/YMM16 (старшую половину регистра YMM6-YMM16 можно изменить)
Если функция возвращает целое число, то оно помещается в регистр RAX, если число с плавающей точкой - то в регистр XMM0.
При использовании функций C/С++ в программе на ассемблере в качестве точки входа используют метку main (а не _start):
global main ; функция main - точка входа section .text main: ; инструкции функции main ret
Кроме того, программа теперь завершается не системным вызовом Linux, а инструкцией ret, как любая обычная функцияю
Код, который проецируется на метку _start, подключается из отдельной библиотеки, которая выполняет всю необходимую инициализацию библиотеки C. Затем этот код с помощью инструкции call вызывает функцию main, которая определена в нашем приложении. После завершения работы функция main с помощью инструкции ret возвращает управление коду на Си.
Возьмем в качестве примера программу, которая выводит строку на консоль с помощью функции printf:
global main ; функция main - точка входа extern printf ; подключаем функцию printf section .data message db "Hello METANIT.COM",10 ; строка для вывода section .text main: sub rsp, 40 ; выравнивание должно быть по 16 байтам c учетом 32 байтов - shadows storage для функции lea rcx, [rel message] ; первый параметр функции printf - строка для вывода на консоль call printf add rsp, 40 ; восстанавливаем стек ret
В отличие от кода на Linux для передачи первого параметра здесь используется регистр RCX, а не RDI. Ну и кроме того для загрузки адреса строки в регистр применяется инструкция
Также скомпилируем объектный файл с помощью стандартной команды:
nasm -f win64 hello.asm -o hello.o
Если для компоновки применяется GCC, то для компоновки в исполняемый файл используется следующая команда:
gcc hello.o -o hello.exe
Если для компоновки используется утилита link.exe от Microsoft из Visual C++, то все несколько сложнее. В этом случае нам дополнительно надо подключить пару библиотек - "legacy_stdio_definitions.lib" и "msvcrt.lib":
link hello.o legacy_stdio_definitions.lib msvcrt.lib /subsystem:console /out:hello2.exe
Для других линкеров/компоновщиков настройки и конкретные параметры также могут отличаться.
Рассмотрим более сложный пример, когда в функция printf принимает несколько параметров:
global main ; функция main - точка входа extern printf ; подключаем функцию section .data message db "Name: %s Age: %u Company: %s Salary: %u",10,0 ; строка для вывода name db "Tom",0 age dq 39 company db "METANIT.COM",0 salary dq 1150 section .text main: sub rsp, 40 ; выравнивание должно быть по 16 байтам lea rcx, [rel message] ; первый параметр функции printf - строка форматирования lea rdx, [rel name] ; первый аргумент для строки форматирования mov r8, [rel age] ; второй аргумент для строки форматирования lea r9, [rel company] ; третий аргумент для строки форматирования mov r10, [rel salary] mov qword [rsp+32], r10 ; четвертый аргумент для строки форматирования call printf add rsp, 40 ; восстанавливаем стек ret
В данном случае передаем в функцию printf 5 аргументов. Первые 4 параметра передаются через регистры rcx, rdx, r8, r9, а пятый параметр - через стек по смещению rsp+32