Системные вызовы Linux
Программы в операционной системе (Windows, Linux) могут работать в двух режимах: в режиме ядра и в пользовательском режиме. В режиме ядра работают компоненты самой операционной системы, драйвера. Обычные прикладные программы выполняются в пользовательском режиме. Это накладывает ограничения на возможности приложений. Так, программы, выполняемые в пользовательском режиме, обычно не имеют прямого доступа к аппаратному обеспечению устройства. Когда программам пользовательского режима действительно необходимо взаимодействовать с другими процессами, получать доступ к файлам и другим системным ресурсам или взаимодействовать с оборудованием, они должны делать это через предоставляемые ОС API-интерфейсы через так называемых системные вызовы (syscall).
Чтобы обартиться к системным ресурсам, программа выполняет системный вызов с помощью инструкции вызова супервизора (supervisor call или SVC). Эта инструкция заставляет процессор сгенерировать исключение SVC, что приводит к приостановке программы и немедленной передаче управления зарегистрированному в ядре обработчику SVC. Затем ядро ОС определяет, какой системный вызов был запрошен, и вызвает соответствующую функцию режима ядра для обслуживания запроса. Как только функция системного вызова завершена, результат системного вызова передается обратно программе, и программа пользовательского режима возобновляется с инструкции, следующей сразу за системным вызовом svc.
Если мы создаем программу под операционную систему Linux, то мы можем воспользоваться встроенные в Linux системными вызовами. Например:
.global _start // устанавливаем стартовый адрес программы _start: mov X0, #1 // 1 = StdOut - поток вывода ldr X1, =hello // строка для вывода на экран mov X2, #19 // длина строки mov X8, #64 // устанавливаем функцию Linux svc 0 // вызываем функцию Linux для вывода строки mov X0, #0 // Устанавливаем 0 как код возврата mov X8, #93 // код 93 представляет завершение программы svc 0 // вызываем функцию Linux для выхода из программы .data hello: .ascii "Hello METANIT.COM!\n" // данные для вывода
В данном случае используем два системных вызова. Первый системный вызов с номером 64 выводит строку в стандартный поток вывода. Второй используемый системный вызов с номером 93 завершает работу программы. В настоящий момент в Linux есть порядка более 400 различных системных вызовов. Они используют программные прерывания для переключения между контекстом наше1 программы и контекстом ядра.
В файловой системе в Linux все номера системных вызовов перечислены в файле /usr/include/asm-generic/unistd.h . Это заголовочный файл на языке Си. Например, возьмем номер вызова для вывода в стандартный поток
В данном случае константа __NR_write как раз и будет представлять системный вызов вывода данных в стандартный поток.
Системные вызовы Linux следуют следующим условностям:
- Через регистры X0–X7 передаются параметры для системных вызовов (то есть мы можем передать до 8 значений в системный вызов)
- В регистр X8 помещается номер системного вызова Linux
- Вызывается программное прерывание с помощью инструкции SVC 0
- Регистр X0 содержит код возврата системного вызова
В качестве кода возврата обычно при успешном выполнении используется 0 или положительное число, а при неудачном выполнении возвращается отрицательное число — код ошибки. Коды ошибок можно найти в других заголовочных файлах на языке Си по пути
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h
Для простоты все системные вызовы для Linux на архитектуре ARM64 перечислены в следующей статье.
Рассмотрим использование некоторых системных вызовов Linux на примере чтения-записи файла.
Запись и чтение файла
Сначала определим файл, который назовем macros.s и который будет содержать все нужные нам макросы для чтения и записи файла:
.EQU O_RDONLY, 0 // режим файла только для чтения .EQU O_WRONLY, 1 // режим файла только для записи .EQU O_CREAT, 0100 // режим создания файла .EQU S_RDWR, 0666 // права для чтения и записи файла .EQU AT_FDCWD, -100 // поиск файла в текущей папке // макрос печати на консоль строки .MACRO print str length MOV X0, #1 // 1 = StdOut - стандартный поток вывода LDR X1, =\str // загружаем адрес выводимой строки MOV X2, \length // в регистр X2 передаем результат макроса copy - длину строки из регистра X0 MOV X8, #64 // функция Linux для вывода в поток SVC 0 // вызываем функцию Linux .ENDM // макрос выхода из программы .MACRO exit code MOV X0, \code // код возврата MOV X8, #93 // устанавливаем функцию Linux для выхода из программы SVC 0 // Вызываем функцию Linux .ENDM // макрос открытия файла. Принимает имя файла и флаги режима файла .MACRO openFile fileName, flags MOV X0, #AT_FDCWD // открываем файл в текущей папке LDR X1, =\fileName // открываемый файл MOV X2, #\flags // открываем для чтения, записи или создания MOV X3, #S_RDWR // Права доступа - доступен для чтения и записи MOV X8, #56 // Функция открытия файла SVC 0 .ENDM // макрос чтения файла. Принимает дескриптор файла, буфер для считывания данных и кол-во считываемых байтов .MACRO readFile fd, buffer, length MOV X0, \fd // устанавливаем дескриптор файла LDR X1, =\buffer // Буфер для считывания MOV X2, #\length // Сколько считываем байтов MOV X8, #63 // устанавливаем функцию Linux для чтения файла SVC 0 // Вызываем функцию Linux .ENDM // макрос записи файла. Принимает дескриптор файла, буфер для записи в файл и кол-во записываемых байтов .MACRO writeFile fd, buffer, length MOV X0, \fd // устанавливаем дескриптор файла LDR X1, =\buffer // Буфер для записи в файл MOV X2, \length // Сколько записываем байтов MOV X8, #64 // устанавливаем функцию Linux для записи файла SVC 0 // Вызываем функцию Linux .ENDM // макрос сброса буфера в файл. Принимает дескриптор файла .MACRO flush fd MOV X0, \fd MOV X8, #83 // сброс данных из буфера в файл SVC 0 .ENDM // макрос закрытия файла. Принимает дескриптор файла .MACRO close fd MOV X0, \fd // дескриптор закрываемого файла MOV X8, #57 // Функция закрытия файла SVC 0 .ENDM
Вначале с помощью директивы .EQU определяется ряд констант, которые описывают режимы и права для работы с файлами и которые затем потребуется передавать в системные вызовы.
Затем определено 7 макросов для разных ситуаций:
- Макрос print принимает данные для вывода на консоль с помощью системного вызова 64
- Макрос exit принимает код возврата и завершает выполнение программы с помощью системного вызова 93
- Макрос openFile предназначен для открытия файла для его последующего чтения или записи
.MACRO openFile fileName, flags MOV X0, #AT_FDCWD // открываем файл в текущей папке LDR X1, =\fileName // открываемый файл MOV X2, #\flags // открываем для чтения, записи или создания MOV X3, #S_RDWR // Права доступа - доступен для чтения и записи MOV X8, #56 // Функция открытия файла SVC 0 .ENDM
- В регистр X0 передаем значение #AT_FDCWD . Поскольку оно равно -100, то программа будет искать файл в текущей папке.
- В регистр X1 помещается имя файла.
- В регистр X2 — флаги режима открытия (для чтения или для записи), которые заданы значениями O_RDONLY , O_WRONLY и O_CREAT
- В регистр X3 передаем значение #S_RDWR , то есть для файла устанавливаются права на чтение и запись
- И в регистр X8 передается собственно номер системного вызова
После выполнения в регистр X0 помещается дескриптор файла, который можно использовать для операций с этим файлом.
.MACRO readFile fd, buffer, length MOV X0, \fd // устанавливаем дескриптор файла LDR X1, =\buffer // Буфер для считывания MOV X2, #\length // Сколько считываем байтов MOV X8, #63 // устанавливаем функцию Linux для чтения файла SVC 0 // Вызываем функцию Linux .ENDM
- X0 — дескриптор файла
- X1 — адрес буфера для считывания данных из файла
- X2 — количество считываемых байт
- X8 — номер системного вызова — 63
.MACRO writeFile fd, buffer, length MOV X0, \fd // устанавливаем дескриптор файла LDR X1, =\buffer // Буфер для записи в файл MOV X2, \length // Сколько записываем байтов MOV X8, #64 // устанавливаем функцию Linux для записи файла SVC 0 // Вызываем функцию Linux .ENDM
- X0 — дескриптор файла
- X1 — адрес буфера для записи данных в файл
- X2 — количество считываемых байт
- X8 — номер системного вызова — 64
По сути мы используем тот же самый системный вызов, что и при выводе на консоль, только теперь в качестве цели вывода применяется файл.
Теперь определим основной файл программы — файл main.s , в котором подключим и используем выше определенные макросы:
.include "macros.s" // подключаем макросы .global _start _start: // запись // отрываем файл на запись openFile filename, O_CREAT+O_WRONLY ADDS X11, XZR, X0 // сохраняем дескриптор файла B.PL write // если нет ошибки, то переходим на метку write print errMessage 21 // выводим сообщение об ошибке B finish // переходим к завершению программы write: writeFile X11, input, 20 // запись в файл 20 символов flush X11 // сбрасываем данные в файл close X11 // закрываем файл print successMesage 16 // вывод сообщения об успешной записи // чтение // отрываем файл на чтение openFile filename, O_RDONLY ADDS X11, XZR, X0 // сохраняем дескриптор файла B.PL read // если нет ошибки, то переходим на метку read print errMessage 21 // выводим сообщение об ошибке B finish // переходим к завершению программы read: readFile X11, output, 255 // считываем файл close X11 // закрываем файл print output 255 // выводим считанные данные на консоль finish: exit 0 // выход из программы .data filename: .ascii "content25.txt" // имя файла errMessage: .ascii "Failed to open file.\n" // сообщение об ошибке input: .asciz "Hello METANIT.COM!\n" // строка для записи successMesage: .asciz "File is written\n" // сообщение об успехе output: .fill 256, 1, 0 // буфер для считывания данных
Сначала открываем файл для записи:
openFile filename, O_CREAT+O_WRONLY
Имя файла задается меткой filename , а для открытия файла в режиме записи применяем пару режимов O_CREAT+O_WRONLY
После открытия файла в регистр X0 помещается дескриптор файла. Но поскольку этот регистр часто используется, сохраняем дескриптор в регистр X11, который далее нигде не применяется.
Но следует учитывать, что при открытии файла мы можем столкнуться с ошибкой, особенно это касается работы с файлами в Android, и в этом случае в X0 будет отрицательный код ошибки. Ведь если возникла ошибка, то нет смысла записывать в файл или производить с ним другие операции. Поэтому нам надо отследить ошибку. Для этого складываем значение X0 с нулевым регистром:
Если значение в X0 отрицательное, то инструкция ADDS соответствующим образом устанавливает флаги. Так, в этом случае устанавливается флаг N . И с помощью инструции
можно проверить его установку. И если флаг НЕ установлен (то есть нет ошибки, значение в X0 положительное), то переходим на метку write . Если же произошла ошибка, то далее выводим на консоль сообщение об ошибке — errMessage и переходим к метке finish для выхода из программы.
Если открытие файла прошло успешно, то на метке write производим запись в файл строки input и закрываем файл:
write: writeFile X11, input, 20 // запись в файл 20 символов flush X11 // сбрасываем данные в файл close X11
Далее повторяем открытие файла, но теперь для его чтения:
openFile filename, O_RDONLY
И если все прошло удачно, переходим к метке read , где считываем из файла данные в буфер output , который представляет 256 байт:
read: readFile X11, output, 255 // считываем файл close X11 print output 255
В итоге в случае удачной записи/считывания консоль выведет:
File is written Hello METANIT.COM!