Внедрение кода ассемблера в код С

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

В прошлых темах было рассмотрено вызов функций С в коде ассемблера и вызов функций ассемблера в коде С. Третья стратегия взаимодействия представляет вкрапление кода ассемблера в различные части программы на Си. Для этого применяется выражение 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850