Вы находитесь на странице: 1из 8
Команды ядра, использующие системные вызовы Linux Обзор SCI и
Команды ядра, использующие системные вызовы Linux Обзор SCI и

Команды ядра, использующие системные вызовы 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. Упрощенная схема системного

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

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

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

Рис.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, который

определен как

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

user.

Макрос

user

просто сообщает компилятору (через noderef),

На втором шаге я обновляю заголовочные файлы, чтобы освободить место для новых функций в таблице системных вызовов. Для этого я обновляю заголовочный файл 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 .long sys_perf_event_open .long sys_getjiffies .long sys_diffjiffies .long sys_pdiffjiffies

/* 335 */

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

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

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

arch/x86/kernel/Makefile:

obj-y += my_call.o

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

static inline long copy_to_user(void

user

* from, unsigned long n)

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 архитектурно-зависима и использует механизм передачи управления

ядру. Аргумент основывается на отображении индексов

представленные в /usr/include/bits/syscall.h (определенный, когда libc собран).

Никогда не используйте этот файл на прямую; вместо него используйте /usr/include/sys/syscall.h.

NR

в символы SYS_,

Традиционный метод требует, чтобы Вы создавали функции, вызовы которых соответствуют определениям в ядре с точки зрения номера системного вызова (так, чтобы Вы вызывали правильные службы ядра). 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 )

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

Обратите внимание, что в Листинге 6 я указал

символические константы

NR

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;

 

}

Заметьте, что индексы

_syscall использует имя функции, чтобы создать индекс

NR

необходимы в этом приложении, потому

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() #define diffjiffies(ujiffies) #define pdiffjiffies(ujiffies, presult)

syscall( NR_getjiffies)

 

syscall(

NR_diffjiffies,

ujiffies)

syscall( ujiffies, presult)

NR_pdiffjiffies,

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

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

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

strace ./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

>

SYS_337(0xb78b0ff4, 0x8048460, 0xbf825588, 0xb77a345, 0xb78d3d20) = 468207

syscall(337, 0xb78b0ff4, 0x8048460, 0xbf825588, 0xb77a345 <unfinished

syscall <

resumed> )

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

Источники

Статья Тима Джонса — Kernel command using Linux system calls;