На процессорах x86, предшествующих процессорам Pentium II, Windows использует инструкцию 0x2e (46 в десятичном выражении), которая приводит к системному прерыванию. Windows заполняет запись 46 в IDT указателем на диспетчер системных служб.
Системное прерывание заставляет выполняемый поток перейти в режим ядра и войти в диспетчер системных служб. В регистр процессора EAX передается числовой аргумент, показывающий номер запрашиваемой системной службы. Регистр EDX указывает на список параметров, который вызывающая программа передает системной службе. Для возвращения в пользовательский режим диспетчер системной службы использует инструкцию iret (interrupt return — возврат из прерывания).
На процессоре x86 Pentium II и выше Windows использует инструкцию sysenter, которую компания Intel определила специально для быстродействующих диспетчеров системных служб. Для поддержки инструкции Windows сохраняет во время загрузки адрес процедуры диспетчера системных служб, находящейся в ядре, в машинно-зависимый регистр (machine-specific register, MSR), связанный с инструкцией.
Выполнение инструкции приводит к переходу в режим ядра и к выполнению кода диспетчера системных служб. Номер системной службы передается в регистр процессора EAX, а регистр EDX указывает на список аргументов вызывающей программы. Для возврата в пользовательский режим диспетчер системных служб обычно выполняет инструкцию sysexit.
В некоторых случаях, например, при установленном на процессоре флаге пошагового выполнения, диспетчер системных служб использует вместо нее инструкцию iret, потому что sysexit не позволяет возвращаться в пользовательский режим с измененным регистром EFLAGS, что необходимо в том случае, если инструкция sysenter была выполнена, когда флаг trap был установлен в результате трассировки, проводимой отладчиком в пользовательском режиме или при пропуске системного вызова.
ПРИМЕЧАНИЕ. Поскольку некоторые старые приложения могли быть жестко запрограммированы на использование инструкции int 0x2e для самостоятельного использования системного вызова (неподдерживаемой операции), 32-разрядная версия Windows сохраняет этот механизм готовым к использованию на системах, поддерживающих инструкцию sysenter, по-прежнему регистрируя обработчик.
В архитектуре x64 Windows использует инструкцию syscall, передавая номер системного вызова в регистре EAX, и любые параметры, вне тех четырех, в стеке.
В архитектуре IA64 Windows использует инструкцию epc (привилегированный режим ввода — EnterPrivilegedMode). Первые 8 аргументов системного вызова передаются в регистрах, а остальные 8 передаются в стеке.
Определение местоположения диспетчера системных служб.
Как уже ранее говорилось, вызовы 32-разрядной системы осуществляются через прерывание, из чего следует, что обработчик должен быть зарегистрирован в IDT или через специальную инструкцию sysenter, которая во время загрузки использует для сохранения адреса обработчика регистр MSR. На некоторых 32-разрядных системах AMD Windows использует вместо нее инструкцию syscall, которая похожа на 64-разрядную инструкцию syscall. Определить местоположение соответствующей процедуры для любого метода можно следующим образом:
- Для просмотра обработчика на 32-разрядных системах с версией системного вызова диспетчера с помощью прерывания 2E нужно набрать в отладчике ядра команду !idt 2e.
lkd> !idt 2e
Dumping IDT:
2e: 8208c8ee nt!KiSystemService
- Для просмотра обработчика на системах с версией sysenter нужно воспользоваться командой отладчика rdmsr для чтения данных из MSR-регистра 0x176, в котором хранится адрес обработчика:
lkd> rdmsr 176
msr[176] = 00000000'8208c9c0
lkd> ln 00000000'8208c9c0
(8208c9c0) nt!KiFastCallEntry
При использовании 64-разрядной машины можно посмотреть на 64-разрядный диспетчер вызова служб, повторяя этот шаг, но используя вместо прежнего MSR-регистр 0xC0000082, который используется для вызова версии syscall для 64-разрядного кода. Вы увидите, что он соответствует
nt!KiSystemCall64:
lkd> rdmsr c0000082
msr[c0000082] = fffff800'01a71ec0
lkd> ln fffff800'01a71ec0
(fffff800'01a71ec0) nt!KiSystemCall64
- Можно дизассемблировать процедуру KiSystemService или процедуру KiSystemCall64 с помощью команды u. В итоге на 32-разрядной системе вы заметите следующие инструкции:
nt!KiSystemService+0x7b:
8208c969 897d04 mov dword ptr [ebp+4],edi
8208c96c fb sti
8208c96d e9dd000000 jmp nt!KiFastCallEntry+0x8f (8208ca4f)
Поскольку реальные операции диспетчеризации системных вызовов являются общими, независимо от механизма, используемого для выхода на обработчик, старый обработчик, основанный на применении прерывания, для выполнения тех же общих задач просто вызывается в середине более нового обработчика, основанного на применении инструкции sysenter.
Единственные отличающиеся части обработчиков связаны с генерацией фрейма системного прерывания и установкой значений конкретных регистров.
Во время загрузки 32-разрядная Windows определяет тип процессора, на котором она выполняется, и устанавливает соответствующий используемый код системного вызова путем сохранения указателя на правильный код в структуре SharedUserData. Код системной службы для NtReadFile в пользовательском режиме имеет следующий вид:
0:000> u ntdll!NtReadFile
ntdll!ZwReadFile:
77020074 b802010000 mov eax,102h
77020079 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub
(7ffe0300)
7702007e ff12 call dword ptr [edx]
77020080 c22400 ret 24h
77020083 90 nop
Номер системной службы — 0x102 (258 в десятичном формате), а инструкция call выполняет установленный ядром код диспетчера системной службы, чей указатель находится по адресу 0x7ffe0300. (Это соответствует элементу SystemCallStub структуры KUSER_SHARED_DATA, который начинается с адреса 0x7FFE0000.)
Поскольку следующий вывод взят из Intel Core 2 Duo, он содержит указатель на sysenter:
0:000> dd SharedUserData!SystemCallStub l 1
7ffe0300 77020f30
0:000> u 77020f30
ntdll!KiFastSystemCall:
77020f30 8bd4 mov edx,esp
77020f32 0f34 sysenter
Так как у 64-разрядных систем есть только один механизм для осуществления системных вызовов, точки входа системной службы в Ntdll.dll, как показано здесь, напрямую используют инструкцию syscall:
ntdll!NtReadFile:
00000000'77f9fc60 4c8bd1 mov r10,rcx
00000000'77f9fc63 b810200000 mov eax,0x102
00000000'77f9fc68 0f05 syscall
00000000'77f9fc6a c3 ret
Kernel-Mode System Service Dispatching
Для обнаружения информации о системной службе в таблице диспетчера системной службы ядро использует номер системного вызова. На 32-разрядных системах эта таблица похожа на таблицу диспетчера прерываний, за исключением того, что каждая запись содержит указатель на системную службу, а не на процедуру обработки прерывания.
На 64-разрядных системах таблица реализована несколько иначе, она содержит не указатели на системные службы, а смещения относительно самой таблицы. Этот механизм адресации лучше подходит имеющемуся в системе x64 двоичному интерфейсу прикладных программ — application binary interface (ABI) и формату кодирования инструкций.
ПРИМЕЧАНИЕ. В зависимости от используемого пакета обновлений номера системных служб могут меняться — компания Microsoft время от времени добавляет или удаляет системные службы, и номера системных служб генерируются автоматически, как часть компиляции ядра.
Диспетчер системных служб, KiSystemService, копирует аргументы вызывающей программы из стека потока пользовательского режима в свой стек режима ядра (чтобы пользователь не смог изменить аргументы при обращении к ним ядра), а затем выполняет системную службу. Ядро получает представление о том, сколько байт стека нужно копировать, благодаря использованию второй таблицы, которая называется таблицей аргументов и является байтовым массивом (в отличие от массива указателей вроде таблицы диспетчеризации). Каждая запись дает описание количества байт для копирования.
На 64-разрядных системах Windows кодирует эту информацию в саму таблицу службы посредством процесса, который называется уплотнением таблицы системных вызовов. Если аргументы, передаваемые системной службе, указывают на буферы в пользовательском пространстве, эти буферы должны быть проверены на доступность, прежде чем код режима ядра сможет копировать данные в эти буферы или из них. Эта проверка осуществляется только тогда, когда предыдущий режим (previous mode) потока установлен на пользовательский режим.
Предыдущий режим является значением (режим ядра или пользовательский режим), которое ядро сохраняет в потоке, когда в нем выполняется обработчик системного прерывания и идентифицируется уровень привилегий входящего исключения, системного прерывания или системного вызова. В качестве оптимизации, если системный вызов поступает от драйвера или от самого ядра, проверка и захват параметров пропускаются, и все параметры считаются указывающими на допустимые буферы режима ядра (также разрешается доступ к данным в режиме ядра).
Поскольку системные вызовы могут также осуществляться кодом, выполняемым в режиме ядра, давайте рассмотрим способ их реализации. Так как код для каждого системного вызова выполняется в режиме ядра, а вызывающая программа уже выполняется в режиме ядра, можно прийти к выводу, что какого-либо прерывания или операции sysenter не требуется: центральный процессор уже находится на нужном уровне привилегий, и драйверы, как и ядро, должны только лишь иметь возможность непосредственного вызова требуемой функции.
В случае исполняющей системы именно это и происходит: ядро имеет доступ ко всем своим собственным процедурам и может просто вызвать их точно так же, как вызывает стандартные процедуры. Но внешне драйверы могут получить доступ к этим системным вызовам только тогда, когда эти вызовы экспортированы подобно другим стандартным API-функциям режима ядра. Фактически, экспортировано довольно много системных вызовов.
Но такой способ доступа для драйверов не предусматривается. Вместо этого драйверы должны использовать Zw-версии этих вызовов, то есть вместо NtCreateFile они должны использовать ZwCreateFile. Эти Zw-версии должны быть также вручную экспортированы ядром, их немного, но на них имеется полная документация и поддержка.
Из-за рассмотренного ранее понятия предыдущего режима Zw-версии официально доступны только для драйверов. Поскольку значение предыдущего режима обновляется только при каждом создании ядром фрейма системного прерывания, в связи с простым API-вызовом оно изменяться не будет, поскольку никакого фрейма системного вызова сгенерировано не будет.
При непосредственном вызове таких функций, как NtCreateFile, ядро сохраняет значение предыдущего режима, которое показывает, что оно относится к пользовательскому режиму, обнаруживает, что переданный адрес относится к адресу режима ядра, и не выполняет вызов, правильно полагая, что приложения пользовательского режима не должны передавать указатели режима ядра. Но на самом деле ведь это не так, тогда как же ядро должно разобраться в правильном предыдущем режиме? Ответ заключается в Zw-вызовах.
Эти экспортированные API-функции на самом деле не являются простыми псевдонимами или оболочками Nt-версий. Вместо этого они являются своеобразными «батутами» для прыжка к соответствующим системным Nt-вызовам, использующим тот же самый механизм диспетчеризации системных вызовов.
Вместо генерирования прерывания или использования инструкции sysenter, которые не отличались бы скоростью работы и (или) не поддерживались бы, они создают искусственный стек прерывания (стек, который центральный процессор сгенерировал бы после прерывания) и непосредственно вызывают процедуру KiSystemService, фактически имитируя прерывание центрального процессора.
Обработчик выполняет те же операции, что и при поступлении этого вызова из пользовательского режима, за исключением того, что он обнаруживает фактический уровень привилегий, с которым поступил вызов, и устанавливает для предыдущего режима значение режима ядра (kernel). Теперь функция NtCreateFile видит, что вызов поступил из ядра, и больше не отвечает отказом. Далее показано, как выглядят батуты режима ядра на 32-разрядной и на 64-разрядной системах. Номер системного вызова выделен жирным шрифтом.
lkd> u nt!ZwReadFile
nt!ZwReadFile:
8207f118 b802010000 mov eax,102h
8207f11d 8d542404 lea edx,[esp+4]
8207f121 9c pushfd
8207f122 6a08 push 8
8207f124 e8c5d70000 call nt!KiSystemService (8208c8ee)
8207f129 c22400 ret 24h
lkd> uf nt!ZwReadFile
nt!ZwReadFile:
fffff800'01a7a520 488bc4 mov rax,rsp
fffff800'01a7a523 fa cli
fffff800'01a7a524 4883ec10 sub rsp,10h
fffff800'01a7a528 50 push rax
fffff800'01a7a529 9c pushfq
fffff800'01a7a52a 6a10 push 10h
fffff800'01a7a52c 488d05bd310000 lea rax,[nt!KiServiceLinkage
(fffff800'01a7d6f0)]
fffff800'01a7a533 50 push rax
fffff800'01a7a534 b803000000 mov eax,3
fffff800'01a7a539 e902690000 jmp nt!KiServiceInternal (fffff800'01a80e40)
В Windows есть две таблицы системных служб, и драйверы сторонних разработчиков не могут расширить таблицы или вставить новые для добавления своих собственных вызовов служб. На 32-разрядных версиях Windows и на версиях IA64 диспетчер системных служб определяет местоположение таблиц через указатель в структуре потоков ядра, а на x64-версиях он находит их по их глобальным адресам
Диспетчер системных служб определяет, в какой таблице содержится запрошенная служба, интерпретируя двухразрядное поле в 32-разрядном номере системной службы в качестве индекса таблицы. Младшие 12 разрядов номера системной службы служат в качестве индекса в таблице, указанной индексом таблицы.