За взаимодействие с системой и обращение к ее ресурсам отвечают системные вызовы или syscalls. Список всех системных вызовов, а также их номера, названия функций и параметры можно посмотреть на странице https://opensource.apple.com/source/xnu/xnu-1504.3.12/bsd/kern/syscalls.master
В целом Apple следует условностям ARM ABI:
Параметры для системных вызовов передаются через регистры X0–X7 в порядке следования, то есть значение для первого параметра передается через регистр Х0, значение для второго - через регистр Х1 и так далее
В регистр X16 помещается номер системного вызова MacOS
Вызывается программное прерывание с помощью инструкции SVC 0
или SVC 0x80
Регистр X0 содержит код возврата системного вызова - результат функции
Например, для выхода из программы отвечает системный вызов с номером 1, который представляет функцию exit
:
void exit(int rval);
В качестве параметра эта функция принимает один целочисленный параметр, который представляет код возврата из функции. Через этот код можно передать, например, код ошибки. И программа должна завершаться с помощью данного вызова.
Рассмотрим еще пару системных функций. Системный вызов для вывода данных на файл имеет номер 4 и реализуется с помощью следующей функции:
user_ssize_t write(int fd, user_addr_t cbuf, user_size_t nbyte);
Она принимает три параметра:
fd
: числовой дескриптор файла, в который записываются данные. Если мы собираемся выводиться данные на консоль (точнее в стандартный поток вывода),
то этому параметру передается число 1
cbuf
: адрес буфера (набора байт), который записывается в файл
user_size_t
: количество записываемых байт
Функция возвращает реальное число записанных в файл байт.
И системный вызов для чтения данных с файла имеет номер 3 и представляет следующую функцию:
user_ssize_t read(int fd, user_addr_t cbuf, user_size_t nbyte);
Эта функция также принимает три параметра:
fd
: числовой дескриптор файла, из которого считываются данные. Если мы считываем данные с консоли (из стандартного потока ввода),
то этому параметру передается число 0
cbuf
: адрес буфера (набора байт), в который считываются данные
nbyte
: количество считываемых байт
Функция возвращает реальное число считанных из файла байт.
Применим эти функции. Допустим, пользователь должен ввести свое имя, а программа в ответ выводит ему приветствие.
.text .global _start .align 2 _start: // печать приглашения к вводу adrp x1, prompt@PAGE // адрес приглашения к вводу add x1, x1, prompt@PAGEOFF mov x2, promptLen bl _print // ввод данных adrp x1, name@PAGE // адрес буфера add x1, x1, name@PAGEOFF mov x2, maxNameLen // максимальное количество символов для ввода bl _read // после этого вызова в регистре Х0 реально считанное количество символов // сохраняем его в переменную nameCount adrp x2, nameLen@PAGE // получаем адрес переменной nameLen add x2, x2, nameLen@PAGEOFF // str x0, [x2] // сохраняем размер введенной строки в nameLen // выводим приветствие adrp x1, hello@PAGE // получаем адрес строки приветствия add x1, x1, hello@PAGEOFF mov x2, helloLen bl _print // выводим имя adrp x1, name@PAGE // адрес имени add x1, x1, name@PAGEOFF adrp x2, nameLen@PAGE // передаем в Х2 адрес страницы переменной count ldr x2, [x2, nameLen@PAGEOFF] bl _print // выход из программы bl _exit // Функция для печати строки на консоль // Параметры // Х1 - адрес строки // Х2 - длина строки _print: mov x0, #1 // для вывода на консоль mov x16, #4 // системная функция write - номер 4 svc 0 // вызов системной функции ret // Функция для ввода строки с консоли // Параметры // Х1 - адрес буфера для ввода // Х2 - длина буфера _read: mov x0, #0 // ввод с консоли mov x16, #3 // номер системной функции read svc 0 // вызываем функцию ret // Функция завершения _exit: mov x0, #0 // устанавливаем код возврата mov x16, #1 // функция exit svc 0 // вызываем системную функцию ret .data prompt: .ascii "Print your name: \n" // приглашение к вводу .equ promptLen, . - prompt // длина приглашения .align 2 hello: .ascii "Hello " // приветствие .equ helloLen, . - hello // длина приветствия .align 3 nameLen: .quad 0 // реальная длина имени name: .fill 20, 1, 0 // для ввода имени - максимальная длина 20 байт .equ maxNameLen, . - name // максимальная длина имени
Здесь работа программы начинается с вывода на консоль приглашения к вводу:
adrp x1, prompt@PAGE // адрес приглашения к вводу add x1, x1, prompt@PAGEOFF mov x2, promptLen bl _print
В регистр Х1 загружаем адрес строки prompt - приглашения к вводу, а в Х2 - размер этой строки. Для вывода этой строки вызываем функцию _print, которая определена ниже
_print: mov x0, #1 // для вывода на консоль mov x16, #4 // системная функция write - номер 4 svc 0 // вызов системной функции ret
В функции _print в регистр Х0 передаем значение для первого параметра системной функции write - число 1, которое указывает, что вывод идет на консоль. Значения для второго и третьего параметров системной функции write передаются через регистры Х1 и Х2, которые установлены выше.
Далее идет ввод данных. Для ввода данных определена переменная name размером в 20 байт. Ее адрес передается в в регистр Х1, а ее размер - в регистр Х2, и затем вызывается функция _read
adrp x1, name@PAGE // адрес буфера add x1, x1, name@PAGEOFF mov x2, maxNameLen // максимальное количество символов для ввода bl _read
Но функция _read в реальности вызывается системную функцию read для считывания данных
_read: mov x0, #0 // ввод с консоли mov x16, #3 // номер системной функции read svc 0 // вызываем функцию ret
Функции read через регистр Х0 передается первый параметр - число 0, которое указывает, что ввод данных идет с консоли. Второй и третий параметр системной функции read передаются соответственно через регистры Х1 и Х2 и устанавливаются до вызова _read.
При ввода данных программа максимально может считать maxNameLen байт. Однако реально считанное количество может быть меньше, если мы вводим имя меньше maxNameLen байт. Это количество системная функция read возвращает через регистр Х0. И мы сохраняем это значение в переменную nameLen для дальнейшего использования
adrp x2, nameLen@PAGE // получаем адрес переменной nameLen add x2, x2, nameLen@PAGEOFF // str x0, [x2] // сохраняем размер введенной строки в nameLen
Далее при выводе на консоль введенного имени мы достаем это значение из переменной nameLen и передаем системной функции write в качестве третьего параметра:
adrp x1, name@PAGE // адрес имени add x1, x1, name@PAGEOFF adrp x2, nameLen@PAGE // передаем в Х2 адрес страницы переменной count ldr x2, [x2, nameLen@PAGEOFF] bl _print
В конце вызывается функция завершения программы - _exit, которая в реальности вызывает системную функцию exit, передавая в регистр Х16 номер функции - 1, а в Х0 в качестве параметра функции - число 0:
_exit: mov x0, #0 // устанавливаем кол возврата mov x16, #1 // функция exit svc 0 // вызываем системную функцию ret
В итоге программа сначала выведет приглашение к вводу, пользователь введет свое имя, и программа выведет приветствие. Пример работы программы:
eugene@MacBook-Pro-Eugene arm64 % as -o hello.o hello.s eugene@MacBook-Pro-Eugene arm64 % ld -o hello hello.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start eugene@MacBook-Pro-Eugene arm64 % ./hello Print your name: Eugene Hello Eugene eugene@MacBook-Pro-Eugene arm64 %
Следует отметить, что поскольку здесь довольно часто применяются несколько инструкций для получения адреса переменной, то мы можем упростить программу с помощью макросов:
.text .global _start .align 2 .macro loadAddr reg, variable adrp \reg, \variable@PAGE add \reg, \reg, \variable@PAGEOFF .endm .macro loadVal reg, variable adrp \reg, \variable@PAGE ldr \reg, [\reg, \variable@PAGEOFF] .endm _start: // печать приглашения к вводу loadAddr x1, prompt mov x2, promptLen bl _print // ввод данных loadAddr x1, name mov x2, maxNameLen bl _read // после этого вызова в регистре Х0 реально считанное количество символов // сохраняем его в переменную nameCount loadAddr x2, nameLen str x0, [x2] // выводим приветствие loadAddr x1, hello mov x2, helloLen bl _print // выводим имя loadAddr x1, name loadVal x2, nameLen bl _print // выход из программы bl _exit // Функция для печати строки на консоль // Параметры // Х1 - адрес строки // Х2 - длина строки _print: mov x0, #1 // для вывода на консоль mov x16, #4 // системная функция write - номер 4 svc 0 // вызов системной функции ret // Функция для ввода строки с консоли // Параметры // Х1 - адрес буфера для ввода // Х2 - длина буфера _read: mov x0, #0 // stdin mov x16, #3 // read svc 0 // call syscall ret // Функция завершения _exit: mov x0, #0 // устанавливаем кол возврата mov x16, #1 // функция exit svc 0 // вызываем системную функцию ret .data prompt: .ascii "Print your name: \n" // приглашение к вводу .equ promptLen, . - prompt // длина приглашения .align 2 hello: .ascii "Hello " // приветствие .equ helloLen, . - hello // длина приветствия .align 3 nameLen: .quad 0 // реальная длина имени name: .fill 20, 1, 0 // для ввода имени - максимальная длина 20 байт .equ maxNameLen, . - name // максимальная длина имени