有网友在论坛上发贴,要求我谈谈ReactOS 是怎样实现系统调用的。另一方面,我上
次已经谈到兼容内核应该如何实现Windows 系统调用的问题,接着谈谈ReactOS 怎样实现
系统调用倒也顺理成章,所以这一次就来谈谈这个话题。不过这显然不属于“漫谈Wine”
的范畴,也确实没有必要再来个“漫谈ReactOS”,因此决定把除Wine 以外的话题都纳入“漫
谈兼容内核”。
ReactOS 这个项目的目标是要开发出一个开源的Windows。不言而喻,它要实现的系统
调用就是Windows 的那一套系统调用,也就是要忠实地实现Windows 系统调用界面。本文
要说的不是Windows 系统调用界面本身,而是ReactOS 怎样实现这个界面,主要是说说用
户空间的应用程序怎样进入/退出内核、即系统空间,怎样调用定义于这个界面的函数。实
际上,ReactOS 正是通过“int 0x2e”指令进入内核、实现系统调用的。虽然ReactOS 并不
是Windows,它的作者们也未必看到过Windows 的源代码;但是我相信,ReactOS 的代码、
至少是这方面的代码,与“正本”Windows 的代码应该非常接近,要有也只是细节上的差别。
下面以系统调用NtReadFile()为例,按“自顶向下”的方式,一方面说明怎样阅读ReactOS
的代码,一方面说明ReacOS 是怎样实现系统调用的。
首先,Windows 应用程序应该通过Win32 API 调用这个接口所定义的库函数,这些库
函数基本上都是在“动态连接库”、即DLL 中实现的。例如,ReadFile()就是在Win32 API
中定义的一个库函数。实现这个库函数的可执行程序在Windows 的“系统DLL”之一
kernel32.dll 中,有兴趣的读者可以在Windows 上用一个工具depends.exe 打开kernel32.dll,
就可以看到这个DLL 的导出函数表中有ReadFile()。另一方面,在微软的VC 开发环境(Visual
Studio)中、以及Win2k DDK 中,都有个“头文件”winbase.h,里面有ReadFile()的接口定
义:
WINBASEAPI
BOOL
WINAPI
ReadFile(
IN HANDLE hFile,
OUT LPVOID lpBuffer,
IN DWORD nNumberOfBytesToRead,
OUT LPDWORD lpNumberOfBytesRead,
IN LPOVERLAPPED lpOverlapped
);
函数名前面的关键词WINAPI 表示这是个定义于Win32 API 的函数。
在ReactOS 的代码中同样也有winbase.h,这是在目录reactos/w32api/include 中:
BOOL WINAPI ReadFile(HANDLE,PVOID,DWORD,PDWORD,LPOVERLAPPED);
显然,这二者实际上是相同的(要不然就不兼容了)。当然,微软没有公开这个函数的代
码,但是ReactOS 为之提供了一个开源的实现,其代码在reactos/lib/kernel32/file/rw.c 中。
BOOL STDCALL
ReadFile( HANDLE hFile,LPVOID lpBuffer,DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,LPOVERLAPPED lpOverLapped )
{
……
errCode = NtReadFile(hFile,
hEvent,
NULL,
IoStatusBlock,
lpBuffer,
nNumberOfBytesToRead,
ptrOffset,
NULL);
……
return(TRUE);
}
我们在这里只关心NtReadFile(),所以略去了别的代码。
如前所述,NtReadFile()是Windows 的一个系统调用,内核中有个函数就叫NtReadFile(),
它的实现在ntoskrnl.exe 中(这是Windows 内核的核心部分),这也可以用depends.exe 打开
ntoskrnl.exe 察看。ReactOS 代码中对内核函数NtReadFile()的定义在reactos/include/ntos/zw.h
中,同样的定义也出现在reactos/w32api/include/ddk/winddk.h 中:
NTSTATUS
STDCALL
NtReadFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE UserApcRoutine OPTIONAL,
IN PVOID UserApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG BufferLength,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL
);
而相应的实现则在reactos/ntoskrnl/io/rw.c 中。
表面上看这似乎挺正常,ReadFile()调用NtReadFile(),reactos/ntoskrnl/io/rw.c 则为其提
供了被调用的NtReadFile()。可是仔细一想就不对了。这ReadFile()是在用户空间运行的,
而reactos/ntoskrnl/io/rw.c 中的代码却是在内核中,是在系统空间。难道用户空间的程序竟能
如此这般地直接调用内核中的函数吗?如果那样的话,那还要什么陷阱门、调用门这些机制
呢?再说,编译的时候又怎样把它们连接起来呢?
这么一想,就可以断定这里面另有奥妙。仔细一查,原来还另有一个NtReadFile(),在
msvc6/iface/native/syscall/Debug/zw.c 中:
__declspec(naked) __stdcall
NtReadFile(int dummy0,int dummy1,int dummy2)
{
__asm {
push ebp
mov ebp,esp
mov eax,152
lea edx,8[ebp]
int 0x2E
pop ebp
ret 9
}
}
原来,用户空间也有一个NtReadFile(),正是这个函数在执行自陷指令“int 0x2e”。我
们看一下这段汇编代码。这里面的152 就是NtReadFile()这个系统调用的调用号,所以当cpu
自陷进入系统空间后寄存器eax 持有具体的系统调用号。而寄存器edx,在执行了lea 这条
指令以后,则持有cpu 在调用这个函数前夕的堆栈指针,实际上就是指向堆栈中调用参数
的起点。在进行系统调用时如何传递参数这个问题上,Windows 和Linux 有着明显的差别。
我们知道,Linux 是通过寄存器传递参数的,好处是效率比较高,但是参数的个数受到了限
制,所以Linux 系统调用的参数都很少,真有大量参数需要传递时就把它们组装在数据结构
中,而只传递数据结构指针。而Windows 则通过堆栈传递参数。读者在上面看到,ReadFile()
在调用NtReadFile()时有9 个参数,这9 个参数都被压入堆栈,而edx 就指向堆栈中的这些
参数的起点(地址最低处)。我们在这个函数中没有看到对通过堆栈传下来的参数有什么操
作,也没有看到往堆栈里增加别的参数,所以传下来的9 个参数被原封不动地传了下去(作
为int 0x2e 自陷的参数)。这样,当cpu 自陷进入内核以后,edx 仍指向用户空间堆栈中的
这些参数。当然,cpu 进入内核以后的堆栈是系统空间堆栈,而不是用户空间堆栈,所以
需要用copy_from_user()一类的函数把这些参数从用户空间拷贝过来,此时edx 的值就可用
作源指针。至于寄存器ebp,则用作调用这个函数时的“堆栈框架”指针。
当内核完成了具体系统调用的操作,cpu 返回到用户空间时,下一条指令是“pop ebp”,
即恢复上一层函数的堆栈框架指针。然后,指令“ret 9”使cpu 返回到上一层函数,同时
调整堆栈指针,使其跳过堆栈上的9 个调用参数。在“正宗”的x86 汇编语言中,用在ret
指令中的数值以字节为单位,所以应该是“ret 24h”,而这里却是以4 字节长字为单位,这
显然是因为用了不同的汇编工具。
子程序的调用者可以把参数压入堆栈,通过堆栈把参数传递给被调用者。可是,当cpu
从子程序返回时,由谁负责从堆栈中清除这些参数呢?显然,要么就是由调用者负责,要么
就是由被调用者负责,这里需要有个约定,使得调用者和被调用者取得一致。在上面
NtReadFile()这个函数中,我们看到是由被调用者负起了这个责任、在调整堆栈指针。函数
代码前面的__stdcall 就说明了这一点。同样,在.h 文件中对NtReadFile()的定义(申明)之前
也加上了STDCALL,也是为了说明这个约定。“Undocumented Windows 2000 Secrets”这本
书中(p51-53)对类似的各种约定有一大段说明,读者可以参考。另一方面,在上面这个函数
的代码中,函数的调用参数是3 个而不是9 个。但是看一下代码就可以知道这些参数根本就
没有被用到,而调用者、即前面的ReadFile()、也是按9 个参数来调用NtReadFile()的。所
以,这里的三个参数完全是虚设的,有没有、或者有几个、都无关紧要,难怪代码中称之为
“dummy”。
用户空间的这个NtReadFile()向上代表着内核函数NtReadFile(),向下则代表着想要调
用内核函数NtReadFile()的那个函数,在这里是ReadFile();但是它本身并不提供什么附加
的功能,这样的中间函数称为“stub”。
当然,ReactOS 的这种做法很容易把读者引入迷茫。相比之下,Linux 的做法就比较清
晰,例如应用程序调用的是库函数write(),而内核中与之对应的函数则是sys_write()。
那么为什么ReactOS 要这么干呢?我只能猜测:
1) Windows 的源代码中就是这样,例如用depends.exe 在ntdll.dll 和ntoskrnl.exe 中
都可看到有名为NtReadFile()的函数,而ReactOS 的人就依葫芦画瓢。
2) 作为一条开发路线,ReactOS 可能在初期不划分用户空间和系统空间,所有的代
码全在同一个空间运行,所以应用程序可以直接调用内核中的函数。这样,例
如对文件系统的开发就可以简单易行一些。然后,到一些主要的功能都开发出
来以后,再来划分用户空间和系统空间,并且补上如何跨越空间这一层。从zw.c
这个文件在native/syscall/Debug 目录下这个迹象看,ReactOS 似乎正处于走出这
一步的过程中。
3) ReactOS 的作者们可能有意让它也可以用于嵌入式系统。嵌入式系统往往不划分
用户空间和系统空间,而把应用程序和内核连接在同一个可执行映像中。这样,
如果需要把代码编译成一个嵌入式系统,就不使用stub;而若要把代码编译成一
个桌面系统,则可以在用户空间加上stub 并在内核中加上处理自陷指令“int
0x2e”的程序。
在Windows 中,stub 函数NtReadFile()在ntdll.dll 中。实际上,所有0x2e 系统调用的stub
函数都在这个DLL 中。显然,所有系统调用的stub 函数具有相同的样式,不同的只是系统
调用号和参数的个数,所以ReactOS 用一个工具来自动生成这些stub 函数。这个工具的代
码在msvc6/iface/native/genntdll.c 中,下面是一个片断:
void write_syscall_stub(FILE* out,FILE* out3,char* name,char* name2,
char* nr_args,unsigned int sys_call_idx)
{
int i;
int nArgBytes = atoi(nr_args);
#ifdef PARAMETERIZED_LIBS
……
#else
fprintf(out,"__asm__(\"
\\n\\t.global _%s\\n\\t\"\n",name); fprintf(out,"\".global _%s\\n\\t\"\n",name2); fprintf(out,"\"_%s:\\n\\t\"\n",name2); #endif fprintf(out,"\t\"pushl\t%%ebp\\n\\t\"\n"); fprintf(out,"\t\"movl\t%%esp,%%ebp\\n\\t\"\n"); fprintf(out,"\t\"mov\t$%d,%%eax\\n\\t\"\n",sys_call_idx); fprintf(out,"\t\"lea\t8(%%ebp),%%edx\\n\\t\"\n"); fprintf(out,"\t\"int\t$0x2E\\n\\t\"\n"); fprintf(out,"\t\"popl\t%%ebp\\n\\t\"\n"); fprintf(out,"\t\"ret\t$%s\\n\\t\");\n\n",nr_args); …… } 代码中的’\t’表示TAB 字符,读者阅读这段代码应该没有什么问题。这段代码根据name、 nr_args、sys_call_idx 等参数为给定系统调用生成stub 函数的汇编代码。那么这些参数从何 而来呢?在ReactOS 代码的reactos/tools/nci 目录下有个文件sysfuncs.lst,下面是从这个文件 中摘出来的几行: NtAcceptConnectPort 6 NtAccessCheck 8 NtAccessCheckAndAuditAlarm 11 NtAddAtom 3 …… NtClose 1 …… NtReadFile 9 …… 这里的NtAcceptConnectPort 就是调用号为0 的系统调用NtAcceptConnectPort(),它有6 个 参数。另一个系统调用NtClose()只有1 个参数。而NtReadFile()有9 个参数,并且正好是这个表 中的第153 行,所以调用号是152。 用户空间的程序一执行int 0x2e,cpu 就自陷进入了系统空间。其间的物理过程这里就 不多说了,有需要的读者可参考“情景分析”或其它有关资料。我这里就从cpu 怎样进入 int 0x2e 的自陷处理程序说起。 像别的中断向量一样,ReactOS 在其初始化程序KeInitExceptions()中设置了int 0x2e 的 向量,这个函数的代码在reactos/ntoskrnl/ke/i386/exp.c 中: VOID INIT_FUNCTION KeInitExceptions(VOID) /* * FUNCTION: Initalize cpu exception handling */ { …… set_trap_gate(0,(ULONG)KiTrap0,0); set_trap_gate(1,(ULONG)KiTrap1,0); set_trap_gate(2,(ULONG)KiTrap2,0); set_trap_gate(3,(ULONG)KiTrap3,3); …… set_system_call_gate(0x2d,(int)interrupt_handler2d); set_system_call_gate(0x2e,(int)KiSystemService); } 显然,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()那段代码的读者应该能至少大体上读懂这个函数。 _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 前面的一些指令主要是在保存现场,类似于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 数据结构。那么这一步 对于系统调用为什么重要呢?我们看一下这个数据结构中的几个成分就可以明白: typedef struct _KTHREAD { /* For waiting on thread exit */ DISPATCHER_HEADER DispatcherHeader; /* 00 */ …… SSDT_ENTRY *ServiceTable; /* DC */ …… UCHAR PrevIoUsMode; /* 137 */ …… } KTHREAD; 每个成分后面的注释说明这个成分在数据结构中以字节为单位的相对位移,例如指针 ServiceTable 的相对位移就是0xdc。事实上,这个指针正是我们此刻最为关注的,因为它直 接与系统调用的函数跳转表有关。每个线程的这个指针都指向一个SSDT_ENTRY结构数组。 既然每个线程都有这么个指针,就说明每个线程都可以有自己的ServiceTable。不过,实际 上每个线程的ServiceTable 通常都指向同一个结构数组,我们等一下再来看这个结构数组, 现在先往下看代码。 /* 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,%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 这里我们关注的是最后这一小段。首先,KTHREAD_SERVICE_TABLE(%esi)就是当前 线程的ServiceTable 指针。常数KTHREAD_SERVICE_TABLE 定义为0xdc: #define KTHREAD_SERVICE_TABLE 0xDC 这跟前面KTHREAD 数据结构的定义显然是一致的。 上面讲过,实际上一般情况下所有线程的ServiceTable 指针都指向同一个结构数组,那 就是KeServiceDescriptorTable[ ]: SSDT_ENTRY __declspec(dllexport) KeServiceDescriptorTable[SSDT_MAX_ENTRIES] = { { MainSSDT,NULL,NUMBER_OF_SYSCALLS,MainSSPT },{ NULL,NULL },NULL } }; 这个数组的大小一般是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 略少一些。 我们继续往下看代码: /* 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 正如代码中的注释所说,开始是检查系统调用号是否在合法范围之内,这里比较的对象 显然就是NUMBER_OF_SYSCALLS。 前面讲过,寄存器%edx 指向用户空间堆栈上的函数调用框架,实际上就是指向所传递 的参数,现在把这个指针复制到%esi 中,这是在为从用户空间堆栈复制参数做准备。但是, 光有复制的起点还不够,还需要有复制的长度(字节数)、即参数的个数乘4,所以需要知道 具体的系统调用有几个参数。这个信息保存在一个以系统调用号为下标的无符号字节数组中 (所以每个系统调用的参数总长度不能超过255 字节),SSDT_ENTRY 数据结构中的第三个 成分(相对位移为12、或0xc)就是指向这个数组的指针。对于常规系统调用,这个数组是 MainSSPT。可想而知,这个数组的内容也应来自sysfuncs.lst。代码中先让%ecx 指向 MainSSPT,再以%eax 中的系统调用号与其相加,就使其指向了数组中的相应元素,而movb 指令就把这个字节取了出来。所以,最后%ecx 持有给定系统调用的参数复制长度。从%esp 的内容中减去%ecx 的内容,就在系统空间堆栈上保留了若干字节,其长度等于参数复制长 度,这样就为把参数从用户空间堆栈复制到系统空间堆栈做好了准备。再往下看: /* Get pointer to function */ movl (%edi),%edi movl (%edi,%eax,4),%eax /* Copy the arguments from the user stack to our stack */ shr $2,%ecx movl %esp,%edi cld rep movsd /* Do the System Call */ call *%eax movl %eax,KTRAP_FRAME_EAX(%ebp) /* Deallocate the kernel stack frame */ movl %ebp,%esp 前面,寄存器%edi 已经指向常规系统调用的SSDT_ENTRY 数据结构,也就是指向了该 数据结构中的第一个成分。SSDT_ENTRY 数据结构的第一个成分是个指针,指向一个函数 指针数组。对于常规系统调用,这就是MainSSDT。指令“movl (%edi),%edi”把%edi 所指 处的内容赋给了%edi,使原来指向这个指针的%edi 现在指向了MainSSDT。这也是个以系 统调用号为下标的数组,其定义为: SSDT MainSSDT[] = { { (ULONG)NtAcceptConnectPort },{ (ULONG)NtAccessCheck },{ (ULONG)NtAccessCheckAndAuditAlarm },…… { (ULONG)NtReadFile },…… } 在我们这个例子中,指令“movl (%edi,%eax”,即“把%edi 加相对位移为‘系 统调用号乘4’之处的内容装入%eax”,使%eax 指向了NtReadFile()。然后就是把参数从用 户空间堆栈拷贝到系统空间堆栈,注意%ecx 中的长度是以字节为单位的,所以要右移两位 变成以长字为单位。 最后,指令“call *%eax”就使cpu 进入了内核里面的NtReadFile(),其代码在 reactos/ntoskrnl/io/rw.c 中。如果按Linux 的规矩,这应该是sys_NtReadFile(): NTSTATUS STDCALL NtReadFile (IN HANDLE FileHandle,IN HANDLE Event OPTIONAL,IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,IN PVOID ApcContext OPTIONAL,OUT PIO_STATUS_BLOCK IoStatusBlock,OUT PVOID Buffer,IN ULONG Length,IN PLARGE_INTEGER ByteOffset OPTIONAL,IN PULONG Key OPTIONAL) { …… } 这个函数的调用界面与应用程序在用户空间进行这个系统调用时所遵循的界面完全相 同,而应用程序压入用户空间堆栈的9 个参数已经被拷贝到了系统空间堆栈中合适的位置 上。于是,对于这个函数而言,就好像其调用者、在我们这个情景中是ReadFile()、就在系 统空间中一样。 回到上面的汇编代码中。当cpu 从目标函数返回时,寄存器%eax 持有该函数的返回值, 这是要返回给用户空间的,所以把它保存在堆栈框架中。 下面就是从内核返回到用户空间的过程,我把代码留给读者自己研究。不过需要给一点 提示: 原文链接:https://www.f2er.com/react/308268.html