Как создать драйвер для windows

Уровень сложности
Средний

Время на прочтение
10 мин

Количество просмотров 26K

Небольшая предыстория

Совсем недавно я перешёл на 3 курс технического университета, где на нашу группу вновь навалилась целая куча новых предметов. Одним из них являлся ИиУВМ(Интерфейсы и устройства вычислительных машин), на котором нам предстояло изучать, как те или иные части компьютера взаимодействуют между собой. Уже на 2 лабораторной работе нам выдали задание, суть которого заключалась в переборе всех устройств на PCI шине и получению их Vendor и Device ID через порты ввода-вывода. Для упрощения задачи преподаватель выдал ссылку на специальный драйвер и dll библиотеку под Windows XP и заявил, что это оптимальный вариант выполнения работы, так как по другому сделать её невозможно. Перспектива писать код под устаревшую OS меня не радовала, а слова про «невозможность» другой реализации лишь разожгли интерес. После недолгих поисков я выяснил, что цель может быть достигнута с помощью самописного драйвера.

В этой статье я хочу поделиться своим опытом, полученным в ходе длительных блужданий по документации Microsoft и попыток добиться от ChatGPT вменяемого ответа. Если вам интересно системное программирование под Windows — добро пожаловать под кат.

Важно знать

Современные драйвера под Windows могут быть основаны на одном из двух фреймворков: KMDF и UMDF (Kernel Mode Driver Framework и User Mode Driver Framework). В данной статье будет рассматриваться разработка KMDF драйвера, так как на него наложено меньше ограничений в отношении доступных возможностей. До UMDF драйверов я пока не добрался, как только поэкспериментирую с ними, обязательно напишу статью!

Разработка драйверов обычно происходит с помощью 2 компьютеров (или компьютера и виртуальной машины), на одном вы пишите код и отлаживаете программу (хост-компьютер), на другом вы запускаете драйвер и молитесь, чтобы система не легла после ваших манипуляций (целевой компьютер).

Перед началом работы с драйверами обязательно создайте точку восстановления, иначе вы рискуете положить свою систему так, что она перестанет запускаться. В таком случае выходом из ситуации станет откат Windows в начальное состояние (то есть в состояние при установке), что уничтожит не сохранённые заранее файлы на системном диске. Автор данной статьи наступил на эти грабли и совсем забыл скопировать свои игровые сохранения в безопасное место…

Первый запуск драйвера после добавления обработки IOCTL запросов на моём компьютере приводит к падению системы с необходимостью откатываться на точку восстановления. После этого драйвер запускается и работает без проблем. Самое странное, что если перенести тот же код в новый проект и запустить этот драйвер, то падения не происходит. Причины этого найти мне не удалось, так что прошу помощи в комментариях.

Я не профессиональный разработчик и данная статья лишь способ проложить дорогу для тех, кому так же, как и мне, стало интересно выйти за рамки привычных user mode приложений и попробовать что-то новое. Критика и дополнения приветствуются, постараюсь по возможности оперативно исправлять все ошибки. Теперь точно всё :)

Установка компонентов

  1. В данной статье я не буду рассматривать вопрос о создании Hello world драйвера, этот вопрос полностью разобран тут. Рекомендую чётко следовать всем указаниям этого руководства, для того чтобы собрать и запустить свой первый драйвер. Это может занять некоторое время, но как можно достигнуть цели не пройдя никакого пути? Если у вас возникнут проблемы с этим процессом, я с радостью помогу вам в комментариях.

  2. Полезно будет установить утилиту WinObj, которая позволяет вам просматривать имена файлов устройств и находить символические ссылки, которые на них указывают. Качаем тут.

  3. Также неплохой утилитой является DebugView. Она позволяет вам просматривать отладочные сообщения, которые отправляются из ядра. Для этого необходимо включить опцию Kernel Capture на верхней панели.

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

Начинаем веселье

В результате выполнения всех пунктов руководства Microsoft вы должны были сформировать файл драйвера cо следующим содержимым (комментарии добавлены от меня):

#include <ntddk.h>
#include <wdf.h>

// Объявление прототипа функции входа в драйвер, аналогично main() у обычных программ
DRIVER_INITIALIZE DriverEntry;

// Объявление прототипа функции для создания экземпляра устройства
// которым будет управлять наш драйвер
EVT_WDF_DRIVER_DEVICE_ADD KmdfHelloWorldEvtDeviceAdd;

// Пометки __In__ сделаны для удобства восприятия, на выполнение кода они не влияют.
// Обычно функции в пространстве ядра не возвращают данные через return 
// (return возвращает статус операции)
// так что при большом числе аргументов такие пометки могут быть полезны
// чтобы не запутаться
NTSTATUS
DriverEntry(
  // Фреймворк передаёт нам этот объект, никаких настроек мы для него не применяем
    _In_ PDRIVER_OBJECT     DriverObject, 
    // Путь, куда наш драйвер будет помещён
    _In_ PUNICODE_STRING    RegistryPath 
)
{
    // NTSTATUS переменная обычно используется для возвращения
    // статуса операции из функции
    NTSTATUS status = STATUS_SUCCESS;

    // Создаём объект конфигурации драйвера
    // в данный момент нас не интерсует его функциональность
    WDF_DRIVER_CONFIG config;

    // Макрос, который выводит сообщения. Они могут быть просмотрены с помощью DbgView
    KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "HelloWorld: DriverEntry\n"));

    // Записываем в конфиг функцию-инициализатор устройства
    WDF_DRIVER_CONFIG_INIT(&config,
        KmdfHelloWorldEvtDeviceAdd
    );

    // Создаём объект драйвера
    status = WdfDriverCreate(DriverObject,
        RegistryPath,
        WDF_NO_OBJECT_ATTRIBUTES,
        &config,
        WDF_NO_HANDLE
    );
    return status;
}

NTSTATUS
KmdfHelloWorldEvtDeviceAdd(
    _In_    WDFDRIVER       Driver,     // Объект драйвера
    _Inout_ PWDFDEVICE_INIT DeviceInit  // Структура-иницализатор устройства
)
{
    // Компилятор ругается, если мы не используем какие-либо параметры функции 
    // (мы не используем параметр Driver)
    // Это наиболее корректный способ избежать этого предупреждения
    UNREFERENCED_PARAMETER(Driver);

    NTSTATUS status;

    // Объявляем объект устройства
    WDFDEVICE hDevice;

    // Снова вывод сообщения
    KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: DeviceAdd\n"));

    // Создаём объект устройства
    status = WdfDeviceCreate(&DeviceInit,
        WDF_NO_OBJECT_ATTRIBUTES,
        &hDevice
    );

    // Утрированный пример того, как можно проверить результат выполнения операции
    if (!NT_SUCCESS(status)) {
        return STATUS_ERROR_PROCESS_NOT_IN_JOB;
    }

    return status;
}

Данный драйвер не делает ничего интересного, кроме 2-х отладочных сообщений и создания экземпляра устройства. Пока мы не можем общаться с устройством из пространства пользователя. Нужно это исправить!

Весь дальнейший код будет добавляться в функциюKmdfHelloWorldEvtDeviceAddДля достижения цели необходимо создать файл устройства и символическую ссылку на него в пространстве ядра. С этим нам помогут функции WdfDeviceInitAssignNameи WdfDeviceCreateSymbolicLinkОднако просто вызвать их, передав имена файлов, не получится, нужна подготовка.

Начнём с тех самых имён. Они представляют собой строки в кодировке UTF-8. Следующий пример показывает способ инициализации строки в пространстве ядра.

UNICODE_STRING  symLinkName = { 0 };
UNICODE_STRING deviceFileName = { 0 };


RtlInitUnicodeString(&symLinkName, L"\\DosDevices\\PCI_Habr_Link");
RtlInitUnicodeString(&deviceFileName, L"\\Device\\PCI_Habr_Dev"); 

Желательно придерживаться показанного в примере стиля именования файла устройства, то есть начинаться имя должно с префикса \Device\

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

 UNICODE_STRING securitySetting = { 0 };
 RtlInitUnicodeString(&securitySetting, L"D:P(A;;GA;;;SY)(A;;GA;;;BA)");

 // SDDL_DEVOBJ_SYS_ALL_ADM_ALL
 WdfDeviceInitAssignSDDLString(DeviceInit, &securitySetting);

Комментарий капсом — это пометка какой тип разрешения на устройство здесь выставлен. В документации Microsoft описаны константы, аналогичные комментарию, однако у меня компилятор их не видел, и мне пришлось вставлять строку в сыром виде. Ссылка на типы разрешений тут.

Далее необходимо настроить дескриптор безопасности для устройства. Если коротко, то это реакция устройства на обращения к своему файлу.

// FILE_DEVICE_SECURE_OPEN означает, что устройство будет воспринимать обращения
// к файлу устройства как к себе
WdfDeviceInitSetCharacteristics(DeviceInit, FILE_DEVICE_SECURE_OPEN, FALSE);

Наконец-то мы можем создать файл устройства:

 status = WdfDeviceInitAssignName(
     DeviceInit,
     &deviceFileName
 );

// Напоминание о том, что результат критичных для драйвера функций нужно проверять
 if (!NT_SUCCESS(status)) {
     WdfDeviceInitFree(DeviceInit);
     return status;
 }

Переходим к символической ссылке, следующий код должен быть вставлен после функции WdfDeviceCreate

 status = WdfDeviceCreateSymbolicLink(
     hDevice,
     &symLinkName
 );

Итоговый код функции KmdfHelloWorldEvtDeviceAdd должен иметь следующий вид:

NTSTATUS
KmdfHelloWorldEvtDeviceAdd(
    _In_    WDFDRIVER       Driver,     // Объект драйвера
    _Inout_ PWDFDEVICE_INIT DeviceInit  // Структура-иницализатор устройства
)
{
    // Компилятор ругается, если мы не используем какие-либо параметры функции 
    // (мы не используем параметр Driver)
    // Это наиболее корректный способ избежать этого предупреждения
    UNREFERENCED_PARAMETER(Driver);

    NTSTATUS status;

    UNICODE_STRING  symLinkName = { 0 };
    UNICODE_STRING deviceFileName = { 0 };
    UNICODE_STRING securitySetting = { 0 };

    RtlInitUnicodeString(&symLinkName, L"\\DosDevices\\PCI_Habr_Link");
    RtlInitUnicodeString(&deviceFileName, L"\\Device\\PCI_Habr_Dev");
    RtlInitUnicodeString(&securitySetting, L"D:P(A;;GA;;;SY)(A;;GA;;;BA)");

    // SDDL_DEVOBJ_SYS_ALL_ADM_ALL
    WdfDeviceInitAssignSDDLString(DeviceInit, &securitySetting);

    WdfDeviceInitSetCharacteristics(DeviceInit, FILE_DEVICE_SECURE_OPEN, FALSE);

    status = WdfDeviceInitAssignName(
        DeviceInit,
        &deviceFileName
    );

    // Hезультат критичных для драйвера функций нужно проверять
    if (!NT_SUCCESS(status)) {
        WdfDeviceInitFree(DeviceInit);
        return status;
    }

    // Объявляем объект устройства
    WDFDEVICE hDevice;

    // Снова вывод сообщения
    KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,"HelloWorld: EvtDeviceAdd\n"));

    // Создаём объект устройства
    status = WdfDeviceCreate(&DeviceInit,
        WDF_NO_OBJECT_ATTRIBUTES,
        &hDevice
    );

    status = WdfDeviceCreateSymbolicLink(
        hDevice,
        &symLinkName
    );

    // Утрированный пример того, как можно проверить результат выполнения операции
    if (!NT_SUCCESS(status)) {
        return STATUS_ERROR_PROCESS_NOT_IN_JOB;
    }

    return status;
}

После сборки и установки драйвера, в утилите WinObj по пути «GLOBAL??» вы сможете увидеть следующее:

Если у вас не появились эти файлы, то, возможно, вы допустили ошибку при инициализации и вам нужно под отладчиком проверить статус всех вызываемых функций

Если у вас не появились эти файлы, то, возможно, вы допустили ошибку при инициализации и вам нужно под отладчиком проверить статус всех вызываемых функций

Общаемся с устройством

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

С чего начнётся этот этап? Правильно! С инициализации необходимых компонентов. Следите за руками:


WDF_IO_QUEUE_CONFIG  ioQueueConfig;
WDFQUEUE  hQueue;

// Инициализируем настройки очереди, в которую будут помещаться запросы
// Параметр WdfIoQueueDispatchSequential говорит то, что запросы будут обрабатываться
// по одному в порядке очереди
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(
    &ioQueueConfig,
    WdfIoQueueDispatchSequential
);

// Обработчик HandleIOCTL будет вызываться в ответ на функцию DeiviceIOControl
// Уже скоро мы создадим его
ioQueueConfig.EvtIoDeviceControl = HandleIOCTL;

// Создаём очередь
status = WdfIoQueueCreate(
      hDevice,    // Объект устройства уже должен существовать
      &ioQueueConfig,
      WDF_NO_OBJECT_ATTRIBUTES,
      &hQueue
);
if (!NT_SUCCESS(status)) {
      return status;
}

Очередь есть, но нет обработчика. Работаем:

// Выглядит страшно, но по сути код может быть любым числом, этот макрос использован 
// для более подробного описания возможностей IOCTL кода для программиста
#define IOCTL_CODE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x3000, METHOD_BUFFERED, GENERIC_READ | GENERIC_WRITE)

VOID HandleIOCTL(
    _In_ WDFQUEUE Queue,   // Объект очереди, применения ему я пока не нашёл
    _In_ WDFREQUEST Request, // Из этого объекта мы извлекаем входной и выходной буферы
    _In_ size_t OutputBufferLength,
    _In_ size_t InputBufferLength,
    _In_ ULONG IoControlCode // IOCTL код, с которым к устройству обратились
)
{
    NTSTATUS status = STATUS_SUCCESS;

    UNREFERENCED_PARAMETER(Queue);
    UNREFERENCED_PARAMETER(InputBufferLength);
    UNREFERENCED_PARAMETER(OutputBufferLength);
    UNREFERENCED_PARAMETER(Request);

   switch (IoControlCode)
   {
      case IOCTL_CODE:
      {
          // Обрабатываем тут
          break;
      }
   }

    // По сути своей return из обработчика
    // Используется, если запрос не возваращает никаких данных
    WdfRequestComplete(Request, status);
}

Вот мы уже и на финишной прямой, у нас есть очередь, есть обработчик, но последний не возвращает и не принимает никаких данных. Сделаем так, чтобы обработчик возвращал нам сумму переданных чисел.

Объявим 2 структуры, из их названий будет понятно для чего они будут использоваться. В режиме ядра лучше использовать системные типы данных, такие как USHORT, UCHAR и другие.

struct DeviceRequest
{
    USHORT a;
    USHORT b;
};

struct DeviceResponse
{
    USHORT result;
};

Обновлённая функция обработки IOCTL запроса:

VOID HandleIOCTL(
    _In_ WDFQUEUE Queue,   // Объект очереди, применения ему я пока не нашёл
    _In_ WDFREQUEST Request, // Из этого объекта мы извлекаем входной и выходной буферы
    _In_ size_t OutputBufferLength,
    _In_ size_t InputBufferLength,
    _In_ ULONG IoControlCode // IOCTL код, с которым к устройству обратились
)
{
    NTSTATUS status = STATUS_SUCCESS;

    UNREFERENCED_PARAMETER(Queue);
    UNREFERENCED_PARAMETER(InputBufferLength);
    UNREFERENCED_PARAMETER(OutputBufferLength);

    size_t returnBytes = 0;

    switch (IoControlCode)
    {
    case IOCTL_CODE:
    {
        struct DeviceRequest request_data = { 0 };

        struct DeviceResponse *response_data = { 0 };

        PVOID buffer = NULL;
        PVOID outputBuffer = NULL;
        size_t length = 0;

        // Получаем указатель на буфер с входными данными
        status = WdfRequestRetrieveInputBuffer(Request, 
                                               sizeof(struct DeviceRequest),
                                               &buffer,
                                               &length);

        // Проверка на то, что мы получили буфер и он соотвествует ожидаемому размеру
        // Очень важно делать такие проверки, чтобы не положить систему :)
        if (length != sizeof(struct DeviceRequest) || !buffer)
        {
            status = STATUS_INVALID_DEVICE_REQUEST;
            break;
        }

        request_data = *((struct DeviceRequest*)buffer);

        // Получаем указатель на выходной буфер
        status = WdfRequestRetrieveOutputBuffer(Request, 
                                                sizeof(struct DeviceResponse), 
                                                &outputBuffer,
                                                &length);
        if (length != sizeof(struct DeviceResponse) || !outputBuffer)
        {
            status = STATUS_INVALID_DEVICE_REQUEST;
            break;
        }
        response_data = (struct DeviceResponse*)buffer;

        // Записываем в выходной буфер результат
        response_data->result = request_data.a + request_data.b;
        // Вычисляем сколько байт будет возвращено в ответ на данный запрос
        returnBytes = sizeof(struct DeviceResponse);

        break;
    }
    }

    // Функция-return изменилась, так как теперь мы возвращаем данные
    WdfRequestCompleteWithInformation(Request, status, returnBytes);
}

Последний шаг — программа в режиме пользователя. Cоздаём обычный С или C++ проект и пишем примерно следующее:

#include <windows.h>
#include <iostream>

//Эта часть аналогична тем же объявлениям в драйвере
//================
#define IOCTL_CODE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x3000, METHOD_BUFFERED, GENERIC_READ | GENERIC_WRITE)

struct DeviceRequest
{
    USHORT a;
    USHORT b;
};

struct DeviceResponse
{
    USHORT result;
};
// ===========================

int main()
{
	HANDLE hDevice = CreateFileW(L"\\??\\PCI_Habr_Link", 
                                 GENERIC_READ | GENERIC_WRITE,
                                 FILE_SHARE_READ, 
                                 NULL, 
                                 OPEN_EXISTING,
                                 FILE_ATTRIBUTE_NORMAL,
                                 NULL);
  
	DeviceRequest request = { 0 };
	request.a = 10;
	request.b = 15;

	LPVOID input = (LPVOID)&request;

	DeviceResponse response = {  0};
	LPVOID answer = (LPVOID)&response;
	DWORD bytes = 0;

	bool res = DeviceIoControl(hDevice, IOCTL_CODE, input, sizeof(DeviceRequest),
		answer, sizeof(DeviceResponse), &bytes, NULL);

	response = *((DeviceResponse*)answer);
	std::cout << "Sum : " << response.result << std::endl;
	char ch;
	std::cin >> ch;

	CloseHandle(hDevice);
}

При запуске вы должны получить такой результат:

Заключение

Собственно, на этом всё. В конечном итоге мы получили драйвер, способный принимать и отвечать на запросы пользовательских программ. Дальнейшие модификации проекта ограничиваются лишь вашей фантазией.

Предыстория

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

После 20 минут поисков по сети я наткнулся на Github Павла Иосифовича (zodiacon — Overview). Личность легендарная в своих кругах, достаточно посмотреть на его репозиторий, публикации и выступления на именитых конференциях. Помимо этого, Павел является автором/соавтором нескольких книг: «Windows Internals» (книга, имеющаяся у меня на полке, которая принесла немало пользы), и «Windows Kernel Programming» 2019 года выпуска (бегло пролистав 11 Глав или 390 страниц, я понял – это то, что нужно!).
Кстати, книгу вы можете купить прямо на сайте Павла

Ссылка скрыта от гостей

Книгу я приобрёл в бумажной версии, чтобы хоть и немного, но поддержать автора. Безупречное качество, несмотря на то, что она издается в мягком переплете. Хорошие плотные листы формата А4 и качественная краска. (книга без проблем пережила вылитую на нее кружку горячего кофе).
Пока я сидел на балконе и читал четвёртую главу книги, в голову пришла мысль: а почему бы не сделать ряд статей на тему «Программирования драйвера под Windows», так сказать, совместить полезное, с еще более полезным.

И вот я здесь, пишу предысторию.

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

Базовые понятия о внутреннем устройстве Windows (Windows Internals)

Для того, чтобы начать разрабатывать Драйвер под Windows, то есть работать на уровне с ядром ОС, необходимо базовое понимание того, как эта ОС утроена. Так как я хочу сосредоточиться на написании драйвера, а не на теории об операционных системах, подробно описывать понятия я не буду, чтобы не растягивать статью, вместо этого прикреплю ссылки для самостоятельного изучения.

Следовательно, нам стоит ознакомиться с такими базовыми понятиями как:

Ссылка скрыта от гостей

Процесс – это объект, который управляет запущенной инстанцией программы.​

Ссылка скрыта от гостей

Технология, позволяющая создавать закрытые пространства памяти для процессов. В своем роде — это песочница.​

Ссылка скрыта от гостей

Это сущность, которая содержится внутри процесса и использует для работы ресурсы, выделенные процессом — такие, как виртуальная память. По сути, как раз таки потоки и запускают код.​

Ссылка скрыта от гостей

В своем роде это прокладка, которая позволяет программе отправлять запросы в Ядро операционной системы, для выполнения нужных ей операций.​

Ссылка скрыта от гостей

Это сложно описать словами коротко, проще один раз увидеть картинку.​

В упрощённом виде это выглядит так:​

image1.png

Ссылка скрыта от гостей

Дескрипторы и объекты необходимы для регулирования доступа к системным ресурсам.​

Объект — это структура данных, представляющая системный ресурс, например файл, поток или графическое изображение.​

Дескриптор – это некая абстракция, которая позволяет скрыть реальный адрес памяти от Программы в пользовательском режиме.​

Для более глубокого понимания Операционных систем могу посоветовать следующие материалы:
Книги:

  • Таненбаум, Бос: Современные операционные системы
  • Windows Internals 7th edition (Part 1)

Видео:

Настройка рабочего пространства

Для разработки драйвера, как и любого другого софта необходима подходящая среда.
Так как мы работаем в операционной системе Windows, её средствами мы и будем пользоваться.

Что нам понадобится:

1. Visual Studio 2017 и старше.​

(Community Version хватает с головой) Также во вкладке „Individual components” необходимо установить

Код:

MSVC v142 - VS 2019 C++ ARM build tools (Latest)
MSVC v142 - VS 2019 C++ ARM Spectre-mitigated libs (Latest)
MSVC v142 - VS 2019 C++ ARM64 build tools (Latest)
MSVC v142 - VS 2019 C++ ARM64 Spectre-mitigated libs (Latest)
MSVC v142 - VS 2019 C++ ARM64EC build tools (Latest - experimental)
MSVC v142 - VS 2019 C++ ARM64EC Spectre-mitigated libs (Latest - experimental)
MSVC v142 - VS 2019 C++ x64/x86 build tools (Latest)
MSVC v142 - VS 2019 C++ x64/x86 Spectre-mitigated libs (Latest)

image10.png

и далее по списку.

2. Windows 10/11 SDK (последней версии)​

Ссылка скрыта от гостей

image3.png

Тут все просто. Качаем iso файл, монтируем и запускаем установщик.

3. Windows 10/11 Driver Kit (WDK)​

Ссылка скрыта от гостей

image4.png

В конце установки вам будет предложено установить расширение для Visual Studio. Обязательно установите его!

image11.png

После закрытия окна установки WDK появится установщик Расширения VisualStudio

image2.png

4. Sysinternals Suite

Ссылка скрыта от гостей

Скачайте и распакуйте в удобное для вас место. Это набор полезных утилит, которые пригодятся для исследования Windows, дебага драйвера и прочего.

image8.png

5. Виртуальная Машина с Windows для тестов.​

Выбор ПО для виртуализации на ваше усмотрение. Я буду использовать «VMware Workstation 16 pro».
Написанные драйверы лучше тестировать именно в виртуальной машине, так как Ядро — ошибок не прощает, и вы будете часто улетать в синий экран смерти.

После того, как все было установлено, пора запускать Visual Studio и начинать писать драйвер.

Создание проекта

Запускаем Visual Studio и создаем новый проект. Создадим пустой проект „Empty WDM Driver“

image21.png

Называем его как душе угодно.

image24.png

И вот он, наш свеженький чистенький проект для нашего первого драйвера.

image27.png

Теперь необходимо создать cpp файл, в котором мы будем писать сам драйвер.

image7.png

image14.png

Вот и все. Настройку системы и среды мы закончили.

image22.png

Первый драйвер

Сначала импортируем ntddk.h эта одна из базовых библиотек для работы с ядром. Больше информации

Ссылка скрыта от гостей

. Как и у любой программы, у драйвера должна быть точка входа DriverEntry, как функция Main в обычной программе. Готовый прототип этой функции выглядит так

C++:

#include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
/*
    In_ это часть SAL(Source Code Ananotation Language) Аннотации не видимы для компилятора,
    но содержат метаданные которые, улучшают анализ и чтение кода.
*/
    return STATUS_SUCCESS;
}

Если мы попробуем собрать наш проект, то получим следующие ошибки и предупреждения.

image15.png

В данном случае пункт 1 является следствием пунктов 2 и 3. Дело в том, что по дефолту в Visual Studio некоторые “предупреждения” расцениваются как ошибки.
Чтобы решить эту проблему есть 2 пути.

  1. Отключить эту фичу в Visual Studio, что делать не рекомендуется. Так как сообщения об ошибках могут быть полезны и сэкономят вам время и нервы в дальнейшем.
  2. Более правильный и классический метод это использовать макросы в c++. Как видно из сообщения с кодом C4100 объекты RegistryPath и DriverObject не упомянуты в теле функции. Подробнее

    Ссылка скрыта от гостей

    .

Для того, чтобы избавиться от предупреждений, и заставить наш код работать, стоит поместить объекты в макрос UNREFERENCED_PARAMETER(ObjectName)

C++:

include <ntddk.h>

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
    return STATUS_SUCCESS;
}

Теперь, если пересобрать проект, то мы увидим, что ошибка С220 и предупреждение C4100 пропали, но к ним на смену пришли LNK2019 и LNK1120. Однако это уже не ошибки компиляции — это ошибки линкера. А кто говорил что будет легко?
О том, что такое линкер можно почитать

Ссылка скрыта от гостей

.

Дело в том, что наша функция не представлена в стандартном линкере С++ и вообще она девушка капризная и хочет Си-линкер. Удовлетворим желание дамы и дадим ей то, чего она хочет.

Делается это просто. Перед функцией надо добавить extern "C" так наш линкер будет понимать, что эта функция должна линковаться С-линкером.

Собираем проект заново и вуаля — Драйвер собрался.

image17.png

Что на данный момент умеет наш драйвер? Сейчас это по сути пустышка, которая после загрузки, в случае успеха, вернет нам сообщения об удачном запуске. Давайте заставим его нас поприветствовать и проверим его работоспособность. Выводить сообщения мы будем при помощи функции KdPrint(()); да именно в двойных кавычках.

Итоговый код драйвера будет выглядеть так:

C++:

#include <ntddk.h>

//Указываем линкеру, что DriverEntry должна линковаться С-линкером
extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
    //Убираем варнинг C4100 и связанную с ним ошибку C220
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);
    //Выводим сообщение
    KdPrint(("Hi Codeby, this is our first driver! Yuhu!\n"));
   
    return STATUS_SUCCESS;
}

Собираем или пересобираем драйвер.

image6.png

Важно! Сборка драйвера должна происходить в режиме Debug!!!

image12.png

После чего в папке нашего проекта мы сможем найти результаты нашего труда. Вы только посмотрите на него, какой маленький и хорошенький.

image5.png

Но что делать дальше? Как проверить его работоспособность?
Для этого нам и понадобится наша виртуальная машина с Windows, но перед запуском на ней драйвера, нам придется проделать пару манипуляций. Дело в том, что в Windows есть встроенная защита, и если драйвер не подписан «нужной» подписью ака сертификатом, то драйвер просто не загрузится.

Дальнейшие действия нужно проделать в Windows на виртуальной машине.
Чтобы отключить эту проверку подписи, а точенее перевести Windows в тестовый режим, запустите cmd.exe от имени администратора и введите следующую команду bcdedit /set testsigning on.

image13.png

Перезагрузите виртуальную машину.
Если все прошло удачно, в правом нижнем углу вы увидите следующую надпись (2 нижнее строчки могут отличиться в зависимости от версии Windows)

image9.png

Возвращаемся в папку с драйвером и копируем его в виртуальную машину. Теперь нам надо создать службу для запуска драйвер. Открываем консоль от имени администратора и вводим следующую команду:
sc create Name type= kernel binPaht= PATH_TO_DRIVER

в моем случае это выглядит так:

image19.png

Также проверить успешность создания можно через реестр.

image16.png

В той же консоли мы можем попробовать запустить нашу службу.
sc start CodebyDriver

image18.png

Отлично, драйвер запустился и мы даже не улетели в синьку, а это всегда приятно. Теперь давайте проверим, выводится ли сообщение от драйвера.
Для этого нам необходимо провести подготовительные работы.

Создадим новый ключ в реестре и назовем его Debug Print Filter.

image23.png

В качестве значения задаем DWORD с именем DEFAULT и определяем данные для значения как 8.

image25.png

Перезагружаем виртуальную машину.

После перезапуска запускаем DebugView данный инструмент находится в архиве Sysinternals, который мы ранее скачали. Ее можно смело скопировать в виртуальную машину.

Запускаем DebugView от имени Администратора и ставим галочку “Capture Kerner”

image20.png

Capture Win32 и Capture Global Win32 можно снять, если летит много сообщений.

Затем запускаем консоль от имени администратора и запускаем службу загрузки драйвера.

image26.png

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

Спасибо за чтение!

P.S: Я сам только начал изучать тему работы с драйверами. Так что если у вас есть предложения или правки по технической части статьи, прошу отписать в комментарии, чтобы я мог внести изменения в статью.
P.P.S: Как вы могли заметить, писать мы будем преимущественно на С++, посему могу посоветовать отличный канал с уроками по С++ — The Cherno.

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

Прежде чем приступить к созданию драйвера, важно иметь хорошее понимание основных принципов работы драйверов и архитектуры операционной системы Windows. Также полезно быть знакомым с языком программирования C/C++, так как большинство драйверов для Windows создается на этом языке.

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

Содержание

  1. Перед началом работы
  2. Сборка необходимых инструментов
  3. Шаг 1: Создание проекта
  4. Создание папки проекта
  5. Шаг 2: Настройка окружения разработки
  6. Установка необходимых пакетов

Перед началом работы

Создание драйвера для операционной системы Windows является сложным процессом, требующим глубокого понимания архитектуры Windows и навыков программирования на языке С/С++. В этой статье мы рассмотрим основные шаги, необходимые для создания драйвера для Windows.

  1. Определение требований
  2. Перед тем, как начать создание драйвера, необходимо определить его требования. К какому устройству он будет подключаться? Какие функции и возможности он должен предоставлять? Какие требования по производительности у него будут? Эти и другие вопросы помогут определить необходимые компоненты и функциональность драйвера.

  3. Выбор среды разработки
  4. Для разработки драйвера для Windows можно использовать различные инструменты и среды разработки. Наиболее популярными из них являются Microsoft Visual Studio и Windows Driver Kit (WDK). Они предоставляют все необходимые инструменты и библиотеки для разработки драйверов.

  5. Подготовка окружения
  6. Перед началом разработки драйвера необходимо настроить окружение разработчика. Это включает установку среды разработки, установку необходимых компонентов, настройку сборки и отладки драйвера, создание необходимых файлов и структур проекта.

  7. Описание устройства
  8. При создании драйвера необходимо описать устройство, к которому он будет подключаться. Это включает описание интерфейсов, команд, свойств, регистров и других характеристик устройства. Это описание будет использоваться при взаимодействии с системой и другими компонентами.

  9. Разработка драйвера
  10. Фактическая разработка драйвера включает в себя написание кода, реализацию необходимой функциональности, обработку событий, обмен данными с устройством и другие операции. Для этого используются языки программирования С и С++, а также специфические библиотеки и API.

  11. Отладка и тестирование
  12. После написания кода необходимо приступить к отладке и тестированию драйвера. Для этого можно использовать отладчики и инструменты, предоставляемые средами разработки Windows. Также рекомендуется провести тестирование с реальным устройством, чтобы убедиться в правильности работы драйвера.

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

Сборка необходимых инструментов

Прежде чем приступить к разработке драйвера для Windows, необходимо собрать все необходимые инструменты. Эти инструменты позволят вам создавать, отлаживать и собирать код в драйвера.

Вот список основных инструментов, которые вам понадобятся:

  1. Visual Studio: Интегрированная среда разработки (IDE) от Microsoft, предназначенная для создания приложений под различные платформы, включая Windows. Вы можете скачать бесплатную Community-версию с официального сайта Microsoft.
  2. Windows Driver Kit (WDK): Набор инструментов для разработки драйверов под платформу Windows. WDK включает в себя компиляторы, отладчики и другие необходимые утилиты. Вы можете скачать последнюю версию WDK с официального сайта Microsoft.
  3. Драйвер поддержки отладки (Debugging Tools for Windows): Сборка утилит для отладки кода драйвера. Этот набор инструментов включает в себя отладчик, профилировщик и другие полезные инструменты. Debugging Tools for Windows доступен для загрузки с официального сайта Microsoft.
  4. Документация: Подробные руководства и документация от Microsoft будут полезны при разработке драйвера. Ознакомьтесь с документацией по WDK и другим связанным технологиям, чтобы лучше понять основные концепции и процессы разработки драйвера.

После установки всех необходимых инструментов вы будете готовы к разработке драйвера для Windows. Вам понадобится Visual Studio для создания проекта драйвера, WDK для сборки и проверки драйвера, а также Debugging Tools for Windows для отладки и профилирования кода драйвера.

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

Шаг 1: Создание проекта

Первый шаг в создании драйвера для Windows — создание проекта. Для этого необходимо выполнить следующие действия:

  1. Открыть среду разработки, такую как Visual Studio.
  2. Выбрать «Создать новый проект».
  3. В появившемся окне выбрать «Проект для создания драйверов Windows».

После выполнения этих шагов будет создан базовый проект для драйвера Windows, который может быть дальше настроен и расширен.

Важно отметить, что при создании проекта необходимо выбрать подходящий для вашего типа драйвера шаблон проекта. Например, для создания драйвера устройства необходимо выбрать шаблон «Драйвер устройства для Windows».

После создания проекта вы можете приступить к следующему шагу, который включает настройку основных параметров проекта, таких как версия Windows, архитектура и многое другое.

Создание папки проекта

Перед тем как начать создание драйвера для Windows, необходимо создать папку проекта, в которой будут храниться все файлы и ресурсы, связанные с драйвером.

Чтобы создать папку проекта, выполните следующие шаги:

  1. Откройте проводник Windows, найдите место на вашем компьютере, где хотите создать папку проекта.
  2. Щелкните правой кнопкой мыши на свободном месте и выберите пункт «Создать» в контекстном меню.
  3. В появившемся подменю выберите «Папку».
  4. Введите название папки проекта и нажмите клавишу «Enter».

Теперь у вас есть папка проекта, в которой вы сможете хранить все необходимые файлы для создания драйвера.

Рекомендация: Для удобства, дайте папке проекта осмысленное имя, отражающее ее содержание и назначение. Например, «MyDriverProject».

Шаг 2: Настройка окружения разработки

После того как вы установили необходимые компоненты для разработки драйвера для Windows, вам потребуется настроить ваше рабочее окружение.

Вот несколько шагов, которые помогут вам настроить окружение разработки.

  1. Установите Visual Studio. Visual Studio — это интегрированная среда разработки (IDE), которая предоставляет мощные инструменты для создания драйверов для Windows.
  2. Установите компоненты Windows Driver Kit (WDK). WDK содержит все необходимые инструменты и библиотеки для разработки драйверов для Windows.
  3. Настройте пути для компилятора и инструментов WDK. Это позволит Visual Studio обнаружить и использовать компоненты WDK при сборке проекта.
  4. Установите отладчик Windows. Он позволит вам отлаживать свой драйвер и выполнять шаг-за-шагом отладку кода.

После завершения этих шагов ваше окружение разработки будет готово к созданию драйвера для Windows.

Установка необходимых пакетов

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

1. Visual Studio

Для разработки драйверов для Windows вам понадобится Visual Studio — интегрированная среда разработки (IDE), разработанная компанией Microsoft. Visual Studio предоставляет все необходимые инструменты и компиляторы для создания драйверов. Вы можете загрузить и установить Visual Studio Community Edition бесплатно с официального веб-сайта Visual Studio.

2. Windows Driver Kit (WDK)

Windows Driver Kit (WDK) — набор инструментов, документации и примеров кода, предоставляемый Microsoft для разработки драйверов для Windows. WDK включает в себя необходимые компиляторы, заголовочные файлы и библиотеки для создания драйверов. Вы можете загрузить и установить WDK с официального сайта Microsoft.

3. Документация Microsoft

Microsoft предоставляет обширную документацию, которая поможет вам разобраться в процессе создания драйвера для Windows. В документации содержится информация об основных концепциях, API и примеры кода. Большая часть документации доступна онлайн на официальном сайте Microsoft.

4. Дополнительные инструменты и библиотеки

В зависимости от требований вашего проекта, вам может потребоваться установить дополнительные инструменты и библиотеки. Например, если вы работаете с определенными устройствами или проводите тестирование драйверов, вам может понадобиться установить дополнительные инструменты, такие как Windows Driver Framework (WDF) и Universal Serial Bus (USB) development tools.

5. Регулярные обновления

Не забывайте регулярно обновлять установленные пакеты, поскольку Microsoft периодически выпускает обновления для Visual Studio, WDK и других инструментов разработки.

После установки всех необходимых пакетов вы будете готовы приступить к созданию своего собственного драйвера для Windows. Удачи!

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

1. Зачем нужен сетевой драйвер?

Сетевой драйвер является программным обеспечением, которое позволяет вашей операционной системе взаимодействовать с сетевым оборудованием, таким как сетевые адаптеры, маршрутизаторы и коммутаторы. Он обеспечивает стабильную и быструю передачу данных между компьютерами в локальной сети и в Интернете.

2. Необходимые инструменты

Для создания сетевого драйвера вам понадобится следующее программное обеспечение:

— Visual Studio — интегрированная среда разработки (IDE) от Microsoft, которая предоставляет широкий спектр инструментов для разработки и отладки программного обеспечения под Windows.

— Windows Driver Kit (WDK) — это пакет разработчика драйверов для Windows, включающий в себя всю необходимую документацию, инструменты и примеры кода для разработки драйверов.

— Windows Hardware Lab Kit (HLK) — это набор тестового оборудования и программного обеспечения, который позволяет проверить и довести до совершенства работу сетевого драйвера.

— Драйверы сетевого оборудования Windows — драйверы сетевых адаптеров и другого оборудования, которые могут помочь вам в разработке сетевого драйвера.

3. Создание проекта сетевого драйвера в Visual Studio

Перед началом разработки сетевого драйвера вам необходимо создать проект в Visual Studio.

Для этого откройте Visual Studio и выберите «File» > «New» > «Project». В списке шаблонов проектов выберите «Windows Driver» и нажмите «Next». Заполните отображаемые поля, включая имя проекта и место сохранения файлов проекта. Нажмите «Finish», чтобы создать проект сетевого драйвера.

4. Написание кода драйвера

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

В качестве одного из примеров рассмотрим создание драйвера Ethernet.

a) Интерфейс драйвера

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

EXTERN_C_START

typedef struct _DRIVER_OBJECT DRIVER_OBJECT;
typedef struct _DEVICE_OBJECT DEVICE_OBJECT;

NTSTATUS DriverEntry(_In_ DRIVER_OBJECT* DriverObject, _In_ PUNICODE_STRING RegistryPath);

_When_(return==0, _At_(DeviceObject, __drv_allocatesMem(Mem)))
EXTERN_C NTSTATUS EthernetCreateDevice(_Inout_ DRIVER_OBJECT* DriverObject, _In_ PWCHAR DeviceName, _Out_ PDEVICE_OBJECT* DeviceObject);

EXTERN_C NTSTATUS EthernetDeleteDevice(_In_ PDEVICE_OBJECT DeviceObject);

EXTERN_C_END

b) Регистрация драйвера

Зарегистрируйте метод DriverEntry как точку входа для вашего драйвера. Эта функция вызывается при загрузке драйвера ядром Windows.

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
 
    PDEVICE_OBJECT DeviceObject = NULL;
    UNICODE_STRING DeviceName;
    RtlInitUnicodeString(&DeviceName, L"\\Device\\EthDevice");

    NTSTATUS Status = EthernetCreateDevice(DriverObject, DeviceName.Buffer, &DeviceObject);
    if (!NT_SUCCESS(Status))
    {
        return Status;
    }

    DriverObject->DriverUnload = EthernetDriverUnload;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = EthernetCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = EthernetClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = EthernetIoControl;
 
    DeviceObject->Flags |= DO_DIRECT_IO;
    DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

    return STATUS_SUCCESS;
}

c) Создание экземпляра устройства

Создайте функцию EthernetCreateDevice, которая будет вызвана при загрузке драйвера и которая создаст экземпляр устройства. Это позволит драйверу взаимодействовать с ядром Windows и управлять устройством.

NTSTATUS EthernetCreateDevice(_Inout_ PDRIVER_OBJECT DriverObject, _In_ PWCHAR DeviceName, _Out_ PDEVICE_OBJECT* DeviceObject)
{
    UNICODE_STRING DevName;
    RtlInitUnicodeString(&DevName, DeviceName);
    NTSTATUS Status = IoCreateDevice(DriverObject, 0, &DevName, FILE_DEVICE_NETWORK, 0, TRUE, DeviceObject);
 
    if (!NT_SUCCESS(Status))
    {
        return Status;
    }

    (*DeviceObject)->StackSize = 5;
    (*DeviceObject)->Flags |= DO_BUFFERED_IO;

    return STATUS_SUCCESS;
}

5. Отладка и тестирование драйвера

После создания драйвера и написания кода его необходимо отладить и протестировать. Для этого используется Windows Debugger и Windows Hardware Lab Kit (HLK).

a) Отладка драйвера

Для отладки драйвера используйте Windows Debugger. Подключите отладочный компьютер к целевой машине и загрузите драйвер в операционную систему. Запустите Windows Debugger и подключитесь к отладочной машине.

Вы можете использовать следующие команды для отладки драйвера:

— bp

— установить точку останова на адресе;

— k — вывести данные стека;

— !devobj

— вывести информацию об устройстве.

b) Тестирование драйвера

Для тестирования драйвера используйте Windows Hardware Lab Kit (HLK). Этот набор обеспечивает все необходимые инструменты, которые позволяют проверить работу драйвера в различных сценариях.

Вы можете использовать следующие инструменты для тестирования драйвера:

— Hardware Lab Kit Studio — конфигурационная утилита;

— Hardware Lab Kit Manager — менеджер тестов;

— Hardware Lab Kit Controller — программное обеспечение для диспетчеризации драйверов.

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

В один прекрасный день меня попросили написать Драйвер. На тот день мои познания в C/C++ ограничивались программой Hellow Word, и поэтому на вопрос: «Напишешь?» я самоуверенно ответил: «Конечно». На следующий день я узнал, что на свете существуют MSDN и DDK. Вскоре я понял, что не все Windows одинаковые, оказалось, что мой драйвер должен работать под Win2000/NT. У меня ушло больше месяца на то, чтобы скомпилировать и запустить свой первый Драйвер. По правде сказать, это был не совсем мой Драйвер, а точнее — это был genport из NTDDK. Но радовался я так, как будто минимум написал свою ОС.

Недавно мне пришлось опять вернуться к Драйверу. И вот вчера я, наконец-то, сдал работающий драйвер и решил написать эту небольшую статью, для того, чтобы как-то систематизировать то, что я узнал, и, чтобы когда мне снова придется взяться за драйвер, было от чего отталкиваться.

Люди, знающие что такое IOCTL, DEVICE_EXTENSION, MajorFunction и DriverEntry не найдут здесь ничего нового. Эта статья для тех, кто, возможно, никогда не слышал слово ДДК, и кто до сего дня никогда не заглядывал в исходники драйверов. И еще, я буду довольно-таки подробно описывать многие, даже очевидные вещи, поэтому напомню о том, что данная статья рассчитана на людей с очень малым опытом программирования, какой был у меня, когда я занялся написанием драйверов.

Первое, с чем сталкивается программист, решивший заняться драйверами, это почти полное отсутствие русскоязычной литературы по данной теме. За год поисков мне удалось найти всего три книги на русском, в которых хотя бы немного говорится о драйверах:

————————
1.   Свен Шрайбер «Недокументированные возможности Windows 2000». Издательство «Питер» 2002 год.
Здесь очень хорошо описан механизм динамической загрузки драйвера.
 ——————
2.   П. И. Рудаков, К. Г. Финогенов «Язык ассемблера: уроки программирования» Диалог МИФИ 2001 год.
Очень полезная книга для того, что бы понять, как писать драйвера без всякий Wizard-ов.
  ——————————  
3.   Светлана Сорокина, Андрей Тихонов, Андрей Щербаков «Программирование драйверов и систем безопасности». Издательство «БХВ-Петербург» 2002 год.
Здесь хорошо описывается многоуровневая модель драйверов.

Я ни в коей мере не претендую как на полноту освещения темы написания драйверов, так и на 100% правильность и достоверность того, что здесь написано (но все приведенные здесь исходники проверены и являются работоспособными). Буду благодарен всем, приславшим мне (01_artem@mail.ru) или высказавшим в форуме свои замечания.

Итак, я обращаюсь к человеку, решившему написать Драйвер уровня ядра под Win2000/NT. Надеюсь, эти заметки помогут сэкономить кучу времени и сил.

Прежде всего, я бы не рекомендовал (исходя из собственного опыта) пользоваться различными библиотеками (типа NuMega и всякими другими визардами). В основном из-за того, что даже для написания простейшего драйвера необходимо хотя бы поверхностное представление о том, как он функционирует. И самый простой способ получить представление об этом — написать драйвер самому. Мне, например, не хватило терпения разобраться с NuMega, и даже оболочки функций динамической загрузки/выгрузки драйвера, предложенные Свен Шрайбером в своей книге, я предпочел переписать.

Итак, начнем. Для начала надо установить на компьютер Visul C++ 6.0, MSDN и NTDDK установку проводить желательно именно в этом  порядке. Лично я пользуюсь редактором UltraEdit для работы с текстами драйверов, но, в принципе, исходный код драйвера можно набирать в любом текстовом редакторе, хоть в NotePad.

Создадим папку, в которой мы будем работать с драйвером (пусть это будет C:myDriver). В этой папке создадим 5 файлов:
1.   myDrv.c
2.   myDrv.h
3.   myDrv.rc
4.   MAKEFILE
5.   SOURCES

Начнем с последнего файла. В SOURCES скопируйте следующее:

TARGETNAME=myDrv
TARGETPATH=obj
TARGETTYPE=DRIVER

SOURCES=myDrv.c MyDrv.rc

  В MAKEFILE скопируйте следующее:

!INCLUDE $(NTMAKEENV)makefile.def

 В myDrv.rc скопируйте следующее:

#include <windows.h>
#include <ntverp.h>
#define VER_FILETYPE VFT_DRV
#define VER_FILESUBTYPE VFT2_DRV_SYSTEM
#define VER_FILEDESCRIPTION_STR     «Generic Port I/O»
#define VER_INTERNALNAME_STR        «myDrv.sys»
#include «common.ver»

А вот так должен выглядеть myDrv.h:

#define FIRST_IOCTL_INDEX  0x800
#define FILE_DEVICE_myDRV  0x00008000

                                          #define TEST_SMTH   CTL_CODE(FILE_DEVICE_myDRV,  \
                        FIRST_IOCTL_INDEX + 101,  \
                                          METHOD_BUFFERED,       \
                      FILE_ANY_ACCESS)

Теперь обратимся к тексту самого драйвера myDrv.c:

#include «ntddk.h»
#include «myDrv.h»
#include «parallel.h»

#define NT_DEVICE_NAME    L»\\Device\\myDrv»
#define DOS_DEVICE_NAME            L»\\DosDevices\\myDrv»

//структура расширения устройства
typedef struct _DEVICE_EXTENSION
{
  PDRIVER_OBJECT DriverObject;
  PDEVICE_OBJECT DeviceObject;
  PFILE_OBJECT   FileObject;
  HANDLE         Handle;

  } DEVICE_EXTENSION, *PDEVICE_EXTENSION;

//прототипы функций
NTSTATUS
DriverDeviceControl(IN PDEVICE_OBJECT DeviceObject,
                    IN PIRP Irp);

VOID
DriverUnload(IN PDRIVER_OBJECT DriverObject);

NTSTATUS
DriverOpen(IN PDEVICE_OBJECT DeviceObject,
           IN PIRP Irp);

NTSTATUS
DriverClose(IN PDEVICE_OBJECT DeviceObject,
            IN PIRP Irp);

/////////////////////////////////////////////////////////////////////////////////реализация функций

NTSTATUS
DriverEntry(IN PDRIVER_OBJECT DriverObject,
            IN PUNICODE_STRING RegistryPath)
{
  PDEVICE_OBJECT deviceObject;    
  UNICODE_STRING deviceNameUnicodeString;
  UNICODE_STRING deviceLinkUnicodeString;
  PDEVICE_EXTENSION extension;
  NTSTATUS ntStatus;

   RtlInitUnicodeString(&deviceNameUnicodeString, NT_DEVICE_NAME);

    ntStatus = IoCreateDevice(DriverObject,
                            sizeof (DEVICE_EXTENSION),
                            &deviceNameUnicodeString,
                            FILE_DEVICE_UNKNOWN,
                            0,
                            FALSE,
                            &deviceObject);

  if (!NT_SUCCESS(ntStatus)) return ntStatus;

  DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDeviceControl;
  DriverObject->DriverUnload = DriverUnload;
  DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverOpen;
  DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverClose;

  extension = (PDEVICE_EXTENSION) deviceObject->DeviceExtension;
  extension->DeviceObject = deviceObject;
  extension->DriverObject = DriverObject;

    // Create counted string version of our Win32 device name.
  RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);

  // Create a link from our device name to a name in the Win32 namespace.
  ntStatus = IoCreateSymbolicLink(&deviceLinkUnicodeString, &deviceNameUnicodeString);
  if (!NT_SUCCESS(ntStatus))
  {
    IoDeleteDevice(deviceObject);
    return ntStatus;
  }  

return STATUS_SUCCESS;
}


//———————————————————————————————-
VOID
DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
  UNICODE_STRING deviceLinkUnicodeString;
  PDEVICE_EXTENSION extension;
  PIRP pNewIrp = NULL;
  ULONG m_size;
  NTSTATUS  ntStatus;
  extension = DriverObject->DeviceObject->DeviceExtension;

    // Create counted string version of our Win32 device name.
  RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);

  // Delete the link from our device name to a name in the Win32 namespace.
  IoDeleteSymbolicLink(&deviceLinkUnicodeString);

  // Finally delete our device object
  IoDeleteDevice(DriverObject->DeviceObject);
  }

//————————————————————————————————
NTSTATUS
DriverOpen(IN PDEVICE_OBJECT DeviceObject,
           IN PIRP Irp)
{
  Irp->IoStatus.Status = STATUS_SUCCESS;
  Irp->IoStatus.Information = 0;
  IoCompleteRequest(Irp, IO_NO_INCREMENT);
  return STATUS_SUCCESS;
}

//————————————————————————————————
NTSTATUS
DriverClose(IN PDEVICE_OBJECT DeviceObject,
            IN PIRP Irp)
{
  Irp->IoStatus.Status = STATUS_SUCCESS;
  Irp->IoStatus.Information = 0;
  IoCompleteRequest(Irp, IO_NO_INCREMENT);
  return STATUS_SUCCESS;
}

//————————————————————————————————
NTSTATUS
DriverDeviceControl(IN PDEVICE_OBJECT DeviceObject,
                    IN PIRP Irp)
{
  NTSTATUS ntStatus;
  PIO_STACK_LOCATION irpStack;
  PDEVICE_EXTENSION extension;
  PULONG ioBuffer;
  ULONG ioControlCode;
  ULONG port = 0;    

   Irp->IoStatus.Status = STATUS_SUCCESS;
  Irp->IoStatus.Information = 0;
  irpStack = IoGetCurrentIrpStackLocation(Irp);
  extension = DeviceObject->DeviceExtension;
  ioBuffer = Irp->AssociatedIrp.SystemBuffer;
  ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
  switch (ioControlCode)
  {  
  case TEST_SMTH:                
         ioBuffer[0] =(ULONG)DriverEntry;//В буфер обмена адрес функции DriverEntry
         Irp->IoStatus.Information = 4;
        break;

  default:
        Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
        break;
  }

    ntStatus = Irp->IoStatus.Status;
  IoCompleteRequest(Irp, IO_NO_INCREMENT);  
  return ntStatus;
}

 Немного поясню содержимое файла myDrv.c. Итак, по порядку:

#define NT_DEVICE_NAME    L»\Device\myDrv»
#define DOS_DEVICE_NAME            L»\DosDevices\myDrv»- Эти строки служат для задания символических имен текстовыми строками с именами объекта устройства, который будет создан нашим драйвером.

Далее приведены прототипы используемых функций. Этим функциям можно дать произвольные имена, НО их сигнатура (состав параметров с типом возвращаемого значения) жестко заданы системой. [2]:Программная часть драйвера начинается с обязательной функции с именем DriverEntry(), которая автоматически вызывается системой на этапе загрузки драйвера. Эта функция должна содержать все действия по его инициализации. В качестве первого параметра наша функция получает указатель на объект драйвера типа PDRIVER_OBJECT.

При загрузке драйвера системы создается объект драйвера (driver object), олицетворяющий образ драйвера в памяти. С другой стороны, объект драйвера представляет собой структуру, содержащую необходимые для функционирования драйвера данные и адреса функций. В процессе инициализации драйвера (в DriverEntry()) создаются один или несколько объектов устройств (device object), олицетворяющие те устройства, с которыми будет работать драйвер. Этот самый  device object необходим для правильного функционирования драйвера и создается (как и в нашем случае) даже тогда, когда драйвер не имеет отношения к какому-либо реальному устройству.

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

Надо иметь в виду, что объект устройства должен иметь два имени, одно — в пространстве имен NT, другое — в пространстве имен Win32. Эти имена должны представлять собой структуры UNICODE_STRING. Имена объектов устройств составляются  по определенным правилам. NT-имя предваряется префиксом Device, а  Win32-имя — префиксом ?? (или DosDevice). При указании имен в Си-программе знак обратной косой черты удваивается. Для того, чтобы указанное в программе драйвера имя можно было использовать в приложении для открытия устройства, следует создать символическую связь между обоими заданными именами устройств. Для этого используем функцию IoCreateSymbolicLink(). Следующая обязательная операция — создание объекта устройства — осуществляется вызовом функции IoCreateDevice(). Первый параметр этой функции — это указатель на объект драйвера, поступающий в DriverEntry(). Второй параметр определяет размер расширения устройства — структуры, которая служит для передачи данных между функциями драйвера (состав этой структуры произволен и полностью определяется разработчиком). Третий параметр — созданное ранее NT-имя устройства. Далее идут: тип устройства (FILE_DEVICE_UNKNOWN), специальные характеристики (0), FALSE означает, что у нас однопоточное устройство. Наконец, последний параметр является выходным — через него функция возвращает указатель на созданный объект устройства.

Далее необходимо занести в объект драйвера адреса основных функций, включенных программистом в текст драйвера. Массив MajorFunction является одним из элементов структурной переменной. В этот массив мы и заносим адреса основных функций (т.е. функций, которые вызываются системой автоматически в ответ на определенные действия приложения или устройства). Завершается функция оператором return с указанием кода успешного завершения.

Функция DriverUnload() вызывается при выгрузке драйвера. Здесь мы должны выполнить действия по удалению объекта устройства, созданного в DriverEntry().

Функции DriverOpen и DriverClose в нашем случае ничего не делают и возвращают просто STATUSS_SUCCESS. Кстати, все эти функции тоже могут иметь произвольные имена, но передаваемые им параметры строго фиксированы.

[2]:Вот мы и добрались до самой содержательной, с точки зрения прикладного программирования, функции DriverDeviceControl(). Эта функция вызывается каждый раз, когда драйверу приходит IRP-пакет с каким либо IOCTL_ кодом. Грубо говоря, IRP-пакет — это структура, передавая указатель на которую, приложение может общаться с драйвером (как, впрочем, и драйвер может общаться с другим драйвером). Более подробное описание того, что такое IRP-пакет, можно найти здесь http://www.lcard.ru/~gorinov/texts/irp99.html.

IRP-пакет содержит так называемый системный буфер, служащий для обмена информацией (переменная SystemBuffer). Таким образом, нам надо получить доступ к IRP-пакету, а через него к SystemBuffer. Для этого объявляем переменную irpStack типа указателя на стековую область ввода-вывода PIO_STACK_LOCATION, и, кроме того, переменная ioBuffer, в которую будет помещен адрес системного буфера обмена. В нашем случае тип этой переменной — PULONG, в действительности тип передаваемых данных может быть каким угодно. С помощью функции IoGetCurrentIrpStackLocation() в переменную irpStack помещается адрес стековой области ввода-вывода, а в переменную ioBuffer заносится адрес системного буфера из структуры IRP. Системный буфер входит в объединение (union) с именем AssociatedIrp, поэтому мы используем конструкцию Irp->AssociatedIrp.SystemBuffer. Конструкция switch-case анализирует содержимое ячейки IoControlCode и в зависимости от значения кода выполняет те или иные действия. В нашей программе только один код действия TEST_SMTH. Засылка в буфер обмена адреса функции DriverEntry() осуществляется через указатель на этот буфер. В переменную Irp->IoStatus.Information заносим количество (4) пересылаемых байт. Для завершения IRP-пакета вызываем IoCompleteRequest().

Итак, драйвер мы написали. Теперь надо его скомпилировать. Т.к. процесс компиляции идет из командной строки, то для этой цели гораздо удобнее пользоваться bat-файлом. Создадим небольшой bat-файл с именем, допустим, Crt.bat и со следующим содержанием:

%SystemRoot%system32cmd.exe /c «cd C:NTDDKbin&&setenv.bat C:NTDDK&&cd    c:myDriver &&build -ceZ»
pause

NTDDK у меня установлено в корень С:, если у Вас по-другому, то вместо C:NTDDKbin и C:NTDDK пропишите полные пути к соответствующим папкам.

Итак, теперь запустим наш Crt.bat. После окончания компиляции в папке C:myDriverobjfrei386 находим готовый драйвер myDrv.sys. Наш драйвер пока умеет только лишь загружаться/выгружаться и по специальному запросу посылает приложению адрес одной из своих процедур.

    Теперь займемся написанием приложения, работающего с нашим драйвером. Еще раз напоминаю, что мы работаем под Win2000. Эта ОС позволяет реализовать динамическую загрузку/выгрузку драйвера.

Точнее, динамическую загрузку/выгрузку служб (service), но т.к. в Win2000 в качестве службы можно рассматривать и драйвер, я буду использовать оба эти термина, подразумевая, в данной статье, наш драйвер.

Для загрузки и выгрузки драйверов используется диспетчер управления службами SC Manager (Service Control Manager). Прежде чем вы сможете работать с интерфейсом SC, вы должны получить дескриптор диспетчера служб. Для этого необходимо обратиться к функции OpenSCManager(). Дескриптор диспетчера служб необходимо использовать при обращении к функциям CreateServise() и OpenService(). Дескрипторы, возвращаемые этими функциями, необходимо использовать при обращении к вызовам, имеющим отношение к конкретной службе. К подобным вызовам относятся функции ControlService(), DeleteService() и StartService(). Для освобождения дескрипторов обоих типов используется вызов CloseServiceHandle().

Загрузка и запуск службы подразумевает выполнение следующих действий:

  • Обращение к функции OpenSCManager() для получения дескриптора диспетчера
  • Обращение к CreateServise() для того, чтобы добавить службу в систему. Если такой сервис уже существует, то CreateServise() выдаст ошибку с кодом 1073 (код ошибки можно прочитать GetLastError()) Данная ошибка означает, что сервис уже существует и надо вместо CreateServise() использовать OpenService().
  • Обращение к StartService() для того, чтобы перевести службу в состояние функционирования.
  • Если служба запустилась успешно, то можно вызвать CreateFile() для получения дескриптора, который мы будем использовать уже непосредственно при обращении к драйверу.
  • И по окончании работы не забудьте дважды обратиться к CloseServiceHandle() для того, чтобы освободить дескрипторы диспетчера и службы.

Если на каком-то шаге этой последовательности возникла ошибка, нужно выполнить действия, обратные тем, которые вы успели выполнить до ошибки. Надо помнить о том, что при обращении к функциям, подобным CreateServise(), необходимо указывать полное имя исполняемого файла службы (в нашем случае полный путь и имя myDrv.sys).

Посмотрим на исходный текст простого консольного приложения, написанного на Visual C++  6.0:

#include <conio.h>
#include «LoadDRV.h»
#include <string.h>
#include <stdio.h>

void main()
 {
  LPTSTR m_name = new char[20];
  strcpy(m_name, «myDrv.sys»);

  if (drvLoad(m_name)) TestSmth();

    drvUnLoad(m_name);
  delete m_name;  
  }

//——————————————————————————
//создаем и посылаем драйверу  IRP-запрос
int TestSmth(void)//0x800 + 101
{
  int test = 0;
  DWORD ReturetLength = 0;

  DeviceIoControl(hDevice, IOCTL_TEST_SMTH, NULL, 0,
                           &test, 4, &ReturetLength, NULL);

    printf(«TestSmth= %in»,test);
  return test;
}

///**************Функции динамической загрузки************************
bool drvLoad(char* name)
{
printf (name);
hSCManager=NULL;
hService=NULL;
bool status;

status=FALSE;


  if(OpenManager())
{
if(drvCreateService(name))
{
if(drvStartService(name))
{
status=TRUE;
printf(«n Driver is now load…n»);
}
}
}

hDevice = CreateFile («//./myDrv», GENERIC_READ,
                              FILE_SHARE_READ | FILE_SHARE_WRITE,
                              NULL, OPEN_EXISTING,
                              FILE_ATTRIBUTE_NORMAL, NULL);

return status;
}

//———————————————————————

bool OpenManager()
{
bool status;
status=FALSE;

   if(hSCManager!=NULL)
   {
        CloseServiceHandle (hSCManager);
        hSCManager=NULL;
   }

hSCManager=OpenSCManager (NULL,NULL,SC_MANAGER_ALL_ACCESS);
if (hSCManager == NULL)
{
  error(_OpenSCManager);
} else status=TRUE;

return status;
}

//———————————————————————
bool drvCreateService(PCHAR pDrvName)
{
   LPTSTR lpBuffer;
  lpBuffer = new char[256];
  bool status = FALSE;
  LPTSTR awPath; // путь к драйверу с именем pDrvName

  // формируем путь к  pDrvName, драйвер должен лежать рядом с exe-шником

      GetCurrentDirectory(256, lpBuffer);
   strcat(lpBuffer,»\»);
   strcat(lpBuffer,pDrvName);
   awPath = lpBuffer;

hService = CreateService(hSCManager,pDrvName,pDrvName,SERVICE_ALL_ACCESS,    SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,awPath,NULL,NULL,NULL,NULL,NULL);

        if(!hService)
        {
     error(_CreateService);
                 status = drvOpenService(pDrvName);//Пытаемся открыть службу
        }
        else status=TRUE;

  delete lpBuffer;
return status;
}

//———————————————————————
bool drvOpenService(PCHAR name)
{

bool status;
status=FALSE;
if(hService!=NULL) CloseService();

hService=OpenService(hSCManager,name,SERVICE_ALL_ACCESS);
if (!hService) error(_OpenService);
else status=TRUE;
return status;
}

//———————————————————————
bool drvStartService(PCHAR name)
{
bool status;
status=FALSE;
      if(!StartService(hService,0,NULL))
{
               error(_StartService);
   printf(«Deleting service…»);
               drvDeleteService(name)
}
else status=TRUE;
return status;
}

//———————————————————————
bool drvDeleteService(PCHAR name)
{
bool status;
status=FALSE;
CloseService();
if(!DeleteService(hService)) error(_DeleteService);
else status=TRUE;
return status;
}

//——————————————————————-
void CloseService()
{
CloseServiceHandle (hService);
hService=NULL;
}

//——————————————————————-
int drvUnLoad(PCHAR name)
{
int status;
status=FALSE;

if (hDevice!=INVALID_HANDLE_VALUE)
{
      if(!CloseHandle(hDevice)) error(_CloseHandle);
 hDevice=INVALID_HANDLE_VALUE;
}

if (hService)
    {
    status = ControlService(hService,SERVICE_CONTROL_STOP,&ServiceStatus);
      if(!status)  error(_SERVICE_CONTROL_STOP);

     status = DeleteService(hService);
      if(!status) error(_DeleteService);

         status = CloseServiceHandle(hService);
      if(!status) error(_CloseServiceHandle);
    }

  if(!CloseServiceHandle(hSCManager)) error(_CloseServiceHandle);
  if (status) printf(«Driver Unload… SUCCESSn»);
  return status;
}

//———————————————————————
void error(error_index erIndex)
{

DWORD err;
err=GetLastError();
switch(erIndex)
{
 case _OpenSCManager:
 printf(«OpenSCManager failed with Error=%in»,err);
 break;

 case _GetFullPathName:
printf(«GetFullPathName failed with Error=%in»,err);
 break;

 case _CreateService:
 switch (err)
 {
 case 1073:
 printf(«The specified service already exists.n»);
 printf(«opening this service…»);
 break;
 default:
 printf(«CreateService failed with Error=%in»,err);
 }
 break;

 case _OpenService:
 printf(«OpenService failed with Error=%in»,err);
 break;

 case _StartService:
 printf(«StartService failed with Error=%in»,err);
 break;
 
 case _DeleteService:
 printf(«DeleteService failed with Error=%in»,err);
 break;
 
 case _SERVICE_CONTROL_STOP:
 printf(«SERVICE_CONTROL_STOP failed with Error=%in»,err);
 break;
 case _CreateFile:
 printf(«CreateFile failed with Error=%in»,err);
 break;
 case _CloseHandle:
 printf(«CloseHandle failed with Error=%in»,err);
 break;
 case _CloseServiceHandle:
 printf(«CloseServiceHandle failed with Error=%in»,err);
 break;

}
}

И содержимое h-файла этого приложения:

#define FIRST_IOCTL_INDEX  0x800
#define FILE_DEVICE_myDrv  0x00008000

                                        #define TEST_SMTH   CTL_CODE(FILE_DEVICE_myDrv,  \
                                          0x800 + 101,  \
                                          METHOD_BUFFERED,       \
                                          FILE_ANY_ACCESS)

Это приложение загружает драйвер, файл которого лежит в одной папке, что и exe-файл данного приложения.

Посмотрим на исходный текст:

В main() мы создаем переменную с именем драйвера (myDrv.sys) и передаем это имя в функцию динамической загрузки драйвера drvLoad(), которая выполняет все необходимые действия по работе с менеджером служб, и в конце вызывает CreateFile(), которая возвращает дескриптор, нужный для работы с драйвером как файловым объектом. Этот дескриптор, в частности, используется при вызове функции DeviceIoControl.

Если драйвер загружен успешно, то вызываем функцию TestSmth(), внутри которой мы создаем и посылаем драйверу IRP-пакет (с помощью вызова DeviceIoControl()). Приняв этот пакет, наш драйвер возвращает адрес своей процедуры DriverEntry. После этого выгружаем драйвер. Все.

Итак, мы написали простейший (дальше некуда) драйвер и приложение, работающее с ним. В этой статье я активно цитировал материалы из книги П. И. Рудаков, К. Г. Финогенов, по возможности делая ссылки на нее в виде:  [2].

Через некоторое время напишу о том, как на базе этого драйвера написать драйвер-фильтр для LPT-порта.

Артем.

  • Как создать загрузочную флешку mac os el capitan из под windows
  • Как создать дискету сброса пароля на windows 7 если дискета не создана
  • Как создать дополнительный аккаунт в windows 10
  • Как создать загрузочную флешку windows 10 для ноутбука asus
  • Как создать загрузочную флешку ios на windows