📄 漫谈兼容内核之二十一windows进程的用户空间.txt
字号:
漫谈兼容内核之二十一: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 + -