Вызов функций C/C++ из ассемблера на Windows

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

Microsoft Windows ABI

При обращении к функциями 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)
    

    то параметры будут размещаться следуюшим образом

    RCXa
    XMM1 и RDXb
    R8c
    XMM3 и R9d
  • Параметры всегда представляют собой 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 возвращает управление коду на Си.

Вызов функций C/C++ на Windows:

Возьмем в качестве примера программу, которая выводит строку на консоль с помощью функции 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. Ну и кроме того для загрузки адреса строки в регистр применяется инструкция lea и адресация относительно регистра RIP.

Также стоит учитывать, что перед вызовом функции устанавливаем стек - прибавляем к указателю в RSP 32 байта для теневого хранилища shadow storage + 8 байт для выравнивания стека, то есть в сумме 40 байт. Почему 40 байт? По умолчанию при запуске программы указатель стека RSP имеет выравнивание по 8 байтам - в него помещен адрес возврата. Перед вызовом функции С/С++ следует установить выравнивание по 16-байтовой границе. То есть если к изначальному выравниванию по 8 байтам прибавить shadow storage - 32 байт, подучится 40 байт. Ближайщее число, которое равно или больше и которое кратно 16 - это число 48. Соотвественно надо прибавить еще 8 байт для выравнивания.

Также скомпилируем объектный файл с помощью стандартной команды:

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

Для других линкеров/компоновщиков настройки и конкретные параметры также могут отличаться.

Передача параметров в функцию C/C++

Рассмотрим более сложный пример, когда в функция 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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850