Интерфейс системных вызовов linux

Содержание
  1. Интерфейс системных вызовов linux
  2. 5.1 Что поддеpживет 386 пpоцессоp?
  3. 5.2 Как Linux использует пpеpывания и исключения.
  4. 5.3 Как Linux устанавливает вектора системных вызовов.
  5. 5.4 Как установить свой собственный системный вызов.
  6. Глава 8 Системные вызовы Linux
  7. Читайте также
  8. 3.2.3. Системные вызовы: brk() и sbrk()
  9. 10.4.4. Системные вызовы, допускающие повторный запуск
  10. Системные вызовы для управления планировщиком
  11. Системные вызовы, связанные с управлением стратегией и приоритетом
  12. Системные вызовы управления процессорной привязкой
  13. Глава 5 Системные вызовы
  14. Почему не нужно создавать системные вызовы
  15. 9.2. Системные вызовы
  16. 12.1.4. Сигналы и системные вызовы
  17. Системные вызовы и функции стандартных библиотек
  18. Глава 7 Системные вызовы в UNIX
  19. 5.1. Системные вызовы fork() и ехес()
  20. Глава 7. Системные трюки
  21. 3.4.2. Системные вызовы wait()
  22. 5.5.2. Системные вызовы
  23. rfLinux
  24. Tuesday, March 18, 2008
  25. Linux — syscalls. Системные вызовы в Linux.
  26. Вместо введения.
  27. Теория. Что такое системные вызовы?
  28. Классический механизм обслуживания системных вызовов в Linux.
  29. Системные вызовы и int 0x80.
  30. system_call().
  31. Новый механизм обработки системных вызовов в Linux. sysenter/sysexit.
  32. Итоги.
  33. Ссылки.

Интерфейс системных вызовов linux

Эта глава pассматpивает пеpвые механизмы поддеpживаемые 386 пpоцессоpом и то как Linux использует эти механизмы. Здесь нет ссылок на конкpетные системные вызовы — их слишком много, сpеди них постоянно появляются новые, и они документиpованы на стpаницах описания Linux.

5.1 Что поддеpживет 386 пpоцессоp?

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

386 пpоцессоp pаспознает два типа пpеpываний: маскиpуемые и немаскиpуемые. Также опpеделяются два типа исключений: опpеделяемые пpоцессоpом и пpогpамные исключения.

Каждое исключение и пpеpывание имеет свой номеp, котоpый в литеpатуpе называется вектоpом. Hемаскиpуемым пpеpываниям и исключениям опpеделяемым пpоцессоpом пpиписываются вектоpа с 0-го по 32, включительно. Вектоpа маскиpуемых пpеpываний опpеделяются аппаpатным обеспечением. Внешние пpеpывания помещают вектоp в шину во вpемя цикла опpеделения пpеpывания. Любой вектоp входящий в диапазон от 32 до 255 может быть использован маскиpуемым пpеpыванием или пpогpамиpуемым исключением. См pис 4.1 для пpосмотpа всех возможных пpеpываний и исключений:

ВЫСШИЙ

HИЗШИЙ

Hеиспpавности включающие в себя неиспpавность отладчика
Hеиспpавность инстpукций INTO, INT n, INT 3.
Отладка неиспpавностей этих инстpукций.
Отладка последующих инстpукций.
Hемаскиpуемое пpеpывание
Пpеpывание INTR

Рис 4.2: Пpиоpитет пpеpываний и исключений.

5.2 Как Linux использует пpеpывания и исключения.

В Linux, запуск системного запpоса вызывается маскиpуемым пpеpыванием, или-же пеpедачей класса исключения, обусловленной инстpукцией int 0x80. Мы используем вектоp 0x80 для пеpедачи контpоля ядpу. Этот вектоp устанавливается во вpем инициализации, сpеди дpугих важнейших вектоpов таких, как вектоp таймеpа.

В веpсии Linux 0.99.2 пpисутствует 116 системных вызовов. Документацию по ним можно найти непосpедственно в самой документации по Linux. Во вpемя обpащения пользователя к системному вызову, пpоисходит следующее:

— Каждый вызов определяется в libc. Каждый вызов внутри библиотеки libc в общем-то представляет собой макрос syscallX, где X — число параметров текущей подпрограммы. Некоторые системные вызовы являются более общими, нежели другие из-за изменяющегося по длине списка аргументов, но два эти типа ничем концептуально не отличаются друг от друга — разве что количеством параметров. Примерами общих системных вызовов могут служить вызовы open() и ioctl().

— Каждый макрос вызова поддерживается ассемблерной подпрограммой, устанавливаемой границы стека вызовов и запускаемой вызов _system_call() через прерывание, пользуясь инструкциями $0x80. К примеру вызов setuid представлен как:

Определение макросов для syscallX() вы можете найти в /usr/include/linux/unistd.h а библиотека системных вызовов пользовательского пространства находится в /usr/src/libc/syscall

— С этой точки зрения системный код вызова не запущен. Он не запускается до запуска int $0x80 осуществляющего переход на ядровую _system_call(). Эта процедура общая для всех системных вызовов. Она обладает возможностью сохранения регистров, проверки на правильность запускаемого кода и затем передачи контроля текущему системному вызову со смещениями в таблице _sys_call_table. Она также может вызвать _ret_from_sys_call(), когда системный вызов завершается, но еще не осуществлен процесс перехода в прстранство пользователя.

Фактический код компонентов sys_call находится в /usr/src/linux/kernel/sys_call.s. Фактический код множества системных вызовов может быть найден в /usr/src/linux/kernel/sys.c. Остальную часть ищите сами.

— После запуска системного вызова, макрос syscallX() проверяет его на отрицательное возвращаемое значение, и если подобное случается он помещает код ошибки в глобальную переменную _errno, так чтобы он был доступен функции типа perror().

5.3 Как Linux устанавливает вектора системных вызовов.

Код startup_32 находящийся в /usr/src/linux/boot/head.S начинает всю работу запуская setup_idt(). Подпрограмма устанавливает IDT (таблицу описания прерываний) с 256 записями. Никаких отправных точек прерываний этой программой не загружается, и делается это лишь после разрешения пейджинга и перехода ядра по адресу 0xC0000000. В IDT находится 256 записей по 4 байта каждая, всего 1024 байта.

Когда вызывается start_kernel() (/usr/src/linux/init/main.c) она запускает trap_init() (описае в /usr/src/linux/kernel/traps.c). trap_init() устанавливает таблицу дескрипторов прерываний как показано на рис 4.3

На этот момент вектор прерывания системных вызовов не установлен. Он инициализируется sched_init() (находится в /usr/src/linux/kernel/sched.c). Вызов set_system_gate (0x80,&system_call) устанавливает прерывание 0x80 как вектор параметра system_call().

5.4 Как установить свой собственный системный вызов.

  1. Создайте каталог в /usr/src/linux/ для вашего кода.
  2. Поместите нужные вам библиотеки в /usr/include/sys/ и /usr/include/linux/
  3. Поместите ваш отлинкованный модуль в ARCHIVES и подкаталог в строки SUBDIRS высокого уровня создания файла. См fs/Makefile — fs.o.
  4. Поместите #define __NR_xx в unistd.h для присвоения номера вашему системному запросу, где xx — индекс описания вашего вызова. Она будет использована для установки вектора с помощью sys_call_table вызываемого ваш код.
  5. Введите отправную точку для вашего системного запроса в sys_call_table в sys.h. Она будет зависеть от индекса xx в предыдущем пункте. Переменная NR_syscalls будет пересчитана автоматически.
  6. Измените какой-нибудь код ядра в /fs/mm/ для установки инсрументов нужных вашему вызову.
  7. Запустите процесс компановки на высшем уровне для создания вашего кода в ядре

После этого вам останется лишь занести системный вызов в ваши библиотеки, или использовать макрос _syscalln() в программе использующей ваши разработки, для разрешения им доступа к новому системному вызову.

В библиографии содержаться несколько полезных ссылок на книги охватывающие эту тему. В частности полезно будет просмотреть «The 386DX Microprocessor Programmer’s Reference Manual» и «Advanced 80386 Programming Techniques» Джеймса Турли.

Источник

Глава 8 Системные вызовы Linux

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

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

? Библиотечная функция — это обычная функция, которая находится во внешней библиотеке, подключаемой к программе. Большинство рассмотренных нами функций содержится в стандартной библиотеке языка С, libc. Вызов библиотечной функции реализуется традиционно: ее аргументы помещаются в регистры процессора или в стек и управление передается в начало кода функции (этот код находится в библиотеке, загруженной в память).

? Системный вызов реализован в ядре Linux. Аргументы вызова упаковываются и передаются ядру, которое берет на себя управление программой, пока вызов не завершится. Системный вызов — это не обычная функция, и для передачи управления ядру требуется специальная подпрограмма. В GNU-библнотеке языка С (реализация стандартной библиотеки, имеющаяся в Linux) для системных вызовов созданы функции-оболочки, упрощающие обращение к ним. В качестве примеров системных вызовов можно привести низкоуровневые функции ввода-вывода, такие как open() и read().

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

Некоторые системные вызовы оказывают очень большое влияние на систему. Например. есть вызовы, позволяющие завершить работу Linux, выделить системные ресурсы или запретить другим пользователям доступ к ресурсам. С такими вызовами связано ограничение: только процессы, выполняющиеся с привилегиями суперпользователя (учетная запись root), имеют право обращаться к ним. В противном случае вызовы завершатся ошибкой.

Внутри себя библиотечная функция может обращаться к другим функциям или системным вызовам.

В настоящее время в Linux есть около 200 системных вызовов. Их список находится в файле /usr/include/asm/unistd.h. Некоторые из них используются только внутри системы, а некоторые предназначены лишь для реализации специализированных библиотечных функций. В этой главе будут рассмотрены те системные вызовы, которые чаще всего используются системными программистами.

Читайте также

3.2.3. Системные вызовы: brk() и sbrk()

3.2.3. Системные вызовы: brk() и sbrk() Четыре функции, которые мы рассмотрели (malloc(), calloc(), realloc() и free()) являются стандартными, переносимыми функциями для управления динамической памятью.На Unix-системах стандартные функции реализованы поверх двух дополнительных, очень примитивных

10.4.4. Системные вызовы, допускающие повторный запуск

10.4.4. Системные вызовы, допускающие повторный запуск Значение EINTR для errno (см. раздел 4.3 «Определение ошибок») указывает, что системный вызов был прерван. Хотя с этим значением ошибки может завершаться большое количество системных вызовов, двумя наиболее значительными

Системные вызовы для управления планировщиком

Системные вызовы для управления планировщиком Операционная система Linux предоставляет семейство системных вызовов для управления параметрами планировщика. Эти системные вызовы позволяют манипулировать приоритетом процесса, стратегией планирования и процессорной

Системные вызовы, связанные с управлением стратегией и приоритетом

Системные вызовы, связанные с управлением стратегией и приоритетом Системные вызовы sched_setscheduler() и sched_getcheduler() позволяют соответственно установить и получить значение стратегии планирования и приоритета реального времени для указанного процесса. Реализация этих

Системные вызовы управления процессорной привязкой

Системные вызовы управления процессорной привязкой Планировщик ОС Linux может обеспечивать жесткую процессорную привязку (processor affinity). Хотя планировщик пытается обеспечивать мягкую или естественную привязку путем удержания процессов на одном и том же процессоре, он

Глава 5 Системные вызовы

Глава 5 Системные вызовы Ядро операционной системы предоставляет набор интерфейсов, благодаря которым процессы, работающие в пространстве пользователя, могут взаимодействовать с системой. Эти интерфейсы предоставляют пользовательским программам доступ к аппаратному

Почему не нужно создавать системные вызовы

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

9.2. Системные вызовы

9.2. Системные вызовы В этой книге практически повсеместно упоминаются системные вызовы, которые являются фундаментальными для программного окружения. На первый взгляд, они выглядят как обычные вызовы функций С. И это не случайно; они представляют собой специальную

12.1.4. Сигналы и системные вызовы

12.1.4. Сигналы и системные вызовы Часто сигналы доставляются процессу, который находится состоянии ожидания наступления некоторого внешнего события. Например, текстовый редактор часто ожидает завершения read(), чтобы возвратить ввод терминала. Когда системный

Системные вызовы и функции стандартных библиотек

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

Глава 7 Системные вызовы в UNIX

Глава 7 Системные вызовы в UNIX В настоящей главе мы рассмотрим самый низкий уровень взаимодействия с операционной системой UNIX системные вызовы. Они являются входами в ядро. Эти средства предоставляются операционной системой; все остальные средства построены на их

5.1. Системные вызовы fork() и ехес()

5.1. Системные вызовы fork() и ехес() Процесс в Linux (как и в UNIX) — это программа, которая выполняется в отдельном виртуальном адресном пространстве. Когда пользователь регистрируется в системе, под него автоматически создается процесс, в котором выполняется оболочка (shell),

Глава 7. Системные трюки

Глава 7. Системные трюки 7.1. Антивирус ClamAV 7.1.1. Вирусы в Linux Первым делом нужно разрушить миф о том, что в Linux нет вирусов. Вирусы в Linux есть. По данным сайта www.virus-list.com для Linux написано всего 1111 вирусов (на момент написания этих строк). Почему «всего»? Потому что по данным того же

3.4.2. Системные вызовы wait()

3.4.2. Системные вызовы wait() Самая простая функция в семействе называется wait(). Она блокирует вызывающий процесс до тех пор, пока один из его дочерних процессов не завершится (или не произойдет ошибка). Код состояния потомка возвращается через аргумент, являющийся указателем

5.5.2. Системные вызовы

5.5.2. Системные вызовы Сокеты являются более гибкими в управлении, чем рассмотренные выше механизмы межзадачного взаимодействия. При работе с сокетами используются следующие функции:? socket() — создает сокет;? close() — уничтожает сокет;? connect() — устанавливает соединение

Источник

rfLinux

Tuesday, March 18, 2008

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

Вместо введения.

По теме внутреннего устройства ядра Linux в общем, его различных подсистемах и о системных вызовах в частности, было писано и переписано уже порядком. Наверно, каждый уважающий себя автор должен хоть раз об этом написать, подобно тому, как каждый уважающий себя программист обязательно должен написать свой собственный файл-менеджер 🙂 Хотя я и не являюсь профессиональным IT-райтером, да и вообще, делаю свои пометки исключительно для своей пользы в первую очередь, чтобы не забыть изученное слишком быстро. Но, если уж кому-то пригодятся мои путевые заметки, конечно, буду только рад. Ну и вообще, кашу маслом не испортишь, поэтому, может даже мне и удастся написать или описать что-то такое, о чём никто не удосужился упомянуть.

Теория. Что такое системные вызовы?

Когда непосвящённым объясняют, что такое ПО (или ОС), то говорят обычно следующее: компьютер сам по себе — это железяка, а вот софт — это то, благодаря чему от этой железяки можно получить какую-то пользу. Грубовато, конечно, но в целом, в чём-то верно. Так же я сказал бы наверно об ОС и системных вызовах. На самом деле, в разных ОС системные вызовы могут быть реализованы по-разному, может розниться число этих самых вызовов, но так или иначе, в том или ином виде механизм системных вызовов есть в любой ОС. Каждый день пользователь явно или не явно работает с файлами. Он конечно может явно открыть файл для редактирования в своём любимом MS Word’е или Notepad’е, а может и просто запустить игрушку, исполняемый образ которой, к слову, тоже хранится в файле, который, в свою очередь, должен открыть и прочитать загрузчик исполняемых файлов. В свою очередь игрушка также может открывать и читать десятки файлов в процессе своей работы. Естественно, файлы можно не только читать, но и писать (не всегда, правда, но здесь речь не о разделении прав и дискретном доступе 🙂 ). Всем этим заведует ядро (в микроядерных ОС ситуация может отличаться, но мы сейчас будем ненавязчиво клониться к объекту нашего обсуждения — Linux, поэтому проигнорируем этот момент). Само по себе порождение нового процесса — это также услуга, предоставляемая ядром ОС. Всё это замечательно, как и то, что современные процессоры работают на частотах гигагерцевых диапазонов и состоят из многих миллионов транзисторов, но что дальше? Да то, что если бы небыло некого механизма, с помощью которого пользовательские приложения могли выполнять некоторые достаточно обыденные и, в то же время, нужные вещи (на самом деле, эти тривиальные действия при любом раскладе выполняются не пользовательским приложением, а ядром ОС — авт.), то ОС была просто вещью в себе — абсолютно бесполезной или же напротив, каждое пользовательское приложение само по себе должно было бы стать операционной системой, чтобы самостоятельно обслуживать все свои нужды. Мило, не правда ли?

Таким образом, мы подошли к определению системного вызова в первой аппроксимации: системный вызов — это некая услуга, которую ядро ОС оказывает пользовательскому приложению по запросу последнего. Такой услугой может быть уже упомянутое открытие файла, его создание, чтение, запись, создание нового процесса, получение идентификатора процесса (pid), монтирование файловой системы, останов системы, наконец. В реальной жизни системных вызовов гораздо больше, нежели здесь перечислено.

Как выглядит системный вызов и что из себя представляет? Ну, из того, что было сказано выше, становится ясно, что системный вызов — это подпрограмма ядра, имеющая соответственный вид. Те, кто имел опыт программирования под Win9х/DOS, наверняка помнят прерывание int 0x21 со всеми (или хотя бы некоторыми) его многочисленными функциями. Однако, есть одна небольшая особенность, касающаяся всех системных вызовов Unix. По соглашению функция, реализующая системный вызов, может принимать N аргументов или не принимать их вовсе, но так или иначе, функция должна возвращать значение типа int. Любое неотрицательное значение трактуется, как успешное выполнение функции системного вызова, а стало быть и самого системного вызова. Значение меньше нуля является признаком ошибки и одновременно содержит код ошибки (коды ошибок определяются в заголовках include/asm-generic/errno-base.h и include/asm-generic/errno.h). В Linux шлюзом для системных вызовов до недавнего времени было прерывание int 0x80, в то время, как в Windows (вплоть до версии XP Service Pack 2, если не ошибаюсь) таким шлюзом является прерывание 0x2e. Опять же, в ядре Linux, до недавнего времени все системные вызовы обрабатывались функцией system_call(). Однако, как выяснилось позднее, классический механизм обрабатки системных вызовов через шлюз 0x80 приводит к существенному падению производительности на процессорах Intel Pentium 4. Поэтому на смену классическому механизму пришёл метод виртуальных динамических разделяемых объектов (DSO — динамический разделяемый объектный файл. Не ручаюсь за правильный перевод, но DSO, это то, что пользователям Windows известно под названием DLL — динамически загружаемая и компонуемая библиотека) — VDSO. В чём отличие нового метода от классического? Для начала разберёмся с классическим методом, работающим через гейт 0x80.

Классический механизм обслуживания системных вызовов в Linux.

Прерывания в архитектуре х86.

Как было сказано выше, ранее для обслуживания запросов пользовательских приложений использовался шлюз 0x80 (int 0x80). Работа системы на базе архитектуры IA-32 управляется прерываниями (строго говоря, это касается вообще всех систем на базе x86). Когда происходит некое событие (новый тик таймера, какая-либо активность на некотором устройстве, ошибки — деление на ноль и пр.), генерируется прерывание. Прерывание (interrupt) названо так, потому что оно, как правило, прерывает нормальный ход выполнения кода. Прерывания принято подразделять на аппаратные и программные (hardware and software interrupts). Аппаратные прерывания — это прерывания, которые генерируются системными и периферическими устройствами. При возникновении необходимости какого-то устройства привлечь к себе внимание ядра ОС оно (устройство) генерирует сигнал на своей линии запроса прерывания (IRQ — Interrupt ReQuest line). Это приводит к тому, что на определённых входах процессора формируется соответствующий сигнал, на основе которого процессор и принимает решение прервать выполнение потока инструкций и передать управление на обработчик прерывания, который уже выясняет, что произошло и что необходимо сделать. Аппаратные прерывания асинхронны по своей природе. Это значит, что прерывание может возникнуть в любое время. Кроме периферийных устройств, сам процессор может вырабатывать прерывания (или, точнее, аппаратные исключения — Hardware Exceptions — например, уже упомянутое деление на ноль). Делается это с тем, чтобы уведомить ОС о возникновении нештатной ситуации, чтобы ОС могла предпринять некое действие в ответ на возникновение такой ситуации. После обработки прерывания процессор возвращается к выполнению прерванной программы. Прерывание может быть инициировано пользовательским приложением. Такое прерывание называют программным. Программные прерывания, в отличие от аппаратных, синхронны. Т.е., при вызове прерывания, вызвавший его код приостанавливается, пока прерывание не будет обслужено. При выходе из обработчика прерывания происходит возврат по дальнему адресу, сохранённому ранее (при вызове прерывания) в стеке, на следующую инструкцию после инструкции вызова прерывания (int). Обработчик прерывания — это резидентный (постоянно находящийся в памяти) участок кода. Как правило, это небольшая программа. Хотя, если мы будем говорить о ядре Linux, то там обработчик прерывания не всегда такой уж маленький. Обработчик прерывания определяется вектором. Вектор — это ни что иное, как адрес (сегмент и смещение) начала кода, который должен обрабатывать прерывания с данным индексом. Работа с прерываниями существенно отличается в реальном (Real Mode) и защищённом (Protected Mode) режиме работы процессора (напомню, что здесь и далее мы подразумеваем процессоры Intel и совместимые с ними). В реальном (незащищённом) режиме работы процессора обработчики прерываний определяются их векторами, которые хранятся всегда в начале памяти выборка нужного адреса из таблицы векторов происходит по индексу, который также является номером прерывания. Перезаписав вектор с определённым индексом можно назначить прерыванию свой собственный обработчик.

В защищённом режиме обработчики прерываний (шлюзы, гейты или вентили) больше не определяются с помощью таблицы векторов. Вместо этой таблицы используется таблица вентилей или, правильнее, таблица прерываний — IDT (Interrupt Descriptors Table). Эта таблица формируется ядром, а её адрес хранится в регистре процессора idtr. Данный регистр недоступен напрямую. Работа с ним возможна только при помощи инструкций lidt/sidt. Первая из них (lidt) загружает в регистр idtr значение, указанное в операнде и являющееся базовым адресом таблицы дексрипторов прерываний, вторая (sidt) — сохраняет адрес таблицы, находящийся в idtr, в указанный операнд. Так же, как происходит выборка информации о сегменте из таблицы дескрипторов по селектору, происходит и выборка дескриптора сегмента, обслуживающего прерывание в защищённом режиме. Защита памяти поддерживается процессорами Intel начиная с CPU i80286 (не совсем в том виде, в каком она представлена сейчас, хотя бы потому, что 286 был 16-разрядным процессором — поэтому Linux не может работать на этих процессорах) и i80386, а посему процессор самостоятельно производит все необходимые выборки и, стало быть, сильно углубляться во все тонкости защищённого режима (а именно в защищённом режиме работает Linux) мы не будем. К сожалению, ни время, ни возможности не позволяют нам остановиться надолго на механизме обработки прерываний в защищённом режиме. Да это и не было целью при написании данной статьи. Все сведения, приводимые здесь касательно работы процессоров семейства х86 довольно поверхностны и приводятся лишь для того, чтобы помочь немного лучше понять механизм работы системных вызовов ядра. Кое-что можно узнать непосредственно из кода ядра, хотя, для полного понимания происходящего, желательно всё же ознакомиться с принципами работы защищённого режима. Участок кода, который заполняет начальными значениями (но не устанавливает!) IDT, находится в arch/i386/kernel/head.S: Несколько замечаний по коду: приведённый код написан на разновидности ассемблера AT&T, поэтому Ваше знание ассемблера в его привычной интеловской нотации может только сбить с толку. Самое основное отличие в порядке операндов. Если для интеловской нотации определён порядок — «аккумулятор» После выяснения типа (со)процессора и проведения всех подготовительных действий в строках 3 и 4 мы загружаем таблицы GDT и IDT, которые будут использоваться на самых первых порах работы ядра.

Системные вызовы и int 0x80.

От прерываний вернёмся обратно к системным вызовам. Итак, что необходимо, чтобы обслужить процесс, который запрашивает какую-то услугу? Для начала, необходимо перейти из кольца 3 (уровень привелегий CPL=3) на наиболее привелигированный уровень 0 (Ring 0, CPL=0), т.к. код ядра расположен в сегменте с наивысшими привелегиями. Кроме того, необходим код обработчика, который обслужит процесс. Именно для этого и используется шлюз 0x80. Хотя системных вызовов довольно много, для всех них используется единая точка входа — int 0x80. Сам обработчик устанавливается при вызове функции arch/i386/kernel/traps.c::trap_init(): Нас в trap_init() больше всего интересует эта строка. В этом же файле выше можно посмотреть на код функции set_system_gate(): Здесь видно, что вентиль для прерывания 0х80 (а именно это значение определено макросом SYSCALL_VECTOR — можете поверить наслово 🙂 ) устанавливается как ловушка (trap) с уровнем привелегий DPL=3 (Ring 3), т.е. это прерывание будет отловлено при вызове из пространства пользователя. Проблема с переходом из Ring 3 в Ring 0 т.о. решена. Функция _set_gate() определена в заголовочном файле include/asm-i386/desc.h . Для особо любопытных ниже приведён код, без пространных объяснений, впрочем: Вернёмся к функции trap_init(). Она вызывается из функции start_kernel() в init/main.c . Если посмотреть на код trap_init(), то видно, что эта функция переписывает некоторые значения таблицы IDT заново — обработчики, которые использовались на ранних стадиях инициализации ядра (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault), заменяются на те, что будут использоваться уже в процессе работы ядра. Итак, мы практически добрались до сути и уже знаем, что все системные вызовы обрабатываются единообразно — через шлюз int 0x80. В качестве обработчика для int 0x80, как опять же видно из приведённого выше куска кода arch/i386/kernel/traps.c::trap_init(), устанавливается функция system_call().

system_call().

Код функции system_call() находится в файле arch/i386/kernel/entry.S и выглядит следующим образом: Код приведён не полностью. Как видно, сперва system_call() настраивает стек для работы в Ring 0, сохраняет значение, переданное ей через eax в стек, сохраняет все регистры также в стек, получает данные о вызывающем потоке и проверяет, не выходит ли переданное значение-номер системного вызова за пределы таблицы системных вызовов и затем, наконец, пользуясь значением, переданным в eax в качестве аргумента, system_call() осуществляет переход на настоящий обработчик системного вывода, исходя из того, на какой элемент таблицы ссылается индекс в eax. Теперь вспомните старую добрую таблицу векторов прерываний из реального режима. Ничего не напоминает? В реальности, конечно, всё несколько сложнее. В частности, системный вызов должен скопировать результаты из стека ядра в стек пользователя, передать код возврата и ещё некоторые вещи. В том случае, когда аргумент, указанный в eax не ссылается на существующий системный вызов (значение выходит за диапазон), происходит переход на метку syscall_badsys. Здесь в стек по смещению, по которому должно находиться значение eax, заносится значение -ENOSYS — системный вызов не реализован. На этом выполнение system_call() завершается.

Таблица системных вызовов находится в файле arch/i386/kernel/syscall_table.S и имеет достаточно простой вид: Иными словами, вся таблица являет собой ничто иное, как массив адресов функций, расположенных в порядке следования номеров системных вызовов, которые эти функции обслуживают. Таблица — обычный массив двойных машинных слов (или 32-разрядных слов — кому как больше нравится). Код части функций, обслуживающих системные вызовы, находится в платформно-зависимой части — arch/i386/kernel/sys_i386.c, а часть, не зависящая от платформы — в kernel/sys.c .

Вот так обстоит дело с системными вызовами и вентилем 0x80.

Новый механизм обработки системных вызовов в Linux. sysenter/sysexit.

Как упоминалось, достаточно быстро выяснилось, что использование традиционного способа обработки системных вызовов на основе гейта 0х80 пиводит к потере производительности на процессорах Intel Pentium 4. Поэтому Линус Торвальдс реализовал в ядре новый механизм, основанный на инструкциях sysenter/sysexit и призванный повысить производительность ядра на машинах, оснащённых процессором Pentium II и выше (именно с Pentium II+ процессоры Intel поддерживают упомянутые инструкции sysenter/sysexit). В чём суть нового механизма? Как ни странно, но суть осталась та же. Изменилось исполнение. Согласно документации Intel инструкция sysenter является частью механизма «быстрых системных вызовов». В частности, эта инструкция оптимизирована для быстрого перехода с одного уровня привелегий на другой. Если точнее, то она ускоряет переход в кольцо 0 (Ring 0, CPL=0). При этом, операционная система должна подготовить процессор к использовании инструкции sysenter. Такая настройка осуществляется единожды при загрузке и инициализации ядра ОС. При вызове sysenter устанавливает регистры процессора согласно машинно-зависимых регистров, ранее установленных ОС. В частности, устанавливаются сегментный регистр и регистр указателя инструкций — cs:eip, а также сегмент стека и указатель вершины стека — ss, esp. Переход на новый сегмент кода и смещение осуществляется из кольца 3 в 0.

Инструкция sysexit выполняют обратные действия. Она производит быстрый переход с уровня привелегий 0 на 3-й (CPL=3). При этом регистр сегмента кода устанавливается в 16 + значение сегмента cs, сохранённое в машинно-зависимом регистре процессора. В регистр eip заносится содержимое регистра edx. В ss заносится сумма 24 и значения cs, занесённое ОС ранее в машинно-зависимый регистр процессора при подготовке контекста для работы инструкции sysenter. В esp заносится содержимое регистра ecx. Значения, необходимые для работы инструкций sysenter/sysexit хранятся по следующим адресам:

  1. SYSENTER_CS_MSR 0х174 — сегмент кода, куда заносится значение сегмента, в котором находится код обработчика системного вызова.
  2. SYSENTER_ESP_MSR 0х175 — указатель на вершину стека для обработчика системного вызова.
  3. SYSENTER_EIP_MSR 0х176 — указатель на смещение внутри сегмента кода. Указывает на начало кода обработчика системных вызовов.

Данные адреса ссылаются на модельно-зависимые регистры, которые не имеют имён. Значения записываются в модельно зависимые регистры с помощью инструкции wrmsr, при этом edx:eax должны содержать страшую и младшую части 64-битного машинного слова соответственно, а в ecx должен быть занесён адрес ригистра, в который будет произведена запись. В Linux адреса модельно-зависимых регистров определяются в заголовочном файле include/asm-i368/msr-index.h следующим образом (до версии 2.6.22 как минимум они определялись в заголовочном файле include/asm-i386/msr.h, напомню, что мы рассматриваем механизм системных вызовов на примере ядра Linux 2.6.22): Код ядра, ответственный за установку модельно-зависимых регистров, находится в файле arch/i386/sysenter.c и выглядит следующим образом: Здесь в переменную tss мы получаем адрес структуры, описывающей сегмент состояния задачи. TSS (Task State Segment) используется для описания контекста задачи и является частью механизма аппаратной поддержки многозадачности для архитектуры x86. Однако, Linux практически не использует аппаратное переключение контекста задач. Согласно документации Intel переключение на другую задачу производится либо путём выполнения инструкции межсегментного перехода (jmp или call), ссылающейся на сегмент TSS, либо на дескриптор вентиля задачи в GDT (LDT). Специальный регистр процессора, невидимый для программиста — TR (Task Register — регистр задачи) содержит селектор дескриптора задачи. При загрузке этого регистра также загружаются программно-невидимые регистры базы и лимита, связанные с TR.

Несмотря на то, что Linux не использует аппаратное переключение контекстов задач, ядро вынуждено отводить запись TSS для каждого процессора, установленного в системе. Это связано с тем, что когда процессор переключается из пользовательского режима в режим ядра, он извлекает из TSS адрес стека ядра. Кроме того, TSS необходим для управления доступом к портам ввода/вывода. TSS содержит карту прав доступа к портам. На основе этой карты становится возможным осуществлять контроль доступа к портам для каждого процесса, использующего инструкции in/out. Здесь tss->x86_tss.esp1 указывает на стек ядра. __KERNEL_CS естественно указывает на сегмент кода ядра. В качестве смещения-eip указывается адрес функции sysenter_entry().

Функция sysenter_entry() определена в файле arch/i386/kernel/entry.S и имеет такой вид: Как и в случае с system_call() основная работа выполняется в строке call *sys_call_table(,%eax,4). Здесь вызывается конкретный обработчик системного вызова. Итак, видно, что принципиально изменилось мало. То обстоятельство, что вектор прерывания теперь забит в железо и процессор помогает нам быстрее перейти с одного уровня привелегий на другой меняет лишь некоторые детали исполнения при прежнем содержании. Правда, на этом изменения не заканчиваются. Вспомните, с чего начиналось повествование. В самом начале я упоминал уже о виртуальных разделяемых объектах. Так вот, если раньше реализация системного вызова, скажем, из системной библиотеки libc выглядела, как вызов прерывания (при том, что библиотека брала некоторые функции на себя, чтобы сократить число переключений контекстов), то теперь благодаря VDSO системный вызов может быть сделан практически напрямую, без участия libc. Он и ранее мог быть осуществлён напрямую, опять же, как прерывание. Но теперь вызов можно затребовать, как обычную функцию, экспортируемую из динамически компонуемой библиотеки (DSO). При загрузке ядро определяет, какой механизм должен и может быть использован для данной платформы. В зависимости от обстоятельств ядро устанавливает точку входа в функцию, выполняющую системный вызов. Далее, функция экспортируется в пользовательское пространство ввиде библиотеки linux-gate.so.1 . Библиотека linux-gate.so.1 физически не существует на диске. Она, если можно так выразиться, эмулируется ядром и существует ровно столько, сколько работает система. Если выполнить останов системы, подмонтировать корневую ФС из другой системы, то Вы не найдёте на корневой ФС остановленной системы этот файл. Собственно, Вы не сможете его найти даже на работающей системе. Физически его просто нет. Именно поэтому linux-gate.so.1 — это нечто иное, как VDSO — т.е. Virtual Dynamically Shared Object. Ядро отображает эмулируемую таким образом динамическую библиотеку в адресное пространство каждого процесса. Убедиться в этом несложно, если выполнить следующую команду: Здесь самая последняя строка и есть интересующий нас объект: Из приведённого примера видно, что объект занимает в памяти ровно одну страницу — 4096 байт, практически на задворках адресного пространства. Проведём ещё один эксперимент: Здесь мы просто навскидку взяли два приложения. Видно, что библиотека отображается в адресное пространство процесса по одному и тому же постоянному адресу — 0xffffe000. Теперь попробуем посмотреть, что же такое хранится на этой странице памяти на самом деле.

Сделать дамп страницы памяти, где хранится разделяемый код VDSO, можно с помощью следующей программы: Строго говоря, раньше это можно было сделать проще, с помощью команды dd if=/proc/self/mem of=test.dump bs=4096 skip=1048574 count=1, но ядра начиная с версии 2.6.22 или, быть может, даже более ранней, больше не отображают память процесса в файл /proc/`pid`/mem. Этот файл, сохранён, очевидно, для совместимости, но не содержит более информации.

Скомпилируем и прогоним приведённую программу. Попробуем дизассемблировать полученный код: Вот он наш шлюз для системных вызовов, весь, как на ладони. Процесс (либо, системная библиотека libc), вызывая функцию __kernel_vsyscall попадает на адрес 0хffffe400 (в нашем случае). Далее, __kernel_vsyscall сохраняет в стеке пользовательского процесса содержимое регистров ecx, edx, ebp, О назначении регистров ecx и edx мы уже говорили ранее, в ebp используется позже для восстановления стека пользователя. Выполняется инструкция sysenter, «перехват прерывания» и, как следствие, очередной переход на sysenter_entry (см. выше). Инструкция jmp по адресу 0xffffe40e вставлена для перезапуска системного вызова с числом 6 аргументами (см. http://lkml.org/lkml/2002/12/18/). Код, размещаемый на странице, находится в файле arch/i386/kernel/vsyscall-enter.S (или arch/i386/kernel/vsyscall-int80.S для ловушки 0x80). Хотя я и нашёл, что адрес функции __kernel_vsyscall постоянный, но есть мнение, что это не так. Обычно, положение точки входа в __kernel_vsyscall() можно найти по вектору ELF-auxv используя параметр AT_SYSINFO. Вектор ELF-auxv содержит информацию, передаваемую процессу через стек при запуске и содержит различную информацию, нужную в процессе работы программы. Этот вектор в частности содержит переменные окружения процесса, аргументы, и проч..

Вот небольшой пример на С, как можно обратиться к функции __kernel_vsyscall напрямую: Данный пример взят со страницы Manu Garg, http://www.manugarg.com. Итак, в приведённом примере мы делаем системный вызов getpid() (номер 20 или иначе __NR_getpid). Чтобы не лазить по стеку процесса в поисках переменной AT_SYSINFO воспользуемся тем обстоятельством, что системная библиотека libc.so при загрузке копирует значение переменной AT_SYSINFO в блок управления потоком (TCB — Thread Control Block). На этот блок информации, как правило, ссылается селектор в gs. Предполагаем, что по смещению 0х10 находится искомый параметр и делаем вызов по адресу, хранящемуся в %gs:$0x10.

Итоги.

На самом деле, практически, особого прироста производительности даже при поддержке на данной платформе FSCF (Fast System Call Facility) добиться не всегда возможно. Проблема в том, что так или иначе, процесс редко обращается напрямую к ядру. И для этого есть свои веские причины. Использование библиотеки libc позволяет гарантировать переносимость программы вне зависимости от версии ядра. И именно через стандартную системную библиотеку идёт большинство системных вызовов. Если даже Вы соберёте и установите самое последнее ядро, собранное для платформы, поддерживающей FSCF, это ещё не гарантия прироста производительности. Дело в том, что Ваша системная библиотека libc.so будет попрежнему использовать int 0x80 и справиться с этим можно лишь пересобрав glibc. Поддерживается ли в glibc вообще интерфейс VDSO и __kernel_vsyscall, я, честно признаться, на данный момент ответить затрудняюсь.

Ссылки.

[1] Manu Garg’s page, http://www.manugarg.com
[2] Scatter/Gather thoughts by Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
[3] Старый добрый Understanding the Linux kernel Куда же без него 🙂
[4] Ну и конечно же, исходные коды Linux (2.6.22)

2008-03-18. Further revisions are possible.

Источник

Читайте также:  Ноутбук dell 3552 windows
Оцените статью