Вы находитесь на странице: 1из 8

Команды ядра, использующие системные вызовы Linux

Обзор SCI и добавление ваших собственных вызовов

Системные вызовы Linux® — мы используем их каждый день. Но знаете ли Вы как


системный вызов осуществляет переход из пространства пользователя в пространство
ядра? Исследуем system call interface (SCI), изучим как добавить новые системные
вызовы и узнаем утилиты связанные с SCI.

Системные вызовы (system calls) являются интерфейсом между приложениями


пространства пользователя и сервисами, которые предоставляет ядро. Вызов не может
быть осуществлен напрямую, так как сервисы предоставляются ядром (реализованы в
ядре); вместо этого Вам необходимо использовать процесс пересечения границы user-
space/kernel. Способы сделать это отличаются в зависимости от конкретной
архитектуры. По этой причине я буду придерживаться наиболее распространенной
архитектуры i386.
В этой статье я рассмотрю Linux SCI, продемонстрирую добавление системного вызова
в ядре 2.6.32 и более поздних (до ядра 2.6.35) и затем вызов этой функции из
пространства пользователя. Я также рассмотрю некоторые из функций, которые Вы
можете счесть полезными для реализации системного вызова. Наконец я опишу
некоторые вспомогательные механизмы связанные с системными вызовами, такие как
трассировка.

SCI
Реализация системных вызовов в Linux различается в зависимости от архитектуры, но
она также может отличатся в рамках одной архитектуры. Например, старые процессоры
x86 использовали механизм прерываний для перехода из пространства пользователя в
пространство ядра, но новые процессоры IA-32 обеспечивают инструкции, которые
оптимизируют этот переход (используя инструкции sysenter и sysexit). Поскольку
существует очень много вариантов и конечный результат является довольно сложным,
то я буду придерживаться поверхностного обсуждения.

Вы не должны полностью понимать строение SCI, чтобы изменять его, поэтому я


рассмотрю простой вариант работы системного вызова (см. Рис.1). Каждый системный
вызов мультиплексируется (multiplexed) в ядро через единую точку входа. Регистр
eax используется для определения конкретного системного вызова, который должен
быть вызван и который определен в библиотеке языка C (для вызова из
пользовательского приложения). Когда библиотека C загрузила индекс (номер)
системного вызова и необходимые аргументы, вызывается программное прерывание
(прерывание 0x80), которое приводит к выполнению (через обработчик прерываний)
функции system_call. Эта функция обрабатывает все системные вызовы, которые
определяются содержимым регистра eax. После нескольких простых тестов, фактически,
системный вызов вызывается, используя system_call_table и номер, содержащийся в
eax. После возвращения из системного вызова, в конечном счете достигается
syscall_exit и вызывается resume_userspace для перехода назад в пространство
пользователя. Выполнение возобновляется в библиотеке C, которая затем возвращается
к пользовательскому приложению.

Рис.1. Упрощенная схема системного вызова, использующего прерывание

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


показана на Рис.2, используя номер в eax для определения, какой системный вызов
должен быть вызван из таблицы (system_call_table). Также показан простой пример
содержимого этой таблицы и расположение системных вызовов.

Рис.2. Таблица системных вызовов и различные связи


Добавление системного вызова Linux
Демультиплексирование системного вызова
Некоторые системные вызовы в дальнейшем демультиплексируются ядром.
Например, вызовы сокетов (socket, bind, connect и т.д.) Berkeley
Software Distribution (BSD) связаны с одним номером системного вызова
(__NR_socketcall), но демультиплексируются в ядре к соответствующему
вызову через дополнительный аргумент. См. функцию sys_socketcall в
/linux/net/socket.c.

Добавление системного вызова является в основном процедурным, хотя Вы должны


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

Для добавления нового системного вызова в ядро, необходимо проделать три основных
шага:

1. Добавить новую функцию.


2. Обновить заголовочные файлы.
3. Обновить таблицу системных вызовов для новой функции.

Создадим новый файл для функций arch/x86/kernel/my_call.c. Первые две функции


показанные в Листинге 1 являются простыми примерами системных вызовов. Третья
функция более сложная, и использует указатель в аргументах.

Листинг 1. Примеры функций системных вызовов

#include <linux/linkage.h>
#include <linux/kernel.h>
#include <linux/jiffies.h>
#include <asm/uaccess.h>
asmlinkage long sys_getjiffies( void )
{
return (long)get_jiffies_64();
}
asmlinkage long sys_diffjiffies( long ujiffies )
{
return (long)get_jiffies_64() - ujiffies;
}
asmlinkage long sys_pdiffjiffies( long ujiffies,
long __user *presult )
{
long cur_jiffies = (long)get_jiffies_64();
long result;
int err = 0;

if (presult) {
result = curr_jiffies — ujiffies;
err = put_user( result, presult );
}

return err ? -EFAULT : 0;


}

В Листинге 1 функции sys_getjiffies и sys_diffjiffies предназначены для


мониторинга jiffies. Первая функция возвращает текущий jiffies, а вторая
возвращает разницу между текущим и значением, которое передается. Обратите
внимание на использование модификатора asmlinkage. Это макрос (определен в
arch/x86/include/asm/linkage.h) говорит компилятору положить все аргументы функции
в стек.
Kernel jiffies
Ядро Linux поддерживает глобальную переменную jiffies, которая
представляет собой число тиков таймера начиная со старта машины. Эта
переменная инициализируется нулем и наращивается с каждым прерыванием
таймера. Вы можете получить значение jiffies с помощью функции
get_jiffies_64, а затем преобразовать это значение в миллисекунды,
используя jiffies_to_msecs или микросекунды с помощью jiffies_to_usecs.
Время работы системы равно jiffies/HZ секунд, где HZ — количество
прерываний системного таймера в секунду (определен в
include/asm-generic/param.h). Переменная jiffies и связанные с ней
функции определены в include/linux/jiffies.h.

Третья функция принимает два аргумента: long и указатель на long, который


определен как __user. Макрос __user просто сообщает компилятору (через noderef),
что указатель не должен быть разыменован (так как это не имеет значения в текущем
адресном пространстве). Эта функция высчитывает разницу между двумя значениями
jiffies, а затем возвращает результат пользователю через указатель пространства
пользователя. Функция put_user размещает значение результата в том месте
пользовательского пространства, на которое указывает presult. Если произойдет
ошибка, то будет возращено значение -EFAULT (плохой адрес; ошибки определены в
файле include/asm-generic/errno-base.h), т.о., также уведомив пользователя о
результате.

На втором шаге я обновляю заголовочные файлы, чтобы освободить место для новых
функций в таблице системных вызовов. Для этого я обновляю заголовочный файл
arch/x86/include/asm/unistd_32.h с номером нового системного вызова. Изменения
выделены жирным шрифтом и показаны в Листинге 2.

Листинг 2. Изменения в unistd_32.h

#define __NR_rt_tgsigqueueinfo 335


#define __NR_perf_event_open 336
#define __NR_getjiffies 337
#define __NR_diffjiffies 338
#define __NR_pdiffjiffies 339

Теперь я имею в ядре системные вызовы и номера для их представления. Все что
осталось сделать, это провести зависимости между номерами (таблицей индексов) и
самими функциями. Третий шаг — обновление таблицы системных вызовов. Как показано
в Листинге 3, я обновил файл arch/x86/kernel/syscall_table_32.S для новых функций,
к которым можно будет обратиться по индексам, показанным в Листинге 2.

Листинг 3. Обновление таблицы системных вызовов

.long sys_rt_tgsigqueueinfo /* 335 */


.long sys_perf_event_open
.long sys_getjiffies
.long sys_diffjiffies
.long sys_pdiffjiffies

Замечание: Размер этой таблицы определен символической константой NR_syscalls

К этому моменту сделаны все изменения. Теперь осталось перекомпилировать ядро и


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

Замечание: Не забудьте добавить запись о файле с нашими системными вызовами в


arch/x86/kernel/Makefile:

obj-y += my_call.o
Чтение и запись пользовательской памяти

Ядро Linux предоставляет несколько функций, которые Вы можете использовать для


перемещения аргументов системного вызова в/из пространства пользователя. Основные
методы включают простые функции для базовых типов (такие как get_user и put_user).
Для перемещения блоков данных, таких как структуры или массивы, вы можете
использовать другой набор функций: copy_from_user и copy_to_user. Для перемещения
NULL-terminated строк (т.е. с нулевым символом) есть свои собственные вызовы:
strncpy_from_user и strlen_from_user. Вы также можете проверить является ли
указатель пространства пользователя действительным через вызов access_ok. Эти
функции определены в include/asm-generic/uaccess.h.

Вы можете использовать макрос access_ok, чтобы проверить действительность


указателя пространства пользователя для данной операции. Эта функция принимает тип
доступа (VERIFY_READ или VERIFY_WRITE) , указатель на блок памяти пространства
пользователя и размер блока (в байтах). Функция возвращает нуль в случае успеха:

#define access_ok(type, addr, size) __access_ok((unsigned long)(addr),(size))

Перемещение простых типов между ядром и пространством пользователя (таких как


целые или длинные) легко осуществляется при помощи put_user и get_user. Каждый из
этих макросов принимает значение и указатель на переменную. Функция get_user
перемещает значение пользовательского пространства, на которое указывает адрес
(ptr) в указанную переменную ядра (var). Функция put_user перемещает значение, на
которое указывает переменная ядра (var) в пространство пользователя по указанному
адресу (ptr). Функции возвращают нуль в случае успеха:

#define get_user(x, ptr)


#define put_user(x, ptr)

Для перемещения больших объектов, таких как структуры или массивы, Вы можете
использовать функции copy_from_user и copy_to_user. Эти функции перемещают весь
блок данных между пространством пользователя и пространством ядра. Функция
copy_from_user перемещает блок данных из пространства пользователя в пространство
ядра, а copy_to_user перемещает блок данных из пространства ядра в пространство
пользователя:

static inline long copy_from_user(void *to,


const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to,
const void *from, unsigned long n)

Наконец вы можете копировать NULL-terminated строки из пространства пользователя в


пространство ядра, используя функцию strncpy_from_user. Перед вызовом этой функции
Вы должны получить размер строки пользовательского пространства с помощью функции
strlen_user:

static inline long


strncpy_from_user(char *dst, const char __user *src, long count)
static inline long strlen_user(const char __user *src)

Эти функции являются основными для перемещения значений между пространством


пользователя и пространством ядра. Существуют дополнительные функции (такие как
те, которые уменьшают количество выполнения проверок). Вы можете найти эти функции
в uaccess.h.
Использование системных вызовов
Использование ядер 2.6.18 и выше
Макрос _syscallN был удален в ядре 2.6.18, так что
вместо использования макросов должны быть использована
сама функция syscall. Эта функция поддерживает произвольное
количество аргументов (int syscall(int number, ...)).

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

В первом методе, Вы вызываете Ваши новые системные вызовы через функцию syscall. В
функции syscall Вы можете вызвать системный вызов указав его номер и набор
аргументов. Например, маленькая программа, показанная в Листинге 4 вызывает Вашу
функцию sys_getjiffies, используя ее номер.

Листинг 4. Использование syscall для исполнения системного вызова

#include <linux/unistd.h>
#include <sys/syscall.h>
#define __NR_getjiffies 320
int main()
{
long jiffies;
jiffies = syscall( __NR_getjiffies );
printf( "Current jiffies is %lx\n", jiffies );
return 0;
}

Как Вы можете заметить, функция syscall включает в себя в качестве первого


аргумента номер из таблицы системных вызовов. Если есть еще аргументы, то они
должны быть указаны после номера системного вызова. Большинство системных вызовов
включает символическую константу SYS_, указывая свое отображение на номера __NR_.
Например, Вы можете ссылаться на номер __NR_getpid в syscall как:

syscall( SYS_getpid )

Функция syscall архитектурно-зависима и использует механизм передачи управления


ядру. Аргумент основывается на отображении индексов __NR в символы SYS_,
представленные в /usr/include/bits/syscall.h (определенный, когда libc собран).
Никогда не используйте этот файл на прямую; вместо него используйте
/usr/include/sys/syscall.h.

Традиционный метод требует, чтобы Вы создавали функции, вызовы которых


соответствуют определениям в ядре с точки зрения номера системного вызова (так,
чтобы Вы вызывали правильные службы ядра). Linux предоставляет набор макросов для
реализации этой возможности. Макрос _syscallN определен в
/usr/include/linux/unistd.h и имеет следующий формат:

_syscall0 ( ret-type, func-name )


_syscall1 ( ret-type, func-name, arg1-type, arg1-name )
_syscall2 ( ret-type, func-name, arg1-type, arg1-name,
arg2-type, arg2-name )

Пространство пользователя и константы __NR


Обратите внимание, что в Листинге 6 я указал
символические константы __NR. Вы можете найти их
в /usr/include/asm/unistd_32.h (для стандартных системных
вызовов).

Макрос _syscall принимает до шести аргументов (хотя здесь показаны только три).

Здесь показано, как используется макрос _syscall, чтобы сделать Ваши новые
системные вызовы видимыми пространству пользователя. Листинг 6 демонстрирует
приложение, которое использует все Ваши системные вызовы, определив их с помощью
макроса _syscall.

Листинг 5. Использование макроса _syscall для разработки приложений пространства


пользователя

#include <stdio.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
#define __NR_getjiffies 320
#define __NR_diffjiffies 321
#define __NR_pdiffjiffies 322
_syscall0( long, getjiffies );
_syscall1( long, diffjiffies, long, ujiffies );
_syscall2( long, pdiffjiffies, long, ujiffies, long*,
presult );
int main()
{
long jifs, result;
int err;
jifs = getjiffies();
printf( "difference is %lx\n", diffjiffies(jifs) );
err = pdiffjiffies( jifs, &result );
if (!err) {
printf( "difference is %lx\n", result );
} else {
printf( "error\n" );
}
return 0;
}

Заметьте, что индексы __NR необходимы в этом приложении, потому что макрос
_syscall использует имя функции, чтобы создать индекс __NR (getjiffies ->
__NR_getjiffies). Результат заключается в том, что теперь Вы можете вызывать Ваши
функции ядра, используя их имена, также как и любой другой системный вызов.

Замечание: Чтобы программа из Листинга 5 работала на ядрах 2.6.18 и выше, следует


заменить:

_syscall0( long, getjiffies );


_syscall1( long, diffjiffies, long, ujiffies );
_syscall2( long, pdiffjiffies, long, ujiffies, long*, presult );

на

#define getjiffies() syscall(__NR_getjiffies)


#define diffjiffies(ujiffies) syscall(__NR_diffjiffies, ujiffies)
#define pdiffjiffies(ujiffies, presult) syscall(__NR_pdiffjiffies, ujiffies, presult)

Трассировка системных вызовов при помощи strace


Ядро Linux предоставляет удобный способ трассировки системных вызовов, которые
вызывает процесс (а также те сигналы, которые получит процесс). Утилита называется
strace и вызывается из командной строки, используя в качестве аргумента
приложение, которое вы хотите трассировать. Например, если вы хотите знать какие
системные вызовы были вызваны в течении работы программы из Листинга 4 (назвав
программу call_test1), выполните следующую команду:

strace ./call_test1

Результатом будет довольно большой дамп, показывающий различные системные вызовы,


которые были выполнены в контексте вызова команды call_test1. Вы можете увидеть
загрузку разделяемых библиотек, отображение памяти и — в конце трассировки — вывод
информации о текущем значении jiffies на стандартный вывод.

...
SYS_337(0xb78b0ff4, 0x8048460, 0xbf825588, 0xb77a345, 0xb78d3d20) = 468207
...
write(1, "Current jiffies is 724ef\n", 25Current jiffies is 724ef
) = 25
exit_group(0) = ?
$

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


установленный набор специальных полей (флагов), то вызывается syscall_trace,
который приводит к вызову функции do_syscall_trace. Вы также можете найти
трассировку функции, как часть системного вызова в arch/x86/kernel/entry_32.S (см.
syscall_trace_entry).

Если воспользоваться командой ltrace, то можно получить чуть более интересный


результат:

$ ltrace -S ./call_test1
...
syscall(337, 0xb78b0ff4, 0x8048460, 0xbf825588, 0xb77a345 <unfinished...>
SYS_337(0xb78b0ff4, 0x8048460, 0xbf825588, 0xb77a345, 0xb78d3d20) = 468207
<...syscall resumed> )
...

Здесь можно видеть вызов функции syscall и передачу ей в качестве аргумента номер
системного вызова (337) и дальнейший вызов нашей функции sys_getjiffies (SYS_337).

Источники
• Статья Тима Джонса — Kernel command using Linux system calls;
• rflinux.ru — Системные вызовы в Linux;
• Manugarg — Sysenter Based System Call Mechanism in Linux 2.6.

Вам также может понравиться