⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄

📁 兼容内核漫谈 适合想将Windows上的程序移植到其它平台上的朋友研究查看
💻
📖 第 1 页 / 共 2 页
字号:
            PVOID SBaseAddress = (PVOID)
                  ((char*)ImageBase + (ULONG_PTR)SectionSegments[i].VirtualAddress);
            MmLockSectionSegment(&SectionSegments[i]);
            Status = MmMapViewOfSegment(Process,
                                        AddressSpace,
                                        Section,
                                        &SectionSegments[i],
                                        &SBaseAddress,
                                        SectionSegments[i].Length,
                                        SectionSegments[i].Protection,
                                        0,
                                        FALSE);
            MmUnlockSectionSegment(&SectionSegments[i]);
            . . . . . .
         }
      }
      *BaseAddress = (PVOID)ImageBase;
   }
   else
   {
      . . . . . .
      if (SectionOffset == NULL)
      {
         ViewOffset = 0;
      }
      else
      {
         ViewOffset = SectionOffset->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,
                                  BaseAddress,
                                  *ViewSize,
                                  Protect,
                                  ViewOffset,
                                  (AllocationType & MEM_TOP_DOWN));
      MmUnlockSectionSegment(Section->Segment);
      . . . . . .
   }

   MmUnlockAddressSpace(AddressSpace);

   return(STATUS_SUCCESS);
}[/code]
    我把这段程序留给读者自己阅读,只是略加提示:Section对象所代表的目标文件分为两大类,一类是可执行映像文件,一类是不同文件。可执行映像文件的映射比普通文件要复杂一些,因为映像文件中一般有好多不同的段,需要映射到不同的地址上去,这就是代码中有两个for循环的原因。每个段的映射则都是由MmMapViewOfSegment()完成的。

[code][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,
                               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);
}[/code]
    这里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数据结构(在上面的代码中是&Process->AddressSpace)。由于已分配使用(而尚未释放)的虚拟地址区间一般都是不连续的,例如用于堆栈的区间和可执行代码的区间就不会连续,所以从数据结构的角度看这“账本”的具体内容总是一个链表,链表中的每一个结点都代表着一个已分配使用的地址区间,在Linux内核中这就是vm_area_struct数据结构(在上面的代码中是MEMORY_AREA数据结构)。在这一方面,不同操作系统的内核在具体的数据结构和程序实现上可以有所不同,但是大体上都是一样的,变不出太多的花样。所以,要把一个Section映射到一个进程的用户空间,首先是对这“账本”的操作。
    但是,光有这账本还不够,因为这账本并不直接对CPU中的页面映射部件MMU起作用,所以还需要有一个用于MMU的页面映射表,这就是“一个表”。所谓挂靠到某个进程,就是把这个进程的页面映射表装入MMU,使得访问用户空间的某个地址时使用的是目标进程的页面映射表。当然,在任何特定的时刻,MMU中只能有一个页面映射表,既然装入了目标进程的页面映射表,就离开了原来进程的页面映射表。但是,不管是什么进程的页面映射表,他们的系统空间部分、即内核部分、则都是共同的。由此可见,“进程挂靠”(和恢复)只能在内核中进行,而不能在用户空间进行。
    这里还要注意,对于页面映射表的“准备”和“使用”是两码事,建立映射时所涉及的是准备,而把准备好了的页面映射表装入MMU才开始了它的使用。
    所以,ZwMapViewOfSection()之所以不需要挂靠到目标进程,是因为建立映射的过程只是账面的操作,而并不真的要去访问(目标进程)用户空间的某个地址。
    按理说,既然是把一个Section映射到目标进程的用户空间,就应该同时完成对账本和映射表的操作。但是ReactOS的代码把这两种操作分离了开来,在NtMapViewOfSection()中只是对账本的操作,而把对映射表的操作推迟了(下面就会看到),那当然也是可以的。
    至此,ntdll.dll的映射已经完成,回到LdrpMapSystemDll()的代码中,下一步是要从这映像中获取LdrInitializeThunk()等函数的入口地址,这时候就需要实施进程挂靠了。

[code][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();

    . . . . . .

    /* 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);
    }
}[/code]
    前面的映射只是记在了新建进程的账本上,却没有改变它的页面映射表,这里的UpdatePageDirs()就来处理这页面映射表了。
    这里KeAcquireDispatcherDatabaseLock()的作用是通过提高中断优先级达到禁止线程调度的目的。因为下面的KiAttachProcess()即将实现用户空间的切换,在这个当口上是不能允许线程调度的。
    下面就是“挂靠”的实施了。

[code][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) {
       
        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");
}[/code]
    注意代码中的Process->StackCount与进程的“堆栈”并无关系,而是指进程挂靠的嵌套深度。
    前面讲过,所谓挂靠到某个进程,就是切换到那个进程的用户空间,就是把那个进程的页面映射表装入MMU,这里调用KiSwapProcess()的原因就在于此。不过在此之前还需要把当前进程的APC队列从ApcState转移到SavedApcState去,所以还调用了KiMoveApcState(),读者可以结合前一篇漫谈把这里的程序读懂。此外,这里KeReleaseDispatcherDatabaseLock()一方面是解除对线程调度的禁令,一方面是回到原来的中断优先级。与之配对的是前面KeAttachProcess()中的KeAcquireDispatcherDatabaseLock()。
    我们接着看KiSwapProcess()的代码。

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

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -