Взаимодействие с кодом C

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

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

Компилятор GNU для языка C позволяет компилировать программы, в которых взаимодействует код на ассемблере и код на языке Си.

Вызов функций С из кода на ассемблере

Вместо того, чтобы с нуля определять весь функционал программы на ассемблере, мы можем использовать уже готовый функционал языка C, который также обладает высокой производительностью. Например, какие-то определенные функции, для написания которых на ассемблере потребовалось бы много времени и для которых не нужна высокая производительность ассемблера. Для вызова функций языка C из кода на ассемблере необходимо добавить в программу среду выполнения языка C. Но стоит отметить, что различные функции среды выполнения С могут опираться на возможности конкретной операционной системы, например, Linux. И если программа создается под Linux (в том числе Android), то для компиляции приложения с помощью компилятора GCC на Windows среди цепочек компиляции на https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads лучше выбрать пакет aarch64-none-linux-gnu.

aarch64-none-linux-gnu для Windows

(Если в силу географической принадлежности доступ к сайту блокируется, то необходимый пакет инструментов для Windows можно загрузить отсюда - arm-gnu-toolchain-13.2.rel1-mingw-w64-i686-aarch64-none-linux-gnu.exe)

На Linux x86-64 можно установить аналогичный пакет gcc-aarch64-linux-gnu:

sudo apt-get install gcc-aarch64-linux-gnu

На Linux ARM64 можно установить пакет gcc:

sudo apt-get install gcc

Следует отметить, что aarch64-none-linux-gnu доступен только для разработки на Linux и Windows, а для MacOS не доступен.

Самый простой способ добавить в исполняемый файл среду выполнения C состоит в компиляции программы с помощью компилятора GNU для языка C, то есть gcc, который также включает и ассемблер as и который при установки располагается в том же каталоге:

взаимодействие кода ассемблера arm64 и кода на языке c

GCC при компиляции автоматически добавит среду выполнения языка C. В зависимости от платформы для компиляции применяется команда

aarch64-none-linux-gnu-gcc -o app app.s     // На Windows
aarch64-linux-gnu-gcc -o app app.s          // На Linux x86-64
gcc -o app app.s                            // На Linux ARM64

Эта команда вызовет ассемблер as для файла app.s и затем автоматически вызовет программу компоновщика ld, которая добавит среду выполнения языка C.

Следует отметить, что для запуска на некоторых системах (например, для запуска на Android) может потребоваться скомпилировать приложение с флагом -static для статической линковки функционала:

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

Рассмотрим простейший пример - выведем на консоль строку с помошью стандартной функции С - printf. Для этого определим файл app.s со следующим кодом:

.global main
main:                       // функция main
    STR LR,[SP,#-16]!       // сохраняем в стеке текущий адрес из регистра LR
    LDR X0, =message        // загружаем выводимую строку
    BL printf               // вызываем стандартную функцию printf языка С
    MOV X0, #0              // код возврата
    LDR LR, [SP], #16       // извлекаем из стека адрес в регистр LR
    RET                     // выходим из функции
.data
    message: .asciz "Hello METANIT.COM!\n"

Прежде всего стоит отметить, что метка, по которой располагается код программы, называется main, а не _start. Среда выполнения языка C уже имеет метку _start. Соответственно ожидается, что сначала будет загружаться среда выполнения C, а затем вызывается собственно код программы. И если мы оставим в программе на ассемблере метку _start, то мы получим ошибку, что такая метка определена более одного раза.

После загрузки срежа выполнения будет обращаться к функции main. И в данном случае код ассемблера по сути представляет функцию main

В функции main сначала сохраняем в стек текущий адрес из регистра LR. Далее в регистр X0 загружаем строку message. Эта строка определяется с помошью директивы .asciz, которая определяет строку с концевым нулевым байтом. Собственно в языке С строки как раз и представляют набор символов, который заканчивается нулевым байтом.

Далее инструкцией BL printf вызываем стандартную функцию языка С - printf, которая выводит строку на консоль.

Затем в регистр X0 помещаем код возврата - число 0. Восстанавливаем значение регистра LR и выходим из функции инструкцией RET.

Скомпилируем этот код с помощью команды:

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

В итоге у нас получится файл app, который содержит среду выполнения C. Минусом такого подхода является более раздутый код файла.

Вывод содержимого регистра

Пример выше, возможно, не очень показательный, поскольку без вызова функции printf нам потребовалось лишь на пару инструкций больше, чтобы вывести строку на консоль. Но посмотрим на другую задачу - вывод содержимого регистра на консоль. На ассемблере нам для этого потребовалось бы написать порядочное число инструкций, а использование функции printf позволяет быстро решить данную задачу:

.global main
main:                       // функция main
    STR LR,[SP,#-16]!       // сохраняем в стеке текущий адрес из регистра LR
    LDR X0, =str            // загружаем строку форматирования str
    MOV X1, #15             // для спецификатора %d
    MOV X2, #15             // для спецификатора %x 
    BL printf               // вызываем стандартную функцию printf языка С
    MOV X0, #0              // код возврата
    LDR LR, [SP], #16       // извлекаем из стека адрес в регистр LR
    RET                     // выходим из функции
.data
    str: .asciz "X1 = %ld  \tX2 = 0x%016lx\n"

Здесь выводимая на консоль строка представляет строку форматирования "X1 = %ld \tX2 = 0x%016lx\n". Здесь используются два спецификатора. Вместо первого спецификатора %ld вставляется десятичное число, которое в языке представляет тип long. Вместо второго спецификатора 0x%016lx вставляется шестнадцатеричное число типа long, для вывода которого используются 16 символов. Если число занимает меньше 16 символов, то оставшиеся позиции заполняются нулями. Фактически в данном случае мы будем выводить значение регистра X1 в виде десятичного числа и значение регистра X2 в виде шестнадцатеричного числа.

В итоге функция printf принимает три аргумента - сама выводимая строка и два значения для ее спецификаторов. Параметры передаются функциям в порядке следования через регистры X0, X1, X2. То есть первый параметр функции printf - строка помещается в регистр X0, второй параметр - значение для спецификатора %ld помещается в регистр X1, значение для второго спецификатора помещается в регистр X2. То есть, если бы мы вызывали функцию printf в Си, то условно ее вызов бы выглядел следующим образом:

printf("X1 = %ld  \tX2 = 0x%016lx\n", X1, X2);

Причем в обоих случае выводим одно и то же число - 15, только в разных системах исчисления. В итоге консольный вывод будет выглядеть следующим образом:

X1 = 15         X2 = 0x000000000000000f

Макрос для универсальной печати регистра

Пойдем дальше и абстрагируемся от конкретного регистра и вынесем код печати регистра в отдельный макрос. Для этого определим файл printRegister.s со следующим кодом:

.macro printRegister reg
    STP X0, X1, [SP, #-16]!
    STP X2, X3, [SP, #-16]!
    STP X4, X5, [SP, #-16]!
    STP X6, X7, [SP, #-16]!
    STP X8, X9, [SP, #-16]!
    STP X10, X11, [SP, #-16]!
    STP X12, X13, [SP, #-16]!
    STP X14, X15, [SP, #-16]!
    STP X16, X17, [SP, #-16]!
    STP X18, LR, [SP, #-16]!
    MOV X2, X\reg               // для спецификатора %d
    MOV X3, X\reg               // для спецификатора %x
    MOV X1, #\reg               // устанавливаем название регистра
    ADD X1, X1, #'0'            // для установки символа для спецификатора %c
    LDR X0, =str                // строка форматирования
    BL printf                   // вызываем функцию printf
    LDP X18, LR, [SP], #16
    LDP X16, X17, [SP], #16
    LDP X14, X15, [SP], #16
    LDP X12, X13, [SP], #16
    LDP X10, X11, [SP], #16
    LDP X8, X9, [SP], #16
    LDP X6, X7, [SP], #16
    LDP X4, X5, [SP], #16
    LDP X2, X3, [SP], #16
    LDP X0, X1, [SP], #16
.endm

.data
    str: .asciz "X%c = %ld, 0x%016lx\n"
    .align 4
.text

Вначале идет 10 строк для сохранения состояния регистров в стек, чтобы внешний код, в котором используется данный макрос, получил те же значения в регистрах, которые были до вызова макроса. Поскольку часть регистров может использовать сам макрос, некоторые регистры могут использовать вызываемые функции типа printf, и хорошей практикой считается сохранение вначале и восстановление в конце регистров.

Сам макрос имеет один параметр - reg, через который макрос получае номер регистра, например, число 1 будет представлять регистр X1.

На консоль здесь выводится строка, которая имеет следующее форматирование: "X%c = %ld, 0x%016lx\n". Через спецификатор %c передается символ - номер регистра. Через спецификатор %ld передается десятичное значение регистра. А через спецификатор %016lx передается шестнадцатеричное значение регистра.

Важный момент - инструкция .align 4. Она позволяет выравнить строку по 4 байтам, что позвляет более быстро загружать данные, выравненные по 4 байтам. И далее идет объявление секции .text

Таким образом, в функцию printf передается четыре параметра, Соответственно, чтобы вывести значение одного регистра нам потребуется 4 регистра. В регистры X2 и X3 помещаются значения для второго и третьего спецификаторов:

MOV X2, X\reg               // для спецификатора %d
MOV X3, X\reg               // для спецификатора %x

В регистр X1 помещается числовой код номера регистра. Чтобы получить числовой код ASCII, прибавляем к номеру числовой код символа "0"

MOV X1, #\reg               // устанавливаем название регистра
ADD X1, X1, #'0'            // для установки символа для спецификатора %c

В главном файле программы, который будет называться app.s, подключим макрос:

.include "printRegister.s"  // подключем макрос printRegister
.global main

main:                       // функция main
    STR LR,[SP,#-16]!       // сохраняем в стеке текущий адрес из регистра LR
    MOV X1, #0x0000000000000005
    MOV X2, #0xFFFFFFFFFFFFFFFF
    MOV X3, #0x000000000000000E
    MOV X4, #0x0000000000000009
    printRegister 1         // обращаемся к макросу, передавая номера регистров - X1
    printRegister 2         // X2
    printRegister 3         // X3
    printRegister 4         // X4
    MOV X0, #0              // код возврата
    LDR LR, [SP], #16       // извлекаем из стека адрес в регистр LR
    RET                     // выходим из функции

Здесь последовательно вызываем макрос printRegister значения регистров X1, X2, X3, X4 и выводим их на консоль. Консольный вывод:

X1 = 5, 0x0000000000000005
X2 = -1, 0xffffffffffffffff
X3 = 14, 0x000000000000000e
X4 = 9, 0x0000000000000009
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850