当前位置: 首页 >> 开源操作系统 >> Windows内存与进程管理器底层分析
 

Windows内存与进程管理器底层分析

作者:      来源:zz     发表时间:2006-06-26     浏览次数:      字号:    


/******************** nt5.0 ******************/
#define BeingDeleted               0x1
#define BeingCreated               0x2
#define BeingPurged                0x4
#define NoModifiedWriting          0x8
#define FailAllIo                  0x10
#define Image                      0x20
#define Based                      0x40
#define File                       0x80
#define Networked                  0x100
#define NoCache                    0x200
#define PhysicalMemory             0x400
#define CopyOnWrite                0x800
#define Reserve                    0x1000
#define Commit                     0x2000
#define FloppyMedia                0x4000
#define WasPurged                  0x8000
#define UserReference              0x10000
#define GlobalMemory               0x20000
#define DeleteOnClose              0x40000
#define FilePointerNull            0x80000
#define DebugSymbolsLoaded         0x100000
#define SetMappedFileIoComplete    0x200000
#define CollidedFlush              0x400000
#define NoChange                   0x800000
#define HadUserReference           0x1000000
#define ImageMappedInSystemSpace   0x2000000

紧随CONTROL_AREA之后的是Subsection的数目Subsections。每一个Subsection都描述了关于具体的文件映射section的信息。例如,read-only, read-write, copy-on-write等等的section。NT5.0的SUBSECTION结构体:

typedef struct _SUBSECTION { // size=0x20 nt5.0
               // +0x10 if GlobalOnlyPerSession
               PCONTROL_AREA   ControlArea; //38, 00
               DWORD           Flags;       //3c, 04
               DWORD           StartingSector;//40, 08
               DWORD           NumberOfSectors; //44, 0c
               PVOID           BasePte;       //48, 10 pointer to start pte
               DWORD           UnusedPtes;    //4c, 14
               DWORD           PtesInSubsect; //50, 18
               PSUBSECTION     pNext;         //54, 1c
               }SUBSECTION, *PSUBSECTION;

在subsection中有指向CONTROL_AREA的指针,标志,指向base Proto PTE的指针,Proto PTE的数目。StartingSector是4K block的编号,文件中的section起始于此。在标志中还有额外的信息:

#define SS_PROTECTION_MASK               0x1f0
#define SS_SECTOR_OFFSET_MASK            0xfff00000 // (low 12 bits)
#define SS_STARTING_SECTOR_HIGH_MASK     0x000ffc00  //  (nt5 only) (in pages)

//other 5 bit(s)

#define ReadOnly       1
#define ReadWrite      2
#define CopyOnWrite    4
#define GlobalMemory   8
#define LargePages 0x200

我们来看剩下的最后一个结构体SEGMENT,它描述了所有的映射和用于映射section的Proto PTE。SEGMENT的内存是从paged pool中分配的。我给出SEGMENT结构体(NT 5.0)

typedef struct  _SEGMENT {
          PCONTROL_AREA ControlArea;    //00
          DWORD         BaseAddr;       //04
          DWORD         TotalPtes;      //08
          DWORD         NonExtendedPtes;//0c
          LARGE_INTEGER SizeOfsegemnt;  //10
          DWORD        ImageCommit;     //18
          DWORD        ImageInfo;       //1c
          DWORD        ImageBase;       //20
          DWORD        Commited;        //24
          PTE          PteTemplate;     //28 or 64 bits if pae enabled
          DWORD BasedAddr;         //2c
          DWORD BaseAddrPae;       //30 if PAE enabled
          DWORD ProtoPtes;         //34
          DWORD ProtoPtesPae;           //38 if PAE enabled
          }SEGMENT,*PSEGMENT;

正如我所料,结构体包含着对CONTROL_AREA的引用,指向Proto PTE的pool的指针和所有section的信息。有个东西需要解释一下。结构体的样子依赖于是否支持PAE。PAE就是Physical Address Extenion。从第5版开始,Windows NT包含了支持PAE的内核Ntkrnlpa.exe。总的来讲,支持PAE就意味着在NT里可以使用的虚拟地址不是4GB而是64GB。在使用PAE时的地址转换又多了一级——所有的虚地址空间被分为4部分。在打开PAE时PTE和PDE的大小不是4B而是8B,这我们可以从SEGMENT结构体中看出。现在还不需要进一步详细的讲PAE,毕竟很少用到,所以我们就此打住。

描述section的所有结构体都介绍过了,而section对象结构体本身还没有提到。从直观上可以想到,它应该会引用到SEGMENT或是CONTROL_AREA,因为有了这两个结构体后就可以得到保存的所有信息。通过反汇编得到的section对象的body为以下形式:

typedef struct _SECTION_OBJECT { // size 0x28
               VAD_HEADER VadHeader; // 0
               PSEGMENT pSegment;         //0x14 Segment
               LARGE_INTEGER SectionSize; //0x18
               DWORD ControlFlags;        //0x20
               DWORD PgProtection;        //0x24
               } SECTION_OBJECT, *SECTION_OBJECT;

#define PageFile      0x10000
#define MappingFile   0x8000000
#define Based         0x40
#define Unknown       0x800000 // not sure, in fact it's AllocAttrib&0x400000

我们看到,所得的结构体完全符合现有的高层信息的描述。唯一可能有疑问的就是VAD_HEADER。它描述了base section在地址空间中的位置。VAD_HEADER位于顶点为_MmSectionBasedRoot的VAD树中。我们再次体会到,要理解操作系统的工作原理,就要理解其内部的结构。为了有一个总体上的把握,下面给出了描述section的结构体间互相联系的一个图。

SECTION_OBJECT->SEGMENT<->CONTROL_AREA->FILE_OBJECT->SECTION_OBJECT_POINTERS+
                               ^                                            |
                               +--------------------------------------------+


08.从内存管理器角度看进程的创建
====================================================

前面我们从Win32角度介绍过进程的创建,也讲过内存管理器和对象管理器的工作原理,以及section对象结构体。现在最有意思的当然就是在进程创建中将内存管理器也考虑进来。

进程是用未公开的系统调用NtCreateProcess()创建的。下面给出其伪代码:

/*****************************************************************/
/* -- Here it is, just wrapper -- */
NtCreateProcess(
      OUT Handle,
      IN ACCESS_MASK Access,
      IN POBJECT_ATTRIBUTES ObjectAttrib,
      IN HANDLE Parent,
      IN BOOLEAN InheritHandles,
      IN HANDLE SectionHandle,
      IN HANDLE DebugPort,
      IN HANDLE ExceptionPort
      )
{
if(Parent)
        {
     ret=PspCreateProcess(Handle,
                    Access,
                    ObjectAttrib,
                    Parent,
                    InheritHandles,
                    SectionHandle,
                    DebugPort,
                    ExceptionPort);
        }
else ret=STATUS_INVALID_PARAMETER;
return ret;
}

我们看到,NtCreateProcess是对另一个内部函数PspCreateProcess的封装。NtCreateProcess进行的唯一工作就是检查Parent(父进程句柄)。但是接下来我们看到,对于NT来说这并没有什么意义,因为总的来说,进程的继承性本身没有特别的意义。现在我们来看PspCreateProcess()。

PspCreateProcess(
      OUT PHANDLE Handle,
      IN ACCESS_MASK Access,
      IN POBJECT_ATTRIBUTES ObjectAttrib,
      IN HANDLE Parent,
      IN BOOLEAN InheritHandles,
      IN HANDLE SectionHandle,
      IN HANDLE DebugPort,
      IN HANDLE ExceptionPort
      );

我很快注意到,函数中的Parent参数可以接受值0,这就表明在NtCreateProcess中检验此参数是为了限制用户模式。函数的参数中有对section、debug port和exception port、父进程的引用。通过调用ObReferenceObjectByHandle,可以得到指向这些对象的指针。实际上父进程句柄通常传递的是-1,这表示是当前进程。如果Parent等于0,则进程的affinity就不从父进程处取得,而是从系统变量中取得。

if(Parent)
      { //Get pointer to father's body
        ObReferenceObjectByHandle(Parent,0x80,PsProcessType,PrevMode,&pFather,0);
        AffinityMask=pFather->Affinity; // on witch processors will be executed
        Prior=8;
      }
else {
      pFather=0;
      AffinityMask=KeActiveProcessors;
      Prior=8;
     }

优先级总是为8。随后,创建进程对象。NT4.0下其大小为504字节。

// size of process body - 504 bytes
// creating process object... (type object PsProcessType)
ObCreateObject(PrevMode,PsProcessType,ObjectAttrib,PrevMode,0,504,&pProcess);
// clear body
memset(pProcess,0,504);

初始化某些域和Quota Block(见对象管理器的相关介绍)。

pProcess->CreateProcessReported=0;
pProcess->DebugPort=pDebugPort;
pProcess->ExceptPort=pExceptPort;

// Inherit Quota Block, if pFather==NULL, PspDefaultQuotaBlock
PspInheritQuota(pProcess,pFather);

if(pFather){
           pProcess->DefaultHardErrorMode=pFather->DefaultHardErrorMode;
           pProcess->InheritedFromUniqueProcessId=pFather->UniqueProcessId;
           }
else {
       pProcess->InheritedFromUniqueProcessId=0;
       pProcess->DefaultHardErrorMode=1;
     }

之后,调用MmCreateProcessAddressSpace,创建地址上下文。参数是函数得到的指向进程的指针、工作集的大小和指向结果结构体的指针。这个结构体形式如下:

struct PROCESS_ADDRESS_SPACE_RESULT{
dword Dt; // dict. table phys. addr.
dword HypSpace; // hyp space page phys. addr.
dword WorkingSet; // working set page phys. addr.
}CASResult;

MmCreateProcessAddressSpace(PsMinimumWorkingSet,pProcess,&CASResult);

我们看到,函数向我们返回的是页表的物理地址描述符(用于新地址空间的CR3的内容),Hyper Space的页地址和工作集的页地址。在此之后是初始化进程对象的某些域:

pProcess->MinimumWorkingSet=MinWorkingSet;
pProcess->MaximumWorkingSet=MaximumWorkingSet;

KeInitializeProcess(pProcess,Prior,AffinityMask,&CASResult,pProcess->
                                             DefaultHardErrorProcessing&0x4);

pProcess->ForegroundQuantum=PspForegroundQuantum;

如果有父进程且设置了标志参数,则会继承父进程的句柄表:

if(pFather) // if there is father and inherithandle, so, inherit handle db
      {
       pFather2=0;
       if(bInheritHandle)pFather2=pFather;
       ObInitProcess(pFather2,pProcess); // see info about ObjectManager
      }

下面的东西比较有意思,证明了NT执行系统的灵活性,从表面上是看不出来的。如果在参数中有指定的section,则使用这个section来初始化进程的地址空间,否则其工作就会像*UNIX中的fork()。

if(pSection)
     {
       MmInitializeProcessAddressSpace(pProcess,0,pSection);
       ObDereferenceObject(pSection);
       res=ObInitProcess2(pProcess); //work with unknown byte +0x22 in process
       if(res>=0)PspMapSystemDll(pProcess,0);
       Flag=1; //Created addr space
     }
else { // if there is futher, but no section, so, do operation like fork()
      if(pFatherProcess){
                   if(PsInitialSystemProcess==pFather){
                             MmRes=MmInitializeProcessAddressSpace(pProcess,0,0);
                                                   }
                   else {
                      pProcess->SectionBaseAddress=pFather->SectionBaseAddress;
                      MmRes=MmInitializeProcessAddressSpace(pProcess,pFather,0);
                      Flag=1; //created addr space
                         }
                  }
     }

接下来是使用PsActiveProcessHead将进程插入Active Process链表,创建Peb和做其它辅助性的工作。我们不再赘述。最后,当所有的工作都做完后,进行安全子系统方面的工作。我们过去曾研究过安全子系统(见对象管理器部分),所以这里只简单的给出其伪代码。只是我注意到,如果父进程是system(句柄值等于PspInitialSystemProcessHandle),则不对其安全性进行检验。

// finally, security operations
if(pFather&&PspInitialSystemProcessHandle!=Father)
           {
            ObGetObjectSecurity(pProcess,&SecurityDescriptor,&MemoryAllocated);
            pToken=PsReferencePrimaryToken(pProcess);
            AccessRes=SeAccessCheck(SecurityDescriptor,&SecurityContext,
                             0,0x2000000,
                             0,0,&PsProcessToken->GenericMapping,
                             PrevMode,pProcess->GrantedAccess,
                             &AccessStatus);
            ObDereferenceObject(pToken);
            ObReleaseObjectSecuryty(SecurityDescriptor,MemoryAllocated);
            if(!AccessRes)pProcess->GrantedAccess=0;
            pProcess->GrantedAccess|=0x6fb;
            }
   else{
        pProcess->GrantedAccess=0x1f0fff;
       }

   if(SeDetailedAuditing)SeAuditProcessCreation(pProcess,pFather);

最有意思的是函数KeInitializeProcess和MmCreateProcessAddressSpace。前一个函数除了初始化进程对象的其它成员之外,还要初始化TSS中的IO位图的偏移。

pProcess->IopmOffset=0x20ad; // IOMAP BASE!!!
                            // You can patch kernel here and
                            // got i/o port control ;)

偏移的选取是这样的,它指向I/O位图,这样就能阻止进程直接使用I/O端口。

在函数MmCreateProcessAddressSpace中进行的是进程地址空间的创建。我就不给出所有的伪代码了,只简要的写写主要的操作。它为Hyper Space, Working Set和Page Directory选择页。反汇编后的代码证实了,它们是从zero frame链表中选出或是由MiZeroPhysicalPage函数来清零的。之后初始化新创建的Page Directory。

pProcess->WorkingSetPage=Frame3; // WorkingSetPage
(MmPfnDatabase+0x18*Frame)->Pte=0xc0300000;
ValidPde_U=ValidPdePde&0xeff^Frame2; // HyperSpace

/**************IMPORTANT!!!!!!!!!!!!!!************************/
/* 重要! 这里初始化PD                                        */
/*************************************************************/

Va=MiMapPageInHyperSpace(Frame,&LastIrql);
// no we got Va of our new Page Directory
// Fill some fields
*(Va+0xc04)=ValidPde_U; // HyperSpace
ValidPde_U=ValidPde_U&0xfff^PhysAddr; // DT
*(Va+0xc00)=ValidPde_U; // self-pde

// copy from current process, kernel address mapping
memcpy(
     (MmVirtualBias+0x80000000)>>0x14+Va, // it's like that we found,
                                          // what MmVirtualBias is it ;)
     (MmVirtualBias+0x80000000)>>0x14+0xc0300000,
     0x80 // 32 pdes -> 4Mb*32=128Mb
     );

memcpy( // copy pdes, corresponding to NonPagedArea
     MmNonPagedSystemStart>>0x14+Va,
     MmNonPagedSystemStart>>0x14+0xc0300000,
     (0xc0300ffc-MmNonPagedSystemStart>>0x14+0xc0300000)&0xfffffffc+4);

memcpy(Va+0xc0c, // cache, forgot about it now, it's another story ;)
       0xc0300c0c,
       (MmSystemCacheEnd>>0x14)-0xc0c+4
       );

也就是将PDE拷贝到内核地址空间中去(其对所有的进程不变,Hyper Space除外),而且是拷贝到不可换出的区域。同时这个空间是属于系统cache的。


09.上下文切换
==========================

知道了ETHREAD、EPROCESS结构体和内存管理器的工作原理,就不难猜到上下文切换时会发生什么。Windows NT的设计者使用线程,不关心共享的是谁的地址空间,也就是说有两种可能:线程属于当前进程——必需要切换到另一个线程(更新堆栈并更换GDT描述符),而线程属于另一个进程,必需切换到那个进程(重新加载CR3)。对此,为了证实我的推测,我反汇编了KeAttachProcess函数。这个函数是未公开的,但所有已知的函数都用其来切换到另一进程的地址空间。通过KeDetachProcess可以返回到当前进程。KeAttachProcess使用下述内部函数:

KiAttachProcess - KeAttachProcess仅仅是对这个函数的封装
KiSwapProcess   - 更换地址空间。(本质上就是重新加载CR3)
SwapContext     - 更换上下文。一般不管地址空间的切换,只调整线程上下文。
KiSwapThred     - 切换到链表中的下一个线程(SwapContext)调用

下面给出这些内部函数的伪代码。
-----------------------------------------------------------------------------
/************************ KeAttachProcess ***************************/
// just wrapper
//
KeAttachProcess(EPROCESS *Process)
{
KiAttachProcess(Process,KeRaiseIrqlToSynchLevel);
}
/************************ KiAttachProcess ***************************/

KiAttachProcess(EPROCESS *Process,Irql){

//CurThread=fs:124h
//CurProcess=CurThread->ApcState.Process;

if(CurProcess!=Process){
     if(CurProcess->ApcStateIndex || KPCR->DpcRoutineActive)KeBugCheckEx...
     }

//if we already in process's context
if(CurProcess==Process){KiUnlockDispatcherDatabase(Irql);return;}

Process->StackCount++;
KiMoveApcState(&CurThread->ApcState,&CurThread->SavedApcState);

// init lists
CurThread->ApcState.ApcListHead[0].Blink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[0].Flink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[1].Blink=&CurThread->ApcState.ApcListHead[1];
CurThread->ApcState.ApcListHead[1].Flink=&CurThread->ApcState.ApcListHead[1];;

//fill curtheads's fields
CurThread->ApcState.Process=Process;

CurThread->ApcState.KernelApcInProgress=0;
CurThread->ApcState.KernelApcPending=0;
CurThread->ApcState.UserApcPending=0;

CurThread->ApcState.ApcStatePointer.SavedApcState=&CurThread->SavedApcState;
CurThread->ApcState.ApcStatePointer.ApcState=&CurThread->ApcState;

CurThread->ApcStateIndex=1;

//if process ready, just swap it...
if(!Process->State)//state==0, ready
     {
     KiSwapProcess(Process,CurThread->SavedApcState.Process);
     KiUnlockDispatcherDatabase(Irql);
     return;
     }

CurThread->State=1; //ready?
CurThread->ProcessReadyQueue=1;

//put Process in Thread's waitlist
CurThread->WaitListEntry.Flink=&Process->ReadyListHead.Flink;
CurThread->WaitListEntry.Blink=Process->ReadyListHead.Blink;

Process->ReadyListHead.Flink->Flink=&CurThread->WaitListEntry.Flink;
Process->ReadyListHead.Blink=&CurThread->WaitListEntry.Flink;

// else, move process to swap list and wait
if(Process->State==1){//idle?
     Process->State=2; //trans
     Process->SwapListEntry.Flink=&KiProcessInSwapListHead.Flink;
     Process->SwapListEntry.Blink=KiProcessInSwapListHead.Blink;
        KiProcessInSwapListHead.Blink=&Process->SwapListEntry.Flink;
     KiSwapEvent.Header.SignalState=1;
     if(KiSwapEvent.Header.WaitListHead.Flink!=&KiSwapEvent.Header.WaitListHead.
Flink)
                    KiWaitTest(&KiSwapEvent,0xa); //fastcall
     }

CurThread->WaitIrql=Irql;
KiSwapThread();
return;
}

从这个函数可以得到以下结论。进程可以处于以下状态——0(准备),1(Idle),2(Trans——切换)。这证实了高层次的信息。KiAttachProcess使用了另外两个函数KiSwapProcess和KiSwapThread。

/************************* KiSwapProcess ****************************/

KiSwapProcess(EPROCESS* NewProcess, EPROCESS* OldProcess)
{
// just reload cr3 and small work with TSS

     // TSS=KPCR->TSS;
     // xor eax,eax
     // mov gs,ax
TSS->CR3=NewProcess->DirectoryTableBase;//0x1c
     // mov cr3,NewProcess->DirectoryTableBase
TSS->IopmOffset=NewProcess->IopmOffset;//0x66
if(WORD(NewProcess->LdtDescriptor)==0){lldt 0x00; return;//}
     //GDT=KPCR->GDT;
(QWORD)GDT->0x48=(QWORD)NewProcess->LdtDescriptor;
(QWORD)GDT->0x108=(QWORD)NewProcess->Int21Descriptor;
lldt 0x48;
return;
}

[1] [2] [3] [4]

责任编辑 webmaster

 
 
 
 
 
评论更多>>
 
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •