漫谈兼容内核之五:Kernel-win32的系统调用机制
[b][align=center][size=5]漫谈兼容内核之五:Kernel-win32的系统调用机制[/size][/align][/b]
[align=center]毛德操[/align]
正如许多网友所言,要在Linux内核中实现Windows系统调用(或别的系统调用),最简单的办法莫过于把这些系统调用“搭载”在Linux系统调用上。具体又有几种不同的方法:
1. 为Linux系统调用ioctl()增加一些“命令码”,每个新的命令码都代表着一个Windows系统调用。
2. 为Linux增加一个新的系统调用、例如win32_syscall()、作为总的入口和载体,然后定义一些类似于ioctl()中所用那样的命令码。
3. 在Linux系统中定义一种虚拟的特殊文件,然后把Windows系统调用搭载在某个文件操作的系统调用上,例如ioctl()、read()等等都可以用于这个目的。作为一种特例,在/proc下面增加一个节点,就可以用于这个目的。又如socket也可以看作是这样的特殊文件。
4. 其它。例如也可以采取类似于“远程过程调用”、即RPC的形式,但是让“服务端”成为内核线程,或者直接在调用者的上下文中执行(这实际上是第3种方法的变种)。
其中又以第1、2两种方法更为简单易行。事实上Kernel-win32正是这样做的。
Kernel-win32原先在这方面提供两种选项。一种是利用Linux的ioctl()系统调用;另一种是为Linux增添一个win32()系统调用,然后在这个新添系统调用的内部采用类似于ioctl()那样的实现。但是在后来的版本中已经放弃了采用ioctl()的选项(在相应的代码中加上了“#if 0”),所以现在已经只采用上述的第二种方法,即为Linux增加一个系统调用作为载体。下面我们看它的代码。
首先,Kernel_win32为Linux定义了一个新的系统调用号:
[code]/* Linux Win32 emulation syscall */
#define __NR_win32 249[/code]
新的调用号__NR_win32定义为249。这个调用号在当时无疑是空闲的,但是在Linux内核的2.6.14版中分配使用的系统调用号已经达到了288,而调用号为249的系统调用是io_cancel()。所以如果要在2.6.14版内核上使用Kernel-win32肯定得要修改这个调用号的定义。其实,这也反映出此种方法的缺点:只要是没有被正式纳入Lunux内核代码的系统调用号,都是靠不住的。此外,注释中说这是用来实现(Windows)系统调用“仿真(emulation)”的,但是我认为这只能说是“模拟(simulation)”,因为只是逻辑上相同、而形式上是不同的。
这个Linux系统调用只是个载体,而实际的Windows系统调用号(更确切地说是Kernel_win32系统调用号),则另有定义:
[code]/* Win32 system call numbers */
typedef enum {
WINESERVER_INITIALISE_WIN32,
WINESERVER_UNINITIALISE_WIN32,
WINESERVER_CLOSE_HANDLE,
WINESERVER_WAIT_FOR_MULTIPLE_OBJECTS,
WINESERVER_CREATE_MUTEX,
……
WINESERVER_MAP_VIEW_OF_FILE,
WINESERVER_UNMAP_VIEW_OF_FILE,
WINESERVER__LAST
} WineSyscallNum;[/code]
一共是30个调用号(最后的WINESERVER__LAST并非有效的调用号)。注意这些调用号跟真正的Windows系统调用号是完全不同的,例如NtCloseHandle()的调用号是24,而在这里是2。而且,所定义的许多系统调用在Windows中并没有对应物,例如开头两个就是这样。至于Windows中有、而在这里没有定义的系统调用,那就更多了(Win2k有248个系统调用)。所以这实际上不能说是Windows系统调用。代码中称为WineSyscallNum,意思大概是把Wine的一些RPC函数转化成了系统调用。
此外,作为可安装模块的Kernel-win32还要在初始化时“登记”用来实现这个系统调用的函数。
[code]static int __init wineserver_init_module(void)
{
#ifdef USE_WIN32SYSCALL
int tmp;
#endif
……
#ifdef USE_WIN32SYSCALL
/* register the syscall */
tmp = register_win32_syscall(__NR_win32,win32syscall, &wineserver_ornament_ops);
if (tmp<0) {
remove_proc_entry("wineserver",NULL);
return tmp;
}
#endif
return 0;
} /* end wineserver_init_module() */[/code]
要登记的函数是win32syscall(),而指针&wineserver_ornament_ops是要传递给这个函数的参数。我们往下看register_win32_syscall()的代码。
[code][wineserver_init_module() > register_win32_syscall()]
int register_win32_syscall(int syscall, win32syscall_func func,
const struct task_ornament_operations *ornament_type)
{
……
if (!w32handler) {
if (cmpxchg(&sys_call_table[syscall],
(long)sys_ni_syscall,
(long)sys_win32)==(long)sys_ni_syscall){
/* we installed it successfully */
w32syscall = syscall;
w32handler = func;
w32ornament_type = ornament_type;
}
ret = 0;
}
……
return ret;
} /* end register_win32_syscall() */[/code]
显然,实际填写到系统调用(跳转)表sys_call_table[ ]中的函数指针是sys_win32()。
应用软件在需要进行Windows系统调用(更确切地说是Kernel_win32系统调用)时通过一个库函数win32()实施调用。例如要调用CreateFile()时就这样调用:
int i = win32(WINESERVER_CREATE_FILE, &args);
参数WINESERVER_CREATE_FILE就是系统调用号,或称“命令码”。而真正用于CreateFile()的参数,则都组装在一个数据结构中,&args就是这个数据结构的地址。
库函数win32()的代码很简单:
[code]static __inline__ int win32(int cmd, void *args)
{
#ifdef USE_WIN32SYSCALL
return syscall(__NR_win32, cmd, args);
#else
#error must use Win32 Syscall
#endif
}[/code]
这里的syscall()是C库中的一个函数,其代码在glibc的一个源文件syscall.s中,有兴趣的读者可以自行阅读。其作用则不言自明:__NR_win32是Linux系统调用号;cmd是命令码、即Kernel_win32系统调用号;args是指向参数结构的指针。后面两项都是对于Linux系统调用win32()的参数。
进入内核以后,CPU根据系统调用号和内核中的系统调用(跳转)表进入内核函数sys_win32():
[code]asmlinkage int sys_win32(unsigned int cmd, void *args)
{
struct task_ornament *orn;
win32syscall_func fnx;
int error;
……
fnx = w32handler;
……
/* find the ornament on the current task (does ornget if successful) */
orn = task_ornament_find(current, w32ornament_type);
……
/* invoke the handler */
error = fnx(orn,cmd,args);
……
return error;
} /* end sys_win32() */[/code]
从代码中可以看出,这个函数只是中转,真正的目的是要通过前面登记的函数指针w32handler调用win32syscall()。那么为什么不直接把win32syscall()放在系统调用表中,从而直接进入win32syscall()呢?比较一下两个函数的调用参数就可以知道,后者要求以指向当前线程的task_ornament结构指针作为参数,而内核根据系统调用表中的函数指针进行调用时是不能带额外参数的。之所以需要这个指针,显然是因为有关Windows线程的补充信息都在这个数据结构中,或者从这个数据结构开始才能找到。
为了要得到当前线程的这个task_ornament结构指针,这里通过task_ornament_find()在当前task_struct的ornament队列中寻找。这里的指针w32ornament_type 指向数据结构wineserver_ornament_ops,这是前面登记系统调用函数指针时设置好的。
[code][sys_win32() > task_ornament_find()]
struct task_ornament *task_ornament_find(struct task_struct *tsk,
struct task_ornament_operations *type)
{
struct task_ornament *orn;
struct list_head *ptr;
read_lock(&tsk->alloc_lock);
for (ptr=tsk->ornaments.next; ptr!=&tsk->ornaments; ptr=ptr->next) {
orn = list_entry(ptr,struct task_ornament,to_list);
if (orn->to_ops==type)
goto found;
}
read_unlock(&tsk->alloc_lock);
return NULL;
found:
ornget(orn);
read_unlock(&tsk->alloc_lock);
return orn;
} /* end task_ornament_find() */[/code]
这段程序扫描给定task_struct结构的ornament队列,从中寻找类型为type、实际上是指向wineserver_ornament_ops的task_ornament数据结构。其实,正如在“对象管理”那篇漫谈所述,这个队列中一般只有一个数据结构,而且其类型也正是wineserver_ornament_ops。所以是否真的需要如此大动干戈是值得推敲的。
找到了这个补充性的数据结构,就可以调用win32syscall()了。
[code][sys_win32() > win32syscall()]
#ifdef USE_WIN32SYSCALL
int win32syscall(struct task_ornament *orn, unsigned int syscall, void *uargs)
{
struct WineThread *thread;
void *args;
int err;
……
if (copy_from_user(args, uargs, ioctl_cmds[syscall].ic_argsize)) {
err = -EFAULT;
goto cleanup;
}
/* invoke the syscall handler */
if (orn)
thread = orn_entry(orn,struct WineThread,wt_ornament);
else
thread = NULL;
……
err = ioctl_cmds[syscall].ic_handler(thread,args,uargs);
……
return err;
} /* end win32syscall() */
#endif[/code]
先从用户空间把实际的调用参数(组装在一个数据结构中)复制到系统空间的一个缓冲区。下面的orn_entry是个宏操作,目的是把task_ornament结构指针换算成WineThread结构指针。因为前者是后者内部的一个成分,所以可以进行换算。
关键的操作就是根据Kernel-win32系统调用号syscall从系统调用表ioctl_cmds[ ]中取得目标函数指针并加以调用。这个数组之所以叫ioctl_cmds,是因为原先Kernel-win32是通过ioctl()进行调用的。
[code]static const struct _ioctl_cmd ioctl_cmds[] =
{
_WIN32(InitialiseWin32),
_WIN32(UninitialiseWin32),
_WIN32(CloseHandle),
……
_WIN32(CreateFileA),
_WIN32(ReadFile),
_WIN32(WriteFile),
……
_WIN32(CreateFileMappingA),
_WIN32(MapViewOfFile),
_WIN32(UnmapViewOfFile),
{ 0, NULL }
};[/code]
这里宏操作_WIN32的定义为:
[code] #define _WIN32(X) { sizeof(struct Wioc##X), X }[/code]
以数组中的元素_WIN32(CreateFileA)为例,经过编译以后就成为:
[code]{sizeof(struct WiocCreateFileA), CreateFileA}[/code]
所以前面引用的ioctl_cmds[syscall].ic_argsize和ioctl_cmds[syscall].ic_handler分别为参数结构的大小和函数指针。再往下的事就不用说了。
把Windows系统调用(或Kernel-win32系统调用)搭载在Linux系统调用上的做法简单易行,但是也有缺点。
首先是降低了效率。从上述的过程一步一步下来,可以看出系统的开销还是不小的。这里面有的是因为Kernel-win32具体的设计所引起,实际上还可优化;有的却是这种方法所固有的,这跟CPU的“间接寻址”与“直接寻址”的区别有些相似。这一点开销,对于本身较大、较费时的系统调用而言固然可以忽略不计;但是对于本身较小、特别是需要频繁调用的系统调用而言却是不可忽略的了。
更重要的是,由于是搭载在Linux系统调用上,返回用户空间时也必定跟Linux系统调用走同一条路线。然而在这方面两个系统是有区别的。在返回的过程中,Linux要检查是否有Signal,如果有就要在用户空间加以执行(类似于对用户空间程序的中断),而Windows则要检查和处理APC。这二者原理相似,但是具体的实现还是有差别的(至少有待研究)。显然,最好是能够各走各的路,否则对于要达到高度兼容的目的是不利的。
另一方面,这种方法对于用户空间DLL的实现、特别是ntdll.dll的实现有了特殊要求。所以这种方法只能说是“模拟”而不是“仿真”。为了达到高度兼容的目标,在测试时最好能够把我们的全套DLL、包括ntdll、安装到Windows上去,再运行Windows应用软件,看其表现和效果是否与使用“原装”DLL时相同。反过来,也最好能把Windows的原装DLL和应用软件安装到Linux上(如果这样做不构成侵犯版权的话),以检验Linux兼容内核对Windows系统调用的支持是否正确与完整。这就要求二者采用相同的机制与手段,例如都采用int 0x2e,发生0x2e自陷时有相同的堆栈内容,采用相同的系统调用号,等等。而且,有些特殊的应用软件甚至可能绕过Win32 API,而直接通过int 0x2e进行系统调用,对于这样的应用显然就无法在DLL中拦截其系统调用并加以转换。更何况Windows的系统调用还不完全限于int 0x2e这一种手段,还有0x2b、0x2c、0x2d也是特殊的系统调用手段,而且那些调用更有可能绕过Win32 API。当然,那些调用的实现是将来的事,甚至可能不会去实现,但作为设计方案也应该加以考虑、或留下余地。
所以,对于兼容内核,我们应该考虑采用int 0x2e作为系统调用的手段,并实现一套跟Windows尽可能一致的机制。具体地,就是要把ReactOS的系统调用机制与Linux的系统调用机制揉合在一起,使其在系统空间与用户空间的分界线上呈现出跟Windows尽量一致、甚至完全一致的特性。其实这也并不像有些人想像的那么难,实际上我们已经调通了这样的一个雏型,春节之后整理一下就可以把源码公开出来。