Ядерная архитектура ОС Windows

1.

Основы ядерной архитектуры
ОС Windows

2.

Применение знаний ядра ОС
• Средства защиты информации – системы
шифрования сетевого трафика, защита данных
на диске (виртуальные диски, фильтрдрайверы, защищенные файловые системы)
• Вредоносное ПО
• Средства анализа в самом широком смысле
(антивирусы, отладчики, системные утилиты)
• Знание ядерной архитектуры важно для
аналитика программного обеспечения

3.

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

4.

Структура курса
Лекции + лабораторные задания:
• простейший драйвер ядра
• создание устройств и символических ссылок драйвером
• консольная прикладная программа для обращения к драйверу
• диалоговая прикладная программа управления драйвером
• взаимодействие драйвера с прикладной программой: Read,
Write, DeviceIoControl
• драйвер-фильтр клавиатуры
• перехват системных сервисов
• Получение копии адресного пространства чужого процесса – на
уровне VAD и на уровне каталога страниц

5.

Программное обеспечение и
литература
• Среда разработки для выполнения
лабораторных работ развернута на виртуальной
машине VirtualBox
Там же:
• Слайды к лекциям
• Задания на лабораторные работы
• Дополнительная литература
– Внутреннее устройство Windows, 7-е издание.
Руссинович, Соломон, Ионеску, Йосифович, 2018
– Programming the Microsoft Windows Driver Model, 2-е
издание. Walter Oney, 2003

6.

Среда разработки
VisualStudio 2008/2010 (WS 2010 не рекомендуется при отсутствии
подключения к интернету)
• Пакет WDK 7.1.0, поддерживает все ОС начиная с WinXP
• Для интеграции WDK с VisualStudio – сторонний продукт VisualDDK
(http://visualddk.sysprog.org), без него – утилита build, работающая в режиме
командной строки
либо
• VisualStudio2019 + пакет WDK for Windows 10 version 2004 (по состоянию на
01.09.2020)
Утилиты SysInternals (www.sysinternals.com – сейчас часть проекта MS
TechNet) – как минимум DbgView, WinObj, ProcExp, ProcMon, AutoRuns.
Лучше брать SysinternalsSuite.
Примечание: В пакете WDK для Windows 8 впервые официально заявлена
возможность использования IDE (Integrated Development Environment)
VisualStudio, но с рядом ограничений (в частности не поддерживается разработка
драйверов для XP, поддерживает только WDF/KMDF-драйверы. Но фактически
создание Legacy-драйверов все равно возможно через мастер создания WDMдрайвера, также оставлена поддержка консольной сборки

7.

Основные понятия
• Процессы, потоки и адресные пространства в ОС
Windows: взаимосвязь с процессорной архитектурой
• Архитектура процессора в защищенном режиме
предоставляет для использования разработчикам ОС
некоторые механизмы:
• Защита памяти («кольца защиты»)
• Сегментация
• Cтраничная организация памяти
• Многозадачность (task switching)
• особенности использования этих процессорных
механизмов во многом определяют архитектуру ОС, в
особенности рассматриваемые понятия: термины
процесс, поток и адресное пространство имеют смысл
только в рамках конкретной ОС

8.

Защита памяти по привилегиям
(«кольца защиты»)
3
Передача управления
User
mode
2
1
0
Доступ к данным
Kernel
mode
Важной особенностью реализации механизма колец защиты является то, что для
каждого кольца защиты хранится своя копия стековых регистров (ESP), и при
переключении кольца защиты аппаратура процессора осуществляет замену
текущего значения ESP на сохраненную копию, соответствующую новому кольцу
защиты.

9.

Особенности организации памяти в Windows на
примере 32-разрядной адресации
Вирт. адрес

10.

Трансляция памяти при использовании
гипервизора с включенным EPT
Виртуальная машина
TLB
(буфер
ассоциативной
трансляции)
Вирт. адрес
Сегментное
селектор:смещениепреобразование
гипервизор
Линейный
адрес
Страничное
преобразование
Физич.
адрес
EPT
Extended
Page Tables
Hostадрес
VMCS
GDT/LDT
EPT
Base
Register
CR3
Intel:
двойной гипервизор
Вариант без EPT: виртуальный TLB в гипервизоре
SMM

11.

ОС NT, хотя и использует селекторы, но использует их в минимальной степени.
NT реализует плоскую 32-разрядную модель памяти с размером линейного
адресного пространства 4 Гб (232 байт).
В NT определено 11 селекторов, из которых нас будут интересовать всего 4:
Селектор
Hex (bin)
Назначение
база
предел
DPL
тип
(RPL)
08 (001000)
Code32 (kernel)
00000000
FFFFFFFF
0 (kernel)
RE
10 (010000)
Data32 (kernel)
00000000
FFFFFFFF
0 (kernel)
RW
1b (011011)
Code32 (user)
00000000
FFFFFFFF
3 (user)
RE
23 (100011)
Data32 (user)
00000000
FFFFFFFF
3 (user)
RW
Эти 4 селектора позволяют адресовать все 4Гб линейного адресного пространства,
разница только в режиме доступа.
Первые 2 селектора имеют DPL=0 и используются драйверами и системными
компонентами для доступа к системному коду, данным и стеку.
Вторые 2 селектора используются кодом пользовательского режима для доступа к
коду, данным и стеку пользовательского режима. Эти селекторы являются
константами для ОС NT.
Сегментное преобразование пары селектор:смещение дает 32-битный линейный
адрес, для всех рассматриваемых селекторов совпадающее со значением смещения
виртуального адреса.

12.

• Реальная защита памяти по привилегиям, как и реализация понятия
адресное пространство, осуществляется посредством механизма
страничной организации памяти:
• Каждый контекст памяти (адресное пространство процесса) описывается
своим каталогом страниц. Физический адрес текущего каталога страниц
содержится в регистре CR3
• элемент таблицы страниц содержит бит, указывающий на возможность
доступа к странице из пользовательского режима (0 - доступ только
ядру, 1 - доступ пользовательскому режиму). При этом все страницы
доступны из режима ядра. Таким образом, механизм защиты страниц
использует всего 2 уровня привилегий - 0 (ядро) и 3 (пользователь). С
помощью этого механизма адресное пространство делится на 2 части:
системную и пользовательскую.
• элемент таблицы страниц содержит бит, указывающий на возможность
записи в соответствующую страницу памяти (0 - доступ только на
чтение, 1 - чтение/запись). Чтение возможно всегда, когда разрешен доступ
по привилегиям.
• В некоторых режимах элемент таблицы страниц содержит бит,
определяющий соответствующую страницу как данные (запрет на
выполнение кода с данной страницы) – см. DEP, NX bit, XD bit

13.

Виртуальное адресное пространство
процесса
Границу между
пользовательским и
системным
диапазоном можно
сдвинуть
Адресное
пространство в
32 разрядном
режиме
0
0
Код и
данные
прикладных
программ
Модули и
объекты
прикладной
программы
user
2Гб
Код и
данные
ядра, память
устройств
4Гб
Взаимодействие между
кодом пользовательского
режима и ядром часто
изображают так:
user
kernel
kernel
3Гб
4Гб
user
kernel
Модули и
объекты ядра

14.


Каталоги страниц всех процессов организованы так, что преобразование
линейного в физический адрес для системного диапазона любого адресного
пространства совпадают – диапазон ядра «общий» для всех адресных
пространств
Адресное
пространство 1
Адресное
пространство N
Код и
данные
процесса 1
Код и
данные
процесса N
0

user
kernel
ядро
4Гб

15.

16.


Каталоги страниц позволяют создавать «совместно используемые» как внутри
одного, так и между несколькими адресными пространствами области памяти
путем трансляции разных виртуальных адресов в одинаковые физические.
Физическая
память
Адресное
пространство 1
Адресное
пространство 2

17.


Разновидность такого механизма – copy-on-write – используется для
реализации механизма отображения файлов в память, через который в свою
очередь подгружаются в адресное пространство все исполняемые модули .exe, .dll, .sys и т.п.
Изначально все страницы адресных
пространств, соответствующие
исполняемому модулю, отображены в общий
набор страниц физической памяти
При попытке записи в какую-либо из таких
страниц в одном из адресных пространств
это адресное пространство вначале получает
свою уникальную копию этой страницы в
физической памяти, и только затем в эту
копию осуществляется запись
Физическая
память
Адресное
пространство 1
Физическая
память
Адресное
пространство 2
Адресное
пространство 1
модификация
Адресное
пространство 2
Исходная
копия
страницы
Модиф.
копия
страницы

18.

Загрузка модулей в адресное пространство
Исполняемые файлы в ОС Windows имеют формат PE (Portable Executable).
При запуске программы создается адресное пространство процесса, причем
системный диапазон адр. простр. запускаемого процесса создается простым
копированием части каталога страниц запускающего процесса.
Затем в пользовательский диапазон памяти отображается исполняемый модуль
(обычно .exe) в соответствии с предпочтительным адресом загрузки, прописанном в
PE-заголовке (задается компилятором, для MS VisualStudio обычно 0x400000).
Далее в соответствии с таблицей импорта модуля начинается подгрузка (также
через механизм отображения в память) всех динамических библиотек, с которыми
связан текущий загружаемый модуль. Каждая такая библиотека (обычно .dll) также
имеет формат PE, и для нее весь процесс повторяется заново.
Если предпочтительный адрес загрузки уже занят, загрузчик перебазирует модуль
на свободный участок виртуальной памяти. Кроме того, компилятор может
создавать неперебазируемые модули, которые обязаны грузиться строго по
указанному в заголовке адресу (обычно используется для некоторых модулей ядра).
Сложность перебазирования определяется необходимостью загрузчику
модифицировать инструкции, осуществляющие обращения по глобальным адресам
(например при доступе к глобальным переменным) – компилятор формирует адреса
таких переменных исходя из назначенного им же базового адреса загрузки, при
перебазировании все такие адреса должны быть модифицированы. Список адресов
кода, в которые требуется внести поправки при перебазировании, хранится в секции
поправок (relocation table).

19.


При завершении загрузки модуля со всеми его зависимостями управление
передается на функцию – точку входа в модуль, прописанную в его PEзаголовке (это касается всех загружаемых модулей).
После отображения в память содержимое файла все еще не прочитано с диска.
Такой файл начинает выступать в роли файла подкачки, а реальное чтение
страниц произойдет при первом обращении к соответствующему адресу
виртуальной памяти.
Даже после выгрузки модуля ОС по возможности сохраняет информацию о
выполненном ранее отображении файла и его физические страницы, для
ускорения возможной повторной загрузки модуля.
Перебазирование модуля – почти катастрофическая ситуация для ОС,
поскольку вносимые поправки приводят к увеличению страниц физической
памяти за счет срабатывания механизма copy-on-write. Кроме того, для
перебазированного модуля становится затруднительным использовать
оставшиеся от предыдущей загрузки страницы памяти.
В ОС начиная с Vista появился механизм Address Space Layout Randomization
(ASLR), предназначенный для усложнения атак со стороны вредоносного
кода, направленных на прямой доступ к коду и данным (в т.ч. стеку)
системных библиотек. В соответствии с этим механизмом, модули
размещаются по «случайно» выбираемым адресам.
В силу указанных выше проблем с перебазированием ОС не может допустить
случайное размещение модуля в разных адресных пространствах. Вместо
этого «случайные» адреса загрузки всех модулей вычисляются разово для
всех модулей всех адресных пространств, и в ходе текущего сеанса работы
ОС не пересчитываются.

20.

21.

Поддержка многозадачности
• Механизм аппаратной поддержки многозадачности, предоставляемый
архитектурой x86 (task switching) – в Windows не используется при
штатном функционировании ОС. Единственный случай его
применения – при обработке BSOD (“синий экран”), т.е. при
фатальном сбое в работе ядра ОС, для обработки которого на время
вывода диагностических сообщений и формирования crashdump
требуется работоспособная программно-аппаратная среда. В момент
штатной работы ОС все происходит в рамках одного task’а, для
обработки BSOD происходит переключение в другую задачу, выхода
из нее уже нет.
• Собственно многозадачность в терминах ОС Windows реализуется
программно и будет рассмотрена позже.

22.

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

23.


Адресное пространство – диапазон виртуальных адресов, доступный
процессу.
Для некоторой части этих адресов средствами ОС при поддержке аппаратуры
осуществляется трансляция в физические адреса, т.е. обращение к таким
адресам будет либо немедленно перенаправлено в ОЗУ или память устройств,
или приведет к задействованию механизма подкачки страниц с диска в ОЗУ с
последующим доступом к ним в ОЗУ.
Если аппаратные механизмы контроля доступа к памяти не позволяют
осуществить доступ, либо трансляция памяти для адресуемой виртуальной
ячейке отсутствует, процессор генерирует исключение.
Адресное пространство разбито на 2 диапазона: по младшим адресам –
диапазон памяти, доступный и коду пользовательского режима (user mode = 3
кольцо защиты), и коду ядра (kernel mode = 4 к.з.; по старшим адресам –
диапазон памяти ядра (доступен только коду ядре).
Диапазон памяти ядра с точки трансляции адресов – «общий» для всех
адресных пространств
Вся память устройств, отображаемая на адресуемую физическую память, в
виртуальном адресном пространстве отображается в диапазон памяти ядра.
Доступ к портам в/в коду user mode также закрыт, в результате прикладной
уровень не имеет возможности прямой работы с оборудованием.

24.

TSS – Task State Segment
Процессор определяет 5 структур для
организации переключения задач
(multitasking):
• TSS - хранит состояние процессора
• Дескриптор TSS (только в GDT, в LDT
быть не может) – задает расположение
TSS в памяти. Используется для вызова
задачи с помощью специальных
call/jmp
• Task-gate descriptor/шлюз задачи
(может находиться в GDT, LDT или IDT).
Содержит значение селектора TSS.
Используется для вызова задачи с
помощью специальных call/jmp или int.
• Task register TR
• Флаг NT в регистре флагов EFLAGS
В защищенном режиме TSS и Дескриптор
TSS должны быть созданы хотя бы для
одной задачи; селектор, определяющий
Дескриптор TSS , д.б. загружен в регистр
задач TR (инструкцией LTR)

25.

26.

Переключение на другую задачу осуществляется
в одном из 4 случаев:
• выполняется инструкция JMP или CALL на
дескриптор TSS в GDT.
• выполняется инструкция JMP или CALL на
шлюз задачи в GDT или LDT.
• происходит прерывание через шлюз задачи в
IDT.
• выполняется инструкция IRET при
установленном флаге NT (Nested Task).

27.

Обработка прерываний и исключений
Два механизма прерывания работы программы
• interrupt – асинхронное событие, как правило генерируемое устройством в/в.
• exception – синхронное событие, генерируемое при попытке выполнения
инструкции, в случае если состояние процессора соответствует одному или
нескольким предопределенным условиям.
Три класса исключений:
• Faults –сохраняемый cs:eip указывает на адрес вызвавшей исключение инструкции.
Позволяет рестартовать инструкцию, вызвавшую исключение. Пример – деление на 0
• Traps – исключение генерируется после выполнения вызвавшей его инструкции.
сохраняемый cs:eip указывает на адрес следующей инструкции. Пример –
отладочные прерывания.
• Aborts – инструкция, вызвавшая исключение, не всегда определена и не м.б.
рестартована.
Три вида дескрипторов прерываний (хранятся в IDT):
• Task Gate – переключает задачу
• Interrupt Gate
• Trap Gate
Разница между Interrupt Gate и Trap Gate – при использовании Interrupt Gate очищается
флаг IF (запрет маскируемых прерываний). При использовании Trap Gate флаг IF остается
неизменным.

28.

DPL задает ограничение на уровень
привилегий кода, который пытается вызвать
прерывание (код 3-го кольца защиты не
сможет вызвать прерывание с DPL=0, т.е. 0
кольца защиты).
селектор в Interrupt и Trap Gate определяет, на
каком кольце защиты будет работать
обработчик прерывания. В ОС Windows все
обработчики прерываний работают в 0 кольце
защиты.
Если при входе/выходе в обработчик
прерывания меняется кольцо защиты
работающего кода – меняется и стек
(значение вершины стека берется из текущего
TSS из поля, соответствующего новому номеру
кольца защиты).
В противном случае стек не меняется.
Пример: любым системным отладчиком в
пошаговом режиме пройти код,
осуществляющий обнуление ESP, в двух
случаях: в пользовательском режиме и в
режиме ядра.

29.

Рост адресов
Вызов прерывания: без переключения колец защиты (стек не меняется)
Свободная
часть стека
ESP после
вызова
прерывания
Error code
EIP
CS
EFLAGS
Занятая
часть стека
ESP перед
вызовом
прерывания
Вызов прерывания: переключение 3->0 кольцо защиты (смена стека)
Свободная
часть стека 3
Занятая часть
стека 3
SS:ESP перед
вызовом
прерывания
Рост адресов
Рост адресов
Свободная
часть стека 0
Error code
EIP
CS
EFLAGS
ESP
SS
Занятая часть
стека 0
ESP после
вызова
прерывания
SS:ESP в
процессе
вызова
прерывания
после
переключения
кольца защиты
– взяты из TSS

30.


Поток – единица исполнения в ОС Windows. Механизм вытесняющей
многозадачности, реализуемый ОС, применяется именно к потокам.
Программный код исполняется в потоке, изначально для процесса создается один
поток, который затем может создать дополнительные.
Все потоки одного процесса работают в рамках общего адресного пространства, но
различаются контекстом – копией регистров, которая сохраняется перед
приостановкой потока при исчерпании потоком кванта времени и подгружается при
возобновлении потока при получении потоком очередного кванта времени.
Контекст потока (thread context) – термин, часто
используемый в ядерной документации. Состояние работы
потока определяется принадлежащей ему копией
процессорных регистров, в том числе отдельной копией
регистра вершины стека для каждого кольца защиты. Для
ядерных функций различают два вида контекстов:
Arbitrary thread context – контекст случайного потока –
как правило для функций, вызов которых произошел из-за
возникновения асинхронного аппаратного события, прервав
работу произвольного работающего потока (на рис. стрелка
вверх)
Non-arbitrary thread context – контекст определенного
потока – как правило – контекст потока, в котором произошла
передача управления в код ядра (на рис. стрелка вниз)
Модули и
объекты
прикладной
программы
user
kernel
Модули и
объекты ядра
аппаратура

31.

Система приоритетов
• Windows NT имеет двухуровневую модель
приоритетов:
• Приоритеты высшего уровня (уровни запросов
прерываний - Interrupt ReQuest Level - IRQL)
управляют аппаратными и программными
прерываниями
• приоритеты низшего уровня (приоритеты
планирования) управляются планировщиком
и управляют исполнением потоков

32.

31 для 32-разрядных ОС
15 для 64-разрядных ОС
Классы
приоритетов в
Win32 API
0
Ядро возвращает
управление коду
user mode только
при уровне IRQL
PASSIVE_LEVEL
Приоритет из этого
диапазона можно
назначить, только
обладая
полномочиями
администратора

33.

Приоритеты планирования
Схема приоритетов, реализуемая ядром ОС:
• При запуске потоку назначается базовый приоритет (б.п.).
• В процессе работы – у потока текущий приоритет (т.п.)
• Если базовый приоритет в динамическом диапазоне, исходно
т.п. = б.п., затем в процессе работы ОС может менять т.п. по
правилу: б.п. ≤ т.п. ≤ 15
• Если базовый приоритет диапазоне realtime, то всегда т.п. =
б.п.
• Win32 API скрывает эту схему: процессу назначается класс
приоритета, а его потокам – относительный приоритет –
значение ±2 относительно класса приоритета потока.
• Вся эта схема все равно сводится к системе приоритетов,
реализуемой ядром ОС.

34.

• Планировщик Windows реализует «карусельную» (roundrobbin) схему переключения потоков по правилу:
• Если есть потоки из диапазона realtime – переключение по
карусельной схеме будет только для потоков с
максимальным значением realtime-приоритета. Любые
потоки с более низким приоритетом никогда не
исполнятся.
• Если потоков с realtime-приоритетом нет – карусельная
схема для всех потоков с динамическим диапазоном
приоритетов. Для исполнения выбираются потоки с
максимальным т.п., но у потоков с более низким т.п. он
иногда инкрементируется, и в конце концов поток даже с
самым низким базовым приоритетом в конце концов
получит квант времени, после чего его т.п. сбрасывается.

35.

• Планировщик Windows реализует «карусельную» (roundrobbin) схему переключения потоков по правилу:
• Если есть потоки из диапазона realtime – переключение по
карусельной схеме будет только для потоков с
максимальным значением realtime-приоритета.
24
24
24
• Любые потоки с более низким приоритетом никогда не
исполнятся.
24
24
20
13
8
0

36.

• Если потоков с realtime-приоритетом нет – карусельная
схема для всех потоков с динамическим диапазоном
приоритетов.
• Для исполнения выбираются потоки с максимальным т.п.,
но у потоков с более низким т.п. он иногда
инкрементируется, так что поток даже с самым низким
базовым приоритетом в конце концов получит квант
времени, после чего его т.п. сбрасывается.
б.п.
т.п.
13
11
0
13
⬆12
⬆1
13
⬆13
⬆2
13
⬇11
⬆3

13
11
⬆13

37.

Диаграмма состояния потоков

38.


Готов (Ready). Поток готов к выполнению/рестарту после завершения ожидания. При поиске потока
для выполнения диспетчер рассматривает только их.
Готовность с отложенным выполнением (Deferred ready). Используется для потоков, выбранных для
выполнения на конкретном процессоре, но еще не запущенных на нем.
В повышенной готовности (Standby). Поток в состоянии повышенной готовности был выбран для
запуска следующим на конкретном процессоре. Как только сложатся соответствующие условия,
диспетчер выполнит контекстное переключение на этот поток. Для каждого процессора в системе в
состоянии повышенной готовности может быть только один поток.
Выполнение (Running). Как только диспетчер выполняет переключение контекста на поток, тот входит
в состояние выполнения. Выполнение прекращается когда:
- истек квант времени (и готов другой поток),
- вытеснение потоком с более высоким приоритетом,
- завершение работы потока,
- поток уступил выполнение
- поток самостоятельно перешел в состояние ожидания.
Ожидание (Waiting). Поток может войти в состояние ожидания в нескольких случаях:
- поток может самостоятельно ожидать объекта для синхронизации своего выполнения;
- операционная система может ждать от имени потока (например, для разрешения страничного в/в) ;
- подсистема среды может приказать потоку приостановиться.
Когда ожидание потока заканчивается, то в зависимости от приоритета поток либо немедленно
начинает выполняться, либо возвращается в состояние готовности.
Переходное состояние (Transition). Поток входит в переходное состояние, если он готов к
выполнению, но его стек ядра выгружен из памяти. Как только его стек ядра вернется в память, поток
войдет в состояние готовности.
Завершение (Terminated). Когда поток заканчивает выполнение, он входит в состояние завершения.
Как только поток будет завершен, объект потока исполняющей системы (структура данных с
описанием потока в невыгружаемом пуле) может быть освобожден или не освобожден (политику,
определяющую время удаления объекта, устанавливает диспетчер объектов). Например, объект
остается, если у потока остались открытые дескрипторы. Поток также может перейти в состояние
завершения из других состояний, если он будет явно уничтожен другим потоком — например
вызовом TerminateThread.
Инициализация (Initialized). Это состояние используется внутри системы при создании потока

39.

Уровни запросов прерываний (IRQL)
IRQL – способ управления маскированием прерываний на конкретном
процессоре. По сути – это некоторое число, назначаемое конкретному процессору
и обозначающее факт маскирования на нем некоторых прерываний. Конкретное
значение IRQL из диапазона DIRQL (Device IRQL) также связывается с каждым
обработчиком аппаратных прерываний. Если такое прерывание в данный момент
не маскировано и оно доставлено процессору, ОС производит смену текущего
IRQL процессора на IRQL прерывания, маскируя при этом все прерывания с
IRQL ≤ нового IRQL процессора. При завершении обработчика прерывания
значение IRQL процессора восстанавливается на старое.
На уровне IRQL_PASSIVE_LEVEL разрешены все прерывания и работает
механизм многозадачности. Код пользовательского режима работает только на
PASSIVE_LEVEL (возможно за исключением функций, вызываемых механизмом
APC на уровне APC_LEVEL – надо проверить)
На уровне APC_LEVEL реализуется механизм асинхронного вызова процедур
Asynchronous Procedure Call (APC), через который в частности ядро ОС реализует
механизм обратного вызова функций (callback functions) прикладного уровня для
уведомления прикладной программы ядром о возникновении некоторой
ожидаемой ситуации. На этом уровне маскированы прерывания с IRQL=
APC_LEVEL. Доступ к выгружаемой (paged) памяти разрешен.

40.

• На уровне IRQL DISPATCH_LEVEL работают так называемые
«отложенные процедуры» (Deferred Procedure Calls - DPC), поэтому этот
уровень иногда называют DPC_LEVEL. DPC создаются обработчиками
прерываний, и туда выносится весь «долгоиграющий» код обработки
прерывания. Задачей обработчика прерывания является как можно
быстрее зафиксировать факт возникновения прерывания и данные,
необходимые для дальнейшей обработки в DPC. Вызовы DPC
помещаются в очередь (своя для каждого процессора), обработка которых
ведется в соответствии с собственной системой приоритетов (lowmedium-high). Возврата к IRQL<DISPATCH_LEVEL не будет до тех пор,
пока в очереди DPC содержится хоть один запрос.
• На этом же уровне работают прерывания, отвечающие за подгрузку
страниц в ОЗУ из файлов подкачки, а также за диспетчеризацию потоков
(проверить). Отсюда самое главное ограничение на код ядра:

41.

• при работе процессора на уровне IRQL≥DISPATCH_LEVEL
запрещено обращение к выгружаемой памяти – оно немедленно
приведет к BSOD с ошибкой IRQL_NOT_LESS_OR_EQUAL.
• На уровне IRQL>PASSIVE_LEVEL нельзя предпринимать никакие
действия, которые могут привести к необходимости переключения
потока (например использовать механизмы синхронизации на основе
диспетчерских объектов (Dispatcher Objects) с помощью функции
WaitFor(Single/Multiple)Objects с ненулевым временем ожидания) –
замаскированы обслуживающие такое переключение прерывания
уровня APC и DISPATCH_LEVEL
• Уровни выше DISPATCH_LEVEL в основном соответствуют
обработчикам аппаратных прерываний (DIRQLs). Маскирована часть
прерываний, к возникновению которых могут приводить в том числе
вызовы многих служебных ядерных функций, поэтому множество
разрешенных к вызову служебных функций сильно ограничено.
Серьезная обработка должна быть вынесена в DPC, а иногда – и в
рабочие потоки с IRQL PASSIVE_LEVEL.

42.

IRQL
DIRQL 2
ISR 2
DIRQL 1
ISR 1 начало
ISR 1 продолжение
DISPATCH_LEVEL
Обработка очереди DPC
APC_LEVEL
PASSIVE_LEVEL
Доставка APC
Код потока
t1
Код потока
t2
t3
t4
t5
t6
t1: Получено прерывание. Задача обработчика – быстро поставить задание в очередь DPC и завершиться
t2: Получено более приоритетное прерывание
t3: Выход из обработчика более приоритетного прерывания в ранее прерванный обработчик
t4: Выход из обработчика прерывания, начатого в момент времени t1 , запуск обработки очереди DPC
t5: Завершение всех заданий очереди DPC, запуск доставки APC
t6: Завершение доставки всех APC, возврат к исполнению ранее прерванного кода потока

43.

!!!В Win10 – появился Rank

44.

Основные структуры управления памятью ОС Windows
• Cr3 -> Page Directory, Page Tables (Каталог и таблицы страниц)
Задает отображение страниц виртуальной памяти в физическую
(для каждого адресного пространства описывает преобразование
виртуального адреса в физический)
Используется аппаратно каждый раз при доступе к виртуальной ячейке памяти
• PFN Database (база данных PFN)
Описывает состояние страниц физической памяти
Используется ОС когда нужно заменить одну страницу памяти другой
Доступна через экспортируемую ядерную переменную
• VAD Tree (дерево дескрипторов виртуальных адресов)
Сбалансированное дерево.
Для каждого адресного пространства в диапазоне виртуальных
адресов пользовательского режима описывает зарезервированные
блоки виртуальной памяти
Используется ОС когда нужно выделить новый блок памяти
Корень дерева доступен через структуру EPROCESS

45.

Основные структуры управления
процессами и потоками
CPU
FS:[0]
FS:[0]
FS:[0]
TEB
(Thread Environment Block)
TIB
[7ffde000]
PEB
(ProcessEnvironment Block)
PEB *Peb
user
kernel
EPROCESS
CPU
FS:[0]
FS:[0]
FS:[0]
KPCR
(Processor Control Region)
(Processor Control Block)
KPRCB *Prcb
[ffdff000]
KPRCB
KTHREAD *CurrentThread
KTHREAD *NextThread
KPROCESS Pcb
PEB *Peb
Таблица_описателей
Корень_дерева_VAD
ETHREAD
KTHREAD Tcb
TEB*Teb
EPROCESS *ThreadsProcess

46.

Пространство имен диспетчера объектов

47.

• Имена ядерных объектов размещаются в едином пространстве
имен. Подсистема Win32 скрывает его наличие, транслируя
обращения к именам объектов различных типов в обращения к
конкретным директориям единого пространства имен, иногда
полностью видоизменяя имя объекта. Например, обращение к
файлу с именем c:\foo.txt будет последовательно
трансформировано следующим образом:
• \??\c:\foo.txt
• В директории \?? будет найден элемент с именем С:, имеющий
тип SymbolicLink и значение \Device\HarddiskVolume1, в
результате подстановки имя примет вид
• \Device\HarddiskVolume1\foo.txt
• В директории \Device будет найден элемент с типом Device, на
этом разбор имени будет закончен, а соответствующему
устройству будет отправлен запрос, частью которого будет
оставшаяся неразобранной часть имени \foo.txt – дальше с ней
будет разбираться драйвер устройства.

48.

«Забегая вперед»
• Неразобранная часть имени, если она есть, хранится в стеке размещения в/в
в поле FileName файлового объекта
PIO_STACK_LOCATION pStack;
pStack = IoGetCurrentIrpStackLocation(pIrp);
if( pStack->FileObject && pStack->FileObject->FileName.Buffer)
DbgPrint(“Name is <%ws>\n”, pStack->FileObject->FileName.Buffer);
• Например для имени \??\c:\foo.txt будет передано \foo.txt
• Если имя разобрано полностью, неразобранной части нет, и в поле
pStack->FileObject->FileName.Buffer будет помещен нулевой указатель
• Файловый объект описывает установленный канал взаимодействия с
драйвером (запрос IRP_MJ_CREATE) и хранящееся в нем имя можно
использовать в любом запросе в/в до уничтожения файлового объекта
(запрос IRP_MJ_CLEANUP)

49.

Структура драйвера
• Работа драйвера начинается с вызова функции DriverEntry,
соответствующей начальной точке входа в исполняемый модуль драйвера,
которая прописана в его PE-заголовке.
• Задача DriverEntry – инициализация работы драйвера. Если она завершается
успешно – функция завершается с кодом STATUS_SUCCESS (=0), после
чего драйвер остается в памяти ядра. При любом другом коде завершения
драйвер выгружается из памяти.
• Работа драйвера после загрузки по сути определяется тем, какие callbackфункции зарегистрированы драйвером при инициализации:
• Диспетчерские функции: массив точек входа DriverObject->MajorFunction[],
вызываемых для обработки запросов, направляемых устройствам драйвера.
• Обработчики прерываний (ISR), отложенных вызовов (DPC).
• Подмена адресов функций обработки вызовов системных сервисов.
• Функции, определяемые типом программного интерфейса задействованного
при инициализации (например NDIS).
• В ОС начиная с Vista – драйверы должны быть подписаны. Механизм
проверки подписи может быть отключен при запуске ОС.

50.

ограничения
• Проблема при попытке написания кода драйвера на C++: функция main()
– не первая функция с которой начинает работать код (инициализация
памяти, вызов конструкторов глобальных экземпляров классов).
• В коде драйвера – жесткие ограничения на библиотечные функции.
Разрешенные к вызову – можно вызывать только функции ядра,
некоторые типы драйверов имеют ограниченный набор функций,
разрешенных к вызову, чтобы соответствовать некоторой спецификации –
например NDIS.
• Для всех доступных для вызова ядерных функций документация
фиксирует жесткие условия, в которых они могут быть вызваны (контекст
потока – случайный или нет, уровень IRQL, с которого может быть
осуществлен вызов). Кроме того – ограничения на доступ к памяти (не
всегда можно работать с paged-памятью) в зависимости от условий
работы.
• В ранних версиях ОС было ограничение (запрет) на работу с
инструкциями FPU, возникающее вследствие несохранения контекста
FPU при переключении user->kernel mode

51.

Многоуровневая модель драйверов
• Legacy Drivers – WDM – KMDF – WDF
• Legacy Drivers – по сути способ написания ядерного драйвера «с нуля»,
опираясь только на знания архитектуры ОС. Архитектура таких
драйверов не претерпела изменений с момента появления ОС Windows
NT 3.51. Такой драйвер будет работать на всех современных Windows
• WDM – Windows Driver Model – модель драйверов, изначально
разработанная как способ реализации универсальных драйверов для
кардинально различающихся линеек ОС: Win9x и WinNT, путем
предоставления драйверу для работы некоторого программного
окружения, соответствующего поведению ядра ОС линейки Windows
NT . Данная архитектура стала стандартом для драйверов всех
последующих ОС данной линейки
• WDF/KMDF – фреймворк, программная оболочка, скрывающая
реальное устройство ядра ОС для облегчения разработки драйверов, но
может стать «тормозом» при разработке «продвинутых» драйверов

52.

VisualStudio2019 + WDK for Windows
10 version 2004 (по состоянию на 01.09.2020)
https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk
• Выбрать Desktop development with C++
• Отметить для установки
– Windows 10 SDK 10.0.19041.0
– MSVC v142 – VS 2019 C++ x64/x86 build tools

53.

54.

55.

Выбор целевой ОС
(сборка для Win7 будет работать и на WinXP)

56.

Изменить свойства проекта

57.

Интеграции WDK с VisualStudio –VisualDDK

58.

59.

Код простейшего драйвера: ничего делать не умеет, после загрузки остается в памяти ядра и не
может быть оттуда выгружен никаким другим способом кроме перезагрузки компьютера.
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0501
#endif
#include <ntddk.h>
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
DbgPrint("simple: DriverEntry, regpath=%ws\n", RegistryPath->Buffer);
return STATUS_SUCCESS;
}
Регистрация драйвера в реестре. Обратить внимание на формат пути в ImagePath
Зарегистрированный драйвер может быть запущен и остановлен с помощью консольных команд
net start имя_сервиса / net stop имя_сервиса
Программно драйвер управляется с помощью WinAPI-функций диспетчера сервисов (SCM ServiceControlManager): CreateService/DeleteService/StartService/ControlService

60.


Для обеспечения возможности выгрузки драйвера он должен реализовывать
обработчик функции выгрузки DriverObject->DriverUnload
Задача этой функции – очистка всех ресурсов, выделенных при работе
драйвера
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0501
#endif
#include <ntddk.h>
void OnUnload(IN PDRIVER_OBJECT DriverObject)
{
DbgPrint("simple: OnUnload\n");
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
DbgPrint("simple: DriverEntry, regpath=%ws\n", RegistryPath->Buffer);
DriverObject->DriverUnload = OnUnload;
return STATUS_SUCCESS;
}
Как DriverEntry, так и DriverObject->DriverUnload всегда
вызываются в контексте одного из рабочих потоков процесса System.
Поскольку адресное пространство одно и то же, используется общая таблица
описателей, т.е. описатель, полученный в DriverEntry, можно закрывать в
DriverUnload

61.

Регистрация и запуск/остановка драйвера
• Внести изменения в reg-файл (в директории с проектом драйвера):
"Start"=dword:00000003
"ImagePath"="\\??\\c:\\work\\drv\\l1\\l1\\x64\\Debug\\l1.sys“
(путь и имя файла должны быть ваши)
• Выполнить reg-файл
• Проконтролировать изменения реестра через редактор рестра regedit
– ищем имя_сервиса и значения ключей в
HKLM\System\CurrentControlSet\Services\
• После первого изменения реестра перезагрузить виртуальную
машину для внесения изменений из реестра в память
• Запуск и остановка сервиса:
net start имя_сервиса
net stop имя_сервиса
• Использовать DbgView для контроля отладочных сообщений

62.

Взаимодействие с драйвером обычно
осуществляется через создаваемые им
устройства, которым направляются запросы в/в
в формате IRP-пакетов
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0501
#endif
#include <ntddk.h>
void OnUnload(IN PDRIVER_OBJECT DriverObject)
{
DbgPrint("simple: OnUnload\n");
IoDeleteDevice(DriverObject->DeviceObject);
}
NTSTATUS OnCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("simple: OnCreate\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
NTSTATUS OnClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("simple: OnClose\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNICODE_STRING DeviceName;
PDEVICE_OBJECT DeviceObject = NULL;
NTSTATUS status;
DbgPrint("simple: DriverEntry, regpath=%ws\n", RegistryPath->Buffer);
RtlInitUnicodeString(&DeviceName,L"\\Device\\simple0");
DriverObject->MajorFunction[IRP_MJ_CREATE] = OnCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = OnClose;
DriverObject->DriverUnload = OnUnload;
status = IoCreateDevice(DriverObject,
0,
&DeviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&DeviceObject);
if (!NT_SUCCESS(status))
return status;
if (!DeviceObject)
return STATUS_UNEXPECTED_IO_ERROR;
DeviceObject->Flags |= DO_DIRECT_IO;
DeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
return STATUS_SUCCESS;
}

63.

#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0501
#endif
#include <ntddk.h>
void OnUnload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING Win32Device;
DbgPrint("simple: OnUnload\n");
RtlInitUnicodeString(&Win32Device,L"\\DosDevices\\simple0");
IoDeleteSymbolicLink(&Win32Device);
IoDeleteDevice(DriverObject->DeviceObject);
}
NTSTATUS OnCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("simple: OnCreate\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
NTSTATUS OnClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
DbgPrint("simple: OnClose\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNICODE_STRING DeviceName,Win32Device;
PDEVICE_OBJECT DeviceObject = NULL;
NTSTATUS status;
DbgPrint("simple: DriverEntry, regpath=%ws\n", RegistryPath->Buffer);
RtlInitUnicodeString(&DeviceName,L"\\Device\\simple0");
RtlInitUnicodeString(&Win32Device,L"\\DosDevices\\simple0");
DriverObject->MajorFunction[IRP_MJ_CREATE] = OnCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = OnClose;
DriverObject->DriverUnload = OnUnload;
status = IoCreateDevice(DriverObject,
0,
&DeviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&DeviceObject);
if (!NT_SUCCESS(status))
return status;
if (!DeviceObject)
return STATUS_UNEXPECTED_IO_ERROR;
DeviceObject->Flags |= DO_DIRECT_IO;
DeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
IoCreateSymbolicLink(&Win32Device, &DeviceName);
return STATUS_SUCCESS;
}
Чтобы к устройству мог обратиться код
пользовательского режима, имя этого
устройства должно стать видимым в
специальной части пространства имен
диспетчера объектов. Это достигается путем
создания символической ссылки.

64.

Взаимосвязь основных ядерных объектов при
прохождении запроса в/в

65.

CreateFile(filename, …)
Вернуть управление в
CreateFile
Native API: ntdll.dll
NtCreateFile(filename, …)
user
kernel
HANDLE – индекс в
таблице описателей
int 2E
SYSCALL
SYSENTER
Диспетчер в/в
Найти адрес функции в
таблице системных сервисов
NtCreateFile(filename, …)
Таблица описателей
DRIVER_OBJECT
FileObject
DeviceObject
MajorFunction[]
DriverUnload()
FILE_OBJECT
DeviceObject
DEVICE_OBJECT
Найти устройство по имени
DriverObject
NextDevice
AttachedDevice
DeviceExtension
Найти драйвер этого устройства
Создать файловый объект, описывающий
сеанс связи с устройством
Сформировать IRP-запрос на
основе параметров CreateFile()
IRP
FileObject
DeviceObject
MajorFunction
Если завершение с ошибкой –
уничтожить файловый объект
= IRP_MJ_CREATE
Передать IRP-запрос для
устройства через IoCallDriver
DrvObj->MajorFunction[IRP_MJ_CREATE](pDevObj, pIrp )
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, 0);
return STATUS_SUCCESS;
Если успешно – поместить его
адрес в таблицу описателей и
вернуть индекс как HANDLE

66.

ReadFile(hfile, …)
Если завершено или вызов
асинхронный – вернуть
управление в ReadFile
Native API: ntdll.dll
Используется APC для
доставки уведомления
о завершении запроса
в/в
NtReadFile(hfile, …)
user
kernel
int 2E
SYSCALL
SYSENTER
HANDLE – индекс в
таблице описателей
Диспетчер в/в
Найти адрес функции в
таблице системных сервисов
NtReadFile(hfile, …)
Таблица описателей
DRIVER_OBJECT
FileObject
Найти драйвер этого устройства
Сформировать IRP-запрос на
основе параметров ReadFile()
DeviceObject
MajorFunction[]
DriverUnload()
FILE_OBJECT
DeviceObject
Найти файловый объект
по описателю
Найти устройство
Если не завершено и вызов
синхронный – не возвращать
управление до завершения запроса
DEVICE_OBJECT
DriverObject
NextDevice
AttachedDevice
DeviceExtension
IRP
FileObject
DeviceObject
MajorFunction
= IRP_MJ_READ
Передать IRP-запрос для
устройства через IoCallDriver
DrvObj->MajorFunction[IRP_MJ_READ](pDevObj, pIrp )
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, 0);
return STATUS_SUCCESS;
//Отложить обработку: сохранить pIrp
IoMarkIrpPending(pIrp);
return STATUS_PENDING;
//где-то в случайном контексте
IoCompleteRequest(pIrp, 0);

67.

Структура управляющей программы
Класс управления
сервисами
Конструктор()
Add()
Del ()
Start ()
Stop()
Open()
Close ()
Деструктор()
Функции WinAPI
Решаемая задача
Функции диспетчера сервисов
OpenSCManager()
CreateService()
DeleteService()
StartService()
ControlService()
CloseServiceHandle()
CreateFile()
CloseHandle()
1. Добавление сервиса
2. Удаление сервиса
3. Запуск сервиса
4. Остановка сервиса
5. Открытие устройства
6. Закрытие устройства

68.

Как выяснилось большинство из вас нифига не помнят/не знают базовые вещи по языку С/С++, а потому
Вспоминаем C/C++:
Объявления и определения
подробнее: https://learnc.info/c/beginners_guide_to_linkers.html
Иногда в русскоязычных статьях по языку c/cpp, например здесь:
https://studfile.net/preview/7165254/page:7/ , вводится ТРИ термина: объявление,
определение и описание. Упрощенно:
Объявление: вводит что-то (имя переменной) определенного типа (имя типа).
Описание – это объявление, которое :
память для переменной не резервирует
для одной переменной может быть >=0 описаний.
Определение – это объявление, которое:
резервирует память для переменной
для одной переменной может быть ТОЛЬКО ОДНО определение
Забудьте эту гадость если где увидите/услышите. Читайте первоисточник на английском,
ссылку в заголовке или базовые статьи в MSDN, например https://docs.microsoft.com/ruru/cpp/cpp/declarations-and-definitions-cpp?view=msvc-160&viewFallbackFrom=vs-2019 .
Или следующие слайды.

69.

В английском языке для c/cpp используется ДВА термина:
• declaration (объявление), описывает переменную некоторого типа НО
НЕ ВЫДЕЛЯЕТ для нее память. Или задает прототип функции без ее
реализации: void f1(int, void*); . Или задает новый тип, например
структуру или класс, без создания переменной – экземпляра типа.
• definition (определение), описывает переменную некоторого типа И
ВЫДЕЛЯЕТ для нее память. Или задает прототип и реализацию
функции: void f1(int a, void *b){ }.
Для одной переменной всегда должно быть единственное
определение и >=0 объявлений. При этом определение всегда
выступает в роли объявления.
Переменная/функция в конкретном c/cpp файле должна быть объявлена
до использования, и должна быть единожды определена неважно где,
возможно даже в другом c/cpp-файле. По стандарту CPP определение –
единственное, иначе – ошибка. По стандарту С определений может быть
много – например по одному в каждом c-файле, и тогда сколько
определений – столько и переменных.

70.

Сборка проекта (build):
препроцессинг, компиляция, компоновка(линковка)
Проект программы (project в терминологии visualstudio) состоит из множества c/cpp
файлов, статических библиотек (.lib-файл, состоящий из множества .obj-файлов) и
библиотек импорта (бинарный .lib или текстовый def-файл, описывающие имена
динамических библиотек .dll и реализованных в них функций и переменных, которые
мы хотим использовать из нашего кода). Все библиотечные переменные и функции
должны быть объявлены – для этого мы включаем в c/cpp через #include
соответствующий стандартный заголовочный h-файл .
При желании в проект можно включать файлы на других языках программирования .asm, смешивать .cpp и .c, паскаль, фортран и т.п., с указанием компилятора и его
параметров для преобразования (компиляции/трансляции) файла с исходным
текстом в объектный файл (.obj)
Заголовочные файлы, которые вы видите в составе проекта, на процесс сборки
проекта не влияют в том смысле что в состав проекта их включать не обязательно.
Например, если в ваш проект включен некоторый файл myfile.cpp, в котором есть
директива #include “header.h”, файл header.h будет обработан на этапе
препроцессинга файла myfile.cpp вне зависимости от того, включили вы header.h в
проект или нет. Заголовочные файлы включаются в состав проекта для вашего
удобства контроля проекта.

71.

Общее правило для проекта программы, состоящего из
нескольких c/cpp – файлов:
• В заголовочных файлах – только объявления. Например
extern int a;
Если напишете в заголовке определение, например int a;
а затем включите через #include такой заголовок в разные
c/cpp-файлы – получите РАЗНЫЕ переменные а в разных
c/cpp-файлах и потенциальную ошибку – притом что на С
проект успешно соберется а на CPP выдаст ошибку.
• В c/cpp-файлах – определения. Здесь можно а иногда
нужно делать объявления, но желательно вынести их в
заголовочный файл, подключенный через #include

72.


Каждый c/cpp-файл является единицей компиляции и НЕЗАВИСИМО от остальных
файлов проекта проходит через этапы:
1.
Препроцессинг: все директивы препроцессора в исходном тексте (начинаются на
#, например #include, #define заменяются на сишный код. В частности в точке
#include подставляется целиком содержимое заголовочного файла, для которого
также выполняется препроцессинг). Результат препроцессинга - c/cpp файл без
директив препроцессора
Настройки препроцессора в VisualStudio – в свойствах проекта пункт C/C++ в
основном в подпункте Preprocessor

73.

2.
Компиляция: генерация псевдокода в терминах некоторого
промежуточного языка (трехадресный код, llvm-представление) или сразу в
виде команд процессора целевой процессорной архитектуры. Результат
компиляции – объектный файл (.obj). Отличие от обычного исполняемого
кода – в нем не определены внешние связи с функциями/переменными
других объектных файлов. Т.е. если в текущем компилируемом сишнике
были описания, но не определения используемых в коде переменных и
функций – для таких участков си-кода работающий фрагмент машинного
кода сгенерировать невозможно без учета других c/cpp файлов. Генерация
таких фрагментов кода откладывается до этапа линковки.
Не все программные ошибки могут быть выявлены на этапе компиляции.
Например – если ни в одном c/cpp файле не было определения
используемой переменной, но были ее описания – не будет ошибки этапа
компиляции но будет ошибка этапа линковки.
Настройки компилятора в VisualStudio – в свойствах проекта пункт C/C++

74.

3. После того как все файлы проекта без ошибок
прошли препроцессинг и компиляцию,
происходит этап компоновки он же линковка
(потому что в VS компоновку делает утилита link).
Компоновке подвергается одновременно все
множество объектных файлов, полученных на
предыдущих этапах. Результат – исполняемый
файл (.exe, .dll, .sys и т.п.) или статическая
библиотека (.lib) как объединение множества
объектных файлов.
Настройки компоновщика в VisualStudio – в
свойствах проекта пункт Linker

75.

Пример: проект состоит из четырех файлов – file1.cpp, file2.cpp, file3.c, file4.asm. Количество
заголовочных файлов в проекте роли не играет. Как происходит сборка (build) проекта?
Вообще говоря
любые файлы,
указанные в
директиве
препроцессора
#include
.h
.h.h
file2.cpp
file1.cpp
Препроцессор
языка c++
.h
.h.h
Препроцессор
языка c++
file3.c
.h
.h.h
file4.asm
Препроцессор
языка c
file1.cpp
file2.cpp
file3.c
Компилятор
языка c++
Компилятор
языка c++
Компилятор
языка c
Транслятор
языка
ассемблер
file1.obj
file2.obj
file3.obj
file4.obj
linker
Исполняемый файл в формате PE, elf или любом
другом, поддерживаемым линковщиком.
Расширение может быть любым - .exe, .dll, .sys –
оно настраивается в свойствах компоновщика

76.

Как запустить сборку (build) проекта
1. Правый клик мышкой на имени проекта – всплывающее меню – Build
2. Меню Build – Build имя_проекта (если проектов в солюшене несколько)
3. Кнопка «Start debugging» или соответствующий пункт меню «Debug». При
нескольких проектах запускается активный (помечен жирным шрифтом). Перед
запуском - если были изменения в исходном коде, VS вначале «спросит» вас –
хотите ли «пересобрать» проект. Лучше не надо так «пересобирать»

77.

Как запустить компиляцию конкретного c/cppфайла или единицы компиляции
1. Правый клик мышкой на имени файла – всплывающее меню – Compile
Напоминаю, что при этом сгенерируется .obj-файл для этого конкретного сишника.
Исполняемый файл сгенерирован не будет – это уже линковка.
Любители gcc всё то же самое
проделывают прямо из
командной строки со всеми
параметрами препроцессора
– компилятора –
компоновщика и им ничего
этого разжёвывать не надо.
Святые люди :)

78.

79.

80.

81.

82.

83.

84.

85.

Оптимизация как часть процесса компиляции
Сам процесс компиляции довольно сложен и является объектом
научных исследований в области компиляторных технологий
IR – Intermediate Representation – промежуточное
представление кода
HIR – High-level IR
LIR - Low-level IR
В следующем семестре:
Control Flow Analysis – Анализ потока управления
Data Flow Analysis – Анализ потока данных

86.

87.

88.

89.

90.

91.

92.

93.

94.

95.

96.

97.

98.

99.

100.

Дополнительные ссылки
• «Для чайников» про то же, но в gcc: https://habr.com/ru/post/478124/
• А вот «продвинутая» статья по структуре проекта, состоящего из
нескольких единиц компиляции (c/cpp-файлов): https://codelive.ru/post/separate-compilation-cpp/
• «Для чайников» про оптимизацию кода
https://habr.com/ru/post/124131/
• А зачем это Вам? «В уме» держим что «обфускация» – запутывание
кода в целях защиты от анализа – строится на тех же принципах
теории оптимизации и по сути может рассматриваться как одно из
«оптимизирующих» преобразований – только цель у него другая ))
• А некоторые из вас, как будущие специалисты в области ИБ, должны
уметь как защищать код от анализа (обфускация), так и исследовать
защищенный код (деобфускация)
• На будущее - «Для совсем (про)двинутых»: серия статей того же
автора https://habr.com/ru/post/119850/ Зачем? Вот на уровне llvm и
делают обфускацию.

101.

Precompiled headers - предварительно
откомпилированные заголовки
Количество строк кода в стандартных заголовочных файлах операционной системы как
правило на 2-3 порядка превосходит объем написанного вами кода.
Вы реально пишете 200-300 строк на с/с++ и считаете это подвигом. На момент выполнения
вами дистанционной работы в объеме первого видеозанятия, размер файла scmgr.cpp – 89
строк, размер файла guiappDlg.cpp – 189 строк.
В scmgr.cpp вы используете некоторые WINAPI-функции и типы данных, другие WINAPIфункции и типы вы используете в guiappDlg.cpp.
Перед использованием WINAPI-функции и типы данных должны были быть объявлены в
КАЖДОМ из этих cpp-файлов, для этого как правило через директиву препроцессора #include в
cpp-файл подключается стандартный заголовочный файл, такой как windows.h
Размер стандартных заголовочных файлов - таких как windows.h, ntddk.h или wdm.h - сотни
тысяч строк кода
К чему это приведет – мы видели на предыдущем слайде: препроцессор к вашим 89 строкам
файла scmgr.cpp добавляет сотни тысяч строк стандартных заголовков и результат отдает
компилятору, затем к 189 строкам файла guiappDlg.cpp добавляет ТЕ ЖЕ САМЫЕ сотни тысяч
строк стандартных заголовков и результат опять отдает компилятору и т.д. для всех cpp-файлов.
Т.е. суммарное время компиляции = O(размер_стандартных_заголовков*число_cpp_файлов),
объем написанного вами кода пренебрежимо мал и не учитывается.
Чтобы время компиляции зависело только от объема написанного вами кода, придумали
Precompiled headers. Общая идея: неизменяемые вами стандартные заголовочные файлы,
многократно включаемые в разные модифицируемые вами c/cpp-файлы, должны быть
вынесены в отдельный проход компиляции, выполняемый один раз на все сборки проекта.

102.

• Для этого надо сделать три вещи:
1. в свойствах проекта в настройках компилятора С/С++ в
подпункте Precompiled Headers включаем использование
Precompiled headers и указываем имя предварительно
компилируемого заголовочного h-файла

103.

2. В свойствах каждого c/cpp-файла, для которого хотим
задействовать процесс предварительной компиляции
заголовков, делаем то же самое, по умолчанию
настройки проекта будут применяться ко всем
создаваемым c/cpp файлам

104.

3. В текст c/cpp-файла, для которого хотим задействовать
процесс предварительной компиляции заголовков, через
#include включить заголовочный файл, указанный в
настройках предварительной компиляции

105.

3.
В текст предварительно компилируемого h-файла, указанного
в настройках предварительной компиляции (у нас это
stdafx.h), через #include включаем набор стандартных
заголовочных файлов, общий для всех c/cpp файлов проекта.
Как правило это
#include <windows.h>
но в принципе это могут быть и ваши заголовки, в которых вы
уверены и знаете что редактировать их уже не будете.
Все остальные заголовочные файлы, которые вы редактируете
в процессе написания кода и потому не должны включать в
состав предкомпилируемых, включаете через #include как
обычно в c/cpp-файлы проекта
В примере на предыдущем слайде: первая #include включает
огромный предкомпилируемый один раз заголовок, две
следующих директивы #include включают маленькие но
постоянно редактируемые вами заголовочные файлы.

106.

Теперь возвращаемся к структуре
текущего проекта
• на следующем слайде - упрощенная схема проекта с
пояснениями
• попытайтесь ее осознать с использованием ранее изложенных
терминов и учетом последовательности сборки проекта
• для понимания крайне рекомендуется полазить по файлам
проекта, сравнить их исходный код со схемой и найти все
указанные на слайде части
• Сверьтесь со своими знаниями С/С++ в части терминов
объявление/определение, кто все понял – молодец. Я у вас на
две группы пока только одного такого студента видел )

107.

Программа с графическим интерфейсом (GUIApp)
guiapp.h
Определение класса
приложения CguiappApp
class CguiappApp : public CWinAppEx
{
virtual BOOL InitInstance();
CWnd *m_pMainWnd;

};
extern CguiappApp theApp;
Объявление переменной
theApp – экземпляра класса
приложения
guiappDlg.h
Определение класса
диалогового окна CguiappDlg
class CguiappDlg : public Cdialog
{
CEdit m_edt_fname;
CEdit m_edt_sname;
CEdit m_edt_lname;
void OnBnClickedBtnFname();
void OnBnClickedBtnAdd();
void OnBnClickedBtnDel();
void OnBnClickedBtnStart();
void OnBnClickedBtnStop();
void OnBnClickedBtnOpen();
void OnBnClickedBtnClose();
…Объявления переменных и функций
};– членов класса диалогового окна. В
частности – объявление функции
OnBnClickedBtnDel
guiapp.cpp
#include "guiapp.h"
#include "guiappDlg.h"
scm.h
scm.cpp
CguiappApp theApp; Определение theApp
Определение функции InitInstance
– члена класса приложения
BOOL CguiappApp::InitInstance()
{

CguiappDlg dlg;
Создание экземпляра класса диалогового окна
m_pMainWnd = &dlg; сохранение его адреса в m_pMainWnd
dlg.DoModal();
и отрисовка окна через DoModal()

}
guiappDlg.cpp
#include "guiapp.h"
#include "guiappDlg.h"
Определение функции DoDataExchange –
#include "scm.h"
члена класса диалогового окна
void CguiappDlg::DoDataExchange(CDataExchange* pDX)
Связывание переменной m_edt_sname с
{
DDX_Control(pDX, IDC_EDT_SNAME, m_edt_sname); ресурсом с идентификатором
IDC_EDT_SNAME
}
BEGIN_MESSAGE_MAP(CguiappDlg, CDialog)
ON_BN_CLICKED(IDC_BTN_DEL, &CguiappDlg::OnBnClickedBtnDel)
Связывание функции OnBnClickedBtnDel с событием
END_MESSAGE_MAP()
ON_BN_CLICKED – нажатие на элемент управления с
void CguiappDlg::OnBnClickedBtnDel()
идентификатором ресурса IDC_BTN_DEL
Определение функции
{
OnBnClickedBtnDel – члена
CSCM scm;
CString sname; класса диалогового окна
m_edt_sname.GetWindowText(sname);
scm.Del(sname);
}

108.

Программа с графическим интерфейсом (GUIApp)
guiapp.h
class CguiappApp :
public CWinAppEx
{
virtual BOOL InitInstance();
CWnd *m_pMainWnd;

};
extern CguiappApp theApp;
guiappDlg.h
class CguiappDlg :
public Cdialog
{
CEdit m_edt_fname;
CEdit m_edt_sname;
CEdit m_edt_lname;
void OnBnClickedBtnFname();
void OnBnClickedBtnAdd();
void OnBnClickedBtnDel();
void OnBnClickedBtnStart();
void OnBnClickedBtnStop();
void OnBnClickedBtnOpen();
void OnBnClickedBtnClose();

};
guiapp.cpp
#include "guiapp.h"
#include "guiappDlg.h"
CguiappApp theApp;
BOOL CguiappApp::InitInstance()
{

CguiappDlg dlg;
m_pMainWnd = &dlg;
dlg.DoModal();

}
scm.h
class CSCM
{
SC_HANDLE hscm;
CSCM(void);
~CSCM(void);
void Del(LPCTSTR sname);

};
guiappDlg.cpp
#include "guiapp.h"
#include "guiappDlg.h"
#include "scm.h"
void CguiappDlg::DoDataExchange(CDataExchange* pDX)
{
DDX_Control(pDX, IDC_EDT_SNAME, m_edt_sname);
}
BEGIN_MESSAGE_MAP(CguiappDlg, CDialog)
ON_BN_CLICKED(IDC_BTN_DEL,
&CguiappDlg::OnBnClickedBtnDel)
END_MESSAGE_MAP()
void CguiappDlg::OnBnClickedBtnDel()
{
CSCM scm;
CString sname;
m_edt_sname.GetWindowText(sname);
scm.Del(sname);
}
scm.cpp
#include "scm.h"
CSCM::CSCM(void)
{
hscm = OpenSCManager(…);
}
CSCM::~CSCM(void)
{
CloseServiceHandle(hscm);
}
void CSCM::Del(LPCTSTR sname)
{
SC_HANDLE hsrv = OpenService(hscm,
sname, SERVICE_ALL_ACCESS);
DeleteService(hsrv);
CloseServiceHandle(hsrv);
}

109.

• В классе CSCM хотим сделать аналог функции printf, которая
печатает диагностические сообщения
• Первый параметр – строка форматирования fmt (почитайте
про строки форматирования printf), дальше идет переменное
число аргументов, которое в языке C обозначается
троеточием:
• int printf(const char *fmt, …)
• По аналогии делаем функцию void pf(char *fmt, …)
• Внутри нее мы хотим сформировать строку, выводимую кудато на печать – для этого используем функцию vsprintf():
• int vsprintf(char *str, const char *fmt, va_list argptr );

110.

Программа с графическим интерфейсом (GUIApp)
guiapp.h
guiapp.cpp
class CguiappApp :
#include "guiapp.h"
public CWinAppEx
#include "guiappDlg.h"
{
virtual BOOL InitInstance();
CguiappApp theApp;
CWnd *m_pMainWnd;

BOOL CguiappApp::InitInstance()
};
{
extern CguiappApp theApp;

CguiappDlg dlg;
m_pMainWnd = &dlg;
dlg.DoModal();

}
guiappDlg.h
class CguiappDlg :
public Cdialog
{
CEdit m_edt_fname;
CEdit m_edt_sname;
CEdit m_edt_lname;
void OnBnClickedBtnFname();
void OnBnClickedBtnAdd();
void OnBnClickedBtnDel();
void OnBnClickedBtnStart();
void OnBnClickedBtnStop();
void OnBnClickedBtnOpen();
void OnBnClickedBtnClose();

};
scm.h
class CSCM
{
SC_HANDLE hscm;
CSCM(void);
~CSCM(void);
void Del(LPCTSTR sname);
void pf(LPCTSTR fmt, …)

};
guiappDlg.cpp
#include "guiapp.h"
#include "guiappDlg.h"
#include "scm.h"
void CguiappDlg::DoDataExchange(CDataExchange* pDX)
{
DDX_Control(pDX, IDC_EDT_SNAME, m_edt_sname);
}
BEGIN_MESSAGE_MAP(CguiappDlg, CDialog)
ON_BN_CLICKED(IDC_BTN_DEL,
&CguiappDlg::OnBnClickedBtnDel)
END_MESSAGE_MAP()
void CguiappDlg::OnBnClickedBtnDel()
{
CSCM scm;
CString sname;
m_edt_sname.GetWindowText(sname);
scm.Del(sname);
}
scm.cpp
#include "scm.h"
Будем делать аналог printf()
void CSCM::pf(LPCTSTR fmt, …)
{
//печать сообщения
}
CSCM::CSCM(void)
{
hscm = OpenSCManager(…);
}
CSCM::~CSCM(void)
{
CloseServiceHandle(hscm);
}
void CSCM::Del(LPCTSTR sname)
{
SC_HANDLE hsrv = OpenService(hscm,
sname, SERVICE_ALL_ACCESS);
if(!hsrv)
{ pf(“ERR: OpenService”); return; }
pf(“OK: OpenService”);
if(!DeleteService(hsrv))
pf(“ERR: DeleteService”);
else
pf(“OK: DeleteService”);
CloseServiceHandle(hsrv);
}

111.

• Кроме обычной форматированной печати строки, нужно уметь
получать код и выводить текстовое описание ошибок,
возникающих при работе WinAPI-функций.
• Для такого вывода делаем функцию void pferr(char *fmt, …)
• Внутри нее мы делаем такую же форматированную печать как
в случае pf, но дополнительно добавим получение кода
ошибки и строку с её текстовым описанием

112.

Программа с графическим интерфейсом (GUIApp)
guiapp.h
guiapp.cpp
class CguiappApp :
#include "guiapp.h"
public CWinAppEx
#include "guiappDlg.h"
{
virtual BOOL InitInstance();
CguiappApp theApp;
CWnd *m_pMainWnd;

BOOL CguiappApp::InitInstance()
};
{
extern CguiappApp theApp;

CguiappDlg dlg;
m_pMainWnd = &dlg;
dlg.DoModal();

}
guiappDlg.h
class CguiappDlg :
public Cdialog
{
CEdit m_edt_fname;
CEdit m_edt_sname;
CEdit m_edt_lname;
void OnBnClickedBtnFname();
void OnBnClickedBtnAdd();
void OnBnClickedBtnDel();
void OnBnClickedBtnStart();
void OnBnClickedBtnStop();
void OnBnClickedBtnOpen();
void OnBnClickedBtnClose();

};
scm.h
class CSCM
{
SC_HANDLE hscm;
CSCM(void);
~CSCM(void);
void Del(LPCTSTR sname);
void pf(LPCTSTR fmt, …);
void pferr(LPCTSTR fmt, …);

};
guiappDlg.cpp
#include "guiapp.h"
#include "guiappDlg.h"
#include "scm.h"
void CguiappDlg::DoDataExchange(CDataExchange* pDX)
{
DDX_Control(pDX, IDC_EDT_SNAME, m_edt_sname);
}
BEGIN_MESSAGE_MAP(CguiappDlg, CDialog)
ON_BN_CLICKED(IDC_BTN_DEL,
&CguiappDlg::OnBnClickedBtnDel)
END_MESSAGE_MAP()
void CguiappDlg::OnBnClickedBtnDel()
{
CSCM scm;
CString sname;
m_edt_sname.GetWindowText(sname);
scm.Del(sname);
}
scm.cpp
#include "scm.h"
void CSCM::pf(LPCTSTR fmt, …)
{ /*печать сообщения*/}
void CSCM::pferr(LPCTSTR fmt, …)
{ /*печать сообщения об ошибке*/}
CSCM::CSCM(void)
{
hscm = OpenSCManager(…);
}
CSCM::~CSCM(void)
{
CloseServiceHandle(hscm);
}
void CSCM::Del(LPCTSTR sname)
{
SC_HANDLE hsrv = OpenService(hscm,
sname, SERVICE_ALL_ACCESS);
if(!hsrv)
{ pferr(“ERR: OpenService”); return; }
pf(“OK: OpenService”);
if(!DeleteService(hsrv))
pferr(“ERR: DeleteService”);
else
pf(“OK: DeleteService”);
CloseServiceHandle(hsrv);
}

113.

Два вопроса:
1. как превратить переменное число
аргументов (…) в единственный параметр
argptr типа va_list?
2. Как получить код ошибок и текстовое
описание ошибок, возникающих при
работе системных WinAPI-функций?

114.

Как превратить переменное число аргументов (…) в единственный параметр argptr типа
va_list?
Для этого в стандартной библиотеке языка C предусмотрены макросы va_start(), va_end()
Макрос va_start()
void va_start( va_list arg_ptr, prev_param );
формирует значение параметра arg_ptr как начало списка параметров текущей функции, начиная с
параметра, следующего за prev_param. Т.е. если у нас функция например из четырех параметров, а мы
хотим описать список параметров, начиная с третьего, то в качестве prev_param мы должны указать
второй параметр.
Макрос va_end(va_list arg_ptr) завершает работу со списком параметров arg_ptr
В случае с нашей функцией void pf(char *fmt, …) мы хотим описать список параметров начиная со
второго (следующий за fmt), а потому в качестве prev_param мы должны указать первый параметр - fmt
В ИТОГЕ получим:
void pf(char *fmt, …)
{
va_list arg_ptr;
char str[1024];
va_start(arg_ptr, fmt);
vsprintf(str, fmt, arg_ptr);
va_end(arg_ptr);
/*Дальше с str что-то делаем для ее вывода на экран. В нашем случае хотим добавить эту строку в
многострочный список ListBox в нашем диалоговом окне */
}
ВАЖНО: это небезопасная реализация работы со строками – возможно переполнение буфера строки.
Для безопасной реализации вместо vsprintf необходимо использовать vsnprintf_s

115.

• Мы хотим сделать наш класс управления сервисами
CSCM быстро переносимым в другие проекты, в
частности в консольные программы.
• Для этого CSCM должен быть минимально привязан к
структуре диалогового MFC-приложения. Потому всё,
что мы будем делать в функции-члене класса CSCM для
печати сообщения – это вызывать функцию печати
сообщения из класса диалогового окна.
• При переносе CSCM в консольное приложение мы
заменим эту реализацию на вызов функции печати в
консоль, и больше никаких изменений не потребуется

116.

Как из функции pf() - члена класса CSCM вызывать функцию vpf()
класса диалогового окна CguiappDlg для печати сообщения в
многострочный список:
• Вспоминаем про theApp – экземпляр класса приложения
(смотрим схему)
• Чтобы в scm.cpp можно было использовать theApp, надо
обязательно включить заголовочный файл “guiappDlg.h” – там
объявление theApp через extern.
• “guiappDlg.h” нельзя включать без предварительного
включения “guiapp.h”, так MFC устроен.
• theApp указывает на class CguiappApp, в котором есть
m_pMainWnd – экземпляр класса нашего диалогового окна
CguiappDlg, из которого хотим вызвать vpf
• НО: m_pMainWnd объявлен как указатель на базовый для
CguiappDlg класс CWnd (смотри схему). Функции CguiappDlg
через этот указатель не видны. Требуется приведение типа:
– theApp.m_pMainWnd - это указатель на CWnd
– (CguiappDlg*)theApp.m_pMainWnd - это указатель на CguiappDlg
– ((CguiappDlg*)theApp.m_pMainWnd)->vpf() - а это функция – член
класса нашего диалогового окна, которую хотим вызвать

117.

Программа с графическим интерфейсом (GUIApp)
guiapp.h
guiapp.cpp
scm.h
class CguiappApp :
#include "guiapp.h"
class CSCM
public CWinAppEx
#include "guiappDlg.h"
{
{
virtual BOOL InitInstance(); CguiappApp theApp;
void pf(LPCTSTR fmt, …);
CWnd *m_pMainWnd;
void pferr(LPCTSTR fmt, …);

BOOL CguiappApp::InitInstance()

};
{
};
extern CguiappApp theApp; …
CguiappDlg dlg;
m_pMainWnd = &dlg;
dlg.DoModal();

}
guiappDlg.h
class CguiappDlg :
public Cdialog
{
CListBox m_lst_log;
void vpf(const char *fmt,
va_list arg_ptr);
void vpferr(const char *fmt,
va_list arg_ptr);

};
guiappDlg.cpp
#include "guiapp.h"
#include "guiappDlg.h"
#include "scm.h"
void CguiappDlg:: vpf(const char *fmt, va_list arg_ptr)
{
char str[1024];
vsprintf(str, fmt, arg_ptr);
m_lst_log.InsertString(-1, str);
}
void CguiappDlg:: vpferr(const char *fmt, va_list arg_ptr)
{
//все как в vpf, но дополнительно к сформированной
//строке str добавляем строку описания ошибки,
//полученную через FormatMessage
}
scm.cpp
#include "guiapp.h"
#include "guiappDlg.h"
#include "scm.h"
void CSCM::pf(LPCTSTR fmt, …)
{
va_list arg_ptr;
va_start(arg_ptr, fmt);
((CguiappDlg*)(theApp.m_pMainWnd))->
vpf(fmt, arg_ptr);
va_end(arg_ptr);
}
void CSCM::pferr(LPCTSTR fmt, …)
{
va_list arg_ptr;
va_start(arg_ptr, fmt);
((CguiappDlg*)(theApp.m_pMainWnd))->
vpferr(fmt, arg_ptr);
va_end(arg_ptr);}

118.

Пример переноса CSCM в консольную программу (TestCons) – когда надо управлять драйвером без использования GUI
testcons.cpp
В состав проекта консольной программы
testcons.h
#include <windows.h>
TestCons включаем две единицы
#include <stdio.h>
void vpf(const char *fmt, va_list arg_ptr);
компиляции: testcons.cpp и scm.cpp.
#include "scm.h"
void vpferr(const char *fmt, va_list arg_ptr);
В testcons.cpp реализуем функции
void vpf(const char *fmt, va_list arg_ptr);
консольной печати и объявляем их в
{
testcons.h
vprintf(fmt, arg_ptr);
В scm.cpp меняем только вызовы функций
puts(“\n”);
печати из диалогового окна – подменяем
}
их функциями печати консольного
приложения
void vpferr(const char *fmt, va_list arg_ptr);
{
vprintf(fmt, arg_ptr);
puts(“\n”);
//добавляем FormatMessage
//и вывод полученной строки
//через puts
}
void main()
{
CSCM scm;
scm.Add(“lab_x”,
“\\??\\C:\\drv\\lab_x.sys”);
scm.Start(“lab_x”);
scm.Open(“\\\\.\\labx_link”);
scm.Close();
scm.Del(“lab_x”);
}
scm.h
class CSCM
{
scm.cpp
#include "testcons.h"
#include "scm.h"
void pf(LPCTSTR fmt, …);
void pferr(LPCTSTR fmt, …);

};
void CSCM::pf(LPCTSTR fmt, …)
{
va_list arg_ptr;
va_start(arg_ptr, fmt);
vpf(fmt, argptr);
va_end(arg_ptr);
}
void CSCM::pferr(LPCTSTR fmt, …)
{
va_list arg_ptr;
va_start(arg_ptr, fmt);
vpferr(fmt, argptr);
va_end(arg_ptr);}

119.

Как получить код ошибок и текстовое описание ошибок,
возникающих при работе системных WinAPI-функций?
Использование функций FormatMessage() и GetLastError()
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
Флаг FORMAT_MESSAGE_ALLOCATE_BUFFER заставляет ОС саму выделить память под формируемую строку, адрес
выделенной памяти возвращается в lpMsgBuf, поэтому передаем его через адрес (&). Если бы не использовали
FORMAT_MESSAGE_ALLOCATE_BUFFER – память под строку должны были бы выделять сами, адрес этой памяти в
lpMsgBuf передавали бы без &.
Флаг FORMAT_MESSAGE_FROM_SYSTEM говорит ОС, что в третьем параметре передается код системной ошибки,
получаемой через GetLastError().
Для системных кодов ошибки есть текстовые описания, которые и возвращаются в lpMsgBuf с учетом настроек языка
(четвертый параметр – в данном случае используются системные языковые настройки)
В принципе можно составлять и свои таблицы строк – например при использовании флага
FORMAT_MESSAGE_FROM_HMODULE во втором параметре надо указывать HMODULE загруженного файла с таблицей
строк.
//используем lpMsgBuf для печати сообщения, например:
m_lst_log.InsertString(-1, (char*)lpMsgBuf);
// Освобождаем выделенную память (только в случае FORMAT_MESSAGE_ALLOCATE_BUFFER)
LocalFree( lpMsgBuf );

120.

Пример использования функции FormatMessage
совместно с GetLastError
LPVOID lpMsgBuf;
char str[2048];
str[2047] = ‘\0’;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
vsprintf(str, fmt, arg_ptr);
strcat(str, (char*)lpMsgBuf);
// Free the buffer.
LocalFree( lpMsgBuf );
m_lst_log.InsertString(-1, str);

121.

Примеры анализа драйвера
• Использование дизассемблера IDA Pro
• Использование среды анализа TREX

122.

Отладочные возможности процессора, особенности
в Windows, средства отладки
• Пошаговая отладка: TraceFlag в регистре флагов
• Точки прерывания:
• Модификация отлаживаемого кода – запись инструкции
int 3 (код 0xCC)
• Отладочные регистры DR0-DR3 (не более 4 точек
прерывания на доступ к памяти r/w/e)
• И при пошаговой отладке, и при использовании
отладочных регистров генерируется прерывание int 1

123.

Проблемы программных отладчиков
• Конкуренция с отлаживаемой программой за общие ресурсы:
программные отладчики могут быть выявлены путем контроля
флагового регистра, отладочных регистров, контроля целостности
кода (выявление программных точек останова)
//выполните любым системным отладчиком (не эмулятором) пошаговую отладку данного кода драйвера:
• При
отладке
ядерным
отладчиком
ядерного
кода RegistryPath)
– использование
NTSTATUS
DriverEntry(IN
PDRIVER_OBJECT
DriverObject,
IN PUNICODE_STRING
{
Задача:
этом коде
общего•стека
(можнонайти
было быошибку
запускать в
обработку
отладочных
static ULONG tmpesp;
DbgPrint("simple:
DriverEntry,
regpath=%ws\n",
RegistryPath->Buffer);
прерываний
в отдельной
задаче,
но ни один
доступный отладчик этого
(исправить
одну
инструкцию)
не делает)__asm

отлаживаемый
код
может
нарушить работу отладчика.
{
Пример.
mov tmpesp, esp
mov esp, 0
mov eax, 0
mov esp, tmpesp
}
• Доставлять
или нет отлаживаемой программе контролируемые
return STATUS_SUCCESS;
}
отладчиком
прерывания? Все существующие отладчики могут быть
выявлены при специальной организации в отлаживаемой программе
обработки исключений (механизм SEH - Structured Exception Handling
или VEH – Vectored Exception Handling). Пример.
• Катастрофическое замедление программы при пошаговой отладке все существующие отладчики могут быть выявлены путем контроля
времени исполнения фрагментов программного кода

124.

«Классические» отладчики уровня ОС фактически вытеснены
отладочными возможностями симуляторов:
backend: VmWare
frontend: интерфейс GDB (IDA, VisualStudio, …)
QEMU, VirtualPC, BOCHS, AMD SimNow
Сам по себе дизассемблер или отладчик не автоматизирует решение
задачи анализа потока данных, если под анализом понимать
отслеживание зависимостей между инструкциями (по управлению и
данным). Автоматизацией занимаются средства-«надстройки»,
например:
автоматизация поверх IDA:
Фирма zynamics (www.zynamics.com), средство BinNavi
Автоматизация поверх отладчиков - трассировка:
BitBlaze/TEMU
Valgrind + Avalanche
TREX

125.

Взаимодействие с устройствами
• ReadFile – ZwReadFile – IRP_MJ_READ
• WriteFile – ZwWriteFile – IRP_MJ_WRITE
• DeviceIoControl – ZwDeviceIoControl – IRP_MJ_DEVICE_CONTROL

126.

Пакет Запроса В/В
IRP
MdlAddress
Flags
AssociatedIrp.MasterIrp
AssociatedIrp.SystemBuffer
IoStatus
Фиксированная
часть IRP
RequestorMode
Cancel
DeviceObject1
Flags
StackSize = 3
AttachedDevice
IoCallDriver()

CancelIrql
Стек размещения
ввода/вывода
MajorFunction
MinorFunction
CancelRoutine
UserBuffer
Tail.Overlay.Thread
Tail.Overlay.ListEntry
Flags
Control
Parameters.Xxx
Device Object
File Object
I/O Stack Location N
DeviceObjectN
FltDeviceObject
Flags
StackSize = 1
AttachedDevice
Flags
StackSize = 2
AttachedDevice

I/O Stack Location 1
Пересылка запросов между устройствами - IoCallDriver()
Получение адреса устройства по имени –
IoGetDeviceObjectPointer()
Подключение фильтра – IoAttachDeviceToDeviceStack()

127.

• Драйвера подразделяются на 3 класса по их
положению в стеке драйверов: драйвера
высшего уровня, драйвера промежуточного
уровня и драйвера низшего уровня.
• Драйвер высшего уровня –верхний в стеке
драйверов, получает запросы через Диспетчер
в/в от компонентов прикладного уровня.
• Драйвер высшего уровня (или, что более
правильно, устройство высшего уровня) имеет
один или несколько стеков размещения в/в.

128.

• Число стеков размещения в/в устанавливается
Диспетчером в/в в поле StackSize объектаустройство. По умолчанию это значение равно 1.
Присваивание происходит при создании устройства
функцией IoCreateDevice(). Если создаётся
многоуровневый драйвер, необходимо установить
StackSize на 1 больше, чем StackSize нижележащего
объекта-устройство. В случае, если устройство будет
использовать больше одного устройства уровнем
ниже, его поле StackSize должно быть на 1 больше
максимального значения StackSize всех устройств
уровнем ниже.

129.

• Код запроса в/в сохранен в поле MajorFunction (есть
еще второстепенный код MinorFunction) текущего
стека размещения ввода - вывода в IRP
PIO_STACK_LOCATION IoStack;
IoStack = IoGetCurrentIrpStackLocation(Irp);
switch (IoStack->MajorFunction)
{
case IRP_MJ_READ:

break;

}
• Доступ к стеку нижележащего устройства:
IoStack = IoGetNextIrpStackLocation(Irp);

130.

BOOL WINAPI ReadFile(
__in HANDLE
hFile,
__out LPVOID
lpBuffer,
__in DWORD
nNumberOfBytesToRead,
__out_opt LPDWORD lpNumberOfBytesRead,
__inout_opt LPOVERLAPPED lpOverlapped );

131.

BOOL WINAPI WriteFile(
__in HANDLE
hFile,
__in LPCVOID
lpBuffer,
__in DWORD
nNumberOfBytesToWrite,
__out_opt LPDWORD lpNumberOfBytesWritten,
__inout_opt LPOVERLAPPED lpOverlapped );

132.

• По адресу lpNumberOfBytesRead/
lpNumberOfBytesWritten в результате успешного
завершения функции помещается содержимое поля
Irp->IoStatus.Information диспетчерской функции
драйвера, обработавшего запрос

133.

Метод передачи буфера, используемый в запросах чтения и записи,
контролируется полем Flags объекта-устройство (DeviceObject->Flags).
После
создания
объекта-устройство
с
помощью
функции
IoCreateDevice() необходимо выставить в нем нужные флаги.
Можно устанавливать несколько флагов, при этом применяются
следующие правила:
Если установлены флаги DO_BUFFERED_IO или DO_DIRECT_IO,
метод передачи буфера будет соответственно буферизованным или
прямым
Если поле флагов не инициализировано (никакие флаги не
установлены), используется метод передачи буфера Neither.
Одновременная
установка
флагов
DO_BUFFERED_IO
DO_DIRECT_IO запрещена и будет являться ошибкой.
и

134.

Прямой в/в
Буферизованный в/в
“никакой” в/в
(Direct I/O)
Buffered I/O
Neither I/O
Буфер инициатора
Описывается с
временный буфер в
виртуальный адрес
запроса
помощью MDL
невыгружаемой системной
инициатора запроса
памяти
Описание буфера в Irp->MdlAddress
IRP
Контекст, при
котором буфер
Irp->AssociatedIrp.SystemBuffer Irp->UserBuffer
содержит указатель виртуальный адрес
на MDL
временного буфера в
Случайный
контекст
не проверенный на
доступность
системной области памяти в виртуальный адрес
невыгружаемой памяти
буфера инициатора
(non-paged pool)
запроса в/в
Случайный контекст
Только контекст потока
- инициатора запроса
может быть
использован
Уровень IRQL, при IRQL <
Любой
котором буфер
DISPATCH_LEVEL
может быть
использован
IRQL <
DISPATCH_LEVEL

135.

• Размер буфера для операций чтения/записи
расположен в стеке размещения в/в:
• Stack->Parameters.Read.Length
• Stack->Parameters.Write.Length
• С точки зрения языка СИ поле Parameters – это
объединение (union) структур для всех видов
запросов
• т.е. одна и та же область памяти (поле Parameters)
может интерпретироваться различно в зависимости
от вида запроса (код главной функции
Stack->MajorFunction)

136.

Neither I/O
Адр. простр.1
Адр. простр.2
virtual
address
физ. память
buf_1
buf_2
user
kernel
чтение/запись

137.

Direct I/O
Адр. простр.1
Адр. простр.2
virtual
•address
MDL – структура, описывающая буфер с сохранением
физ. память
информации
об
адресном
пространстве,
по
сути
buf_1
buf_2
описываются страницы физической памяти
user
• Буфер в пользовательском диапазоне
адресов, описанный
kernel
через MDL, можно использовать вне зависимости от
текущего адресного пространства
MDL
• MmGetSystemAddressForMdl()
Преобразование MDL в буфер в виртуальной памяти в
системном диапазоне адресов:
buf_3
• OutBuffer = MmGetSystemAddressForMdl(
Irp->MdlAddress );
чтение/запись

138.

Buffered I/O
Адр. простр.1
Адр. простр.2
virtual
address
физ. память
buf_1
1. Передача запроса в/в
Запись: копирование
Чтение: ничего
Буфер в
невыгружаемой
памяти
buf_2
2. Завершение запроса в/в
Запись: ничего
Чтение: копирование
Irp->IoStatus.Information
buf_3
чтение/запись
user
kernel

139.

BOOL WINAPI DeviceIoControl(
__in HANDLE
hDevice,
__in DWORD
dwIoControlCode,
__in_opt LPVOID
lpInBuffer,
__in DWORD
nInBufferSize,
__out_opt LPVOID lpOutBuffer,
__in DWORD
nOutBufferSize,
__out_opt LPDWORD lpBytesReturned,
__inout_opt LPOVERLAPPED lpOverlapped );

140.

• По адресу lpBytesReturned в результате успешного
завершения функции помещается содержимое поля
Irp->IoStatus.Information диспетчерской функции
драйвера, обработавшего запрос
• Способ передачи буфера в отличие от запросов
чтения/записи управляется полем Method в значении
контрольного кода dwIoControlCode

141.

CTL_CODE( DeviceType, Function, Method, Access )
31 30
c
o
m
m
o
n
16 15 14 13 12
c
u
Access s
t
o
m
DeviceType
2
Function
1
0
Method
DeviceType определяет тип объекта-устройство, которому предназначен запрос. Это тот
самый тип устройства, который передается функции IoCreateDevice() при создании
устройства.
Существует два диапазона значений типов устройств:
0-32767 – зарезервированные значения для стандартных типов устройств,
32768-65535 – диапазон значений типов устройств для выбора разработчиком.
Function идентифицирует конкретные действия, которые должно предпринять устройство
при получении запроса. Значение должны быть уникальным внутри устройства. Два
диапазона значений:
0-2047 – зарезервированный диапазон значений,
2048-4095 – диапазон значений, доступный разработчикам устройств.
FILE_ANY_ACCESS
FILE_READ_ACCESS
FILE_WRITE_ACCESS
0
0x01
0x02
METHOD_BUFFERED
METHOD_IN_DIRECT
METHOD_OUT_DIRECT
METHOD_NEITHER
0
1
2
3

142.

METHOD_BUFFERED
InBuf
где
расположен
Адрес промежуточного буфера в
Irp->AssociatedIrp.SystemBuffer
Длина
OutBuf
METHOD_IN_DIRECT METHOD_OUT_DIRECT METHOD_NEITHER
В стеке размещения
в/в виртуальный адрес
инициатора запроса в
Parameters.
DeviceIoControl.
Type3InputBuffer
в текущем стеке размещения в/в
Parameters.DeviceIoControl.InputBufferLength.
где
Адрес
MDL, адрес в Irp->MdlAddress Виртуальный адрес
расположен
инициатора запроса в
промежуточного
Irp->UserBuffer
буфера в
Irp->AssociatedIrp.
SystemBuffer
Длина
в текущем стеке размещения в/в
Parameters.DeviceIoControl.OutputBufferLength.

143.

METHOD_BUFFERED
InBuf[InBufferSize ]
OutBuf[OutBufferSize ]
1. Перед вызовом
IRP_MJ_DEVICE_CONTROL
копируется InBufferSize байт
2. После успешного завершения
IRP_MJ_DEVICE_CONTROL
копируется Irp->IoStatus.Information байт
Irp->AssociatedIrp.SystemBuffer
Промежуточный буфер
[max(InBufferSize, OutBufferSize)]

144.

Механизмы синхронизации
• Спин-блокировки – для межпроцессорной
синхронизации, синхронизации на уровне
IRQL=DISPATCH_LEVEL (обычные блокировки) или
>DISPATCH_LEVEL (DIRQL)(блокировки обработчиков
прерываний)
• Для синхронизации на уровне <DISPATCH_LEVEL –
диспетчерские объекты,
• либо Ресурсы Исполнительной системы (позволяют
управлять синхронизацией вида «писатель читатели»)

145.

GUI_App
HANDLE hFile=CreateFile(“\\.\SymLinkName”, …)
DriverObject
NextDevice
AttachedDevice
DeviceExtension
FILE_OBJECT
DeviceObject
DRIVER_OBJECT
(TargetDriver)
DEVICE_OBJECT
GuiDev
DriverObject
NextDevice
DeviceExtension
DEVICE_OBJECT
TargetDev
DeviceObject
MajorFunction[]
DRIVER_OBJECT
(FilterDriver)
DeviceObject
MajorFunction[]
DEVICE_OBJECT
AttachedDev1
DriverObject
NextDevice
AttachedDevice
DeviceExtension
DEVICE_OBJECT
FilterDev

DriverObject
NextDevice
AttachedDevice
DeviceExtension

146.

• IoGetDeviceObjectPointer() – по имени
устройства получаем его DEVICE_OBJECT
(TargetDevice)
• IoCreateDevice() создаем FilterDevice
• IoAttachDeviceToDeviceStack() подключаем
FilterDevicve к TargetDevice, в результате
получаем адрес последнего устройства в
цепочке AttachedDevice

147.

Спин-блокировки (spin-lock)
• Спинлоки служат для обеспечения монопольного
доступа потока к защищаемой структуре данных.
• Физически спинлок представляет собой переменную в
памяти и реализуется на атомарных операциях, которые
должны присутствовать в системе команд процессора.
Каждый процессор, желающий получить доступ к
разделяемому ресурсу, атомарно записывает условное
значение «занято» в эту переменную, используя аналог
операции swap (в архитектуре x86 — xchg). Если
предыдущее значение переменной (возвращаемое
командой) было «свободно» то считается, что данный
процессор получил доступ к ресурсу, в противном
случае, процессор возвращается к операции swap и
крутится в цикле ожидая, пока спинлок будет
освобождён. После работы с разделяемым ресурсом
процессор-владелец спинлока должен записать в него
условное значение «свободно».

148.

Пример реализации спин-блокировки
(для ядра Windows неверен)
mov eax, spinlock_address
mov ebx, SPINLOCK_BUSY
wait_cycle:
lock xchg [eax], ebx
cmp ebx, SPINLOCK_FREE
jnz wait_cycle
Поток 1
Поток 2
1.
3,4...
SP
2.

149.

CPU
3.
6.
Поток 1
8.
Поток 2
IRQL=PASSIVE_LEVEL
1.
7.
4,5...
SP
2.
9.
IRQL=PASSIVE_LEVEL

150.

CPU
3.
6.
Поток 1
Поток 2
IRQL=PASSIVE_LEVEL
IRQL=DISPATCH_LEVEL
1.
4,5...
SP
2.
IRQL=PASSIVE_LEVEL
Квантование времени выключено,
переключение на поток1 невозможно
deadlock

151.

Правильная реализация спин-блокировок в ядре Windows:
• в момент захвата спин-блокировки уровень IRQL повышается до некоторого уровня
IRQL>=DISPATCH, ассоциированного со спин-блокировкой;
• в момент освобождения - восстановление старого уровня IRQL.
CPU1
CPU2
CPU
4.
Поток 1
Поток 2
IRQL=PASSIVE_LEVEL
IRQL=DISPATCH_LEVEL
1.
3.
5
SP
2.
6.
IRQL=DISPATCH_LEVEL
Однопроцессорная система: если какой-то
поток уже захватил блокировку,
переключение на другой поток невозможно
до ее освобождения (за счет отключения
механизма вытесняющей многозадачности)
Поток 1
Поток 2
IRQL=PASSIVE_LEVEL
IRQL=DISPATCH_LEVEL
1.
5.
3,4...
SP
2.
6.
IRQL=DISPATCH_LEVEL
Многопроцессорная система: попытка захвата
уже занятой блокировки может последовать
только со стороны другого процессора, его
работа блокируется до освобождения
блокировки первым процессором

152.

Правило использования спин-блокировок –
ограничение на IRQL до и после захвата:
IRQLдо <= IRQLпосле
С каждой спин-блокировкой связан конкретный уровень
IRQL, на который перейдет процессор после захвата
блокировки.
В соответствии с правилом, нельзя использовать блокировку
из кода, работающего на IRQL>IRQL блокировки.
В Windows – 2 вида спин-блоктровок:
• Обычные – с ними связан IRQL=DISPATCH_LEVEL
• Спин-блокировки синхронизации прерываний – с ними
связан один из DIRL

153.

Функции для работы с обычными спин-блокировками
VOID KeInitializeSpinLock(IN PKSPIN_LOCK SpinLock);
VOID KeAcquireSpinLock(IN PKSPIN_LOCK SpinLock, OUT PKIRQL OldIrql);
VOID KeReleaseSpinLock(IN PKSPIN_LOCK SpinLock, IN KIRQL NewIrql);
VOID KeAcquireLockAtDpcLevel(IN PKSPIN_LOCK SpinLock);
VOID KeReleaseLockFromDpcLevel(IN PKSPIN_LOCK SpinLock);
typedef struct _DEVICE_EXTENSION
{
...
KSPIN_LOCK spinlock
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
NTSTATUS DriverEntry(....)
{
KeInitializeSpinLock(&extension->spinlock);
}
NTSTATUS DispatchReadWrite( .... )
{
KIRQL OldIrql;
...
KeAcquireSpinLock(&extension->spinlock, &0ldIrql);
// произвести обработку данных, защищенных спин-блокировкой
KeReleaseSpinLock(&extension->spinlock, OldIrql);
}

154.

Взаимоблокировки (deadlocks)
• Решение: блокировки должны
захватываться всеми потоками
в одном порядке
• Проблема:
Поток 1
1
SP1
1.
Поток 1
4
3,...
4,...
Поток 2
2
SP2
SP1
5,...
3.
Поток 2
6.
2.
SP2

155.

Диспетчерские объекты
Dispatcher Objects
• набор механизмов синхронизации, рассчитанных
на применение в основном для уровня IRQL
PASSIVE_LEVEL.
• В начале каждого объекта – структура
DISPATCHER_HEADER
• Два состояния: сигнальное и несигнальное
• Типы диспетчерских объектов различаются
правилом изменения состояния (правило перехода
в сигнальное или несигнальное состояние
• поток, ожидающий захвата диспетчерского
объекта, блокирован и помещен в список
ожидания в структуре DISPATCHER_HEADER.

156.

• Блокирование потока – состояние
потока, при котором он не занимает
время процессора.
• Блокированный поток не будет
поставлен планировщиком в очередь на
исполнение до тех пор, пока не будет
выведен из состояние блокирования.

157.

Тип Объекта
Переход в сигнальное состояние
Результат для ожидающих потоков
Мьютекс (Mutex)
Освобождение мьютекса
Освобождается один из ожидающих
потоков
Семафор
Счетчик захватов становится
Освобождается некоторое число
(Semaphore)
ненулевым
ожидающих потоков
Событие
Установка события в сигнальное
Освобождается один из ожидающих
синхронизации
состояние
потоков
Событие
Установка события в сигнальное
Освобождаются все ожидающие
оповещения
состояние
потоки
Таймер
Time arrives or interval elapses
Освобождается один из ожидающих
синхронизации
потоков
Таймер оповещения Time arrives or interval elapses
Освобождаются все ожидающие
потоки
Процесс
Поток
Завершился последний поток
Освобождаются все ожидающие
процесса
потоки
Поток завершился
Освобождаются все ожидающие
потоки
Файл
Завершена операция в/в
Освобождаются все ожидающие

158.

• Диспетчерские объекты могут иметь имена в
пространстве имен диспетчера объектов
(обычно директория \BaseNamedObjects)
• Через эти имена существующий объект можно
открывать из приложений пользовательского
режима или ядра ОС.
• Кроме того, код ядра может получить доступ к
объекту по его описателю (HANDLE):
ObReferenceObjectByHandle()
• Для окончания использования объекта,
полученного через
ObReferenceObjectByHandle() – функция
ObDereferenceObject()

159.

Ожидание (захват) диспетчерских объектов
• Для ожидания момента перехода объекта из несигнального в
сигнальное состояние служат специальные функции ожидания:
KeWaitForSingleObject() и KeWaitForMultipleObjects()
• в качестве одного из их параметров указывается интервал времени
ожидания.
• Функции вернут управление либо при захвате объекта, либо при
истечении времени ожидания
• !!! либо если были вызваны с ненулевым временем ожидания на
IRQL=DISPATCH_LEVEL (вытесняющая многозадачность отключена,
заменить текущий поток нечем) – так делать нельзя!!!
• С ненулевым временем ожидания можно вызывать при
IRQL<DISPATCH_LEVEL
• С нулевым временем ожидания можно вызывать на
IRQL<=DISPATCH_LEVEL
• Нулевое время ожидания:
параметр Timeout == NULL или *Timeout == 0
• Нулевое время ожидания используется для проверки состояния
объекта без блокирования потока
• На DISPATCH_LEVEL можно использовать потому, что поток не
блокируется, т.е. нет необходимости переключаться на другой поток

160.

Мьютексы ядра
• Мьютекс (mutex = Mutually EXclusive) означает
взаимоисключение, т.е. мьютекс обеспечивает
нескольким потокам взаимоисключающий доступ к
совместно используемому ресурсу. В отличие от спинблокировки, ожидающий поток не блокирует
процессор.
• захват мьютекса является уникальным в рамках
конкретного контекста потока. Поток, в контексте
которого произошел захват мьютекса, является его
владельцем, и может впоследствии рекурсивно
захватывать его. Драйвер, захвативший мьютекс в
конкретном контексте потока, обязан освободить его в
том же контексте потока, нарушение этого правила
приведет к появлению “синего экрана”.
• Для мьютексов предусмотрен механизм исключения
взаимоблокировок

161.

• VOID KeInitializeMutex(IN PKMUTEX Mutex, IN ULONG Level);
• LONG KeReleaseMutex(IN PKMUTEX Mutex, IN BOOLEAN Wait);
Если параметр Wait равен TRUE, сразу за вызовом
KeReleaseMutex() должен следовать вызов одной из функций
ожидания KeWaitXxx(). В этом случае гарантируется, что
пара функций – освобождение мьютекса и ожидание – будет
выполнена как одна операция, без возможного в противном
случае переключения контекста потока.
• LONG KeReadStateMutex(IN PKMUTEX Mutex);

162.

семафор
более гибкая форма мьютексов. В отличие от мьютексов, программа
имеет контроль над тем, сколько потоков одновременно могут
разблокироваться семафором.
VOID KeInitializeSemaphore(
IN PKSEMAPHORE Semaphore,
IN LONG Count,
IN LONG Limit);
• Count – начальное значение, присвоенное
семафору, определяющее число свободных в
данный момент ресурсов. Если Count=0, семафор
находится в несигнальном состоянии (свободных
ресурсов нет), если >0 – в сигнальном.
• Limit – максимальное значение, которое может
достигать Count (максимальное число свободных
ресурсов).

163.

LONG KeReleaseSemaphore(
_Inout_ PRKSEMAPHORE Semaphore,
_In_
KPRIORITY Increment,
_In_
LONG Adjustment,
_In_
BOOLEAN Wait
);
Adjustment:
• на сколько должно увеличиться поле Count
семафора (Count = Count + Adjustment ).
• Не м.б. отрицательным.
• Если Count + Adjustment > Limit, изменение Count не
происходит, генерируется исключение
STATUS_SEMAPHORE_LIMIT_EXCEEDED

164.

Использование семафоров: задача
«потребители – производители»
Начальное состояние:
очередь заданий пуста, все задания в списке пустых заданий, число заданий = MaxJobs
S1(Count=0, Limit=MaxJobs)
S2(Count=MaxJobs, Limit=MaxJobs)
Потоки - потребители
Потоки - производители
ReleaseSemaphore
Обработка и
возврат
обработанного
задания
производителю
WaitFor…
S1
S2
WaitFor…
Очередь
заданий
ReleaseSemaphore
Список
пустых
заданий
Заполнение
задания на
обработку
потребителем

165.

События (events)
Позволяют проводить синхронизацию исполнения
различных потоков, т.е. один или несколько потоков
могут ожидать перевода события в сигнальное состояние
другим потоком.
два вида событий:
• события синхронизации (synchronization events).
при переводе в сигнальное состояние будет
разблокирован один поток, после чего событие
автоматически переходит в несигнальное состояние.
• Оповещающие события (notification event):
при переводе в сигнальное состояние будут
разблокированы все ожидающие событие потоки.
Перевод события в несигнальное состояние - вручную.

166.


KeInitializeEvent()
IoCreateNotificationEvent()
IoCreateSynchronizationEvent()
KeClearEvent() и KeResetEvent() – сброс в
несигнальное состояние
• KeSetEvent() – перевод в сигнальное состояние
• KePulseEvent – пара KeSetEvent(), KeResetEvent()
• Уведомляющие события:
При использовании KePulseEvent или подряд идущих
KeSetEvent() + KeClearEvent() / KeResetEvent()
гарантируется освобождение ВСЕХ ожидающий это
событие потоков
English     Русский Правила