您的位置:首页 > 其它

再谈Windows NT/2000环境切换

2012-07-17 20:49 357 查看
线程是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) ",

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! ");

return;

}

DbgPrint(" TID KTEB Addr StackBtm StackTop StackPtr User TEB Process

Name (PID)");

DbgPrint(" ---- -------- -------- -------- -------- -------- -------

----- ----- ");

KPEBListHead=KPEBListPtr=(PLIST_ENTRY)(((char *)PsInitialSystemProcess)+KPEBListOffset);

while (KPEBListPtr->Flink!=KPEBListHead) {

void *kpeb;

kpeb=(void *)(((char *)KPEBListPtr)-KPEBListOffset);

DisplayThreadFromKPEB(kpeb);

DbgPrint(" ");

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: