读《毛德操:关于进程挂靠》

晨曦之光 发布于 2012/04/10 15:01
阅读 951
收藏 0

WINDOWS 可以创建远程线程,但是linux却不可以,为何?windows是基于对象的,是异步的,在很多情况下都没有进程上下文,试想一下,windows实际 上就像一个超大容器,里面有形形色色各种对象,进程是对象,线程是对象,中断也是对象,文件是对象...,内核管理着这些对象,通过管理它们为用户提供服 务,而如果对象不交互就不需要管理,系统也就没有存在的意义了,实际上系统基于对象实现就是为了提供服务,管理它们的交互,所以对象交互是一个很大的问 题,既然进程线程都是对象,那么让他们交互有问题吗?一点问题都没有,这和线程和文件交互没有本质的区别,在windows中,进程线程的意义并不比其他 对象更大,人们太过注意它们是因为它们是用户的东西。
再者,windows是异步的,很多时候没有上下文,但是最终服务的对象却是线程(在内核看来,一切对象平等;在用户看来,进程/线程最重要),比如一个 驱动要往进程地址空间复制数据,但是这个驱动运行在任意上下文,那么怎么办?难道要进程间通信吗?在进程显得不那么重要的系统上这合适吗?所以就可以直接 挂靠到那个进程,完成任务后再切回来,这实际上更直接的和那个进程通信了,怎么找到那个对象呢?看看内核或者最起码看看windows api就会发现,很多api参数都有一个叫做句柄的概念,这个句柄实际上就是一个对象的偏移索引,本身并不重要,重要的是,只要能找到对象在哪,我就可能 与之通信,[动作(对象,发给对象的消息)这种调用方式十分人性化]真正能不能还得看对象愿不愿意,每个对象都有一个关于安全控制的字段,所以说 windows的管理粒度很细,是对象,而反观unix/linux,它们的管理粒度是进程/线程。
在unix/linux系统内核里面,它们使进程/线程的管理模块实现几乎所有机制,将策略完全留给用户,它们太偏爱进程/线程了。不过linux最近也 实现了异步io(仅仅io?但本质还是同步的),可是用户还是不得不在程序中进行aio_wait等调用,后来内核实现了eventfd,使得内核可以实 现异步通知了,这样linux在异步道路上又走了一步,在aio开始的时候,linux实际上就可以实现进程挂靠了,看看linux的 use_mm/unuse_mm内核函数,直接就可以将当前的进程挂靠到别的进程的地址空间(linux的地址空间用mm_struct表示),这样比 windows实现的更轻量,当然也要切换硬件环境,理论上是这样,但事实上没有任何人这样用过,只有aio相关的内核线程用(因为要操作别的进程空间的 数据),为何没有人用,不是不想用,而是实在没有必要,程序员和设计者一样,偏爱进程/线程。
不光两大系统的设计理念不同,它们每一个下面都有一个庞大的开发队伍,这两大队伍的开发理念也不会相同的,所以我认为毛教授提倡的兼容内核并不能行得通。附上毛教授原文,文章真的很不错:

上一篇漫谈在介绍APC机制时提到:线程在Windows内核中运行时有时候需要暂时"挂(Attach)"到别的进程的用户空间,即暂时切换到另 一个进程的用户空间.这称为"进程挂靠",因为用户空间是一个进程最主要的特征. 显然,要是当前线程的操作与用户空间无关,不需要访问用户空间,那么当时的用户空间到底是谁的用户空间根本就无关紧要,所以这必定发生在与用户空间有关的 操作中.
一般而言,如果线程T属于进程P,那么当这个线程在内核中运行时的用户空间应该就是进程P的用户空间,它也没有必要访问到别的进程的用户空间去.可是,Windows内核允许一些跨进程的操作,特别是跨用户空间的操作,所以有时候就需要把当时的用户空间切
换 到别的进程的用户空间,或者说挂靠到别的进程.在Windows中,一个进程实际上只是意味着一个用户(地址)空间,说一个线程属于某个进程的意思是它使 用的是某个特定的用户空间,系统空间则是由所有线程共用的.那么"某个特定的用户空间"是什么意思呢 实质上就是一个具体的页面映射方案,或者一套具体的映射目录和页面表,以及相关的其它数据结构.而所谓"切换到某个进程的用户空间",就是把这套具体的映 射目录和页面表装入CPU中的页面映射机构,使其真正发生作用.当然,在完成了有关的操作以后还要回到原来的用户空间,否则就无法从内核"返回"自己的用 户空间了. 然而究竟什么时候需要用到进程挂靠呢 最好还是通过一个实例来加以说明. 前几篇漫谈中说到,在启动一个PE格式的EXE映像运行时先要创建一个进程,然后把目标EXE映像和ntdll.dll的映像映射到新建进程的用户空间, 并且在映射后的ntdll.dll映像中找到LdrInitializeThunk()等函数的入口.在这个过程中,当前线程属于作为创建者的那个进程, 或"父进程",而其部分操作的对象则在新建进程,即"子进程"的用户空间.所以此时就用到了进程挂靠,使当前线程挂靠到新建进程的用户空间.下面我们通过 LdrpMapSystemDll()的代码来说明为什么有进程挂靠,以及怎样实现进程挂靠. 在创建进程的过程中要调用到一个函数LdrpMapSystemDll(),其作用是把"系统DLL",即ntdll.dll映射到新建进程的用户空间, 并从中获取几个重要函数的入口.当然,这是个内核函数,是在系统空间运行的.
NTSTATUS LdrpMapSystemDll(HANDLE ProcessHandle, PVOID* LdrStartupAddr)
{
CHAR BlockBuffer [1024];
. . . . . .
UNICODE_STRING DllPathname =
ROS_STRING_INITIALIZER(L"//SystemRoot//system32//ntdll.dll");
. . . . . .
/*
* Locate and open NTDLL to determine ImageBase
* and LdrStartup
*/
2
InitializeObjectAttributes(&FileObjectAttributes, &DllPathname, 0, NULL, NULL);
DPRINT("Opening NTDLL/n");
Status = ZwOpenFile(&FileHandle, FILE_READ_ACCESS, &FileObjectAttributes,
&Iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
. . . . . .
Status = ZwReadFile(FileHandle, 0, 0, 0, &Iosb, BlockBuffer, sizeof(BlockBuffer), 0, 0);
. . . . . .
. . . . . .
DosHeader = (PIMAGE_DOS_HEADER) BlockBuffer;
NTHeaders = (PIMAGE_NT_HEADERS) (BlockBuffer + DosHeader->e_lfanew);
. . . . . .
ImageBase = NTHeaders->OptionalHeader.ImageBase;
ImageSize = NTHeaders->OptionalHeader.SizeOfImage;

/*
* Create a section for NTDLL
*/
DPRINT("Creating section/n");
Status = ZwCreateSection(&NTDllSectionHandle, SECTION_ALL_ACCESS, NULL,
NULL, PAGE_READWRITE, SEC_IMAGE | SEC_COMMIT, FileHandle);
. . . . . .
ZwClose(FileHandle);

/*
* Map the NTDLL into the process
*/
ViewSize = 0;
ImageBase = 0;
Status = ZwMapViewOfSection(NTDllSectionHandle, ProcessHandle,
(PVOID*)ℑageBase, 0, ViewSize, NULL,
&ViewSize, 0, MEM_COMMIT, PAGE_READWRITE);
. . . . . .
. . . . . .
CurrentProcess = PsGetCurrentProcess();
if (Process != CurrentProcess)
{
DPRINT("Attaching to Process/n");
KeAttachProcess(⪻ocess->Pcb);
}
/*
* retrieve ntdll's startup address
*/
3
if (SystemDllEntryPoint == NULL)
{
RtlInitAnsiString (⪻ocedureName,
"LdrInitializeThunk");
Status = LdrGetProcedureAddress ((PVOID)ImageBase,
⪻ocedureName,
0,
&SystemDllEntryPoint);
. . . . . .
*LdrStartupAddr = SystemDllEntryPoint;
}
. . . . . .
. . . . . .
if (Process != CurrentProcess)
{
KeDetachProcess();
}
ObDereferenceObject(Process);
ZwClose(NTDllSectionHandle);
return(STATUS_SUCCESS);
}
先看一下大致的流程:
通 过InitializeObjectAttributes()设置好一个OBJECT_ATTRIBUTES数据结构 FileObjectAttributes;然后用这个数据结构作为参数之一,通过系统调用ZwOpenFile()打开目标文件ntdll.dll.之 所以如此,是因为ZwOpenFile()并不接受文件名作为参数,而必须把文件名放在OBJECT_ATTRIBUTES数据结构中.当然,这个数据结 构中还有别的信息. 通过ZwReadFile()读入目标文件的开头1K字节,目的在于获取其DosHeader和NTHeaders,进而获取其 NTHeaders->OptionalHeader中的ImageBase和SizeOfImage两项信息,前者是映像在文件中的起点,后者是 映像的大小. 通过ZwCreateSection()为目标文件建立(并打开)一个Section对象.从逻辑的意义上,这个Section对象就与目标文件的内容划 上了等号. 至此,目标文件已经可以关闭,因为不再需要通过文件读写等常规的文件操作访问这个文件了. 通过ZwMapViewOfSection()将已建立的Section,即目标文件的内容映射到目标进程的用户空间. 通过KeAttachProcess()将当前线程挂靠到目标进程. 通过LdrGetProcedureAddress()从已经映射到目标进程用户空间的映像中获取函数LdrInitializeThunk()的入口地 址.
再通过LdrGetProcedureAddress()获取若干其它函数的入口地址. 通过KeDetachProcess()撤销挂靠,回到当前线程所属的进程. 关闭所创建的Section对象. 首先要说明,函数名以Zw开头的函数实际上就是以Nt开头的对应系统调用.以打开文件为例,在用户空间调用时要用NtOpenFile(),在内核中调用 则用ZwOpenFile(). 显然,这个流程中的进程挂靠,即KeAttachProcess()和KeDetachProcess(),是因为要执行 LdrGetProcedureAddress()而产生的需求.对此我们很自然地就会有两个问题:首先,为什么 LdrGetProcedureAddress()需要进程挂靠 其次,既然LdrGetProcedureAddress()需要,那为什么ZwMapViewOfSection()倒又不需要 二者不是都涉及目标进程的用户空间吗 要回答这两个问题,就得近一步深入到这两个函数的代码中. 如前所述,系统调用NtCreateSection()在内核中创建一个Section对象,并使这个对象与一个(已经打开的)目标文件挂上勾,此后就可 以通过另一个系统调用NtMapViewOfSection()将目标文件的部分或全部内容映射到某个用户空间(Section可以为多个进程共享,分别 映射到不同空间的相同或不同地址上).
下面先看NtMapViewOfSection().
[LdrpMapSystemDll() > NtMapViewOfSection()]
NTSTATUS STDCALL
NtMapViewOfSection(IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress OPTIONAL,
IN ULONG ZeroBits OPTIONAL,
IN ULONG CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PULONG ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType OPTIONAL,
IN ULONG Protect)
{
PVOID SafeBaseAddress;
LARGE_INTEGER SafeSectionOffset;
ULONG SafeViewSize;
PSECTION_OBJECT Section;
PEPROCESS Process;
KPROCESSOR_MODE PreviousMode;
PMADDRESS_SPACE AddressSpace;
NTSTATUS Status = STATUS_SUCCESS;

PreviousMode = ExGetPreviousMode();

if(PreviousMode != KernelMode)
{
. . . . . .
}
else
{
SafeBaseAddress = (BaseAddress != NULL *BaseAddress : NULL);
5
SafeSectionOffset.QuadPart = (SectionOffset != NULL SectionOffset->QuadPart : 0);
SafeViewSize = (ViewSize != NULL *ViewSize : 0);
}
. . . . . .
AddressSpace = ⪻ocess->AddressSpace;
. . . . . .
Status = MmMapViewOfSection(Section,
Process,
(BaseAddress != NULL &SafeBaseAddress : NULL),
ZeroBits,
CommitSize,
(SectionOffset != NULL &SafeSectionOffset : NULL),
(ViewSize != NULL &SafeViewSize : NULL),
InheritDisposition,
AllocationType,
Protect);
. . . . . .
return(Status);
}
参 数SectionHandle代表着一个Section对象,ProcessHandle则代表着一个用户空间,BaseAddress是要求装入的地 址,而SectionOffset是目标文件中的起点.还有个参数Protect是对映射后的内存区间(而不是目标文件)的访问保护,在这里是 PAGE_READWRITE.
显然,实际的操作是由MmMapViewOfSection()完成的,函数名中的前缀Mm表示这个函数属于内存管理.
[LdrpMapSystemDll() > NtMapViewOfSection() > MmMapViewOfSection()]
NTSTATUS STDCALL
MmMapViewOfSection(IN PVOID SectionObject, ……)
{
. . . . . .
PMADDRESS_SPACE AddressSpace;
. . . . . .
Section = (PSECTION_OBJECT)SectionObject;
AddressSpace = ⪻ocess->AddressSpace;
MmLockAddressSpace(AddressSpace);
if (Section->AllocationAttributes & SEC_IMAGE)
6
{
ULONG i;
ULONG NrSegments;
ULONG_PTR ImageBase;
ULONG ImageSize;
PMM_IMAGE_SECTION_OBJECT ImageSectionObject;
PMM_SECTION_SEGMENT SectionSegments;
ImageSectionObject = Section->ImageSection;
SectionSegments = ImageSectionObject->Segments;
NrSegments = ImageSectionObject->NrSegments;
ImageBase = (ULONG_PTR)*BaseAddress;
if (ImageBase == 0)
{
ImageBase = ImageSectionObject->ImageBase;
}
ImageSize = 0;
for (i = 0; i < NrSegments; i++)
{
if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD))
{
ULONG_PTR MaxExtent;
MaxExtent = (ULONG_PTR)SectionSegments[i].VirtualAddress +
SectionSegments[i].Length;
ImageSize = max(ImageSize, MaxExtent);
}
}
/* Check there is enough space to map the section at that point. */
if (MmLocateMemoryAreaByRegion(AddressSpace, (PVOID)ImageBase,
PAGE_ROUND_UP(ImageSize)) != NULL)
{
. . . . . .
/* Otherwise find a gap to map the image. */
ImageBase = (ULONG_PTR)MmFindGap(AddressSpace,
PAGE_ROUND_UP(ImageSize), PAGE_SIZE, FALSE);
. . . . . .
}
for (i = 0; i u.LowPart;
}
. . . . . .
if ((*ViewSize) == 0)
{
(*ViewSize) = Section->MaximumSize.u.LowPart - ViewOffset;
}
else if (((*ViewSize)+ViewOffset) > Section->MaximumSize.u.LowPart)
{
(*ViewSize) = Section->MaximumSize.u.LowPart - ViewOffset;
}
MmLockSectionSegment(Section->Segment);
Status = MmMapViewOfSegment(Process,
AddressSpace,
Section,
Section->Segment,
8
BaseAddress,
*ViewSize,
Protect,
ViewOffset,
(AllocationType & MEM_TOP_DOWN));
MmUnlockSectionSegment(Section->Segment);
. . . . . .
}
MmUnlockAddressSpace(AddressSpace);
return(STATUS_SUCCESS);
}
我 把这段程序留给读者自己阅读,只是略加提示:Section对象所代表的目标文件分为两大类,一类是可执行映像文件,一类是不同文件.可执行映像文件的映 射比普通文件要复杂一些,因为映像文件中一般有好多不同的段,需要映射到不同的地址上去,这就是代码中有两个for循环的原因.每个段的映射则都是由 MmMapViewOfSegment()完成的.
[LdrpMapSystemDll() > NtMapViewOfSection() >
MmMapViewOfSection() > MmMapViewOfSegment()]
NTSTATUS STATIC
MmMapViewOfSegment(PEPROCESS Process,
PMADDRESS_SPACE AddressSpace,
PSECTION_OBJECT Section,
PMM_SECTION_SEGMENT Segment,
PVOID* BaseAddress,
ULONG ViewSize,
ULONG Protect,
ULONG ViewOffset,
BOOL TopDown)
{
PMEMORY_AREA MArea;
NTSTATUS Status;
KIRQL oldIrql;
PHYSICAL_ADDRESS BoundaryAddressMultiple;
BoundaryAddressMultiple.QuadPart = 0;
Status = MmCreateMemoryArea(Process,
AddressSpace,
MEMORY_AREA_SECTION_VIEW,
BaseAddress,
9
ViewSize,
Protect,
&MArea,
FALSE,
TopDown,
BoundaryAddressMultiple);
. . . . . .
KeAcquireSpinLock(&Section->ViewListLock, &oldIrql);
InsertTailList(&Section->ViewListHead,
&MArea->Data.SectionData.ViewListEntry);
KeReleaseSpinLock(&Section->ViewListLock, oldIrql);
ObReferenceObjectByPointer((PVOID)Section,
SECTION_MAP_READ,
NULL,
ExGetPreviousMode());
MArea->Data.SectionData.Segment = Segment;
MArea->Data.SectionData.Section = Section;
MArea->Data.SectionData.ViewOffset = ViewOffset;
MArea->Data.SectionData.WriteCopyView = FALSE;
MmInitialiseRegion(&MArea->Data.SectionData.RegionListHead,
ViewSize, 0, Protect);
return(STATUS_SUCCESS);
}
这 里MmCreateMemoryArea()的作用是为一个段的影射分配虚存区间: 按给定的地址要求在目标进程的用户空间找到足够大的"空隙" 如果并非必须映射在给定的地址,就找一个足够大的空隙, 从这个空隙中划出一块给定大小的区间 分配/创建一个MEMORY_AREA数据结构,并将其挂入相应的AddressSpace队列. MEMORY_AREA数据结构除可挂入AddressSpace队列外还可挂入Section对象中的队列,这样就把内存区间,Section对象,以 及目标文件结合了起来. 对于了解Linux内核中存储管理和共享内存区映射的读者,这些操作和过程应该是容易理解的.但是我在这里要说的重点却并不在于这个过程本身,而在于这个 过程中并无进程挂靠. 读者或许已经注意到,上面在以NtMapViewOfSection()为入口的整个流程中,我们并没有看到对于KeAttachProcess()的调 用,即并没有进行进程挂靠.虽然这是在父进程的上下文中把一个Section,即"区间",影射到子进程的用户空间,但是却并不需要挂靠到子进程,这是为 什么呢 要回答这个问题,我们先要搞清:所谓一个进程的用户空间是怎么体现的.简而言之,这主要体现为"一本账,一个表". 首先,一个"用户空间"是一大片虚拟地址空间,在Linux中是3GB,在Windows中是2GB的地址空间.但是这么大一片虚拟地址空间并不是都已分 配使用,都已经映射到了物理页面,或是某个映射文件或盘区.所以就需要有个账本,记下哪一些虚拟地址区间已经分配使用了,这就是"一本账".在 Linux内核中,这个账本就是以mm_struct (在上面的代码中是MADDRESS_SPACE)为根的一整套数据结构,在"进程控制块"task_struct中有个指针指向本进程的 mm_struct数据结构(在上面的代码中是⪻ocess->AddressSpace).由于已分配使用(而尚未释放)的虚拟地址 区间一般都是不连续的,例如用于堆栈的区间和可执行代码的区间就不会连续,所以从数据结构的角度看这"账本"的具体内容总是一个链表,链表中的每一个结点 都代表着一个已分配使用的地址区间,在Linux内核中这就是vm_area_struct数据结构(在上面的代码中是MEMORY_AREA数据结 构).在这一方面,不同操作系统的内核在具体的数据结构和程序实现上可以有所不同,但是大体上都是一样的,变不出太多的花样.所以,要把一个 Section映射到一个进程的用户空间,首先是对这"账本"的操作. 但是,光有这账本还不够,因为这账本并不直接对CPU中的页面映射部件MMU起作用,所以还需要有一个用于MMU的页面映射表,这就是"一个表".所谓挂 靠到某个进程,就是把这个进程的页面映射表装入MMU,使得访问用户空间的某个地址时使用的是目标进程的页面映射表.当然,在任何特定的时刻,MMU中只 能有一个页面映射表,既然装入了目标进程的页面映射表,就离开了原来进程的页面映射表.但是,不管是什么进程的页面映射表,他们的系统空间部分,即内核部 分,则都是共同的.由此可见,"进程挂靠"(和恢复)只能在内核中进行,而不能在用户空间进行. 这里还要注意,对于页面映射表的"准备"和"使用"是两码事,建立映射时所涉及的是准备,而把准备好了的页面映射表装入MMU才开始了它的使用. 所以,ZwMapViewOfSection()之所以不需要挂靠到目标进程,是因为建立映射的过程只是账面的操作,而并不真的要去访问(目标进程)用户 空间的某个地址. 按理说,既然是把一个Section映射到目标进程的用户空间,就应该同时完成对账本和映射表的操作.但是ReactOS的代码把这两种操作分离了开来, 在NtMapViewOfSection()中只是对账本的操作,而把对映射表的操作推迟了(下面就会看到),那当然也是可以的. 至此,ntdll.dll的映射已经完成,回到LdrpMapSystemDll()的代码中,下一步是要从这映像中获取 LdrInitializeThunk()等函数的入口地址,这时候就需要实施进程挂靠了.
[LdrpMapSystemDll() > KeAttachProcess()]
VOID STDCALL
KeAttachProcess(PKPROCESS Process)
{
KIRQL OldIrql;
PKTHREAD Thread = KeGetCurrentThread();
DPRINT("KeAttachProcess: %x/n", Process);
/* Make sure that we are in the right page directory */
UpdatePageDirs(Thread, Process);
/* Lock Dispatcher */
OldIrql = KeAcquireDispatcherDatabaseLock();
. . . . . .
11
/* Check if the Target Process is already attached */
if (Thread->ApcState.Process == Process ||
Thread->ApcStateIndex != OriginalApcEnvironment) {

DPRINT("Process already Attached. Exitting/n");
KeReleaseDispatcherDatabaseLock(OldIrql);
} else {

KiAttachProcess(Thread, Process, OldIrql, &Thread->SavedApcState);
}
}
前 面的映射只是记在了新建进程的账本上,却没有改变它的页面映射表,这里的UpdatePageDirs()就来处理这页面映射表了. 这里KeAcquireDispatcherDatabaseLock()的作用是通过提高中断优先级达到禁止线程调度的目的.因为下面的 KiAttachProcess()即将实现用户空间的切换,在这个当口上是不能允许线程调度的. 下面就是"挂靠"的实施了.
[KeAttachProcess() > KiAttachProcess()]
VOID STDCALL
KiAttachProcess(PKTHREAD Thread, PKPROCESS Process,
KIRQL ApcLock, PRKAPC_STATE SavedApcState)
{
. . . . . .

/* Increase Stack Count */
Process->StackCount++;
/* Swap the APC Environment */
KiMoveApcState(&Thread->ApcState, SavedApcState);

/* Reinitialize Apc State */
InitializeListHead(&Thread->ApcState.ApcListHead[KernelMode]);
InitializeListHead(&Thread->ApcState.ApcListHead[UserMode]);
Thread->ApcState.Process = Process;
Thread->ApcState.KernelApcInProgress = FALSE;
Thread->ApcState.KernelApcPending = FALSE;
Thread->ApcState.UserApcPending = FALSE;

/* Update Environment Pointers if needed*/
if (SavedApcState == &Thread->SavedApcState) {
12

Thread->ApcStatePointer[OriginalApcEnvironment] = &Thread->SavedApcState;
Thread->ApcStatePointer[AttachedApcEnvironment] = &Thread->ApcState;
Thread->ApcStateIndex = AttachedApcEnvironment;
}

/* Swap the Processes */
KiSwapProcess(Process, SavedApcState->Process);

/* Return to old IRQL*/
KeReleaseDispatcherDatabaseLock(ApcLock);

DPRINT("KiAttachProcess Completed Sucesfully/n");
}
注意代码中的 Process->StackCount与进程的"堆栈"并无关系,而是指进程挂靠的嵌套深度. 前面讲过,所谓挂靠到某个进程,就是切换到那个进程的用户空间,就是把那个进程的页面映射表装入MMU,这里调用KiSwapProcess()的原因就 在于此.不过在此之前还需要把当前进程的APC队列从ApcState转移到SavedApcState去,所以还调用了 KiMoveApcState(),读者可以结合前一篇漫谈把这里的程序读懂.此外,这里 KeReleaseDispatcherDatabaseLock()一方面是解除对线程调度的禁令,一方面是回到原来的中断优先级.与之配对的是前面 KeAttachProcess()中的KeAcquireDispatcherDatabaseLock(). 我们接着看KiSwapProcess()的代码.
[KeAttachProcess() > KiAttachProcess() > KiSwapProcess()]
VOID
STDCALL
KiSwapProcess(PKPROCESS NewProcess, PKPROCESS OldProcess)
{
//PKPCR Pcr = KeGetCurrentKpcr();
/* Do they have an LDT */
if ((NewProcess->LdtDescriptor) || (OldProcess->LdtDescriptor)) {
/* FIXME : SWitch GDT/IDT */
}
DPRINT("Switching CR3 to: %x/n", NewProcess->DirectoryTableBase.u.LowPart);
Ke386SetPageTableDirectory(NewProcess->DirectoryTableBase.u.LowPart);

/* FIXME: Set IopmOffset in TSS */
}
这里 Ke386SetPageTableDirectory()的作用就是切换用户空间,即装入目标进程的页面映射表,这主要是对寄存器CR3的操作. 读懂了KeAttachProcess(),自然也就懂得了KeDetachProcess(). 回 到 前 面LdrpMapSystemDll()代 码 中, 可 以 看 到夹在KeAttachProcess()和KeDetachProcess()之间的操作主要是LdrGetProcedureAddress(),就 可以明白这是为什么了.因为LdrGetProcedureAddress()是根据一个函数名从给定的映像中找到该函数的程序入口(当然,这必须是由目 标映像导出的函数,否则也找不到).这里要找的就是LdrInitializeThunk()以及其它几个函数的入口.要在目标映像中寻找函数入口,当然 就得访问这个映像,即访问这个映像在用户空间的所在地址区间,这就涉及页面映射表的使用(而不是准备)了.于是,就需要暂时切换到目标进程的用户空间,也 就是"挂靠"到目标进程.当然,完成了操作之后还得切换回来,那就是KeDetachProcess()的事了. 这里还要说一下,从程序的角度看,KeAttachProcess()以后就可以根据目标映像在用户空间的起始地址访问这个映像了,似乎很简单.但是实际 的过程却并不那么简单.这个映像虽然已经在用户空间有了映射,也就是在页面映射表中有了相应的表项,但是此刻可能(应该说多半)还没有相应的物理页面,所 以在第一次访问这个映像时就会发生缺页异常.然后,在内核对缺页异常的处理中,将会发现所映射的是一个磁盘文件,即映像文件中的一个逻辑页面,就为其分配 一个物理页面并从磁盘文件读入这逻辑页面.从缺页异常返回以后,CPU重新执行访问用户空间的那条指令,才能获得成功.就这样,访问到哪,就缺页到哪,读 入到哪,慢慢地就星罗棋布,把许多页面从磁盘读了进来.然而,也许到目标映像结束运行时还有许多页面是从未读入内存的.所谓"工作集"的概念就是这样来 的,但是那已经不在本文的话题之内了.


原文链接:http://blog.csdn.net/dog250/article/details/5303601
加载中
返回顶部
顶部