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

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

📁 漫谈系统内核内幕 收集得很辛苦 呵呵 大家快下在吧
💻 TXT
📖 第 1 页 / 共 4 页
字号:
   }
  
   /* 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 + -