再談Windows NT/2000環境切換(http://webcrazy.yeah.net)

來源:互聯網
上載者:User
再談Windows NT/2000環境切換
                           WebCrazy(tsu00@263.net)

    線程是Windows NT/2000環境切換的最基本單位。在<<淺析Windows NT/2000環境切換>>(Nsfocus Magazine 12)一文中,我只對進程CR3切換進行了較詳細的討論,但未涉及線程調度的內容,本文將盡量講述這些部分內容。在這之前,還是先看看以下的代碼:

    //-----------------------------------------------
    //
    // EnumThreads-information from KPEB and KTEB
    // Only test on Windows 2000 Server Chinese Edition
    // Build 2195(Free)!Programmed By WebCrazy
    // (tsu00@263.net) on 5-23-2000!
    //
    //-----------------------------------------------

    #define KTEBListOffsetKPEB         0x50
    #define PIDOffset                  0x9c  
    #define KPEBListOffset             0xa0
    #define ProcessNameOffset          0x1fc    

    #define StackTopOffset             0x18
    #define StackBtmOffset             0x1c
    #define UserTEBOffset              0x20
    #define StackPtrOffset             0x28
    #define KTEBListOffset             0x1a4
    #define KTEBPIDOffset              0x1e0
    #define TIDOffset                  0x1e4

    void DisplayThreadFromKPEB(void *kpeb)
    {

        char ProcessName[16];
        ULONG PID;
        ULONG TID;
        ULONG StackBtm,StackTop,StackPtr,UserTEB;

        PLIST_ENTRY KTEBListHead, KTEBListPtr;

        KTEBListHead=KTEBListPtr=(PLIST_ENTRY)((int)kpeb+KTEBListOffsetKPEB);

        do
          {
           void *kteb;
           kteb=(void *)((*(ULONG *)KTEBListPtr)-KTEBListOffset);
           TID=*(ULONG *)(((char *)kteb)+TIDOffset);
           StackBtm=*(ULONG *)(((char *)kteb)+StackBtmOffset);
           StackTop=*(ULONG *)(((char *)kteb)+StackTopOffset);
           StackPtr=*(ULONG *)(((char *)kteb)+StackPtrOffset);
           UserTEB=*(ULONG *)(((char *)kteb)+UserTEBOffset);
           memset(ProcessName, 0, sizeof(ProcessName));
           memcpy(ProcessName, ((char *)kpeb)+ProcessNameOffset, 16);
           PID=*(ULONG *)(((char *)kpeb)+PIDOffset);
           // or PID=*(ULONG *)(((char *)kteb)+KTEBPIDOffset);
           DbgPrint("  %04X  %08X   %08X  %08X  %08X  %08X  %s(%X)/n",
                    TID,kteb,StackBtm,StackTop,StackPtr,UserTEB,ProcessName,PID);
           KTEBListPtr=KTEBListPtr->Flink;
          }while (KTEBListPtr->Flink!=KTEBListHead);
     }

     void EnumThreads()
     {

        PLIST_ENTRY KPEBListHead, KPEBListPtr;

        if(((USHORT)NtBuildNumber)!=2195){
           DbgPrint("Only test on Windows 2000 Server Build 2195!/n");
           return;
        }

       DbgPrint("/n  TID   KTEB Addr  StackBtm  StackTop  StackPtr  User TEB  Process
                  Name  (PID)");
       DbgPrint("/n  ----  --------   --------  --------  --------  --------  -------
                  ----- -----/n");

       KPEBListHead=KPEBListPtr=(PLIST_ENTRY)(((char *)PsInitialSystemProcess)+KPEBListOffset);
       while (KPEBListPtr->Flink!=KPEBListHead) {
           void *kpeb;
           kpeb=(void *)(((char *)KPEBListPtr)-KPEBListOffset);
           DisplayThreadFromKPEB(kpeb);
           DbgPrint("/n");
           KPEBListPtr=KPEBListPtr->Flink;
       }
    }

    這段代碼列出的EnumThreads函數在Windows 2000 Server Build 2195中的輸出結果如下:

  TID   KTEB Addr  StackBtm  StackTop  StackPtr  User TEB  Process Name(PID)
  ----  --------   --------  --------  --------  --------  -----------------
  0004  FE4E19E0   F9019000  F901C000  F901B9C4  00000000  System(8)
  000C  FE4E0C80   F9021000  F9024000  F9023D34  00000000  System(8)
  0010  FE4E0A00   F9025000  F9028000  F9027D34  00000000  System(8)
  0014  FE4E0780   F9029000  F902C000  F902BD34  00000000  System(8)
  0018  FE4E0500   F902D000  F9030000  F902FD34  00000000  System(8)
  001C  FE4E0280   F9031000  F9034000  F9033D34  00000000  System(8)
                               .
                               .(略)
                               .

    從運行結果可知EnumThreads主要是實現將系統當前所有的線程列出,上面的輸出格式與SoftICE的thread命令一致。代碼中使用了一些Undocumented的KPEB/KTEB資料項目:

    1.進程的線程鏈表
      這是一個LIST_ENTRY結構的項(佔用兩個32位的指標即8位元組),位於KPEB後的50h處,上面代碼由KTEBListOffsetKPEB表示。
    2.線程鏈表相對KTEB的位移
      位於KTEB後1a4h處(KTEBListOffset定義)。

    輸出結果中的如StackBtm、StackTop、StackPtr等請參閱<<SOFTICE COMMAND REFERENCE>>,它們在KTEB中的位置請直接看代碼前的定義。

    在理解了EnumThreads程式段與我上次給出的實現EnumProcesses的底層代碼後,也差不多明白了Windows NT/2000是如何組織、管理進程與線程了,這對理解線程調度可是至關重要的。雖然如此但要討論區對話調度,還是再看看幾個重要的資料(我不再具體說明如何取得這些資料具體位置的方法了,如果您很想知道還是建議您再看看<<淺析Windows NT/2000環境切換>>):

    1. 進程狀態(Status) //KPEB+65h UCHAR

    典型的進程狀態有:Running、Ready與Idle。
    應該說明的是在單一處理器的機子中處於Running狀態的進程只有一個,且其不受KPEB中的這個值約束,系統通過調用IoGetCurrentProcess核心常式獲得。我在<<再談Windows NT/2000內部資料結構>>對IoGetCurrentProcess進行了比較詳細的介紹。

    當KPEB中Status值為0時,進程狀態為Ready;為1時進程狀態為Idle;為2時進程狀態為Transition等等。
    正像前面所提及的線程是Windows NT/2000環境切換的基本單位,實際上系統並不執行進程,進程狀態和以下將要提及的進程優先順序是很抽象的概念,只是系統調用時對線程的範圍限制,Microsoft提出這些概念我想主要是隱藏系統內部Thread調度行為。但在有線程狀態的前提下其也不是說就隨便附個值即可。曾有次我將System進程的狀態從Ready改為Transition後,只能眼巴巴的看著螢幕上的程式碼,不能存檔。因為此時系統已經變得懶洋洋的,不再響應我的千呼萬喚了。

   2. 線程狀態   //KTEB+2dh UCHAR

    在KTEB中有一成員State主要是指出當前線程狀態,其位於KTEB+2d處(單位元組)。它主要有如下幾個值(值取自SoftICE的輸出結果):

       0 - Initialized (表示State的值為0時,表示線程狀態為Initialized,以下類同)
       1 - Ready
       2 - Running
       3 - StandBy
       4 - Terminated
       5 - Waiting
       6 - Transition

    在David Solomon與Mark Russinovich的<<INSIDE MICROSOFT WINDOWS 2000,THIRD EDITION>>中是如此描述的:

    To quote:
    --------
    The thread states are as follows:

    Ready
    When looking for a thread to execute, the dispatcher considers only the pool of threads in the
 ready state. These threads are simply waiting to execute.

    Standby
    A thread in the standby state has been selected to run next on a particular processor. When
the correct conditions exist,the dispatcher performs a context switch to this thread. Only one
thread can be in the standby state for each processor on the     system.

    Running
    Once the dispatcher performs a context switch to a thread, the thread enters the running
state and executes. The thread's execution continues until the kernel preempts it to run a higher priority thread, its quantum ends, it terminates, or it voluntarily enters the wait state.

    Waiting
    A thread can enter the wait state in several ways: a thread can voluntarily wait on an object to synchronize its execution, the operating system (the I/O system, for example) can wait on the thread's behalf, or an environment subsystem can direct the thread to suspend itself. When the thread's wait ends, depending on the priority, the thread either begins running immediately or is
moved back to the ready state.

    Transition
    A thread enters the transition state if it is ready for execution but its kernel stack is
paged out of memory. For example, the thread's kernel stack might be paged out of memory. Once
its kernel stack is brought back into memory, the thread enters the ready state.

    Terminated
    When a thread finishes executing, it enters the terminated state. Once terminated, a thread
object might or might not be deleted. (The object manager sets policy regarding when to delete
the object.) If the executive has a pointer to the thread object, it can reinitialize the thread
object and use it again.

    Initialized
    Used internally while a thread is being created.
    --------

    我以下主要對waiting的狀態進行分析:

    在Windows NT/2000中線程能調用KeWaitForSingleObject、KeWaitForMultipleObjects等自動放棄自己的執行時間總量(Quantum)。系統當前執行的線程由系統中的Processor Control Block(PRCB,注意與 Processor Control Region區別)中的CurrentThread成員指定。還記得我介紹過的如何得到當前線程嗎(取FS:124H中的DWORD值)?其實就是指向這個CurrentThread成員了。PRCB的定義KPRCB在ntddk.h中。系統通過如下函數獲得KPRCB指標:

    _KeGetCurrentPrcb
    0008:80465310  MOV       EAX,[FFDFF020]  取KPCR(Processor Control Region)成員Prcb
    0008:80465315  RET

    系統當前線程狀態為Running。其它線程狀態由幾個(通常最大為THREAD_WAIT_OBJECTS+1個,否則就會出現BSOD,但Microsoft還定義了個MAXIMUM_WAIT_OBJECTS,這就要看您傳遞給系統的參數了)KWAIT_BLOCK結構表示,這些值以及以下將要談到的表示線程等待理由的KWAIT_REASON也均可從ntddk.h中找到。線程KWAIT_BLOCK結構資料處於KTEB+6ch處。上次我提到的發生context switch的兩種情況,要麼可以用event,semphore等同步對象,要麼可以用timer核心對象表示,這樣可以形成線程等待對列,來表示線程目前狀態。

    由於KWAIT_BLOCK、KWAIT_REASON、還有event、timer等在Windows NT/2000中是少有的幾個Documented成員,您在知道KWAIT_BLOCK的具體位置後,大可以自己讀出線程等待隊列。不過SoftICE已經為你呈現了所有這些內部結構了。

    從上分析對於線程狀態,牽涉到比較多的內容,我將一部分分析抄錄如下:

    :u _KeReadStateThread
    _KeReadStateThread
    0008:8042F029  MOV       EAX,[ESP+04]
    0008:8042F02D  MOV       AL,[EAX+04]
    0008:8042F030  RET       0004

   :bpx _KeReadStateThread if (tid==_tid)

   :bl  //這命令後退出調試器
   00)   BPX _KeReadStateThread  IF (TID==0x3BC)

   Break due to BPX _KeReadStateThread  IF (TID==0x3BC) (ET=22.28 seconds)

   //分析一下_KeReadStateThread的第一個參數(也是唯一的參數)
   :what dword(@(esp+04)) //您應該理解每個線程,每個時刻線程狀態由哪個核心對象確定都是不固定的吧
   The value FF6811E0 is (a) Kernel Timer object (handle=0230) for explorer(398)
                                     |                 |
                                     |_Timer核心對象   |_這個對象在explorer進程中的控制代碼
  
   :timer dword(@(esp+04)) 
   Timer Object at FF6811E0
   Dispatcher Type: 08
   Dispatcher Size: 000A
   Signal State: Signaled 
         .
         .(略)
         .

   //SoftICE中的timer命令只是讀出timer對象資料
   //所以你可以直接讀DISPATCHER_HEADER(Common dispatcher object header)中的SignalState成員(見ntddk.h)
   //即下面這個命令

   #byte(@(@(esp+04)+4))
   00000001  0000000001  ""    //1代表Signaled,試過將其從1改為0嗎?(從Signaled改為Not Signaled)

   好了以上分析的這個線程目前狀態取決於timer對象(Object Pointer:0xFF6811E0)的狀態(Jeffrey Richter說Signaled表示When time comes due)。我已經是從最簡單的方面來分析了,很多線程目前狀態往往不僅僅取決於一個對象,SoftICE中Thread Wait List也即是這個概念。

   談了這麼多讓線程等待的對象,現在來說說KWAIT_REASON,在KTEB中有專門表示thread wait reason的一個成員,它位於KTEB位移57h處,佔用一個CHAR的空間,嚴格的說這才是真正表示致使線程處於wait狀態的原因,上面的那麼多的討論只不過是解釋什麼核心對象造成這一wait reason的。DDK Documentation是這樣定義wait reason的

   WaitReason
   Specifies the reason for the wait. A driver should set this value to Executive, unless it is doing work on behalf of a user and is running in the context of a user thread, in which case it should set this value to UserRequest.

   其中所提及的是否user thread是由KTEB另一個位於55h位移處的單字元成員,它由0代表核心模式,1代表使用者模式。上面提到了wait reason在驅動程式編程中最常見(並不是系統核心態代碼中最多見的)的兩個值:Executive與UserRequest,至於其其它值請參閱ntddk.h。

   3.進程優先順序(KPEB+62h)、線程基本優先順序(BasePriority,KTEB+68h)、線程動態優先順序(Dyn Priority,KTEB+33h)

   這三個值各自佔用一個位元組。其中Thread Dyn Priority在Spy++中顯示為Current Priority,而在Microsoft的WinDbg與Windows 2000 Server Resource Kit中的一些小工具,如pstat.exe等中則直接用Priority表示,但在SoftICE中則顯示為Dyn Priority。由於直接用Priority又不容易表達這麼多的優先順序。鑒於我文中所有內容都基於SoftICE的分析,我在本文中均沿用SoftICE中的名稱。其實Microsoft在KTEB結構中還提供PriorityDecrement等其它使系統隨時動態更改當前優先順序,這也是我比較喜歡使用Dyn Priority的一個原因之一。至於這些優先順序的詳細討論請參閱參考資料中的<<WINDOWS NT DEVICE DRIVER DEVELOPMENT>>,其對核心態的這些值的作用進行了比較多的說明。

   4.線程親緣性(Affinity)
  
    由於我目前尚未有條件測試多處理機的情況,我也不好在這多說,有條件的朋友我很希望您能說說。

   5.線程擁有的時間總量(Thread Quantum)

    單位元組,位於KTEB+6b處。指出CPU可以讓線程調度的時間總量(Quantum)。在Processor Control Block中系統存有三個_KTHREAD(KTEB)結構的成員CurrentThread、NextThread與IdleThread,分別代表系統當前處理器正在執行的線程、將要被調用的線程與系統空閑(Idle)線程,Idle線程通常只是簡單的調用KiIdleLoop,直到系統新的中斷來臨,以對其它線程進行調用。Windows NT/2000中調用KiQuantumEnd判斷當前線程是否使用完自己的時間總量,如果當前線程已執行完Quantum,則在KPRCB中NextThread非空時返回NextThread,作為系統調用的下一個線程。系統通過調用KiFindReadyThread尋找下一個處於Ready狀態的線程。

    _KiQuantumEnd
    0008:804315B9  PUSH      EBP
    0008:804315BA  MOV       EBP,ESP
    0008:804315BC  PUSH      ECX
    0008:804315BD  PUSH      EBX
    0008:804315BE  PUSH      ESI
    0008:804315BF  PUSH      EDI
    0008:804315C0  MOV       EAX,DS:[FFDFF020]               ;KPRCB->EAX
    0008:804315C6  MOV       EDI,EAX                         ;KPRCB->EDI
    0008:804315C8  MOV       EAX,FS:[00000124]               ;Current Thread's KTEB->EAX
    0008:804315CE  MOV       ESI,EAX                         ;Current Thread's KTEB->ESI
    0008:804315D0  CALL      [__imp__KeRaiseIrqlToDpcLevel]  ;將IRQL提升到DISPATCH_LEVEL,學過
                                                             ;ddk的朋友應該都比較熟悉
    0008:804315D6  XOR       EBX,EBX
    0008:804315D8  MOV       [EBP-01],AL                     ;Save Old IRQL
    0008:804315DB  CMP       [ESI+6B],BL                     ;判斷當前線程的Quantum
    0008:804315DE  JG        804315F2                        ;在Quantum小於等於0時擷取NextThread
    0008:804315E0  MOV       EAX,[ESI+44]
    0008:804315E3  CMP       [EAX+69],BL
    0008:804315E6  JZ        80431608
    0008:804315E8  CMP       BYTE PTR [ESI+33],10
    0008:804315EC  JL        80431608
    0008:804315EE  MOV       BYTE PTR [ESI+6B],7F
    0008:804315F2  MOV       ESI,[EDI+08]                     ;KPRCB's NextThread->ESI
    0008:804315F5  CMP       ESI,EBX                          ;KPRCB's NextThread是否為空白
    0008:804315F7  JNZ       80431601
    0008:804315F9  MOV       CL,[EBP-01]
    0008:804315FC  CALL      @KiUnlockDispatcherDatabase
    0008:80431601  MOV       EAX,ESI                          ;將NextThread返回
    0008:80431603  POP       EDI
    0008:80431604  POP       ESI
    0008:80431605  POP       EBX
    0008:80431606  LEAVE
    0008:80431607  RET
    0008:80431608  MOVSX     EDX,BYTE PTR [ESI+33]
           .
           .(代碼很長,牽涉到調度演算法,看來只能您自己去認真看看了)
           .            

   6.線程所屬進程的KPEB(KTEB+22ch處)

    主要是更容易的在KTEB與KPEB間進行些資料交換。

    其實上面部分內部資料在Linux中也可以找到實現對應功能的體現,如Windows NT/2000中的Thread Quantum對應Linux task_struct中counter成員等等。我在<<淺析Windows NT/2000環境切換>>中就指出過Windows NT/2000實際上發生任務切換的情況只有兩種,我也在上次給出了時間中斷的部分代碼,給出了SwapContext(主要是CR3切換)代碼。Windows中的每一個進程都分別擁有私人的記憶體空間,私人的核心對象(控制代碼表Handle Table)等等,這些都是在環境切換的基礎上實現的,也是一個作業系統Robustness and Reliability的基礎。http://www.research.microsoft.com/中有很多文章對這有過驗證,大可以翻翻看看。關於這部分的實現您還可以再看看如下的一些常式(限於篇幅我不再列出代碼):

    ⊙ KiFindReadyThread
    ⊙ KiReadyThread
    ⊙ KiSwapThread
    ⊙ SwapContext

    其中SwapContext與KiSwapThread是系統真正切換的代碼,是不是經常在stack trace中看到這個函數,在此你應該可以比較容易的明白了吧。(關於stack trace請參閱DDK Documentation中的Anatomy of a Stack Trace段或SoftICE Command Reference中的stack與thread命令的解釋)

    另一種線程自動放棄執行的情況,跟蹤KeWaitForSingleObject、KeSetEvent等就相對比較容易了,拿Event Object舉個例子吧,由於知道Event的結構,在您的實驗用機上您大可以隨便更改DISPATCHER_HEADER中的SignalState成員更改Object狀態,看您要它是Clear還是Signalled了(上面我給出了如何用SoftICE實現)。甚至在您理解了我開頭給出的代碼(其實說白了只是讀出一雙向鏈表中的資料,如果是在Linux中,我相信你也根本沒有耐心看到這兒了),還有理解了KWAIT_BLOCK所定義Thread Wait List後,只要給您一核心調試器,我相信您想阻塞哪個線程就哪個線程了,加上理解了線程優先順序後,您想讓哪個線程多佔用CPU時間都可以。不過我可不保證您這時候機子是否還Robustness and Reliability了。

    還是順便提一下,Windows NT/2000中調度代碼運行在DISPATCH_LEVEL IRQL上,已防止通常運行在PASSIVE_LEVEL的普通代碼對其的中斷。

    Jeffrey Richter在<<PROGRAMMING APPLICATIONS for MICROSOFT WINDOWS,FOURTH EDITION>>中曾指出:

      ⊙ Microsoft doesn't fully document the behavior of the scheduler.
      ⊙ Microsoft doesn't let applications take full advantage of the scheduler's features.
      ⊙ Microsoft tells you that the scheduler's algorithm is subject to change so that you
can code defensively.

    從這幾點看Microsoft並沒將調度行為固定,實際上Windows NT 4.0與Windows 2000在調度演算法上就有不同,而本文所提及的所有代碼均取自或只在Windows 2000 Server Build 2195中測試過。

    我想對DDK有過學習的朋友應該都知道PsSetCreateProcessNotifyRoutine與PsSetCreateThreadNotifyRoutine這兩個讓使用者註冊系統建立與刪除進程或線程時調用回呼函數的Fully Documented常式,知道它們就是Mark Russinovich的NTPMON的實現的最主要的兩個函數吧。其實在Windows 2000中Microsoft還提供實作類別似功能的與環境切換有關的函數,即KeSetSwapContextNotifyRoutine和KeSetThreadSelectNotifyRoutine,不過這兩個函數卻是Undocumented的,並且只能在Windows 2000的特定版本上運行(請查閱MSDN Magazine)。

    在Linux中所有的代碼都是公開的,但真正要理解調度代碼還是很困難的,何況本身就比Linux複雜(我尚未瀏覽過Linux 2.4.X源碼,我這樣說純系個人目前感覺)而且只能在一大堆彙編代碼中搜尋的Windows NT/2000,其分析的難度真的是可想而知的。所以我希望您能將文中錯誤或遺漏說明的地方告訴我(tsu00@263.net),謝謝!
  
    參考資料:
      1.Jeffrey Richter
          <<PROGRAMMING APPLICATIONS for MICROSOFT WINDOWS,FOURTH EDITION>>
      2.Peter Viscarola and Anthony Mason <<WINDOWS NT DEVICE DRIVER DEVELOPMENT>>
      3.Numega <<SOFTICE COMMAND REFERENCE>>
      4.Windows 2000 DDK Documentation
      5.David Solomon and Mark Russinovich <<INSIDE MICROSOFT WINDOWS 2000,THIRD EDITION>>

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.