#опытным
Существует строгая граница между приложениями, которые вы запускаете, и ядром компьютера (аппаратным обеспечением и ядром ОС).
Зачем она нужна? Пользователям нельзя доверять. Нужно оградить процессы, чтобы они не пострадали от действий другого процесса(написанного говнокодером или хакером).
Системный вызов является фундаментальным интерфейсом между приложением и ядром системы. Только через него пользователи могут запросить у системы выполнение какой-то задачи.
Для большинства сисколы - это просто хэндлеры, которые они дергают, чтобы получить нужный результат. Но там много интересностей происходит под капотом и именно об этом пойдет сегодня речь.
Будем все рассматривать на примере linux, как самой популярной серверной ОС.
0️⃣ В коде все начинается с какой-нибудь обертки для системного вызова
Оператор<< для плюсового объекта потока принимает высокоуровневые объекты и вызывает сискол write, чтобы записать данные в поток.
Но даже если вы напрямую из кода вызываете write, то вы вызываете обертку POSIX API, которая уже сама дергает одноименный системный вызов. Без оберток напрямую сисколл можно вызывать только ассемблерными вставками и прочей магией.
int main() {
write(1, "Hello\n", 6);
return 0;
}1️⃣ Подготовка данных в пользовательском пространстве
Функция обертка при необходимости делает некий препроцессинг аргументов(например форматирование в функции printf).
После копирует номер системного вызова и его готовые аргументы в определённые регистры процессора, где ядро ожидает их найти. В архитектуре x86-64 для Linux это делается так:
- В rax кладется номер системного вызова.
- В rdi, rsi, rdx, r10, r8, r9 кладутся аргументы (не больше шести).
Дальше обёртка выполняет специальную инструкцию, которая инициирует переход в ядро. Например,
syscall (в 64-битном режиме x86-64).2️⃣ Переключение в режим ядра
Как и инструкция call, syscall делает какую-то работу. Только в отличие от call, она минимальна:
👉🏿 сохраняет адрес возврата (следующую инструкцию) в регистр
%rcx;👉🏿 сохраняет регистр флагов RFLAGS в %r11;
👉🏿 загружает новый указатель инструкций из MSR-регистра
IA32_LSTAR_MSR (там лежит адрес функции entry_SYSCALL_64 - входной точки для всех сисколов).После выполнения инструкции syscall ядро получает доступ к привилегированным ресурсам: управлению памятью, прерываниями, устройствами ввода-вывода.
3️⃣ Обработка в ядре
Управление передаётся функции
entry_SYSCALL_64 - единой точки входа в ядро. Здесь начинается основная работа. И в первую очередь происходит смена контекста - ядро сохраняет значения регистров текущего потока и заменяет пользовательский стек на стек ядра. Это нужно для лучшей защиты от злонамеренного изменения стека ядрённых вызовов.
После нужно найти конкретный обработчик. Ядро извлекает номер системного вызова из
%rax и проверяет, не выходит ли он за допустимые пределы. После номер вызова используется как индекс в глобальной таблице ядра – sys_call_table. Это массив указателей на функции-обработчики.Перед выполнением операции ядро проверяет, есть ли у текущего процесса достаточно прав (например, на запись в файл). И затем вызывается нужная функция, внутри которой и выполняется полезная логика сискола.
4️⃣ Возврат в пользовательский режим
После того как обработчик завершил работу происходит следующее:
👉🏿 Сохранение результата – возвращаемое значение обработчика помещается в регистр
%rax. Если произошла ошибка – возвращается отрицательное число (код ошибки).👉🏿 Восстановление контекста – ядро восстанавливает сохранённые пользовательские регистры и стек.
👉🏿 Обратный переход – выполняется инструкция
sysret. Она использует сохранённые в %rcx и %r11 значения, чтобы восстановить адрес возврата и флаги, переключает процессор обратно в пользовательский режим и передаёт управление коду, следующему за syscall.И самый главный вопрос здесь: а какой оверхэд приходится на обеспечение системного вызова(не учитывая его логику)? В следующем посте обсудим это.
Be privileged. Stay cool.
#OS #howitworks