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

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

Microsoft Windows ABI

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

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

    RCXa
    XMM1b
    R8c
    XMM3d
  • Параметры всегда представляют собой 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/C++

Например, возьмем функцию 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850