亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

? 歡迎來到蟲蟲下載站! | ?? 資源下載 ?? 資源專輯 ?? 關于我們
? 蟲蟲下載站

?? 漫談兼容內核之二十windows線程的系統空間堆棧.txt

?? 漫談系統內核內幕 收集得很辛苦 呵呵 大家快下在吧
?? TXT
?? 第 1 頁 / 共 4 頁
字號:
漫談兼容內核之二十:Windows線程的系統空間堆棧 毛德操 在計算機技術的發展史上,堆棧的發明有著劃時代的意義。從那以后,實際上已經不再存在可以脫離堆棧而運行的程序。我們從堆棧的用途和內容可以看出其重要性:? l 記錄子程序調用的軌跡,使嵌套的(多層的)子程序調用成為可能。? l 通過堆棧傳遞子程序調用參數,使程序設計得以簡化。要是不能通過堆棧傳遞參數,實際上就不會有現代的程序設計。? l 在堆棧上為子程序內部的“局部變量”分配空間,這既使“模塊化程序設計”和“結構化程序設計”成為可能,也為線程(進程)概念的產生提供了條件。? l 程序執行的軌跡和局部變量結合在一起成為“脈絡”、即“上下文”,由此產生了線程(進程)的概念,使多線程(多進程)系統及其調度/切換成為可能。 可見堆棧與“程序的執行”有著不可分割的關系;而程序的執行又恰恰是線程的最重要、最本質的特征。在線程的概念和應用出現之前,人們常說“進程是程序的執行”;后來有了線程的概念,人們在相當長一段時期中都不很重視線程和進程在概念上的區分,還是常說“進程是程序的執行”;現在則應該說“線程是程序的執行”了。總之,沒有堆棧就無所謂線程,線程是離不開堆棧的,而且線程最根本的“私有財產”就是它的堆棧(以及以后要講到的“堆棧本地存儲(TLS)”),別的都是屬于進程、而不是屬于個別線程的“集體財產”。 像Unix/Linux一樣,除“內核線程”外,每個Windows線程都有兩個堆棧,一個是用戶空間堆棧,一個是系統空間堆棧。由于“內核線程”只在內核中執行而沒有用戶空間,所以就沒有用戶空間堆棧。 在系統完成初始化以后,當CPU運行于用戶空間時,它就一定是在執行某個線程、即“當前線程”的程序,因而就使用這個線程的用戶空間堆棧。當CPU運行于系統空間時,也一定有個“當前線程”,具體取決于當時處于運行狀態的是哪一個線程,因而就使用這個線程的系統空間堆棧。或者也可以反過來說,當前正在使用哪一個線程的系統空間堆棧,這個線程就是當前線程。不過,CPU運行于系統空間時卻不一定是在執行當前線程的程序、或者說不一定是在為當前線程而執行程序(內核中的程序是由所有線程公有的),而有可能是在為它人做嫁衣裳。例如,中斷處理就未必是為當前線程而執行的。所以Windows的內核又分成上、下兩大層,其中上面的一層稱為“執行層(Executive)”,執行層中的程序肯定是為當前線程而執行的,執行層下面就不一定了(不過也并非肯定不是)。其實Linux內核也是這樣,只不過沒有“執行層”這么個說法。 這樣,不管CPU是在用戶空間還是系統空間,不管是在做些什么事情、為誰做,它一定是在使用當前線程的堆棧,具體使用哪一個則取決于CPU當時運行于哪一個空間。 線程的用戶空間堆棧比較容易為人們所理解。因為一來CPU在用戶空間的活動基本上是由應用軟件的程序員們編排的,人們比較熟悉;二來CPU在用戶空間是專一的,總是在為當前線程而勞碌,不會“開小差”。 而系統空間堆棧就不同了,人們對此往往會有似懂非懂的感覺,所以本文對線程的系統空間堆棧作一些說明,具體包括:? l 系統空間堆棧的分配和建立。? l 系統空間堆棧在各種情況下的消長變化。? l 系統空間堆棧上的陷阱框架和調用框架。 不難想像,線程的系統空間堆棧是在創建線程的時侯分配和建立的。在系統調用NtCreateThread()內部,線程初始化的一部分就是為其系統空間堆棧分配存儲區間。 [NtCreateThread() > PsInitializeThread() > KeInitializeThread()] VOID STDCALL KeInitializeThread(PKPROCESS Process, PKTHREAD Thread, BOOLEAN First) { PVOID KernelStack; . . . . . . . . . . . . /* If this is isn't the first thread, allocate the Kernel Stack */ if (!First) { PFN_TYPE Page[MM_STACK_SIZE / PAGE_SIZE]; KernelStack = NULL; MmLockAddressSpace(MmGetKernelAddressSpace()); Status = MmCreateMemoryArea(NULL, MmGetKernelAddressSpace(), MEMORY_AREA_KERNEL_STACK, &KernelStack, MM_STACK_SIZE, 0, &StackArea, FALSE, FALSE, BoundaryAddressMultiple); MmUnlockAddressSpace(MmGetKernelAddressSpace()); . . . . . . /* Mark the Stack */ for (i = 0; i < (MM_STACK_SIZE / PAGE_SIZE); i++) { Status = MmRequestPageMemoryConsumer(MC_NPPOOL, TRUE, &Page[i]); . . . . . . } /* Create a Virtual Mapping for it */ Status = MmCreateVirtualMapping(NULL, KernelStack, PAGE_READWRITE, Page, MM_STACK_SIZE / PAGE_SIZE); . . . . . . /* Set the Kernel Stack */ Thread->InitialStack = (PCHAR)KernelStack + MM_STACK_SIZE; Thread->StackBase = (PCHAR)KernelStack + MM_STACK_SIZE; Thread->StackLimit = (ULONG_PTR)KernelStack; Thread->KernelStack = (PCHAR)KernelStack + MM_STACK_SIZE; } else { /* Use the Initial Stack */ Thread->InitialStack = (PCHAR)init_stack_top; Thread->StackBase = (PCHAR)init_stack_top; Thread->StackLimit = (ULONG_PTR)init_stack; Thread->KernelStack = (PCHAR)init_stack_top; } /* * Establish the pde's for the new stack and the thread structure within the * address space of the new process. They are accessed while taskswitching or * while handling page faults. At this point it isn't possible to call the * page fault handler for the missing pde's. */ MmUpdatePageDir((PEPROCESS)Process, (PVOID)Thread->StackLimit, MM_STACK_SIZE); MmUpdatePageDir((PEPROCESS)Process, (PVOID)Thread, sizeof(ETHREAD)); . . . . . . Thread->KernelStackResident = 1; . . . . . . Thread->EnableStackSwap = 0; . . . . . . } 參數First表明目標線程是否整個系統中(初始化以后)的第一個線程,也就是第一個進程中的第一個線程。系統中的第一個線程用init_stack作為它的系統空間堆棧,這是一塊固定的緩沖區,所以就無需分配了。當然,我們此刻關心的絕不是系統中的第一個線程。 只要不是系統中的第一個線程,就通過MmCreateMemoryArea()在系統空間分配一塊虛擬地址區間。由于實際參數KernelStack事先設置成NULL,所以對具體的位置并無要求,而由內存管理自由分配。區間的大小為MM_STACK_SIZE、實際上是3個4KB的頁面。再通過MmRequestPageMemoryConsumer()分配相應數量的物理頁面,然后通過MmCreateVirtualMapping()建立虛存頁面和物理頁面之間的映射。與Linux內核不同,Windows內核中并非所有頁面都不受換出/換入,但是各線程的系統空間堆棧所占頁面一般是不受換出/換入的。 為新建線程分配了系統空間堆棧之后,還要在這堆棧上虛構出一個“陷阱框架(Trap Frame)”,使堆棧的內容看起來就像這個線程是從用戶空間通過系統調用進入內核、而尚未返回一樣,為其受調度運行后在內核中的活動以及“返回”用戶空間埋下伏筆。如果是新建進程中的第一個線程則還要為APC函數LdrInitializeThunk()的執行作好安排。有關情況在以前的幾篇漫談中已有述及,后面也還要提到。 關于“框架(Frame)”,此刻只要把它理解為當CPU在某個特定函數中執行時堆棧上與其相關的內容。一般而言,框架是因為函數調用而形成的,每調用一個函數就會引起堆棧的擴張,堆棧上就多出一個框架;返回時則堆棧收縮,堆棧上消失一個框架。除一般的函數調用以外,系統調用、中斷、以及異常也會在系統空間堆棧上形成一個框架,不同的是此時所形成的框架有可能是跨空間的、因而是跨堆棧的。而且,如果跨空間,就總是原來在用戶空間,而新的框架則形成在系統空間堆棧上。這是因為系統調用、中斷、和異常都使CPU進入系統空間,并使用系統空間堆棧。“陷阱框架”這個詞最初可能是因系統調用而來,因為系統調用一般是通過自陷指令實現的;但是后來因中斷和異常所引起的框架也都稱為陷阱框架了。 系統空間堆棧屬于線程所有、歸線程使用,因而其位置自然就要記錄在線程的KTHREAD數據結構中。注意代碼中變量KernelStack所指向的是區間的(物理的)起點、即這個區間中地址最低的字節,而堆棧是從上向下伸展的,因此所記錄的(邏輯的)堆棧起點是(PCHAR)KernelStack+MM_STACK_SIZE,這里MM_STACK_SIZE是以字節為單位的區間長度,而StackLimit、即其(邏輯的)終點倒是KernelStack。 這里把KTHREAD數據結構中的InitialStack、StackBase、和KernelStack三個字段都設置成指向系統空間堆棧區間的終點、即上部邊界。顯然這只是初值相同,三個字段應該有不同的意義和用途。其實,KTHREAD結構中與堆棧有關的字段還不止上面所看到的四個: typedef struct _KTHREAD { . . . . . . PVOID InitialStack; /* 18 */ ULONG_PTR StackLimit; /* 1C */ /* Pointer to the thread's environment block in user memory */ PTEB Teb; /* 20 */ /* Pointer to the thread's TLS array */ PVOID TlsArray; /* 24 */ PVOID KernelStack; /* 28 */ . . . . . . UCHAR KernelStackResident; /* 11E */ UCHAR NextProcessor; /* 11F */ PVOID CallbackStack; /* 120 */ . . . . . . UCHAR EnableStackSwap; /* 134 */ UCHAR LargeStack; /* 135 */ . . . . . . PVOID StackBase; /* 15C */ . . . . . . } KTHREAD; 設立EnableStackSwap字段的用意顯然是想有控制地允許換出/換入系統空間堆棧所占的頁面。如果允許換出/換入的話,KernelStackResident想必是用來表明這些頁面當前是否在內存中。可是為什么要允許換出/換入系統空間堆棧呢?我們在前面看到,每個線程的系統空間堆棧的大小只是區區3個4KB的頁面,似乎也并不是很大一筆資源。可是另一個字段LargeStack給了我們一些線索,看來設計者的意圖是也可以采用“大堆棧”,而在采用大堆棧的條件下換出/換入其存儲頁面就有意義了。不過,就目前ReactOS的代碼而言,這幾個字段尚無實際的意義。 另一個字段CallbackStack跟從系統空間“回調(callback)”用戶空間的函數有關,在回調用戶空間函數時會暫時使用另一個堆棧,所以才需要這個字段;另一方面這也解釋了為什么要有InitialStack這個字段。不過我們此刻對此并不關心。 對于線程的運行有實質性意義的是KernelStack這個字段。這個字段在剛為系統空間堆棧分配存儲區間時指向區間的終點、即上部邊界,但是隨著進一步的初始化、以及為新建線程虛構陷阱框架的過程、就有了變化。事實上,完成了對新建線程基本的初始化以后,NtCreateThread()的代碼中還有個函數調用KiArchInitThreadWithContext(),這實際上是個宏操作,對于x86處理器定義為Ke386InitThreadWithContext(),這就是對系統空間堆棧的進一步初始化。讀者在以前講述線程創建的漫談中見到過這個函數,但是現在需要從堆棧使用的角度加以深入考察: [NtCreateThread() > Ke386InitThreadWithContext()] NTSTATUS Ke386InitThreadWithContext(PKTHREAD Thread, PCONTEXT Context) { PULONG KernelStack; ULONG InitSize; PKTRAP_FRAME TrapFrame; PFX_SAVE_AREA FxSaveArea; /* Setup a stack frame for exit from the task switching routine */ InitSize = 6 * sizeof(DWORD) + sizeof(DWORD) + 6 * sizeof(DWORD) + + sizeof(KTRAP_FRAME) + sizeof (FX_SAVE_AREA); KernelStack = (PULONG)((char*)Thread->KernelStack - InitSize); /* Set up the initial frame for the return from the dispatcher. */ KernelStack[0] = (ULONG)Thread->InitialStack-sizeof(FX_SAVE_AREA); /* TSS->Esp0 */ KernelStack[1] = 0; /* EDI */ KernelStack[2] = 0; /* ESI */ KernelStack[3] = 0; /* EBX */ KernelStack[4] = 0; /* EBP */ KernelStack[5] = (ULONG)&PsBeginThreadWithContextInternal ; /* EIP */ /* Save the context flags. */ KernelStack[6] = Context->ContextFlags; /* Set up the initial values of the debugging registers. */ KernelStack[7] = Context->Dr0; . . . . . . KernelStack[12] = Context->Dr7; /* Set up a trap frame from the context. */ TrapFrame = (PKTRAP_FRAME)(&KernelStack[13]); TrapFrame->DebugEbp = (PVOID)Context->Ebp; . . . . . . TrapFrame->Eax = Context->Eax; TrapFrame->PreviousMode = UserMode; TrapFrame->ExceptionList = (PVOID)0xFFFFFFFF; TrapFrame->Fs = TEB_SELECTOR; TrapFrame->Edi = Context->Edi; . . . . . . TrapFrame->Eip = Context->Eip; TrapFrame->Eflags = Context->EFlags | X86_EFLAGS_IF; TrapFrame->Eflags &= ~(X86_EFLAGS_VM | X86_EFLAGS_NT | X86_EFLAGS_IOPL); TrapFrame->Esp = Context->Esp; TrapFrame->Ss = (USHORT)Context->SegSs; /* FIXME: Should check for a v86 mode context here. */ /* Set up the initial floating point state. */ /* FIXME: Do we have to zero the FxSaveArea or is it already? */ FxSaveArea = (PFX_SAVE_AREA) ((ULONG_PTR)KernelStack + InitSize - sizeof(FX_SAVE_AREA)); if (KiContextToFxSaveArea(FxSaveArea, Context)) { Thread->NpxState = NPX_STATE_VALID; } else { Thread->NpxState = NPX_STATE_INVALID; } /* Save back the new value of the kernel stack. */ Thread->KernelStack = (PVOID)KernelStack; return(STATUS_SUCCESS); } 調用參數Context指向一個CONTEXT數據結構,這是作為系統調用NtCreateThread()的參數傳下來的。這個數據結構給定了當新建線程開始在用戶空間運行時的初始上下文,例如Context->Eip就是新建線程在用戶空間的程序入口,Context->Esp就是新建線程開始在用戶空間運行時的堆棧指針,等等。 我們看這里的指針KernelStack,注意最后KTHREAD結構中的KernelStack字段就是被設置成了這個指針的值,從而與前述InitialStack和StackBase兩個字段拉開了距離。那么指針KernelStack的值是什么呢?從代碼中可以看出,是從原來的區間終點開始下調,調整的距離是InitSize,這段距離邏輯上分成三個部分、用于三個目的: 一、首先是一個FX_SAVE_AREA數據結構。在有浮點處理器的系統中,這是用來保存浮點處理器狀態的,后面的指針FxSaveArea就是指向這個數據結構的起點。這個數據結構其實不屬于系統空間堆棧,只是借用它一塊寶地而已。在這下面才是真正意義上的堆棧,而這也就是系統空間的原點。當系統空間堆棧為空時,堆棧指針就指向這里。特別地,當CPU運行于用戶空間時,當前線程的系統空間堆棧總是空的。 二、然后是一個KTRAP_FRAME數據結構,這就是一個陷阱框架。事實上,只要CPU運行于系統空間,當前線程系統空間堆棧的(區間)頂部一定是一個陷阱框架。但是系統調用的陷阱框架和中斷的陷阱框架不一樣,而這里要構筑的是系統調用的陷阱框架,要為新建線程制造出一個處于系統調用過程中的假象。這樣,當CPU從實現具體系統調用的函數返回、到達_KiServiceExit的時侯,堆棧指針應該恰好指向這個數據結構的起點、即陷阱框架的下部邊界。KTRAP_FRAME數據結構的定義如下: typedef struct _KTRAP_FRAME { PVOID DebugEbp; PVOID DebugEip; PVOID DebugArgMark; PVOID DebugPointer; PVOID TempCs; PVOID TempEip; ULONG Dr0; ULONG Dr1; ULONG Dr2; ULONG Dr3; ULONG Dr6; ULONG Dr7; USHORT Gs; USHORT Reserved1; USHORT Es; USHORT Reserved2; USHORT Ds; USHORT Reserved3; ULONG Edx; ULONG Ecx; ULONG Eax; ULONG PreviousMode; PVOID ExceptionList; USHORT Fs; USHORT Reserved4; ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp; ULONG ErrorCode; ULONG Eip; ULONG Cs; ULONG Eflags; ULONG Esp; USHORT Ss; USHORT Reserved5; USHORT V86_Es; USHORT Reserved6; USHORT V86_Ds; USHORT Reserved7; USHORT V86_Fs; USHORT Reserved8; USHORT V86_Gs; USHORT Reserved9; } KTRAP_FRAME, *PKTRAP_FRAME; 這里要注意:? l 這數據結構中各個字段的次序與實際壓入堆棧的次序正好相反,因為堆棧是從上向下伸展的。所以,例如Ds就比Es先壓入堆棧。? l 結構中有些字段的類型是16位的USHORT,但是CPU的堆棧操作都是32位的,所以這些16位字段實際上都是合二為一的。例如字段Gs和Reserved1其實是合在一起的,余可類推。之所以如此,是因為段寄存器都是16位的。? l 在用戶空間通過int指令進行系統調用時,CPU自動壓入堆棧的第一項數據是用戶空間堆棧段寄存器SS的值,就是這里字段Ss和Reserved5的組合。按理說這已經是堆棧區間的頂部,再往上沒有別的數據了。可是這里的數據結構定義中在這上面還有V86_Es等4個棧項,這是因為如果在進入系統空間之前CPU運行于V86模式的話就還有4項額外的數據,即在V86模式下段寄存器ES、DS、FS、GS的值。所以,數據結構KTRAP_FRAME實質上是一個Union。但是這并不妨礙通過KTRAP_FRAME指針訪問普通陷阱框架中的數據,只是在一般情況下框架頂部到Ss和Reserved5為止,也就是說一般情況下的系統調用框架比這里的短一點。 當新建線程受調度運行而“返回”用戶空間時,CPU逐步恢復保存在陷阱框架中的上下文,通過iret指令返回用戶空間的時侯,從系統空間堆棧中取出的最后一項數據是(用戶空間)段寄存器SS的值,所以堆棧指針就停留在了指向安排用于V86_Es的位置。對于不運行于V86模式的線程,其系統空間堆棧指針不會再往上跑了,這不意味著系統空間堆棧不為空了嗎?應該說,物理意義上確實是這樣,但是邏輯意義上仍然是空的,而且這只發生于新建線程的第一次返回用戶空間。這是因為當CPU下一次因系統調用、中斷、異常而再次進入系統空間、切換到當前線程的系統空間堆棧時,是從其原點開始的。此時所形成的陷阱框架大小則與當時是否運行于V86模式有關。這樣,以后的框架消長就總是平衡的,CPU每次回到用戶空間時其系統空間堆棧指針就總是真正地回到原點。 三、在陷阱框架下面還有13個棧項,即KernelStack[0]至KernelStack[12]。其中KernelStack[7]至KernelStack[12]用于調試寄存器Dr0、Dr1等等。而KernelStack[5]是Eip的映像,本來應該是函數調用的返回地址,這里則指向一段匯編代碼。這就相當于在陷阱框架下面又開了一個函數調用框架。事實上,這段匯編代碼最后是通過jmp指令跳轉到_KiServiceExit的,所以新建線程就相當于是身處一個在_KiServiceExit前面調用的子程序中。KernelStack[1]至KernelStack[4]則用于為新建線程準備下幾個寄存器的初值(都是0)。 下面就是上述的匯編代碼_PsBeginThreadWithContextInternal ,讀者可以結合、對照上面Ke386InitThreadWithContext()的代碼閱讀: _PsBeginThreadWithContextInternal : /* This isn't really a function, we are called as the return address of a context switch */ /* Do the necessary prolog before the context switch */ call _PiBeforeBeginThread /* Load the context flags. */ popl %ebx /* Load the debugging registers */ testl $(CONTEXT_DEBUG_REGISTERS & ~CONTEXT_i386), %ebx jz .L1 popl %eax /* Dr0 */ movl %eax, %dr0 popl %eax /* Dr1 */ movl %eax, %dr1 popl %eax /* Dr2 */ movl %eax, %dr2 popl %eax /* Dr3 */ movl %eax, %dr3 popl %eax /* Dr6 */ movl %eax, %dr6 popl %eax /* Dr7 */ movl %eax, %dr7 jmp .L3 .L1: addl $24, %esp .L3: /* Load the rest of the thread's user mode context. */ movl $0, %eax jmp _KiServiceExit 開始時對于PiBeforeBeginThread()的調用是為了改變代碼的運行級別,我們在此不必關心。 這里call指令以后對堆棧的操作消去了堆棧上對應于前面代碼中從KernelStack[6]至KernelStack[12]的7個表項,其中各調試寄存器的內容根據當前實際上是否在調試而或者裝入相應的寄存器,或者通過調整堆棧指針予以跳過、丟棄。這樣,當最后跳轉到_KiServiceExit時,堆棧指針恰好指向陷阱框架的(區間)起點。所以,對于KernelStack[5]的設置實際上是為線程的調度/切換準備的,目的是為新建線程提供一個虛構的程序執行“斷點”,仿佛原先就是在這里被剝奪了運行,下次受調度運行時就從這一點上恢復。 那么KernelStack[0]又是干什么用的呢?注釋中說了這就是TSS->Esp0,這跟線程切換的機制與過程有關,這里先簡單說一下。當新建線程受調度運行時,會將這個數值寫入“任務狀態段”TSS中的ESP0字段,而每當CPU從用戶空間進入系統空間、需要切換到系統空間堆棧時,就總是把TSS中的這個數值裝入ESP,所以這就是系統空間堆棧的原點。從代碼中可以看到,這就是Thread->InitialStack-sizeof(FX_SAVE_AREA)。 最后又要回到對Thread->KernelStack的設置。KTHREAD結構中這個字段的用途是在線程切換的時侯保存堆棧指針。就是說,其它寄存器的內容都保存在堆棧上,而堆棧指針則保留在KTHREAD結構中。當一個線程暫時放棄運行、或被剝奪運行時(一定發生于系統空間),就把它當時的堆棧指針保存在這兒;到下一次又被調度運行時則從這兒恢復其堆棧指針。 當新建線程受調度運行并跳轉到_KiServiceExit處時,其系統空間堆棧上已經只剩下陷阱框架了,而隨后的“恢復現場”以及最后iret指令的執行則使陷阱框架消失,邏輯意義上系統空間堆棧的大小收縮到0、堆棧指針回到原點;同時CPU返回到用戶空間、切換到用戶空間堆棧。以后每次從系統調用、中斷、或異常返回用戶空間時,則物理意義上也都是如此。顯然,這一點是很重要的,要不然系統空間堆棧就早晚會被耗盡。 但是,如果需要執行APC函數的話,那就還有個不小的插曲和變化。我們先看_KiServiceExit處對APC函數的處理: _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 . . . . . . /* Deliver APCs */ sti pushl %ebx pushl $0 pushl $UserMode call _KiDeliverApc@12 cli 如果是要返回到用戶空間(本次系統調用是從系統空間啟動),并且有APC請求存在,就要調用KiDeliverApc(),并把此時堆棧指針的內容作為參數之一傳下去,因為這也就是指向陷阱框架即KTRAP_FRAME結構的指針。這個函數干些什么呢?就我們此刻所關心的角度而言,它干了兩件事: (1)、根據執行APC函數的安排和要求修改陷阱框架,使得CPU返回到用戶空間時不是返回到原來啟動系統調用的地方,而是“返回”到執行APC函數的地方。不過陷阱框架的大小和結構并不改變,所以回到用戶空間時其系統空間堆棧同樣為空。 (2)、把原來的陷阱框架保存在用戶空間堆棧上,留待執行完APC函數以后加以恢復(并在恢復后再次返回用戶空間),因為要不然就回不到當初啟動系統調用的地方去了。為此,需要修改當前線程的用戶空間堆棧,在上面增添一個CONTEXT數據結構以及為執行APC函數所需的函數框架。 我們不妨想想,可以把原來的陷阱框架保存在哪里呢?無非是兩種現實的可能。一種是保存在當前線程的系統空間堆棧上,即保留原來的陷阱框架不動,下面再嵌套一個新的陷阱框架。另一種是保存在用戶空間堆棧上,而且本來就需要在原來的框架下面嵌套一個APC函數框架。比較下來,還是以保存在用戶空間堆棧上為好。不過并不是原封不動地保存,而是另外定義了一個略有不同的CONTEXT數據結構: typedef struct _CONTEXT { DWORD ContextFlags; DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; FLOATING_SAVE_AREA FloatSave; DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs; BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT; 之所以略有不同,是因為跟浮點運算有關的信息也需要保存,但是卻不在陷阱框架中,這前面已經講過了。從陷阱框架到CONTEXT結構的轉換在KiInitializeUserApc()里面完成,反過來則由KeContextToTrapFrame()和KiContextToFxSaveArea()實現轉換。 從KiDeliverApc()返回到_KiServiceExit下面以后,CPU繼續其返回用戶空間的行程。最后,當執行iret指令的時侯,陷阱框架就從系統空間堆棧上消失了,系統空間的堆棧指針又回到了原點。所不同的只是“返回”到了用戶空間執行APC函數的地方,而用戶空間堆棧上則已經為此準備好了執行APC函數的框架。 但是,CPU最終還得要回到用戶空間原先的地方,我們知道那是通過系統調用NtContinue()實現的,而在啟動NtContinue()時則又以堆棧上的CONTEXT結構作為參數帶回內核,使內核得以恢復原先的陷阱框架。注意NtContinue()本身的執行也會產生一個陷阱框架,但是這個框架被根據CONTEX數據結構恢復過來的框架所覆蓋。因為這個框架本來就沒有什么意義,NtContinue()是不返回其調用地的。 現在,假定新建線程已經在用戶空間正常運行(不在執行APC函數),我們又要從用戶空間開始,考察系統調用時其系統空間堆棧的消長,特別是陷阱框架和其它框架的形成和消失。 當CPU在用戶空間因執行int指令而切換到系統空間時,會從“任務狀態段”TSS獲取當前線程系統空間堆棧的原點,自動把下列內容依次壓入這個堆棧:? l 用戶空間堆棧段寄存器SS的內容。? l 用戶空間堆棧指針ESP的當前內容。此時ESP指向用戶空間堆棧上最后壓入的調用參數。由于將調用參數壓入堆棧的次序與它們出現在代碼中的次序相反,ESP所指向的是第一個調用參數在用戶空間堆棧上的位置。根據這個指針,內核是可以找到用戶空間堆棧上的各個調用參數的,但是為方便起見ReactOS讓寄存器EDX也指向這個位置,這在“ReactOS怎樣實現系統調用”一文中已經講過。當然,這意味著在執行系統調用之前要先把EDX原來的內容先保存在用戶空間堆棧上。? l 然后是“標志寄存器”EFLAGS的當前內容。這個寄存器記錄著許多有關CPU運行模式和狀態的信息,值得特別一提的是其中的中斷控制位決定著是否允許中斷。實際上當CPU運行于用戶空間時總是允許中斷的,也沒有關閉中斷的手段,而標志寄存器內容的(自動)保存和恢復保證了這一點。? l 接著是用戶空間代碼段寄存器CS的內容。? l 最后是指令計數器的當前內容,這實際上就是用戶空間的返回地址。注意這返回地址是壓在系統空間堆棧上,而不是在用戶空間堆棧上。 所以,當CPU從用戶空間進入系統空間的時侯,當前線程的系統堆棧上已經有了5項數據,都是由CPU自動壓入的。剩下來的就是軟件的事了。換言之,本次系統調用的陷阱框架已經開始形成,但是尚未完成。系統調用在內核中的入口是_KiSystemService,我們重點看堆棧的消長以及陷阱框架的形成: _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 /* 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) 代碼中注釋了各寄存器的內容映像在堆棧上的位置,不過注意這是按壓入堆棧的次序相對于堆棧原點的位移,是從上往下計算的。前面的5項數據就是CPU自動壓入的,而軟件壓入堆棧的第一項數據是數值0,這是將來返回給用戶空間程序的出錯代碼,暫時設為0。把段寄存器FS的內容壓入堆棧是因為要把FS設置成指向數據結構KPCR、即“處理器控制區”。這個段寄存器在用戶空間指向當前線程的TEB,在內核中則指向KPCR。FS指向KPCR以后就可以從中獲取指向當前線程的“異常處理隊列”的指針,并把這個指針也保存在堆棧上,并把KPCR中的這個指針設置成-1,這是與“結構化異常處理”、即SEH有關的操作,并非我們此刻所關心的話題。KPCR中還有個指針指向當前線程的ETHRAD數據結構,這里讓寄存器ESI指向這個數據結構。 所謂“先前模式(Previous Mode)”,是說CPU在進入本次系統調用之前的運行模式,即用戶模式或內核模式。這是Windows內核所特有的,原因是Windows內核允許在內核中進行系統調用,這就需要知道本次系統調用是在哪一個空間啟動,因為這牽涉到應該從何處獲取調用參數的問題。這個信息可以從先前的段寄存器CS中獲取,段選擇項的最低兩位是運行級別RPL,內核為0而用戶空間為3,所以檢查其最低位即可獲取這個信息。系統調用是通過int指令啟動的,所以即便是在系統空間啟動,CPU也會自動把當時的CS隨同EIP一起壓入堆棧,因而這個寄存器的映像一定在陷阱框架中。而ETHREAD數據結構中(實際上是KTHREAD結構中)則有個PreviousMode字段,這里就把這信息記錄在這個字段中。然而,既然允許在內核中啟動系統調用,那就有可能發生嵌套的系統調用,每次都需要記錄先前模式,可是當前線程的PreviousMode字段只有一個。怎么辦呢?這里就把前一次(上一層)系統調用的先前模式記錄在本次系統調用的陷阱框架中。這樣,對于嵌套的系統調用,就會形成一個先前模式的鏈、實際上是先前模式的堆棧。顯然,前面對于“異常處理隊列”指針的處理也與此相似。 我們繼續往下看。 /* 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 /* Trick gdb 6 into backtracing over the system call */ mov 0x6c(%esp), %ebx pushl 4(%ebx) /* DebugEIP */ // + 0x74 #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) 這里對其余一些寄存器內容的保存是不言自明的。接著在堆棧指針上減去了0x28,使堆棧指針下移了40個字節,在堆棧上跳過了陷阱框架中從DebugArgMark到Dr7的一共10個棧項,因為這些數據與常規的系統調用無關,而只與Debug有關。陷阱框架中的最后兩項數據顯然也是與Debug有關的。 至此,從代碼中可以看出,程序的設計者認為陷阱框架已經形成了,此時堆棧指針ESP所指向的地方就被認為是陷阱框架所占區間的起點。所以,在將段寄存器DS和ES的值設置成KERNEL_DS以后,先將KTHREAD結構中TrapFrame字段的內容保存在陷阱框架中本來用于寄存器EDX映像的單元中,再通過寄存器EBP將其地址寫入當前進程KTHREAD結構中的TrapFrame字段。另一方面,這樣寄存器EBP也指向了陷阱框架的起點。KTHREAD結構中的TrapFrame字段是個_KTRAP_FRAME結構指針,此時堆棧上的陷阱框架已經與這個數據結構的定義相符(除與VM86有關的幾個字段外)。 考慮到系統調用可能嵌套,顯然應該把TrapFrame字段原來的值保存在堆棧上,但是這里利用了本來用于寄存器EDX映像的棧項,以求節約一點空間。在系統調用的時候,寄存器EDX用來傳遞用戶空間堆棧上調用參數所在位置的,所以在系統調用完成以后就失去了意義。其實用戶空間堆棧上的調用參數位置也并不非得要傳遞下來,因為用戶空間的堆棧指針就保存在當前的陷井框架中固定的位置上,而知道了當時的用戶空間堆棧指針,當然也就知道了參數所在的位置,所以通過edx傳遞參數所在位置只是使得進入內核以后的處理更方便一些。所以,利用這個棧項保存TrapFrame字段原來的值是可行的。 下面的一些與陷阱框架無關的操作就不是我們在這里所關心的了,況且在“ReactOS怎樣實現系統調用”一文中也已談及,所以我們跳過這些操作、只關心與堆棧有關的操作,直至對目標函數的調用。 . . . . . . /* Allocate space on our stack */ subl %ecx, %esp /* 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 注意這里又通過sub指令對堆棧指針進行了調整,在堆棧上分配了一些空間。這里寄存器ECX持有調用參數所占的字節數,每個系統調用的參數所占的字節數都是預定的,從堆棧指針上減去這么多字節,就在堆棧上為這些參數分配了空間,然后就通過重復的movsd指令把調用參數從用戶空間堆棧復制到系統空間堆棧上,重復的次數為ECX的數值除于4,即把字節數換算成長字數。注意這并不意味著對用戶空間的訪問已經就此完成,因為此時復制過來的往往是指針,所以進入目標函數以后可能還需要把這些指針所指的數據復制到系統空間中來。 由于前面的一些我們已經略過的操作,這里寄存器EAX持有來自系統調用跳轉表的函數指針,對具體系統調用目標函數的調用就是由這里的call指令實現的。 這call指令的執行使堆棧區間中當前框架的下方開始形成一個新的函數調用框架,因為至少這call指令要將返回地址壓入堆棧。進入目標函數以后,堆棧指針ESP所指的就是由CPU壓入堆棧的返回地址,就是代碼中movl指令所在的地址。在這上面是調用參數,再上面就是陷阱框架。從道理上說這些調用參數也在陷阱框架之中,但是上面所說的“陷阱框架”是狹義的,只是指定義于KTRAP_FRAME數據結構中的那部分內容。系統調用實質上是跨地址空間的函數調用,如果把系統調用在內核中的入口_KiSystemService看作一個函數的起點,那就應該有一個函數調用(執行)框架,這個函數框架的主體部分就是狹義的陷阱框架,而整個函數框架則可看成一個廣義的陷阱框架。這樣,可以認為每個返回地址所在的單元就是一個框架的起點。對于嵌套的函數調用,所形成的框架也是嵌套的,低層的框架嵌在高層的框架之中。不過人們往往是在比較寬松的語境下談論堆棧框架,而并非總是基于嚴格的定義;作者也只是就概念而言,而并不涉及對于框架的嚴格定義。 進入目標函數之后,典型的開始幾條指令一般總是類似于這樣: pushl %ebp movl %esp, %ebp subl $12, %esp 在x86系統結構的程序中,寄存器EBP一般都用作“框架指針”,指向當前框架的起點(從上往下算)。不過按理說框架的起點是返回地址,因而第一條指令就應該是“movl %esp, %ebp”,但是這里有個保存EBP原有內容(這是上一層框架的指針)的問題,先得有一條“pushl %ebp”才行,所以“框架指針”指向的并非物理意義上的框架起點。這么一來,EBP所指向的是當前框架中它本身老的映像,而4(%ebp)、即EBP加4處的內容才是返回地址,8(%ebp)則是第一個調用參數,余類推。 然后,如果需要的話,會有一條類似于“subl $12, %esp”的指令,目的是在堆棧上為當前框架、即當前函數中的局部變量分配空間,具體的大小則取決于所減去的數值。這樣,就可以借助框架指針訪問局部變量了。例如-4(%ebp)就可能是其中的一個局部變量,具體的指派則由編譯/匯編工具決定。到要返回的時侯,只要執行一條“movl %ebp, %esp”指令,就把堆棧指針撥回了框架的邏輯起點。再執行一條“popl %ebp”,就一方面恢復了EBP原有的內容、即上一層的框架指針,同時也使堆棧指針真正回到了框架的起點,為執行ret指令作好了準備。將堆棧指針撥回框架起點,特別是恢復了EBP的原值,就意味著丟棄了框架中的內容,例如當前函數的局部變量就不復存在了,所以這幾條指令一定是放在最后(緊挨著ret指令)才執行的。一執行ret指令,這個框架就不存在了。 這只是就大多數的、典型的情況而言,有些函數的匯編代碼中也許根本就不使用框架指針,但那并不意味著框架就不存在了,而只是情況特殊而已。 回到前面的代碼,當CPU從目標函數返回時,寄存器EAX的內容就是函數的返回值、實際上是出錯代碼,這里把它寫入框架中保存EAX映像的單元中,代碼中的位移量KTRAP_FRAME_EAX定義為0x44。注意這里對EBP的使用是特殊的,從前面代碼中可以看出,它并不指向框架的起點,而是指向陷阱框架地址的起點,那就是堆棧上由上往下數,序號(從0開始)為0x78的單元所在處。于是0x78-0x44=0x34,就是序號為0x34的所在。 至于堆棧上的調用參數,則有兩種常用的方法可以使之消去。一種是在目標函數中使用帶有調整堆棧指針功能的ret指令,因而使CPU在返回的過程中自動調整了堆棧指針,所以返回后調用參數已經不在堆棧上了。那CPU怎么知道需要調整多少個字節呢?這是由匯編工具根據函數的參數表定義計算出來、編碼在ret指令中的。另一種就是由調用者在返回以后通過add指令調整堆棧指針,使其跳過調用參數。而在上面的代碼中則把EBP的值賦給ESP,使其跳過陷阱框架之下的所有單元,調用參數當然也就隨之而去了。 再往下就是從系統調用返回、系統空間堆棧逐步收縮、最后使陷阱框架消失的過程了。這基本上只是前述過程的逆操作,所以這里就不再詳述,讀者可以自己閱讀ReactOS的有關源碼。 注意寄存器EAX和EDX的內容雖然保存了,返回時也予以恢復,但是實際上它們原來的值在回到用戶空間后已經不再使用,因為EAX是用來傳遞系統調用號的,系統調用完成以后當然失去了意義。而EDX是用來傳遞用戶空間堆棧上調用參數所在位置的,所以在系統調用完成以后也失去了意義。不過,EAX原來的值固然是失去了意義,但是這個寄存器是用來返回系統調用的函數值、一般是出錯代碼的,所以在返回時表面上是通過pop指令“恢復”其內容,其實卻是被設置成需要返回的數值。 再看中斷。同樣,中斷時陷阱框架的前一部分是由硬件形成的,后一部分則由中斷響應入口處的匯編指令完成。其中由硬件形成的部分因發生中斷時CPU的運行模式而有所不同。如果發生中斷時CPU運行于用戶空間,則為:? l SS ? l ESP ? l EFLAGS ? l CS ? l EIP 顯然,這與從用戶空間啟動系統調用時相同。 而若發生中斷時CPU運行于系統空間,則沒有前面兩項,而只有EFLAGS、CS、和EIP三項。 內核中對于每個中斷號都有個程序入口,不同中斷號的程序基本上都一樣,只是作為參數壓入堆棧的“中斷向量”各不相同。下面是3號中斷程序入口_irq_handler_3的代碼: _irq_handler_3: cld pusha pushl %ds pushl %es pushl %fs pushl %gs movl $0xceafbeef,%eax pushl %eax movw $KERNEL_DS,%ax movw %ax,%ds movw %ax,%es movw %ax,%gs movl $PCR_SELECTOR, %eax movl %eax, %fs pushl %esp pushl $(IRQ_BASE + 3) call _KiInterruptDispatch popl %eax popl %eax popl %eax popl %gs popl %fs popl %es popl %ds popa iret 這里的指令pusha是“push所有通用寄存器”,popa則相反。 以call指令為中心,這些代碼可以分成三個部分。在call指令之前、除最后的兩條push指令以外、是中斷框架的形成階段;之后則是中斷框架的消亡階段;而call指令對KiInterruptDispatch()的調用是實質性操作的階段。注意call指令前面的最后兩條push指令把兩個調用參數壓入堆棧。其中之一是當時的堆棧指針(不過沒有使用EBP),由于此時框架已經形成,堆棧指針實際上指向框架的起點地址、即地址最低點。另一個參數(IRQ_BASE+3)是“中斷向量”、實際上就是絕對中斷號。 對于因為中斷而形成的框架,ReactOS為其另外定義了一個數據結構: typedef struct _KIRQ_TRAPFRAME { ULONG Magic; ULONG Gs; ULONG Fs; ULONG Es; ULONG Ds; ULONG Eax; ULONG Ecx; ULONG Edx; ULONG Ebx; ULONG Esp; ULONG Ebp; ULONG Esi; ULONG Edi; ULONG Eip; ULONG Cs; ULONG Eflags; } KIRQ_TRAPFRAME, *PKIRQ_TRAPFRAME; 顯然,這個框架與前面因系統調用而形成的框架有明顯的不同,而且也小得多。這意味著中斷的進入與返回不能跟系統調用的進入與返回共用相同的代碼,這我們已經看到了。相比之下,在Linux內核中它們基本上是共用相同代碼的。 代碼的作者稱系統調用的框架為KTRAP_FRAME、即“陷阱框架”,而稱中斷的框架為KIRQ_TRAPFRAME、即“中斷請求陷阱框架”。但是其實中斷與陷阱并不是一回事,說起來又拗口,有時候還容易混淆,還不如稱為“中斷框架”更好。 代碼中對段寄存器DS、ES、GS、FS的設置這里就不多說了,注意這里并不在意“先前模式”等等,所以比系統調用時形成的陷阱框架要簡單一些。常數0xceafbeef就是數據結構中的Magic。注意這個數據結構中并不包括當中斷發生于用戶空間時自動壓入堆棧的SS和ESP兩項數據,所以實際上并不完整。不過是否存在于這個數據結構中只是形式,是否存在于堆棧上才是實質;再說這些數據的恢復是由CPU在執行ret指令時自動完成的,所以跟編程關系不大。 下面可以看KiInterruptDispatch()的代碼了,我們還是把重點放在堆棧和框架。 VOID KiInterruptDispatch (ULONG vector, PKIRQ_TRAPFRAME Trapframe) { KIRQL old_level; KTRAP_FRAME KernelTrapFrame; PKTHREAD CurrentThread; PKTRAP_FRAME OldTrapFrame=NULL; . . . . . . /* Actually call the ISR. */ KiInterruptDispatch2(vector, old_level); . . . . . . if (old_level==PASSIVE_LEVEL && Trapframe->Cs != KERNEL_CS) { CurrentThread = KeGetCurrentThread(); if (CurrentThread!=NULL && CurrentThread->Alerted[1]) { . . . . . . if (CurrentThread->TrapFrame == NULL) { OldTrapFrame = CurrentThread->TrapFrame; KeIRQTrapFrameToTrapFrame(Trapframe, &KernelTrapFrame); CurrentThread->TrapFrame = &KernelTrapFrame; } Ke386EnableInterrupts(); KiDeliverApc(KernelMode, NULL, NULL); Ke386DisableInterrupts(); if (CurrentThread->TrapFrame == &KernelTrapFrame) { KeTrapFrameToIRQTrapFrame(&KernelTrapFrame, Trapframe); CurrentThread->TrapFrame = OldTrapFrame; } } } } 這里的KiInterruptDispatch2()處理實際的中斷服務。它的調用參數只有兩個,一個是“中斷向量”vector;另一個是中斷發生前的CPU運行級別old_level,這是由軟件實現的級別,現在可以暫不深究。由此可以看出,這個函數的執行并不涉及陷阱框架。 本來,執行完KiInterruptDispatch2(),就可以原路返回了,但是這里有個APC函數的問題。即使APC請求存在,在中斷返回時執行APC函數也是有條件的,條件是中斷發生于用戶空間(Trapframe->Cs != KERNEL_CS)、并且不是發生于執行APC函數的過程中(old_level==PASSIVE_LEVEL)。我們在前面看到,啟動APC函數在用戶空間的執行要將原來的陷阱框架轉換成CONTEXT結構、并保存到用戶空間堆棧上,但那是對“正宗”的陷阱框架KTRAP_FRAME而言,而現在我們面對的是中斷框架,所以要先通過KeIRQTrapFrameToTrapFrame()將其轉換成一個正宗陷阱框架,放在一個臨時的數據結構KernelTrapFrame中,并使當前線程的KTHREAD結構中的指針Trapframe指向這個數據結構,以便把它轉化成CONTEXT結構并保存到用戶空間堆棧上。事后則再通過KeTrapFrameToIRQTrapFrame()轉換回來。那么,使CurrentThread->TrapFrame指向KernelTrapFrame的時侯把它的原值保存在哪里呢?這一次不打擾陷阱框架上的EDX映像了。從代碼中可以看到,這一次保存在一個局部變量OldTrapFrame中。還是在堆棧上,可是不在陷阱框架中,而在陷阱框架下面的函數調用框架中。還有個問題,要是中斷嵌套怎么辦?如果中斷嵌套,那么新的中斷就發生于系統空間,CPU就不從TSS裝入系統空間堆棧指針、而只是繼續使用系統空間堆棧指針。于是,在前述函數調用框架的下方就又會形成一個新的陷阱框架,而陷阱框架的下方又會有函數調用框架,新的函數調用框架中又會有局部變量OldTrapFrame。可是,這兩個OldTrapFrame雖然都在堆棧上,卻分屬不同的框架,互相井水不犯河水。當然,是否允許中斷嵌套則又是另一個問題了。 最后再看異常。異常的框架與系統調用的框架相同,都是KTRAP_FRAME。 但是這里有個不同之處。在系統調用時寄存器EDX用來傳遞調用參數在用戶空間堆棧上的位置,為此用戶空間的程序在int 0x2e指令之前要先把EDX原有的內容保存在用戶空間堆棧上,到返回用戶空間后再予恢復。這樣,在系統調用時可以把KTHREAD結構中的指針Trapframe保存在框架中EDX所在的單元中。然而異常就不同了。異常可以發生在任何時候,根本就不可能事先保存EDX的內容。 還有個不同之處,那就是從異常返回時不考慮APC函數,那倒好辦。 我們以14號異常的程序入口_KiTrap14為例來看如何解決保存EDX內容的問題。 _KiTrap14: pushl %ebp pushl %ebx pushl %esi movl $14, %esi jmp _KiTrapProlog _KiTrapProlog: pushl %edi pushl %fs . . . . . . /* Load the PCR selector into fs */ movl $PCR_SELECTOR, %ebx movl %ebx, %fs /* Save the old exception list */ movl %fs:KPCR_EXCEPTION_LIST, %ebx pushl %ebx /* Get a pointer to the current thread */ movl %fs:KPCR_CURRENT_THREAD, %edi . . . . . . /* Save the old previous mode */ movl $0, %ebx movb %ss:KTHREAD_PREVIOUS_MODE(%edi), %bl pushl %ebx . . . . . . /* Save other registers */ pushl %eax pushl %ecx pushl %edx pushl %ds pushl %es pushl %gs movl %dr7, %eax pushl %eax /* Dr7 */ . . . . . . movl %esp, %ebx movl %esp, %ebp /* Save the old trap frame. */ cmpl $0, %edi je .L7 movl %ss:KTHREAD_TRAP_FRAME(%edi), %edx pushl %edx jmp .L8 .L7: pushl $0 .L8: /* Save a pointer to the trap frame in the current KTHREAD */ cmpl $0, %edi je .L6 movl %ebx, %ss:KTHREAD_TRAP_FRAME(%edi) .L6: /* Call the C exception handler */ pushl %esi pushl %ebx call _KiTrapHandler addl $4, %esp addl $4, %esp /* Get a pointer to the current thread */ movl %fs:KPCR_CURRENT_THREAD, %esi /* Restore the old trap frame pointer */ popl %ebx movl %ebx, KTHREAD_TRAP_FRAME(%esi) /* Return to the caller */ jmp _KiTrapEpilog 從14號異常的入口_KiTrap14開始,如果對照著KTRAP_FRAME數據結構的定義跟蹤所有的push指令,就可以看到二者完全相符。而“movl %esp, %ebx”和“movl %esp, %ebp”這兩條指令使寄存器EBX和EBP都指向了陷阱框架的地址起點,此后的push指令對堆棧的操作就不在KTRAP_FRAME數據結構的范圍之內了。 這里EDI指向當前進程的KTHREAD數據結構,KTHREAD_TRAP_FRAME(%edi)則為該數據結構中的Trapframe字段。代碼中先由一條mov指令將其裝入寄存器EDX,再把它壓入堆棧。這樣,就在堆棧上為其保存了一個副本,這與把它保存在局部變量中實質上是一樣的。在此過程中雖然用到了EDX,卻沒有涉及陷阱框架中的EDX映像。而在執行了KiTrapHandler()以后,則由后面的pop指令和mov指令將所保存的副本恢復到KTHREAD數據結構的Trapframe字段中。

?? 快捷鍵說明

復制代碼 Ctrl + C
搜索代碼 Ctrl + F
全屏模式 F11
切換主題 Ctrl + Shift + D
顯示快捷鍵 ?
增大字號 Ctrl + =
減小字號 Ctrl + -
亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频
欧美大片一区二区| 1区2区3区国产精品| 国产精品妹子av| 日韩精品免费视频人成| 97久久超碰国产精品| 日韩欧美一区二区久久婷婷| 亚洲天堂a在线| 国产精品一区二区三区乱码 | 一区二区三区资源| 寂寞少妇一区二区三区| 欧美日韩一区二区三区不卡| 中文字幕第一区二区| 国产一区二区在线看| 欧美高清精品3d| 亚洲观看高清完整版在线观看| 91免费在线视频观看| 精品电影一区二区| 青娱乐精品视频在线| 在线免费不卡电影| 亚洲精品免费看| 91在线高清观看| 成人免费一区二区三区视频| 成人h版在线观看| 日本一区二区视频在线| 国产99久久久国产精品潘金网站| 日韩一卡二卡三卡四卡| 午夜免费欧美电影| 欧美日韩在线免费视频| 一区二区三区不卡在线观看 | 91麻豆精品国产综合久久久久久| 亚洲永久精品大片| 色94色欧美sute亚洲线路二| 亚洲欧美国产高清| 91国产免费看| 亚洲一区二区三区美女| 欧美巨大另类极品videosbest| 亚洲国产视频a| 精品视频在线免费| 偷拍一区二区三区四区| 欧美久久久久久久久久| 日韩—二三区免费观看av| 欧美美女直播网站| 日本成人在线网站| 精品日产卡一卡二卡麻豆| 精品在线一区二区三区| 国产亚洲欧美一区在线观看| 成人福利在线看| 国产乱人伦偷精品视频不卡| wwwwww.欧美系列| 国产精品一区在线观看你懂的| 久久久亚洲国产美女国产盗摄 | 国产女人18毛片水真多成人如厕| 成人爱爱电影网址| 一区二区在线观看免费| 欧美另类z0zxhd电影| 麻豆成人久久精品二区三区红 | 欧美精三区欧美精三区| 九一久久久久久| 中文字幕一区二区不卡| 欧美性感一区二区三区| 九九精品一区二区| 最新高清无码专区| 3751色影院一区二区三区| 久久成人久久鬼色| 亚洲图片另类小说| 日韩一区二区免费视频| 国产成人8x视频一区二区| 亚洲国产cao| 国产欧美综合色| 欧美性高清videossexo| 狠狠v欧美v日韩v亚洲ⅴ| 亚洲日本在线a| 日韩欧美一二三四区| 99精品视频中文字幕| 日本不卡视频在线观看| 亚洲色图欧美在线| 精品人伦一区二区色婷婷| av亚洲精华国产精华| 麻豆91在线看| 亚洲精品视频在线观看网站| 久久婷婷国产综合国色天香 | 精品国产污网站| 一本到三区不卡视频| 在线观看网站黄不卡| 美日韩一区二区| 一卡二卡三卡日韩欧美| 国产日本欧美一区二区| 日韩午夜av一区| 欧美在线免费观看视频| 99久久精品免费观看| 国产麻豆91精品| 强制捆绑调教一区二区| 亚洲午夜av在线| 亚洲欧洲韩国日本视频| 国产欧美日产一区| 亚洲精品一线二线三线无人区| 欧美日本精品一区二区三区| 色噜噜夜夜夜综合网| 成人美女在线视频| 国产风韵犹存在线视精品| 青青草伊人久久| 天堂在线一区二区| 亚洲自拍与偷拍| 亚洲人成网站精品片在线观看| 欧美国产在线观看| 国产婷婷一区二区| 久久久国产综合精品女国产盗摄| 日韩一区二区在线免费观看| 欧美精品18+| 欧美精品第一页| 91精品国产综合久久久久久漫画 | 精品久久一二三区| 91麻豆精品国产自产在线 | 欧美日韩欧美一区二区| 在线亚洲一区观看| 欧美性大战久久| 欧美午夜一区二区| 欧美性做爰猛烈叫床潮| 欧美少妇xxx| 91精选在线观看| 欧美一区二区三区四区久久 | 国产精品系列在线| 国产精品全国免费观看高清 | 91看片淫黄大片一级在线观看| 不卡高清视频专区| 色综合天天做天天爱| 在线观看日韩av先锋影音电影院| 色呦呦一区二区三区| 精品视频全国免费看| 日韩丝袜美女视频| 日本一区二区电影| 一区二区三区精品视频| 伊人色综合久久天天| 午夜天堂影视香蕉久久| 麻豆成人在线观看| 国产成人av电影在线观看| 91一区二区三区在线观看| 欧美日韩午夜在线视频| 日韩欧美自拍偷拍| 国产精品视频在线看| 洋洋成人永久网站入口| 裸体在线国模精品偷拍| 国产夫妻精品视频| 欧美无人高清视频在线观看| 欧美一区二区三区思思人| 国产日韩一级二级三级| 亚洲精品国产第一综合99久久| 偷拍一区二区三区四区| 国产精品自拍在线| 欧美午夜寂寞影院| 久久精品在这里| 亚洲午夜影视影院在线观看| 激情小说欧美图片| 一本一道久久a久久精品综合蜜臀| 制服丝袜亚洲色图| 中文字幕在线观看一区二区| 天天影视网天天综合色在线播放| 国产在线一区二区综合免费视频| 99这里只有精品| 欧美一区二区在线播放| 亚洲欧美激情一区二区| 国产一区二区三区免费观看 | 成人av资源下载| 欧美精品一级二级| 国产精品久久久久婷婷二区次| 日韩国产欧美视频| 91丨九色丨尤物| 久久精品水蜜桃av综合天堂| 婷婷丁香久久五月婷婷| 99久久国产综合精品麻豆| 欧美精品一区二区三区在线 | 这里只有精品99re| 亚洲精品日产精品乱码不卡| 国产二区国产一区在线观看| 538prom精品视频线放| 亚洲精品日韩专区silk| 国产白丝精品91爽爽久久| 5月丁香婷婷综合| 亚洲精品国久久99热| 国产盗摄女厕一区二区三区| 日韩欧美久久一区| 亚洲成人一区在线| 色婷婷亚洲婷婷| 国产精品三级电影| 国产东北露脸精品视频| 欧美成人精品1314www| 日韩一区精品字幕| 欧美日韩在线观看一区二区| 亚洲精品亚洲人成人网| 91在线小视频| 综合自拍亚洲综合图不卡区| 成人午夜激情影院| 中文字幕免费在线观看视频一区| 激情综合色丁香一区二区| 欧美一区二区网站| 免费高清在线一区| 日韩精品一区在线| 免费观看在线综合色| 欧美一区二区在线视频| 久久成人综合网|