В прошлых темах было рассмотрено вызов функций С в коде ассемблера и вызов функций ассемблера в коде С. Третья стратегия взаимодействия представляет вкрапление кода ассемблера в различные части программы на Си. Для этого применяется выражение asm() - внутри скобок определяется код на ассемблере, который может напрямую обращаться к коду на языке Си. В общем случае инструкция asm() имеет следующий синтаксис:
asm asm-qualifiers ( AssemblerTemplate : OutputOperands [ : InputOperands] [ : Clobbers ] ] [ : GotoLabels])
Инструкция принимает следующие параметры:
AssemblerTemplate
: собственно код ассемблера. Он может содержать подстановки, которые начинаются с символа % и вместо
которых компилятор языка С подставляет входные и выходные параметры.
OutputOperands:
: выходные параметры - список переменных или регистров, через которые код ассемблера возвращает некоторый результат.
InputOperands
: входные параметры - список переменных или регистров, через которые в код ассемблера передаются некоторые значения.
Clobbers
: список регистров, которые будут использоваться программой и значения которых будут затерты в процессе выполнения программы.
GotoLabelsr
: список меток в коде С, на которые можно совершать переход из кода ассемблера.
Рассмотрим на примере копирования строки на ассемблере, которое выполняется внутри программы на С. Для этого определим файл app.c со следующим кодом на языке С:
#include <stdio.h> int main(void) { char source[] = "Hello Work"; // строка, которую копируем char target[20]; // куда копируем строку int length; // количество скопированных символов asm ( "MOV X4, %2\n" // сохраняем адрес начала строки, чтобы потом вычислить ее длину // в цикле получаем все байты, пока не дойдем до нулевого байта "loop: LDRB W5, [X0], #1\n" // загружаем из X0 один байт - один символ в W5 и увеличиваем адрес в X0 на 1 байт "CMP W5, #0\n" // сравниваем с нулевым байтом "STRB W5, [%2], #1\n" // если символы равны, заменяем байт по адресу X1 и увеличиваем адрес в X1 на 1 байт "B.NE loop\n" // если ненулевой байт, переход обратно к метке loop для копирования следующего символа "SUB %0, %2, X4\n" // помещаем в регистр X0 длину строки - X0 = X1 - X4 : "=r" (length) : "r" (source), "r" (target) : "r4", "r5" ); printf("Source: %s \n", source); printf("Target: %s \n", target); printf("String length: %d \n", length); return 0; }
В данном случае копируем строку source
в массив символов target
. А переменная length
будет хранить количество скопированных символов.
Основная логика размещена в инструкции asm. Рассмотрим ее по компонентам:
Параметр AssemblerTemplate
представляет набор строк на ассемблере, каждая из которых заканчивается символом
перевода строки "\n". Его суть состоит в том, что мы проходим по каждому байту из строки, адрес которой хранится в регистре Х0 и копируем его в строку, адрес которой хранится в регистре Х1. Когда дойдем
до нулевого байта, то выходим из цикла, в регистр Х0 помещаем длину строки и завершаем выполнение функции.
Параметр OutputOperands
содержит один выходной параметр
: "=r" (length)
в этом выражении =r
означает выходной регистр (X0), а (length)
указывает, что результат этого регистра будет помещаться в переменную length
. Таким образом,
будет осуществляться связь результата кода и переменных С.
Параметр InputOperands
содержит два параметра
: "r" (source), "r" (target)
Это выражение означает, что у нас будет два входных параметра. Первый параметр будет представлять переменную source
и будет помещаться в регистр X0, а второй параметр
представляет переменную target
и помещается в следующий по номеру регистр X1.
Параметр Clobbers
в данном случае представляет два значения
: "r4", "r5"
Это выражение означает регистры X4 и X5
По умолчанию входным и выходным параметрам присваиваются имена наподобие %0, %1, %2 и т.д. Плейсхолдер %0
будемт представлять результат, который помещается в переменную
length
. Плейсхолдеры %1
и %2
представляют соответственно первый и второй параметры - адреса строк source
и target
Фактически код ассемблера будет эквивалентен следующему:
MOV X4, X1 // сохраняем адрес начала строки, чтобы потом вычислить ее длину // в цикле получаем все байты, пока не дойдем до нулевого байта loop: LDRB W5, [X0], #1 // загружаем из X0 один байт - один символ в W5 и увеличиваем адрес в X0 на 1 байт CMP W5, #0 // сравниваем с нулевым байтом STRB W5, [X1], #1 // если символы равны, заменяем байт по адресу X1 и увеличиваем адрес в X1 на 1 байт B.NE loop // если ненулевой байт, переход обратно к метке loop для копирования следующего символа SUB X0, X1, X4
Скомпилируем программу
aarch64-none-linux-gnu-gcc -o app app.s -static // На Windows aarch64-linux-gnu-gcc -o app app.s -static // На Linux x86-64 gcc -o app app.s -static // На Linux ARM64
Запустим приложение и получим следующий консольный вывод:
Source: Hello Work Target: Hello Work String length: 11