Системные вызовы Linux

Системные вызовы Linux. Чтение и запись файла

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

Программы в операционной системе (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. Это заголовочный файл на языке Си. Например, возьмем номер вызова для вывода в стандартный поток

#define __NR_write 64

В данном случае константа __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
    

    Он принимает имя открываемого файла и фраги - режим открытия. Для открытия файла применяется системный вызов 56. Для ее работы надо установить несколько регистров:

    • В регистр X0 передаем значение #AT_FDCWD. Поскольку оно равно -100, то программа будет искать файл в текущей папке.

    • В регистр X1 помещается имя файла.

    • В регистр X2 - флаги режима открытия (для чтения или для записи), которые заданы значениями O_RDONLY, O_WRONLY и O_CREAT

    • В регистр X3 передаем значение #S_RDWR, то есть для файла устанавливаются права на чтение и запись

    • И в регистр X8 передается собственно номер системного вызова

    После выполнения в регистр X0 помещается дескриптор файла, который можно использовать для операций с этим файлом.

  • Макрос readFile считывает содержимое файла в буфер

    .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

  • Макрос writeFile записывает данные из буфера в файл

    .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

    По сути мы используем тот же самый системный вызов, что и при выводе на консоль, только теперь в качестве цели вывода применяется файл.

  • Макрос flush сбрасывает оставшиеся данные из буфера в файл. Он принимает дескриптор записываемого файла и номер системного вызова - 83

  • Макрос close закрывает файл. Он принимает дескриптор записываемого файла и номер системного вызова - 57

Теперь определим основной файл программы - файл 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 с нулевым регистром:

ADDS X11, XZR, X0

Если значение в X0 отрицательное, то инструкция ADDS соответствующим образом устанавливает флаги. Так, в этом случае устанавливается флаг N. И с помощью инструции

B.PL write

можно проверить его установку. И если флаг НЕ установлен (то есть нет ошибки, значение в 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!
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850