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

📄 漫谈兼容内核之二十一windows进程的用户空间.txt

📁 漫谈系统内核内幕 收集得很辛苦 呵呵 大家快下在吧
💻 TXT
📖 第 1 页 / 共 4 页
字号:
漫谈兼容内核之二十一:Windows进程的用户空间


漫谈兼容内核之二十一:Windows进程的用户空间
                                                毛德操

    进程的用户空间是应用软件活动的舞台,所以搞清楚Windows进程用户空间的格局和轮廓对于深入理解Windows进程的运行有着重要的意义。而对于兼容内核的开发者,则这个问题更是非搞清楚不可,因为开发兼容内核的意图就是要让Windows应用软件得以在Linux系统上运行。其实,在前面的一些漫谈中,随着当时话题的开展也曾讲到过有关这个问题的一些大概,但是一方面只是散见于各处,不够集中;另一方面也不够系统和完整,实际上也因而不够详细和确切;因此有必要专门把它作为一个话题加以比较系统、完整的深入介绍。有道是“耳闻为虚,眼见为实”,对于我们来说,所谓“眼见”就是要见到有关的源代码。当然,我们所能见到的只是RreactOS的源代码,好在各种迹象表明RreactOS与Windows在这方面也是相当贴近和一致的。
    我们先从几个宏定义着手:

#define MM_HIGHEST_USER_ADDRESS           *MmHighestUserAddress
#define MM_USER_PROBE_ADDRESS             *MmUserProbeAddress
#define MM_LOWEST_USER_ADDRESS            (PVOID)0x10000

#define KI_USER_SHARED_DATA                  0xffdf0000
#define SharedUserData           \
                ((KUSER_SHARED_DATA  *CONST) KI_USER_SHARED_DATA)

    在ReactOS的代码中,每当需要引用用户空间的上限、即最高地址时,一般就把MM_HIGHEST_USER_ADDRESS用作常数,但实际上这是个宏操作,是在引用指针MmHighestUserAddress的内容。与此相似的还有MM_USER_PROBE_ADDRESS,实际上是在引用指针MmUserProbeAddress的内容。这两个指针的内容是在内核初始化时设置好的:

MmInit1(…)
{
   ……
   MmUserProbeAddress = 0x7fff0000;
   MmHighestUserAddress = (PVOID)0x7ffeffff;
   ……
}

    就是说,应用软件在用户空间可以访问的最高地址(虚拟地址)是0x7ffeffff,从0x7fff0000开始就不能访问了。一般文献中讲Windows系统中用户空间与系统空间的分界线是0x80000000,而这里之所以是0x7fff0000而不是0x80000000,是因为在分界下面留了64KB的空间不让访问,以此作为隔离区。
    应用软件在用户空间可以访问的最低地址是MM_LOWEST_USER_ADDRESS,这是个常数,定义为0x10000,就是第一个64KB边界的地方,而从0开始的64KB也是不让访问的。
    还有个常数KI_USER_SHARED_DATA更是值得一说。这个常数定义为0xffdf0000,是一个区间的起点。这个区间按说是在系统空间,却又划出来让用户空间的程序访问,就好像是用户空间的一小片飞地。这片飞地的目的是用来让用户空间的程序访问内核中的一些数据,好像是在系统空间的地皮上挖了口井。而且,这个区间是由系统空间和所有用户空间共享,也就是为所有进程所共享,所以这个常数名为KI_USER_SHARED_DATA。那么这个“井”里有些什么数据呢?上面的宏定义为此定义了一个数据结构KUSER_SHARED_DATA,并定义了一个相应的结构指针SharedUserData,让它指向这个地址:

typedef struct _KUSER_SHARED_DATA {
    ULONG TickCountLowDeprecated;
    ULONG TickCountMultiplier;
    volatile KSYSTEM_TIME InterruptTime;
    volatile KSYSTEM_TIME SystemTime;
    volatile KSYSTEM_TIME TimeZoneBias;
    USHORT ImageNumberLow;
    USHORT ImageNumberHigh;
    WCHAR NtSystemRoot[260];
    ULONG MaxStackTraceDepth;
    ULONG CryptoExponent;
    ULONG TimeZoneId;
    ULONG LargePageMinimum;
    ULONG Reserved2[7];
    NT_PRODUCT_TYPE NtProductType;
    BOOLEAN ProductTypeIsValid;
    ULONG NtMajorVersion;
    ULONG NtMinorVersion;
    BOOLEAN ProcessorFeatures
;
    ULONG Reserved1;
    ULONG Reserved3;
    volatile ULONG TimeSlip;
    ALTERNATIVE_ARCHITECTURE_TYPE AlternativeArchitecture;
    LARGE_INTEGER SystemExpirationDate;
    ULONG SuiteMask;
    BOOLEAN KdDebuggerEnabled;
    volatile ULONG ActiveConsoleId;
    volatile ULONG DismountCount;
    ULONG ComPlusPackage;
    ULONG LastSystemRITEventTickCount;
    ULONG NumberOfPhysicalPages;
    BOOLEAN SafeBootMode;
    ULONG TraceLogging;
    ULONGLONG Fill0;
    UCHAR SystemCall[16];
    union {
        volatile KSYSTEM_TIME TickCount;
        volatile ULONG64 TickCountQuad;
    };
} KUSER_SHARED_DATA, *PKUSER_SHARED_DATA;

    这样,地址0xffdf0000就是KUSER_SHARED_DATA数据结构的起点,而SharedUserData就指向这个数据结构。而用户空间的程序则可以通过指针SharedUserData读取这个结构中各个成分的内容。下面我们看几个通过使用这个指针从内核获取某些信息的例子。第一个例子是ntdll.dll中的一个函数LdrpMapDllImageFile():

[LdrLoadDll() > LdrpLoadModule() > LdrpMapDllImageFile()]

LdrpMapDllImageFile (…)
{
  . . . . . .

  . . . . . .
  if (SearchPath == NULL)
  {
      /* get application running path */
      . . . . . .
      wcscat (SearchPathBuffer, L";");
      wcscat (SearchPathBuffer, SharedUserData->NtSystemRoot);
      wcscat (SearchPathBuffer, L"\\system32;");
      . . . . . .
      SearchPath = SearchPathBuffer;
  }
  . . . . . .
}

    “宽字符”数组NtSystemRoot[]是KUSER_SHARED_DATA数据结构中的一个成分,其内容是系统根目录的路径名。
    再看第二个例子,这是kernel32.dll导出的一个函数:

DWORD STDCALL GetTickCount(VOID)
{
  return (DWORD)((ULONGLONG)SharedUserData->TickCountLowDeprecated *
                              SharedUserData->TickCountMultiplier / 16777216);
}

    GetTickCount()是W32 API界面上的一个函数,用户空间的程序可以通过这个函数获取内核中的Tick计数,类似于Linux内核中的jiffies计数。
    如果应用软件的代码都不直接引用SharedUserData,而一律都通过ntdll.dll或kernel32.dll导出的函数间接地访问这个数据结构,那么这两个DLL大体上可以把对于这个数据结构的访问嫁接到Linux内核上,比方说通过相关的系统调用、或/proc机制等等,那都还是可行的。例如,Wine对GetTickCount()的实现是这样的:

DWORD WINAPI GetTickCount(void)
{
    struct timeval t;
    gettimeofday( &t, NULL );
    return ((t.tv_sec * 1000) + (t.tv_usec / 1000)) - server_startticks;
}

    显然,这是依靠Linux系统调用gettimeofday()实现了对访问SharedUserData中有关数据的模拟。对其它成分的访问也有望通过类似的手法实现。
    但是,既然从用户空间可以通过基地址0xffdf0000访问这个数据结构,而且已经为人所知,就保不住没有应用软件会不守规矩,放着好好的阳关道不走偏要去走独木桥,直接就通过这个地址来达到目的。这在早期的Windows软件中似乎更有可能,因为当初DOS软件中就常常会通过一些地址约定来获取某些特定的信息。因此,要达到与Windows软件的高度兼容,就得考虑在Linux系统空间的相同位置上也挖出这么一口“井”来。

    读者在上面看到的还只是对用户空间格局的安排,而并未实际将其实现,还算不上是眼见为实;再说也过于粗线条,只是划定了一个边界而已。下面我们就来看实际的实现和有关的细节。既然用户空间是进程的用户空间,那么用户空间的创建自然就与进程的创建连系在一起。事实上,用户空间的创建及其格局的实现有很多都是在函数PspCreateProcess()内部实现的。我们以前也看过这个函数的代码,只是当时的侧重面不同,现在我们就把目光集中在与用户空间有关的存储管理上。

[NtCreateProcess() > PspCreateProcess()]

PspCreateProcess(OUT PHANDLE ProcessHandle, …)
{
   PEPROCESS Process;.
    . . . . .

   . . . . . .
   MmInitializeAddressSpace(Process, &Process->AddressSpace);
   ObCreateHandleTable(pParentProcess, InheritObjectTable, Process);
   MmCopyMmInfo(pParentProcess ? pParentProcess : PsInitialSystemProcess, Process);
   . . . . . .
   /* Now we have created the process proper */
   MmLockAddressSpace(&Process->AddressSpace);

   /* Protect the highest 64KB of the process address space */
   BaseAddress = (PVOID)MmUserProbeAddress;
   Status = MmCreateMemoryArea(Process, &Process->AddressSpace,
                   MEMORY_AREA_NO_ACCESS,
                   &BaseAddress, 0x10000, PAGE_NOACCESS,
                   &MemoryArea, FALSE, FALSE, BoundaryAddressMultiple);
   . . . . . .

   /* Protect the lowest 64KB of the process address space */
#if 0
   BaseAddress = (PVOID)0x00000000;
   Status = MmCreateMemoryArea(Process, &Process->AddressSpace,
                   MEMORY_AREA_NO_ACCESS,
                   &BaseAddress, 0x10000, PAGE_NOACCESS,
                   &MemoryArea, FALSE, FALSE, BoundaryAddressMultiple);
   . . . . . .
#endif

    每个进程都有个用户空间。进程控制块EPROCESS数据结构中有个成分AddressSpace,就是代表着用户空间的MADDRESS_SPACE数据结构。所以在创建进程时要通过MmInitializeAddressSpace()对新建进程的地址空间AddressSpace进行初始化,特别是将此数据结构中的成分LowestAddress设置成MM_LOWEST_USER_ADDRESS即0x10000。然后通过MmCopyMmInfo()从创建者进程或系统进程“System”复制其系统空间的页面目录。这里的指针PsInitialSystemProcess指向系统进程“System”的EPROCESS数据结构。不过,这里复制的只是系统空间的页面目录,而与用户空间无关。
    下面就开始从新建的用户空间具体地配置虚拟地址区间了。首先是从MmUserProbeAddress所指向的0x7fff0000开始往上,长度为0x10000、即64KB的区间,这个区间的性质是禁止访问。这里MEMORY_AREA_NO_ACCESS表示区间的性质,将被记入该进程AddressSpace中有关的数据结构中,而PAGE_NOACCESS则将被记入页面映射目录相应的表项中。本来还把下面从地址0开始向上的64KB也设置成禁止访问,后来用编译条件“#if 0”将其删去了,原因大概是既然已经把用户空间的地址下限设定为MM_LOWEST_USER_ADDRESS,因而不会把这下面的区间分配出去,反正没有映射,也就没有必要多此一举了。
    这就好像是房地产商“批地”的过程。至此,从MM_LOWEST_USER_ADDRESS即0x10000开始,到0x7fff0000为止的用户空间“地皮”就形成了。不过现在的整块地皮都是空的,只要未经分配和映射,就都不能访问。
    读者可能注意到了,上面讲到把用户空间的下限设置成0x10000,却没有设置上限。这是因为用户空间还要有一块“飞地”在USER_SHARED_DATA即0xffdf0000的地方,那就差不多已是整个CPU寻址范围的尽头了。我们继续往下看:

[NtCreateProcess() > PspCreateProcess()]

   /* Protect the 60KB above the shared user page */
   BaseAddress = (char*)USER_SHARED_DATA + PAGE_SIZE;
   Status = MmCreateMemoryArea(Process, &Process->AddressSpace,
                   MEMORY_AREA_NO_ACCESS,
                   &BaseAddress, 0x10000 - PAGE_SIZE, PAGE_NOACCESS,
                   &MemoryArea, FALSE, FALSE, BoundaryAddressMultiple);
   . . . . . .

   /* Create the shared data page */
   BaseAddress = (PVOID)USER_SHARED_DATA;
   Status = MmCreateMemoryArea(Process, &Process->AddressSpace,
                   MEMORY_AREA_SHARED_DATA,
                   &BaseAddress, PAGE_SIZE, PAGE_READONLY,
                   &MemoryArea, FALSE, FALSE, BoundaryAddressMultiple);
   MmUnlockAddressSpace(&Process->AddressSpace);
   . . . . . .

    这是在建立前面所讲的用户共享区,就像是在系统空间地皮上的“飞地”。先看后面那个操作,这是把从USER_SHARED_DATA即0xffdf0000开始的一个页面分配给用户空间,让用户空间的程序可以对此页面作“只读”访问。再看前面那个操作,这是把从0xffdf0000开始的64KB除已分配的最低那个页面之外全都设置成禁止访问。

    至此,用户空间的本土和飞地都已圈好,下面就开始分配和使用其本土的地皮了。

[NtCreateProcess() > PspCreateProcess()]

   /* FIXME - Map ntdll */
   Status = LdrpMapSystemDll(hProcess, &LdrStartupAddr);
   . . . . . .
   /* Map the process image */
   if (SectionObject != NULL)
   {
      . . . . . .
      Status = MmMapViewOfSection(SectionObject, Process, (PVOID*)&ImageBase, 0,
               ViewSize, NULL, &ViewSize, 0, MEM_COMMIT, PAGE_READWRITE);

⌨️ 快捷键说明

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