写用户态程序(包括普通的应用程序、服务程序、用户态驱动、一些扩展插件)时,每当我们遇到一个不熟悉的API,我们就会打开开发文档,或者MSDN,查查每个参数是怎么填的。(我想你应该没把这些API给背下来了吧^^)
我们在调试时发现执行到这些API时是跟不进去的,那么你想过它们是如何进入操作系统里面工作的吗?它们又是如何在操作系统里面工作的?
比如文件系统的一个API:
1
2
3
4
5
6
7
8
9
|
HANDLE
CreateFile(
LPCTSTR
lpFileName,
DWORD
dwDesiredAccess,
DWORD
dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD
dwCreationDisposition,
DWORD
dwFlagsAndAttributes,
HANDLE
hTemplateFile
);
|
那么这系列文章就是尝试弄清楚这些的一些问题。但是在实际摸索过程中我发现这些远比我想象中复杂,甚至经常迷失了。如果你发现文章有不对的或者可以改进的地方请不吝赐教。
在没有文档的情况下去研究CE的源码是件比较吃力的事,这就是我分享这一系列文章的原因。不管你是在CE平台还是在嵌入式Linux平台下面开发,深入一下CE内部都是件有趣并且有意义的事。而且这些知识对在CE上从事较靠近Kernel的开发以及对问题的深一层次追踪是非常有帮助的。
再说这些机制在不同操作系统上也大同小异(它们本身也会相互学习和借鉴),熟悉一个之后这些知识能够很好的重用在别的地方。所以与只知道拖控件相比,知道How does xxx work?会更有意义。
这篇文章就是对《Windows Embedded CE 6.0 Internals (4) The Mechanism of API》的进一步补充和深入,把一些迷糊的问题搞清楚点。
我大体罗列了这篇文章涉及的问题:
在文章《Inside Windows CE API Calls》非常好的描述了CE的API机制,我在这里简单翻译和整理一下,以便先从整体上了解一下:
CE的API是由若干个服务进程(PSL)实现的:内核进程(NK.exe)、filesys.exe、gwes.exe、device.exe、services.exe(在CE6.0之前是这样的,而在CE6.0/7.0时这些exe大多已经被实现成dll,比如kernel.dll、filesys.dll、device.dll、gwes.dll等。并且PSL分成了Kernel-mode server和User-mode server,这个我在后面详细介绍。)
当我们的应用程序调用这些PSL的API时,我们一般会跳进这些PSL去执行对应的API实现。
大部分API都是从coredll.dll导出的,我们的应用程序会链接到这个DLL文件。当我们调用API时,比如GetTickCount,我们其实是调用coredll.dll导出的GetTickCount(coredll.dll相当于一个wrapper,我们经常也叫做“thunk”,kcoredll.dll在后面介绍。):
1
2
3
4
|
DWORD
xxx_GetTickCount()
{
return
GetTickCount();
}
|
你能从这里看到更多的thunk,并能看到更多的xxx_前缀的thunk(但并不是全部都是):
1
|
%_WINCEROOT%\private\winceos\coreos\core\thunks\*
|
我们发现这里的函数名前面都有xxx_,但是实际我们在文档里看到的并没有xxx_前缀,这是因为在这里我们重命名这些API了:
1
|
%_WINCEROOT%\private\winceos\coreos\core\coredll.def
|
像这样:
1
|
GetTickCount=xxx_GetTickCount
|
在上面的xxx_GetTickCount函数里面我们就直接return一个函数:GetTickCount,那么这个函数到底是什么呢?从这里你能找到答案:
1
|
%_WINCEROOT%\public\common\oak\inc\*
|
它其实只是个宏定义,像这样:
1
|
define GetTickCount COMPLICATED_MACRO(..., SH_WIN32, W32_GetTickCount, ...)
|
SH_WIN32在SDK中被定义成0(GetTickCount所在的API set table所在的ID),而W32_GetTickCount在另一个OAK头文件中被定义成13(GetTickCount在API set table里面的索引)。
为了思路清晰,COMPLICATED_MACRO我们不再展开了,实际上当调用这个宏时它就会跳转到一个地址,一个什么地址呢?在文章《Windows Embedded CE 6.0 Internals (4) The Mechanism of API》中提到了KDataStruct这个结构体,其开始地址是固定的PUserKData,对于ARM处理器是0xFFFFC800,而其它处理器是0x00005800。恩,就是跳转到偏移PUserKData一定量的位置,不管是0xFFFF…还是0x0000…,都是个特殊的位置,这些位置都是Trap,向这些Trap的跳转会产生Exception,《Inside Windows CE API Calls》用一句小幽默解释道:
All exceptions go to the kernel first, and the kernel says,"A-ha! I know this invalid address. It's the encoding for an API, index 13 of API table 0."
之后Kernel会去做一些工作:marshals (maps) arguments, adjusts permissions, flushes cache and TLB(页表缓冲),如果需要的话。一起都准备好之后,请求的API所在的PSL进程里的线程将会被执行。
可以参考:
1
2
|
%_WINCEROOT%\private\winceos\coreos\nk\kernel\x86\fault.c, Int20SyscallHandler.
%_WINCEROOT%\private\winceos\coreos\nk\kernel\objdisp.c, ObjectCall()
|
当PSL进程里的API调用执行完之后,调用返回时会触发另一个Exception(Kernel设置了返回地址为另一个指定的代码地址),Kernel再一次adjusts arguments, permissions, and other state,比如必要的话。就这样CPU执行权再一次返回到原调用进程。
可以参考:
1
|
%_WINCEROOT%\private\winceos\coreos\nk\kernel\x86\fault.c, ServerCallReturn.
|
从应用程序开始的API调用的一个简单的流程我们搞清楚了。(实际上整个操作系统的API调用机制是非常复杂的,这里说的只是最一般的情况。)
文章《Inside Windows CE API Calls》后面关于KMODE部分的介绍是针对CE 6.0之前版本的,在CE 6.0时有较大变化。另外作者说到:
In Windows CE, "kernel mode" threads have permission to access memory addresses outside of their own process.
这是不准确的(你从后面的留言中能够了解到更多),这在CE 6.0/7.0上就更不准确了,因为连slot都没有了。
作者: 王克伟
出处: http://wangkewei.cnblogs.com/