Многие из вас, уважаемыее, пользуются функциями ядра Windows. Однако далеко не все представляют себе, как же эти функции вызываются. А знание некоторых механизмов ядра может быть весьма полезным.
Этот текст не предназначается драйверописателям, которые (по идее) знают это даже на более глубоком уровне.
Начнем мы с того, что в режиме ядра гораздо больше функций, чем во всем известном WinAPI. Связано это с тем, что в ядро заложено множество недокументированных функций(правда, ситуация улучшилась с выходом Win7 и Win8, многие такие функции попадают на MSDN). Список таких функций можно посмотреть, например, здесь.
Эти функции можно разделить на несколько групп:
- Kernel API — функции находятся в ntoskrnl.exe (на самом деле обычная dll), основные функции ядра.
- Windowing API — функции находятся в gdi32.dll, функции для работы с окнами и графикой.
- Messaging API — функции находятся в user32.dll, функции обработки сообщений.
Как же все это вызывается?
Начнем с вызова из пользовательского режима.
— Первым делом мы обратимся к NtDll.dll — «прослойке» между режимом пользователя и режимом ядра. В ней находится нужная нам функция ядра. Эта функция помещает в регистр EAX свой номер. Он является индексом в двух таблицах, известных ядру: ServiceTable и ArgumentTable. Характерно то, что номер этот специфичен для каждой версии ОС. Нередко он меняется не просто при переходе от, скажем, WinXP к Vista, а даже при переходе от Vista к Vista SP1.
— Далее, в EDX помещается адрес вершины стека параметров, в большинстве случаев это значение регистра ESP. Казалось бы, зачем копировать уже существующее значение? Дело в том, что после выполнения прерывания на стек сбросится все состояние процессора, и ESP уже будет не тем, что нужен нам.
— Наконец происходит переключение в режим ядра. На старых процессорах и системах это выполняется с помощью прерывания 0x2e, на современных же для этого существует специальная инструкция процессора: SysEnter для Intel, SysCall для AMD.
— Результатом вызова является выполнение процессором кода, адрес которого находится в глобальной таблице прерываний в определенной ячейке. Этот код вызывает либо KiSystemService(), в случае прерывания, либо KiFastCallEntry(), в случае специальной инструкции процессору. Кстати говоря, вторая зарегистрирована в скрутом регистре MSR, что обеспечивает быстрый доступ к ней.
— Далее код, работающий в ядре, выполняется немного по-разному, в зависимости от того, откуда пришел вызов функции, из режима ядра или из пользовательского режима. Задача кода ядра: по внутренней таблице функций выполнить вызов функции из EAX. Эта таблица называется KiServiceDescriptorTable. Это структура с тремя полями:
- адресом на таблицу ServiceTable (nt!KiServiceTable) — массив функций
- адресом на ArgumentTable (nt!KiArgumentTable) — кол-во байтов, которое занимают параметры
- количество элементов в этих таблицах
Когда вызываются функции SysEnter and SysCall, им нужно выполнить диспетчеризацию к функциям ОС. Каждый поток имеет стек пользовательского режима и режима ядра. ОС берет в таблице ArgumentTable значение ячейки, которое показывает, сколько байтов занимают параметры на стеке(для этого мы запоминали EDX). Потом она копирует это количество байтов на стек ядра. И после этого выполняет вызов функций через ServiceTable. Вся эта морока с двумя стеками нужна, разумеется, для уверенности, что никакие изменения в пользовательском режиме не навредят выполнению функции в ядре.
— После того, как функция вызвана и выполнена, и ее работа завершается, возврат из режима ядра происходит с помощью инструкии iret или SysExit/SysRet.
Вызов функций ядра в режиме ядра.
В принципе, имеем все то же самое, но…
Если внимательно просмотреть список функций ядра, то можно заметить, что существуют функции, полностью аналогичные nt- функциям, но с префиксом zw-. Все они однозначно отображены на nt функции. Однако в режиме ядра они работают по-разному.
Разница же состоит в том, что в функциях zw- не проводится валидация параметров. Иными словами, при вызове из пользовательского режима система тщательно следит за параметрами, передаваемыми в функцию — не дай бог программист что-то упустит, и привет, синий экран. В режиме ядра же примущественно работают с драйверами, и там система уже полагает, что вы все параметры сами проверите, и гарантируете их валидность, ведь дополнительные проверки съедят драгоценное время. Работает это так: если вызывается zw, то он загружает в EAX номер функции, в EDX ложит указатель на стек ядра, а потом вызывает nt- вариант, который не проводит валидацию.
В связи с чем возникает вопрос — а что, если я выхову zw- функцию из режима пользователя? Система все простит? Не совсем. Если в usermode вызывается zw, то он просто вызовет своего nt- близнеца.
Вот, в общем-то и вся базовая информация о вызовах функций ядра. Надеюсь, что вам было интересно.
Автор: Angelore