📄 漫谈兼容内核之十九windows线程间的强相互作用.txt
字号:
漫谈兼容内核之十九:Windows线程间的强相互作用
毛德操
在现代的计算机系统中,一项作业(Job)往往需要多个进程或线程的协作,而操作系统则要为进程或线程间的协作提供基础设施和机制上的支持。操作系统、特别是内核,提供什么样的设施和手段,系统中的进程之间和线程之间就会有什么样的相互作用。如果把一个系统比作一个社会,那么系统中的进程和线程就好像是社会中的成员。成员的行为和成员之间的关系有其“社会性”的一面、即互相影响的一面。例如一个线程的调度优先级就有社会性,因为这个线程的优先级高了就意味着其它线程的优先级相对降低了。再如一个线程睡眠就使其它线程得到了更多的运行机会。而进程间的通信和同步,则更是体现着进程间的相互控制和协调的一面。
以前说过,Linux就像是一个比较自由化、各个成员比较有独立自主性的社会。Linux进程(线程)之间直接作用的手段基本上就是包括Signal在内的进程间通信,而进程间通信基本上是在双方都自愿、至少是知情的条件下进行的温和行为。通过信号值为SIGKILL的Signal“杀死”对方是唯一的例外。而别的剧烈作用,例如使另一个进程“挂起”即暂停其运行,将一个文件映射到另一个进程的用户空间等等,则根本就不提供这样的手段。所以Linux进程基本上不具备直接控制、支配另一个进程的能力。所以说,Linux进程(线程)之间的作用是“弱相互作用”而不是“强相互作用”。
而Windows就不同了。Windows允许进程/线程之间的“强相互作用”,并为此提供手段、即系统调用。下面介绍Windows所提供的线程间控制和监视手段,其中有些就是“强作用”。至于以前讲到的跨进程操作,那就不止是“强作用”、而已经是“粗暴作用”了。
Windows为进程/线程间(包括对自身)的控制和信息获取提供了不少系统调用,我们考察其中比较重要的几个。
首先是系统调用NtQueryInformationProcess(),用来获取一个已打开进程对象的各种信息。这大致上相当于Linux在目录/proc下面提供的信息,但是种类更多。从PROCESSINFOCLASS类型的定义可以看出这些信息的种类之多(不过并非都已实现):
typedef enum _PROCESSINFOCLASS {
ProcessBasicInformation,
ProcessQuotaLimits,
ProcessIoCounters,
ProcessVmCounters,
ProcessTimes,
ProcessBasePriority,
ProcessRaisePriority,
ProcessDebugPort,
ProcessExceptionPort,
ProcessAccessToken,
ProcessLdtInformation,
ProcessLdtSize,
ProcessDefaultHardErrorMode,
ProcessIoPortHandlers,
ProcessPooledUsageAndLimits,
ProcessWorkingSetWatch,
ProcessUserModeIOPL,
ProcessEnableAlignmentFaultFixup ,
ProcessPriorityClass,
ProcessWx86Information,
ProcessHandleCount,
ProcessAffinityMask,
ProcessPriorityBoost,
ProcessDeviceMap,
ProcessSessionInformation,
ProcessForegroundInformation,
ProcessWow64Information,
ProcessImageFileName,
ProcessLUIDDeviceMapsEnabled,
ProcessBreakOnTermination,
ProcessDebugObjectHandle,
ProcessDebugFlags,
ProcessHandleTracing,
ProcessUnknown33,
ProcessUnknown34,
ProcessUnknown35,
ProcessCookie,
MaxProcessInfoClass
} PROCESSINFOCLASS;
其中许多种类都有相应的数据结构。每次调用NtQueryInformationProcess()时都要指定一个种类,所返回的则是按相应数据结构组织的数据。这里有几个种类需要作些说明。首先是ProcessBasicInformation,其相应的数据结构为PROCESS_BASIC_INFORMATION:
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
KAFFINITY AffinityMask;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
HANDLE InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;
这里指针PebBaseAddress的意义自明;AffinityMask用于多处理器系统结构,表示该进程可以在那些处理器上运行;UniqueProcessId和InheritedFromUniqueProcessId分别是本进程和父进程对象的Handle。BasePriority是进程的基本调度优先级。在Windows中受调度运行的是线程,而线程的调度优先级由两部分构成,一部分是其所在进程的调度优先级,另一部分是线程在进程中的相对优先级。而进程的调度优先级又有基本优先级(BasePriority)和提升优先级(RaisePriority),后者是为克服“优先级倒置”问题而临时提高了的优先级。此外,进程的优先级还因进程的性质和状态而分成不同的类,例如“空转”、“常规”、“实时”等等。所以Windows进程/线程的调度优先级是个相当复杂的话题。
类型ProcessQuotaLimits与“时间片”大小有关,是为“时间片”设置的上限。
类型ProcessAccessToken与进程的访问权限有关。每一个进程在创建时都被授予一个“证章(Token)”,实际上是个PROCESS_ACCESS_TOKEN数据结构,其下面是包括TOKEN在内的一系列数据结构,里面记载着该进程对各种对象的访问权限,进程的EPROCESS结构中则有个PROCESS_ACCESS_TOKEN指针。这要留到将来在谈到Windows的安全管理时再作介绍。
类型ProcessCookie就是指EPROCESS结构中的字段Cookie,这是个由时间等等信息杂凑形成的特征值,其数值有一定的随机性。
其余的讲不胜讲。有些看看名称就可明白;有些现在暂不明白,以后随着对Windows的了解逐步深入自会慢慢明白。
现在我们看代码:
NTSTATUS STDCALL
NtQueryInformationProcess(IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL)
{
. . . . . .
PreviousMode = ExGetPreviousMode();
DefaultQueryInfoBufferCheck(ProcessInformationClass,
PsProcessInfoClass,
ProcessInformation,
ProcessInformationLength,
ReturnLength,
PreviousMode,
&Status);
. . . . . .
if(ProcessInformationClass != ProcessCookie)
{
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_QUERY_INFORMATION, PsProcessType,
PreviousMode, (PVOID*)&Process, NULL);
. . . . . .
}
else if(ProcessHandle != NtCurrentProcess())
{
/* retreiving the process cookie is only allowed for the calling process
itself! XP only allowes NtCurrentProcess() as process handles even if a
real handle actually represents the current process. */
return STATUS_INVALID_PARAMETER;
}
switch (ProcessInformationClass)
{
case ProcessBasicInformation:
{
PPROCESS_BASIC_INFORMATION ProcessBasicInformationP =
(PPROCESS_BASIC_INFORMATION)ProcessInformation;
_SEH_TRY
{
ProcessBasicInformationP->ExitStatus = Process->ExitStatus;
ProcessBasicInformationP->PebBaseAddress = Process->Peb;
ProcessBasicInformationP->AffinityMask = Process->Pcb.Affinity;
ProcessBasicInformationP->UniqueProcessId = Process->UniqueProcessId;
. . . . . .
if (ReturnLength)
{
*ReturnLength = sizeof(PROCESS_BASIC_INFORMATION);
}
}
. . . . . .
break;
}
case ProcessQuotaLimits:
case ProcessIoCounters:
. . . . . .
case ProcessLdtInformation:
case ProcessWorkingSetWatch:
case ProcessWx86Information:
Status = STATUS_NOT_IMPLEMENTED;
break;
case ProcessHandleCount:
. . . . . .
case ProcessWow64Information:
Status = STATUS_NOT_IMPLEMENTED;
break;
case ProcessVmCounters:
. . . . . .
/* Note: The following 10 information classes are verified to not be
implemented on NT, and do indeed return STATUS_INVALID_INFO_CLASS; */
case ProcessBasePriority:
case ProcessRaisePriority:
case ProcessExceptionPort:
case ProcessAccessToken:
case ProcessLdtSize:
case ProcessIoPortHandlers:
case ProcessUserModeIOPL:
case ProcessEnableAlignmentFaultFixup :
case ProcessAffinityMask:
case ProcessForegroundInformation:
default:
Status = STATUS_INVALID_INFO_CLASS;
}
if(ProcessInformationClass != ProcessCookie)
{
ObDereferenceObject(Process);
}
return Status;
}
ProcessInformation是用来返回结果的缓冲区,参数ProcessInformationClass指定什么信息类型,这缓冲区就被用作什么数据结构。这里先由宏操作DefaultQueryInfoBufferCheck()根据信息类型对缓冲区的大小与映射作一检查。然后根据目标进程的Handle获取指向其EPROCESS结构的指针。这里有个特例,就是Process->Cookie只允许由本进程读取,别的都可以跨进程读取。下面就是对具体信息的获取了,这些信息大多来自目标进程的EPROCESS结构,所以这里只列出对ProcessBasicInformation类信息的读出,别的就省略了。还有些类型的信息是不允许读出的,有的则尚未实现。
系统调用NtQueryInformationProcess()只是用于获取信息,与其相对应的另一个系统调用NtSetInformationProcess()就是用来设置数据的了。但是,设置一个进程对象的某些数据,实际上就改变了它某些方面的性状和行为特性。例如设置其ProcessPriorityClass就改变了目标进程、从而其所有线程的调度优先级。又如设置其ProcessAccessToken就等于是给它换发了一个证章。这样的操作,如果是由父进程对子进程实施,那倒还无可非语。事实上读者已经看到,在CreateProcessW()中就是通过NtSetInformationProcess()设置子进程的优先级类型的。但是,如果是发生在没有父子关系的两个进程之间,那就属于进程间的“强作用”了。至于这个函数的代码,因为简单,我们就不看了。
进程间如此,线程间也是如此,Windows也提供了NtQueryInformationThread()和NtSetInformationThread()两个系统调用,操作的目标是已经打开的线程对象。为此先要知道目标线程的“客户ID”、即CID,然后通过NtOpenThread()打开其线程对象,再把由此得到的Handle作为这两个系统调用的参数。
如果说这已经属于进程间和线程间的“强作用”,那系统调用NtSuspendThread()就更加了。这个系统调用的作用是使另一个正在运行中的线程被“挂起”、即暂停运行。为此,同样先要打开目标线程对像,并且在打开的时候就说明要求允许对其实行挂起/恢复的操作,然后把目标线程对像的Handle用于NtSuspendThread()的调用。
NTSTATUS STDCALL
NtSuspendThread(IN HANDLE ThreadHandle,
IN PULONG PreviousSuspendCount OPTIONAL)
{
PETHREAD Thread;
NTSTATUS Status;
ULONG Count;
PAGED_CODE();
Status = ObReferenceObjectByHandle(ThreadHandle, THREAD_SUSPEND_RESUME,
PsThreadType, UserMode, (PVOID*)&Thread, NULL);
. . . . . .
Status = PsSuspendThread(Thread, &Count);
. . . . . .
if (PreviousSuspendCount != NULL)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -