系统调用接口
===========================
Я смотрел на снег весь день... Падающий...
Всегда вниз. Падающий весь день.
И тогда я закричал "Это жизнь?"
(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寄存器为堆栈中参数的地址。现在我们来看NTOSKRNL.EXE中_KiSystemService(中断2Eh的处理程序)的部分代码。有意思的是下面这一段:
[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
这样,如果调用产生于ring0,则处理程序检查参数是否位于用户地址区域中(见8013CC84)。之后,处理程序检查传递给它的参数(向ring0堆栈中拷贝参数起始于标号KiSystemServiceCopyArguments)。如果没有错误,则按照预先从服务地址表中选出的地址进行CALL EBX。接着,注意到两个有趣的地方。第一个是,所有的核心线程都能够取得服务地址表的地址(参照8013CBF0和8013CC33处的代码)。第二个有趣的地方是,服务表可以有四个(对于每个线程来说)。标号_KiSystemServiceRepeat处代码的调用依赖于位0x3000的值,从四个描述服务表地址的描述符中选择一个。描述符占据16字节并连续排列。这四个描述符总称服务描述符表。对于每一个线程,在内核线程结构体中都有其自己的指向服务描述符表的指针。这个指针可以从线程结构体的0DCh偏移处取得(Windows NT 4.0下)。线程结构体的地址可以在内核模式下从PCRB的偏移124h处取得。(MOV EAX, FS:[124h])。每个线程都有自己指向服务描述符表的指针,实际上,所有线程中的指针都指向两个描述符表中的一个。这两个表位于NTOSKERNEL.EXE,分别叫做KeServiceDescriptorTable和KeServiceDescriptorTableShadow。表中的描述符的格式如下:
typedef struct _ServiceDescriptor{
DWORD* ServiceTable; /* 指向服务地址表的指针 */
DWORD Reserved; /* 在checked build下使用 */
DWORD ServiceLimit; /* 表中服务的数目 */
BYTE* ArgumentTable; /* 指向服务堆栈中参数表大小的指针 */
/* 实际上等于 (4*参数数目) */
}ServiceDescriptor;
在系统初始化(KiInitSystem)时,表KeServiceDescriptorTable和KeServiceDescriptorShadow的描述符0被初始化为以下这个样子(伪代码):
KeServiceDescriptorTable [0].ServiceTable = KiServiceTable;
KeServiceDescriptorTable [0].ServiceLimit = KiServiceLimit;
KeServiceDescriptorTable [0].ArgumentTable = KiArgumentTable;
memcpy (&KeServiceDescriptorTableShadow[0], &KeServiceDescriptorTable[0],0x10);
其余的描述符都为0。KiServiceTable是NTOSKRNL.EXE中函数的偏移表。KiArgumentTable为服务参数数目乘以4(参数堆栈frame的大小)。KiServiceLimit为KiServiceTable表中服务的数目。KeServiceDescriptorTableShadow表的描述符0,为创建的描述符的副本。因此,描述符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;
}
函数很简单,但可从中获取不少信息。这个函数填充四个描述符中的一个,一般来说,可能填充shadow table,也有可能填充主表(若描述符为0,则未使用)。但有一个特殊之处很有意思——如果添加描述符1,则该描述符只会被添加到shadow table中。初始化WIN32K.SYS时,恰好添加的就是描述符1。此时,其余的描述符并未使用。我们知道,为了提高效率,在Windows NT v4.0下,Win32子系统的USER和GDI函数都是在内核中实现的。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
如果调用的是WIN32K.SYS的服务,但用于当前线程的描述符1并未初始化,则在2e中断处理程序中会调用函数PsConvertToGuiThread。服务描述符表有两个——主表和shadow表。在主表中只有一个偏移为0的非零描述符,其描述了基本的系统服务。在shadow表中除描述符0之外,还有WIN32K.SYS初始化的描述符1,其描述了GDI和USER服务。对于GUI线程,在线程结构体的偏移0DCh处是shadow表的地址,对于其它线程,该处为主表的地址。如果线程请求WIN32K.SYS的服务,则它要成为GUI线程。在研究了服务表的结构以及描述符1的用途之后,可以看到Win32子系统与内核非常紧密的结合在一起。描述符1的特殊性在于其嵌入到了内核代码中。KeAddSystemServiceTable函数是未公开的函数,非常简单并可以在添加新服务的驱动程序中静态的调用。我们注意到,IIS使用了两个描述符。所以最好在第三个描述符中添加自己的服务。
Windows NT调用的特殊之处在于用户模式下的大量指针。几乎每一个内核函数都是以检查指针区域参数正确性这一繁琐工作开始的。因此所有的用户地址空间都与内核空间重合,并且,在用户模式下工作的同时,内核由于页保护被隔绝开,而在内核模式下,不正确的用户指针可以寻址到内核区域。如果看一下选择子10和23的界限,则可以看到它们是一样的(0xFFFFFFFF)。选择子23(用户模式的选择子)的界限应该等于内核空间起始地址减1(0x7FFFFFFF)。例如,在LINUX下就是这样的。如果试图强行在调试器中修改这个界限值,则Windows NT会出BSOD。为什么会这样?答案令人不可思议:在内核中执行线程时竟使用选择子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 ;模拟ring3堆栈
PUSH EDX ;指向ring3堆栈参数的指针
SUB DWORD PTR [ESP], 4 ;在ring3的堆栈中
PUSHFD
OR DWORD PTR [ESP], 0x200 ;模拟ring3的标志寄存器
PUSH 0x1B ;ring3的CS选择子
PUSH ECX ;ring3的EIP
;..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 ;从ring3调用?
JZ ReturnFromInterrupt ; 非 - iret
TEST DWORD PTR [ESP+8], 0x20000 ;从v86调用?
JNZ ReturnFromInterrupt ; 是 - iret
POP EDX ;返回的eip
ADD ESP, 8 ;回收模拟的中断堆栈
POP ECX ;ring3的esp 3
STI
SYSEXIT
ReturnFromInterrupt:
IRETD
如此——内核支持两种系统调用接口。但是NTDLL.DLL与Windows NT 4.0下的相同,包含着对系统调用的封装。这样,Windows NT就不能使用fast system call形式的接口。看来,下一版的NTDLL.DLL将包含两种接口。或者对PII之前和之后的处理器提供两种不同的NTDLL.DLL。
---------------------------------------------------------------------------
(c)Gloomy aka Peter Kosyh, Melancholy Coding'2001
http://gloomy.cjb.net
mailto:gl00my@mail.ru
===========================
Я смотрел на снег весь день... Падающий...
Всегда вниз. Падающий весь день.
И тогда я закричал "Это жизнь?"
(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寄存器为堆栈中参数的地址。现在我们来看NTOSKRNL.EXE中_KiSystemService(中断2Eh的处理程序)的部分代码。有意思的是下面这一段:
[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
这样,如果调用产生于ring0,则处理程序检查参数是否位于用户地址区域中(见8013CC84)。之后,处理程序检查传递给它的参数(向ring0堆栈中拷贝参数起始于标号KiSystemServiceCopyArguments)。如果没有错误,则按照预先从服务地址表中选出的地址进行CALL EBX。接着,注意到两个有趣的地方。第一个是,所有的核心线程都能够取得服务地址表的地址(参照8013CBF0和8013CC33处的代码)。第二个有趣的地方是,服务表可以有四个(对于每个线程来说)。标号_KiSystemServiceRepeat处代码的调用依赖于位0x3000的值,从四个描述服务表地址的描述符中选择一个。描述符占据16字节并连续排列。这四个描述符总称服务描述符表。对于每一个线程,在内核线程结构体中都有其自己的指向服务描述符表的指针。这个指针可以从线程结构体的0DCh偏移处取得(Windows NT 4.0下)。线程结构体的地址可以在内核模式下从PCRB的偏移124h处取得。(MOV EAX, FS:[124h])。每个线程都有自己指向服务描述符表的指针,实际上,所有线程中的指针都指向两个描述符表中的一个。这两个表位于NTOSKERNEL.EXE,分别叫做KeServiceDescriptorTable和KeServiceDescriptorTableShadow。表中的描述符的格式如下:
typedef struct _ServiceDescriptor{
DWORD* ServiceTable; /* 指向服务地址表的指针 */
DWORD Reserved; /* 在checked build下使用 */
DWORD ServiceLimit; /* 表中服务的数目 */
BYTE* ArgumentTable; /* 指向服务堆栈中参数表大小的指针 */
/* 实际上等于 (4*参数数目) */
}ServiceDescriptor;
在系统初始化(KiInitSystem)时,表KeServiceDescriptorTable和KeServiceDescriptorShadow的描述符0被初始化为以下这个样子(伪代码):
KeServiceDescriptorTable [0].ServiceTable = KiServiceTable;
KeServiceDescriptorTable [0].ServiceLimit = KiServiceLimit;
KeServiceDescriptorTable [0].ArgumentTable = KiArgumentTable;
memcpy (&KeServiceDescriptorTableShadow[0], &KeServiceDescriptorTable[0],0x10);
其余的描述符都为0。KiServiceTable是NTOSKRNL.EXE中函数的偏移表。KiArgumentTable为服务参数数目乘以4(参数堆栈frame的大小)。KiServiceLimit为KiServiceTable表中服务的数目。KeServiceDescriptorTableShadow表的描述符0,为创建的描述符的副本。因此,描述符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;
}
函数很简单,但可从中获取不少信息。这个函数填充四个描述符中的一个,一般来说,可能填充shadow table,也有可能填充主表(若描述符为0,则未使用)。但有一个特殊之处很有意思——如果添加描述符1,则该描述符只会被添加到shadow table中。初始化WIN32K.SYS时,恰好添加的就是描述符1。此时,其余的描述符并未使用。我们知道,为了提高效率,在Windows NT v4.0下,Win32子系统的USER和GDI函数都是在内核中实现的。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
如果调用的是WIN32K.SYS的服务,但用于当前线程的描述符1并未初始化,则在2e中断处理程序中会调用函数PsConvertToGuiThread。服务描述符表有两个——主表和shadow表。在主表中只有一个偏移为0的非零描述符,其描述了基本的系统服务。在shadow表中除描述符0之外,还有WIN32K.SYS初始化的描述符1,其描述了GDI和USER服务。对于GUI线程,在线程结构体的偏移0DCh处是shadow表的地址,对于其它线程,该处为主表的地址。如果线程请求WIN32K.SYS的服务,则它要成为GUI线程。在研究了服务表的结构以及描述符1的用途之后,可以看到Win32子系统与内核非常紧密的结合在一起。描述符1的特殊性在于其嵌入到了内核代码中。KeAddSystemServiceTable函数是未公开的函数,非常简单并可以在添加新服务的驱动程序中静态的调用。我们注意到,IIS使用了两个描述符。所以最好在第三个描述符中添加自己的服务。
Windows NT调用的特殊之处在于用户模式下的大量指针。几乎每一个内核函数都是以检查指针区域参数正确性这一繁琐工作开始的。因此所有的用户地址空间都与内核空间重合,并且,在用户模式下工作的同时,内核由于页保护被隔绝开,而在内核模式下,不正确的用户指针可以寻址到内核区域。如果看一下选择子10和23的界限,则可以看到它们是一样的(0xFFFFFFFF)。选择子23(用户模式的选择子)的界限应该等于内核空间起始地址减1(0x7FFFFFFF)。例如,在LINUX下就是这样的。如果试图强行在调试器中修改这个界限值,则Windows NT会出BSOD。为什么会这样?答案令人不可思议:在内核中执行线程时竟使用选择子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 ;模拟ring3堆栈
PUSH EDX ;指向ring3堆栈参数的指针
SUB DWORD PTR [ESP], 4 ;在ring3的堆栈中
PUSHFD
OR DWORD PTR [ESP], 0x200 ;模拟ring3的标志寄存器
PUSH 0x1B ;ring3的CS选择子
PUSH ECX ;ring3的EIP
;..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 ;从ring3调用?
JZ ReturnFromInterrupt ; 非 - iret
TEST DWORD PTR [ESP+8], 0x20000 ;从v86调用?
JNZ ReturnFromInterrupt ; 是 - iret
POP EDX ;返回的eip
ADD ESP, 8 ;回收模拟的中断堆栈
POP ECX ;ring3的esp 3
STI
SYSEXIT
ReturnFromInterrupt:
IRETD
如此——内核支持两种系统调用接口。但是NTDLL.DLL与Windows NT 4.0下的相同,包含着对系统调用的封装。这样,Windows NT就不能使用fast system call形式的接口。看来,下一版的NTDLL.DLL将包含两种接口。或者对PII之前和之后的处理器提供两种不同的NTDLL.DLL。
---------------------------------------------------------------------------
(c)Gloomy aka Peter Kosyh, Melancholy Coding'2001
http://gloomy.cjb.net
mailto:gl00my@mail.ru