⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄

📁 兼容内核漫谈 适合想将Windows上的程序移植到其它平台上的朋友研究查看
💻
📖 第 1 页 / 共 4 页
字号:
漫谈兼容内核之七:Wine的二进制映像装入和启动

[b][size=4][align=center]漫谈兼容内核之七:Wine的二进制映像装入和启动[/align][/size][/b]
[align=center]毛德操[/align]

    上一篇漫谈中介绍了几种二进制可执行映像的识别方法,而识别的目的当然是为了要装入并启动这些映像的执行。映像的装入和启动一般总是和创建进程相连系,所以本来就是个相当复杂的过程。而对于Wine,则在进程创建方面又增添了一些额外的复杂性。
    为什么呢?我们这样来考虑:在Windows或ReactOS中,创建进程是由CreateProcessW()完成的,系统中的“始祖”进程就是个Windows进程,代代相传下来,总是由Windows进程创建Windows进程,所以映像的装入和启动只发生在CreateProcessW()中,所装入的也总是Windows或DOS上的二进制映像。而在Linux中,所谓“创建进程”实际上是将一个线程转化成进程,这是由execve()一类的系统调用完成的,那也只是Linux进程的代代相传,所装入的也总是Linux上的二进制映像,包括a.out和ELF映像。
    可是Wine就不同了。Wine是在Linux内核上运行,系统里的“始祖”进程是Linux进程,但是却需要由作为其后代的Linux进程创建出Windows进程来。另一方面,创建出来的Windows进程则有可能通过CreateProcessW()再创建新一代的Windows进程(实际上Wine还允许Windows进程创建Linux进程,这我们就不说了)。
    兼容内核则与Wine相似,也有“从Linux到Windows”和“从Windows到Windows”两种不同的进程创建。那么,在这两种条件下的映像装入是否也有明显的区别呢?就Wine而言,这两种进程创建(从而映像装入)都是在Linux环境下在内核外面实现,区别应该是不大的。至于真正的从Windows到Windows的进程创建和映像装入,则应该再到ReactOS中去寻找借鉴。所以我们最好要分别考察Wine和ReactOS两个系统的进程创建和映像装入。在这篇漫谈中我们先考察Wine系统中的映像装入,而Wine系统中的映像装入又分两种,一种是在Linux环境下通过键盘命令启动一个Windows应用程序,另一种是由一个Windows应用程序通过CreateProcessW()再创建一个Windows进程。

    应该说,Wine的映像装入和启动的过程是相当复杂的。要了解这个过程,我们不妨从一些装入工具着手,所以我们通过目录wine/loader下面的几个源码文件来说明问题。在这几个文件中,有两个.c文件是有main()函数的。一个是main.c,另一个是glibc.c。编译/连接以后,glibc.c中的main()进入工具wine,而main.c中的main()则分别进入wine-kthread和wine-pthread两个工具。在功能上wine-kthread和wine-pthread二者的作用是一样的,只是后者依靠程序库libpthread.a实现和管理线程,而前者则直接依靠内核实现和管理线程。另外还有一个文件preloader.c,虽然并不带有函数main(),但是编译/连接以后也成为一个可以独立运行的工具wine-preloader。

    我们先看由Linux的shell启动执行一个Windows应用软件的过程。
    实际上Windows应用并不是由Linux的Shell直接启动的,而是通过一条类似于“wine notpad.exe”的命令由Shell启动Wine的装入/启动工具wine,再由wine间接地装入/启动具体的应用程序。整个过程要依次经由wine、wine-preloader、以及wine-kthread或wine-pthread共三个工具软件的接力才能完成。我们就顺着这个轨迹从wine开始。
    可执行程序wine的入口main()在loader/glibc.c中(不是在main.c中!)。

[code]int main( int argc, char *argv[] )
{
    const char *loader = getenv( "WINELOADER" );
    const char *threads = get_threading();

    if (loader)
    {
        const char *path;
        char *new_name, *new_loader;

        if ((path = strrchr( loader, '/' ))) path++;
        else path = loader;

        new_name = xmalloc( (path - loader) + strlen(threads) + 1 );
        memcpy( new_name, loader, path - loader );
        strcpy( new_name + (path - loader), threads );

        /* update WINELOADER with the new name */
        new_loader = xmalloc( sizeof("WINELOADER=") + strlen(new_name) );
        strcpy( new_loader, "WINELOADER=" );
        strcat( new_loader, new_name );
        putenv( new_loader );
        wine_exec_wine_binary( new_name, argv, NULL, TRUE );
    }
    else
    {
        wine_init_argv0_path( argv[0] );
        wine_exec_wine_binary( threads, argv, NULL, TRUE );
    }
    fprintf( stderr, "wine: could not exec %s\n", argv[0] );
    exit(1);
}[/code]
    先由getenv()检查是否已经设置了环境变量WINELOADER,如已设置则getenv()返回其定义,否则返回0。下面就可看到,检查的目的其实只是为了将其设置正确。接着的get_threading()则是为了确定是否在使用libpthread,从而确定下一步应该使用wine-kthread还是wine-pthread。这二者之间的选择与线程的实现模式有关,pthread中的’p’表示Posix,而kthread中的’k’表示Kernel。不过这是个专门的话题,在这里就不深入下去了,只是说明我们一般都使用kthread,因此这个函数一般返回字符串“wine-kthread”,于是字符串threads的值就是“wine-kthread”。
    如果环境变量WINELOADER有定义,即loader非0,就要把该变量的值设置成threads的值。但是WINELOADER的值很可能包含着整个路径,所以需要先进行一些字符串的处理,把threads的值与原来的目录路径拼接在一起。然后就是关键所在、即对于函数wine_exec_wine_binary()的调用了。
    如果环境变量WINELOADER没有定义,那就不需要去设置它了。但是这里通过wine_init_argv0_path()设置了argv0_path和argv0_name两个静态变量,然后也是对函数wine_exec_wine_binary()的调用。
    总之,wine_exec_wine_binary()是这里的关键。读者下面就会看到,对这个函数的调用要是成功就不会返回。此外,注意在这里调用这个函数时的最后一个参数都是TRUE。

[code][main() > wine_exec_wine_binary()]

/* exec a wine internal binary (either the wine loader or the wine server) */
void wine_exec_wine_binary( const char *name, char **argv, char **envp, int use_preloader )
{
    const char *path, *pos, *ptr;

    if (name && strchr( name, '/' ))
    {
        argv[0] = (char *)name;
        preloader_exec( argv, envp, use_preloader );
        return;
    }
    else if (!name) name = argv0_name;

    /* first, try bin directory */
    argv[0] = xmalloc( sizeof(BINDIR "/") + strlen(name) );
    strcpy( argv[0], BINDIR "/" );
    strcat( argv[0], name );
    preloader_exec( argv, envp, use_preloader );
    free( argv[0] );

    /* now try the path of argv0 of the current binary */
    . . . . . .

    /* now search in the Unix path */
    . . . . . .
}[/code]
    在我们这个情景中,这里的参数name是“wine-kthread”,参数use_preloader是TRUE,而argv[]数组中各项则依次是“wine”和“notepad.exe”,相当于命令行“wine notepad.exe”。这里实质性的操作显然是preloader_exec()。但是在调用preloader_exec()之前把argv[0]换成了“wine-kthread”。这样,对于传给preloader_exec()的argv[],与其相当的命令行就变成了“wine-kthread  notepad.exe”。当然,这是在为执行另一个工具wine-kthread作准备。
    如果原来的argv[0]是个路径,那就把程序名wine替换掉以后加以调用就是。而如果只是个程序名而并不包括目录路径的话,那就要反复尝试,首先是目录/usr/local/bin,然后是当前目录,在往下就是逐一尝试环境变量PATH中定义的各个路径。在正常的情况下,preloader_exec()是不返回的(所以wine_exec_wine_binary()不返回),如果返回就说明在给定的目录中找不到wine-kthread。所以,要是逐一尝试全都失败的话,wine_exec_wine_binary()就会返回而在main()中显示出错信息。
    接着往下看preloader_exec()的代码。

[code][main() > wine_exec_wine_binary() > preloader_exec()]

/* exec a binary using the preloader if requested; helper for wine_exec_wine_binary */
static void preloader_exec( char **argv, char **envp, int use_preloader )
{
#ifdef linux
    if (use_preloader)
    {
        static const char preloader[] = "wine-preloader";
        char *p, *full_name;
        char **last_arg = argv, **new_argv;

        if (!(p = strrchr( argv[0], '/' ))) p = argv[0];
        else p++;

        full_name = xmalloc( p - argv[0] + sizeof(preloader) );
        memcpy( full_name, argv[0], p - argv[0] );
        memcpy( full_name + (p - argv[0]), preloader, sizeof(preloader) );

        /* make a copy of argv */
        while (*last_arg) last_arg++;
        new_argv = xmalloc( (last_arg - argv + 2) * sizeof(*argv) );
        memcpy( new_argv + 1, argv, (last_arg - argv + 1) * sizeof(*argv) );
        new_argv[0] = full_name;
        if (envp) execve( full_name, new_argv, envp );
        else execv( full_name, new_argv );
        free( new_argv );
        free( full_name );
        return;
    }
#endif
    if (envp) execve( argv[0], argv, envp );
    else execv( argv[0], argv );
}[/code]
    当然,条件编译控制量linux是有定义的,而参数use_preloader则为TRUE。这里实质性的操作是系统调用execve()或execv(),视调用参数envp是否为非0、即是否需要传递环境变量而定。但是这是一段有点“奥妙”的代码。奥妙之处是把原来的argv[]扩大成了new_argv[],使原来的argv[0]变成了new_argv[1],而new_argv[0]则固定设置为“wine-preloader”,并保持原有的目录路径不变。由于传给execve()或execv()的是new_argv[],这就相当于在命令行“wine-kthread  wine notepad.exe”前面添上了一项,变成了这样(如果忽略目录路径):
    “wine-preloader  wine-kthread  wine notepad.exe”
    这就是说,本来是要启动装入工具wine-kthread,现在却变成了wine-preloader,而原来的命令行则变成了传给wine-preloader的命令行参数。
限于篇幅,这里就不介绍Linux内核如何装入ELF格式可执行映像的过程了。只是指出:wine-preloader是个ELF格式的可执行映像,但这是个特殊的ELF可执行映像。特殊在哪里呢?可以看一下Makefile中对于如何编译/连接这个程序的说明:

[code]wine-preloader: preloader.o Makefile.in
$(CC) -o $@ -static -nostartfiles -nodefaultlibs -Wl,-Ttext=0x78000000 preloader.o \
$(LIBPORT) $(LDFLAGS)[/code]
    注意这里的连接可选项-nostartfiles和-nodefaultlibs,这说明在连接时不使用通常都要使用的C程序库。那么,C程序库中都有些什么函数呢?有些是大家都很熟悉的。例如printf(),malloc()等等,再如C库对系统调用的包装open()、read()、 mmap()等等,就是大家所熟知的。但是还有一些则知道的人不多,现在就涉及到这些函数了。
    大家知道C程序的总入口是main(),可是这只是就程序设计而言。其实操作系统内核在装入可执行映像以后最初跳转进入的是一个名为_start()的函数。是这个函数为main()和其余目标程序的运行做好各种准备、即系统层次上的初始化,然后再调用main()的。不管用户程序是什么样,干什么事,这一部分的操作都是一样的,所不同的只是一些参数(例如程序段和数据段的大小和位置等等),而这些参数都存放在具体可执行映像的头部。这样,与此有关的代码就不需要由用户的程序员来反复地编写,而是统一放在C库中供大家连接使用。正因为如此,_start()对于一般的程序员是不可见的,因而也就不为人所知了。而main()之所以是“总入口”,只是因为标准C库中的有关程序总是在完成初始化以后调用main()。对于wine-preloader,上述可选项的作用就是告诉连接程序ld,让它别用C库,而由preloader.c自己来提供_start()。既然preloader.c中的_start()是自己写的,当然就有了自由度,而不必使用main()这个函数名了,这就是preloader.c中没有main()的原因。
    另一方面,对于映像装入内存后的地址也作了明文规定,就是从0x78000000开始。
    不光是wine-preloader特殊,wine-kthread和wine-pthread的编译/连接也有点特殊,它们的装入地址是可以浮动的。虽然它们各自都有个main(),实际上却接近于共享库、即.so模块。
    对于wine-preloader,我们从_start()开始看它的代码。这是一段汇编代码。

[code]__ASM_GLOBAL_FUNC(_start,
                  "\tmovl %esp,%eax\n"
                  "\tleal -128(%esp),%esp\n"  /* allocate some space for extra aux values */
                  "\tpushl %eax\n"           /* orig stack pointer */
                  "\tpushl %esp\n"           /* ptr to orig stack pointer */
                  "\tcall wld_start\n"
                  "\tpopl %ecx\n"            /* remove ptr to stack pointer */
                  "\tpopl %esp\n"            /* new stack pointer */
                  "\tpush %eax\n"           /* ELF interpreter entry point */
                  "\txor %eax,%eax\n"

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -