📄 漫谈兼容内核之一:reactos怎样实现系统调用.txt
字号:
set_system_call_gate(0x2e,(int)KiSystemService);
}
[/code]
显然,int 0x2e的向量指向KiSystemService()。
ReactOS在其内核函数的命名和定义上也力求与Windows一致,所以ReactOS内核中也有前缀为ke和ki的函数。前缀ke表示属于“内核”模块。注意Windows所谓的“内核(kernel)”模块只是内核的一部分,而不是整个内核,这一点我以后在“漫谈Wine”中还要讲到。而前缀ki,则是指内核中与中断响应和处理有关的函数。KiSystemService()是一段汇编程序,其作用相当于Linux内核中的system_call(),这段代码在reactos/ntoskrnl/ke/i386/syscall.S中。限于篇幅,我在这篇短文中就不详细讲解这个函数的全部代码了,而只是分段对一些要紧的关节作些说明。一般而言,能读懂Linux内核中system_call()那段代码的读者应该能至少大体上读懂这个函数。
[code]
_KiSystemService:
/*
* Construct a trap frame on the stack.
* The following are already on the stack.
*/
// SS + 0x0
// ESP + 0x4
// EFLAGS + 0x8
// CS + 0xC
// EIP + 0x10
pushl $0 // + 0x14
pushl %ebp // + 0x18
pushl %ebx // + 0x1C
pushl %esi // + 0x20
pushl %edi // + 0x24
pushl %fs // + 0x28
/* Load PCR Selector into fs */
movw $PCR_SELECTOR, %bx
movw %bx, %fs
/* Save the previous exception list */
pushl %fs:KPCR_EXCEPTION_LIST // + 0x2C
/* Set the exception handler chain terminator */
movl $0xffffffff, %fs:KPCR_EXCEPTION_LIST
/* Get a pointer to the current thread */
movl %fs:KPCR_CURRENT_THREAD, %esi
[/code]
前面的一些指令主要是在保存现场,类似于Linux内核中的宏操作SAVE_ALL。这里关键的一步是从%fs:KPCR_CURRENT_THREAD这个地址取得当前线程的指针并将其存放在寄存器%esi中。每个线程在内核中都有个KTHREAD数据结构,某种意义上相当于Linux内核中的“进程控制块”、即task_struct。Windows内核中也有“进程控制块”,但只是相当于把进程内各线程所共享的信息剥离了出来,而“线程控制块”则起着更重要的作用。所谓当前线程的指针,就是指向当前线程的KTHREAD数据结构的指针。当内核调度一个线程运行时,就将其KTHREAD数据结构的地址存放在%fs:KPCR_CURRENT_THREAD这个地址中,而(CPU在系统空间的)%fs的值则又固定存放在PCR_SELECTOR这个地址中(定义为0x30)。附带提一下,Win2k内核把%fs:0映射到线性地址0xffdff000(见“Secrets”一书p428)。
总之,从现在起,寄存器%esi就指向了当前线程的KTHREAD数据结构。那么这一步对于系统调用为什么重要呢?我们看一下这个数据结构中的几个成分就可以明白:
[code]
typedef struct _KTHREAD
{
/* For waiting on thread exit */
DISPATCHER_HEADER DispatcherHeader; /* 00 */
……
SSDT_ENTRY *ServiceTable; /* DC */
……
UCHAR PreviousMode; /* 137 */
……
} KTHREAD;
[/code]
每个成分后面的注释说明这个成分在数据结构中以字节为单位的相对位移,例如指针ServiceTable的相对位移就是0xdc。事实上,这个指针正是我们此刻最为关注的,因为它直接与系统调用的函数跳转表有关。每个线程的这个指针都指向一个SSDT_ENTRY结构数组。既然每个线程都有这么个指针,就说明每个线程都可以有自己的ServiceTable。不过,实际上每个线程的ServiceTable通常都指向同一个结构数组,我们等一下再来看这个结构数组,现在先往下看代码。
[code]
/* Save the old previous mode */
pushl %ss:KTHREAD_PREVIOUS_MODE(%esi) // + 0x30
/* Set the new previous mode based on the saved CS selector */
movl 0x24(%esp), %ebx
andl $1, %ebx
movb %bl, %ss:KTHREAD_PREVIOUS_MODE(%esi)
/* Save other registers */
pushl %eax // + 0x34
pushl %ecx // + 0x38
pushl %edx // + 0x3C
pushl %ds // + 0x40
pushl %es // + 0x44
pushl %gs // + 0x48
sub $0x28, %esp // + 0x70
#ifdef DBG
……
#else
pushl 0x60(%esp) /* DebugEIP */ // + 0x74
#endif
pushl %ebp /* DebugEBP */ // + 0x78
/* Load the segment registers */
sti
movw $KERNEL_DS, %bx
movw %bx, %ds
movw %bx, %es
/* Save the old trap frame pointer where EDX would be saved */
movl KTHREAD_TRAP_FRAME(%esi), %ebx
movl %ebx, KTRAP_FRAME_EDX(%esp)
/* Allocate new Kernel stack frame */
movl %esp,%ebp
/* Save a pointer to the trap frame in the TCB */
movl %ebp, KTHREAD_TRAP_FRAME(%esi)
CheckValidCall:
#ifdef DBG
……
#endif
/*
* Find out which table offset to use. Converts 0x1124 into 0x10.
* The offset is related to the Table Index as such: Offset = TableIndex x 10
*/
movl %eax, %edi
shrl $8, %edi
andl $0x10, %edi
movl %edi, %ecx
/* Now add the thread's base system table to the offset */
addl KTHREAD_SERVICE_TABLE(%esi), %edi
[/code]
这里我们关注的是最后这一小段。首先,KTHREAD_SERVICE_TABLE(%esi)就是当前线程的ServiceTable指针。常数KTHREAD_SERVICE_TABLE定义为0xdc:
[code]
#define KTHREAD_SERVICE_TABLE 0xDC
[/code]
这跟前面KTHREAD数据结构的定义显然是一致的。
上面讲过,实际上一般情况下所有线程的ServiceTable指针都指向同一个结构数组,那就是KeServiceDescriptorTable[ ]:
[code]
SSDT_ENTRY
__declspec(dllexport)
KeServiceDescriptorTable[SSDT_MAX_ENTRIES] = {
{ MainSSDT, NULL, NUMBER_OF_SYSCALLS, MainSSPT },
{ NULL, NULL, 0, NULL },
{ NULL, NULL, 0, NULL },
{ NULL, NULL, 0, NULL }
};
[/code]
这个数组的大小一般是4,但是只用了前两个元素。这里只用了第一个元素,这就是常规Windows系统调用的跳转表。
我以前曾经谈到,Windows在发展的过程中把许多原来实现于用户空间的功能(主要是图形界面操作)移到了内核中,成为一个内核模块win32k.sys,并相应地增加了一组“扩充系统调用”。这个数组的第二个元素就是为扩充系统调用准备的,但是在源代码中这个元素是空的,这是因为win32k.sys可以动态安装,安装了以后才把具体的数据结构指针填写进去。扩充系统调用与常规系统调用的区别是:前者的系统调用号均大于等于0x1000,而后者则小于0x1000。显然,内核需要根据具体的系统调用号来确定应该使用哪一个跳转表,或者说上述数组内的哪一个元素。每个元素的大小是16个字节,所以只要根据具体的系统调用号算出一个相对位移量,就起到了选择使用跳转表的作用。具体地,如果算得的位移量是0,那就是使用常规跳转表,而若是0x10就是使用扩充跳转表。
上面的代码中正是这样做的。把系统调用号的副本(在%edi中)右移8位,再跟0x10相与,就起到了这个效果。于是,指令“addl KTHREAD_SERVICE_TABLE(%esi), %edi”就使寄存器%edi指向了应该使用的跳转表结构,即SSDT_ENTRY数据结构。代码的作者加了个注释,说是“把0x1124转换成0x10”,其意思实际上是:“如果系统调用号是0x1124,那么计算出来的相对位移是0x10”;后面一句说的是“相对位移 = 数组下标乘上0x10”。
SSDT_ENTRY数据结构中的第三个成分,即相对位移为8之处是个整数,说明在函数跳转表中有几个指针,也即所允许的最大系统调用号。对于常规系统调用,这个整数是NUMBER_OF_SYSCALLS,在ReactOS的代码中定义为232,比Win2K略少一些。
我们继续往下看代码:
[code]
/* Get the true syscall ID and check it */
movl %eax, %ebx
andl $0x0FFF, %eax
cmpl 8(%edi), %eax
/* Invalid ID, try to load Win32K Table */
jnb KiBBTUnexpectedRange
/* Users's current stack frame pointer is source */
movl %edx, %esi
/* Allocate room for argument list from kernel stack */
movl 12(%edi), %ecx
movb (%ecx, %eax), %cl
movzx %cl, %ecx
/* Allocate space on our stack */
subl %ecx, %esp
[/code]
正如代码中的注释所说,开始是检查系统调用号是否在合法范围之内,这里比较的对象显然就是NUMBER_OF_SYSCALLS。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -