Взаимодействие с кодом C/C++

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

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

Стоит отметить, что в программе лучше использовать адресацию относительно регистра %rip при доступе к данных. Например, для загрузки адреса из некоторой переменной message в регистр %rcx писать не

leaq message, %rcx

а

leaq message(%rip), %rcx

Например, выведем простейшую строку на консоль:

.globl main

.data
message: .asciz "Hello METANIT.COM!\n"     # сообщение

.text
main:
    subq $40, %rsp    # теневое хранилище 32 байта + 8 байт для выравнивания
    leaq message(%rip), %rcx
    call puts         # вызываем функцию puts
    addq $40, %rsp
    movq $22, %rax      # статусный код возврата
    ret

Здесь в секции .data определена строка message. Для ее вывода на консоль применяется встроенная библиотечная функция языка Си - функция puts:

int puts(char *s);

То есть функция принимает один параметр, соответственно данные для него передаются через регистр %rcx:

leaq message(%rip), %rcx

Вызов функции puts происходит также, как и вызов любой стандартной ассемблерной функции:

call puts

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

subq $40, %rsp    # теневое хранилище 32 байта + 8 байт для выравнивания

Стоит отметить, что для некоторых функций С/С++ можно вообще никак не изменить указатель стека, и эти функции будут работать. Тем не менее лучше придерживаться общих соглашений, даже если выделенное место в стеке расходуется впустую (как в случае выше).

Для компиляции выполним следующую команду:

gcc hello.s -o hello.exe

Она сформирует файл hello.exe, который будет весить порядка 130-140 КБ и который будет выводить строку на консоль:

c:\asm>gcc hello.s -o hello.exe

c:\asm>hello.exe
Hello METANIT.COM!


c:\asm>echo %ERRORLEVEL%
22

c:\asm>

Возьмем пример чуть посложнее. Допустим, нам надо передать в функцию 5 аргументов. Пусть это будет функция printf:

.globl main

.data
message: .asciz "Name: %s \nAge: %d \nPosition: %s \nCompany: %s\n"
name: .asciz "Tom"
age: .quad 39
position: .asciz "Developer"
company: .asciz "SuperComp"
.text
main:
    subq $40, %rsp
    leaq message(%rip), %rcx    # %rcx - первый параметр функции printf
    leaq name(%rip), %rdx    # %rdx - второй параметр функции printf
    movq age(%rip), %r8    # %r8 - третий параметр функции printf

    leaq position(%rip), %r9 # %r9 - четвертый параметр функции printf

    leaq company(%rip), %r10
    movq %r10, 32(%rsp)    # 32(%rsp) - пятый параметр функции printf
    call printf         # вызываем функцию printf
    addq $40, %rsp      # восстанавливаем стек
    movq $23, %rax      # статусный код возврата
    ret

Функция printf принимает строку форматирования и также может принимать произвольное количество аргументов, которые вставляются в эту строку. В данном случае передаем в функцию printf - 5 аргументов. Первые 4 параметра передаются через регистры %rcx, %rdx, %r8, %r9, а пятый параметр - через стек по смещению 32(%rsp)

Компиляция и консольный вывод программы:

c:\asm>gcc hello.s -o hello.exe

c:\asm>hello.exe
Name: Tom
Age: 39
Position: Developer
Company: SuperComp

c:\asm>echo %ERRORLEVEL%
23

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