📄 漫谈兼容内核之二十一windows进程的用户空间.txt
字号:
}
/* store stack information from InitialTeb */
if(InitialTeb != NULL)
{
if(InitialTeb->StackBase && InitialTeb->StackLimit) /* fixed-size stack */
{
Teb.Tib.StackBase = InitialTeb->StackBase;
Teb.Tib.StackLimit = InitialTeb->StackLimit;
Teb.DeallocationStack = InitialTeb->StackLimit;
}
else /* expandable stack */
{
Teb.Tib.StackBase = InitialTeb->StackCommit;
Teb.Tib.StackLimit = InitialTeb->StackCommitMax;
Teb.DeallocationStack = InitialTeb->StackReserved;
}
}
/* more initialization */
Teb.Cid.UniqueThread = Thread->Cid.UniqueThread;
Teb.Cid.UniqueProcess = Thread->Cid.UniqueProcess;
Teb.CurrentLocale = PsDefaultThreadLocaleId;
/* Terminate the exception handler list */
Teb.Tib.ExceptionList = (PVOID)-1;
. . . . . .
/* write TEB data into teb page */
Status = NtWriteVirtualMemory(ProcessHandle, TebBase,
&Teb, sizeof(TEB), &ByteCount);
. . . . . .
if (TebPtr != NULL)
{
*TebPtr = (PTEB)TebBase;
}
return Status;
}
先根据初始TEB以及目标线程ETHREAD结构中的一些信息准备好一个作为草稿的TEB数据结构,然后通过NtWriteVirtualMemory()将其复制到目标线程的TEB。从代码中可见线程的TEB中记录着有关其堆栈的信息。TEB数据结构的第一个成分是个NT_TIB数据结构,即“线程信息块”Tib。Tib中的StackBase指向本线程堆栈的原点、即地址最高处,而StackLimit则指向堆栈所在区间的下部边界、即地址最低处。
最后,如果参数TebPtr非0,还要通过这个指针返回目标线程所在的地址TebBase。
读者很自然会有个问题:同一个进程中以后还会创建新的线程,它们的堆栈和TEB在那里呢?Win32 API为随后的线程创建提供了一个库函数CreateThread(),这个库函数则调用CreateRemoteThread()。可别被“Remote”给搞糊涂了,这只是说也可以在别的进程中创建线程,而CreateThread()则总是在调用者本身所在进程的内部创建线程。
HANDLE STDCALL
CreateRemoteThread(HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId)
{
. . . . . .
PIMAGE_NT_HEADERS pinhHeader =
RtlImageNtHeader(NtCurrentPeb()->ImageBaseAddress);
. . . . . .
/* FIXME: do more checks - e.g. the image may not have an optional header */
if(pinhHeader == NULL)
{
nStackReserve = 0x100000;
nStackCommit = PAGE_SIZE;
}
else
{
nStackReserve = pinhHeader->OptionalHeader.SizeOfStackReserve;
nStackCommit = pinhHeader->OptionalHeader.SizeOfStackCommit;
}
. . . . . .
/* create the thread */
nErrCode = RtlRosCreateUserThreadVa(hProcess,
&oaThreadAttribs, dwCreationFlags & CREATE_SUSPENDED,
0, &nStackReserve, &nStackCommit,
(PTHREAD_START_ROUTINE)ThreadStartup, &hThread, &cidClientId,
2, lpStartAddress, lpParameter);
/* success */
if(lpThreadId) *lpThreadId = (DWORD)cidClientId.UniqueThread;
return hThread;
}
从RtlRosCreateUserThreadVa()开始往下的操作就跟以前所见到的一样了。只是所分配的堆栈区间未必就紧挨着前一个线程的堆栈,因为在此之前可能有已经在运行的线程通过NtAllocateVirtualMemory()分配了虚存区间。所以,除第一个线程的堆栈固定在0x20000处以外,别的就没有固定的位置了。至于TEB,则前面已经看到,是随着指针Process->TebLastAllocated的值逐次下移,每次一个页面。这样,从第一个线程的TEB开始,依次为0x7FFDE000,0x7FFDD000,0x7FFDC000,0x7FFDB000等等。
如前所述,进程环境块PEB的起点是0x7FFDF000,大小为0x1000;而从0x7fff0000开始的64KB是隔离区。那么从0x7ffe0000开始的64KB呢?“Undocumented Windows 2000 Secrets”书中说这里用作KUSER_SHARED_DATA、即0xffdf0000处共享数据区的镜像,就是说从0x7ffe0000处可以读到本来应该在0xffdf0000处的数据。不过在ReactOS的代码中我们没有看到这样的实现。当然,要实现也很容易。
最后还要谈一下段寄存器FS在用户空间的作用。从前面RtlRosInitializeContext()的代码中可以看到,在为新建线程虚构的上下文中把段寄存器FS的映像Context->SegFs设置成了TEB_SELECTOR。这样,当新建线程进入用户空间运行时,段寄存器FS的内容就会“恢复”成TEB_SELECTOR。
而当这个线程因为系统调用、中断、异常等原因而进入内核时,则FS的内容又被替换成PCR_SELECTOR,这一点可以从例如_KiSystemService的代码中看出:
_KiSystemService:
/* Construct a trap frame on the stack. The following are already on the stack. */
// SS + 0x0
// ESP + 0x4
// EFLAGS + 0x8
// CS + 0xC
// EIP + 0x10
pushl $0 // + 0x14
pushl %ebp // + 0x18
pushl %ebx // + 0x1C
pushl %esi // + 0x20
pushl %edi // + 0x24
pushl %fs // + 0x28
/* Load PCR Selector into fs */
movw $PCR_SELECTOR, %bx
movw %bx, %fs
. . . . . .
所以,当CPU运行于用户空间时,段寄存器FS的内容是TEB_SELECTOR;而当CPU运行于系统空间时则是PCR_SELECTOR。这两个常数有什么不同呢?且看它们的定义:
#define NULL_SELECTOR (0x0)
#define KERNEL_CS (0x8)
#define KERNEL_DS (0x10)
#define USER_CS (0x18 + 0x3)
#define USER_DS (0x20 + 0x3)
/* Task State Segment */
#define TSS_SELECTOR (0x28)
/* Processor Control Region */
#define PCR_SELECTOR (0x30)
/* Thread Environment Block */
#define TEB_SELECTOR (0x38 + 0x3)
#define RESERVED_SELECTOR (0x40)
/* Local Descriptor Table */
#define LDT_SELECTOR (0x48)
#define TRAP_TSS_SELECTOR (0x50)
就是说PCR_SELECTOR是0x30,而TEB_SELECTOR是(0x38 + 0x3)、即0x3b。
段寄存器的可见部分是16位的,其最低两位为RPL、即运行级别;bit2是“段描述表”选择位,为0时选择“全局段描述表”GDT,为1时选择“局部段描述表”LDT;从bit3到bit15为下标,表示从GDT或LDT中选用哪一个表项。所以:
? l PCR_SELECTOR为0x30,表示下标为6,选择GDT,RPL为0。
? l TEB_SELECTOR为0x3b,表示下标为7,选择GDT,RPL为3。
可见,段寄存器FS指向何处并不仅仅取决于它本身,也取决于GDT中的表项。ReactOS代码中的数组KiBootGdt[]提供了一个原始的GDT映像:
USHORT KiBootGdt[11 * 4] =
{
0x0, 0x0, 0x0, 0x0, /* Null */
0xffff, 0x0000, 0x9a00, 0x00cf, /* Kernel CS */
0xffff, 0x0000, 0x9200, 0x00cf, /* Kernel DS */
0xffff, 0x0000, 0xfa00, 0x00cf, /* User CS */
0xffff, 0x0000, 0xf200, 0x00cf, /* User DS */
0x0, 0x0, 0x0, 0x0, /* TSS */
0x0fff, 0x0000, 0x9200, 0xff00, /* PCR */
0x0fff, 0x0000, 0xf200, 0x0000, /* TEB */
0x0, 0x0, 0x0, 0x0, /* Reserved */
0x0, 0x0, 0x0, 0x0, /* LDT */
0x0, 0x0, 0x0, 0x0 /* Trap TSS */
};
表中的每一行代表GDT的一个表项,每个表项的大小是4个16位短字,即64位,按从低位到高位的次序排列。“Intel系统结构软件开发手册”第3卷中有对GDT表项格式的说明,此处不拟详述,只是简要地作一些说明:
? l 下标为6的表项是PCR,数据0x0fff和0x0000表示段的长度为4KB,数据0x9200表示其优先级DPL为最高的0级,为数据描述项、类型为2、即可读可写。其余数据表明基地址为0xff000000。
? l 下标为7的表项是TEB,数据0x0fff和0xff00表示段的长度为4KB,数据0xf200表示其优先级DPL为最低的3级,为数据描述项、类型为2、即可读可写。其余数据表明基地址为0。
这个数据结构所提供的只是系统刚引导后的原始PCR表项和TEB表项,它们与基地址有关的位段随着系统的运行还要改变。
首先是下标为6的PCR表项。在单CPU的系统中这个段的基地址是0xff000000,以后读者将会看到,这实际上是个指针,指向内核中代表着CPU的“处理器控制区”,即KPCR数据结构。但是,在多CPU的系统中则每个CPU都有这么一个数据结构,因而它们的基地址要互相岔开。所以系统在初始化时要根据具体情况改变各CPU的GDT中的这个表项,这是在函数KiInitializeGdt()中完成的:
[KiSystemStartup() > KeApplicationProcessorInit() > KiInitializeGdt()]
KiInitializeGdt(PKPCR Pcr)
{
......
/*
* Set the base address of the PCR
*/
Base = (ULONG)Pcr;
Entry = PCR_SELECTOR / 2;
Gdt[Entry + 1] = (USHORT)(((ULONG)Base) & 0xffff);
Gdt[Entry + 2] = Gdt[Entry + 2] & ~(0xff);
Gdt[Entry + 2] = (USHORT)(Gdt[Entry + 2] | ((((ULONG)Base) & 0xff0000) >> 16));
Gdt[Entry + 3] = Gdt[Entry + 3] & ~(0xff00);
Gdt[Entry + 3] = (USHORT)(Gdt[Entry + 3] | ((((ULONG)Base) & 0xff000000) >> 16));
......
}
参数Pcr是个指针,具体的值取决于这是系统中第几个CPU的GDT,但总是在0xff000000以上不远的地方,因为每个CPU只占一个页面。这里的代码,如果读者有兴趣阅读的话,需要结合Intel手册中GDT表项的格式说明才能明白,这里就不多花时间了。
当然,这里修改的是数据结构的内容,只是GDT的映像,修改完了以后还要通过执行lgdt指令把这映像装入CPU。这样,当CPU运行于系统空间时,%%fs:0就总是指向0xff000000以上不远的某个地方,这就是所在CPU的KPCR数据结构。
这里还要说明一下,“Undocumented Windows 2000 Secrets”书中说当CPU运行于系统空间时%%fs:0指向0xffdff000,而不是我们从ReactOS代码中所见的0xff000000。不过有一点是共同的,那就是都说指向KPCR数据结构。其实地址的绝对值在这里并不那么重要,重要的是指向KPCR,找到KPCR数据结构才是关键。另一方面,之所以要为获取KPCR结构的地址而专门使用一个段寄存器,正是因为这个地址可能并不固定,否则就不合理了。
下标为7的TEB表项则又不同。每当内核在调度一个线程运行、并且要切换到这个目标线程时,就要根据这是在哪个CPU上运行而修改相应GDT中的这个表项,使其指向目标线程的TEB。下面是函数Ki386ContextSwitch()中的片断:
_Ki386ContextSwitch
......
/*
* Get the pointer to the new thread.
*/
movl 8(%ebp), %ebx
/* Set the base of the TEB selector to the base of the TEB for this thread. */
pushl %ebx
pushl KTHREAD_TEB(%ebx)
pushl $TEB_SELECTOR
call _KeSetBaseGdtSelector
addl $8, %esp
popl %ebx
......
调用函数KeSetBaseGdtSelector()是为了设置GDT表项中的基地址部分,这个函数有两个参数。第一个是目标表项的“选择码”,表示选择哪一个表项,这里压入堆栈的是TEB_SELECTOR,这就是当线程进入用户空间时寄存器FS应有的数值。第二个参数是新的基地址,这里来自KTHREAD_TEB(%ebx)。寄存器Ebx的值来自KeSetBaseGdtSelector()的第一个调用参数,这是个指向目标线程KTHREAD数据结构的指针。另一方面,常数KTHREAD_TEB定义为0x20,而KTHREAD数据结构中位移为0x20的字段就指向该线程的TEB。所以,KTHREAD_TEB(%ebx)就是目标线程的TEB起始地址。当然,修改后的映像也要通过lgdt指令装入CPU。
这样,在刚完成线程切换的时侯,GDT中下标为7的表项已经指向新线程的TEB,但是此时寄存器FS的内容还是PCR_SELECTOR、即下标为6。而当目标线程进入用户空间时,FS的内容就变成了TEB_SELECTOR,下标变成了7。于是,在用户空间,%%fs:0就总是指向当前线程的TEB了。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -