Linux相容核心項目官方網站: http://linux.insigma.com.cn
Linux相容核心項目自由論壇: http://linux.insigma.com.cn/devbbs/index.asp
Linux相容核心項目QQ討論群: 15340505
漫談相容核心之一:ReactOS怎樣實現系統調用
毛德操
有網友在論壇上發貼,要求我談談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()的介面定義:
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中:
BOOL WINAPI ReadFile(HANDLE, PVOID, DWORD, PDWORD, LPOVERLAPPED);
顯然,這二者實際上是相同的(要不然就不相容了)。當然,微軟沒有公開這個函數的代碼,但是ReactOS為之提供了一個開源的實現,其代碼在reactos/lib/kernel32/file/rw.c中。
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);
}
我們在這裡只關心NtReadFile(),所以略去了別的代碼。
如前所述,NtReadFile()是Windows的一個系統調用,核心中有個函數就叫NtReadFile(),它的實現在ntoskrnl.exe中(這是Windows核心的核心部分),這也可以用depends.exe開啟ntoskrnl.exe察看。ReactOS代碼中對核心功能NtReadFile()的定義在reactos/include/ntos/zw.h中,同樣的定義也出現在reactos/w32api/include/ddk/winddk.h中:
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
);
而相應的實現則在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中:
__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
}
}
原來,使用者空間也有一個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中,下面是一個片斷:
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);
……
}
代碼中的’/t’表示TAB字元,讀者閱讀這段代碼應該沒有什麼問題。這段代碼根據name、nr_args、sys_call_idx等參數為給定系統調用產生stub函數的彙編代碼。那麼這些參數從何而來呢?在ReactOS代碼的reactos/tools/nci目錄下有個檔案sysfuncs.lst,下面是從這個檔案中摘出來的幾行:
NtAcceptConnectPort 6
NtAccessCheck 8
NtAccessCheckAndAuditAlarm 11
NtAddAtom 3
……
NtClose 1
……
NtReadFile 9
……
這裡的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中:
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);
set_system_call_gate(0x2e,(int)KiSystemService);
}
顯然,int 0x2e的向量指向KiSystemService()。
ReactOS在其核心功能的命名和定義上也力求與Windows一致,所以ReactOS核心中也有首碼為ke和ki的函數。首碼ke表示屬於“核心”模組。注意Windows所謂的“核心(kernel)”模組只是核心的一部分,而不是整個核心,這一點我以後在“漫談Wine”中還要講到。而首碼ki,則是指核心中與中斷響應和處理有關的函數。KiSystemService()是一段組譯工具,其作用相當於Linux核心中的system_call(),這段代碼在reactos/ntoskrnl/ke/i386/syscall.S中。限於篇幅,我在這篇短文中就不詳細講解這個函數的全部代碼了,而只是分段對一些要緊的關節作些說明。一般而言,能讀懂Linux核心中system_call()那段代碼的讀者應該能至少大體上讀懂這個函數。
_KiSystemService:
/*
* Construct a trap frame on the stack.
* The following are already on the stack.
*/
// SS + 0x0
// ESP + 0x4
// EFLAGS + 0x8
// CS + 0xC
// EIP + 0x10
pushl $0 // + 0x14
pushl %ebp // + 0x18
pushl %ebx // + 0x1C
pushl %esi // + 0x20
pushl %edi // + 0x24
pushl %fs // + 0x28
/* Load PCR Selector into fs */
movw $PCR_SELECTOR, %bx
movw %bx, %fs
/* Save the previous exception list */
pushl %fs:KPCR_EXCEPTION_LIST // + 0x2C
/* Set the exception handler chain terminator */
movl $0xffffffff, %fs:KPCR_EXCEPTION_LIST
/* Get a pointer to the current thread */
movl %fs:KPCR_CURRENT_THREAD, %esi
前面的一些指令主要是在儲存現場,類似於Linux核心中的宏操作SAVE_ALL。這裡關鍵的一步是從%fs:KPCR_CURRENT_THREAD這個地址取得當前線程的指標並將其存放在寄存器%esi中。每個線程在核心中都有個KTHREAD資料結構,某種意義上相當於Linux核心中的“進程式控制制塊”、即task_struct。Windows核心中也有“進程式控制制塊”,但只是相當於把進程內各線程所共用的資訊剝離了出來,而“線程式控制制塊”則起著更重要的作用。所謂當前線程的指標,就是指向當前線程的KTHREAD資料結構的指標。當核心調度一個線程運行時,就將其KTHREAD資料結構的地址存放在%fs:KPCR_CURRENT_THREAD這個地址中,而(CPU在系統空間的)%fs的值則又固定存放在PCR_SELECTOR這個地址中(定義為0x30)。附帶提一下,Win2k核心把%fs:0映射到線性地址0xffdff000(見“Secrets”一書p428)。
總之,從現在起,寄存器%esi就指向了當前線程的KTHREAD資料結構。那麼這一步對於系統調用為什麼重要呢?我們看一下這個資料結構中的幾個成分就可以明白:
typedef struct _KTHREAD
{
/* For waiting on thread exit */
DISPATCHER_HEADER DispatcherHeader; /* 00 */
……
SSDT_ENTRY *ServiceTable; /* DC */
……
UCHAR PreviousMode; /* 137 */
……
} KTHREAD;
每個成分後面的注釋說明這個成分在資料結構中以位元組為單位的相對位移,例如指標ServiceTable的相對位移就是0xdc。事實上,這個指標正是我們此刻最為關注的,因為它直接與系統調用的函數跳轉表有關。每個線程的這個指標都指向一個SSDT_ENTRY結構數組。既然每個線程都有這麼個指標,就說明每個線程都可以有自己的ServiceTable。不過,實際上每個線程的ServiceTable通常都指向同一個結構數組,我們等一下再來看這個結構數組,現在先往下看代碼。
/* Save the old previous mode */
pushl %ss:KTHREAD_PREVIOUS_MODE(%esi) // + 0x30
/* Set the new previous mode based on the saved CS selector */
movl 0x24(%esp), %ebx
andl $1, %ebx
movb %bl, %ss:KTHREAD_PREVIOUS_MODE(%esi)
/* Save other registers */
pushl %eax // + 0x34
pushl %ecx // + 0x38
pushl %edx // + 0x3C
pushl %ds // + 0x40
pushl %es // + 0x44
pushl %gs // + 0x48
sub $0x28, %esp // + 0x70
#ifdef DBG
……
#else
pushl 0x60(%esp) /* DebugEIP */ // + 0x74
#endif
pushl %ebp /* DebugEBP */ // + 0x78
/* Load the segment registers */
sti
movw $KERNEL_DS, %bx
movw %bx, %ds
movw %bx, %es
/* Save the old trap frame pointer where EDX would be saved */
movl KTHREAD_TRAP_FRAME(%esi), %ebx
movl %ebx, KTRAP_FRAME_EDX(%esp)
/* Allocate new Kernel stack frame */
movl %esp,%ebp
/* Save a pointer to the trap frame in the TCB */
movl %ebp, KTHREAD_TRAP_FRAME(%esi)
CheckValidCall:
#ifdef DBG
……
#endif
/*
* Find out which table offset to use. Converts 0x1124 into 0x10.
* The offset is related to the Table Index as such: Offset = TableIndex x 10
*/
movl %eax, %edi
shrl $8, %edi
andl $0x10, %edi
movl %edi, %ecx
/* Now add the thread's base system table to the offset */
addl KTHREAD_SERVICE_TABLE(%esi), %edi
這裡我們關注的是最後這一小段。首先,KTHREAD_SERVICE_TABLE(%esi)就是當前線程的ServiceTable指標。常數KTHREAD_SERVICE_TABLE定義為0xdc:
#define KTHREAD_SERVICE_TABLE 0xDC
這跟前面KTHREAD資料結構的定義顯然是一致的。
上面講過,實際上一般情況下所有線程的ServiceTable指標都指向同一個結構數組,那就是KeServiceDescriptorTable[ ]:
SSDT_ENTRY
__declspec(dllexport)
KeServiceDescriptorTable[SSDT_MAX_ENTRIES] = {
{ MainSSDT, NULL, NUMBER_OF_SYSCALLS, MainSSPT },
{ NULL, NULL, 0, NULL },
{ NULL, NULL, 0, NULL },
{ NULL, NULL, 0, NULL }
};
這個數組的大小一般是4,但是只用了前兩個元素。這裡只用了第一個元素,這就是常規Windows系統調用的跳轉表。
我以前曾經談到,Windows在發展的過程中把許多原來實現於使用者空間的功能(主要是圖形介面操作)移到了核心中,成為一個核心模組win32k.sys,並相應地增加了一組“擴充系統調用”。這個數組的第二個元素就是為擴充系統調用準備的,但是在原始碼中這個元素是空的,這是因為win32k.sys可以動態安裝,安裝了以後才把具體的資料結構指標填寫進去。擴充系統調用與常規系統調用的區別是:前者的系統調用號均大於等於0x1000,而後者則小於0x1000。顯然,核心需要根據具體的系統調用號來確定應該使用哪一個跳轉表,或者說上述數組內的哪一個元素。每個元素的大小是16個位元組,所以只要根據具體的系統調用號算出一個相對位移量,就起到了選擇使用跳轉表的作用。具體地,如果算得的位移量是0,那就是使用常規跳轉表,而若是0x10就是使用擴充跳轉表。
上面的代碼中正是這樣做的。把系統調用號的副本(在%edi中)右移8位,再跟0x10相與,就起到了這個效果。於是,指令“addl KTHREAD_SERVICE_TABLE(%esi), %edi”就使寄存器%edi指向了應該使用的跳轉表結構,即SSDT_ENTRY資料結構。代碼的作者加了個注釋,說是“把0x1124轉換成0x10”,其意思實際上是:“如果系統調用號是0x1124,那麼計算出來的相對位移是0x10”;後面一句說的是“相對位移 = 數組下標乘上0x10”。
SSDT_ENTRY資料結構中的第三個成分,即相對位移為8之處是個整數,說明在函數跳轉表中有幾個指標,也即所允許的最大系統調用號。對於常規系統調用,這個整數是NUMBER_OF_SYSCALLS,在ReactOS的代碼中定義為232,比Win2K略少一些。
我們繼續往下看代碼:
/* Get the true syscall ID and check it */
movl %eax, %ebx
andl $0x0FFF, %eax
cmpl 8(%edi), %eax
/* Invalid ID, try to load Win32K Table */
jnb KiBBTUnexpectedRange
/* Users's current stack frame pointer is source */
movl %edx, %esi
/* Allocate room for argument list from kernel stack */
movl 12(%edi), %ecx
movb (%ecx, %eax), %cl
movzx %cl, %ecx
/* Allocate space on our stack */
subl %ecx, %esp
正如代碼中的注釋所說,開始是檢查系統調用號是否在合法範圍之內,這裡比較的對象顯然就是NUMBER_OF_SYSCALLS。
前面講過,寄存器%edx指向使用者空間堆棧上的函數調用架構,實際上就是指向所傳遞的參數,現在把這個指標複製到%esi中,這是在為從使用者空間堆棧複製參數做準備。但是,光有複製的起點還不夠,還需要有複製的長度(位元組數)、即參數的個數乘4,所以需要知道具體的系統調用有幾個參數。這個資訊儲存在一個以系統調用號為下標的無符號位元組數組中(所以每個系統調用的參數總長度不能超過255位元組),SSDT_ENTRY資料結構中的第三個成分(相對位移為12、或0xc)就是指向這個數組的指標。對於常規系統調用,這個數組是MainSSPT。可想而知,這個數組的內容也應來自sysfuncs.lst。代碼中先讓%ecx指向MainSSPT,再以%eax中的系統調用號與其相加,就使其指向了數組中的相應元素,而movb指令就把這個位元組取了出來。所以,最後%ecx持有給定系統調用的參數複製長度。從%esp的內容中減去%ecx的內容,就在系統空間堆棧上保留了若干位元組,其長度等於參數複製長度,這樣就為把參數從使用者空間堆棧複製到系統空間堆棧做好了準備。再往下看:
/* Get pointer to function */
movl (%edi), %edi
movl (%edi, %eax, 4), %eax
/* Copy the arguments from the user stack to our stack */
shr $2, %ecx
movl %esp, %edi
cld
rep movsd
/* Do the System Call */
call *%eax
movl %eax, KTRAP_FRAME_EAX(%ebp)
/* Deallocate the kernel stack frame */
movl %ebp, %esp
前面,寄存器%edi已經指向常規系統調用的SSDT_ENTRY資料結構,也就是指向了該資料結構中的第一個成分。SSDT_ENTRY資料結構的第一個成分是個指標,指向一個函數指標數組。對於常規系統調用,這就是MainSSDT。指令“movl (%edi), %edi”把%edi所指處的內容賦給了%edi,使原來指向這個指標的%edi現在指向了MainSSDT。這也是個以系統調用號為下標的數組,其定義為:
SSDT MainSSDT[] = {
{ (ULONG)NtAcceptConnectPort },
{ (ULONG)NtAccessCheck },
{ (ULONG)NtAccessCheckAndAuditAlarm },
……
{ (ULONG)NtReadFile },
……
}
在我們這個例子中,指令“movl (%edi, %eax, 4), %eax”,即“把%edi加相對位移為‘系統調用號乘4’之處的內容裝入%eax”,使%eax指向了NtReadFile()。然後就是把參數從使用者空間堆棧拷貝到系統空間堆棧,注意%ecx中的長度是以位元組為單位的,所以要右移兩位變成以長字為單位。
最後,指令“call *%eax”就使CPU進入了核心裡面的NtReadFile(),其代碼在reactos/ntoskrnl/io/rw.c中。如果按Linux的規矩,這應該是sys_NtReadFile():
NTSTATUS STDCALL
NtReadFile (IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL)
{
……
}
這個函數的調用介面與應用程式在使用者空間進行這個系統調用時所遵循的介面完全相同,而應用程式壓入使用者空間堆棧的9個參數已經被拷貝到了系統空間堆棧中合適的位置上。於是,對於這個函數而言,就好像其調用者、在我們這個情景中是ReadFile()、就在系統空間中一樣。
回到上面的彙編代碼中。當CPU從目標函數返回時,寄存器%eax持有該函數的傳回值,這是要返回給使用者空間的,所以把它儲存在堆棧架構中。
下面就是從核心返回到使用者空間的過程,我把代碼留給讀者自己研究。不過需要給一點提示:
(1).代碼中的APC指“非同步程序呼叫(Asynchronous Procedure Call)”,相當於Linux中的Signal。
(2).Windows把核心的運行狀態分成若干層級。最高的一些層級是不允許硬體中斷(不允許層級更低的硬體中斷);其次(2級和1級)是不允許進程調度(但是允許硬體中斷),DPC(2級,相當於bh函數)和APC(1級,相當於signal)都應該在禁止調度的條件下執行;最低(0級)就是允許進程調度。
(3).從核心中也可以通過_KiSystemService()進行系統調用(不過要經過一個核心版本的stub函數),所以代碼中需要檢測和區分CPU進入_KiSystemService()之前的運行模式,並且線程的KTHREAD資料結構中也有個成分PreviousMode,用來儲存這個資訊。而KTHREAD_PREVIOUS_MODE(%esi)就指向當前進程的PreviousMode。
KeReturnFromSystemCall:
/* Get the Current Thread */
movl %fs:KPCR_CURRENT_THREAD, %esi
/* Restore the old trap frame pointer */
movl KTRAP_FRAME_EDX(%esp), %ebx
movl %ebx, KTHREAD_TRAP_FRAME(%esi)
_KiServiceExit:
/* Get the Current Thread */
cli
movl %fs:KPCR_CURRENT_THREAD, %esi
/* Deliver APCs only if we were called from user mode */
testb $1, KTRAP_FRAME_CS(%esp)
je KiRosTrapReturn
/* And only if any are actually pending */
cmpb $0, KTHREAD_PENDING_USER_APC(%esi)
je KiRosTrapReturn
/* Save pointer to Trap Frame */
movl %esp, %ebx
/* Raise IRQL to APC_LEVEL */
movl $1, %ecx
call @KfRaiseIrql@4
/* Save old IRQL */
pushl %eax
/* Deliver APCs */
sti
pushl %ebx
pushl $0
pushl $UserMode
call _KiDeliverApc@12
cli
/* Return to old IRQL */
popl %ecx
call @KfLowerIrql@4
KiRosTrapReturn:
/* Skip debug information and unsaved registers */
addl $0x30, %esp // + 0x48
popl %gs // + 0x44
popl %es // + 0x40
popl %ds // + 0x3C
popl %edx // + 0x38
popl %ecx // + 0x34
popl %eax // + 0x30
/* Restore the old previous mode */
popl %ebx // + 0x2C
movb %bl, %ss:KTHREAD_PREVIOUS_MODE(%esi)
/* Restore the old exception handler list */
popl %fs:KPCR_EXCEPTION_LIST // + 0x28
/* Restore final registers from trap frame */
popl %fs // + 0x24
popl %edi // + 0x20
popl %esi // + 0x1C
popl %ebx // + 0x18
popl %ebp // + 0x14
add $4, %esp // + 0x10
/* Check if previous CS is from user-mode */
testl $1, 4(%esp)
/* It is, so use Fast Exit */
jnz FastRet
/*
* Restore what the stub pushed, and return back to it.
* Note that we were CALLed, so the first thing on our stack is the ret EIP!
*/
pop %edx // + 0x0C
pop %ecx // + 0x08
popf // + 0x04
jmp *%edx
IntRet:
iret
FastRet:
/* Is SYSEXIT Supported/Wanted? */
cmpl $0, %ss:_KiFastSystemCallDisable
jnz IntRet
……
熟悉Linux的讀者知道CPU在返回使用者空間之前應該調用有關進程(線程)調度的函數,因而會期待在這段代碼中也看到這樣的操作,然而卻沒有看到。但是實際上確實有這樣的操作,只不過是深藏在函數KfLowerIrql()裡面而已。
搞懂了這個函數的讀者現在應該知道我們將要怎樣做了。不過,我們的目標不是把KiSystemService()與Linux的system_call()堆積、並列在一起,而是要把前者溶入到後者中去。再說,即使照搬了KiSystemService(),總不能因為這個程式調用了KfLowerIrql(),就又照搬KfLowerIrql()吧。如果按這樣類推,那就勢必要把整個ReactOS核心堆積到Linux核心中去了。由此可見,我們既要參考、借鑒ReactOS核心的實現,又要研究怎樣把它融合、嫁接到Linux核心中去,這當然是一項富有挑戰性的工作。
下載文章