Windows之漫谈兼容内核
毛德操
目录
01.漫谈兼容内核之一:ReactOS怎样实现系统调用 p3
02.漫谈兼容内核之二:关于kernel-win32的对象管理 p17
03.漫谈兼容内核之三:关于kernel-win32的文件操作 p33
04.漫谈兼容内核之四:Kernel-win32的进程管理 p47
05.漫谈兼容内核之五:Kernel-win32的系统调用机制 p54
06.漫谈兼容内核之六:二进制映像的类型识别 p61
07.漫谈兼容内核之七:Wine的二进制映像装入和启动 p70
08.漫谈兼容内核之八:ELF映像的装入(一) p89
09.漫谈兼容内核之九:ELF映像的装入(二) p107
10.漫谈兼容内核之十:Windows的进程创建和映像装入 p121
11.漫谈兼容内核之十一:Windows_DLL的装入和连接 p129
12.漫谈兼容内核之十二:Windows的APC机制 p157
13.漫谈兼容内核之十三:关于“进程挂靠” p178
14.漫谈兼容内核之十四:Windows的跨进程操作 p191
15.漫谈兼容内核之十五:Windows线程的等待和唤醒机制 p206
16.漫谈兼容内核之十六:Windows的进程间通信 p225
17.漫谈兼容内核之十七:再谈windows的进程创建 p257
18.漫谈兼容内核之十八:Windows的LPC机制 p289
19.漫谈兼容内核之十九:Windows线程间的强相互作用 p313
20.漫谈兼容内核之二十:Windows线程的系统空间堆栈 p330
21.漫谈兼容内核之二十一:Windows进程的用户空间 p352
22.漫谈兼容内核之二十二:Windows线程的调度和运行 p375
23.漫谈兼容内核之二十三:关于TLS p395
24.漫谈兼容内核之二十四:Windows的结构化异常处理(一) p413
25.漫谈兼容内核之二十五:Windows的结构化异常处理(二) p433
26.漫谈兼容内核之二十六:Windows的结构化异常处理(三) p466
1
漫谈兼容内核之一
漫谈兼容内核之一漫谈兼容内核之一
漫谈兼容内核之一:
::
:
ReactOS 怎样实现系统调用
怎样实现系统调用怎样实现系统调用
怎样实现系统调用
毛德操
有网友在论坛上发贴,要求我谈谈
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);
2
显然,这二者实际上是相同的
(
要不然就不兼容了
)
。当然,微软没有公开这个函数的代
码,但是
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,
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
);
3
而相应的实现则在
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