- Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Re: Пример добавления системного вызова в Linux
- Русские Блоги
- «Проектирование и реализация ядра Linux» Заметки для чтения (5) — Системный вызов
- 1. Что такое системный вызов
- 2. Принцип реализации системного вызова в Linux
- 2.1 Сообщите ядру, какой системный вызов вызывать
- 2.2 Пользовательская программа передает ядру параметры системного вызова
- 2.3 Пользовательская программа получает значение, возвращаемое системным вызовом, возвращаемое ядром
- 3. Выполнение простого системного вызова.
- 3.1 Подготовка окружающей среды
- 3.2 Измените соответствующие файлы в исходном коде ядра
- rfLinux
- Tuesday, March 18, 2008
- Linux — syscalls. Системные вызовы в Linux.
- Вместо введения.
- Теория. Что такое системные вызовы?
- Классический механизм обслуживания системных вызовов в Linux.
- Системные вызовы и int 0x80.
- system_call().
- Новый механизм обработки системных вызовов в Linux. sysenter/sysexit.
- Итоги.
- Ссылки.
Пример добавления системного вызова в Linux
Если вы часто пишете программы и постоянно мучаете себя вопросом, почему нету такого хорошего вызова XYZ в ядре, то эта статья для вас. Она научит вас как самим реализовать этот хороший вызов XYZ, и возможно, вы поймете, что он не такой и хороший. да и вовсе не нужный 🙂
Re: Пример добавления системного вызова в Linux
IBM в последнее время радует позновательными статьями.
Re: Пример добавления системного вызова в Linux
> как самим реализовать этот хороший вызов XYZ, и возможно, вы поймете, что он не такой и хороший. да и вовсе не нужный
Re: Пример добавления системного вызова в Linux
Спасибо, познавательная статься. Прочитал с удовольствием. :d
>Another way to make your services visible to user-space is through the /proc file system.
Я так понимаю, об этом будет следующая статья? Понимаю, что [|||||], но все же было бы очень интересно почитать.. Не забудьте только на лоре новость запостить.. 😉
>older x86 processors used an interrupt mechanism to migrate from user-space to kernel-space, but new IA-32 processors provide instructions that optimize this transition
Следует ли из этого, что на современных процессорах этот переход является дешевым? =\
Re: Пример добавления системного вызова в Linux
Там в ресурсах есть ссылка на статью как раз про файловую систему/proc.
Re: Пример добавления системного вызова в Linux
> Она научит вас как самим реализовать этот хороший вызов XYZ, и возможно, вы поймете, что он не такой и хороший. да и вовсе не нужный 🙂
А прочитав эту статью, вы поймёте, что статьи не нужны — если есть исходный код ядра ? 🙁
Re: Пример добавления системного вызова в Linux
>Там в ресурсах есть ссылка на статью как раз про файловую систему/proc.
Ура! Надо же, самую первыю ссылку и не заметил. 😀
Re: Пример добавления системного вызова в Linux
«Вы делаете успехи, мой юный подаван» (с)
Статьи, документация и т.п. должны быть только для общих вещий, какие-то специфики все равно нереально отразить «на живую». В общем, код, это лучшая документация, но, не каждый код может быть понятный 🙁
Re: Пример добавления системного вызова в Linux
Re: Пример добавления системного вызова в Linux
Имхо, стоит написать статью «как осознать, что придуманный вами системный вызов вовсе не нужен». 🙂
Re: Пример добавления системного вызова в Linux
Посмотрим на досуге, чего там понаписали.. 😉
Re: Пример добавления системного вызова в Linux
Готов написать статью, после прочтения которой вы поймете, что и программа вам не нужна 😉
Re: Пример добавления системного вызова в Linux
>Если вы часто пишете программы и постоянно мучаете себя вопросом, почему нету такого хорошего вызова XYZ в ядре
Верный признак идиота от программирования. Все «хорошие вызовы» еще в прошлом веке придумали бородатые хиппи, на которых иногда можно натолкнуться в коридорах комитета POSIX.
Re: Пример добавления системного вызова в Linux
>вы поймете, что и программа вам не нужна 😉
Источник
Русские Блоги
«Проектирование и реализация ядра Linux» Заметки для чтения (5) — Системный вызов
основное содержание:
- Что такое системный вызов
- Принцип реализации системного вызова в Linux
- Реализация простого системного вызова
1. Что такое системный вызов
Проще говоря, системный вызов — это мост между пользовательской программой и аппаратным устройством.
При необходимости пользовательская программа использует аппаратное устройство посредством системных вызовов.
Существование системных вызовов имеет следующие важные значения:
1) Пользовательская программа использует оборудование через системные вызовы, не заботясь о конкретных аппаратных устройствах, что значительно упрощает разработку пользовательских программ.
Например, пользовательская программа может записывать данные в файл с помощью системного вызова write (), не беспокоясь о том, находится ли файл на диске, гибком диске или другом хранилище.
2) Системные вызовы делают пользовательские программы более переносимыми.
Пока интерфейс системного вызова, предоставляемый операционной системой, является одинаковым, пользовательская программа может быть перенесена из одной системы в другую без изменений.
3) Системные вызовы позволяют ядру лучше управлять пользовательскими программами и повышать стабильность системы.
Поскольку системный вызов реализуется ядром, ядро использует системный вызов для управления тем, какие функции и разрешения открыты для пользовательских программ.
Это может помешать пользовательским программам неправильно использовать аппаратные устройства, что приведет к разрушению других программ.
4) Системный вызов эффективно разделяет разработку пользовательской программы и ядра.
Пользовательской программе нужно заботиться только об API системных вызовов и использовать эти API для разработки своих собственных приложений, а не конкретной реализации API.
Ядру нужно заботиться только о реализации API системных вызовов, независимо от того, как они вызываются.
Вызывающие отношения между пользовательскими программами, системными вызовами, ядрами и аппаратными устройствами следующие:
2. Принцип реализации системного вызова в Linux
Для реализации системного вызова в основном реализованы следующие аспекты:
- Сообщите ядру, какой системный вызов вызывать
- Пользовательская программа передает в ядро параметры системного вызова.
- Пользовательская программа получает возвращаемое ядром значение системного вызова.
Давайте посмотрим, как Linux реализует указанные выше три функции.
2.1 Сообщите ядру, какой системный вызов вызывать
Каждый системный вызов имеет номер системного вызова.Когда происходит системный вызов, ядро знает, какой системный вызов основан на номере входящего системного вызова.
В архитектуре x86 пользовательское пространство помещает номер системного вызова в eax, а обработчик системных вызовов получает номер системного вызова через eax.
Номер системного вызова определен в коде ядра: arch / alpha / include / asm / unistd.h. Видно, что в Linux не так много системных вызовов.
2.2 Пользовательская программа передает ядру параметры системного вызова
Параметры системного вызова также передаются в ядро через регистры. В системе x86 первые 5 параметров системного вызова помещаются в ebx, ecx, edx, esi и edi. Если параметров много, необходим отдельный регистр. Указатель на адрес всех параметров пользовательского пространства.
Доступ к общим системным вызовам осуществляется через библиотеку C (наиболее часто используемой является библиотека glibc), а ядро Linux предоставляет способ прямого доступа к системным вызовам из пользовательских программ.
См. Код ядра: arch / cris / include / arch-v10 / arch / unistd.h
В нем определены 6 макросов, которые могут вызывать системные вызовы с количеством параметров от 0 до 6.
Системные вызовы с более чем 6 параметрами встречаются редко, поэтому здесь определены только 6.
2.3 Пользовательская программа получает значение, возвращаемое системным вызовом, возвращаемое ядром
Получение возвращаемого значения системного вызова также осуществляется через регистр.В системе x86 возвращаемое значение помещается в eax.
3. Выполнение простого системного вызова.
Поняв принцип работы системных вызовов в Linux, вы сможете реализовать простой системный вызов самостоятельно.
3.1 Подготовка окружающей среды
Чтобы не разрушить существующую систему, я экспериментировал с виртуальной машиной.
Хост: fedora16 x86_64 system + kvm (разновидность технологии виртуализации, такая как virtualbox, vmware и т. Д.)
Виртуальная машина: Также установлена система Fedora16 x86_64 (легко установить систему через virt-manager)
Загрузите исходный код ядра: www.kernel.org Загрузите последнюю версию
3.2 Измените соответствующие файлы в исходном коде ядра
В основном изменяйте следующие файлы:
Я добавил 2 функции в sys.c: sys_foo и sys_bar
Если вы добавляете системный вызов в ядро x86_64, вам нужно только изменить arch / x86 / include / asm / unistd_64.h, например sys_bar.
Пожалуйста, обратитесь к следующему файлу сравнения для измененного содержимого:
Источник
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 хранятся по следующим адресам:
- SYSENTER_CS_MSR 0х174 — сегмент кода, куда заносится значение сегмента, в котором находится код обработчика системного вызова.
- SYSENTER_ESP_MSR 0х175 — указатель на вершину стека для обработчика системного вызова.
- 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.
Источник