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

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

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

Ассемблер GAS и компилятор GCC позволяет вызывать функции C/C++ в программе на ассемблере. Это упрощает создание приложений. Так, мы можем использовать уже готовые функции языка Си вместо того, чтобы тратить время и писать аналогичные функции.

При вызове функций в Linux применяется ряд условностей, которые еще называют ABI (Application Binary Interface или бинарный интерфейс приложения). В Linux применяемый интерфейс ABI еще называется "System V ABI". Согласно этим условностям вызываемая функция должна сохранять содержимое регистров %rbp, %rbx и %r12, %r13, %r14, %r15 (например, сохранять их в стек). Остальные регистры могут быть сохраняться по мере необходимости. Это также означает, что если вы вызываете функцию, то любой регистр, кроме указанных выше, может быть изменен во время вызова функции.

Параметры передаются в функцию через следующие регистры (в порядке следования параметров):

  1. %rdi

  2. %rsi

  3. %rdx

  4. %rcx

  5. %r8

  6. %r9

Итак, если есть только один параметр, он передается в %rdi. Если их два, первый параметр передается в %rdi, а второй — в %rsi. Если параметров более шести, то все дополнительные параметры помещаются в стек в виде чисел .quad (например, с помощью pushq). Последний параметр первым помещается в стек.

Возвращаемые значения возвращаются в %rax. Спецификация ABI также позволяет использовать %rdx, если есть второе возвращаемое значение, то есть значение возвращается через два регистра %rdx:%rax. Обычно, если требуется больше возвращаемых значений, либо %rax будет содержать указатель на набор значений, либо входные параметры будут включать указатели на участки памяти, где должны храниться эти дополнительные возвращаемые значения.

Если функция в качестве параметров принимает числа с плавающей точкой, то они передаются через регистры xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7. Если функция принимает значения как с плавающей точкой, так и целые числа, то целые числа передаются обычным способом (с использованием %rdi, %rsi, %rdx, %r8 и %r9), а числа с плавающей точкой передаются с использованием регистров XMM.

Кроме того, для функций (таких как printf), которые принимают переменное количество аргументов, количество используемых регистров XMM должно быть помещено в %rax. Причина этого в том, что вызываемая функция знает, сколько регистров XMM ей необходимо сохранить.

Если возвращаемое значение является значением с плавающей запятой, оно возвращается в %xmm0. Если возвращаются два значения, то через регистры %xmm1:%xmm0.

Например, пусть у нас есть функция с сигнатурой вызова

double myfunc(int a, double b, int c, double d)

В этом случае мы бы передали параметр a в %rdi, параметр b в %xmm0, параметр c в %rsi и параметр d в %xmm1.. Возвращаемое значение будет в %xmm0.

Если бы это была функция с переменным количеством аргументов, то мы бы установили %rax равным 2, чтобы указать, что только два регистра XMM предназначены для параметров.

Также согласно System V ABI, стек должен быть выровнен по размеру, кратному 16 байтам, непосредственно перед каждым вызовом функции. "Выравнивание по 16 байтам" означает, что адрес указателя стека (%rsp) должен быть кратен 16. Это не всегда обязательно, но вызовы некоторых функций приведут к сбою, если это выравнивание не будет соблюдаться. Поскольку все вызовы функций будут включать в себя сохранение адреса возврата, соответственно при вызове функции стек может быть выровнен по 8 байтам. Нередко в стек дополнительно сохраняют предыдущее значение регистра %RBP (базового указателя фрейма стека). В итоге получается 16 байт. Это упрощает управление стеком, поскольку внутри функции достаточно следует выделять в стеке размер, кратный 16 байтам. Хотя опять же это не является строго необходимым, если функция не вызывает другие функции.

Кроме того, при вызове функций, которые принимают неопределенное количество аргументов (так называемые функции с переменным числом аргументов), рекомендуется установить %rax равным нулю, если не нужно передавать какие-либо значения с плавающей запятой.

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

Статические библиотеки в Linux обычно заканчиваются расширением .a, которое означает "архив".

Для примера используем стандартную функцию abs(), которая возвращает абсолютное значение числа. То есть если мы передим отрицательное число, то функция вернет нам это число, умноженное на -1. Для этого определим следующий файл hello.s:

.globl _start 

.text
_start:
    movq $-5, %rdi  # параметр для функции abs
    call abs        # вызываем функцию

    movq %rax, %rdi

    movq $60, %rax      
    syscall

Через регистр %rdi функция получает единственный параметр - в данном случае число -5. Функция вызывается также, как и функции ассемблера - с помощью инструкции call. После вызова функции в регистр %rax будет содержаться результат.

Скомпилируем файл:

as hello.s -o hello.o

Функция abs определена в стандартной библиотеке языка C. Файлы библиотек называются по шаблону libX.a, где X - название библиотеки. Например, файл, который содержит функционал стандартной библиотеки Си, называется libc.a. Для компоновки с этим файлом библиотеки выполним команду:

ld hello.o -static -lc -o hello

Флаг -static указывает компоновщику, что необходимо добавить в программу функции библиотеки. А флаг -l указывает компоновщику на имя библиотеки, которую надо слинковать с программой. Стандартной библиотека языка Си, которая содержит функцию abs(), называется c. В итоге компоновщик будет искать функции в файле libc.a.

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

root@Eugene:~/asm# as hello.s -o hello.o
root@Eugene:~/asm# ld hello.o -static -lc -o hello
root@Eugene:~/asm# ./hello
root@Eugene:~/asm# echo $?
5
root@Eugene:~/asm#

Определение точки входа

Хотя описанный способ работает для функции abs, однако он в реальности редко используется. Нередко функции C требуют запуска определенных функций настройки. По этой причине при связывании с другими библиотеками в программе на ассемблере обычно в качестве точки входа используют метку main (а не _start). Код, который проецируется на метку _start, подключается из отдельной библиотеки, которая выполняет всю необходимую инициализацию библиотеки C.

Так, изменим код файла программы следующим образом:

.globl main

.text
main:
    movq $-5, %rdi
    call abs
    ret

Теперь входная точка программы называется "main". Кроме того, программа теперь завершается не системным выховом Linux, а инструкцией ret, как любая обычная функцияю

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

gcc hello.s -static -o hello

В данном случае мы вызываем компилятор GCC, который автоматически компонует программу с библиотекой c. При необходимости можно добавить дополнительные библиотеки, используя флаг -l и имя дополнительной библиотеки, которую надо связать. Флаг -static позволяет физически включить код из статических библиотек. Однако тут также есть минусы - к окончательному размеру кода добавляется не менее 500 килобайт.

Полный пример компиляции и выполнения программы:

root@Eugene:~/asm# gcc hello.s -static -o hello
root@Eugene:~/asm# ./hello
root@Eugene:~/asm# echo $?
5
root@Eugene:~/asm#

Благодаря пожобному подходу мы можем вызывать и другие стандартные функции С. Например, выведем на консоль некоторую строку с помощью функции printf:

.globl main

.data
message: .asciz "Hello METANIT.COM\n"  # строка форматирования

.text
main:
    subq $8, %rsp           # выравнивание должно быть по 16 байтам
    movq $message, %rdi     # первый параметр функции printf - строка форматирования
    call printf
    addq $8, %rsp
    ret

Перед вызовом функции printf установим выравнивание стека по 16-байтной границе. По умолчанию при вызове функции стек выровнен по 8-байтам, а регистр %rsp указывает на адрес возврата. Поэтому вычитаем еще 8 байт от адреса в %rsp:

subq $8, %rsp

Первый параметр функции printf представляет строку форматирования, которая выводится на экран. В нашем случае это глобальная переменная message, адрес которой помещается в регистр %rdi:

movq $message, %rdi

Пример компиляции и выполнение программы:

root@Eugene:~/asm# gcc hello.s -static -o hello
root@Eugene:~/asm# ./hello
Hello METANIT.COM
root@Eugene:~/asm#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850