- linux notes
- суббота, 13 февраля 2016 г.
- Системные вызовы Linux небольшой справочник
- Где вы найдете таблицу системных вызовов для Linux?
- rfLinux
- Tuesday, March 18, 2008
- Linux — syscalls. Системные вызовы в Linux.
- Вместо введения.
- Теория. Что такое системные вызовы?
- Классический механизм обслуживания системных вызовов в Linux.
- Системные вызовы и int 0x80.
- system_call().
- Новый механизм обработки системных вызовов в Linux. sysenter/sysexit.
- Итоги.
- Ссылки.
linux notes
суббота, 13 февраля 2016 г.
Системные вызовы Linux небольшой справочник
Знание системных вызовов очень облегчает работу программистов (для написаний программ на языке программирования С) и системным администратором при дебаге какой-либо программы, с помощью таких инструментов, как: strace/gdb и пр. Вот небольшое описание системных вызовов Linux.
1. exit Завершение процесса
2. fork Создание дочернего процесса
3. read чтение из файлового дескриптора
4. write Запись в файловый дескриптора
5. open открытие файла или устройства
6. close закрытие файлового дескриптора
7. waitpid ожидание завершения процесса
8. creat создание файла или устройства (man 2 open)
9. link задание нового имени файла
10. unlink удаление имени и файла
11. execve выполнить программу
12. chdir изменить рабочую директорию
13. time получить время в секундах
14. mknod создание специального блочного или символьного файла
15. chmod изменение прав доступа к файлу
16. lchown изменение владельца файла
17. stat получения статус файла
18. lseek установка смещения для позиционирования операций чтения/записи
19. getpid получение идентификатора процесса
20. mount монтирование файловой системы
21. umount размонтирование файловой системы
22. setuid установить идентификатор пользователя
23. getuid получить идентификатор пользователя
24. stime установить системное время и дату
25. ptrace разрешить родительскому процессу контролировать выполнеине дочернего процесса
26. alarm установка времени доставки тревожного сигнала
27. fstat получить статус файла
28. pause останавливает процесс до получения сигнала
29. utime изменить время доступа и/или модификация inode
30. access проверить права доступа пользователя к файлу
31. nice изменить приоритет процесса
32. sync сброс буферов файловой системы
33. kill отправить сигнал процессу
34. rename изменить имя или расположение файла
35. mkdir создать директорию
36. rmdir удалить директорию
37. dup создать дубликат файлового дескриптора
38. pipe создает межпроцессорный канал
39. times ведет запись времен процесса
40. brk изменение размера сегмента данныхх
41. setdig установить идентификатор группы
42. getgid получить идентификатор группы
43. sys_signal ANSI C обработка сигналов
44. geteuid получить эффективный идентификатор пользователя, установленный setuid битом на исполняемом файле
45. getegid получить эффективный идентификатор группы, установленные setuid битом на исполняемом файле
46. acct включает или выключает учет процессов
47. umount2 размонтировать файловую систему
48. ioctl управление устройствами
49. fcntl манипуляция с файловым дескриптором
50. setpgid установить идентификатор группы дял процесса
51. umask получение или установка маски режима создания файлов
52. chroot изменение корневой директории
53. ustat получить статистику по файловой системе
54. dup2 создать дубликат файлового дескриптора
55. getppid получить ID родительского процесса
56. getpgrp получить ID группы процесса
57. setsid создает сеанс и устанвливает идентификатор группы
58. sigaction POSIX обработка сигналов
59. sgetmask ANSI C обработка сигналов
60. ssetmask ANCI C обработки сигналов
61. setreuid устанавливает действительный и/или действующий идентификатор пользователя
62. setregid устанавливает дейсвтительный и/или действующий идентификатор группы
63. sigsuspend временно изменяет значение маски блокировки сигналов процесса
64. sigpendind позволяет определить наличие ожидающих сигналов
65. sethostname устанавливает хостнейм
66. setrlimit устанавливает ограничения использования ресурсов
67. getrlimit получить ограничение использования ресурсов
68. getrusage получить максимальное ограничение использования ресурсов
69. gettimeofday получить время
70. settimeofday установить время
71. getgroups получить список дополнительных идентификаторов групп
72. setgroups установить список дополнительных идентификаторов групп
73. old_select синхронизировать I/O мультиплексирование
74. symlink создать символическую ссылку на файл
75. lstat считать статус файла
76. readlink вывести значение символической ссылки
77. uselib выбрать разделяемую библиотеку
78. swapon разрешает подкачку данных в файл/устройство
79. reboot перезагружает систему и разрешает/запрещает использование клавш Ctrl-Alt-Del
80. old_readdir считывает данные директории
81. old_mmap отражает файлы или устройства в памяти
82. munmap снимает отражение файлов или устройств в памяти
83. truncate укорачивает файл до заданной длины (файл должен быть доступен для записи)
84. ftruncate укорачивает файл до заданноой длины (файл должен быть открыт для записи)
85. fchmod изменить права доступа к файлу
86. fchown изменить владельца и группу файла
87. getpriority получить приоритет программы
88. setpriority установить приоритет программы
89. profil выполняет профилирование времени
90. statfs получить статистику файловой системы
91. fstatfs получить статистику файловой системы
92. ioperm устанавливает права на работу с портами ввода/вывода
93. socketcall системные вызовы сокетов
94. syslog читает и/или очищает кольцевой буфер сообщений ядра, устанавливает console_loglevel
95. setitimer устанавливает значение таймера интервалов (interval timer)
96. getitimer считывает значение таймера интервалов
97. sys_newstat получить статус файла
98. sys_newlstat получить статус файла
99. sys_newfstat получить статус файла
100. iopl менять уровень привелений ввода-вывода
101. vhangup имитирует прекращение работы на текущем терминале
102. idle делает процесс 0 простаиваемым
103. vm86old включить виртуальный 8086 режим
104. wait4 ожидает завершение работы процесса (стиль BSD)
105. swapoff запрещает подкачку данных в файл/устройство
106. sysinfo возвращает общесистемную статистику
107. ipc системные вызовы System V IPC
108. fsync синхронизирует состояние файла в памяти с состоянием на диске
109. sigreturn возвращается из обработчика сигнала и очищает временный стек
110. clone создать процесс-потомок
110. setdomainname установить имя домена
111. uname сообщает информацию о данном компьютере и операционной системе
112. modify_ldt получает или устанавливает ldt
113. adjtimex тонкая настройка часов в ядре
114. mprotect контролирует доступ к области памяти
115. sigprocmask POSIX-фукнция обработки сигналов
116. create_module создает загружаемый модуль
117. init_module инициализирует загружаемый модуль
118. delete_module удаляет загружаемый модуль
119. get_kernel_syms retrieve exported kernel and module symbols
120. quotactl работает с дисковыми квотами
121. getpgid получает группу процессов
122. fchdir изменить рабочий каталог
123. bdflush запустить, активизировать или настроить демона записи буферов кэша
124. sys_newfstat получает информацию о файловой системе
125. personality устанавливает домен исполнения процесса
126. setfsuid устанавливает идентификатор пользователя файловой системы
127. setfsgid устанавливает идентификатор группы для досутпа к файловой системе
128. sys_llseek move extended read/write file pointer
129. getdents получить записи каталога
130. select многопоточный синхронный ввод-вывод
131. flock установить или снять advisory Блокировку на открытый файл
132. msync синхронизирует содержимое файла с его отражением в памяти
133. readv чтение данных из нескольких буферов
134. writev запись данных в несколько буферов
135. sys_getsid get process group ID of session leader
136. fdatasync снихронизирует содержимое файла в памяти с содержимым на диске
137. sysctl читает/записывает параметры системы
138. mlock запрещает страинчный обмен в некоторых областях памяти
139. munlock разрашает страинчный обмен в некоторых областях памяти
140. mlockall запрещает страничный обмен всему процессу
141. munlockall разрашает страничный обмен всему процессу
142. sched_setparam устанавливает параметры планировщика
143. sched_getparam получает параметры планировщика
144. sched_setscheduler устанавливает алгоритм планировщика (и его параметры)
145. sched_getscheduler получает алгоритм планировщика (и его параметры)
146. sched_yield освободить процессор
147. sched_get_priority_max задать верхнюю планку диапазона статических приоритетов
148. sched_get_priority_min задать нижнию планку диапазона статических приоритетов
149. sched_rr_get_interval определяет временной интервал SCHED_RR для заданного процесса
150. nanosleep останавливает работу процесса на заданное время (нано секунды)
151. mremap изменяет отражение адреса виртуальной памяти
152. setresuid устанавливает идентификатор реальной, эффективной и сохранненной групп пользователей
153. getresuid получает идентификатор реальной, эффективной и сохранненной групп пользователей
154. vm86 войти в виртуальный режим 8086
155. query_module query the kernel for various bits pertain ing to modules
156. poll ожидает некоторое событие в файловом описателе
157. nfsservctl интерфейсная функция системного демона nfs
158. setresgid устанавливает идентификатор реальной, эффективной и сохранненной групп пользователей
159. getresgid получает идентификатор реальной, эффективной и сохранненной групп пользователей
160. prctl оперирует процессами
161. pread чтение информации из опистаеля файла согласно заданному смещению
162. pwrite запись информации из описателя файла согласно заданному смещению
163. chown изменяет владельца и группу файлов
164. getcwd возвращает текущий рабочий каталог
165. capget получить возможности процесса
166. capset установить возможности процесса
167. sigaltstack считывает или устанавливает содержимое дополнительного стека сигнала
168. sendfile производит обмен данными между описателями файлов
169. vfork создает дочерний процесс и блокирует родительский
Источник
Где вы найдете таблицу системных вызовов для Linux?
Я вижу много людей, ссылающихся на интернет
для таблицы системных вызовов, это работает отлично. Но много других ссылок
который обычно находится в пакете заголовков. Как приходят syscall_64.tbl шоу,
Правильный ответ и unistd.h показывает,
И тогда это показывает __NR_read как
Почему это 63, а не 1? Как мне разобраться /include/uapi/asm-generic/unistd.h ? Еще /usr/include/asm/ есть
Может ли кто-нибудь сказать мне разницу между этими unistd файлами. Объясните как unistd.h работает? И какой лучший способ найти таблицу системных вызовов?
Когда я исследую подобные вещи, я считаю полезным обратиться напрямую к компилятору (подробности см. В разделе « Печать стандартных предопределенных макросов C / GCC в терминале» ):
Это показывает , что заголовки , участвующие (в Debian) являются /usr/include/x86_64-linux-gnu/sys/syscall.h , /usr/include/x86_64-linux-gnu/asm/unistd.h , /usr/include/x86_64-linux-gnu/asm/unistd_64.h , и /usr/include/x86_64-linux-gnu/bits/syscall.h , и печатает номер системного вызова для read , что 0 на x86-64.
Вы можете найти номера системных вызовов для других архитектур, если у вас установлены соответствующие системные заголовки (в среде кросс-компиляции). Для 32-битной x86 это довольно просто:
который включает в себя /usr/include/asm/unistd_32.h среди других заголовочных файлов, и печатает номер 3.
Таким образом, с точки зрения пользовательского пространства, 32-битные системные вызовы x86 определены в asm/unistd_32.h 64-битных системных вызовах x86 asm/unistd_64.h . asm/unistd_x32.h используется для x32 ABI .
uapi/asm-generic/unistd.h перечисляет системные вызовы по умолчанию, которые используются в архитектурах, которые не имеют специфической для архитектуры таблицы системных вызовов.
В ядре ссылки немного отличаются и зависят от архитектуры (опять же, для архитектур, которые не используют общую таблицу системных вызовов). Именно здесь arch/x86/entry/syscalls/syscall_64.tbl приходят такие файлы (и в конечном итоге они создают файлы заголовков, которые используются в пространстве пользователя unistd_64.h и т. Д.). Вы найдете гораздо больше подробностей о системных вызовах в паре статей LWN на тему: Анатомия части 1 системного вызова и Анатомия части 2 системного вызова .
Источник
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.
Источник