What is linux system call

System Calls¶

Lecture objectives:¶

  • Linux system calls implementation
  • VDSO and virtual syscalls
  • Accessing user space from system calls

Linux system calls implementation¶

At a high level system calls are «services» offered by the kernel to user applications and they resemble library APIs in that they are described as a function call with a name, parameters and return value.

However, on a closer look, we can see that system calls are actually not function calls, but specific assembly instructions (architecture and kernel specific) that do the following:

  • setup information to identify the system call and its parameters
  • trigger a kernel mode switch
  • retrieve the result of the system call

In Linux, system calls are identified by numbers and the parameters for system calls are machine word sized (32 or 64 bit). There can be a maximum of 6 system call parameters. Both the system call number and the parameters are stored in certain registers.

For example, on 32bit x86 architecture, the system call identifier is stored in the EAX register, while parameters in registers EBX, ECX, EDX, ESI, EDI, EBP.

System libraries (e.g. libc) offers functions that implement the actual system calls in order to make it easier for applications to use them.

When a user to kernel mode transition occurs, the execution flow is interrupted and it is transfered to a kernel entry point. This is similar with how interrupts and exception are handled (in fact on some architectures this transition happens as a result of an exception).

The system call entry point will save registers (which contains values from user space, including system call number and system call parameters) on stack and then it will continue with executing the system call dispatcher.

During the user — kernel mode transition the stack is also switched from ther user stack to the kernel stack. This is explained in more details in the interrupts lecture.

The purpose of the system call dispatcher is to verify the system call number and run the kernel function associated with the system call.

To demonstrate the system call flow we are going to use the virtual machine setup, attach gdb to a running kernel, add a breakpoint to the dup2 system call and inspect the state.

In summary, this is what happens during a system call:

  • The application is setting up the system call number and parameters and it issues a trap instruction
  • The execution mode switches from user to kernel; the CPU switches to a kernel stack; the user stack and the return address to user space is saved on the kernel stack
  • The kernel entry point saves registers on the kernel stack
  • The system call dispatcher identifies the system call function and runs it
  • The user space registers are restored and execution is switched back to user (e.g. calling IRET)
  • The user space application resumes

System call table¶

The system call table is what the system call dispatcher uses to map system call numbers to kernel functions:

System call parameters handling¶

Handling system call parameters is tricky. Since these values are setup by user space, the kernel can not assume correctness and must always verify them throughly.

Pointers have a few important special cases that must be checked:

  • Never allow pointers to kernel-space
  • Check for invalid pointers

Since system calls are executed in kernel mode, they have access to kernel space and if pointers are not properly checked user applications might get read or write access to kernel space.

For example, lets consider the case where such a check is not made for the read or write system calls. If the user passes a kernel-space pointer to a write system call then it can get access to kernel data by later reading the file. If it passes a kernel-space pointer to a read system call then it can corrupt kernel memory.

Likewise, if a pointer passed by the application is invalid (e.g. unmapped, read-only for cases where it is used for writing), it could «crash» the kernel. There two approaches that could be used:

  • Check the pointer against the user address space before using it, or
  • Avoid checking the pointer and rely on the MMU to detect when the pointer is invalid and use the page fault handler to determine that the pointer was invalid

Although it sounds tempting, the second approach is not that easy to implement. The page fault handler uses the fault address (the address that was accessed), the faulting address (the address of the instruction that did the access) and information from the user address space to determine the cause:

  • Copy on write, demand paging, swapping: both the fault and faulting addresses are in user space; the fault address is valid (checked against the user address space)
  • Invalid pointer used in system call: the faulting address is in kernel space; the fault address is in user space and it is invalid
  • Kernel bug (kernel accesses invalid pointer): same as above
Читайте также:  Как сделать чтобы окна windows открывались во весь экран

But in the last two cases we don’t have enough information to determine the cause of the fault.

In order to solve this issue Linux uses special APIs (e.g copy_to_user() ) to accesses user space that are specially crafted:

  • The exact instructions that access user space are recorded in a table (exception table)
  • When a page fault occurs the faulting address is checked against this table

Although the fault handling case may be more costly overall depending on the address space vs exception table size, and it is more complex, it is optimized for the common case and that is why it is preferred and used in Linux.

Cost Pointer checks Fault handling
Valid address address space search negligible
Invalid address address space search exception table search

Virtual Dynamic Shared Object (VDSO)¶

The VDSO mechanism was born out of the necessity of optimizing the system call implementation, in a way that does not impact libc with having to track the CPU capabilities in conjunction with the kernel version.

For example: x86 has two ways of issuing system calls: int 0x80 and sysenter. The later is significantly faster so it should be used when available. However, it is only available for processors newer than Pentium II and only for kernel versions greater than 2.6.

With VDSO the system call interface is decided by the kernel:

  • a stream of instructions to issue the system call is generated by the kernel in a special memory area (formatted as an ELF shared object)
  • that memory area is mapped towards the end of the user address space
  • libc searches for VDSO and if present will use it to issue the system call

An interesting development of the VDSO are the virtual system calls (vsyscalls) which run directly from user space. These vsyscalls are also part of VDSO and they are accessing data from the VDSO page that is either static or modified by the kernel in a separate read-write map of the VDSO page. Examples of system calls that can be implemented as vsyscalls are: getpid or gettimeofday.

  • «System calls» that run directly from user space, part of the VDSO
  • Static data (e.g. getpid())
  • Dynamic data update by the kernel a in RW map of the VDSO (e.g. gettimeofday(), time(), )

Accessing user space from system calls¶

As we mentioned earlier, user space must be accessed with special APIs ( get_user() , put_user() , copy_from_user() , copy_to_user() ) that check wether the pointer is in user space and also handle the fault if the pointer is invalid. In case of invalid pointers they return a non zero value.

Let’s examine the simplest API, get_user, as implemented for x86:

The implementation uses inline assembly, that allows inserting ASM sequences in C code and also handles access to / from variables in the ASM code.

Based on the type size of the x variable, one of __get_user_1, __get_user_2 or __get_user_4 will be called. Also, before executing the assembly call, ptr will be moved to the first register EAX while after the completion of assembly part the value of EAX will be moved to __ret_gu and the EDX register will be moved to __val_gu.

It is equivalent to the following pseudo code:

The __get_user_1 implementation for x86 is the following:

The first two statements check the pointer (which is stored in EDX) with the addr_limit field of the current task (process) descriptor to make sure that we don’t have a pointer to kernel space.

Then, SMAP is disabled, to allow access to user from kernel, and the access to user space is done with the instruction at the 1: label. EAX is then zeroed to mark success, SMAP is enabled, and the call returns.

The movzbl instruction is the one that does the access to user space and its address is captured with the 1: label and stored in a special section:

For each address that accesses user space we have an entry in the exception table, that is made up of: the faulting address(from), where to jump to in case of a fault, and a handler function (that implements the jump logic). All of these addresses are stored on 32bit in relative format to the exception table, so that they work for both 32 and 64 bit kernels.

All of the exception table entries are then collected in the __ex_table section by the linker script:

The section is guarded with __start___ex_table and __stop___ex_table symbols, so that it is easy to find the data from C code. This table is accessed by the fault handler:

All it does is to set the return address to the one in the to field of the exception table entry which, in case of the get_user exception table entry, is bad_get_user which return -EFAULT to the caller.

© Copyright The kernel development community.

Источник

Разбираемся с системными вызовами в Linux с помощью strace

Перевод статьи подготовлен специально для студентов базового и продвинутого курсов Administrator Linux.

Системный вызов — это механизм взаимодействия пользовательских программ с ядром Linux, а strace — мощный инструмент, для их отслеживания. Для лучшего понимания работы операционной системы полезно разобраться с тем, как они работают.

В операционной системе можно выделить два режима работы:

  • Режим ядра (kernel mode) — привилегированный режим, используемый ядром операционной системы.
  • Пользовательский режим (user mode) — режим, в котором выполняется большинство пользовательских приложений.

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

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

Большая часть этих деталей скрыта от пользователя в системных библиотеках (glibc в Linux-системах). Системные вызовы по своей природе являются универсальными, но несмотря на это, механика их выполнения во многом аппаратно-зависима.

В этой статье рассматривается несколько практических примеров анализа системных вызовов с помощью strace . В примерах используется Red Hat Enterprise Linux, но все команды должны работать и в других дистрибутивах Linux:

Для начала убедитесь, что в вашей системе установлены необходимые инструменты. Проверить установлен ли strace можно с помощью приведенной ниже команды. Для просмотра версии strace запустите ее с параметром -V:

Если strace не установлен, то установите запустив:

Для примера создайте тестовый каталог в /tmp и два файла с помощью команды touch :

(Я использую каталог /tmp только потому, что доступ к нему есть у всех, но вы можете использовать любой другой.)

С помощью команды ls проверьте, что в каталоге testdir создались файлы:

Вероятно, вы используете команду ls каждый день, не осознавая того, что под капотом работают системные вызовы. Здесь в игру вступает абстракция. Вот как работает эта команда:

Команда ls вызывает функции из системных библиотек Linux (glibc). Эти библиотеки, в свою очередь, вызывают системные вызовы, которые выполняют большую часть работы.

Если вы хотите узнать, какие функции вызывались из библиотеки glibc, то используйте команду ltrace со следующей за ней командой ls testdir/ :

Если ltrace не установлен, то установите:

На экране будет много информации, но не беспокойтесь — мы это рассмотрим далее. Вот некоторые из важных библиотечных функций из вывода ltrace :

Изучив этот вывод, вы, вероятно, поймете, что происходит. Каталог с именем testdir открывается с помощью библиотечной функции opendir , после чего следуют вызовы функций readdir , читающих содержимое каталога. В конце происходит вызов функции closedir , которая закрывает каталог, открытый ранее. Пока проигнорируйте остальные функции, такие как strlen и memcpy .

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

Для просмотра системных вызовов используйте strace с командой ls testdir , как показано ниже. И вы снова получите кучу бессвязной информации:

В результате выполнения strace вы получите список системных вызовов, выполненных при работе команды ls . Все системные вызовы можно разделить на следующие категории:

  • Управление процессами
  • Управление файлами
  • Управление каталогами и файловой системой
  • Прочие

Есть удобный способ анализа полученной информации — записать вывод в файл с помощью опции -o .

На этот раз на экране не будет никаких данных — команда ls отработает, как и ожидается, показав список файлов и записав весь вывод strace в файл trace.log . Для простой команды ls файл содержит почти 100 строк:

Взгляните на первую строку в файле trace.log :

  • В начале строки находится имя выполняемого системного вызова — это execve.
  • Текст в круглых скобках — это аргументы, передаваемые системному вызову.
  • Число после знака = (в данном случае 0) — это значение, возвращаемое системным вызовом.

Теперь результат не кажется слишком пугающим, не так ли? И вы можете применить ту же логику и для других строк.

Обратите внимание на ту единственную команду, которую вы вызвали — ls testdir . Вам известно имя каталога, используемое командой ls , так почему бы не воспользоваться grep для testdir в файле trace.log и не посмотреть, что найдется? Посмотрите внимательно на результат:

Возвращаясь к приведенному выше анализу execve , можете ли вы сказать, что делает следующий системный вызов?

Не нужно запоминать все системные вызовы и то, что они делают: все есть в документации. Man-страницы спешат на помощь! Перед запуском команды man убедитесь, что установлен пакет man-pages :

Помните, что вам нужно добавить «2» между командой man и именем системного вызова. Если вы прочитаете в man про man ( man man ), то увидите, что раздел 2 зарезервирован для системных вызовов. Аналогично если вам нужна информация о библиотечных функциях, то нужно добавить 3 между man и именем библиотечной функции.

Ниже приведены номера разделов man :

Для просмотра документации по системному вызову запустите man с именем этого системного вызова.

В соответствии с документацией системный вызов execve выполняет программу, которая передается ему в параметрах (в данном случае это ls ). В него также передаются дополнительные параметры для ls. В этом примере это testdir . Следовательно, этот системный вызов просто запускает ls с testdir в качестве параметра:

В следующий системный вызов stat передается параметр testdir :

Для просмотра документации используйте man 2 stat . Системный вызов stat возвращает информацию об указанном файле. Помните, что все в Linux — файл, включая каталоги.

Далее системный вызов openat открывает testdir . Обратите внимание, что возвращается значение 3. Это дескриптор файла, который будет использоваться в последующих системных вызовах:

Теперь откройте файл и обратите внимание на строку, следующую после системного вызова openat . Вы увидите системный вызов getdents , который делает большую часть необходимой работы для выполнения команды ls testdir . Теперь выполним grep getdents для файла trace.log :

В документации ( man getdents ) говорится, что getdents читает записи каталога, это, собственно, нам и нужно. Обратите внимание, что аргумент для getdent равен 3 — это дескриптор файла, полученный ранее от системного вызова openat .

Теперь, когда получено содержимое каталога, нужен способ отобразить информацию в терминале. Итак, делаем grep для другого системного вызова write , который используется для вывода на терминал:

В аргументах вы можете видеть имена файлов, которые будут выводится: file1 и file2 . Что касается первого аргумента (1), вспомните, что в Linux для любого процесса по умолчанию открываются три файловых дескриптора:

  • 0 — стандартный поток ввода
  • 1 — стандартный поток вывода
  • 2 — стандартный поток ошибок

Таким образом, системный вызов write выводит file1 и file2 на стандартный вывод, которым является терминал, обозначаемый числом 1.

Теперь вы знаете, какие системные вызовы сделали большую часть работы для команды ls testdir/ . Но что насчет других 100+ системных вызовов в файле trace.log ?

Операционная система выполняет много вспомогательных действий для запуска процесса, поэтому многое из того, что вы видите в файле trace.log — это инициализация и очистка процесса. Посмотрите файл trace.log полностью и попытайтесь понять, что происходит во время запуска команды ls .

Теперь вы можете анализировать системные вызовы для любых программ. Утилита strace так же предоставляет множество полезных параметров командной строки, некоторые из которых описаны ниже.

По умолчанию strace отображает не всю информацию о системных вызовах. Однако у нее есть опция -v verbose , которая покажет дополнительную информацию о каждом системном вызове:

Хорошая практика использовать параметр -f для отслеживания дочерних процессов, созданных запущенным процессом:

А если вам нужны только имена системных вызовов, количество их запусков и процент времени, затраченного на выполнение? Вы можете использовать опцию -c , чтобы получить эту статистику:

Если вы хотите отследить определенный системный вызов, например, open , и проигнорировать другие, то можно использовать опцию -e с именем системного вызова:

А что, если нужно отфильтровать по нескольким системным вызовам? Не волнуйтесь, можно использовать ту же опцию -e и разделить необходимые системные вызовы запятой. Например, для write и getdent :

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

Мы не будем запускать демона, а используем команду cat , которая отображает содержимое файла, переданного ему в качестве аргумента. Но если аргумент не указать, то команда cat будет просто ждать ввод от пользователя. После ввода текста она выведет введенный текст на экран. И так до тех пор, пока пользователь не нажмет Ctrl+C для выхода.

Запустите команду cat на одном терминале.

На другом терминале найдите идентификатор процесса (PID) с помощью команды ps :

Теперь запустите strace с опцией -p и PID’ом, который вы нашли с помощью ps . После запуска strace выведет информацию о процессе, к которому он подключился, а также его PID. Теперь strace отслеживает системные вызовы, выполняемые командой cat . Первый системный вызов, который вы увидите — это read, ожидающий ввода от потока с номером 0, то есть от стандартного ввода, который сейчас является терминалом, на котором запущена команда cat :

Теперь вернитесь к терминалу, где вы оставили запущенную команду cat , и введите какой-нибудь текст. Для демонстрации я ввел x0x0 . Обратите внимание, что cat просто повторил то, что я ввел и x0x0 на экране будет дважды.

Вернитесь к терминалу, где strace был подключен к процессу cat . Теперь вы видите два новых системных вызова: предыдущий read , который теперь прочитал x0x0 , и еще один для записи write , который записывает x0x0 обратно в терминал, и снова новый read , который ожидает чтения с терминала. Обратите внимание, что стандартный ввод (0) и стандартный вывод (1) находятся на одном и том же терминале:

Представляете, какую пользу может принести вам запуск strace для демонов: вы можете увидеть все, что делается в фоне. Завершите команду , нажав . Это также прекратит сеанс , так как отслеживаемый процесс был прекращен.

Для просмотра отметок времени системных вызовов используйте опцию -t :

А если вы хотите узнать время, проведенное между системными вызовами? Есть удобная опция -r , которая показывает время, затраченное на выполнение каждого системного вызова. Довольно полезно, не так ли?

Заключение

Утилита strace очень удобна для изучения системных вызовов в Linux. Чтобы узнать о других параметрах командной строки, обратитесь к man и онлайн-документации.

Источник

Читайте также:  Поднять nat windows server 2012 r2
Оцените статью