淺析Windows NT/2000環境切換


本文假設您已經了解Windows NT/2000系統體系,對Windows NT/2000內部KPEB/KTEB等數據結構與內核工作方式已有一定的概念,對80x86保護模式,Intel/AT&T格式匯編語言有過學習,能熟練使用SoftICE for Windows NT,且曾經接觸過Microsoft Visual Studio及其附帶工具,翻閱過Linux內核代碼,如果您對這些方面不甚了解,請自行參閱相關書籍。
   環境切換(Context Switch)牽涉到很多方面的內容,本文僅對與其有關的幾個數據進行詳細的討論,並給出取得這些數據的部分程序段,還列出Windows 2000的少量環境切換代碼。另外文中討論的系統內部數據均未來自Microsoft官方文檔,在Windows NT/2000的下個版本甚至目前各版本間均會有差別,所以我盡量詳細的將文中所涉及的軟硬件列於下面,所有因硬件體系、軟件版本不同等因素引起的差異,請自行根據您的情況予以調整。
⊙ x86平台單處理機Windows 2000 Server Build 2195
⊙ Numega SoftICE 4.05 for Windows NT/2000 Build 334
⊙ Linux 2.0.30內核
⊙ Datarescue IDA 4.0.4.362
⊙ Microsoft Visual Studio 6.0 SP3
⊙ Windows 2000 DDK
   80x86產生的環境切換有以下幾種可能:
   1.當前任務執行一個FAR CALL或JMP指令,而選擇器指向一個TSS描述符或一個任務門。
   2.當前任務執行IRET指令返回先前任務,IRET只在EFLAGS寄存器中的NT位置1時產生切換。
   3.發生一個中斷或異常情況,並且IDT項是個任務門。
   Linux內核中有如下代碼:
   /*
   /usr/src/linux/include/asm-i386/system.h
   僅列出單處理器實現代碼
   */
   #define switch_to(prev,next) do { /
   __asm__("movl %2,"SYMBOL_NAME_STR(current_set)"/n/t" /
   "ljmp %0/n/t" /
   "cmpl %1,"SYMBOL_NAME_STR(last_task_used_math)"/n/t" /
   "jne 1f/n/t" /
   "clts/n" /
   "1:" /
   : /* no outputs */ /
   :"m" (*(((char *)&next->tss.tr)-4)), /
   "r" (prev), "r" (next)); /
   /* Now maybe reload the debug registers */ /
      .
      .
      .
   } /
   } while (0)
   這段代碼使用了上面討論的第一種情況。
   眾所周知,Linux是個開放源代碼的操作系統,而Microsoft則沒如此“大方”,但我們仍能對其進行些逆向工程,可喜的是網上目前已經有很多人對此有過研究,現摘錄Mark Russinovich部分成果( http://www.sysinternals.com/tips.htm ):
   //
   // NT's main
   // NTOSKRNL main
   //
   int main( boot parameters )
   {
      //
      // Fire up NT!
      //
      KiSystemStartup();
      return 0;
   }
   從中可看出ntoskrnl.exe(PE格式,可方便的使用反匯編工具進行分析)是NT OSLOADER真正調用內核的開始,其對文件對象(File)、作業對象(Job)、進程對象(Process)、線程對象(Thread)、纖程對象(Fiber)、文件映射對象(FileMapping)、事件對象(Event)、互斥對象(Mutex)、信號對象(Semaphore)等許多內核對象進行管理,其也負責線程調度,內存管理,進程間通信等所有操作系統功能,讓它們協調工作,我們要討論的線程切換代碼也在此模塊中。
   用IDA等對ntoskrnl.exe進行反匯編所得的結果,其分析的工作量恐怕大家都是可想而知的。在我們討論Windows 2000環境切換詳細代碼前,還是先讓我們看看以下幾個重要的與環境切換有關的系統數據:
   1 進程Context
   進程Context是指80x86在保護模式下內存分頁機制中當前進程的頁目錄所在的物理地址,其存放在系統CR3寄存器中,在Windows 2000中所處的位置為KPEB偏移后18h處,看看SoftICE的輸出結果吧(限於篇幅,我對輸出結果進行了刪減,但仍對重要數據進行注解,應注意的是與您當前運行的程序等系統環境密切相關,隨機性很強,下同):
   :cpu //顯示當前cpu的寄存器值
   Processor 00 Registers
   ----------------------
   CS:EIP=0008:80069582 SS:ESP=0010:8046FD98
   EAX=8046BDF0 EBX=FFDFF000 ECX=FFDFF878 EDX=0000BA5A
   ESI=8046BDF0 EDI=8046BB60 EBP=FFDFF800 EFL=00000213
   DS=0023 ES=0023 FS=0030 GS=0000
   CR0=8000003B PE MP TS ET NE PG
   CR2=76EE18EC
   CR3=00030000
        |
        |_當前進程的CR3
   CR4=000002D1 VME PSE MCE PGE
      .
      .
      .
   :proc idle
   Process KPEB   PID Threads Pri User Time  Krnl Time  Status
   *Idle  8046BB60 0   1    0  00000000  0000BA5A  Running
    |      |
    |      |_Idle進程的KPEB
    |_系統中當前進程(SoftICE中用不同顏色突出,且前面有個*)
   :dd 8046bb60+18 l 4 //dd Idle's KPEB+18h
   0010:8046BB78 00030000 00000000 00000000 00000000 ................
              |
              |_Idle進程Context
   :addr
   CR3    LDT Base:Limit KPEB Addr PID Name
   00030000           FE4E1C60  0008 System
   02D59000           FF8E6540  0090 smss
   01D41000           FF8E17E0  00AC csrss
   00686000           FE51BAE0  00C0 winlogon
   0095D000           FF8A7AE0  00DC services
   0276E000           FF8A5D60  00E8 lsass
   00394000           FF881020  0180 svchost
   02CAE000           FF884020  01A4 SPOOLSV
   00882000           FF85B560  01D0 msdtc
   02993000           FF83F020  0238 svchost
   00D2F000           FF83D760  024C llssrv
   0063A000           FF837860  0274 regsvc
   02EFA000           FF6ECD60  0318 dfssvc
   00A5E000           FF823A20  0328 inetinfo
   03612000           FF6AF860  0384 explorer
   003A2000           FF68E460  03B4 internat
   003A7000           FF68CD60  0130 OSA
   008C1000           FF6769A0  03E8 svchost
   01BAA000           FF65A020  01C0 cmd
   00822000           FF86C960  038C conime
   03362000           FF6B3540  0388 notepad
   *00030000          8046BB60  0000 Idle
      |
      |__當前進程Context,是不是與上一命令輸出結果一致。
   可以用同樣的方法進行再次進行驗證。
   2.Context Switches Times 線程已被操作系統調度次數
   當每次操作系統調用線程時,都會將這個值加一的。Visual Studio所附的工具Spy++,在Thread窗口中Thread Properties中Context Switches指出系統中該線程已調度的次數(Spy++只有在Windows NT/2000中運行時才會顯示出這個值,9x中則沒有)。Switch Times在系統中所處的位置在KTEB的偏移4ch處。
   :thread idle
   TID  Krnl TEB StackBtm StkTop  StackPtr User TEB Process(Id)
   0000 8046BDF0 8046D040 80470040 8046FD90 00000000 Idle(00)
   :dd 8046bdf0+4c l 4
   0010:8046BE3C 0000E778 00000000 00000002 00000000 x...............
              |
              |_指出當前Idle線程已經被系統調用0E778h(十進制59256)次了。用e命令改改再看看Spy++的輸出結果!
   3.線程所屬進程的KPEB與進程名 //分別位於KTEB+44h與KPEB+1fch處
   具體見我在《再談Windows NT/2000內部數據結構》(Nsfocus Magazine 11)一文所述。
   應該指出的是,以上討論只是針對Windows 2000 Server Build 2195的,如果您的系統不是的話或想知道如何得到這些值的具體位置,請參閱我以下的敘述的方法:
   通過找突破口,正像上面我所描述的Context Switches Times在Spy++中顯示的一樣,然后可以用逆向工程法,我也是用這個方法來取得這個具體位置的。
   舉個例子吧!我曾經對SoftICE for NT中的addr命令輸出結果(包含進程Context)來自何處感到困惹,也曾經在國外的一些著名的新聞組中提問過,不過至今仍沒人應答(可能是我的英文水平太差,人家看不懂什么意思吧!)
   addr命令輸出結果見上。
   后來無奈之下我還是想到CR3(存放進程頁目錄物理地址的寄存器)應與特定的進程有關,其應該存放在KPEB結構中(實際上的確是這樣的)。而如果真是這樣的話,不是只要枚舉(Enum)出系統中所有的KPEB,則能得到所有的CR3值(當然前提是找出其相對KPEB的偏移值),相應的使用我在《再談Windows NT/2000內部數據結構》(Nsfocus Magazine 11)的方法就可以取出所有進程的進程名了嗎?(PID也是一樣的)。
   我在通過分析PSAPI.DLL中枚舉系統進程的函數后(EnumProcesses等),發現系統啟動后的第一個進程system的KPEB是存放在ntoskrnl.exe導出的PsInitialSystemProcess指出的地址處的,而系統中各個KPEB由一鏈表聯結着,至於鏈表的定義在Windows 2000 DDK中的ntdef.h中如下定義的:
   typedef struct _LIST_ENTRY {
      struct _LIST_ENTRY *Flink;
      struct _LIST_ENTRY *Blink;
   } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
   有了KPEB,跟蹤相應的代碼(這段反匯編代碼我將在下面列出),就能找出偏移地址18h處的CR3值。
   以上操作,在Windows 9x中可由Kernel32.dll中序號為1的Undocumented函數(NONAME,Softice Export列表中顯示為ORD_0001)實現,但因為Windows 9x與NT/2000的內核的不同,Softice for 9x與NT的addr命令輸出的格式也完全不同。至於在Windows NT/2000中不知道是否有現成的函數可以得到結果,至少現在我也沒找到,這可是題外話。
   因為上面的敘述還是比較抽象,我還是將SoftICE的輸出結果列於此,更利於理解:
   :dd PsInitialSystemProcess l 4
   0008:8046A844 FE4E1C60 E1000968 00000000 00000000 `.N.h...........
              |
              |_System進程的KPEB
   :dd @PsInitialSystemProcess+18 l 4
   0008:FE4E1C78 00030000 00000000 00000000 00000000 ................
              |
              |_System進程的Context
   :dd @PsInitialSystemProcess+9c l 4 //9ch是PID相對KPEB的位置
   0008:FE4E1CFC 00000008 FF8E65E0 8046A180 00000000 .....e....F.....
              |
              |_System進程PID
   :dd @PsInitialSystemProcess+1fc l 10 //1fch是Process Name相對KPEB的位置
   0008:FE4E1E5C 74737953 00006D65 00000000 00000000 System..........
                                      |
                      System進程Process Name_|
   :? @(@PsInitialSystemProcess+a0)-a0
   //計算System指向的下一個進程的KPEB,0a0h是鏈狀結構相對KPEB的偏移
   FF8E6540 4287522112 (-7445184) "巈@"
    |
    |_System進程KPEB指向的下一個KPEB,從Process Name可知為smss.exe(見下)
   :dd @(@PsInitialSystemProcess+a0)-a0+18 l 4
   0008:FF8E6558 02D59000 02D5A000 00000000 00000000 ................
              |
              |_smss.exe的進程Context
   :dd @(@PsInitialSystemProcess+a0)-a0+9c l 4
   0008:FF8E65DC 00000090 FF8E1880 FE4E1D00 00000518 ..........N.....
              |
              |_smss.exe的PID
   :dd @(@PsInitialSystemProcess+a0)-a0+1fc l 10
   0008:FF8E673C 73736D73 6578652E 00000000 00000000 smss.exe........
                                      |
                     smss.exe進程Process Name_|
   :dd @(@(@PsInitialSystemProcess+a0))-a0+18 l 4
        .
        . (可以用Softice用同樣的方法一直跟蹤到鏈表結束)
        .
   實現代碼段如下:
   /*
   由於以下代碼段均要求獲取系統數據結構,即要求在Ring 0狀態下運行,所
   以必須位於NT/2000設備驅動程序中。因設備驅動程序的架構,決定的代碼的長
   度較長,此處僅列出關鍵代碼段,您可以找本WDM的書,將此程序段置入您的代碼中,
   或是直接聯系我( tsu00@263.net )。
   */
      .
      .
      .
   PLIST_ENTRY KPEBListHead, KPEBListPtr;       //PLIST_ENTRY定義見上
   ULONG KPEBListOffset=0xa0;               //定義鏈表相對KPEB的偏移值
   ULONG ProcessNameOffset=0x1fc;            //定義ProcessName相對KPEB的偏移值
   ULONG ProcessContextOffset=0x18;           //定義Process Context相對KPEB的偏移值
   ULONG PIDOffset=0x9c;                  //定義PID相對KPEB的偏移值
   if(((USHORT)NtBuildNumber)!=2195){
      DbgPrint("Only test on Windows 2000 Server Build 2195!/n");
      return;
   }
   DbgPrint("/n CR3/t/tKPEB Addr/tPID/t Name");
   DbgPrint("/n ---/t/t-------- /t---/t ----/n");
   KPEBListHead=KPEBListPtr=(PLIST_ENTRY)(((char *)PsInitialSystemProcess)+KPEBListOffset);
   while (KPEBListPtr->Flink!=KPEBListHead) {
      void *kpeb;
      char ProcessName[16];
      ULONG ProcessContext;
      ULONG PID;
      //取KPEB
      kpeb=(void *)(((char *)KPEBListPtr)-KPEBListOffset);
      //取ProcessName
      memset(ProcessName, 0, sizeof(ProcessName));
      memcpy(ProcessName, ((char *)kpeb)+ProcessNameOffset, 16);
      //取Process Context
      ProcessContext=*(ULONG *)(((char *)kpeb)+ProcessContextOffset);
      //取PID
      PID=*(ULONG *)(((char *)kpeb)+PIDOffset);
      //向Debugger輸出結果
      DbgPrint(" %08X/t%08X/t%04X/t %s/n",ProcessContext, kpeb,PID,ProcessName);
      //指向下一鏈表
      KPEBListPtr=KPEBListPtr->Flink;
   }
        .
        .
        .
   使用Checked方式編譯運行后調試器輸出結果如下(儼然就是一個最底層的EnumProcesses實現方法):
   CR3    KPEB Addr PID  Name
   ---    --------  ---  ----
   00030000 FE4E1C60  0008 System
   02D59000 FF8E7920  0090 smss.exe
   003C1000 FE520520  00AC csrss.exe
   026C6000 FE51A020  00A8 winlogon.exe
   03209000 FF8A8D60  00DC services.exe
        .
        . 略
        .
   我之所以花如此大的篇幅去講述進程Context的獲得,似乎與環境切換的主題不相一致,主要是由於Windows NT/2000的封閉性,我覺得真正要明白環境切換,Linux平台就可以比較容易理解,在NT中重要是知道如何取得些與此有關的重要數據結構,然后再與x86平台體系結構聯系在一起,就能更好的幫助自己理解。使用上面所述的類似方法,還可以找出很多KPEB/KTEB重要信息,如進程優先級(KPEB+62h)、進程在內核態與用戶態所使用的時間(KPEB+38h與KPEB+3ch)、線程ID(KTEB+1e4h)等等,可與linux的task_struct等結構比較比較。
   好了談了這么多,我還是簡單說說Windows 2000中的環境切換代碼吧。
   那么Windows NT/2000什么情況下發生環境切換呢?曾見過一DDK FAQ中是這樣描述的:
   Q:What are the causes of a context switch in Windows NT?
   A:There are only two ways that a thread context is switched.
      1.The thread yields it's quantum by blocking on something(event,semaphore,etc.).
      2.The time period is up.This is caused by a timer interrupt.
   KiDispatchInterrupt是NT/2000中定時進行環境切換例程,以下列出其部分代碼:
   ;Linux中實現類似功能的代碼位於/usr/src/linux/kernel/sched.c
   00403A58 KiDispatchInterrupt proc near
   00403A58
   00403A58 var_C = dword ptr -0Ch
   00403A58 var_8 = dword ptr -8
   00403A58 var_4 = dword ptr -4
   00403A58
   00403A58 mov ebx, ds:0FFDFF01Ch
   00403A5E lea eax, [ebx+800h]
   00403A64 cli
   00403A65 cmp eax, [eax]
   00403A67 jz short loc_403A86
   00403A69 push ebp
   00403A6A push dword ptr [ebx]
   00403A6C mov dword ptr [ebx], 0FFFFFFFFh
   00403A72 mov edx, esp
   00403A74 mov esp, [ebx+81Ch]
   00403A7A push edx
   00403A7B mov ebp, eax
   00403A7D call sub_460BA4
   00403A82 pop esp
   00403A83 pop dword ptr [ebx]
   00403A85 pop ebp
  
        .
        .(限於篇幅,此處略去部分,感興趣的自己步步跟蹤)
        .
   ;另CR3切換代碼:
   ;Linux中實現此功能的代碼位於/usr/src/linux/include/asm-i386/pgtable.h
   ;由宏定義SET_PAGE_DIR實現,請參考之。
   ;此時EDI存儲KPEB(自己用SoftICE跟跟),執行后EAX則為進程Context
   ;這句結合mov cr3,eax是不是可以跟蹤到CR3在KPEB的具體位置的呢,我就是從這兒跟蹤到的。
   00403B87 mov eax, [edi+18h]
   00403B8A mov ebp, [ebx+40h]
   00403B8D mov ecx, [edi+30h]
   00403B90 mov [ebp+1Ch], eax
   00403B93 mov cr3, eax ;EAX->CR3
   00403B96 mov [ebp+66h], cx
   00403B9A xor eax, eax
   00403B9C cmp [edi+20h], ax
   ;轉去錯誤處理,必要時還會調用KeBugCheck,出現可怕的藍屏死機.
   00403BA0 jnz short loc_403BCE
   ;LDTR置空選擇器
   00403BA2 lldt ax
   00403BA5 lea ecx, [ecx]
   00403BA7
   00403BA7 loc_403BA7: ; CODE XREF: .text:00403B7Dj
   00403BA7 ; .text:00403BFAj
   ;將Context Switches Times加一
   00403BA7 inc dword ptr [esi+4Ch]
   00403BAA inc dword ptr [ebx+5C0h]
   00403BB0 pop ecx
   00403BB1 mov [ebx], ecx
   00403BB3 cmp byte ptr [esi+49h], 0
   00403BB7 jnz short loc_403BBD
   00403BB9 popf
   00403BBA xor eax, eax
   00403BBC retn
   00403BBD loc_403BBD: ; CODE XREF: .text:00403BB7j
   00403BBD popf
   00403BBE jnz short loc_403BC3
   00403BC0 mov al, 1
   00403BC2 retn
   00403BC3 loc_403BC3: ; CODE XREF: .text:00403BBEj
   00403BC3 mov cl, 1
   00403BC5 call ds:HalRequestSoftwareInterrupt
   00403BCB xor eax, eax
   00403BCD retn
        .
        .(略)
        .
   部分代碼我尚未進行注解,主要是一些代碼與代碼運行環境有關,如處理NT執行體的錯誤檢查(包括有效性、安全性等),而且我這兒也未列出代碼,如果你有興趣的話用SoftICE步步跟蹤可以發現很多NT內部機制。
   Windows 2000是搶占式多線程操作系統,文中並未涉及到線程調度的具體方法。真正線程調度切換,還要考慮很多因素,如線程狀態(是否可調度等)、線程優先級(Priority)、線程的親緣性(Affinity)等,這些具體的重要信息,也都由ntoskrnl.exe模塊處理,我也不可能都詳細的在此列出。本文只討論如何獲得這信息,不過若想知道得更多,仍可以根據文中的討論對其進行進一步的分析。
   我曾經接觸過單片機,也曾經對其似乎從頭開始設計一個OS(可能只是幾條指令,單片機高手千萬別見笑)感到不解,但當我接觸過NT Kernel后才覺得自己是多么的可笑。不過在接觸NT內核時,可大量參照Linux代碼,畢竟她們原理應該是一樣的,雖然Linux不是一個微內核OS,而NT/2000是。個人認為linux的task_struct與NT的KPEB,linux中的Bottom half機制與NT的DPC(延時過程調用)等有其相似的地方(雖然在機制上實現方法上仍有很大的不同)。由於Microsoft未提供任何官方文檔且NT內核的復雜性(曾有人批評NT的微內核比Linux還要大呢),本文所討論的,我也不能保證其絕對的正確性,如果您發現任何錯誤之處或是有什么建議,請予以告之,謝謝!
   參考資料:
   1.Jeffrey Richter
      <<Programming Applications for Microsoft Windows,Fourth Edition>>
   2.Linux相關文檔
   3.Mark Russinovich相關文檔
   4.Intel Corp<<Intel Architecture Software Developer&#39;s Manual,Volume 3>>

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com