Системный вызов — это запрос к ОС, чтобы она выполнила некоторые операции для пользовательского процесса. В современных операционных системах некоторые операции ни в коем случае не могут выполняться непосредственно пользовательским процессом без помощи ОС. Примерами таких операций являются чтение с диска, запись на диск, разветвление процесса и т. д.

Зачем нужны системные вызовы

Есть две основные причины, по которым такие инструкции недоступны непосредственно для процесса:

  • Первый — защита: ОС должна сделать множество проверок, чтобы оценить право процесса запрашивать такую ​​инструкцию.
  • Второй заключается в том, что ОС должна обновлять свои структуры данных при выполнении инструкции. Если мы воспользуемся системным вызовом fork, ОС потребуется обновить множество структур данных, связанных с процессами, чтобы учесть новый процесс, добавить его в очередь выполнения и т. д.

Функция-оболочка системного вызова

Следует сделать очень важное различие. В C, когда мы открываем файл с помощью fopen("filename.txt","r"), мы не делаем системный вызов. Вместо этого мы вызываем функцию fopen библиотеки C, которая сделает за нас системный вызов open. Код функции fopen определен в реализации библиотеки C, такой как Glibc, а код вызова open system определен в ядре. ОС.

Переключение из пользовательского режима в режим ядра

Когда процесс выполняется, он может работать в двух режимах: пользовательском режиме или режиме ядра. Он запускается в пользовательском режиме, когда выполняет обычные инструкции ЦП, не требующие привилегий, таких как переход по адресу, загрузка из памяти, запись в память и т. д.Однако, когда процесс должен выполняться с привилегиями инструкции, которые он должен передать операционной системе для выполнения от ее имени, это то, что мы называем режимом ядра.

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

  1. Понятие текущей привилегии хранится в процессоре. Например, в процессорах x86 эта информация доступна в регистре CS под двухбитным флагом CPL (текущий уровень привилегий). Его значение равно 0 в режиме ядра и 3 в пользовательском режиме. Таким образом, первым шагом системного вызова является изменение значения CPL на 0.
  2. Сегмент кода больше не является сегментом кода процесса (который включает инструкции выполняемой программы). Теперь сегмент кода является сегментом кода ядра (это потому, что инструкции open доступны не в исполняемой программе, а в коде ядра).
  3. Сегмент стека больше не указывает на пользовательский стек, а указывает на стек ядра. Каждый процесс (в более общем случае каждый поток) имеет 2 стека: стек пользовательского режима, в котором мы выполняем инструкции в программе, и стек режима ядра, в котором мы выполняем инструкции ядра, такие как обработка системных вызовов. Есть много причин, по которым у нас два стека вместо одного. По сути, это вопрос защиты, чтобы никакая информация, которую ядро ​​использовало при выполнении, не была доступна процессу при переключении обратно в пользовательский режим, а во-вторых, чтобы код ядра не отвечал за переполнение стека пользовательской программы…

Для переключения программы из пользовательского режима в режим ядра нет другого способа, кроме прерываний.

В общем, прерывание может быть аппаратным прерыванием, таким как прерывание таймера, которое заставляет ЦП переключаться в режим ядра и выполнять, например, переключение процесса, или это может быть программное прерывание, вызванное самой программой, такое как деление нулем, ошибкой страницы или .. системным вызовом.

Прерывания обрабатываются операционной системой с помощью таблицы, называемой таблицей дескрипторов прерываний (IDT), которая сопоставляет каждый тип прерывания с функцией, которую ОС будет выполнять при возникновении прерывания (мы называем эту функцию обработчиком прерывания). Например, первая запись этой таблицы может содержать код, который будет выполняться при делении на ноль.

Инструкция «int $0x80»

Чтобы сделать системный вызов в процессоре x86, мы выполняем инструкцию int $0x80. int означает прерывание, а 0x80 (шестнадцатеричное число, соответствующее 128) — это позиция в таблице IDT обработчика системных вызовов. Перед вызовом этой инструкции мы сохраняем в четко определенных регистрах номер системного вызова функции, которую мы хотим выполнить (открыть, прочитать, разветвить) и ее аргументы. Например, номер системного вызова должен храниться в регистре EAX.

Выполнение этой инструкции произведет переключение из режима пользователя в режим ядра. Он извлекает сегмент стека ядра из сегмента TSS, а затем помещает регистры режима пользователя в этот стек ядра (регистр сегмента пользовательского стека SS, указатель стека ESP, регистр сегмента кода CS, EIP указателя инструкции и eflags). Затем сегмент кода ядра и указатель инструкции извлекаются из таблицы IDT, на которую указывает регистр idtr .

По завершении выполнения int $0x80 указатель инструкций указывает на первую инструкцию в обработчике системных вызовов, которая выполнит некоторые проверки и некоторое сохранение состояния перед выполнением инструкций read, open, .. или любого другого системного вызова, который мы сделали.

Стоит подчеркнуть разницу между обычным вызовом функции и системным вызовом. Когда мы выполняем вызов функции, мы помещаем аргументы в стек и переходим к функции. Системный вызов не может сделать то же самое, потому что мы не выполняем в одном и том же стеке, поэтому аргументы скорее хранятся в регистрах, и поскольку код ядра не находится в том же сегменте кода, поэтому простая инструкция перехода не будет работать. .

Возврат из системного вызова: инструкция «iret»

Когда ядро ​​завершило выполнение системного вызова, оно должно вернуть руку пользовательскому процессу. Точно так же, как существует специальная инструкция для переключения из пользовательского режима в режим ядра, существует еще одна специальная инструкция для переключения обратно в пользовательский режим. Речь идет о iret инструкции. Он извлекает пользовательские регистры, которые были отправлены int(CS, SS, ESP, EIP и eflags, и сохраняет их обратно в соответствующих регистрах. Сделав это, мы вернулись в пользовательский режим и готовы продолжить выполнение нашей программы.