[Previous] [Next]

Функции для работы с файлами

Невозможно представить полноценное средство разработки кода, если в нем нет способов работы с файлами. Для чего файлы могут понадобиться в драйвере? Два примера, первыми приходящие на ум: необходимость считывания кода прошивок внешних устройств (которые могут быть размером до десятков килобайт) и ведение протоколирования в файле на диске.

Для открытия файла из драйвера режима ядра используется универсальная функция ZwCreateFile. Универсальность этого вызова состоит в том, что с его помощью производится и открытие существующих файлов, и создание новых.

Специфика системной функции ZwCreateFile состоит в том, что она имеет протокольных параметров даже больше, чем пользовательский вызов CreateFile. Существенная часть входной информации об открываемом объекте поступает внутри структуры OBJECT_ATTRIBUTES, которую следует предварительно создать и заполнить соответствующим корректными данными. Для ведения учетной информации открытого объекта используется структура данных IO_STATUS_BLOCK, которую следует предоставить при вызове (инициализировать ее не следует).

Поскольку вызов ZwCreateFile имеет большое разнообразие флаговых значений практически для каждого из входных параметров, ограничимся кратким общим обзором, представленным в таблице ниже. Более полную информацию обо всех возможных значения параметров вызова ZwCreateFile следует получить из документации пакета DDK. (Джозеф Ньюкамер уделил в своей книге описанию этой функции 7 страниц!)

Таблица 7.39. Прототип вызова ZwCreateFile

NTSTATUS ZwCreateFile IRQL == PASSIVE_LEVEL
Параметры Предоставляет доступ к системным ресурсам (в том числе файлам) в режиме ядра
OUT PHANDLE pHandle Указатель на переменную, куда следует поместить дескриптор открытого объекта (файла, подраздела реестра и т.п.)
IN ACCESS_MASK DesiredAccess Характеристика доступа к объекту. Для файлов вполне приемлемы значения GENERIC_READ или GENERIC_WRITE, которые представляют сложные комбинации из более простых масок доступа (типа FILE_APPEND_DATA и т.п.)
IN POBJECT_ATTRIBUTES pObjAttributes Указатель на заполненную вызывающим кодом структуру данных, которая описывает имя, местоположение и некоторые другие характеристики открываемого объекта (см. ниже)
OUT PIO_STATUS_BLOCK
pIOStatus
Указатель на буфер, в котором будет размещена информация об открытом объекте в формате структуры IO_STATUS_BLOCK
IN PLARGE_INTEGER
AllocationSize OPTIONAL
Начальный размер файла в байтах. Ненулевое значение принимается во внимание только при создании и перезаписи файла
IN ULONG FileAttributes Атрибуты открываемого файла. Типовым является значение FILE_ATTRIBUTE_NORMAL
IN ULONG SharedAccessFlags Описывает, разрешен ли совместный доступ, например, FILE_SHARE_READ — для чтения
IN ULONG CreateDispositionFlags Способ открытия файла, например, FILE_OPEN_IF — если не существует, создать
IN ULONG CreateOptions Комбинация флагов создания, например, FILE_SYNCHRONOUS_IO_NONALERT — все операции над файлом выполняются как синхронные (DesiredAccess должен включать флаг SYNCHRONIZE)
IN PVOID EaBuffer OPTIONAL Для драйверов устройств и драйверов средних слоев следует указывать NULL
IN ULONG EaLength Для драйверов устройств и драйверов средних слоев следует указывать 0
Возвращаемое значение STATUS_SUCCESS или код ошибки (несколько более подробную информацию можно найти в структуре IO_STATUS_BLOCK)

Поле pIOStatus->Information (структуры IO_STATUS_BLOCK) может после вызова иметь одно из следующих значений: FILE_CREATED, FILE_OPENED, FILE_OVERWRITTEN, FILE_SUPERSEDED, FILE_EXISTS или FILE_DOES_NOT_EXIST.

Ниже приводится пример создания (или открытия — при повторных обращениях) файла C:\Example\testfile.txt и запись в него строки " string : test write!\"

NTSTATUS          status;
UNICODE_STRING    fullFileName;
HANDLE            fileHandle;
IO_STATUS_BLOCK   iostatus;
OBJECT_ATTRIBUTES oa;


RtlInitUnicodeString( &fullFileName,
                      L"\\??\\C:\\Example\\testfile.txt");

InitializeObjectAttributes( &oa,
                            &fullFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
                            NULL,
                            NULL );

status = ZwCreateFile ( &fileHandle,
                        GENERIC_WRITE | SYNCHRONIZE,
                        &oa,
                        &iostatus,
                        0,  // alloc size = none
                        FILE_ATTRIBUTE_NORMAL,
                        FILE_SHARE_WRITE,
                        FILE_OPEN_IF,
                        FILE_SYNCHRONOUS_IO_NONALERT,
                        NULL,
                        0);
// Здесь:
// GENERIC_WRITE равно STANDARD(0x40000000L)
//
// FILE_GENERIC_WRITE равно STANDARD_RIGHTS_WRITE|FILE_WRITE_DATA |
//    FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA |
//    SYNCHRONIZE, что можно увидеть в заголовочном файле winnt.h

if( NT_SUCCESS(status))
{
	// Строка для записи в файл
	char myString[100]="string : test write!\r\n";
	// Структура, которая поможет определить длину файла:
	FILE_STANDARD_INFORMATION fileInfo;

	status =        // Получаем информацию о файле
 		ZwQueryInformationFile( fileHandle,
                               &iostatus,
                               &fileInfo,
                               sizeof(FILE_STANDARD_INFORMATION),
                               FileStandardInformation
                               );
	ULONG len = strlen(myString);
	if( NT_SUCCESS(status) )
	{
		LARGE_INTEGER ByteOffset = fileInfo.EndOfFile;
		status = ZwWriteFile(fileHandle,
                            NULL,
                            NULL,
                            NULL,
                            &iostatus,
                            myString, len,   // Записываемая строка
                            &ByteOffset,     // a если NULL? см. ниже
                            NULL);
		if( !NT_SUCCESS(status) || iostatus.Information != len )
		{
			DbgPrint("Error on writing. Status = %x.", status);
		}
	}
	ZwClose(fileHandle);
break;

}

Следует заметить, что без магической комбинации "\\??\\" в начале имени файла, вызов ZwCreateFile непременно возвратит ошибку. Этот префикс является обязательным для файлов на диске.

Таблица 7.40. Прототип вызова ZwWriteFile

NTSTATUS ZwWriteFile IRQL == PASSIVE_LEVEL
Параметры Производит модификацию объекта (файла), указанного открытым дескриптором
IN HANDLE FileHandle Дескриптор открытого для модификации файлового объекта
IN HANDLE Event OPTIONAL Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
IN PIO_APC_ROUTINE
pApcRoutine OPTIONAL
Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
IN PVOID ApcContext OPTIONAL Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
OUT PIO_STATUS_BLOCK pIoStatusBlock

В поле pIoStatusBlock->Information по завершении вызова находится число реально записанных байт

IN PVOID Buffer Буфер с данными для записи
IN ULONG Length Размер записываемой порции данных
IN PLARGE_INTEGER pByteOffset OPTIONAL Указатель на переменную, где содержится смещение в файле (от начала), по которому следует производить запись данных
IN PULONG Key OPTIONAL Драйверы устройств и драйверы средних слоев должны установить этот параметр равным NULL
Возвращаемое значение STATUS_SUCCESS или код ошибки

При помощи ZwCreateFile можно получать и доступ к драйверам, как это делается из вызова CreateFile пользовательского режима. Таким образом, в частности, предлагается получать доступ к драйверу отладочной печати DebugPrint. Разумеется, вид имени для использования будет несколько другим. Для драйвера DebugPrint это будет имя L"\\Device\\PHDDebugPrint". По данному имени через ZwCreateFile можно получить доступ к драйверу из другого драйвера, несмотря на то, что символьная ссылка этим драйвером не создается. Аналогично, к нашему драйверу Example можно получить доступ из другого драйвера по имени L"\\Device\\Example", но ничего не получится, если пытаться использовать имя L"\\\\.\\Example", как в вызове CreateFile.

Рассмотрим подробнее некоторые функции, использованные в приведенном выше примере.

Макроопределение InitializeObjectAttributes используется для заполнения полей структуры OBJECT_ATTRIBUTES, что делает эту операцию компактнее. Это макроопределение вводится в заголовочном файле ntdef.h пакета DDK. Там же описана и внутренняя организация OBJECT_ATTRIBUTES.

Собственно запись в файл в приведенном выше примере выполняется системной функцией ZwWriteFile.

Способ вызова ZwReadFile во многом повторяет прототип для ZwWriteFile, приведенный в таблице 7.40.

Если параметр pByteOffset указан равным NULL, то в большинстве случаев это воспринимается как нулевое смещение, и запись была бы выполнена (скажем, в приведенном примере) с начала файла. Однако на это допущение лучше не полагаться. Например, если бы мы пытались произвести запись в упомянутое выше устройство PHDDebugPrint, то нас преследовали бы ошибки, пока значение *pByteOffset не было бы задано явно, то есть:

pByteOffset->QuadPart = 0i64;   // 0 для LARGE_INTEGER.

Упоминания в документации DDK XР, по поводу того, что при наличии в параметре DesiredAccess (при вызове ZwCreateFile для получения доступа к файлу) флага FILE_APPEND_DATA запись всегда производится в конец файла, мягко говоря, не совсем справедливы. При наличии этого флага (среди параметров вызова ZwCreateFile) значение ByteOffset играет по-прежнему ту же роль в вызове ZwWriteFile, что и 6eз использования этого флага при открытии файла.

В примере, представленном выше, для получения информации о размере файла был использован системный вызов ZwQueryInformationFile. Данный вызов достаточно универсален, и возвращаемая им информация зависит от его последнего параметра, который может принимать следующие значения:

Разумеется, для каждого типа вызова должна быть предоставлена своя структура, для чего предусмотрен предпоследний параметр, где при вызове указывается ее размер. То есть больше указанного таким образом размера вызов ZwQueryInformationFile использовать не может. Недостаток места для записи всей информации может быть причиной неудачного завершения.

Функции для работы со ссылками на объекты

В некоторых ситуациях необходимо получать доступ к объектам режима ядра. В операционной системе Windows объектами представлены многие компоненты: программные потоки, объекты синхронизации, файловые объекты, драйверы, устройства, объекты DPC процедур, прерываний и т.п. Правильнее говорить, что эти явления представлены структурами данных для удобства ведения их учета. Структуры эти в полной мере (например, в смысле ООП) объектами не являются. У них нет конструкторов, деструкторов, виртуальных методов в привычном смысле С++. Тем не менее, понятие "объект" широко используется и, в некотором смысле, справедливо — оперировать этими структурами предпочтительнее специально для того предназначенными системными вызовами, которые и можно считать методами для данных объектов.

Рассмотрим пример, в котором объектом синхронизации является программный поток. Программные потоки могут своим окончанием сигнализировать другим программным потокам о том, что пора приступать к работе. В частности, программный поток может быть запущен вызовом PsCreateSystemThread (см. главу 10). Другой поток может дожидаться его окончания, организовав ожидание при помощи одного из вызовов KeWaitForXxx. Возникает небольшое затруднение. Системный вызов PsCreateSystemThread, создавая поток, возвращает его дескриптор (HANDLE), a функции KeWaitForXxx принимают в качестве входного аргумента указатель на объект, по состоянию которого организуется ожидание.

Выход состоит в использовании вызова ObReferenceObjectByHandle, который в состоянии по корректному дескриптору возвратить указатель на соответствующий ему объект. Прототип ее вызова описан ниже.

Таблица 7.41. Прототип вызова ObReferenceObjectByHandle

NTSTATUS ObReferenceObjectByHandle IRQL == PASSIVE_LEVEL
Параметры Предоставляет указатель на объект режима ядра по открытому дескриптору
IN HANDLE Handle Исходный дескриптор объекта
IN ACCESS_MASK
DesiredAccess
Маска доступа, интерпретация которой зависит от типа рассматриваемого объекта
IN POBJECT_TYPE ObjectType
OPTIONAL
Можно установить NULL (если параметр AccessMode равен KernelMode) или одно из значений IoFileObjectType or ExEventObjectType
IN KPROCESSOR_MODE
AccessMode
KernelMode — для работы в режиме ядра
UserMode
OUT PVOID *ppObject Указатель на переменную типа PVOID, в которой будет возвращен указатель, соответствующий исходному дескриптору
OUT POBJECT_HANDLE_INFORMATION Handlelnfо OPTIONAL Указатель на структуру, в которой будет представлена дополнительная информация об объекте и правам доступа к нему
Возвращаемое значение • STATUS_SUCCESS
• STATUS_OBJECT_TYPE_MISMATCH
• STATUS_ACCESS_DENIED
• STATUS_INVALID_HANDLE

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

Общим правилом операционной системы является то, что счетчик ссылок поддерживается для каждого объекта режима ядра, и когда он уменьшается до нуля, происходит уничтожение объекта. В некоторых ситуациях, как раз необходимо произвести увеличение счетчика ссылок на объект, чтобы продлить ему жизнь. В режиме ядра более распространен способ работы с объектами через указатели на них. Поэтому, если необходимо увеличить счетчик ссылок на объект, но в распоряжении имеется только указатель на него — не беда. На этот случай существует системный вызов ObReferenceObjectByPointer, который увеличивает ссылку на объект, используя в качестве исходных данных указатель, а не дескриптор.

Таблица 7.42. Прототип вызова ObReferenceObjectByPointer

NTSTATUS ObReferenceObjectByPointer IRQL == PASSIVE_LEVEL
Параметры Предоставляет указатель на объект режима ядра по открытому дескриптору
IN PVOID pObject Указатель на объект
IN ACCESS_MASK DesiredAccess Маска доступа, интерпретация которой зависит от типа рассматриваемого объекта
IN POBJECT_TYPE ObjectType
OPTIONAL
Можно установить NULL (если параметр AccessMode равен KernelMode) или одно из значений IoFileObjectType or ExEventObjectType
IN KPROCESSOR_MODE AccessMode KernelMode — для работы в режиме ядра
UserMode
Возвращаемое значение • STATUS_SUCCESS
• STATUS_OBJECT_TYPE_MISMATCH

Вернемся к случаю с ожиданием, организованным по объекту программного потока одним из вызовов KeWaitForXxx. Когда поток завершился, ожидание прекращено. При этом объект потока еще не уничтожен, поскольку в результате вызова ObReferenceObjectByHandle получилось так, что число ссылок на него не равно нулю. Это положение можно изменить, если уменьшить число ссылок при помощи вызова ObDereferenceObject, что, вероятно, приведет и к удалению объекта потока из системы.

Таблица 7.43. Прототип вызова ObDereferenceObject

VOID ObDereferenceObject IRQL == PASSIVE_LEVEL
Параметры Уменьшает счетчик ссылок на объект
IN PVOID pObject Указатель на объект
Возвращаемое значение void

Заметим, что при выполнении вызова ZwClose происходит автоматическое уменьшение счетчика ссылок на объект и проверка, не пора ли его удалить из системы.

Функции для работы с системным представлением времени

Внутреннее время в Windows 2000/XP/Server 2003 хранится как число 100-наносекундных отсчетов с 1 января 1601 года. Это очень большое число, и для его хранения используется 64-разрядный тип данных в структуре, обозначенной как тип данных LARGE_INTEGER. В таблице 7.44 перечислены функции, предназначенные для работы с такими данными.

Таблица 7.44. Функции для работы с системным представлением времени

Функции Описание
KeQuerySystemTime Возвращает 64-разрядное значение абсолютного системного времени
KeQueryTickCount Возвращает число прерываний системного таймера (часов) с момента последней загрузки системы
KeQueryTimelncrement Возвращает число 100-нс интервалов, добавляемых к системному времени при каждом прерывании, поступающем от системного таймера (часов)
RtlTimeToTimeFields Разбивает 64-разрядное системное время на поля даты и времени
RtlTimeFieldsToTime Преобразует дату и время в 64-разрядное значение абсолютного системного времени
RtlConvertLongToLargeInteger Создает знаковое LARGE_INTEGER
RtlConvertULongToLargelnteger Создает положительное LARGE_INTEGER
RtlLargelntegerXxx

Различные арифметические и логические операции над типом данных LARGE_INTEGER (вместо этих функций в пакете DDK рекомендовано использовать встроенную поддержку компилятора для 64-разрядных операций)

Все приведенные функции могут вызваться на любом уровне IRQL. Прототип одной из них, вызова KeQuerySystemTime, приводится ниже.

Таблица 7.45. Прототип вызова KeQuerySystemTime

VOID KeQuerySystemTime IRQL == любой
Параметры Возвращает текущее время
OUT PLARGE_INTEGER
CurrentTime
Время в 100 нc интервалах с 1 января 1601 года
Возвращаемое значение void