淺析Windows NT/2000環境切換
WebCrazy(tsu00@263.net)
注:本文最初見於www.nsfocus.com
本文假設您已經瞭解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's Manual,Volume 3>>