?? 漫談兼容內核之一:reactos怎樣實現系統調用.txt
字號:
漫談兼容內核之一:ReactOS怎樣實現系統調用
[align=center][size=5][b]漫談兼容內核之一:ReactOS怎樣實現系統調用[/b][/size][/align]
[align=center] 毛德操[/align]
有網友在論壇上發貼,要求我談談ReactOS是怎樣實現系統調用的。另一方面,我上次已經談到兼容內核應該如何實現Windows系統調用的問題,接著談談ReactOS怎樣實現系統調用倒也順理成章,所以這一次就來談談這個話題。不過這顯然不屬于“漫談Wine”的范疇,也確實沒有必要再來個“漫談ReactOS”,因此決定把除Wine以外的話題都納入“漫談兼容內核”。
ReactOS這個項目的目標是要開發出一個開源的Windows。不言而喻,它要實現的系統調用就是Windows的那一套系統調用,也就是要忠實地實現Windows系統調用界面。本文要說的不是Windows系統調用界面本身,而是ReactOS怎樣實現這個界面,主要是說說用戶空間的應用程序怎樣進入/退出內核、即系統空間,怎樣調用定義于這個界面的函數。實際上,ReactOS正是通過“int 0x2e”指令進入內核、實現系統調用的。雖然ReactOS并不是Windows,它的作者們也未必看到過Windows的源代碼;但是我相信,ReactOS的代碼、至少是這方面的代碼,與“正本”Windows的代碼應該非常接近,要有也只是細節上的差別。
下面以系統調用NtReadFile()為例,按“自頂向下”的方式,一方面說明怎樣閱讀ReactOS的代碼,一方面說明ReacOS是怎樣實現系統調用的。
首先,Windows應用程序應該通過Win32 API調用這個接口所定義的庫函數,這些庫函數基本上都是在“動態連接庫”、即DLL中實現的。例如,ReadFile()就是在Win32 API中定義的一個庫函數。實現這個庫函數的可執行程序在Windows的“系統DLL”之一kernel32.dll中,有興趣的讀者可以在Windows上用一個工具depends.exe打開kernel32.dll,就可以看到這個DLL的導出函數表中有ReadFile()。另一方面,在微軟的VC開發環境(Visual Studio)中、以及Win2k DDK中,都有個“頭文件”winbase.h,里面有ReadFile()的接口定義:
[code]
WINBASEAPI
BOOL
WINAPI
ReadFile(
IN HANDLE hFile,
OUT LPVOID lpBuffer,
IN DWORD nNumberOfBytesToRead,
OUT LPDWORD lpNumberOfBytesRead,
IN LPOVERLAPPED lpOverlapped
);
函數名前面的關鍵詞WINAPI表示這是個定義于Win32 API的函數。
在ReactOS的代碼中同樣也有winbase.h,這在目錄reactos/w32api/include中:
[code]
BOOL WINAPI ReadFile(HANDLE, PVOID, DWORD, PDWORD, LPOVERLAPPED);[/code]
[/code]
顯然,這二者實際上是相同的(要不然就不兼容了)。當然,微軟沒有公開這個函數的代碼,但是ReactOS為之提供了一個開源的實現,其代碼在reactos/lib/kernel32/file/rw.c中。
[code]
BOOL STDCALL
ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverLapped )
{
……
errCode = NtReadFile(hFile,
hEvent,
NULL,
NULL,
IoStatusBlock,
lpBuffer,
nNumberOfBytesToRead,
ptrOffset,
NULL);
……
return(TRUE);
}
[/code]
我們在這里只關心NtReadFile(),所以略去了別的代碼。
如前所述,NtReadFile()是Windows的一個系統調用,內核中有個函數就叫NtReadFile(),它的實現在ntoskrnl.exe中(這是Windows內核的核心部分),這也可以用depends.exe打開ntoskrnl.exe察看。ReactOS代碼中對內核函數NtReadFile()的定義在reactos/include/ntos/zw.h中,同樣的定義也出現在reactos/w32api/include/ddk/winddk.h中:
[code]
NTSTATUS
STDCALL
NtReadFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE UserApcRoutine OPTIONAL,
IN PVOID UserApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG BufferLength,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL
);
[/code]
而相應的實現則在reactos/ntoskrnl/io/rw.c中。
表面上看這似乎挺正常,ReadFile()調用NtReadFile(),reactos/ntoskrnl/io/rw.c則為其提供了被調用的NtReadFile()。可是仔細一想就不對了。這ReadFile()是在用戶空間運行的,而reactos/ntoskrnl/io/rw.c中的代碼卻是在內核中,是在系統空間。難道用戶空間的程序竟能如此這般地直接調用內核中的函數嗎?如果那樣的話,那還要什么陷阱門、調用門這些機制呢?再說,編譯的時候又怎樣把它們連接起來呢?
這么一想,就可以斷定這里面另有奧妙。仔細一查,原來還另有一個NtReadFile(),在msvc6/iface/native/syscall/Debug/zw.c中:
[code]
__declspec(naked) __stdcall
NtReadFile(int dummy0, int dummy1, int dummy2)
{
__asm {
push ebp
mov ebp, esp
mov eax,152
lea edx, 8[ebp]
int 0x2E
pop ebp
ret 9
}
}
[/code]
原來,用戶空間也有一個NtReadFile(),正是這個函數在執行自陷指令“int 0x2e”。我們看一下這段匯編代碼。這里面的152就是NtReadFile()這個系統調用的調用號,所以當CPU自陷進入系統空間后寄存器eax持有具體的系統調用號。而寄存器edx,在執行了lea這條指令以后,則持有CPU在調用這個函數前夕的堆棧指針,實際上就是指向堆棧中調用參數的起點。在進行系統調用時如何傳遞參數這個問題上,Windows和Linux有著明顯的差別。我們知道,Linux是通過寄存器傳遞參數的,好處是效率比較高,但是參數的個數受到了限制,所以Linux系統調用的參數都很少,真有大量參數需要傳遞時就把它們組裝在數據結構中,而只傳遞數據結構指針。而Windows則通過堆棧傳遞參數。讀者在上面看到,ReadFile()在調用NtReadFile()時有9個參數,這9個參數都被壓入堆棧,而edx就指向堆棧中的這些參數的起點(地址最低處)。我們在這個函數中沒有看到對通過堆棧傳下來的參數有什么操作,也沒有看到往堆棧里增加別的參數,所以傳下來的9個參數被原封不動地傳了下去(作為int 0x2e自陷的參數)。這樣,當CPU自陷進入內核以后,edx仍指向用戶空間堆棧中的這些參數。當然,CPU進入內核以后的堆棧是系統空間堆棧,而不是用戶空間堆棧,所以需要用copy_from_user()一類的函數把這些參數從用戶空間拷貝過來,此時edx的值就可用作源指針。至于寄存器ebp,則用作調用這個函數時的“堆??蚣堋敝羔槨? 當內核完成了具體系統調用的操作,CPU返回到用戶空間時,下一條指令是“pop ebp”,即恢復上一層函數的堆??蚣苤羔?。然后,指令“ret 9”使CPU返回到上一層函數,同時調整堆棧指針,使其跳過堆棧上的9個調用參數。在“正宗”的x86匯編語言中,用在ret指令中的數值以字節為單位,所以應該是“ret 24h”,而這里卻是以4字節長字為單位,這顯然是因為用了不同的匯編工具。
子程序的調用者可以把參數壓入堆棧,通過堆棧把參數傳遞給被調用者??墒?,當CPU從子程序返回時,由誰負責從堆棧中清除這些參數呢?顯然,要么就是由調用者負責,要么就是由被調用者負責,這里需要有個約定,使得調用者和被調用者取得一致。在上面NtReadFile()這個函數中,我們看到是由被調用者負起了這個責任、在調整堆棧指針。函數代碼前面的__stdcall就說明了這一點。同樣,在.h文件中對NtReadFile()的定義(申明)之前也加上了STDCALL,也是為了說明這個約定。“Undocumented Windows 2000 Secrets”這本書中(p51-53)對類似的各種約定有一大段說明,讀者可以參考。另一方面,在上面這個函數的代碼中,函數的調用參數是3個而不是9個。但是看一下代碼就可以知道這些參數根本就沒有被用到,而調用者、即前面的ReadFile()、也是按9個參數來調用NtReadFile()的。所以,這里的三個參數完全是虛設的,有沒有、或者有幾個、都無關緊要,難怪代碼中稱之為“dummy”。
用戶空間的這個NtReadFile()向上代表著內核函數NtReadFile(),向下則代表著想要調用內核函數NtReadFile()的那個函數,在這里是ReadFile();但是它本身并不提供什么附加的功能,這樣的中間函數稱為“stub”。
當然,ReactOS的這種做法很容易把讀者引入迷茫。相比之下,Linux的做法就比較清晰,例如應用程序調用的是庫函數write(),而內核中與之對應的函數則是sys_write()。
那么為什么ReactOS要這么干呢?我只能猜測:
(1).Windows的源代碼中就是這樣,例如用depends.exe在ntdll.dll和ntoskrnl.exe中都可看到有名為NtReadFile()的函數,而ReactOS的人就依葫蘆畫瓢。
(2).作為一條開發路線,ReactOS可能在初期不劃分用戶空間和系統空間,所有的代碼全在同一個空間運行,所以應用程序可以直接調用內核中的函數。這樣,例如對文件系統的開發就可以簡單易行一些。然后,到一些主要的功能都開發出來以后,再來劃分用戶空間和系統空間,并且補上如何跨越空間這一層。從zw.c這個文件在native/syscall/Debug目錄下這個跡象看,ReactOS似乎正處于走出這一步的過程中。
(3).ReactOS的作者們可能有意讓它也可以用于嵌入式系統。嵌入式系統往往不劃分用戶空間和系統空間,而把應用程序和內核連接在同一個可執行映像中。這樣,如果需要把代碼編譯成一個嵌入式系統,就不使用stub;而若要把代碼編譯成一個桌面系統,則可以在用戶空間加上stub并在內核中加上處理自陷指令“int 0x2e”的程序。
在Windows中,stub函數NtReadFile()在ntdll.dll中。實際上,所有0x2e系統調用的stub函數都在這個DLL中。顯然,所有系統調用的stub函數具有相同的樣式,不同的只是系統調用號和參數的個數,所以ReactOS用一個工具來自動生成這些stub函數。這個工具的代碼在msvc6/iface/native/genntdll.c中,下面是一個片斷:
[code]
void write_syscall_stub(FILE* out, FILE* out3, char* name, char* name2,
char* nr_args, unsigned int sys_call_idx)
{
int i;
int nArgBytes = atoi(nr_args);
#ifdef PARAMETERIZED_LIBS
……
#else
fprintf(\"\\n\\t.global _%s\\n\\t\"\n",name);
fprintf(out,"\".global _%s\\n\\t\"\n",name2);
fprintf(out,"\"_%s:\\n\\t\"\n",name);
fprintf(out,"\"_%s:\\n\\t\"\n",name2);
#endif
fprintf(out,"\t\"pushl\t%%ebp\\n\\t\"\n");
fprintf(out,"\t\"movl\t%%esp, %%ebp\\n\\t\"\n");
fprintf(out,"\t\"mov\t$%d,%%eax\\n\\t\"\n",sys_call_idx);
fprintf(out,"\t\"lea\t8(%%ebp),%%edx\\n\\t\"\n");
fprintf(out,"\t\"int\t$0x2E\\n\\t\"\n");
fprintf(out,"\t\"popl\t%%ebp\\n\\t\"\n");
fprintf(out,"\t\"ret\t$%s\\n\\t\");\n\n",nr_args);
……
}
[/code]
代碼中的’\t’表示TAB字符,讀者閱讀這段代碼應該沒有什么問題。這段代碼根據name、nr_args、sys_call_idx等參數為給定系統調用生成stub函數的匯編代碼。那么這些參數從何而來呢?在ReactOS代碼的reactos/tools/nci目錄下有個文件sysfuncs.lst,下面是從這個文件中摘出來的幾行:
[code]
NtAcceptConnectPort 6
NtAccessCheck 8
NtAccessCheckAndAuditAlarm 11
NtAddAtom 3
……
NtClose 1
……
NtReadFile 9
……
[/code]
這里的NtAcceptConnectPort就是調用號為0的系統調用NtAcceptConnectPort(),它有6個參數。另一個系統調用NtClose()只有1個參數。而NtReadFile()有9個參數,并且正好是這個表中的第153行,所以調用號是152。
用戶空間的程序一執行int 0x2e,CPU就自陷進入了系統空間。其間的物理過程這里就不多說了,有需要的讀者可參考“情景分析”或其它有關資料。我這里就從CPU怎樣進入int 0x2e的自陷處理程序說起。
像別的中斷向量一樣,ReactOS在其初始化程序KeInitExceptions()中設置了int 0x2e的向量,這個函數的代碼在reactos/ntoskrnl/ke/i386/exp.c中:
[code]
VOID INIT_FUNCTION
KeInitExceptions(VOID)
/*
* FUNCTION: Initalize CPU exception handling
*/
{
……
set_trap_gate(0, (ULONG)KiTrap0, 0);
set_trap_gate(1, (ULONG)KiTrap1, 0);
set_trap_gate(2, (ULONG)KiTrap2, 0);
set_trap_gate(3, (ULONG)KiTrap3, 3);
……
set_system_call_gate(0x2d,(int)interrupt_handler2d);
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -