ИНТЕРФЕЙС СИСТЕМНЫХ ВЫЗОВОВ =========================== Я смотрел на снег весь день... Падающий... Всегда вниз. Падающий весь день. И тогда я закричал "Это жизнь?" (c) by My Dying Bride Системные вызовы - интерфейс посредством которого поток пользовательского режима может перейти в режим ядра. Естественно, рассмотрение реализации механизма системных вызовов представляет огромный интерес, если говорить об устойчивости и надежности системы. Ошибки в реализации системных сервисов это дыры в безопасности системы, так как любой поток пользовательского режима сможет воспользоваться такой ошибкой для доступа к ресурсам ядра. Итак, поток, находясь в пользовательском режиме, может вызвать системный сервис и перейти в режим ядра. Системные сервисы вызываются через прерывание 2Eh. Модуль пользовательского режима NTDLL.DLL пере направляет вызовы многих функций в ядро, если невозможно без этого обойтись. Например, код экспортируемой функции NtQuerySection выглядит следующим образом: 7F67CDC public _NtQuerySection@20 7F67CDC _NtQuerySection@20 proc near 7F67CDC 7F67CDC arg_0 = byte ptr 4 7F67CDC 7F67CDC mov eax, 77h ; NtQuerySection 7F67CE1 lea edx, [esp+arg_0] 7F67CE5 int 2Eh 7F67CE7 retn 14h 7F67CE7 _NtQuerySection@20 endp На самом деле, все вызовы других сервисов ядра из NTDLL.DLL выглядят точно также. Как видно из исходного текста, при вызове прерывания 2Eh в регистр EAX заносится номер сервисной функции, а в регистр EDX адрес параметров в стеке. Теперь посмотрим на участки кода _KiSystemService в NTOSKRNL.EXE. (обработчик 2E). Интерес представляют следующие фрагменты: [skipped] 8013CB20 _KiEndUnexpectedRange proc near 8013CB20 cmp ecx, 10h ; if call to win32k.sys 8013CB23 jnz short Kss_LimitError 8013CB25 push edx 8013CB26 push ebx 8013CB27 call _PsConvertToGuiThread@0 8013CB2C or eax, eax 8013CB2E pop eax 8013CB2F pop edx [skipped] 8013CBD0 ; S u b r o u t i n e 8013CBD0 8013CBD0 public _KiSystemService 8013CBD0 _KiSystemService proc near ; DATA XREF: INIT:801C7A50 o [skipped] 8013CBD8 mov ebx, 30h 8013CBDD db 66h 8013CBDD mov fs, bx ; set fs to 30 (processor contol region) 8013CBE0 push dword ptr ds:0FFDFF000h 8013CBE6 mov dword ptr ds:0FFDFF000h, 0FFFFFFFFh 8013CBF0 mov esi, ds:0FFDFF124h ; Current Kernel Thread Pointer 8013CBF6 push dword ptr [esi+137h] ; previous mode: Kernel/user [skipped] 8013CC29 _KiSystemServiceRepeat: 8013CC29 mov edi, eax ; function number 8013CC2B shr edi, 8 8013CC2E and edi, 30h 8013CC31 mov ecx, edi 8013CC33 add edi, [esi+0DCh] ; got service tables address 8013CC39 mov ebx, eax 8013CC3B and eax, 0FFFh 8013CC40 cmp eax, [edi+8] ; num of services 8013CC43 jnb _KiEndYnexpectedRange [skipped] 8013CC6E mov esi, edx ; parameters addres 8013CC70 mov ebx, [edi+0Ch] ; table with sizes 8013CC73 xor ecx, ecx 8013CC75 mov cl, [eax+ebx] ; size of parameters 8013CC78 mov edi, [edi] ; handler's table 8013CC7A mov ebx, [edi+eax*4] ; got function address 8013CC7D sub esp, ecx ; clear stack 8013CC7F shr ecx, 2 8013CC82 mov edi, esp 8013CC84 cmp esi, ds:_MmUserProbeAddress ; 7fff0000 8013CC8A jnb kss80 8013CC90 KiSystemServiceCopyArguments: 8013CC90 repe movsd ; copy to ring0 stack 8013CC92 kssdoit: 8013CC92 call ebx 8013CC94 kss60: 8013CC94 mov esp, ebp 8013CC96 kss70: 8013CC96 mov ecx, ds:0FFDFF124h 8013CC9C mov edx, [ebp+3Ch] 8013CC9F mov [ecx+128h], edx 8013CC9F _KiSystemService endp 8013CC9F 8013CCA5 _KiServiceExit proc near [skipped] 8013CE34 kss80: 8013CE34 test byte ptr [ebp+6Ch], 1 ; kernel/user 8013CE38 jz KiSystemServiceCopyArguments 8013CE3E mov eax, 0C0000005h ; STATUS_ACCESS_VIOLATION 8013CE43 jmp kss60 Таким образом, если вызов произошел из кольца 0, то обработчик проверяет, находятся ли параметры в пользовательском диапазоне адресов (см. 8013CC84). Затем, в любом случае обработчик проверяет доступность переданных ему параметров (происходит копирование параметров в стек кольца 0 около метки KiSystemServiceCopyArguments с отслеживанием страничной ошибки. Ошибка отслеживается в обработчике страничной ошибки, который здесь для простоты не приводится.) Если ошибки не произошло, осуществляется вызов сервиса командой CALL EBX по предварительно выбранному адресу из таблицы адресов сервисов. Следует отметить два интересных факта. Первый факт - это то, что все потоки в режиме ядра могут получить адреса таблиц указателей на сервисы (это следует из кода по адресам 8013CBF0 и 8013CC33). Второй интересный факт - что таблиц сервисов, может быть четыре (для каждого потока). Около метки _KiSystemServiceRepeat происходит анализ кода вызова и, в зависимости от значений бит 0x3000, выбирается один из четырех дескрипторов, описывающих таблицы указателей сервисов. (Эти биты отбрасывается при индексировании таблицы адресов сервисов). Дескрипторы занимают по 16 байт и расположены один за другим. Назовем совокупность четырех дескрипторов - таблицей дескрипторов сервисов. Для каждого потока, в структуре потока ядра, имеется свой указатель на таблицу дескрипторов сервисов. Его можно получить по смещению 0DCh в структуре потока (ОС Windows NT 4.0). Адрес структуры потока можно получить по смещению 124h от начала PCRB (MOV EAX, FS:[124h]) находясь в режиме ядра. Не смотря на то, что каждый поток имеет свой указатель на таблицу дескрипторов сервисов, фактически, указатели во всех потоках указывают на одну из двух таблиц дескрипторов. Обе таблицы находятся в NTOSKERNEL.EXE и называются KeServiceDescriptorTable и KeServiceDescriptorTableShadow. Формат дескрипторов в таблицах можно представить следующим образом: typedef struct _ServiceDescriptor{ DWORD* ServiceTable; /*указатель на таблицу адресов сервисов*/ DWORD Reserved; /* используется в checked build .*/ DWORD ServiceLimit; /* Число сервисов в таблице */ BYTE* ArgumentTable; /* указатель на размер массива параметров */ /* в стеке для сервисов */ /* фактически равен (4*количество параметров) */ }ServiceDescriptor; При инициализации системы (KiInitSystem) дескрипторы 0 таблицы KeServiceDescriptorTable и KeServiceDescriptorShadow инициализируется следующим образом (псевдокод): KeServiceDescriptorTable [0].ServiceTable = KiServiceTable; KeServiceDescriptorTable [0].ServiceLimit = KiServiceLimit; KeServiceDescriptorTable [0].ArgumentTable = KiArgumentTable; memcpy (&KeServiceDescriptorTableShadow[0], &KeServiceDescriptorTable[0],0x10); Остальные дескрипторы изначально нулевые. KiServiceTable - таблица смещений на функции NTOSKRNL.EXE. KiArgumentTable - количество параметров в сервисе умноженное на 4 (размер фрейма стека с параметрами). KiServiceLimit - число смещений в таблице KiServiceTable. (Число сервисов). Дескриптор 0, таблицы KeServiceDescriptorTableShadow, заполняется копией созданного дескриптора. Итак, дескриптор 0 заполняется при инициализации ядра и описывает базовые сервисы ядра. Этот дескриптор одинаков для всех потоков. А что с остальными дескрипторами? При инициализации драйвера WIN32K.SYS происходит вызов функции ядра KeAddSystemServiceTable. Ее псевдокод BOOL KeAddSystemServiceTable ( PVOID* ServiceTable, ULONG Reserved, ULONG Limit, BYTE* Arguments, ULONG NumOfDesc) { if (NumOfDesc>3) return 0; Descriptor= &KeServiceDescriptorTable [NumOfDesc*16]; if (Descriptor->ServiceTable)return 0; ShadowDescriptor= &KeServiceDescriptorTableShadow[NumOfDesc*16]; if (ShadowDescriptor->ServiceTable) return 0; ShadowDescriptor->ServiceTable=ServiceTable; ShadowDescriptor->Reserved=Reserved; ShadowDescriptor->ServiceLimit=Limit; ShadowDescriptor->ArgumentTable=Arguments; if (NumOfDesc!=1){ Descriptor->ServiceTable=ServiceTable; Descriptor->Reserved=Reserved; Descriptor->ServiceLimit=Limit; Descriptor->ArgumentTable=Arguments; } return 1; } Функция очень несложная, но информативная. Эта функция заполняет один из четырех дескрипторов, в общем случае и в теневой и в основной таблицах (если дескрипторы нулевые - т.е. не используемые). Но есть одна интересная особенность - в случае, если добавляется дескриптор 1, он добавляется ТОЛЬКО в теневую таблицу. При инициализации WIN32K.SYS, добавляется как раз дескриптор 1. Остальные дескрипторы на данный момент не используются. Известно, что в целях повышения производительности, в Windows NT v4.0, функции USER и GDI подсистемы Win32 реализованы в ядре. Win32k это драйвер режима ядра, реализующий функции Win32 и дескриптор 1 описывает эти сервисы. Теперь посмотрим, какие из таблиц (теневая и основная) предоставляются потокам. Всего две строчки кода из функции KeInitializeThread: 80119344 mov esi, [ebp+lpThread] [skipped] 80119394 mov dword ptr [esi+0DCh], offset _KeServiceDescriptorTable А теперь еще пару строк, но уже из функции PsConvertToGuiThread: 80192919 mov ecx, [ebp+lpServiceDescriptorTable] ; thread struct + 0dch [skipped] 80192926 mov dword ptr [ecx], offset _KeServiceDescriptorTableShadow Функция PsConvertToGuiThread вызывается в обработчике 2e, если произошел вызов сервиса WIN32K.SYS, но дескриптор 1 для текущего потока не инициализирован. Вывод такой. Есть две таблицы дескрипторов сервисов - основная и теневая. В основной таблице есть только один не нулевой дескриптор по смещению 0, который описывает базовые сервисы. В теневой таблице, помимо такого же дескриптора 0, имеется инициализированный WIN32K.SYS дескриптор 1, описывающий сервисы GDI и USER. Для GUI потоков в структуре потока по смещению 0DCh находится адрес теневой таблицы, для остальных - основной таблицы. Если поток требует WIN32K.SYS сервиса, он становится GUI потоком. После рассмотрения структуры таблицы сервисов и назначения дескриптора 1 напрашивается вывод об очень тесной интеграции Win32 подсистемы с ядром. Особенность дескриптора 1 "зашита" в коде ядра. Функция KeAddSystemServiceTable недокументированна. Тем не менее, о ней знает множество народу. Функция очень простая и может спокойно использоваться в своих драйверах для добавления новых сервисов. Заметим, что 2 дескриптор используется IIS и может быть иногда занят. Поэтому лучше добавлять свои сервисы в 3-й дескриптор. Особенностью вызовов ОС Windows NT является обилие указателей пользовательского режима. Почти каждая функция ядра, начинает с трудной работы по проверки правильности диапазона указателей. А все потому, что адресное пространство пользовательского режима совпадает с пространством ядра, и если во время работы в пользовательском режиме ядро изолированно с помощью страничной защиты, то в режиме ядра, некорректный пользовательский указатель может адресовать область ядра. Если посмотреть на границы селекторов 10 и 23, то можно заметить, что они совпадают (0xFFFFFFFF), Казалось бы, почему не поставить границу селектора 23 (пользовательский селектор) равную начальному адресу пространства ядра минус 1. (Как правило, это 0x7FFFFFFF). Именно так сделано, например, в LINUX. Если попытаться насильно понизить границу в отладчике, ОС Windows NT упадет с BSOD (Blue Screen Of Death - неформальное название синего экрана дампа, при падении системы). Почему так происходит? Ответ парадоксален: поток использует селектор данных 23 даже при выполнении его в режиме ядра. С одной стороны, это удобно - драйвер работает с указателями пользователя как с обычными указателями. С другой стороны, это потенциальное место для ошибок. Как я уже говорил, в LINUX пространства пользователя и ядра не совпадают и при работе с пользовательскими указателями в режиме ядра приходится использовать функции подобные copy_from_user(). (Для i386 эти функции просто содержат рутины копирования из разных сегментов) Такое "неудобство" заставляет программиста ядра контролировать и минимизировать работу с пользовательскими указателями. Совпадающие пространства пользователя и ядра ОС Windows NT привела к многочисленным ошибкам в коде сервисов в начальных версиях ОС. Эти ошибки не так легко обнаружить - ведь с сервисами обычно работает подсистема Win32, которая обычно передает ядру корректные параметры. Изменения в интерфейсе системных вызовов в ядре Windows 2K =========================================================== // Это написано в основном на основе чъей-то статьи, на которую я как-то // натолкнулся. :( Я не собираюсь присваивать чужие копирайты - но // я действительно не помню чья это была статья, и где я ее видел... Ядро Windows 2K кроме интерфейса системных вызовов через прерывание 2Eh поддерживает переход в режим ядра через инструкции SYSENTER/SYSEXIT. Эти инструкции имеются в процессорах Pentium II+. Обработчик SYSENTER располагается в ядре около KiSystemService и называется KiFastCallEntry. Начало KiFastCallEntry выглядит так: MOV ESP, SS:[0xFFDFF040] MOV ESP, [ESP+4] ;set ring-0 stack PUSH 0x23 ;имитирует стек 3-го кольца PUSH EDX ;указатель на параметры в стеке 3-го кольца SUB DWORD PTR [ESP], 4 ;в стеке ESP 3-го кольца PUSHFD OR DWORD PTR [ESP], 0x200 ;имитируеть флаговый регистр 3-го кольца PUSH 0x1B ;селектор CS 3-го кольца PUSH ECX ;EIP 3-го кольца ;..fill in KeTrapFrame ;..далее - на общую часть обработчика системного вызова Становится очевидно, для общей части обработчика совершенно прозрачно каким образом выполнен системный вызов - вышеприведенный код имитирует состояние стека при вызове прерывания. Кроме того - теперь можно привести примерный вид кода, который делает системный вызов с помощью механизма Fast System Call. MOV EAX, NtCallCode ; код системного вызова LEA EDX, [ESP+4] ; параметры в стеке LEA ECX, SYSEXIT_POINT ; точка возврата SYSENTER SYSEXIT_POINT: Все это очень напоминает вызов из NTDLL.DLL с помощью прерывания 2E . Другими словами интерфейс сделан настолько похожим, насколько это возможно. Завершается обработка системного вызова так: TEST KeFeatureBits, 0x1000 ;поддержка fast system call JZ ReturnFromInterrupt ;нет - iret TEST DWORD PTR [ESP+4], 1 ;вызов из 3-го кольца? JZ ReturnFromInterrupt ; нет - на iret TEST DWORD PTR [ESP+8], 0x20000 ;из v86? JNZ ReturnFromInterrupt ; да - iret POP EDX ;eip возврата ADD ESP, 8 ;убрать имитацию стека прерывания POP ECX ;esp 3-го кольца STI SYSEXIT ReturnFromInterrupt: IRETD Итак - ядро поддерживает два интерфейса системных вызовов. Однако NTDLL.DLL содержит примерно такие же заглушки, как и для ОС Windows NT 4.0. Таким образом интерфейс fast system call не используется в ОС Windows NT . Видимо, NTDLL.DLL следующих версий ОС будут содержать в себе поддержку двух интерфейсов. Либо будут две разные NTDLL.DLL для процессоров выше PII и старых процессоров. --------------------------------------------------------------------------- (c)Gloomy aka Peter Kosyh, Melancholy Coding'2001 http://gloomy.cjb.net mailto:gl00my@mail.ru