Что такое kernel driver
Сведения о программных интерфейсах, которые драйвер может реализовывать или вызывать, см. в справочнике по драйверам режима ядра.
Этот раздел содержит общие понятия, помогающие понять программирование в режиме ядра и описывает конкретные методы программирования ядра. общие сведения о драйверах Windows см. в разделе начало работы с драйверами Windows, в которых представлен общий обзор компонентов Windows, перечислены типы драйверов устройств, используемых в Windows, обсуждаются цели Windows драйверы устройств и обсуждаются универсальные примеры драйверов устройств, входящие в комплект.
Этот раздел содержит общие сведения, описывающие и помогающие создавать драйверы режима ядра.
Обзор , содержащий:
компоненты режима ядра описывают основные диспетчеры режима ядра и компоненты операционной системы Windows.
написание драйверов wdm и введение в wdm предоставляют сведения, необходимые для записи драйверов с помощью WDM (WDM).
Объекты устройств и другие разделы в объектах устройств и в стеках устройств описывают, как операционная система представляет устройства по объектам устройств.
управление памятью для драйверов Windows показывает, как драйверы режима ядра выделяют память для таких целей, как хранение внутренних данных, буферизация данных во время операций ввода-вывода и совместное использование памяти с другими компонентами режима ядра и пользовательского режима.
Безопасность Чтобы обеспечить максимально безопасную безопасность, от управления доступом к устройствам и привилегиями на SDDL для объектов устройств.
Обработка IRP описывает, как драйверы режима ядра обрабатывают пакеты запросов ввода-вывода (IRP).
Канал DMA Прямой доступ к памяти (DMA) — это важный аспект разработки драйверов, а темы на этом узле охватывают DMA от а до я.
Объекты контроллера представляют собой контроллер физического устройства с подключенными устройствами.
Программы-обработчики прерываний (ISR) обработают прерывания для драйверов физического устройства, принимающего прерывания.
Сигнальные прерывания вызывают прерывание, записывая значение в определенный адрес памяти.
Отложенные вызовы процедур (объекты DPC) могут быть поставлены в очередь из ISR и выполняться позже, а уровень IRQL — ниже, чем при ISR.
Plug and Play (PnP) специализируется на поддержке программного обеспечения для pnp и о том, как драйверы используют эту поддержку для реализации pnp.
Управление питанием описывает архитектуру, обеспечивающую комплексный подход к управлению питанием системы и устройств.
инструментарий управления Windows (WMI) (WMI) являются расширениями для драйвера в режиме ядра, что позволяет драйверу стать поставщиком WMI. Поставщик WMI делает данные измерений и инструментирования доступными для потребителей WMI, таких как приложения пользовательского режима.
Методики программирования драйверов для драйверов программирования в режиме ядра Windows требуются методы, которые иногда значительно отличаются от обычных средств программирования в пользовательском режиме.
Данная статья нацелена на тех, кто только недавно начал разрабатывать kernel-драйвера под ОС Windows. В 100-ый раз видишь ненавистную надпись IRQL_NOT_LESS_OR_EQUAL и этот грустный смайлик? Тогда прошу пройти под кат.
Одной из основных ошибок, которую я и сам совершал, является жонглирование IRQL так, как душе угодно, и неполное понимание внутреннего устройства работы приоритетов потоков в ядре Windows.
К примеру, у вас есть кусок кода, который генерирует какое-либо событие по PID-процесса.
Внутри данного кода, используются разделяемые данные, синхронизация которых, обеспечивается спинлоком. Также нам нужно получить имя процесса, чтобы залогировать событие.
Уже увидели ошибку в данном фрагменте?
PsLookupProcessByProcessId() – требует соблюдения условия: IRQL
Так что, подобный код будет часто выдавать BSOD с кодом ошибки IRQL_NOT_LESS_OR_EQUAL.
И вот дальше, начинается самое интересное. Первое решение, которое придёт в голову новичкам, будет менять уровень IRQL перед вызовом данной функции так, чтобы условие соблюдалось.
То есть, переписать код — вот так:
Вот теперь то, всё работает достаточно стабильно. Но, на самом деле это не так. Данный код только хорошо маскирует проблему, снижая шансы её проявления до минимума, но в 1 из 1000 случаев, она всё же всплывёт, а вы будет рвать на себе волосы, пытаясь понять в чём же ошибка.
И тут нужно вспомнить одно из правил написания драйверов, а именно:
«Понижать IRQL можно только в том случае, если вы его собственноручно повышали, и только до его предыдущего значения!»
Если какой-либо код вызвал вашу функцию на IRQL = APC_LEVEL, то вы не имеете права опустить его ниже данного уровня. Вы можете поднять IRQL до DISPATCH_LEVEL, потом опустить обратно до APC_LEVEL, но не ниже.
Таким образом, более приемлемым вариантом кода, будет:
А вспомогательные функции по типу SetIrql() из 2-го примера, в принципе не являются адекватными с точки зрения интерфейса, т.к. при проектировании отдельных методов в вашем драйвере, важно продумывать ограничения накладываемые на предусловия вызова вашей функции.
Для описания данных предусловий, удобно использовать аннотации SAL, их список вы можете посмотреть тут:
Также Microsoft предоставляет небольшой whitepaper(в самом низу статьи) по управлению приоритетами потоков в ядре, и более подробно рассказывает некоторые тонкости по работе с ними:
Если же, вам всё-таки нужно каким-либо образом вызвать какое-либо Api, требующее более низких значений IRQL, то одним из вариантов решения данной проблемы могут стать WorkItem’ы. Но о них, я расскажу уже в другой статье.
Меня всегда интересовало низкоуровневое программирование – общаться напрямую с оборудованием, жонглировать регистрами, детально разбираться как что устроено. Увы, современные операционные системы максимально изолируют железо от пользователя, и просто так в физическую память или регистры устройств что-то записать нельзя. Точнее я так думал, а на самом деле оказалось, что чуть ли не каждый производитель железа так делает!
В чём суть, капитан?
В архитектуре x86 есть понятие «колец защиты» («Ring») – режимов работы процессора. Чем ниже номер текущего режима, тем больше возможностей доступно исполняемому коду. Самым ограниченным «кольцом» является «Ring 3», самым привилегированным – «Ring -2» (режим SMM). Исторически сложилось, что все пользовательские программы работают в режиме «Ring 3», а ядро ОС – в «Ring 0»:
Режимы работы x86 процессора
В «Ring 3» программам запрещены потенциально опасные действия, такие как доступ к I/O портам и физической памяти. По логике разработчиков, настолько низкоуровневый доступ обычным программам не нужен. Доступ к этим возможностям имеют только операционная система и её компоненты (службы и драйверы). И всё бы ничего, но однажды я наткнулся на программу RW Everything:
RW Everything действительно читает и пишет практически всё
Эта программа была буквально напичкана именно теми функциями, которые обычно запрещаются программам «Ring 3» - полный доступ к физической памяти, I/O портам, конфигурационному пространству PCI (и многое другое). Естественно, мне стало интересно, как это работает. И выяснилось, что RW Everything устанавливает в систему прокси-драйвер:
Смотрим последний установленный драйвер через OSR Driver Loader
Прокси-драйвера
В итоге получается обходной манёвр – всё, что программе запрещено делать, разработчик вынес в драйвер, программа устанавливает драйвер в систему и уже через него программа делает, что хочет! Более того – выяснилось, что RW Everything далеко не единственная программа, которая так делает. Таких программ не просто много, они буквально повсюду. У меня возникло ощущение, что каждый уважающий себя производитель железа имеет подобный драйвер:
Софт для обновления BIOS (Asrock, Gigabyte, HP, Dell, AMI, Intel, Insyde…)
Софт для разгона и конфигурации железа (AMD, Intel, ASUS, ASRock, Gigabyte)
Софт для просмотра сведений о железе (CPU-Z, GPU-Z, AIDA64)
Софт для обновления PCI устройств (Nvidia, Asmedia)
Во многих из них практически та же самая модель поведения – драйвер получает команды по типу «считай-ка вот этот физический адрес», а основная логика – в пользовательском софте. Ниже в табличке я собрал некоторые прокси-драйвера и их возможности:
Результаты краткого анализа пары десятков драйверов. Могут быть ошибки!
Mem – чтение / запись физической памяти
PCI – чтение / запись PCI Configuration Space
I/O – чтение / запись портов I/O
Alloc – аллокация и освобождение физической памяти
Map – прямая трансляция физического адреса в вирутальный
MSR – чтение / запись x86 MSR (Model Specific Register)
Жёлтым обозначены возможности, которых явно нет, но их можно использовать через другие (чтение или маппинг памяти). Мой фаворит из этого списка – AsrDrv101 от ASRock. Он устроен наиболее просто и обладает просто огромным списком возможностей, включая даже функцию поиска шаблона по физической памяти (!!)
Неполный перечень возможностей AsrDrv101
Чтение / запись RAM
Чтение / запись IO
Чтение / запись PCI Configuration Space
Чтение / запись MSR (Model-Specific Register)
Чтение / запись CR (Control Register)
Чтение TSC (Time Stamp Counter)
Чтение PMC (Performance Monitoring Counter)
Alloc / Free физической памяти
Поиск по физической памяти
Самое нехорошее в такой ситуации - если подобный драйвер остаётся запущенным на ПК пользователя, для обращения к нему не нужно даже прав администратора! То есть любая программа с правами пользователя сможет читать и писать физическую память - хоть пароли красть, хоть ядро пропатчить. Именно на это уже ругались другие исследователи. Представьте, что висящая в фоне софтина, красиво моргающая светодиодиками на матплате, открывает доступ ко всей вашей системе. Или вирусы намеренно ставят подобный драйвер, чтобы закрепиться в системе. Впрочем, любой мощный инструмент можно в нехороших целях использовать.
Через Python в дебри
Конечно же я захотел сделать свой небольшой "тулкит" для различных исследований и экспериментов на базе такого драйвера. Причём на Python, мне уж очень нравится, как просто выглядит реализация сложных вещей на этом языке.
Первым делом нужно установить драйвер в систему и запустить его. Делаем "как положено" и сначала кладём драйвер (нужной разрядности!) в System32:
Раньше в похожих ситуациях я извращался с папкой %WINDIR%\Sysnative, но почему-то на моей текущей системе такого алиаса не оказалось, хотя Python 32-битный. (по идее, на 64-битных системах обращения 32-битных программ к папке System32 перенаправляются в папку SysWOW64, и чтобы положить файлик именно в System32, нужно обращаться по имени Sysnative).
Затем регистрируем драйвер в системе и запускаем его:
А дальше запущенный драйвер создаёт виртуальный файл (кстати, та самая колонка "имя" в таблице с анализом дров), через запросы к которому и осуществляются дальнейшие действия:
И ещё одна полезная программа для ползания по системе, WinObj
Тоже ничего особенного, открываем файл и делаем ему IoCtl:
Вот здесь чутка подробнее. Я долго думал, как же обеспечить адекватную обработку ситуации, когда таких "скриптов" запущено несколько. Не останавливать драйвер при выходе нехорошо, в идеале нужно смотреть, не использует ли драйвер кто-то ещё и останавливать его только если наш скрипт "последний". Долгие упорные попытки получить количество открытых ссылок на виртуальный файл драйвера ни к чему не привели (я получал только количество ссылок в рамках своего процесса). Причём сама система точно умеет это делать - при остановке драйвера с открытым файлом, он остаётся висеть в "Pending Stop". Если у кого есть идеи - буду благодарен.
В конечном итоге я "подсмотрел", как это делают другие программы. Выяснилось, что большинство либо не заморачиваются, либо просто ищут запущенные процессы с тем же именем. Но одна из исследованных программ имела кардинально другой подход, который я себе и перенял. Вместо того, чтобы переживать по количеству ссылок на файл, просто на каждый запрос открываем и закрываем файл! А если файла нет, значит кто-то остановил драйвер и пытаемся его перезапустить:
А дальше просто реверсим драйвер и реализуем все нужные нам вызовы:
Легко и непринуждённо в пару команд читаем физическую память
PCI Express Config Space
Немного отвлечёмся на один нюанс про PCIE Config Space. С этим адресным пространством не всё так просто - со времён шины PCI для доступа к её конфигурационному пространству используется метод с использованием I/O портов 0xCF8 / 0xCFC. Он применён и в нашем драйвере AsrDrv101:
Чтение и запись PCI Config Space
Но через этот метод доступны только 0x100 байт конфигурационного пространства, в то время как в стандарте PCI Express размер Config Space у устройств может быть достигать 0x1000 байт! И полноценно вычитать их можно только обращением к PCI Extended Config Space, которая замаплена где-то в адресном пространстве, обычно чуть пониже BIOS:
Адресное пространство современного x86 компа, 0-4 ГБ
На чипсетах Intel (ну, в их большинстве) указатель на эту область адресного пространства можно взять из конфига PCI устройства 0:0:0 по смещению 0x60, подробнее описано в даташитах:
У AMD я такого не нашёл (наверняка есть, плохо искал), но сам факт неуниверсальности пнул меня в сторону поиска другого решения. Погуглив стандарты, я обнаружил, что указатель на эту область передаётся системе через ACPI таблицу MCFG
А сами ACPI таблицы можно найти через запись RSDP, поискав её сигнатуру по адресам 0xE0000-0xFFFFF, а затем распарсив табличку RSDT. Отлично, здесь нам и пригодится функционал поиска по памяти. Получаем нечто такое:
На всякий случай оставляем вариант для чипсетов Intel
Всё, теперь осталось при необходимости заменить чтение PCI Express Config Space через драйвер на чтение через память. Теперь-то разгуляемся!
Читаем BIOS
В качестве примера применения нашего "тулкита", попробуем набросать скрипт чтения BIOS. Он должен быть "замаплен" где-то в конце 32-битного адресного пространства, потому что компьютер начинает его исполнение с адреса 0xFFFFFFF0. Обычно в ПК стоит флеш-память объёмом 4-16 МБ, поэтому будем "сканировать" адресное пространство с адреса 0xFF000000, как только найдём что-нибудь непустое, будем считать, что тут начался BIOS:
В результате получаем:
Вот так в 10 строчек мы считали BIOS
Но подождите-ка, получилось всего 6 мегабайт, а должно быть 4 или 8 что-то не сходится. А вот так, у чипсетов Intel в адресное пространство мапится не вся флешка BIOS, а только один её регион. И чтобы считать всё остальное, нужно уже использовать SPI интерфейс.
Не беда, лезем в даташит, выясняем, что SPI интерфейс висит на PCI Express:
И для его использования, нужно взаимодействовать с регистрами в BAR0 MMIO по алгоритму:
Задать адрес для чтения в BIOS_FADDR
Задать параметры команды в BIOS_HSFTS_CTL
Прочитать данные из BIOS_FDATA
Пилим новый скрипт для чтения через чипсет:
Исполняем и вуаля - в 20 строчек кода считаны все 8 МБ флешки BIOS! (нюанс - в зависимости от настроек, регион ME может быть недоступен для чтения).
Точно так же можно делать всё, что заблагорассудится - делать снифер USB пакетов, посылать произвольные ATA команды диску, повышать частоту процессора и переключать видеокарты. И это всё - с обычными правами администратора:
Немного помучившись, получаем ответ от SSD на команду идентификации
А если написать свой драйвер?
Некоторые из вас наверняка уже подумали - зачем так изворачиваться, реверсить чужие драйвера, если можно написать свой? И я о таком думал. Более того, есть Open-Source проект chipsec, в котором подобный драйвер уже разработан.
Зайдя на страницу с кодом драйвера, вы сразу наткнетесь на предупреждение:
В этом предупреждении как раз и описываются все опасности, о которых я рассказывал в начале статьи - инструмент мощный и опасный, следует использовать только в Windows режиме Test Mode, и ни в коем случае не подписывать. Да, без специальной подписи на обычной системе просто так запустить драйвер не получится. Поэтому в примере выше мы и использовали заранее подписанный драйвер от ASRock.
Если кто сильно захочет подписать собственный драйвер - понадобится регистрировать собственную компанию и платить Microsoft. Насколько я нагуглил, физическим лицам такое развлечение недоступно.
Точнее я так думал, до вот этой статьи, глаз зацепился за крайне интересный абзац:
У меня под рукой нет Windows DDK, так что я взял 64-битный vfd.sys , скомпилированный неким critical0, и попросил dartraiden подписать его «древне-китайским способом». Такой драйвер успешно загружается и работает, если vfdwin запущена с правами администратора
Драйвер из статьи действительно подписан, и действительно неким китайским ключом:
Как оказалось, сведения о подписи можно просто посмотреть в свойствах.. А я в HEX изучал
Немного поиска этого имени в гугле, и я натыкаюсь на вот эту ссылку, откуда узнаю, что:
есть давно утёкшие и отозванные ключи для подписи драйверов
если ими подписать драйвер - он прекрасно принимается системой
малварщики по всему миру используют это для создания вирусни
Основная загвоздка - заставить майкрософтский SignTool подписать драйвер истёкшим ключом, но для этого даже нашёлся проект на GitHub. Более того, я нашёл даже проект на GitHub для другой утилиты подписи драйверов от TrustAsia, с помощью которого можно подставить для подписи вообще любую дату.
Несколько минут мучений с гугл-переводчиком на телефоне, и мне удалось разобраться в этой утилите и подписать драйвер одним из утекших ключей (который довольно легко отыскался в китайском поисковике):
И в самом деле, китайская азбука
И точно так же, как и AsrDrv101, драйвер удалось без проблем запустить!
А вот и наш драйвер запустился
Из чего делаю вывод, что старая идея с написанием своего драйвера вполне себе годная. Как раз не хватает функции маппинга памяти. Но да ладно, оставлю как TODO.
Выводы?
Как видите, имея права администратора, можно делать с компьютером практически что угодно. Будьте внимательны - установка утилит от производителя вашего железа может обернуться дырой в системе. Ну а желающие поэкспериментировать со своим ПК - добро пожаловать на низкий уровень! Наработки выложил на GitHub. Осторожно, бездумное использование чревато BSODами.
В Windows есть фича "Изоляция ядра", которая включает I/O MMU, защищает от DMA атак и так далее (кстати об этом - в следующих сериях)
Так вот, при включении этой опции, некоторые драйвера (в том числе RW Everything и китайско-подписанный chipsec_hlpr) перестают запускаться:
Ядро — это та часть операционной системы, работа которой полностью скрыта от пользователя, т. к. пользователь с ним не работает напрямую: пользователь работает с программами. Но, тем не менее, без ядра невозможна работа ни одной программы, т.е. они без ядра бесполезны. Этот механизм чем-то напоминает отношения официанта и клиента: работа хорошего официанта должна быть практически незаметна для клиента, но без официанта клиент не сможет передать заказ повару, и этот заказ не будет доставлен.
В Linux ядро монолитное, т.е. все его драйвера и подсистемы работают в своем адресном пространстве, отделенном от пользовательского. Сам термин «монолит» говорит о том, что в ядре сконцентрировано всё, и, по логике, ничего не может в него добавляться или удаляться. В случае с ядром Linux — это правда лишь отчасти: ядро Linux может работать в таком режиме, однако, в подавляющем большинстве сборок возможна модификация части кода ядра без его перекомпиляции, и даже без его выгрузки. Это достигается путем загрузки и выгрузки некоторых частей ядра, которые называются модулями. Чаще всего в процессе работы необходимо подключать модули драйверов устройств, поддержки криптографических алгоритмов, сетевых средств, и, чтобы уметь это правильно делать, нужно разбираться в строении ядра и уметь правильно работать с его модулями. Об этом и пойдет речь в этой статье.
В современных ядрах при подключении оборудования модули подключаются автоматически, а это событие обрабатывается демоном udev, который создает соответствующий файл устройства в каталоге "/dev". Все это выполняется в том случае, если соответствующий модуль корректно установлен в дерево модулей. В случае с файловыми системами ситуация та же: при попытке монтирования файловой системы ядро подгружает необходимый модуль автоматически, и выполняет монтирование.
Если необходимость в модуле не на столько очевидна, ядро его не загружает самостоятельно. Например, для поддержки функции шифрования на loop устройстве нужно вручную подгрузить модуль «cryptoloop», а для непосредственного шифрования — модуль алгоритма шифрования, например «blowfish».
Поиск необходимого модуля
Модули хранятся в каталоге "/lib/modules/" в виде файлов с расширением «ko». Для получения списка всех модулей из дерева можно выполнить команду поиска всех файлов с расширением «ko» в каталоге с модулями текущего ядра:
find /lib/modules/`uname -r` -name ‘*.ko’
Полученный список даст некоторое представление о доступных модулях, их назначении и именах. Например, путь «kernel/drivers/net/wireless/rt2x00/rt73usb.ko» явно указывает на то, что этот модуль — драйвер устройства беспроводной связи на базе чипа rt73. Более детальную информацию о модуле можно получить при помощи команды modinfo:
Загрузка и выгрузка модулей
Загрузить модуль в ядро можно при помощи двух команд: «insmod» и «modprobe», отличающихся друг от друга возможностью просчета и удовлетворения зависимостей. Команда «insmod» загружает конкретный файл с расширением «ko», при этом, если модуль зависит от других модулей, еще не загруженных в ядро, команда выдаст ошибку, и не загрузит модуль. Команда «modprobe» работает только с деревом модулей, и возможна загрузка только оттуда по имени модуля, а не по имени файла. Отсюда следует область применения этих команд: при помощи «insmod» подгружается файл модуля из произвольного места файловой системы (например, пользователь скомпилировал модули и перед переносом в дерево ядра решил проверить его работоспособность), а «modprobe» — для подгрузки уже готовых модулей, включенных в дерево модулей текущей версии ядра. Например, для загрузки модуля ядра «rt73usb» из дерева ядра, включая все зависимости, и отключив аппаратное шифрование, нужно выполнить команду:
После загрузки модуля можно проверить его наличие в списке загруженных в ядро модулей при помощи команды «lsmod»:
Module | Size | Used by | |
rt73usb | 17305 | 0 | |
crc_itu_t | 999 | 1 | rt73usb |
rt2x00usb | 5749 | 1 | rt73usb |
rt2x00lib | 19484 | 2 | rt73usb,rt2x00usb |
Из вывода команды ясно, что модуль подгружен, а так же в своей работе использует другие модули.
Чтобы его выгрузить, можно воспользоваться командой «rmmod» или той же командой «modprobe» с ключем "-r". В качестве параметра обоим командам нужно передать только имя модуля. Если модуль не используется, то он будет выгружен, а если используется — будет выдана ошибка, и придется выгружать все модули, которые от него зависят:
Для автоматической загрузки модулей в разных дистрибутивах предусмотрены разные механизмы. Я не буду вдаваться здесь в подробности, они для каждого дистрибутива свои, но один метод загрузки всегда действенен и удобен: при помощи стартовых скриптов. В тех же RedHat системах можно записать команды загрузки модуля прямо в "/etc/rc.d/rc.local" со всеми опциями.
Файлы конфигурация модулей находится в каталоге "/etc/modprobe.d/" и имеют расширение «conf». В этих файлах преимущественно перечисляются альтернативные имена модулей, их параметры, применяемые при их загрузке, а так же черные списки, запрещенные для загрузки. Например, чтобы вышеупомянутый модуль сразу загружался с опцией «nohwcrypt=1» нужно создать файл, в котором записать строку:
options rt73usb nohwcrypt=1
Черный список модулей хранится преимущественно в файле "/etc/modules.d/blacklist.conf" в формате «blacklist ». Используется эта функция для запрета загрузки глючных или конфликтных модулей.
Сборка модуля и добавление его в дерево
Иногда нужного драйвера в ядре нет, поэтому приходится его компилировать вручную. Это так же тот случай, если дополнительное ПО требует добавление своего модуля в ядро, типа vmware, virtualbox или пакет поддержки карт Nvidia. Сам процесс компиляции не отличается от процесса сборки программы, но определенные требования все же есть.
Во первых, нужен компилятор. Обычно установка «gcc» устанавливает все, что нужно для сборки модуля. Если чего-то не хватает — программа сборки об этом скажет, и нужно будет доустановить недостающие пакеты.
Во вторых, нужны заголовочные файлы ядра. Дело в том, что модули ядра всегда собираются вместе с ядром, используя его заголовочные файлы, т.к. любое отклонение и несоответствие версий модуля и загруженного ядра ведет к невозможности загрузить этот модуль в ядро.
Если система работает на базе ядра дистрибутива, то нужно установить пакеты с заголовочными файлами ядра. В большинстве дистрибутивов это пакеты «kernel-headers» и/или «kernel-devel». Для сборки модулей этого будет достаточно. Если ядро собиралось вручную, то эти пакеты не нужны: достаточно сделать символическую ссылку "/usr/src/linux", ссылающуюся на дерево сконфигурированных исходных кодов текущего ядра.
После компиляции модуля на выходе будет получен один или несколько файлов с расширением «ko». Можно попробовать их загрузить при помощи команды «insmod» и протестировать их работу.
Если модули загрузились и работают (или лень вручную подгружать зависимости), нужно их скопировать в дерево модулей текущего ядра, после чего обязательно обновить зависимости модулей командой «depmod». Она пройдется рекурсивно по дереву модулей и запишет все зависимости в файл «modules.dep», который, в последствие, будет анализироваться командой «modprobe». Теперь модули готовы к загрузке командой modprobe и могут загружаться по имени со всеми зависимостями.
Стоит отметить, что при обновлении ядра этот модуль работать не будет. Нужны будут новые заголовочные файлы и потребуется заново пересобрать модуль.
Ошибка «Invalid argument» может говорить о чем угодно, саму ошибку ядро на консоль написать не может, только при помощи функции «printk» записать в системный лог. Посмотрев логи можно уже узнать в чем ошибка:
В этом примере выведена только последняя строка с ошибкой, чтобы не загромаждать статью. Модуль может написать и несколько строк, поэтому лучше выводить полный лог, или хотя бы последние строк десять.
Ошибку уже легко найти: значение «2» неприемлемо для параметра «nohwcrypt». После исправления, модуль корректно загрузится в ядро.
Из всего сказанного можно сделать один вывод: ядро Linux играет по своим правилам и занимается серьезными вещами. Тем не менее — это всего лишь программа, оно, по сути, не сильно отличается от других обычных программ. Понимание того, что ядро не так уж страшно, как кажется, может стать первым шагом к пониманию внутреннего устройства системы и, как результат, поможет быстро и эффективно решать задачи, с которыми сталкивается любой администратор Linux в повседневной работе.
"Ну вот! Начали за здравие, кончили за упокой. При чем тут службы?" - спросите вы. и будете неправы. Очень даже при чем.
Я было начал эту статью с описания простейшего драйвера, но, по ходу дела, был вынужден отвлекаться на то, чтобы объяснять, как его зарегистрировать, запустить и т.д. и т.п. Тогда я решил, что будет логичнее, сначала поведать о том, как драйверы регистрируются, запускаются… Но тут возникла похожая проблема. Мне пришлось говорить о том, к каким действиям еще не написанного драйвера, приведет тот или иной вызов диспетчера управления службами или диспетчера ввода-вывода. Тут уж ничего не поделаешь. Слишком тесно эти компоненты связаны друг с другом. Как бы там ни было, но я остановился на втором варианте: сначала я глаголю о диспетчере управления службами, потом, в следующей статье, о простейшем драйвере, затем о диспетчере ввода-вывода, и наконец, разберем полнофункциональный драйвер. Когда вы прочитаете последующие статьи, будет неплохо вернуться к предыдущим - тогда многое встанет на свои места. Так что запаситесь терпением.
Поскольку у меня нет ни малейшего желания выступать в качестве переводчика официальной документации Microsoft, по крайней мере безвозмездно, то информацию о функциях, которыми мы будем пользоваться, принимаемых ими параметрах и их значениях, я буду давать лишь в объеме, необходимом для реализации наших целей. За подробностями обращайтесь к MSDN, API Reference и DDK.
- Диспетчер управления службами (Service Control Manager, SCM). Именно благодаря ему мы будем иметь возможность легко и просто загружать наши драйверы;
- Программа управления службой (Service Control Program, SCP). Работает в тесной связке с SCM;
- Собственно сам драйвер.
Диспетчер управления службами
На конечном этапе загрузки системы, перед появлением диалога регистрации пользователя, запускается SCM (\%SystemRoot%\System32\Services.exe), который, просматривая раздел реестра HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\, создает свою внутреннюю базу данных (ServicesActive database или SCM database). Далее SCM находит в созданной базе все драйверы устройств и службы, помеченные для автоматического запуска, и загружает их.
Чтобы получить кое-какое представление об этом, запустите редактор реестра (\%SystemRoot%\regedit.exe), откройте раздел HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ и изучите его содержимое.
Теперь запустите оснастку Администрирование > Службы ( Administrative Tools > Services ). Вы увидите список установленных служб (именно служб, а не драйверов).
Чтобы просмотреть список загруженных драйверов, запустите Администрирование > Управление компьютером ( Administrative Tools > Computer Management ) и в левом окне откройте ветвь Служебные программы > Сведения о системе > Программная среда > Драйверы ( System Tools > System Information > Software Environment > Drivers ).
Проанализировав содержимое этих трех окон, вы заметите, что они во многом совпадают.
Вышеупомянутый раздел реестра содержит подразделы, обозначенные внутренним именем драйвера или службы. Соответственно, каждый подраздел содержит сведения о конфигурации драйвера или службы.
Рассмотрим минимально возможный набор параметров, необходимых для запуска драйвера. Более подробно можно почитать тут: Windows 2000 DDK > Setup, Plug Play, Power Management > Design Guide > Reference > Part3: Setup > 1.0 INF File Sections and Directives > INF AddService Directive . В качестве примера, возьмем простейший драйвер режима ядра beep.sys (о нем самом мы поговорим в следующий раз). Подраздел реестра соответствующий этому драйверу и его содержимое представлен на рис 2-1.
Рис. 2-1. Подраздел реестра для драйвера beep.sys
Автоматический запуск драйвера иногда называют статической загрузкой, по аналогии со статически загружаемыми VxD в Windows 9x. Это не правильно, т.к. автоматически загруженный драйвер можно выгрузить и удалить из базы данных SCM в любой момент.
Программа управления службой
Как следует из самого названия, программа управления службой (далее SCP) призвана выполнять некие действия по отношению к драйверу. Делает она это под наблюдением SCM, вызывая соответствующие функции. Все они экспортируются модулем \%SystemRoot%\System32\advapi.dll (Advanced API).
Вот код простейшей SCP, которая будет управлять драйвером beep.sys. Находится в файле scp.asm.
Читайте также: