При обращении к функциями 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.
Стоит отметить, что в программе лучше использовать адресацию относительно регистра %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>