Глава 7. Системные вызовы До сих пор, все что мы делали, это использовали ранее определенные механизмы ядра для регистрации файлов в файловой системе /proc и файлов устройств. Это все замечательно, но это годится только для создания драйверов устройств. А что если вы хотите сделать действительно что-то необычное, например изменить реакцию системы на какое либо событие? Мы как раз вступаем в ту область, где программирование действительно становится опасным. При разработке примера, приведенного ниже, я "уничтожил" системный вызов open. В результате система потеряла возможность открывать любые файлы, что равносильно отказу системы выполнять любые программы. Я не мог даже остановить систему командой shutdown. Из-за этого пришлось прибегнуть к "помощи" кнопки выключения питания. К счастью, ни один файл не был уничтожен. Чтобы обезопасить себя от потери данных, в аналогичных ситуациях, перед загрузкой подобных модулей всегда выполняйте резервное копирование. Забудьте про /proc, забудьте и про файлы устройств. Реальный механизм взаимодействия процессов с ядром -- это системные вызовы. Когда процесс запрашивает какую-либо услугу ядра (например, открытие файла, запуск нового процесса или выделение дополнительной памяти), используется механизм системных вызовов. Если вы хотите изменить поведение ядра, то системные вызовы -- это как раз то место, куда можно приложить свои знания и умения. Между прочим, если вы захотите увидеть -- какие системные вызовы используются той или иной программой, запустите: strace <command> <arguments>. Строго говоря, процесс не имеет доступа в пространство ядра. Он не может обращаться к памяти ядра и не может вызывать функции в ядре. Микропроцессор ограничивает такого рода доступ на аппаратном уровне (вот почему режим исполнения ядра называется защищенным, или привилегированным). Системные вызовы являются исключением из этого правила. Чтобы исполнить системный вызов, процесс заполняет регистры микропроцессора соответствующими значениями и выполняет специальную инструкцию, которая производит переход в предопределенное место в пространстве ядра (разумеется, точка перехода доступна пользовательским процессам на чтение). Для платформы Intel -- это инструкция прерывания с вектором 0x80. Микропроцессор воспринимает это как переход из ограниченного пользовательского режима в защищенный режим ядра, где позволено делать все, что вам заблагорассудится. Точка перехода, в ядре, называется system_call. Процедура, которая там находится, проверяет номер системного вызова, который сообщает ядру -- какую именно услугу запрашивает процесс. Затем, она просматривает таблицу системных вызовов (sys_call_table), отыскивает адрес функции ядра, которую следует вызвать, после чего вызывается нужная функция. По окончании работы системного вызова, выполняется ряд дополнительных проверок и лишь после этого управление возвращается вызывающему процессу (или другому процессу, если вызывающий процесс исчерпал свой квант времени). Код, выполняющий все вышеперечисленные действия, вы найдете в файле arch/<architecture>/kernel/entry.S, после строки ENTRY(system_call). Итак, если вас одолевает желание изменить поведение некоторого системного вызова, то первое, что необходимо сделать -- это написать вашу собственную функцию, которая выполняла бы требуемые действия (обычно, после выполнения своих действий, в подобных случаях, вызывается первоначальная функция, реализующая системный вызов), затем -- изменить указатель в sys_call_table так, чтобы он указывал на вашу функцию. Поскольку ваш модуль впоследствии может быть выгружен, то следует предусмотреть восстановление системы в ее первоначальное состояние, чтобы не оставлять ее в нестабильном состоянии. Это делается в пределах функции cleanup_module. Ниже приводится исходный текст такого модуля. Он "шпионит" за выбранным пользователем, и посылать через printk сообщение всякий раз, когда данный пользователь открывает какой-либо файл. Для этого, системный вызов open(), подменяется функцией с именем our_sys_open. Она проверяет UID (User ID) текущего процесса, и если он равен заданному, то вызывает printk, чтобы сообщить имя открываемого файла, и в заключение вызывает оригинальную функцию open() с теми же параметрами, которая открывает требуемый файл. Функция init_module изменяет соответствующий указатель в sys_call_table и сохраняет его первоначальное значение в переменной. Функция cleanup_module восстанавливает указатель в sys_call_table, используя эту переменную. В данном подходе кроются свои "подводные камни" из-за возможности существования двух модулей, перекрывающих один и тот же системный вызов. Представьте себе: имеется два модуля, А и B. Пусть модуль A перекрывает системный вызов open, своей функцией A_open, а модуль B -- функцией B_open. Первым загружается модуль A, он заменяет системный вызов open на A_open. Затем загружается модуль B, который заменит системный вызов A_open на B_open. Модуль B полагает, что он подменил оригинальный системный вызов, хотя на самом деле был подменен вызов A_open. Теперь, если модуль B выгрузить первым, то ничего страшного не произойдет -- он просто восстановит запись в таблице sys_call_table в значение A_open, который в свою очередь вызывает оригинальную функцию sys_open. Однако, если первым будет выгружен модуль А, а затем B, то система "рухнет". Модуль А восстановит адрес в sys_call_table, указывающий на оригинальную функцию sys_open, "отсекая" таким образом модуль B от обработки действий по открытию файлов. Затем, когда будет выгружен модуль B, он восстановит адрес в sys_call_table на тот, который запомнил сам, потому что он считает его оригинальным. Т.е. вызовы будут направлены в функцию A_open, которой уже нет в памяти. На первый взгляд, проблему можно решить, проверкой -- совпадает ли адрес в sys_call_table с адресом нашей функции open и если не совпадает, то не восстанавливать значение этого вызова (таким образом B не будет "восстанавливать" системный вызов), но это порождает другую проблему. Когда выгружается модуль А, он "видит", что системный вызов был изменен на B_open и "отказывается" от восстановления указателя на sys_open. Теперь, функция B_open будет по прежнему пытаться вызывать A_open, которой больше не существует в памяти, так что система "рухнет" еще раньше -- до удаления модуля B. Обратите внимание: подобные проблемы делают такую "подмену" системных вызовов неприменимой для широкого распространения.С целью предотвращения потенциальной опасности, связанной с подменой адресов системных вызовов, ядро более не экспортирует sys_call_table. Поэтому, если вы желаете сделать нечто большее, чем просто пробежать глазами по тексту данного примера, вам надлежит наложить "заплату" на ядро. В каталоге с примерами вы найдете файл README и "заплату". Как вы наверняка понимаете, подобные модификации сопряжены с определенными трудностями, поэтому я не рекомендую производить их на системах, владельцем которых вы не являетесь или не в состоянии быстро восстановить. Если вас одолевают сомнения, то лучшим выбором будет отказ от прогона этого примера. Пример 7-1. syscall.c /* * syscall.c * * Пример "перехвата" системного вызова. */ /* * Copyright (C) 2001 by Peter Jay Salzman */ /* * Необходимые заголовочные файлы */ #include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/moduleparam.h> /* для передачи параметров модулю */ #include <linux/unistd.h> /* Список системных вызовов */ /* * Необходимо, чтобы уметь определять * user id вызвавшего процесса. */ #include <linux/sched.h> #include <asm/uaccess.h> /* * Таблица системных вызовов (таблица адресов функций). * Просто определим ее как ссылку на внешнюю таблицу. * * sys_call_table больше не экспортируется ядрами 2.6.x. * Если вы намереваетесь опробовать этот ОПАСНЫЙ модуль, * вам следует наложить "заплату" на ядро и пересобрать его */ extern void *sys_call_table[]; /* * UID пользователя, за которым "шпионим", * принимается из командной строки */ static int uid; module_param(uid, int, 0644); /* * Указатель на оригинальную функцию, выполняющую системный вызов. * Мы сохраняем ее, вместо того, чтобы напрямую вызывать оригинальную * функцию, для того, чтобы имелась возможность вызова * обработчиков, вставленных до нас * Это не гарантирует 100% безопасность, поскольку другой модуль, * замещающий sys_open может быть выгружен раньше нашего модуля. * * Другая причина -- мы не можем получить адрес оригинальной sys_open. * Этот адрес не экспортируется ядром. */ asmlinkage int (*original_call) (const char *, int, int); /* * Функция, замещающая sys_open (вызывается * всякий раз, когда делается обращение к системному вызову open). * Прототип функции, количество аргументов и их тип * вы найдете в fs/open.c. * * Теоретически - мы "привязаны" к данной конкретной * версии ядра. Практически -- системные вызовы * очень редко подвергаются кардинальному изменению, * поскольку это сделало бы огромное количество программного обеспечения * несовместимым с ядром и потребовало бы их пересборки. */ asmlinkage int our_sys_open(const char *filename, int flags, int mode) { int i = 0; char ch; /* * Проверить -- это искомый пользователь? */ if (uid == current->uid) { /* * Зафиксировать */ printk("Opened file by %d: ", uid); do { get_user(ch, filename + i); i++; printk("%c", ch); } while (ch != 0); printk("\n"); } /* * Вызвать оригинальную версию системного вызова sys_open - иначе * система потеряет возможность открывать файлы */ return original_call(filename, flags, mode); } /* * Инициализация модуля - подмена системного вызова */ int init_module() { /* * Внимание - предупреждение запоздало, но * может быть в следующий раз... */ printk("I'm dangerous. I hope you did a "); printk("sync before you insmod'ed me.\n"); printk("My counterpart, cleanup_module(), is even"); printk("more dangerous. If\n"); printk("you value your file system, it will "); printk("be \"sync; rmmod\" \n"); printk("when you remove this module.\n"); /* * Сохранить указатель на оригинальную функцию * в переменной original_call, и затем заменить указатель * в таблице системных вызовов */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open; /* * Чтобы получить адрес любого системного вызова * с именем foo, обращайтесь к записи sys_call_table[__NR_foo]. */ printk("Spying on UID:%d\n", uid); return 0; } /* * Завершение работы модуля - восстановление * указателя на оригинальный системный вызов */ void cleanup_module() { /* * Восстановить адрес системного вызова */ if (sys_call_table[__NR_open] != our_sys_open) { printk("Somebody else also played with the "); printk("open system call\n"); printk("The system may be left in "); printk("an unstable state.\n"); } sys_call_table[__NR_open] = original_call; } Пример 7-2. "Заплата" на ядро (export_sys_call_table_patch_for_linux_2.6.x) --- kernel/kallsyms.c.orig 2003-12-30 07:07:17.000000000 +0000 +++ kernel/kallsyms.c 2003-12-30 07:43:43.000000000 +0000 @@ -184,7 +184,7 @@ iter->pos = pos; return get_ksymbol_mod(iter); } - + /* If we're past the desired position, reset to start. */ if (pos < iter->pos) reset_iter(iter); @@ -291,3 +291,11 @@ EXPORT_SYMBOL(kallsyms_lookup); EXPORT_SYMBOL(__print_symbol); +/* START OF DIRTY HACK: + * Purpose: enable interception of syscalls as shown in the + * Linux Kernel Module Programming Guide. */ +extern void *sys_call_table; +EXPORT_SYMBOL(sys_call_table); + /* see http://marc.free.net.ph/message/20030505.081945.fa640369.html + * for discussion why this is a BAD THING(tm) and no longer supported by 2.6.0 + * END OF DIRTY HACK: USE AT YOUR OWN RISK */ Пример 7-3. Makefile obj-m += syscall.o Пример 7-4. README.txt Основная проблема, связанная с данным примером, состоит в невозможности определить адрес sys_call_table, поскольку он более не экспортируется ядрами 2.6.x. Возможность "перекрытия" системных вызовов через sys_call_table потенциально опасна поэтому, начиная с версии 2.5.41, она больше не поддерживается Обсуждение проблемы вы найдете на: http://www.ussg.iu.edu/hypermail/linux/kernel/0305.0/0711.html http://marc.free.net.ph/message/20030505.081945.fa640369.html http://marc.theaimsgroup.com/?l=linux-kernel&m=105212296015799&w=2 Чтобы иметь возможность опробовать данный пример на ядрах версии 2.5.41 и выше вам необходимо наложить заплату на ядро. ВНИМАНИЕ: НЕ ИСПОЛЬЗУЙТЕ ЭТУ ЗАПЛАТУ НА ПРОМЫШЛЕННЫХ ИЛИ ИНЫХ СИСТЕМАХ, КОТОРЫЕ СОДЕРЖАТ ЦЕННУЮ ИНФОРМАЦИЮ. Если бы я писал встроенную справку к этой заплате в Configure.help то я бы пометил ее как <dangerous> и дал бы следующее описание: ####################################################################### Эта опция экспортирует sys_call_table, что делает возможным "перекрытие" (подмену) системных вызовов. Подмена системных вызовов потенциально опасна и может стать причиной потери даных или еще хуже. Скажите Y, если желаете опробовать прилагаемый пример и вас не беспокоит возможная потеря данных. Практически любой должен здесть сказать N. ####################################################################### Если ваш старенький PC используется только как игрушка можете наложить эту заплату и опробовать пример. Предполагается, что исходные тексты ядра 2.6.x находятся в каталоге /usr/src/linux/ (http://www.linuxmafia.com/faq/Kernel/usr-src-linux-symlink.html) Ниже приводится текст сценария, выполняющий наложение заплаты. Эта заплата протестирована с ядрами 2.6.[0123], и может накладываться или не накладываться на другие версии. #!/bin/sh cp export_sys_call_table_patch_for_linux_2.6.x /usr/src/linux/ cd /usr/src/linux/ patch -p0 < export_sys_call_table_patch_for_linux_2.6.x Глава 8. Блокировка процессов Что вы делаете, когда кто-то просит вас о чем-то, а вы не можете сделать это немедленно? Пожалуй единственное, что вы можете ответить: "Пожалуйста, не сейчас, я пока занят.". А что должен делать модуль ядра? У него есть другая возможность. Он можете приостановить работу процесса до тех пор, пока не сможет обслужить его. В конечном итоге, ядро постоянно то приостанавливает, то вновь возобновляет работу процессов. Именно так обеспечивается возможность одновременного исполнения нескольких процессов на единственном процессоре. Пример ниже демонстрирует такую возможность. Модуль создает файл /proc/sleep, который может быть открыт только одним процессом, в каждый конкретный момент времени. Если файл уже был открыт кем-нибудь, то модуль вызывает wait_event_interruptible. [10] Эта функция изменяет состояние "задачи" (здесь, под термином "задача" понимается структура данных в ядре, которая хранит информацию о процессе), присваивая ему значение TASK_INTERRUPTIBLE, это означает, что задача не будет выполняться до тех пор, пока не будет "разбужена" каким либо образом, и добавляет процесс в очередь ожидания WaitQ, куда помещаются все процессы, желающие открыть файл /proc/sleep. Затем функция передает управление планировщику, который в свою очередь предоставляет возможность поработать другому процессу. Когда процесс закрывает файл, это приводит к вызову module_close. Она запускает все процессы, которые "сидят" в очереди WaitQ (к сожалению нет механизма, который позволил бы "разбудить" только один процесс). Затем управление возвращается процессу, который только что закрыл файл и он продолжает свою работу. После того, как данный процесс исчерпает свой квант времени, планировщик передаст управление другому процессу. Таким образом, один из процессов, ожидавших своей очереди доступа к файлу, в конечном итоге получит управление и продолжит исполнение с точки, следующей за вызовом wait_event_interruptible. [11] Он установит глобальную переменную, извещающую остальные процессы о том, что файл открыт и займется обработкой открытого файла. Когда другие процессы получат свой квант времени, они обнаружат, что файл все еще открыт и опять приостановят свою работу. Чтобы как-то оживить повествование замечу, что module_close не обладает монопольным правом на возобновление работы ожидающих процессов. Сигнал Ctrl-C (SIGINT) также может "разбудить" процесс. [12] В этом случае процессу немедленно возвращается -EINTR. Таким образом пользователи могут, например, прервать процесс прежде, чем он получит доступ к файлу. Тут есть еще один момент, о котором хотелось бы упомянуть. Некоторые процессы не желают быть заблокированными, такие процессы должны либо получить в свое распоряжение открытый файл немедленно, либо извещение о том, что их запрос не может быть удовлетворен в настоящий момент. Такие процессы используют флаг O_NONBLOCK при открытии файла. Если ядро не в состоянии немедленно удовлетворить запрос, оно отвечает кодом ошибки -EAGAIN. Пример 8-1. sleep.c /* * sleep.c - Создает файл в /proc, доступ к * которому может получить только один процесс, * все остальные будут приостановлены. */ #include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/proc_fs.h> /* Необходимо для работы с /proc */ #include <linux/sched.h> /* Взаимодействие с планировщиком */ #include <asm/uaccess.h> /* определение функций get_user и put_user */ /* * Место хранения последнего принятого сообщения, * которое будет выводиться в файл, чтобы показать, что * модуль действительно может получать ввод от пользователя */ #define MESSAGE_LENGTH 80 static char Message[MESSAGE_LENGTH]; static struct proc_dir_entry *Our_Proc_File; #define PROC_ENTRY_FILENAME "sleep" /* см. include/linux/fs.h */ static ssize_t module_output(struct file *file, /* буфер с данными (в пространстве пользователя) */ char *buf, /* размер буфера */ size_t len, loff_t * offset) { static int finished = 0; int i; char message[MESSAGE_LENGTH + 30]; /* * Для индикации признака конца файла возвращается 0. * В противном случае процесс будет продолжать читать из файла * угодив в бесконечный цикл. */ if (finished) { finished = 0; return 0; } /* * Для передачи данных из пространства ядра в пространство пользователя * следует использовать put_user. * В обратном направлении -- get_user. */ sprintf(message, "Last input:%s\n", Message); for (i = 0; i < len && message[i]; i++) put_user(message[i], buf + i); finished = 1; return i; /* Вернуть количество "прочитанных" байт */ } /* * Эта функция принимает введенное пользователем сообщение */ static ssize_t module_input(struct file *file, /* Собственно файл */ const char *buf, /* Буфер с сообщением */ size_t length, /* размер буфера */ loff_t * offset) { /* смещение в файле - игнорируется */ int i; /* * Переместить данные, полученные от пользователя в буфер, * который позднее будет выведен йункцией module_output. */ for (i = 0; i < MESSAGE_LENGTH - 1 && i < length; i++) get_user(Message[i], buf + i); /* Обычная строка, завершающаяся символом \0 */ Message[i] = '\0'; /* * Вернуть число принятых байт */ return i; } /* * 1 -- если файл открыт */ int Already_Open = 0; /* * Очередь ожидания */ DECLARE_WAIT_QUEUE_HEAD(WaitQ); /* * Вызывается при открытии файла в /proc */ static int module_open(struct inode *inode, struct file *file) { /* * Если установлен флаг O_NONBLOCK, * то процесс не должен приостанавливаться * В этом случае, если файл уже открыт, * необходимо вернуть код ошибки * -EAGAIN, что означает "попробуйте в другой раз" */ if ((file->f_flags & O_NONBLOCK) && Already_Open) return -EAGAIN; /* * Нарастить счетчик обращений, * чтобы невозможно было выгрузить модуль */ try_module_get(THIS_MODULE); /* * Если файл уже открыт -- приостановить процесс */ while (Already_Open) { int i, is_sig = 0; /* * Эта функция приостановит процесс и поместит его в очередь ожидания. * Исполнение процесса будет продолжено с точки, следующей за вызовом * этой функции, когда кто нибудь сделает вызов * wake_up(&WaitQ) (это возможно только внутри module_close, когда * файл будет закрыт) или когда процессу поступит сигнал Ctrl-C */ wait_event_interruptible(WaitQ, !Already_Open); for (i = 0; i < _NSIG_WORDS && !is_sig; i++) is_sig = current->pending.signal.sig[i] & ~current-> blocked.sig[i]; if (is_sig) { /* * Не забыть вызвать здесь module_put(THIS_MODULE), * поскольку процесс был прерван * и никогда не вызовет функцию close. * Если не уменьшить счетчик обращений, то он навсегда останется * больше нуля, в результате модуль можно будет * уничтожить только при перезагрузке системы */ module_put(THIS_MODULE); return -EINTR; } } /* * В этой точке переменная Already_Open должна быть равна нулю */ /* * Открыть файл */ Already_Open = 1; return 0; } /* * Вызывается при закрытии файла */ int module_close(struct inode *inode, struct file *file) { /* * Записать ноль в Already_Open, тогда один * из процессов из WaitQ * сможет записать туда единицу и открыть файл. * Все остальные процессы, ожидающие доступа * к файлу опять будут приостановлены */ Already_Open = 0; /* * Возобновить работу процессов из WaitQ. */ wake_up(&WaitQ); module_put(THIS_MODULE); return 0; } /* * Эта функция принимает решение о праве на выполнение операций с файлом * 0 -- разрешено, ненулеое значение -- запрещено. * * Операции с файлом могут быть: * 0 - Исполнениеe (не имеет смысла в нашей ситуации) * 2 - Запись (передача от пользователя к модулю ядра) * 4 - Чтение (передача от модуля ядра к пользователю) * * Эта функция проверяет права доступа к файлу * Права, выводимые командой ls -l * могут быть проигнорированы здесь. */ static int module_permission(struct inode *inode, int op, struct nameidata *nd) { /* * Позволим любому читать файл, но * писать -- только root-у (uid 0) */ if (op == 4 || (op == 2 && current->euid == 0)) return 0; /* * Если что-то иное -- запретить доступ */ return -EACCES; } /* * Указатели на функции-обработчики для нашего файла. */ static struct file_operations File_Ops_4_Our_Proc_File = { .read = module_output, /* чтение из файла */ .write = module_input, /* запись в файл */ .open = module_open, /* открытие файла */ .release = module_close, /* закрытие файла */ }; /* * Операции над индексной записью нашего файла. Необходима * для того, чтобы указать местоположение структуры * file_operations нашего файла, а так же, чтобы задать * функцию определения прав доступа к файлу. Здесь можно указать адреса * других функций-обработчиков, но нас они не интересуют. */ static struct inode_operations Inode_Ops_4_Our_Proc_File = { .permission = module_permission, /* check for permissions */ }; /* * Начальная и конечная функции модуля */ /* * Инициализация модуля - регистрация файла в /proc */ int init_module() { int rv = 0; Our_Proc_File = create_proc_entry(PROC_ENTRY_FILENAME, 0644, NULL); Our_Proc_File->owner = THIS_MODULE; Our_Proc_File->proc_iops = &Inode_Ops_4_Our_Proc_File; Our_Proc_File->proc_fops = &File_Ops_4_Our_Proc_File; Our_Proc_File->mode = S_IFREG | S_IRUGO | S_IWUSR; Our_Proc_File->uid = 0; Our_Proc_File->gid = 0; Our_Proc_File->size = 80; if (Our_Proc_File == NULL) { rv = -ENOMEM; remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); printk(KERN_INFO "Error: Could not initialize /proc/test\n"); } return rv; } /* * Завершение работы модуля - дерегистрация файла в /proc. * Чревато последствиями * если в WaitQ остаются процессы, ожидающие своей очереди, * поскольку точка их исполнения * практически находится в функции open, которая * будет выгружена при удалении модуля. * Позднее, в 9 главе, я опишу как воспрепятствовать * удалению модуля в таких случаях */ void cleanup_module() { remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); }
|