• Уменьшение отступа

    Обратная связь

    (info@ru-sfera.pw)

Windows Kernel Programming:Глава 2.Начало работы с инструментами разработчика ядра


virt

Просветленный
Просветленный
Регистрация
24.11.2016
Сообщения
706
Репутация
228
Это вторая часть, с первой частью можно ознакомиться здесь:Windows Kernel Programming:Глава 1.Основные моменты и архитектура ядра

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

В этой главе:

• Установка инструментов.
• Создание проекта драйвера.
• Описание процедур DriverEntry и Unload.
• Разработка драйвера.
• Простая трассировка.

1)Установка инструментов

В старые времена (до 2012 года) процесс разработки и сборки драйверов включал использование особого инструмента для сборки из комплекта драйверов устройств (DDK).
Нужно-было качать специальный пакет компиляторов, далее в командной строке собирать драйвер, не было никакой интеграции в Visual Studio.
Были некоторые обходные пути, но ни один из них не был ни идеален, ни официально поддержан.

К счастью, начиная с Visual Studio 2012 и Windows Driver Kit 8, Microsoft начала официально поддерживать сборку драйверов с помощью Visual Studio (и msbuild), без необходимости использовать отдельный компилятор и инструменты сборки.
Чтобы начать разработку драйверов, необходимо установить следующие инструменты (в следующем порядке):

• Visual Studio 2017 или 2019 с последними обновлениями.
Убедитесь, что C ++ выбрана во время установки. Обратите внимание, что подойдет любой SKU, включая бесплатная версия Community.
• Windows 10 SDK.
• Windows 10 Driver Kit (WDK).
• Инструменты Sysinternals, необходимы для отладки драйверов, можно скачать бесплатно на . Нажмите на Sysinternals Suite слева от этой веб-страницы и загрузите файл Sysinternals Suite. Распакуйте в любую папку, и инструменты готовы к работе.

Быстрый способ убедиться, что шаблоны WDK установлены правильно, - это открыть Visual Studio и выберите «Новый проект» и найдите проекты драйверов, например «Пустой драйвер WDM».

2)Создание проекта драйвера

При наличии вышеуказанных пакетов можно создать новый проект драйвера.
Шаблон, который вы будете использовать в этом разделе «Пустой драйвер WDM».
Рисунок 2-1 показывает, как выглядит диалог New Project для этого типа драйвера в Visual Studio 2017. На рисунке 2-2 показан тот же начальный мастер в Visual Studio 2019.

1582806506252.png

Рисунок 2-1.Новый проект драйвера в Visual Studio 2017
1582806506294.png

Рисунок 2-2.Новый проект драйвера в Visual Studio 2019
После создания проекта в обозревателе решений отображается один файл - Sample.inf. Вам не нужен этот файл в этом примере, так что просто удалите его.

Теперь пришло время добавить исходный файл. Щелкните правой кнопкой мыши узел «Исходные файлы» в обозревателе решений и выберите:

Добавить/Новый элемент ... из меню Файл.

Выберите исходный файл C ++ и назовите его Sample.cpp. Нажмите ОК, чтобы создать файл.

2)Функции DriverEntry и Unload

По умолчанию каждый драйвер имеет точку входа DriverEntry.
Это можно считать «main» драйвера, если сопоставить с классическим основным приложением пользовательского режима.

DriverEntry имеет следующий прототип, показанный здесь:
C:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);

_In_ аннотации являются частью языка аннотаций исходного кода (SAL). Эти аннотации прозрачны для компилятора, но предоставляют метаданные, полезные для читателей и инструментов статического анализа.

Мы постараемся максимально использовать их для улучшения ясности кода.
DriverEntry может просто вернуть успешный статус, например так:
C:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
return STATUS_SUCCESS;
}

Этот код еще не скомпилируется. Во-первых, вам нужно включить заголовок, который имеет определения для типов, присутствующих в DriverEntry.
Например так:

#include <ntddk.h>

Теперь код имеет больше шансов на компиляцию, но все равно потерпит неудачу.

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

Эти предупреждения могут быть устранены путем полного удаления имен аргументов (или комментирования их), что хорошо для файлов C ++.
Существует еще один классический способ решения этой проблемы, который заключается в использовании макроса UNREFERENCED_PARAMETER:
C:
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}

Вот объявление данного макроса:

#define UNREFERENCED_PARAMETER(P) (P)

Сборка проекта теперь компилируется нормально, но вызывает ошибку компоновщика.

Функция DriverEntry должна-быть C-linkage, который не используется по умолчанию при компиляции C ++.
Вот финальная версия успешной сборки драйвера, состоящего только из функции DriverEntry:
C:
extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}

В какой-то момент драйвер может быть выгружен.
В то время все, что сделано в функции DriverEntry должно быть отменено.

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

DriverObject->DriverUnload = SampleUnload;

Процедура выгрузки принимает объект драйвера (тот же, что передан в DriverEntry).
Поскольку пример нашего драйвера ничего не делает с точки зрения распределения ресурсов в DriverEntry, то и в процедуре Unload ничего не нужно делать, поэтому мы можем пока оставить ее пустой:
C:
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
UNREFERENCED_PARAMETER(DriverObject);
}
Вот полный код драйвера на данный момент:
C:
#include <ntddk.h>

void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
   UNREFERENCED_PARAMETER(DriverObject);
}

extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = SampleUnload;
return STATUS_SUCCESS;
}

3)Развертывание и запуск драйвера

Теперь у нас есть успешно скомпилированный файл драйвера Sample.sys, давайте установим его в систему, а затем загрузим его.
Обычно лучше устанавливать и загружать драйвер на виртуальную машину, чтобы устранить риск сбоя вашей основной машины.

Установка программного драйвера, как и установка службы в пользовательском режиме, требует вызова сервисного API с правильными аргументами или с использованием существующих инструментов.

Один из известных инструментов для этого программа Sc.exe, это встроенный в Windows инструмент для управления сервисами. Мы будем использовать этот инструмент для установки и затем загрузим драйвер.
Обратите внимание, что установка и загрузка драйверов является привилегированной операцией, обычно разрешено только для администраторов.

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

sc create sample type= kernel binPath= c:\dev\sample\x64\debug\sample.sys

Обратите внимание, что между типом и знаком равенства нет пробела, а между знаком равенства и kernel пробел. То же самое касается второй части.
Если все идет хорошо, результат должен указывать на успех. Чтобы проверить установку, вы можете открыть реестр редактор (regedit.exe) и найдите драйвер в HKLM\System\CurrentControlSet\Services\Sample.

Рисунок 2-3 показывает снимок экрана редактора реестра после предыдущей команды.

1582806506342.png

Рисунок 2-3. Снимок экрана редактора реестра

Чтобы загрузить драйвер, мы можем снова использовать инструмент Sc.exe, на этот раз с параметром запуска, который использует API-интерфейс StartService для загрузки драйвера (тот же API, который используется для загрузки служб).

Однако на 64 бит системные драйверы должны быть подписаны, и поэтому обычно следующая команда не будет работать:

sc start sample

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

С командным окном с повышенными правами тестовый режим может быть включен следующим образом:

bcdedit /set testsigning on

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

ВАЖНО:

Если вы тестируете в Windows 10 с включенной безопасной загрузкой, нельзя включить тестовый режим.
Это одно из свойств, защиты Secure Boot (также защищен от локальной отладки ядра).

Если вы не можете отключить безопасную загрузку через настройки BIOS, из-за ИТ-политики или по какой-то другой причине, ваш лучший вариант - это тестирование на виртуальной машине.

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

Диалоговое окно, как показано на рисунке 2-4.
Обратите внимание, что я выбрал все конфигурации и все платформы, чтобы при переключении конфигураций (Debug / Release) или платформ (x86 / x64 / ARM / ARM64), настройка сохранялась.

1582806506377.png

Рисунок 2-4. Настройки целевой системы, под которую будет собираться драйвер

Когда тестовый режим включен и драйвер загружен, вы должны увидеть следующее (sc state sample):

1582806506408.png


Это означает, что все хорошо, и драйвер загружен. Чтобы это проверить, мы можем открыть Process Explorer и найти Sample.sys в процессе System:

1582806506444.png

Рисунок 2-5. Скриншет Process Explorer и поиск драйвера Sample

Что-бы выгрузить драйвер, достаточно ввести команду:sc stop sample.
Выгрузка драйвера вызывает вызов процедуры Unload, которая в этом драйвере ничего не делает.
Вы можете убедиться, что драйвер действительно выгружен, снова взглянув на Process Explorer.

4)Простая трассировка

Как мы можем точно знать, что процедуры DriverEntry и Unload действительно выполнены ?
Давайте добавим базовое отслеживание этих функций. Драйверы могут использовать макрос KdPrint для вывода текста в стиле printf, который можно просмотреть с помощью отладчика ядра и других инструментов.

KdPrint - это макрос, который компилируется только в Debug,создает и вызывает базовый API ядра DbgPrint.

Пример обновленного драйвера, который использует отладочные выводы:
C:
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {

UNREFERENCED_PARAMETER(DriverObject);
KdPrint(("Sample driver Unload called\n"));
}

extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {

UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = SampleUnload;
KdPrint(("Sample driver initialized successfully\n"));

return STATUS_SUCCESS;
}

Обратите внимание на двойные скобки при использовании KdPrint. Это необходимо, потому что KdPrint является макросом, но по-видимому, принимает любое количество аргументов, а-ля printf.
Поскольку макросы не могут получить переменное число аргументов, трюк компилятора используется для вызова реальной функции DbgPrint.

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

Перед запуском DebugView вам нужно сделать некоторые приготовления.

Во-первых, начиная с Windows Vista, вывод DbgPrint фактически не генерируется, если в реестре не указано определенное значение.

Вам нужно добавить ключ с именем Debug Print Filter в HKLM\SYSTEM\CurrentControlSet\Control\Session Manager

Ключ обычно не существует. В этом новом ключе добавьте значение DWORD с именем DEFAULT (это не значение по умолчанию, которое существует в любом ключе) и установите его значение равным 8

Рисунок 2-6 показывает настройку в RegEdit.
К сожалению, вам придется перезагрузить систему чтобы этот параметр вступил в силу.

1582806506477.png

Рисунок 2-6. Включение отображение дебажного вывода в драйвере

После применения этого параметра запустите DebugView (DbgView.exe) с повышенными правами.
В меню «Параметры» убедитесь, что выбрано Capture Kernel (или нажмите Ctrl + K).

Соберите драйвер, если вы этого еще не сделали. Теперь вы можете снова загрузить драйвер.
Вы должны увидеть выходные данные в DebugView, как показано на рисунке 2-7.
Третья строка вывода получена из другого драйвера и не имеет ничего общего с нашим примером драйвера.

1582806506504.png

Рисунок 2-7. Вывод утилиты DebugView

Упражнения

Добавьте код в образец DriverEntry для вывода версии ОС Windows: мажорной, минорной и номер сборки. Используйте функцию RtlGetVersion для получения информации. Проверьте результаты с DebugView.

Резюме

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

В следующей главе мы рассмотрим основные API ядра, концепции и структуры.
 
Последнее редактирование модератором:

virt

Просветленный
Просветленный
Регистрация
24.11.2016
Сообщения
706
Репутация
228
Решение задания, вывод версии системы, примерно так:
Код:
#include <ntddk.h>

void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {

    UNREFERENCED_PARAMETER(DriverObject);
    KdPrint(("Sample driver Unload called\n"));
}

extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {

    OSVERSIONINFOW lpVersionInformation;

    UNREFERENCED_PARAMETER(RegistryPath);
    DriverObject->DriverUnload = SampleUnload;
    KdPrint(("Sample driver initialized successfully\n"));

    //Вывод мажорной/минорной версии ядра
    int error = RtlGetVersion(&lpVersionInformation);
    if (error == STATUS_SUCCESS) {
        KdPrint(("OS version major:%x\n", lpVersionInformation.dwMajorVersion));
        KdPrint(("OS version minor:%x\n", lpVersionInformation.dwMinorVersion));
        KdPrint(("OS build version:%x\n", lpVersionInformation.dwBuildNumber));
        KdPrint(("OS Platform:%x\n", lpVersionInformation.dwPlatformId));
    }

    return STATUS_SUCCESS;
}

И сразу опечатки, команду создания сервиса нужно вводить без пробелов, так:
Код:
sc create sample type=kernel binPath=c:\dev\sample\x64\debug\sample.sys

Для вывода дебажных принтов, нужно добавить в такую ветку:
Вам нужно добавить ключ с именем Debug Print Filter в HKLM\SYSTEM\CurrentControlSet\Control\Session Manager

1582806506477-png.59479


Ну и в Visual Studio нужно выбрать проект "Kernel Mode Driver Empty" (KMDF).

Проделал то-что в статье, всё работает остальное.

Может кому пригодится мануалл.)))
 

Maks Maksov

Пользователь
Форумчанин
Регистрация
10.09.2020
Сообщения
15
Репутация
5
для входной процедуры DriverEntry используется (как её правильно назвать) лексема extern "C" . я так понимаю это даёт указание компилятору что это точка входа при старте. однако где-то еще я встречал что некоторые другие процедуры драйвера тоже ее используют. При этом созданный по шаблону в визуал студии драйвер не имеет этой лексемы. при этом не теряет работоспособности. а если самому драйвер собирать с пустого модуля то она необходима.
моё знакомство с Ц++ в самом начале- не могли бы вы прояснить и небольшой абзац мануальчик дать. почему некоторые процедуры (а не только драйвер ентри) нуждаются в этой строке, как шаблонный драйвер обходится без нее? если несколько процедур используют эту строку то как отличить где точка входа? в общем прояснить как это работает
 

X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 068
Репутация
8 176
Вы немного неправильно это поняли.)

extern "C" говорит компилятору, что глобальная функция будет доступна в си стиле, в C++ там метки функции по другому просто называются в ассемблере...

Пример для понимания:

- Функция в с++ стиле:
C:
extern void qqq (void);
void foo (void)
{
  qqq ();
}

В дизасемблере:

...
call _Z3qqqv
...

В Си стиле:
C:
extern "C" void qqq (void);
void foo (void)
{
  qqq ();
}

В дизасемблере:

...
call qqq
...
Как видно, если поставить extern "C", функцию можно вызывать из сторонних модулей по ее имени, а если ее нет, то увы нельзя и она будет доступна по метки какой-то такой _Z3qqqv.

Короче это нужно для доступа этой функции в C++ программе из модуля который написан на си.
 

Maks Maksov

Пользователь
Форумчанин
Регистрация
10.09.2020
Сообщения
15
Репутация
5
А почему именно DriverEntry получает управление при старте? а если изменить название у нее? почему в случае простейшего драйвера в 10 строк требуется extern "C" ведь нет кода который вызывает эту DriverEntry? простите моё невежество. но не смог толковое объяснение этих вопросов найти нигде.
 

X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 068
Репутация
8 176
По умолчанию каждый драйвер имеет точку входа DriverEntry.
Это можно считать «main» драйвера, если сопоставить с классическим основным приложением пользовательского режима.

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

Про extern "C" в данном случае, можно и без него, думаю будет работать.)
 
Автор темы Похожие темы Форум Ответы Дата
X-Shar Windows Kernel Programming 0
X-Shar Windows Kernel Programming 0
X-Shar Windows Kernel Programming 0
X-Shar Windows Kernel Programming 3
X-Shar Windows Kernel Programming 2
X-Shar Windows Kernel Programming 3
X-Shar Windows Kernel Programming 6
X-Shar Windows Kernel Programming 19
virt Windows Kernel Programming 9
virt Windows Kernel Programming 1
virt Windows Kernel Programming 1
X-Shar Windows Kernel Programming 8
Верх Низ