При обращении к функциями C/C++ в коде ассемблера необходимо следовать некоторым соглашениям, в соответствии с которыми работает данные функции. Используя MASM, нам следует следовать соглашениям Microsoft Windows ABI (Application Binary Interface). Рассмотрим основные принципы этих соглашений
Между базовыми типами в C/C++ и ассемблера применяются следующие соответствия:
С/С++ | Размер (в байтах) | Ассемблер |
char/signed char | 1 | sbyte |
unsigned char | 1 | byte |
short int | 2 | sword |
short unsigned | 2 | word |
int | 4 | sdword |
unsigned (unsigned int) | 4 | dword |
long / long int | 4 | sdword |
unsigned long | 4 | dword |
long long | 8 | sqword |
unsigned long long | 8 | qword |
float | 4 | real4 |
double | 4 | real8 |
указатель | 8 | qword |
Также в плане использования регистров существуют следующие соглашения:
Вызывающий код передает первые четыре параметра в регистры. Для передачи в функцию первых четырех параметров (целочисленных) используются регистры RCX, RDX, R8 и R9 соответственно. Если параметры представляют числа с плавающей точкой, то они передаются через регистры XMM0, XMM1, XMM2 и XMM3.
Параметры начиная с 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 | b |
R8 | c |
XMM3 | 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.
Например, возьмем функцию printf()
. Через регистр RCX в функцию передается выводимая строка, а через остальные регистры
передаются значения, которые передаются на место спецификаторов в строке, например, %s, %d и т.д. Рассмотрим передачу параметров в эту функцию. Пусть у нас определен файл
hello.asm со следующим кодом:
option casemap:none .data user byte "Tom", 0 ; для спецификатора %s age dword 38 ; для спецификатора %d text byte "Name: %s Age: %d", 10, 0 .code ; подключаем определение функции printf() из C/C++ externdef printf:proc ; определяем процедуру hello public hello hello proc sub rsp, 40 ; резервируем в стеке 40 байт lea rcx, text ; в регистр rcx загружаем адрес строки text lea rdx, user ; в регистр rdx загружаем адрес строки name mov r8d, age ; в регистр r8 загружаем адрес переменной age call printf ; вызываем функцию printf add rsp, 40 ; восстанавливаем значение в стеке ret ; возвращаемся в вызывающий код hello endp end
В секции данных определены три переменных. Прежде всего это строка text, которая содержит спецификаторы %s
и %d
. Поскольку все строки в С должны завершаться
нулевым байтом, то последний байт этой строки - 0. Также для спецификатора %s определена строка user, а для спецификатора %d - переменная age.
В соответствие с требованиями Windows ABI, перед вызовом функции printf()
резервируем в стеке 40 байт:
sub rsp, 40 ; резервируем в стеке 40 байт
Из них 32 байта - это так называемое shadow storage или теневое хранилище, необходимое для вызова любой функции С/С++, даже если она не имеет пароаметров. Но при вызове функции hello в стек уже помещается адрес возврата и таким образом 8 байт в нем занято. Соответственно получим 32 байта (shadows storage) + 8 байт (адрес возврата) = 40 байт - это значение НЕ выровнено по 16 байтам (то есть не кратно 16 байтам). Нам же в соответствии с Windows ABI обязательно надо иметь стек выровненным по 16 байтам - чтобы 4 младших бита в RSP имели значение 0. Поэтому к 32 байтам для выравнивания добавляем 8 байт.
Для передачи этих значений в качестве параметров в функцию printf
сначала загружаем адрес строки text в регистр rcx:
lea rcx, text
В rdx загружаем адрес строки user для первого спецификатора - %s.
lea rdx, user
Значение для второго спецификатора - %d помещаем в регистр r8:
mov r8d, age
Стоит отметить, что поскольку age - 32-битное число, и для спецификатора %d требуется целое 32-битное число, то данные помещаем в младшие 32 бита регистра r8 - r8d. Кроме того, для этого используем инструкцию mov.
Для вызова ассемблерного кода определим файл app.c со следующим кодом на языке C:
#include <stdio.h> extern void hello(void); int main() { hello(); }
Сначала скомпилируем код ассемблера из файла hello.asm с помощью команды:
ml64 /c hello.asm
И скомпилируем файл app.c в файл приложения с помощью команды:
cl app.c hello.obj
После компиляции запустим файл app.exe:
c:\asm>ml64 /c hello.asm Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm c:\asm>cl app.c hello.obj Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32532 for x64 Copyright (C) Microsoft Corporation. All rights reserved. app.c Microsoft (R) Incremental Linker Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:app.exe app.obj hello.obj c:\asm>app Name: Tom Age: 38 c:\asm>
Если функция C/C++ принимает указатель, то, поскольку указатели в архитектуре x86-64 имеют размер 8 байт, соответствующий параметр тоже должен быть равен 8 байтам. В качестве указателя из кода на ассемблере можно передать адрес объекта, который вычисляется либо с помощью оператора offset, либо с помощью инструции lea. Например, определим в файле app.c следующую программу на С:
// подключаем внешнюю функцию calc из файла hello.asm extern void calc(void); void square(unsigned int* n) { *n = *n * *n; } int main(void) { calc(); }
Здесь определена функция square, которая принимает указатель на число типа unsigned int
и возводит его в квадрат.
Основные действия разворачиваются в функции calc, которая подключается как внешняя функция на ассемблере. Определим ее в файле hello.asm:
option casemap:none .data number dword 5 text byte "number = %u", 10, 0 .code externdef square:proc externdef printf:proc calc proc sub rsp, 40 lea rcx, number call square lea rcx, text ; в регистр rcx загружаем адрес строки text mov edx, number ; в регистр rdx загружаем адрес числа number call printf ; вызываем функцию printf add rsp, 40 ret calc endp end
Здесь данные, которые будут передаваться в функцию square, определены в виде переменной number. Перед вызовом функции square в регистр RCX помещаем адрес переменной number.
lea rcx,number
В качестве альтернативы можно было бы использовать оператор offset
:
mov rcx, offset number
Но инструкция lea
является более предпочтительной, потому что занимает меньше байт (7 против 10 у offset).
После выполнения функции square проверяем значение переменной number с помощью функции printf. Результат работы программы:
c:\asm>app number = 25