РАСЧЕТНО-ПОЯСНИТЕЛЬНАЯ ЗАПИСКА
к курсовому проекту на тему:
Профилировщик приложений
Содержание
1. Введение
2. Аналитический раздел
2.1. Техническое задание
2.2. Обзор архитектуры Windows NT 5.x
2.3. Классификация драйверов
2.4. Общая структура Legacy-драйвера
2.4.1. Процедура DriverEntry
2.4.2. Процедура DriverUnload
2.4.3. Рабочие процедуры обработки IRP-пакетов
2.4.3.1. Заголовок IRP пакета
2.4.3.2. Стек IRP-пакета
2.4.3.3. Функция обработки пакетов IRP_MJ_CREATE
2.4.3.4. Функция обработки пакетов IRP_MJ_CLOSE
2.4.3.5. Функция обработки пакетов IRP_MJ_DEVICE_CONTROL
2.4.4. ISR – процедура обработки прерываний
2.4.5. DPC – процедура отложенного вызова
3. Конструкторский раздел
3.1. Legacy-драйвер
3.1.1. Процедура DriverEntry
3.1.2. DriverUnload
3.1.3. DispatchCreate и DispatchClose
3.1.4. DispatchDeviceControl
3.1.4.1. IOCTL_LAST_CLIENT
3.1.4.2. IOCTL_LOCK_INFO и IOCTL_UNLOCK_INFO
3.1.4.3. IOCTL_PROCESS_FIRST и IOCTL_PROCESS_NEXT
3.1.4.4. IOCTL_THREAD_FIRST и IOCTL_THREAD_NEXT
3.1.4.5. IOCTL_OPEN_THREAD
3.1.4.6. IOCTL_CLOSE_THREAD
3.1.4.7. IOCTL_GET_THREAD_CONTEXT
3.2. Пользовательское приложение
4. Технический раздел
4.1. Выбор операционной системы и среды программирования.
4.2. Интерфейс
4.3. Системные требования
5. Заключение.
6. Список использованной литературы.
1. Введение
Очень часто при разработке программного обеспечения возникает необходимость, проследить за его работой: сколько времени его потоки выполняются в режиме ядра, сколько – в пользовательском режиме, сколько времени они проводят в ожидании, а также количество переключений контекста из одного режима в другой. Всё это важно, так как каждый из режимов имеет свои особенности. В режиме ядра код выполняется быстрее, но существует потенциальная возможность повреждения данных/кода системы. В противоположность режиму ядра, пользовательский режим ограничен в предоставляемых ему сервисах так, чтобы его код не мог привести к краху системы. Для этой же цели в пользовательском режиме выполняются дополнительные проверки, позволяющие предотваратить выполнение вредоносных инструкций. Поэтому скорость выполнения кода пользовательского режима существенно ниже. Количество переключений контекста тоже влияет на скорость выполнения кода, так как это операция является довольно дорогостоящей (около 2000 тактов). Это было хорошо заметно при разработке лабораторных работ и курсового проекта по машинной графике: при рисовании изображения попиксельно с помощью функции SetPixel, скорость прорисовки была несоизмеримо меньше, чем при использовании буфера пользовательского режима, в который постепенно заносилась информация о цвете соответствующих элементам буффера пикселям. Это происходило засчёт того, что при использовании функции SetPixel происходило два переключения контекста (из пользовательского режима в режим ядра и обратно) на один пиксель, а при использовании буфера, хранящего контекстно независимое представление цвета, - те же два переключения, но один раз на прорисовку целого кадра.
Таким образом, возможность узнать вышеуказанную статистическую информацию о целевом программном обеспечении, позволит своевременно заметить так называемые «узкие» места в программе, которые мешают улучшению производительности приложения в целом.
2. Аналитический раздел
2.1 Техническое задание
Целью представленного курсового проекта является разработка простого профилировщика приложений, который в себя включает:
Legacy-драйвер, который должен:
Периодически обновлять информацию о процессах и их потоках;
Предоставлять базовую информацию о процессах и их потоках;
Предоставлять аппаратный контекст выбранного потока;
Обеспечивать безопасное обращение к этой информации от нескольких пользовательских приложений-клиентов.
Пользовательское приложение, позволяющее:
Корректно устанавливать и удалять драйвер без необходимости перезагрузки системы;
Обращаться к драйверу с запросами для получении различной информации.
2.2 Обзор архитектуры Windows NT 5.x
В Windows приложения отделены от самой операционной системы. Код её ядра выполняется в привилегированном режиме процессора (режим ядра
), который обеспечивает доступ к системным данным и оборудованию. Код приложений выполняется в непривилегированном режиме процессора (пользовательский режим
) с неполным набором интерфейсов, ограниченным доступом к системным данным и без прямого доступа к оборудованию. Когда программа пользовательского режима вызывает системный сервис, процессор перехватывает вызов и переключает вызывающий поток в режим ядра. По окончании системного сервиса операционная система переключает контекст потока обратно в пользовательский режим и продолжает его выполнение.
Далее схематично показана та часть архитектуры Windows, которая затронута данным курсовым проектом. Здесь не указываются такие компоненты, как: процессы поддержки системы, процессы сервисов, подсистемы окружения, уровень абстрагирования от оборудования и поддержка окон и графики.
Пользовательские приложения — один из типов пользовательских процессов, выполняющихся в пользовательском режиме, в рамках которого они ограничены использованием непривилегированных инструкций.
DLL подсистем — в Windows пользовательские приложения не могут вызывать сервисы операционной системы напрямую, вместо этого они работают с одной или несколькими DLL подсистем, назначение которых заключается в трансляции документированных функций в соответствующие внутренние (и обычно недокументированные) вызовы системных сервисов.
Исполнительная система — содержит базовые сервисы операционной системы, которые обеспечивают управление памятью, процессами и потоками, защиту, ввод-вывод и взаимодействие между процессами.
Ядро — содержит низкоуровневые функции операционной системы, которые поддерживают, например, планирование потоков, диспетчеризацию прерываний и исключений. Оно также предоставляет набор процедур и базовых объектов, применяемых исполнительной системой для реализации структур более высокого уровня.
Драйверы — служат для расширения функциональных возможностей ядра.
2.3 Классификация драйверов
В отличие от пользовательского приложения, драйвер не является процессом и не имеет потока исполнения. Вместо этого управление драйверу передаётся в результате запроса на ввод/вывод от пользовательского приложения или драйвера, либо возникает в результате прерывания. В первом случае контекст исполнения драйвера точно известен - это прикладная программа. Во втором случае контекст исполнения может быть как известным, так и случайным - это зависит от контекста исполнения функции вызывающего драйвера. В третьем случае контекст исполнения случайный, поскольку прерывание (и, соответственно, исполнение кода драйвера) может произойти при выполнении любой прикладной программы.
По расположению в стеке драйверов:
Драйверы высшего уровня — получают запросы от пользовательского приложения и взаимодействуют с нижестоящими драйверами;
Промежуточные драйверы — получают запросы от вышестоящих драйверов и взаимодействуют с нижестоящими драйверами;
Драйверы низшего уровня — получают запросы от вышестоящих драйверов, осуществляют конечную обработку пакетов запросов.
Также выделяют понятие монолитного драйвера
– драйвера высшего уровня, не взаимодействующего ни с какими другими драйверами.
В связи с усовершенствованием модели драйверов Windows (WDM – Windows Driver Model), в которой были добавлены поддержка Plug and Play и энергосберегающие технологии, драйвера стали разделять на:
Унаследованные драйвера (Legacy-драйвера, драйвера «в стиле NT») — драйвера, написанные в старом манере, без поддержки нововведений;
WDM-драйвера – драйвера, которые удовлетворяют всем требованиям расширенной модели WDM.
2.4 Общая структура Legacy-драйвера
Legacy-драйвер имеет следующие основные точки входа:
DriverEntry – процедура загрузки драйвера;
DriverUnload – процедура выгрузки драйвера;
Рабочие процедуры обработки IRP-пакетов;
ISR-процедура (Interrupt Service Routine) – процедура обработки прерывания;
DPC-процедура (Deferred Procedure Call) – процедура отложенного вызова.
2.4.1 Процедура DriverEntry
Данная процедура присутствует в любом драйвере и вызывается диспетчером ввода/вывода при загрузке драйвера.
Legacy-драйверы выполняют в ней существенно большую работу, нежели WDM-драйвера, так как они вынуждены выполнять работу процедуры AddDevice, обязательной для WDM-драйверов. Помимо решения инициализационных задач и регистрации точек входа рабочих процедур обработки поддерживаемых IRP-пакетов и процедуры выгрузки драйвера, здесь:
Определяется аппаратное обеспечение, которое драйвер будет контролировать;
Создаются объекты устройств (функция IoCreateDevice) для каждого физического или логического устройства под управлением данного драйвера;
Для устройств, которые должны быть видимы пользовательским приложениям, создаются символьные ссылки (функция IoCreateSymbolicLink);
При необходимости, устройство подключается к объекту прерываний. В случае, если ISR-процедура требует использования DPC-процедуры, то соответсвующий ей объект создаётся и инициализируется на этом этапе;
Выделение памяти, необходимой для работы драйвера.
2.4.2 Процедура DriverUnload
Диспетчер ввода/вывода вызывает данну процедуру при динамической выгрузке драйвера. Эта процедура выполняет действия, «обратные» тем, что выполняются в процедуре DriverEntry.
Для Legacy-драйверов характерны следующие шаги:
Для некоторых типов аппаратуры необходимо сохранить ее состояние в системном реестре, т.к. при последующей загрузке драйвера эти данные могут быть использованы;
Если прерывания разрешены для обслуживаемого устройства, то процедура выгружки должна запретить их и произвести отключение от объекта прерываний. Ситуация, когда устройство будет порождать прерывания для несуществующего объекта прерывания, неминуемо приведет к краху системы;
Удаление символьной ссылки из пространства имен, видимого пользовательскими приложениями (IoDeleteSymbolicLink);
Удаление объекта устройства (IoDeleteDevice);
Освобждение памяти, выделенной драйверу в процессе работы.
2.4.3 Рабочие процедуры обработки IRP-пакетов
Все функции, зарегистрированные в процедуре DriverEntry путём заполнения массива MajorFunction, вызываются Диспетчером ввода/вывода для обработки соответсвующих запросов от клиентов драйвера. Эти запросы всегда оформлены в виде специальных структур данных – IRP-пакетов, память под которые выделяется Диспетчером ввода/вывода в нестраничном системном пуле. Структура IRP-пакета такова, что он состоит из заголовка фиксированного размера и IRP-стека, размер которого зависит от количества объектов устройств в стеке.
2.4.3.1 Заголовок IRP пакета
. Структура заголовка IRP-пакета имеет следующие поля:
Поле IoStatus типа IO_STATUS_BLOCK содержит два подполя:
Status содержит значение, которое устанавливает драйвер после обработки пакета;
В Information чаще всего помещается число переданных или полученных байт.
Поле AssociatedIrp.SystemBuffer типа PVOID содержит указатель на системный буфер для случая, если устройство поддерживает буферизованный ввод/вывод;
Поле MdlAddress типа PMDL содержит указатель на MDL-список, если устройство поддерживает прямой ввод вывод;
Поле UserBuffer типа PVOID содержит адрес пользовательского буфера для ввода/вывода;
Поле Cancel типа BOOLEAN - это индикатор того, что пакет IRP должен быть аннулирован.
2.4.3.2 Стек IRP-пакета. Основное назначение ячеек стека IRP-пакета состоит в том, чтобы хранить функциональный код и параметры запроса на ввод/вывод. Для запроса, который адресован драйверу самого нижнего уровня, соответствующий IRP пакет имеет только одну ячейку стека. Для запроса, который послан драйверу верхнего уровня, Диспетчер ввода/вывода создает пакет IRP с несколькими стековыми ячейками – по одной для каждого объекта устройства.
Каждая ячейка IRP-стека содержит:
MajorFunction типа UCHAR – это код, описывающий назначение операции;
MinorFunction типа UCHAR – это код, описывающий суб-код операции;
DeviceObject типа PDEVICE_OBJECT – это указатель на объект устройства, которому был адресован данный запрос IRP;
FileObject типа PFILE_OBJECT – файловый объект для данного запроса;
Parameters типа union – применение зависит от значения MajorFunction.
Диспетчер ввода/вывода использует поле MajorFunction для того, чтобы извлечь из массива MajorFunction нужную для обработки запроса процедуру.
Каждая процедура обработки IRP пакетов должна в качестве параметров принимать:
Указатель на объект устройства, для которого предназначен IRP запрос;
Указатель на пакет IRP, описывающий этот запрос;
2.4.3.3 Функция обработки пакетов IRP_MJ_CREATE
. Данная функция предназначена для обработки запросов на получение дескриптора драйвера от пользовательских приложений или вышестоящих драйверов. Как правило, эта функция просто помечает IRP-пакет, как завершённый.
2.4.3.4 Функция обработки пакетов IRP_MJ_CLOSE
. Данная функция предназначена для обработки запросов на закрытие дескриптора драйвера от пользовательских приложений или вышестоящих драйверов. Как правило, эта функция просто помечает IRP-пакет, как завершённый.
2.4.3.5 Функция обработки пакетов IRP_MJ_DEVICE_CONTROL
. Данная функция позволяет обрабатывать расширенные запросы от клиентов пользовательского режима, служат чаще всего для обмена данными между приложением и драйвером. Такой запрос может быть сформирован посредством вызова функции DeviceIoControl из пользовательского режима.
Здесь используются IOCTL-коды (I/O Control code), часть из которых предопределена операционной системой, а часть может создаваться разработчиком драйвера. Такой код задаётся в запросе Диспетчером ввода/вывода при формировании IRP-пакета.
Операции драйвера, которые работают с IOCTL-запросами, часто требуют задания буферной области для размещения входных или выходных данных. Возможна такая ситуация, когда в одном запросе используются оба буффера.
Метод доступа к данным, обеспечиваемый Диспетчером ввода/вывода, определяется в IOCTL-коде. Такими методами могут быть:
METHOD_BUFFERED: входной пользовательский буфер копируется в системный, а по окончании обработки системный копируется в выходной пользовательский буфер.
METHOD_IN_DIRECT: необходимые страницы пользовательского буфера загружаются с диска в оперативную память и блокируются. Далее с помощью DMA осуществляется передача данных между устройством и пользователем.
METHOD_OUT_DIRECT: необходимые страницы пользовательского буфера загружаются с диска в оперативную память и блокируются. Далее с помощью DMA осуществляется передача данных между устройством и пользователем.
METHOD_NEITHER: при данном методе передачи не производится проверка доступности памяти, не выделяются промежуточные буфера и не создаются MDL. В IRP-пакете передаются виртуальные адреса буферов в адресном пространстве инициатора запроса ввода/вывода.
В данном случае флаги, определяющие тип буферизации в объекте устройства, не имеют значения при работе с IOCTL запросами. Механизм буферизованного обмена определяется при каждом задании значения IOCTL в специально предназначенном для этого фрагменте этой структуры данных. Данный подход обеспечивает максимальную гибкость при работе с вызовом пользовательского режима DeviceIoControl.
С точки зрения драйвера, доступ к буферным областям, содержащим данные или предназначенным для данных, осуществляется с помощью следующих полей структур [1]:
METHOD_BUFFERED | METHOD_IN_DIRECT или METHOD_OUT_DIRECT | METHOD_NEITHER | |
Input Буфер с данными |
Использует буферизацию (системный буфер) Адрес буфера в системном адресном пространстве указан в pIrp->AssociatedIrp.SystemBuffer |
Клиентский виртуальный адрес в Parameters. DeviceIoControl. Type3InputBuffer | |
Длина указана в Parameters.DeviceIoControl.InputBufferLength | |||
Output Буфер для данных |
Использует буферизацию (системный буфер) Адрес буфера в системном адресном пространстве указан в pIrp-> AssociatedIrp. SystemBuffer |
Использует прямой доступ, клиентский буфер преобразован в MDL список, указатель на который размещен в pIrp->MdlAddress | Клиентский виртуальный адрес в pIrp->UserBuffer |
Длина указана в Parameters.DeviceIoControl.OutputBufferLength |
2.4.4 ISR
– процедура обработки прерываний
Эту функцию драйвер регистрирует, чтобы она получала управление в момент, когда аппаратура, обслуживаемая драйвером, передала сигнал прерывания. Задача этой функции выполнить минимальную работу и зарегистрировать процедуру отложенного вызова (DPC) для обслуживания прерывания. Вызов диспетчером прерываний ядра может произойти в любом контексте: как ядра, так и пользовательского процесса.
2.4.5 DPC – процедура отложенного вызова
Такие процедуры выполняются при более низком уровне запроса прерывания (IRQL), чем ISR, что позволяет не блокировать другие прерывания. В них может выполняться вся или завершаться начатая в ISR работа по обслуживанию прерываний.
3. Конструкторский раздел
Так выглядит схема взаимодействия пользовательского приложения с драйвером через компоненты системы:
3.1 Legacy-драйвер
В Legacy-драйвере данного курсового проекта реализованы следующие процедуры:
DriverEntry;
DriverUnload;
DispatchCreate (обработка IRP_MJ_CREATE-пакета);
DispatchClose (обработка IRP_MJ_CLOSE-пакета);
DispatchDeviceControl (обработка IRP_MJ_DEVICE_CONTROL-пакета).
3.1.1 Процедура DriverEntry
Здесь выполняются типичные для инициализации драйвера драйвера действия.
Регистрируются точки входа в драйвер:
pDriverObject->DriverUnload = SpectatorDriverUnload;
PDRIVER_DISPATCH * majorFunction = pDriverObject->MajorFunction;
majorFunction[ IRP_MJ_CREATE ] = SpectatorDispatchCreate;
majorFunction[ IRP_MJ_CLOSE ] = SpectatorDispatchClose;
majorFunction[ IRP_MJ_DEVICE_CONTROL ] = SpectatorDispatchDeviceControl;
Создаётся объект устройства с именем DEVICE_NAME:
#define DEVICE_NAME L"DeviceSpectator"
RtlInitUnicodeString( &deviceName, DEVICE_NAME );
status = IoCreateDevice
(pDriverObject,
sizeof( DEVICE_EXTENSION ),
&deviceName,
FILE_DEVICE_SPECTATOR,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&pDeviceObject);
Для созданного обекта устройства регистрируется символьная ссылка SYMBOLIC_LINK:
#define SYMBOLIC_LINK L"DosDevicesSpectator"
RtlInitUnicodeString( &symbolicLink, SYMBOLIC_LINK );
status = IoCreateSymbolicLink( &symbolicLink, &deviceName);
Создаётся объект ядра мьютекс:
NTSTATUS CreateMutex()
{BEGIN_FUNC( CreateMutex );
NTSTATUS status = STATUS_SUCCESS;
status = _ExAllocatePool( g_pMutex, NonPagedPool, sizeof( KMUTEX ) );
if ( NT_SUCCESS( status ) )
{KeInitializeMutex( g_pMutex, 0 );
status = STATUS_SUCCE
END_FUNC( CreateMutex );
return ( status );}
Впервые загружается информация о процессах и их потоках:
if ( LockInfo() == STATUS_SUCCESS )
{ReloadInfo();
UnlockInfo();}
Функции LockInfo() и UnlockInfo() являются просто напросто функциями-обёртками для функций LockMutex() и UnlockMutex() соответственно. Первая из последних двух функций ожидает на объекте ядра мьютекс.
Объекты ядра «мьютексы» гарантируют потокам взаимоисключающий доступ к един ственному ресурсу. Отсюда и произошло название этих объектов (mutual exclusion, mutex). Они содержат счетчик числа пользователей, счетчик рекурсии и переменную, в которой запоминается идентификатор потока. Мьютексы ведут себя точно так же, как и критические секции. Однако, если последние являются объектами пользователь ского режима, то мьютексы — объектами ядра. Кроме того, единственный объект-мью текс позволяет синхронизировать доступ к ресурсу нескольких потоков из разных процессов; при этом можно задать максимальное время ожидания доступа к ресурсу.
Именно благодаря этому мьютексу обеспечивается требование по безопасности при обращении к хранимой информации.
Инициализируется работа таймера:
Таймер необходим для того, чтобы с определённым интервалом обновлять хранимую информацию.
Для этого создаётся объект ядра «таймер»:
status = _ExAllocatePool( g_pTimer, NonPagedPool, sizeof( KTIMER ) );
KeInitializeTimerEx( g_pTimer, SynchronizationTimer );
Замечание
: память под объекты ядра должна выделяться исключительно в нестраничном пуле (ключевое слово NonPagedPool).
Таймеры могут быть двух типов:
SynchronizationTimer — по истечении указанного временного интервала или очередного периода, он переводится в сигнальное состояние, пока один из потоков, ждущих его, не будет пробуждён. Тогда же таймер переводится в несигнальное состояние.
NotificationTimer — по истечении указанного временного интервала или очередного периода, он переводится в сигнальное состояние, причём пробуждаются все потоки ожидающие на нём. Такой таймер остаётся в сигнальном состоянии до тех пор, пока он не будет явно переведён в несигнальное.
Для того, чтобы выполнять какую-то полезную работу по таймеру, необходимо зарегистрировать DPC-процедуру OnTimer(). Для неё необходимо создать собственный DPC-объект, который будет периодически ставится в общесистемную очередь:
status = _ExAllocatePool( g_pTimerDpc, NonPagedPool, sizeof( KDPC ) );
KeInitializeDpc( g_pTimerDpc, OnTime, NULL );
Далее, в силу того, что в данном драйвере по таймеру должны выполняться действия, требующие пользовательского контекста, необходимо их вынести из функции OnTimer(), которая является DPC-процедурой, а следовательно, во время её выполнения доступен лишь системный контекст. Тем не менее, необходимо обеспечить приемлемую синхронность выполнения необходимой работы с моментом извлечения DPC-объекта функции из очереди для обработки. Для этого создадим поток, который будет посвящён ожиданию некоторого события:
OBJECT_ATTRIBUTES objectAttributes;
InitializeObjectAttributes( &objectAttributes, NULL, OBJ_KERNEL_HANDLE,
NULL, NULL );
status = PsCreateSystemThread( &hThread, THREAD_ALL_ACCESS, &objectAttributes,
NULL, NULL, UpdateThreadFunc, NULL );
KeInitializeEvent( g_pUpdateEvent, SynchronizationEvent, FALSE );
Замечание
: объекты ядра «события» по своему типу идентичны объектам ядра «таймер».
При поступлении этого события поток будет обновлять системную информацию о процессах и их потоках. Объект этого события будем переводить в сигнальное состояние в функции OnTimer(). Данный способ синхронизации позволил обеспечить выполнение необходимых действий через заданный интервалом с точностью до милисекунды, что следует из нижеприведённых сообщений, перехваченных программой DebugView от отладочной версии драйвера:
0.00075233 [Spectator] ^^^^^^^^ OnTime ^^^^^^^^
0.00116579 [Spectator] ======== LockInfo ========
0.00118814 [Spectator] ======== ReloadInfo ========
0.99727142 [Spectator] ^^^^^^^^ OnTime ^^^^^^^^
1.00966775 [Spectator] ======== LockInfo ========
1.00968981 [Spectator] ======== ReloadInfo ========
1.99729049 [Spectator] ^^^^^^^^ OnTime ^^^^^^^^
2.05610037 [Spectator] ======== LockInfo ========
2.05632067 [Spectator] ======== ReloadInfo ========
2.99727035 [Spectator] ^^^^^^^^ OnTime ^^^^^^^^
2.99741030 [Spectator] ======== LockInfo ========
2.99743295 [Spectator] ======== ReloadInfo ========
3.99727631 [Spectator] ^^^^^^^^ OnTime ^^^^^^^^
3.99739385 [Spectator] ======== LockInfo ========
3.99741673 [Spectator] ======== ReloadInfo ========
4.99728107 [Spectator] ^^^^^^^^ OnTime ^^^^^^^^
4.99742365 [Spectator] ======== LockInfo ========
4.99744749 [Spectator] ======== ReloadInfo ========
5.99728870 [Spectator] ^^^^^^^^ OnTime ^^^^^^^^
5.99742651 [Spectator] ======== LockInfo ========
5.99744844 [Spectator] ======== ReloadInfo ========
Здесь OnTime – момент входа в процедуру таймера OnTimer, LockInfo – момент, когда пробудился поток, отвечающий за обновление информации, ReloadInfo – момент, когда информация была действительно обновлена.
Как видно из перехвата, в первые две секунды периодичность не на высоком уровне, но потом ситуация стабилизируется и точность улучшается, как и было заявлено, до одной миллисекунды.
После всех этих действий, наконец, запускается таймер:
LARGE_INTEGER dueTime = RtlConvertLongToLargeInteger( 0 );
BOOLEAN existed = KeSetTimerEx( g_pTimer, dueTime, g_timerPeriod, g_pTimerDpc );
Здесь dueTime – время до первого вызова процедуры OnTime(), а g_timerPeriod – период дальнейших вызовов.
Вконце концов, в процедуре DriverEntry происохдит обнуление счётчика пользовательских приложений-клиентов, получивших описатель данного драйвера: pDeviceExtension->clientCount = 0;
Благодаря одной этой переменной становится возможным одновременное обращение к драйверу сразу нескольких пользовательских приложений. Единственным ограничением для них ялвяется эксклюзивность доступа к информации о процессах и их потоках.
3.1.2 DriverUnload
В этой процедуре, если число клиентов драйвера равно нулю, происходит удаление всех объектов созданных для организации работы таймера, удаление мьтекса, объекта устройства и его символьной ссылки. Если же число клиентов отлично от нуля, то драйвер не выгружается, так как, в противном случае, это нарушит нормальную работу других пользовательских приложений-клиентов.
3.1.3 DispatchCreate и DispatchClose
В этих функциях происходит учёт количества открытых описателей данного драйвера полученных с помощью API-вызова CreateFile(). Сколько описателей было открыто – столько же должно быть закрыто API-вызовом CloseHandle(). Иначе драйвер по окончании работы пользовательского приложения останется в операционной системе, что, естественно, крайне не желательно.
3.1.4 DispatchDeviceControl
Эта процедура обслуживает IOCTL-запросы от пользовательских приложений посылаемые API-вызовом DeviceIoControl(). В данном курсовом проекте взаимодействие с драйвером большею частью и построено на их применении, здесь реализована основная функциональность драйвера: то, для чего он и предназначался. Поэтому данная процедура наиболее объёмна.
Сначала, назависимо от конкретного IOCTL-запроса, получается указатель на ячейку IRP-стека IRP-пакета, предназначенную для объекта устройства драйвера:
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
Далее, из этой ячейки извлекается код IOCTL-запроса, на основе которого с помощью оператора switch происходит дополнительная диспетчеризация IRP-пакета.
В рассматриваемом драйвере все IOCTL-запросы используют буферизованный метод передачи данных, так как во всех запросах их объём действительно невелик. При таком подходе передачи данных в системном нестраничном пуле выделяется столько памяти, чтобы поместился больший из входного и выходного буферов. Перед началом обработки запроса содержимое входного пользовательского буфера копируется в системный буфер, а по завершении из системного – в выходной пользовательский буфер. Так как для обоих пользовательских буферов используется всего лишь один системный, то необходимо быть аккуратным при обработке данных, так как есть вероятность при записи повредить ещё непрочитанные входные данные и тогда они будут утеряны навсегда.
Длины (в байтах) пользовательских буферов, входного и выходного, извлекаются из поля Parameters ячейки IRP-стека: Parameters.DeviceIoControl.InputBufferLength и Parameters.DeviceIoControl.OutputBufferLength соответственно. А адрес системного буфера извлекается из заголовка IRP-пакета: AssociatedIrp.SystemBuffer.
3.1.4.1 IOCTL_LAST_CLIENT
. Входные данные
: [нет]
Выходные данные
: [нет]
Данный IOCTL-запрос служит для обращения к драйверу, чтоб тот дал ответ на вопрос является ли инициатор запроса единственным клиентом, работающим с драйвером на данный момент. Этот запрос посылается драйверу каждым пользовательски приложением, когда оно собирается вот-вот завершиться. Если ответ положительный, то это приложение пытается завершить работу драйвера, иначе оно просто завершается, будучи уверенным, что есть другие клиенты, работающие с драйвером и что то приложение, которое будет завершаться последним, позаботится о выгрузке драйвера.
3.1.4.2 IOCTL_LOCK_INFO
и IOCTL_UNLOCK_INFO. Входные данные
: [нет]
Выходные данные
: [нет]
Первый IOCTL-запрос из этой служит для захвата пользовательским приложением системной информации в монопольное пользование. Другой – соответствено, для осовбождения этого ресурса. В них просто вызываются одноимённые функции LockInfo() и UnlockInfo(), о которых было рассказано ранее, когда речь шла о процедуре DriverEntry данного раздела.
3.1.4.4 IOCTL_PROCESS_FIRST
и IOCTL_PROCESS_NEXT. Входные данные
: [нет]
Выходные данные
: структура с базовой информацие о процессе.
Эта пара IOCTL-запросов позволяет их инициатору последовательно проссматривать структуры, описывающие запущенные процессы в системе. Каждый из них вызывает одноимённую функцию ProcessFirst() и ProcessNext() соответственно. Первая функция устанавливает указатель на первую запись, а вторая перемещает указатель на следующую, если такая имеется. Результатом выполнения каждой из этих функций является заполненная структура с информацией оп процессе, если не достигнут конец списка. В том случае, когда конец списка всё-таки достигается, IRP-пакет, тем не менее, помечается как успешно обработанный, но значение количества переданных байтов устанавливается равным нулю, что и позволяет пользовательскому приложению правильно распознать такую ситуацию и своевременно прекратить посылать драйверу дальнейшие IOCTL_PROCESS_NEXT-запросы.
3.1.4.5 IOCTL_THREAD_FIRST
и IOCTL_THREAD_NEXT. Входные данные
: [нет]
Выходные данные
: структура с базовой информацие о потоке.
Как и в предыдущем пункте, эта пара IOCTL-запросов позволяет их инициатору последовательно проссматривать структуры, описывающие потоки выбранного процесса. Логика обработки этих запросов аналогична получению информации о процессах.
3.1.4.6 IOCTL_OPEN_THREAD
. Входные данные
: права доступа, уникальный идентификатор целевого потока.
Выходные данные
: описатель целевого потока.
При обработке данного IOCTL-запроса осуществляется попытка открыть описатель потока, имеющего указанный идентификатор с правами, которые были запрошены пользовательским приложением-клиентом.
3.1.4.6 IOCTL_CLOSE_THREAD
. Входные данные
: описатель целевого потока.
Выходные данные
: [нет].
Во время обработки этого IOCTL-запроса предпринимается попытка закрыть описатель потока, открытый ранее с помощью IOCTL_OPEN_THREAD-запроса.
3.1.4.7 IOCTL_GET_THREAD_CONTEXT
. Входные данные
: структура аппаратного контекста, описатель целевого потока.
Выходные данные
: структура аппаратного контекста.
Этот IOCTL-запрос наиболее полно использует возможности API-вызова DeviceIoControl, так как здесь задействованы оба, входной и выходной, буферы. На вход поступает структура для аппаратного контекста с инициализированным полемы CONTEXT::ContextFlags, указывающим какие группы регистров аппаратного контекста должны быть возвращены в этой структуре при удачном завершении запроса. В этом проекте запрашивается весь аппаратный контекст.
3.2 Пользовательское приложение
Пользовательское приложение включает в себя два класса: CDialog и CDriver. Как понятно из названий эти классы отвечают соответственно за взаимодействие с пользователем через диалоговое окно приложения и взаимодействие с драйвером преимущественно через IOCTL-запросы.
При запуске экземпляр пользовательского приложения первым делом пытается установить драйвер, в том случае, если это не было сделано ранее другим экземпляром. Если установка вызвала ошибку, то пользователю выдаётся соответствующее сообщение, в котором в текстовом виде указывается причина её возникновения, если она была предусмотрена, иначе – просто указывается её код. Пользователь может запросить установку драйвера ещё раз, дав положительный ответ на соответствующее предложение программы. Такая процедура повторятся до тех пор, пока установка драйвера не пройдёт успешно либо пользователь откажется от повторной попытки.
После этого загружается ниспадающий список запущенных процессов, отсортированных в алфавитном порядке по своим именам, выбирается первый процесс из списка, и уже его потоки отображаются во втором ниспадающем списке. Эти списки обновляются каждый раз, когда пользователь хочет выбрать другой процесс или поток, так как для этого ему нужны последние сведения.
Далее создаётся таймер, работа которого никак не связана с работой таймера драйвера. Этот таймер отвечает за периодичный вывод полученной от драйвера информации на форму диалога.
Эта информация получается через драйвера, как уже говорилось, с помощью API-вызова DeviceIoControl:
BOOL DeviceIoControl
(HANDLE ,
DWORD ,
LPVOID , DWORD ,
LPVOID , DWORD ,
LPDWORD ,
LPOVERLAPPED );
HANDLE – описатель устройства, которому посылается запрос;
DWORD – код IOCTL-запроса;
LPVOID – адрес входного буфера;
DWORD – длина входного буфера;
LPVOID – адрес выходного буфера;
DWORD - длина выходного буфера;
LPDWORD – количество переданных байтов;
LPOVERLAPPED – структура, необходимая при использовании асинхронного выполнения запроса, чего нет в данном приложении.
Использование этого API-вызова полностью инкапсулировано в классе CDriver, в котором для выполнения каждого запроса реализован отдельный метод с именем, близким к названию IOCTL-запроса, что обеспечивает интуитивное понимание интерфейса этого класса.
Также этот класс инкапсулирует в себя использование Менеджера управления сервисами (SCM - Service Control Manager), с помощью которого осуществляется динамическая установка, запуск, останов и удаление драйвера.
4. Технический раздел
4.1 Выбор операционной системы и среды программирования
В качестве операционной системы была выбрана система Widows. Это обусловлено тем, что операционная система DOS уже устарела в силу многих причин (мы уже ушли от ОС, работающих в однозадачном режиме), а других операционных систем для персональных машин с хорошим интерфейсом, действительно удобных для пользователя, еще нет. Windows по прежнему остается самой распространенной ОС для ПК. Кроме того различные среды разработки программных продуктов разработаны именно под Windows:
Visual C++, Visual Basic, Borland C++ Builder, Delphi и другие.
Языком написания пользовательской программы был выбран С++. Язык С++ дает очень богатые возможности для программистов и, пожалуй является наиболее распространенным в их среде. Это очень мощный операторный язык. Кроме того, он обеспечивает достаточную свободу в написании программ, в то время как Pascal ставит очень узкие рамки, в частности, в описании переменных и не дает возможности построения сложных операторных выражений. Языком написания драйвера был выбран С. Применение этого языка обеспечивает переносимость меджу системами: максимум, что придётся сделать – это пересобрать драйвер. В качестве среды разработки была выбрана Microsoft Visual Studio .Net, поскольку она дает мощные и удобные средства не только визуальной разработки интерфейса программного продукта, но и настройки проектов, что позволяет эффективно организовать своё рабочее место.
4.2 Интерфейс
Так выглядит окно экземпляра пользовательского приложения «Профилировщик»:
В верхней части диалога находятся два ниспадающих списка, верхний из которых отображает список запущенных процессов в системе, а нижний – список потоков этого процесса. С помощью этих элементов управления можно указать приложению, за каким процессом и каким потоком этого процесса вести наблюдение.
На диалоге есть три группы:
Группа «Информация о процессе»:
ProcessID – идентификатор процесса;
ParentID – идентификатор процесса-родителя;
BasePriority – базовый приоритет по-умолчанию для потоков процесса;
ThreadCount – количество потоков процесса;
KernelTime – суммарное время, проведённое в режиме ядра потоками процесса, 1 единица равна 100 нс;
UserTime - суммарное время, проведённое в пользовательском режиме потоками процесса, 1 единица равна 100 нс.
Группа «Информация о потоке»:
ThreadID – идентификатор потока;
BasePriority – базовый приоритет потока;
Priority – приоритет потока;
ContextSwitches – количество переключений контекста, осуществлённых потоком;
KernelTime –время, проведённое в режиме ядра (1 единица равна 100 нс);
UserTime - время, проведённое в пользовательском режиме (1 единица равна 100 нс).
WaitTime – момент времени, когда поток перешёл в состояние ожидания (отсчёт ведётся от момента запуска системы).
Группа «Контекст потока»:
Здесь представлен аппаратный контекст потока. Большинство приложений ожидают ввода от пользователя. При наблюдении за потоками такого процесса можно вообще не увидеть какие-либо изменения. Поэтому для более наглядного просмотра стоит запускать задачи, требующие больших вычислительных затрат. Например, WinAmp, с помощью которого можно проигрывать музыку – тот поток, который за это отвечает, сразу виден по изменению регистров общего назначения. Но наиболее частые изменения в регистрах различного назначения происходят в по-настоящему «тяжеловесных» задачах, к примеру, можно взять курсовой проект по Машинной графике.
4.3 Системные требования
Драйвер написан с расчётом на Windows NT версии 5.x.
Обработка запросов от несколькоих пользовательских приложений-клиентов проверена только на Windows XP Service Pack 2.
Заключение
В результате работы над проектом были реализованы пользовательское приложение, взаимодействующее с Legacy-драйвером. С его помощью оно получает базовую информацию о выбранном процессе, базовую информацию и аппаратный контекст выбранного потока указанного процесса. Это приложение является базой для реализации полноценных профилировщиков приложений для трассировки целевых приложений и для обнаружения в них узких мест, что может существенно повысить эффективность труда программиста и разрабатываемого им программного обеспечения.
Список использованной литературы
1. В.П.Солдатов «Программирование драйверов Windows». Изд. 3-е, перераб. и доп. — М.: ООО «Бином-Пресс», 2006 г. — 576 с.: ил.
2. М.Руссинович, Д.Соломон «Внутреннее устройство Microsoft Windows: Windows Server 2003, Windows XP и Windows 2000», 4-е издание.
3. Дж.Рихтер «Windows для профессионалов: создание эффективных Win32 приложений с учетом специфики 64-разрядной версии Windows»/Пер, англ - 4-е изд. - СПб; Питер; М.: Издательско-торговый дом "Русская Редакция", 2001.
4. Schreiber, Sven B., 1958-Undocumented Windows 2000 secrets: a programmer's cookbook.
5. Garry Nebbett, Windows NT/2000 Native API.