Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Перехват системных вызовов

 

Одно из основных действий, выполняемых драйвером – перехват системных вызовов.

Это довольно небезопасный прием, так как если 2 драйвера перехватят один и тот же вызов, и будут выгружены не в том порядке, в каком загрузились, последний восстановит неверный адрес в таблице вызовов, вследствие чего произойдет сбой при следующей попытке сделать данный вызов. В связи с этим, начиная с версии 2.5, ядро более не экспортирует таблицу системных вызовов. Тем не менее, эта проблема устраняется небольшим исправлением ядра (patch), который добавляет в произвольный файл ядра следующие строки, экспортирующие из ядра данную таблицу.

extern void *sys_call_table[];

EXPORT_SYMBOL_GPL (sys_call_table);

Для перехвата системных вызовов имеются 3 таблицы указателей – оригинальных (системных) обработчиков, наших пре-обработчиков (вызываемых ДО оригинального, и принимающих аргументы) и пост-обработчиков (вызываемых ПОСЛЕ и принимающих возвращаемое значение):

void *old_sys_call [NR_syscalls];

void *sys_call_trap [NR_syscalls];

void *sys_call_exit [NR_syscalls];

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

struct syscall_handler

{

/* Syscall nr */

int nr;

/* Pre-call & post-call handler */

void *hand1, *hand2;

};

Функция capture_syscalls(), вызываемая при инициализации драйвера, копирует эти адреса в предыдущие 2 таблицы и записывает в sys_call_table по нужным номерам адрес универсального перехватчика – syscalls_entry, находящегося в файле syscalls-entry.

Необходимость в ассемблерном коде обусловлена механизмом обработки системных вызовов в Linux. На рис. 3 показан стек на момент вызова обработчика системного вызова (замененного или стандартного). Проблема заключается в том, что некоторые стандартные обработчики требуют, чтобы стек имел именно такой вид, и если вызывать их из нового обработчика, они правильно работать не будут. В связи с этим syscalls_entry сначала вызывает пре-обработчик системного вызова, затем заменяет в стеке адрес возврата на адрес следующей инструкции и делает переход на стандартный обработчик, который получает кадр стека в изначальном виде. Затем, при возврате, мы попадаем на следующую инструкцию, которая вызывает пост-обработчик и делает перход на изначальный адрес возврата, на код в arch/i386/kernel/entry.S (точки входа всех системных вызовов в Linux). Этот адрес сохраняется внизу стека ядра, там же где хранится указатель на текущую задачу и некоторая другая служебная информация ядра. Данные действия продемонстрированы на рис. 3.3–3.5.

Данный драйвер перехватывает следующие системные вызовы:

m[un] map() – выделение / освобождение региона памяти

mremap() – перемещение региона памяти

brk() – расширение / сужение сегмента данных программы

m[un] lock[all]() – блокировка набора страниц в рабочем множестве процесса

fsync() – используется в качестве маркера в журнале событий

Для каждого из вызовов в журнал печатается имя вызова, PID вызвавшего процесса и список (с расшифровкой, там, где это имеет смысл) аргументов.

 

Кольцевой буфер событий

 

Для хранения журнала событий, таких как выделение блоков виртуальной памяти и страниц, использует кольцевой буфер, защищенный спин-блокировкой. Размер данного буфера может задаваться при загрузке драйвера в качестве его параметра, значение по умолчанию – 32 кб, минимальное – 8 кб. Память для буфера выделяется функцией kzalloc() – аналогом фукнций malloc/calloc() из стандартной библиотеки С. Передаваемый ей параметр GFP_KERNEL означает, что память выделяется для ядра (т.е. не может быть позже выгружена с диска), но не в атомарном контексте (т.е. текущий процесс может быть отложен до освобождения необходимой памяти).

Каждая запись в буфере представляет из себя следующую структуру:

enum memmon_event_type – тип события

{

NOTUSED = 0

MMAP2,

MUNMAP,

MREMAP,

MLOCK,

MUNLOCK,

MLOCKALL,

MUNLOCKALL,

BRK,

FSYNC,

ANON_PF, – страничная ошибка на анонимной странице

SWAP_PF, – на странице из файла подкачки

FILE_PF, – из разделяемого файла

SYSCALLRET – возврат из системного вызова

};

struct memmon_event

{

enum memmon_event_type type; – тип события

pid_t pid; – PID вызвавшего процесса

union – специфичны для события данные

{

struct

{

void __user *start;

size_t len;

} munmap;

 

struct

{

void __user *start;

size_t len;

unsigned long prot, flags;

unsigned long fd, off;

} mmap2;

……

};

}

С буфером событий связаны следующие переменные:

static struct memmon_event *events; – собственно буфер

static int ev_start; – индекс самой старой записи в буфере

static int ev_end; – индекс последней записи

static int ev_ovf = 0; – было ли уже переполнение буфера

DECLARE_WAIT_QUEUE_HEAD (ev_waitq); – очередь ожидания (для блокирующего чтения)

spinlock_t ev_lock = SPIN_LOCK_UNLOCKED; – спин-блокировка для защиты от гонок при обращении к буферу

Пользовательские приложения запрашивают содержимое буфера событий, читая файл /proc/memmon/events. Если при открытии файла не был установлен флаг O_NONBLOCK, операция чтения по нему блокирующая – т.е., если новых данных в буфере нет, read() переводит процесс в состояние ожидания (interruptible sleep) вызовом функции wait_event_interruptible() до получения сигнала либо появления новых данных в буфере.

Помимо open() и release(), вызываемых при открытии (создании новой структуры file) и ее уничтожении, в file_operations данного файла определены всего 2 точки входа – read() и poll(). Обработчик poll() вызываемается, когда какой-то процесс делает вызов select() по данному файлу – ожидает, пока на нем будут доступные для чтения данные. Кроме того, в флагах структуры file вызовом nonseekable_open() сбрасывается бит, позволяющий делать вызов llseek() по файлу (т. к. данная операция лишена смысла для кольцевого буфера).

Для реализации функции read() используется абстракция под названием seq_file, предназначенная для буферизации считываемых данных. Она требует задания 4 функций – seq_start(), вызываемой при начале чтения из файла, seq_next(), вызываемой перед копированием в буфер пользователя записи об очередном событии, seq_show(), собственно осуществляющющей это копирование, и seq_stop(), вызываемой при завершении передачи данных в пользовательский буфер (когда скопировано затребованное количество данных или не осталось событий в буфере).

Рис. показывает связь между этими функциями и структурами.

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

 

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...