Почему приложения не могут непосредственно совершать системные вызовы
При программировании на C мы используем средства стандартной библиотеки языка, такие как тип FILE*, функции fopen(), fread(), fwrite(), fclose(). Эти функции кроссплатформенные и основываются внутри на более низкоуровневых функциях.
Например, C-функция fread и fwrite:
На POSIX-совместимых системах (в том числе Linux) она сводится к функции
На Windows она сводится к функциям из WinAPI:
Если использовать только функции из библиотеки C, то можно писать переносимые программы, которые будут работать под разными ОС. Но если использовать API операционной системы, то можно получить больше возможностей и несколько лучшую производительность за счёт меньшего числа копирований данных в промежуточные буферы.
Системные вызовы
Как же устроены те самые POSIX-функции read() и write()?
Они реализуются посредством системных вызовов. Системный вызов (англ. system call) — обращение прикладной программы к ядру операционной системы для выполнения какой-либо операции.
Современные операционные системы предусматривают разделение полномочий, препятствующее обращению исполняемых программ к данным других программ и оборудованию. Ядро ОС исполняется в привилегированном режиме работы процессора. Для выполнения межпроцессной операции или операции, требующей доступа к оборудованию, программа обращается к ядру, которое, в зависимости от полномочий вызывающего процесса, исполняет либо отказывает в исполнении такого вызова.
Обычное пользовательское приложение работает в непривилегированном режиме в своём виртуальном адресном пространстве. В обычном случае для выполнения вычислительной работы, для доступа к памяти и пр. не требуются системные вызовы. Например, функции библиотеки C, такие как strlen() и memcpy(), не имеют ничего общего с ядром и всегда выполняются целиком в приложении. Однако такие функции, как malloc() и printf(), могут делать внутри системные вызовы.
Набор системных вызовов разный в разных операционных системах. Итого в ядре Linux около 310 системных вызовов. С ними можно познакомиться в таблице. Для сравнения, в ОС Windows системных вызовов около 460.
GNU C Library
Когда мы компилируем обычную программу на C под Linux, она автоматически линкуется с библиотекой glibc.
Библиотека GNU C Library (часто используется название glibc) — это вариант реализации стандартной библиотеки С от проекта GNU. Является одним из основных компонентов операционной системы GNU/Linux.
Реализует как стандартные C-функции типа malloc(), strcpy(), fopen() (они являются частью стандарта языка программирования C и доступны на всех платформах), так и POSIX-функции типа getpid(), open() (эти функции не входят в стандарт C и, как правило, скажем, под Windows не реализованы).
Библиотека GNU C Library предоставляет программисту удобный интерфейс для работы с ОС в виде интерфейсных функций. Многие функции в libc являются тонкими обёртками над системными вызовами. Однако не каждая POSIX-функция является системным вызовом. Так и наоборот, не для каждого системного вызова есть соответствующая C-функция.
Размещение
Библиотека libc является одним файлом (динамическая so и статическая a), размещается в каталоге /usr/lib. Кроме того, в состав glibc (GNU libc) входят ещё несколько библиотек:
- libm — математическая библиотека (там реализованы функции вида sin(), cos(). )
- libpthread — POSIX Threads — библиотека для работы с потоками (мы обратимся к ней на следующих занятиях)
Всего функций в glibc много, мы рассмотрим только несколько.
Пример: fwrite
Вот шаги, которые включает в себя вызов C-функции fwrite:
- fwrite вместе с остальной частью стандартной библиотеки C реализован в glibc.
- fwrite вызывает более низкоуровневую функцию write.
- write загружает идентификатор системного вызова (который равен 1 для write) и аргументы в регистры процессора, а затем заставит процессор переключиться на уровень ядра. То, как это делается, зависит от архитектуры процессора, а иногда и от модели процессора. Например, процессоры x86 обычно вызывают прерывание 80, а процессоры x86-64 используют инструкцию процессора syscall.
- Процессор, который теперь работает в режиме ядра, передает идентификатор системного вызова в таблицу системных вызовов, извлекает указатель функции со смещением 1 и вызывает функцию. Эта функция, sys_write, является реализацией записи в файл.
Как работают системные вызовы
Из пользовательского пространства (ring 3) нельзя просто так вызвать функцию из ядра (ring 0), как обычную функцию. На шаге №3 в предыдущем примере используется тот или иной механизм перехода в режим ядра в зависимости от архитектуры компьютера. На компьютерах самой популярной архитектуры x86 для системный вызов делается тем или иным методом:
- через программное прерывание,
- через инструкцию sysenter,
- через инструкцию syscall.
Программное прерывание
Прерывания (англ. interrupts) — это как бы сигнал процессору, что надо прервать выполнение (их поэтому и назвали прерываниями) текущего кода и срочно сделать то, что указано в обработчике.
Прерывание извещает процессор о наступлении высокоприоритетного события, требующего прерывания текущего кода, выполняемого процессором. Процессор отвечает приостановкой своей текущей активности, сохраняя свое состояние, и выполняя функцию, называемую обработчиком прерывания (или программой обработки прерывания), который реагирует на событие и обслуживает его, после чего возвращает управление в прерванный код.
Программное прерывание — синхронное прерывание, которое может осуществить программа с помощью специальной инструкции.
В процессорах архитектуры x86 для явного вызова синхронного прерывания имеется инструкция int, аргументом которой является номер прерывания (от 0 до 255). В защищённом и длинном режиме обычные программы не могут обслуживать прерывания, эта функция доступна только системному коду (операционной системе).
В ОС Linux номер прерывания 0x80 (в десятичной системе — 128) используется для выполнения системных вызовов. Обработчиком прерывания 0x80 является ядро Linux. Программа перед выполнением прерывания помещает в регистр eax номер системного вызова, который нужно выполнить. Когда управление переходит в ring 0, то ядро считывает этот номер и вызывает нужную функцию.
Метод этот широко применялся на 32-битных системах, на 64-битных он считается устаревшим и не применяется, но тоже работает, хотя с целым рядом ограничений (например, нельзя в качестве параметра передать 64-битный указатель).
- Поместить номер системного вызова в eax.
- Поместить аргументы в регистры ebx, ecx, edx, esi, edi, ebp.
- Вызвать инструкцию int 0x80.
- Получить результат из eax.
Пример реализации mygetpid() (получение PID текущего процесса) на ассемблере (для системного вызова getpid используется номер 20):
Инструкция sysenter
Спустя некоторое время, ещё когда не было x86-64, в Intel поняли, что можно ускорить системные вызовы, если создать специальную инструкцию системного вызова, тем самым минуя некоторые издержки прерывания. Ускорение достигается за счёт того, что на аппаратном уровне при выполнении инструкции sysenter опускается множество проверок на валидность дескрипторов, а так же проверок, зависящих от уровня привилегий.
На сегодня эти инструкции (sysenter и sysexit) поддерживаются процессорами Intel в 32- и 64-битных режимах, процессорами AMD — только в 32-битном (на 64-битном приводит к исключению неизвестного опкода).
Поскольку 32-битные архитектуры теряют популярность, рассматривать не будем.
Инструкция syscall
Так как именно AMD разработали x86-64 архитектуру, которая и называется AMD64, то они решили создать свою собственную инструкцию для системных вызовов.
Эти инструкции (syscall и парная sysret) поддерживаются процессорами Intel только в 64-битном режиме, процессорами AMD — во всех режимах.
Системные вызовы при помощи этой инструкции делаются в современных версиях 64-битного Linux.
- Номер системного вызова помещается в rax.
- Аргументы записываются в rdi, rsi, rdx, r10, r8 и r9.
- Затем вызывается syscall.
- Когда управление возвращается, результат находится в rax.
- Значения всех регистров, кроме r11, rcx и rax, системным вызовом не изменяются, дополнительно сохранять их не требуется.
Пример реализации mygetpid() (получение PID текущего процесса) на ассемблере (для системного вызова getpid по таблице используется номер 39):
Производительность
Системные вызовы требуют переключения контекста и перехода процессора в режим с высоким уровнем привилегий. Поэтому системный вызов выполняется относительно медленно по сравнению с вызовом обычной C-функции. Ещё хуже стало после обнаружения уязвимости Meltdown: патч KPTI (kernel page-table isolation), помогающий против уязвимости, приводит к сбросу TLB-кешей и дополнительному падению производительности.
Ориентировочные цифры, сколько занимает системный вызов на конкретном процессоре [1]:
- int 80h — 500 нс,
- sysenter — 340 нс,
- syscall — 280 нс,
- патч KPTI увеличивает эти числа на 180 нс,
- вызов обычной C-функции — единицы нс.
Есть приёмы под названием vsyscall (уже устарел) и vDSO (virtual dynamic shared object) [2], которые позволяют в некоторых случаях избежать переключения контекста и ускорить выполнение. Помогает для системных вызовов, которым реально не нужны высокие привилегии, например gettimeofday. Удобно, если надо часто получать таймстемпы, например, для логов.
Мониторинг системных вызовов
Существует несколько инструментов, которые можно использовать для просмотра системных вызовов, которые выполняются программами. Самый известный из них, strace, доступен во многих операционных системах, и, вероятно, он уже установлен на вашем компьютере.
strace может запустить новый процесс или подключиться к уже запущенному. Вы можете многое узнать, подглядывая за системными вызовами, сделанными различными программами.
Использование в коде на C
Заголовочный файл unistd.h — основной, его наличие и содержимое обеспечивается стандартом POSIX.1. Видимо, «uni» пошло от UNIX.
Типы данных
Примеры типов данных POSIX (тем не менее, они не являются стандартными типами в языке C).
- pid_t — идентификаторы процессов.
- ssize_t — аналогичен size_t, но обязан быть знаковым и, главное, обязан уметь хранить минус единицу (это число используется как возвращаемое значение в случае ошибки во многих функциях).
- off_t и off64_t — знаковый тип для хранения смещения в файле.
Получение справки
Сигнатуры функций и информацию об использовании можно почерпнуть из man-страниц:
Бонус: пишем «Hello, world!» на ассемблере
Теперь посмотрим, что, кроме служебных вызовов при загрузке бинарника, наша программа выполнила именно ожидаемые два системных вызова.
Большинство из нас училось писать программы, которые выполняются по четко намеченному алгоритму. Мы разделяли программу на задачи и подзадачи, и каждая функция решала свою задачу, вызывая другие функции для решения соответствующих подзадач. Мы ожидали, что, получив нужные входные данные, функция выдаст правильный результат с определенными побочными эффектами.
Реалии развития компьютерных систем разрушили этот идеал. Ресурсы компьютеров ограничены; иногда происходят аппаратные сбои; многие программы выполняются одновременно; пользователи и программисты делают ошибки. Часто все это проявляется на границе между приложением и операционной системой. Следовательно, используя системные вызовы для доступа к ресурсам, осуществления операций ввода-вывода или других целей, нужно понимать не только то, что именно происходит при успешном завершении вызова, но также при каких обстоятельствах он может завершиться неуспешно.
Сбои системных вызовов происходят в самых разных ситуациях.
? В системе могут закончиться ресурсы (или же программа может исчерпать лимит ресурсов, наложенный на нее системой). Например, программа может запросить слишком много памяти, записать чересчур большой объем данных на диск или открыть чрезмерное количество файлов одновременно.
? Операционная система Linux блокирует некоторые системные вызовы, когда программа пытается выполнить операцию при отсутствии должных привилегий. Например, программа может попытаться осуществить запись в доступный только для чтения файл, обратиться к памяти другого процесса или уничтожить программу другого пользователя.
? Аргументы системного вызова могут оказаться неправильными либо по причине ошибочно введенных пользователем данных, либо из-за ошибки самой программы. Например, программа может передать системному вызову неправильный адрес памяти или неверный дескриптор файла. Другой вариант ошибки — попытка открыть каталог вместо обычного файла или передать имя файла системному вызову, ожидающему имя каталога.
? Системный вызов может аварийно завершиться по причинам, не зависящим от самой программы. Чаще всего это происходит при доступе к аппаратным устройствам. Устройство может работать некорректно или не поддерживать требуемую операцию. либо в дисковод просто не вставлен диск.
? Выполнение системного вызова иногда прерывается внешними событиями, к каковым относится, например, получение сигнала. Это не обязательно означает ошибку, но ответственность за перезапуск системного вызова возлагается на программу.
В хорошо написанной программе, часто обращающейся к системным вызовам, большая часть кода посвящена обнаружению и обработке ошибок, а не решению основной задачи.
Номера системных вызовов
Номера системных вызовов Каждому системному вызову операционной системы Linux присваивается номер системного вызова (syscall number). Этот уникальный номер используется для обращения к определенному системному вызову. Когда процесс выполняет системный вызов из пространства
Производительность системных вызовов
Производительность системных вызовов Системные вызовы в операционной системе Linux работают быстрее, чем во многих других операционных системах. Это отчасти связано с невероятно малым временем переключения контекста. Переход в режим ядра и выход из него являются хорошо
Обработка системных вызовов
Обработка системных вызовов Приложения пользователя не могут непосредственно выполнять код ядра. Они не могут просто вызвать функцию, которая существует в пространстве ядра, так как ядро находится в защищенной области памяти. Если программы смогут непосредственно
Реализация системных вызовов
Реализация системных вызовов Реализация системного вызова в ОС Linux не связана с поведением обработчика системных вызовов. Добавление нового системного вызова в операционной системе Linux является сравнительно простым делом. Тяжелая работа связана с разработкой и
9.2.1. Ограничения системных вызовов
9.2.1. Ограничения системных вызовов Режим ядра защищен от влияния режима пользователя. Одна из таких защит состоит в том, что тип данных, передаваемых между режимами ядра и пользователя, ограничен, легко верифицируется и следует строгим соглашениям.• Длина каждого
9.2.3. Использование системных вызовов
9.2.3. Использование системных вызовов Интерфейс, с которым вам, как программисту, возможно, доведется работать, представляет собой набор оболочек библиотеки С для системных вызовов. В оставшейся части этой книги под системным вызовом будет подразумеваться функция
Обработка прерванных системных вызовов
Обработка прерванных системных вызовов Термином медленный системный вызов (slow system call), введенным при описании функции accept, мы будем обозначать любой системный вызов, который может быть заблокирован навсегда. Такой системный вызов может никогда не завершиться. В эту
В.1. Трассировка системных вызовов
В.1. Трассировка системных вызовов Многие версии Unix предоставляют возможность трассировки (отслеживания) системных вызовов. Зачастую это может оказаться полезным методом отладки.Работая на этом уровне, необходимо различать системный вызов и функцию. Системный вызов
22.4. Трассировка системных вызовов
22.4. Трассировка системных вызовов Вы когда-нибудь задумывались о том, какие системные вызовы использует наша программа во время своего выполнения? Если да, то этот пункт как раз для вас. Возможно, пока он только удовлетворит ваше любопытство, но через некоторое время эта
16.6. Семантика вызовов
16.6. Семантика вызовов В листинге 15.24 мы привели пример клиента интерфейса дверей, повторно отсылавшего запрос на сервер при прерывании вызова door_call перехватываемым сигналом. Затем мы показали, что при этом процедура сервера вызывается дважды, а не однократно. Потом мы
Реализация групповых вызовов
2.2.3. Коды ошибок системных вызовов
2.2.3. Коды ошибок системных вызовов Большинство системных вызовов возвращает 0, если операция выполнена успешно, и ненулевое значение — в случае сбоя. (В некоторых случаях используются другие соглашения. Например, функция malloc() при возникновении ошибки возвращает нулевой
Использование системных вызовов операционной системы MS-DOS
Использование системных вызовов операционной системы MS-DOS Функция Краткое описание bdos вызов системы MS-DOS; используются только регистры DX и AL dosexterr получение значений регистров из системы MS-DOS вызовом 59H FP_OFF возвращает смещение far-указателя FP_SEG возвращает сегмент
Оптимизация вызовов
Оптимизация вызовов На уровнях 2 и 3 неизбежно использование явных вызовов процедуры подобных my_polygon.set_size (5) для изменения значения атрибута. Существует опасение, что использование такого стиля на уровне 4 негативно скажется на производительности. Тем не менее компилятор
Цепочка вызовов
Цепочка вызовов Обсуждая механизм обработки исключений, полезно иметь ясную картину последовательности вызовов, приведших в итоге к исключению. Это понятие уже появлялось при рассмотрении механизма языка Ada. Рис. 12.1. Цепочка вызововПусть r0 будет корневой процедурой
Что такое системный вызов в операционной системе?
Системный вызов является механизмом , который обеспечивает интерфейс между процессом и операционной системой. Это программный метод, при котором компьютерная программа запрашивает сервис у ядра ОС.
Системный вызов предлагает услуги операционной системы пользовательским программам через API (интерфейс прикладного программирования). Системные вызовы являются единственными точками входа в систему ядра.
Из этого руководства по операционной системе вы узнаете:
Пример системного вызова
Например, если нам нужно написать программный код для чтения данных из одного файла, скопируйте эти данные в другой файл. Первой информацией, которая требуется программе, является имя двух файлов, входных и выходных файлов.
В интерактивной системе этот тип выполнения программы требует некоторых системных вызовов ОС.
Как работает системный вызов?
Вот шаги для системного вызова:
Как вы можете видеть на приведенной выше диаграмме.
Шаг 1) Процессы, выполняемые в пользовательском режиме до тех пор, пока системный вызов не прервет его.
Шаг 2) После этого системный вызов выполняется в режиме ядра в приоритетном порядке.
Шаг 3) По завершении выполнения системного вызова управление возвращается в режим пользователя.,
Шаг 4) Выполнение пользовательских процессов возобновляется в режиме ядра.Зачем вам системные вызовы в ОС?
Ниже приведены ситуации, которые требуют системных вызовов в ОС:
- Чтение и запись из файлов требуют системных вызовов.
- Если файловая система хочет создать или удалить файлы, требуются системные вызовы.
- Системные вызовы используются для создания и управления новыми процессами.
- Сетевые подключения требуют системных вызовов для отправки и получения пакетов.
- Доступ к аппаратным устройствам, таким как сканер, принтер, требуется системный вызов.
Типы системных вызовов
Вот пять типов системных вызовов, используемых в ОС:
- Контроль процесса
- Управление файлами
- Управление устройством
- Информационное обслуживание
- связи
Контроль процесса
Эти системные вызовы выполняют задачу создания процесса, завершения процесса и т. Д.
- Конец и Прервать
- Загрузить и выполнить
- Создать процесс и завершить процесс
- Ожидание и подписанное событие
- Выделить и освободить память
Управление файлами
Системные вызовы управления файлами обрабатывают задания по обработке файлов, такие как создание файла, чтение, запись и т. Д.
- Создать файл
- Удалить файл
- Открыть и закрыть файл
- Читать, писать и перемещать
- Получить и установить атрибуты файла
Управление устройством
Управление устройствами выполняет работу с устройствами, такими как чтение из буферов устройств, запись в буферы устройств и т. Д.
- Запрос и релиз устройства
- Логически подключать / отключать устройства
- Получить и установить атрибуты устройства
Информационное обслуживание
Он обрабатывает информацию и ее передачу между ОС и программой пользователя.
- Получить или установить время и дату
- Получить атрибуты процесса и устройства
Связь:
Эти типы системных вызовов специально используются для межпроцессного взаимодействия.
Правила передачи параметров для системного вызова
Вот общие общие правила для передачи параметров в системный вызов:
- Параметры должны быть загружены или извлечены из стека операционной системой.
- Параметры могут быть переданы в регистрах.
- Когда параметров больше, чем регистров, они должны храниться в блоке, а адрес блока должен передаваться в качестве параметра в регистр.
Важные системные вызовы, используемые в ОС
Подождите()
В некоторых системах процесс должен ждать, пока другой процесс завершит свое выполнение. Этот тип ситуации возникает, когда родительский процесс создает дочерний процесс, и выполнение родительского процесса остается приостановленным, пока не выполнится его дочерний процесс.
Приостановка родительского процесса автоматически происходит с помощью системного вызова wait (). Когда дочерний процесс завершает выполнение, элемент управления возвращается к родительскому процессу.
вилка ()
Процессы используют этот системный вызов для создания процессов, которые являются их копиями. С помощью этого системного вызова родительский процесс создает дочерний процесс, и выполнение родительского процесса будет приостановлено до выполнения дочернего процесса.
Этот системный вызов выполняется, когда исполняемый файл в контексте уже запущенного процесса заменяет старый исполняемый файл. Однако исходный идентификатор процесса остается, поскольку новый процесс не создается, но стек, данные, заголовок, данные и т. Д. Заменяются новым процессом.
убийство():
Системный вызов kill () используется ОС для отправки сигнала завершения процессу, который побуждает процесс завершиться. Однако системный вызов kill не обязательно означает уничтожение процесса и может иметь различные значения.
Выход():
Системный вызов exit () используется для прекращения выполнения программы. Этот вызов, особенно в многопоточной среде, определяет, что выполнение потока завершено. ОС восстанавливает ресурсы, которые использовались процессом после использования системного вызова exit ().
Про системные вызовы уже много было сказано, например здесь или здесь. Наверняка вам уже известно, что системный вызов — это способ вызова функции ядра ОС. Мне же захотелось копнуть глубже и узнать, что особенного в этом системном вызове, какие существуют реализации и какова их производительность на примере архитектуры x86-64. Если вам также интересны ответы на данные вопросы, добро пожаловать под кат.
System call
Каждый раз, когда мы хотим что-то отобразить на мониторе, записать в устройство, считать с файла, нам приходится обращаться к ядру ОС. Именно ядро ОС отвечает за любое общение с железом, именно там происходит работа с прерываниями, режимами процессора, переключениями задач… Чтобы пользователь программой не смог завалить работу всей операционной системы, было решено разделить пространство памяти на пространство пользователя (область памяти, предназначенная для выполнения пользовательских программ) и пространство ядра, а также запретить пользователю доступ к памяти ядра ОС. Реализовано это разделение в x86-семействе аппаратно при помощи сегментной защиты памяти. Но пользовательской программе нужно каким-то образом общаться с ядром, для этого и была придумана концепция системных вызовов.
Системный вызов — способ обращения программы пользовательского пространства к пространству ядра. Со стороны это может выглядеть как вызов обычной функции со своим собственным calling convention, но на самом деле процессором выполняется чуть больше действий, чем при вызове функции инструкцией call. Например, в архитектуре x86 во время системного вызова как минимум происходит увеличение уровня привилегий, замена пользовательских сегментов на сегменты ядра и установка регистра IP на обработчик системного вызова.
Программист обычно не работает с системными вызовами напрямую, так как системные вызовы обернуты в функции и скрыты в различных библиотеках, например libc.so в Linux или же ntdll.dll в Windows, с которыми и взаимодействует прикладной разработчик.
Теоретически, реализовать системный вызов можно при помощи любого исключения, хоть при помощи деления на 0. Главное — это передача управления ядру. Рассмотрим реальные примеры реализаций исключений.
Способы реализации системных вызовов
Выполнение неверной инструкции.
Ранее, ещё на 80386 это был самый быстрый способ сделать системный вызов. Для этого обычно применялась бессмысленная и неверная инструкция LOCK NOP, после исполнения которой процессором вызывался обработчик неверной инструкции. Это было больше 20 лет назад и, говорят, этим приёмом обрабатывались системные вызовы в корпорации Microsoft. Обработчик неверной инструкции в наши дни используется по назначению.
Call gates
Для того, чтобы иметь доступ к сегментам кода с различным уровнем привилегий, в Intel был разработан специальный набор дескрипторов, называемый gate descriptors. Существует 4 вида таких дескрипторов:
- Call gates
- Trap gates (для исключений, вроде int 3, требующих выполнения участка кода)
- Interrupt gates (аналогичен trap gates, но с некоторыми отличиями)
- Task gates (полагалось, что будут использоваться для переключения задач)
Нам интересны только call gates, так как именно через них планировалось реализовывать системные вызовы в x86.
Call gate реализован при помощи инструкции call far или jmp far и принимает в качестве параметра call gate-дескриптор, который настраивается ядром ОС. Является достаточно гибким механизмом, так как возможен переход и на любой уровень защитного кольца, и на 16-битный код. Считается, что call gates производительней прерываний. Этот способ использовался в OS/2 и Windows 95. Из-за неудобства использования в Linux механизм так и не был реализован. Со временем совсем перестал использоваться, так как появились более производительные и простые в обращении реализации системных вызовов (sysenter/sysexit).
Системные вызовы, реализованные в Linux
В архитектуре x86-64 ОС Linux существует несколько различных способов системных вызовов:
- int 80h
- sysenter/sysexit
- syscall/sysret
- vsyscall
- vDSO
В реализации каждого системного вызова есть свои особенности, но в общем, обработчик в Linux имеет примерно одинаковую структуру:
- Включается защита от чтения/записи/исполнения кода пользовательского пространства.
- Заменяется пользовательский стек на стек ядра, сохраняются callee-saved регистры.
- Выполняется обработка системного вызова
- Восстановление стека, регистров
- Отключение защиты
- Выход из системного вызова
Рассмотрим немного подробнее каждый системный вызов.
int 80h
Изначально, в архитектуре x86, Linux использовал программное прерывание 128 для совершения системного вызова. Для указания номера системного вызова, пользователь задаёт в eax номер системного вызова, а его параметры располагает по порядку в регистрах ebx, ecx, edx, esi, edi, ebp. Далее вызывается инструкция int 80h, которая программно вызывает прерывание. Процессором вызывается обработчик прерывания, установленный ядром Linux ещё во время инициализации ядра. В x86-64 вызов прерывания используется только во время эмуляции режима x32 для обратной совместимости.
В принципе, никто не запрещает пользоваться инструкцией в расширенном режиме. Но вы должны понимать, что используется 32-битная таблица вызовов и все используемые адреса должны помещаться в 32-битное адресное пространство. Согласно SYSTEM V ABI [4] §3.5.1, для программ, виртуальный адрес которых известен на этапе линковки и помещается в 2гб, по умолчанию используется малая модель памяти и все известные символы находятся в 32-битном адресном пространстве. Под это определение подходят статически скомпилированные программы, где и возможно использовать int 80h. Пошаговая работа прерывания подробно описана на stackoverflow.
В ядре обработчиком этого прерывания является функция entry_INT80_compat и находится в arch/x86/entry/entry_64_compat.S
Или в расширенном режиме (программа работает так как компилируется статически)
sysenter/sysexit
Спустя некоторое время, ещё когда не было x86-64, в Intel поняли, что можно ускорить системные вызовы, если создать специальную инструкцию системного вызова, тем самым минуя некоторые издержки прерывания. Так появилась пара инструкций sysenter/sysexit. Ускорение достигается за счёт того, что на аппаратном уровне при выполнении инструкции sysenter опускается множество проверок на валидность дескрипторов, а так же проверок, зависящих от уровня привилегий [3] §6.1. Также инструкция опирается на то, что вызывающая её программа использует плоскую модель памяти. В архитектуре Intel, инструкция валидна как для режима совместимости, так и для расширенного режима, но у AMD данная инструкция в расширенном режиме приводит к исключению неизвестного опкода [3]. Поэтому в настоящее время пара sysenter/sysexit используется только в режиме совместимости.
В ядре обработчиком этой инструкции является функция entry_SYSENTER_compat и находится в arch/x86/entry/entry_64_compat.S
Несмотря на то, что в реализации архитектуры от Intel инструкция валидна, в расширенном режиме скорее всего такой системный вызов никак не получится использовать. Это из-за того, что в регистре ebp сохраняется текущее значение стека, а адрес верхушки независимо от модели памяти находится вне 32-битного адресного пространства. Это всё потому, что Linux отображает стек на конец нижней половины каноничного адреса пространства.
Разработчики ядра Linux предостерегают пользователей от жесткого программирования sysenter из-за того, что ABI системного вызова может измениться. Из-за того, что Android не последовал этому совету, Linux пришлось откатить свой патч для сохранения обратной совместимости. Правильно реализовывать системный вызов нужно используя vDSO, речь о которой будет идти далее.
syscall/sysret
Так как именно AMD разработали x86-64 архитектуру, которая и называется AMD64, то они решили создать свой собственный системный вызов. Инструкция разрабатывалась AMD, как аналог sysenter/sysexit для архитектуры IA-32. В AMD позаботились о том, чтобы инструкция была реализована как в расширенном режиме, так и в режиме совместимости, но в Intel решили не поддерживать данную инструкцию в режиме совместимости. Несмотря на всё это, Linux имеет 2 обработчика для каждого из режимов: для x32 и x64. Обработчиками этой инструкции является функции entry_SYSCALL_64 для x64 и entry_SYSCALL_compat для x32 и находится в arch/x86/entry/entry_64.S и arch/x86/entry/entry_64_compat.S соответственно.
Кому интересно более подробно ознакомиться с инструкциями системных вызовов, в мануале Intel [0] (§4.3) приведён их псевдокод.
Для тестирования следующего примера потребуется ядро с конфигурацией CONFIG_IA32_EMULATION=y и компьютер AMD. Если же у вас компьютер фирмы Intel, то можно запустить пример на виртуалке. Linux может без предупреждения изменить ABI и этого системного вызова, поэтому в очередной раз напомню: системные вызовы в режиме совместимости правильнее исполнять через vDSO.
Непонятна причина, по которой AMD решили разработать свою инструкцию вместо того, чтобы расширить инструкцию Intel sysenter на архитектуру x86-64.
vsyscall
При переходе из пространства пользователя в пространство ядра происходит переключение контекста, что является не самой дешёвой операцией. Поэтому, для улучшения производительности системных вызовов, было решено их обрабатывать в пространстве пользователя. Для этого было зарезервировано 8 мб памяти для отображения пространства ядра в пространство пользователя. В эту память для архитектуры x86 поместили 3 реализации часто используемых read-only вызова: gettimeofday, time, getcpu.
Со временем стало понятно, что vsyscall имеет существенные недостатки. Фиксированное размещение в адресном пространстве является уязвимым местом с точки зрения безопасности, а отсутствие гибкости в размере выделяемой памяти может негативно сказаться на расширении отображаемой области ядра.
Для того, чтобы пример работал, необходимо, чтобы в ядре была включена поддержка vsyscall: CONFIG_X86_VSYSCALL_EMULATION=y
Linux не отображает vsyscall в режиме совместимости.
На данный момент, для сохранения обратной совместимости, ядро Linux предоставляет эмуляцию vsyscall. Эмуляция сделана для того, чтобы залатать дыры безопасности в ущерб производительности.
Эмуляция может быть реализована двумя способами.
Первый способ — при помощи замены адреса функции на системный вызов syscall. В таком случае виртуальный системный вызов функции gettimeofday на x86-64 выглядит следующим образом:
Где 0x60 — код системного вызова функции gettimeofday.
Второй же способ немного интереснее. При вызове функции vsyscall генерируется исключение Page fault, которое обрабатывается Linux. ОС видит, что ошибка произошла из-за исполнения инструкции по адресу vsyscall и передаёт управление обработчику виртуальных системных вызовов emulate_vsyscall (arch/x86/entry/vsyscall/vsyscall_64.c).
Реализацией vsyscall можно управлять при помощи параметра ядра vsyscall. Можно как отключить виртуальный системный вызов при помощи параметра vsyscall=none , задать реализацию как при помощи инструкции syscall syscall=native , так и через Page fault vsyscall=emulate .
vDSO (Virtual Dynamic Shared Object)
Также vDSO используется в качестве выбора наиболее производительного способа системного вызова, например в режиме совместимости.
Список разделяемых функций можно посмотреть в руководстве.
Для режима совместимости:
Правильнее всего искать функции vDSO при помощи извлечения адреса библиотеки из вспомогательного вектора AT_SYSINFO_EHDR и последующего парсинга разделяемого объекта. Пример парсинга vDSO из вспомогательного вектора можно найти в исходном коде ядра: tools/testing/selftests/vDSO/parse_vdso.c
Или если интересно, то можно покопаться и посмотреть, как парсится vDSO в glibc:
- Парсинг вспомогательных векторов: elf/dl-sysdep.c
- Парсинг разделяемой библиотеки: elf/setup-vdso.h
- Установка значений функций: sysdeps/unix/sysv/linux/x86_64/init-first.c, sysdeps/unix/sysv/linux/x86/gettimeofday.c, sysdeps/unix/sysv/linux/x86/time.c
Согласно System V ABI AMD64 [4] вызовы должны происходить при помощи инструкции syscall. На практике же к этой инструкции добавляются вызовы через vDSO. Поддержка системных вызовов в виде int 80h и vsyscall остались для обратной совместимости.
Сравнение производительности системных вызовов
С тестированием скорости системных вызовов всё неоднозначно. В архитектуре x86 на выполнение одной инструкции влияет множество факторов таких как наличие инструкции в кэше, загруженность конвейера, даже существует таблица задержек для данной архитектуры [2]. Поэтому достаточно сложно определить скорость выполнения участка кода. У Intel есть даже специальный гайд по замеру времени для участка кода [1]. Но проблема в том, что мы не можем замерить время согласно документу из-за того, что нам нужно вызывать объекты ядра из пользовательского пространства.
Поэтому было решено замерить время при помощи clock_gettime и тестировать производительность вызова gettimeofday, так как он есть во всех реализациях системных вызовов. На разных процессорах время может отличаться, но в целом, относительные результаты должны быть схожи.
Программа запускалась несколько раз и в итоге бралось минимальное время исполнения.
Тестирование int 80h, sysenter и vDSO-32 производилось в режиме совместимости.
О системе
cat /proc/cpuinfo | grep "model name" -m 1 — Intel® Core(TM) i7-5500U CPU @ 2.40GHz
uname -r — 4.14.13-1-ARCH
Таблица Результатов
Реализация | время (нс) |
---|---|
int 80h | 498 |
sysenter | 338 |
syscall | 278 |
vsyscall emulate | 692 |
vsyscall native | 278 |
vDSO | 37 |
vDSO-32 | 51 |
Как можно увидеть, каждая новая реализация системного вызова является производительней предыдущей, не считая vsysvall, так как это эмуляция. Как вы наверное уже догадались, если бы vsyscall был таким, каким его задумывали, время вызова было бы аналогично vDSO.
Все текущие сравнения производительности были произведены с патчем KPTI, исправляющим уязвимость meltdown.
Бонус: Производительность системных вызовов без KPTI
Патч KPTI был разработан специально для исправления уязвимости meltdown. Как известно, данный патч замедляет производительность ОС. Проверим производительность с выключенным KPTI (pti=off).
Таблица результатов с выключенным патчем
Реализация | Время (нс) | Увеличение времени исполнения после патча (нс) | Ухудшение производительности после патча (t1 - t0) / t0 * 100% |
---|---|---|---|
int 80h | 317 | 181 | 57% |
sysenter | 150 | 188 | 125% |
syscall | 103 | 175 | 170% |
vsyscall emulate | 496 | 196 | 40% |
vsyscall native | 103 | 175 | 170% |
vDSO | 37 | 0 | 0% |
vDSO-32 | 51 | 0 | 0% |
Переход в режим ядра и обратно в среднем после патча стал занимать примерно на 180 нс. больше времени, видимо это и есть цена сброса TLB-кэша.
Производительность системного вызова через vDSO не ухудшилась по причине того, то в данном типе вызова нет перехода в режим ядра, и, следовательно, нет причин сбрасывать TLB-кэш.
Читайте также: