Системные вызовы в MacOS. Консольный ввод и вывод

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

За взаимодействие с системой и обращение к ее ресурсам отвечают системные вызовы или 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   // максимальная длина имени
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850