?? 漫談兼容內核之七:wine的二進制映像裝入和啟動.txt
字號:
漫談兼容內核之七: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 + -