📄
字号:
下面接着看start_process():
[code][main() > wine_init() > __wine_process_init() > __wine_kernel_init() > wine_switch_to_stack()
> start_process()]
static void start_process (void *arg )
{
__TRY
{
PEB *peb = NtCurrentTeb()->Peb;
IMAGE_NT_HEADERS *nt;
LPTHREAD_START_ROUTINE entry;
LdrInitializeThunk( main_exe_file, 0, 0, 0 ); //装入所需的DLL并完成动态连接。
nt = RtlImageNtHeader( peb->ImageBaseAddress );
entry = (LPTHREAD_START_ROUTINE)((char *)peb->ImageBaseAddress +
nt->OptionalHeader.AddressOfEntryPoint);
. . . . . .
ExitProcess( entry( peb ) );
}
__EXCEPT(UnhandledExceptionFilter)
{
TerminateThread( GetCurrentThread(), GetExceptionCode() );
}
__ENDTRY
}[/code]
这是一段带出错处理的代码,意思是这样:在执行__TRY{}里面的代码之前,先把陷阱(Trap)响应/处理的向量指向这里的UnhandledExceptionFilter()。这样,如果在执行的过程中落下了陷阱,例如访问了某个未经映射、或者受到保护的地址,就会转到UnhandledExceptionFilter()来执行。
显然,在目标映像投入运行前,还得根据映像中提供的信息装入所有需要直接、间接用到的DLL,并建立好与这些DLL的动态连接,这是由LdrInitializeThunk()完成的。所以,LdrInitializeThunk()是个十分重要的过程,本文因篇幅所限就不深入下去了,以后再回到这个话题上来。
将一个PE映像装入内存以后,其“进程环境块”PEB中的ImageBaseAddress指向这个映像的起点,映像的开头是一个IMAGE_DOS_HEADER数据结构,里面有个指针指向映像的主体部分,而主体部分的开头则是一个IMAGE_NT_HEADERS数据结构。RtlImageNtHeader()就根据这个关系找到IMAGE_NT_HEADERS数据结构(在内存中)的起点。IMAGE_NT_HEADERS是个比较复杂的多层数据结构,里面的成分OptionalHeader.AddressOfEntryPoint就是可执行程序的入口地址(相对于映像起点的位移)。所以,entry就是目标程序在内存中的入口地址。最后,目标程序、在这里是notepad.exe、的执行是以entry(peb)的形式实现的,即把入口地址当成一个函数的起点,而把已经设置好的PEB数据结构作为参数。这样,当CPU从entry()返回时,下一步就是ExitProcess()、即退出/终止Windows进程了。事实上ExitProcess()里面的最后一个系统调用是exit(),所以这既是作为Windows进程的终结,也是作为Linux进程的终结。
注意在此过程中wine-preloader、wine-kthread、以及目标映像notepad.exe都是在同一个进程、即同一个地址空间中活动。先是wine-preloader转化成了wine-kthread,然后wine-kthread又转化成notepad.exe,就像对动态库的调用一样。这中间并没有创建/启动新的进程。而从wine到wine-preloader则略有不同,那是一个进程通过execv()“从新做人”,变成了另一个进程。如果从“进程控制块”、即task_struct数据结构的角度看,则从wine开始一直到notepad.exe都是在同一个进程内变迁。但是,从效率的角度看,则先后装入了四个软件的映像(不包括“解释器”、例如/lib/ld-linux.so.2的映像)。相比之下,Linux内核在装入普通的ELF目标映像时只要装入一个映像就够了(也不包括“解释器”的映像)。
应该说,整个过程确实很复杂。为什么要这么复杂呢?这又要讲到Wine的宗旨,那就是不触动内核,也即“内核差异核外补”。这里的内核差异在于Linux内核不能装入PE格式的映像,也不能实施PE映像与DLL之间的动态连接。于是,要实施PE映像与DLL之间的动态连接,就得在核外启动wine-kthread。这倒没有什么,Linux在装入ELF映像时也要启动ld-linux.so。但是在装入wine-kthread时又得避开PE映像所需要的地方,所以就需要先启动wine-preloader。另一方面,Linux内核并不知道wine-preloader,也不知道该用wine-kthread还是wine-pthread,于是就又得通过另一个工具wine再过渡一下。复杂性就这么层层加码累积起来了。类似的过程,如果是由核内核外分工合作,就可以简化和提高效率。例如,内核读取目标映像的头部就可以知道这是个PE映像,从而需要动用wine-kthread,并且在装入wine-kthread时需要避开应该为PE映像预留的空间,而这对于内核是不难办到的,这样wine和wine-preloader都可以不需要了。进一步,wine-kthread所做的事情也有许多可以移到内核中去做,这样既可提高效率又可简化程序。
上面所说的是在Linux环境下通过键盘命令启动Windows软件的过程,也就是前面所说的“从Linux进程到Windows进程”的过程。下面再看一下由一个Windows进程通过CreateProcessW()创建另一个Windows进程的过程,即“从Windows到Windows”过程,这样才算完整。
[code]BOOL WINAPI CreateProcessW( LPCWSTR app_name, ……)
{
……
get_file_name( app_name, cmd_line, name, sizeof(name), &hFile );
……
switch( MODULE_GetBinaryType( hFile, &res_start, &res_end ))
{
case BINARY_PE_EXE:
TRACE( "starting %s as Win32 binary (%p-%p)\n",
debugstr_w(name), res_start, res_end );
retv = create_process( hFile, name, tidy_cmdline, envW, cur_dir,
process_attr, thread_attr, inherit, flags, startup_info,
info, unixdir, res_start, res_end );
break;
case BINARY_OS216:
case BINARY_WIN16:
case BINARY_DOS:
TRACE( "starting %s as Win16/DOS binary\n", debugstr_w(name) );
retv = create_vdm_process( name, tidy_cmdline, envW, cur_dir, process_attr,
thread_attr, inherit, flags, startup_info, info, unixdir );
break;
case BINARY_PE_DLL:
TRACE( "not starting %s since it is a dll\n", debugstr_w(name) );
SetLastError( ERROR_BAD_EXE_FORMAT );
break;
case BINARY_UNIX_LIB:
. . . . . .
break;
case BINARY_UNKNOWN:
/* check for .com or .bat extension */
if ((p = strrchrW( name, '.' )))
{
if (!strcmpiW( p, comW ) || !strcmpiW( p, pifW ))
{
TRACE( "starting %s as DOS binary\n", debugstr_w(name) );
retv = create_vdm_process( name, tidy_cmdline, envW, cur_dir, process_attr,
thread_attr, inherit, flags, startup_info, info, unixdir );
break;
}
if (!strcmpiW( p, batW ))
{
TRACE( "starting %s as batch binary\n", debugstr_w(name) );
retv = create_cmd_process( name, tidy_cmdline, envW, cur_dir, process_attr,
thread_attr, inherit, flags, startup_info, info );
break;
}
}
/* fall through */
case BINARY_UNIX_EXE:
{
……
}
break;
}
CloseHandle( hFile );
…….
return retv;
}[/code]
看到这段代码,读者也许会想起前边的__wine_kernel_init(),那是在工具wine-kthread中执行的。但是这里的相似只是形式上的,而不是实质上的。
这里MODULE_GetBinaryType()的作用是判断目标映像的类型,这大家都知道了。而我们主要关心的是32位Windows应用,即PE格式的EXE映像,所以只关心create_process()。
[code][CreateProcessW() > create_process()]
static BOOL create_process (HANDLE hFile, LPCWSTR filename, LPWSTR cmd_line, . . .)
{
设置好目标进程的环境和参数。
创建两对pipe, startfd[]和execfd[]。
fork()
子进程:
read( startfd[0]; //企图从一个pipe中读,实际上是等待父进程发来开始运行的指令。
close( startfd[0] );//关闭该pipe。
wine_exec_wine_binary(NULL, argv, NULL, TRUE);
//启动“wine-preloader wine-kthread …”。
write (execfd[1], &err, sizeof(err) );//通过另一个pipe向父进程发送运行后的出错代码。
_exit(1); //退出运行并终结。
父进程:
SERVER_START_REQ( new_process )
. . . . . .
SERVER_END_REQ; //向wineserver登记子进程。
write( startfd[1], &dummy, 1 ); //向子进程发送1,令其运行。
read( execfd[0]); //企图从另一个pipe中读,实际上是等待子进程的运行结果。
WaitForSingleObject( process_info, INFINITE ); //等待目标进程运行。
SERVER_START_REQ( get_new_process_info )
. . . . . .
SERVER_END_REQ; //取得有关子进程运行的信息,这是要返回给调用者的。
}[/code]
这里的操作分两个方面。一方面是子进程的创建,父、子进程间的同步,与wineserver的交互等等。另一方面是创建子进程的具体方法和过程。关于后者,在这里就是对wine_exec_wine_binary()的调用。读者在前边已经看到过对这个函数的调用,那是在工具wine中执行的,在loader/glibc.c的main()中受到调用。
二者都调用wine_exec_wine_binary(),而且在调用时的最后一个参数use_preloader也都是TRUE,表示应该使用wine-preloader。显然,从wine_exec_wine_binary()开始,下面的装入/启动过程就都与前面所述完全相同了。
所以,二者在这方面的相似性才是实质性的。实际上,当前进程、即调用CreateProcessW()的进程,起着与wine相同的作用,即启动“wine-preloader wine-kthread …”的运行。其实工具wine是可以省略的,例如我们既然知道应该用wine-kthread,就可以这样来启动notepad.exe:“wine-preloader wine-kthread notepad.exe”。但是,要求使用者每次都要这样来键入命令行似乎不现实。
我们不妨再深入一些,看看问题的焦点在那里。显然,现有的Linux内核(到2.4.14为止)并不具备直接装入PE映像的能力,所以需要在用户空间有个wine-kthread来完成目标PE映像和DLL的装入,并完成目标映像和DLL之间的动态连接,最后跳转到目标映像的入口。所以wine-kthread对于PE映像起着类似于ELF“解释器”的作用,但是又远比后者复杂,因为它还负责PE映像的装入。既然这么复杂,就不能像ELF“解释器”那样把它做成一个小小的共享库了。于是就来了空间冲突的问题。
映像wine-preloader和wine-kthread的装入地址都是固定的。前者的装入地址是0x78000000,大小不超过8KB;后者的装入地址则是0x7bf00000,大小不超过32KB。于是这里就有了两个问题:
1) 既然wine-kthread的装入地址是0x7bf00000,与为PE格式映像所保留的空间并不冲突,那为什么还要用wine-preloader来保留这些空间呢?
2) 即使真有必要为PE格式映像保留空间,而PE映像又是由wine-kthread装入的,那为什么不可以由wine-kthread自己来保留这些空间呢?比方说,在它的main()中一开始就保留好这些空间,然后才进行其余的活动。
对这两个问题的回答是互相连系在一起的。原来,ELF映像头部所提供的装入位置、大小等等只是静态的数据,并不表示目标映像在运行时就只占这么一点地方。特别重要的是,表面上wine-preloader只装入了wine-kthread和ld-linux.so.2这么两个ELF映像,但是实际上需要装入的还不止于此。这是因为wine-kthread的运行本身还需要得到某些共享库的支持。我们在wine-kthread的代码中看到了对getenv()、strlen()、malloc()等等函数的调用,可是这些函数都在共享库libc.so中。事实上,wine-kthread的运行需要得到libc.so和libwine.so两个共享库的支持。共享库的装入地址都是浮动的,其代码在编译时都要在命令行中加上-fPIC选项。可是,虽然是浮动的,但是在通过mmap()将其影像“装入”虚存空间时却非常可能(甚至肯定)会动用本应该为PE映像保留的区间中。另一方面,共享库在本进程空间的装入、以及目标映像(在这里是wine-kthread)与共享库的动态连接是由“解释器”在启动目标映像之前完成的。当CPU进入目标映像的main()时,共享库的装入和连接早就完成了,到这时候再来为PE映像保留空间早就为时已晚。正因为如此,又不想触及内核,那就只能由wine-preloader过渡一下了。然而,要保留本进程的一部分用户空间,如果是在内核中实现的话,那是轻而易举的事。而Wine之所以搞得这么繁琐,完全就是因为不想触及内核。
从以上的叙述和分析可以看出,Wine的PE映像装入/启动过程存在着两方面的问题。首先是心理和使用习惯方面的,要用户在需要启动notepad.exe时必须键入“wine notepad.exe”会使用户感到别扭。另一方面则是效率方面的。
其实前者是比较容易解决的,最简单的办法是为每个目标程序准备一个脚本,例如为notepad.exe准备一个脚本notepad,其内容可以是类似于这样:
[code]#! /bin/sh
wine-preloader wine-kthread notepad.exe[/code]
当然,这样一来效率比使用工具wine更低了,但是这毕竟是在人机界面上。
更好的办法当然是从内核着手。比较简单的办法是让内核在检测到目标映像是PE格式时把命令行“notepad …”改成“wine-preloader wine-kthread notepad.exe …”,然后实际装入并启动wine-preloader的映像(这是ELF格式的映像)。这样效率比前者高一些,但还是多转了一道弯,所以还可以改进。再进一步,可以在内核中先为PE映像保留好空间,而跳过wine-preloader。
如果说通过键盘命令启动Windows软件毕竟是人机交互,效率低一些也关系不大;那么通过CreateProcessW()创建新进程的过程就比较敏感了。对于经常要创建新进程的应用,这种效率上的降低可能是很容易被觉察的。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -