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

📄 漫谈兼容内核之十一:windows dll的装入和连接.txt

📁 漫谈系统内核内幕 收集得很辛苦 呵呵 大家快下在吧
💻 TXT
📖 第 1 页 / 共 5 页
字号:
漫谈兼容内核之十一:Windows DLL的装入和连接

[align=center]毛德操[/align]

    在PE映像的装入和启动过程中,DLL的装入和连接是一个重要的环节。读者在上一篇漫谈中看到,Windows的DLL装入(除ntdll.dll外)和连接是通过ntdll.dll中的一个函数LdrInitializeThunk()实现的。在Wine中,这个环节也是通过一个同名的函数实现的,只不过这个函数不在ntdll.dll中,而是wine-kthread里面的一个函数。在ReactOS中则同样也是LdrInitializeThunk(),同样也在ntdll.dll中。DLL的装入和连接当然离不开PE格式,但是本文的目的不在于完整、系统地介绍PE格式,而在于从LdrInitializeThunk()这个函数入手讲述DLL动态连接的过程,这个过程涉及PE格式中的什么成分,就讲解什么成分。这样,整个过程下来,PE格式中关键性的部件也就差不多都见到了。
    先对LdrInitializeThunk()这个函数名作些解释。“Ldr”显然是“Loader”的缩写。而“Thunk”意为“翻译”、“转换”、或者某种起着“桥梁”作用的东西。这个词在一般的字典中是查不到的,但却是个常见于微软的资料、文档中的术语。这个术语起源于编译技术,表示一小片旨在获取某个地址的代码,最初用于函数调用时“形参”和“实参”的结合。后来这个术语有了不少新的特殊含义和使用,但是DLL的动态连接与函数调用时的“形实结合”确实有着本质的相似。其实Unix/Linux也常常用到类似的技术,只是很少使用这个术语。例如,在Linux内核(i386版本)中,current是作为指针使用的,但实际上却是个宏操作:

[code]#define current get_current()[/code]

而get_current()是一个函数:

[code]static inline struct task_struct * get_current(void)
{
struct task_struct *current;
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
return current;
}[/code]

    这显然就是一个Thunk。还有,ELF映像(与.so模块)的连接所用的“过程连接表”PLT和“全局位移表”GOT本质上也是Thunk。所以,Thunk就是“动态连接”的同义词。

    下面我们就来看LdrInitializeThunk()的代码。由于Windows不公开它的代码,而Wine需要处理PE和ELF两种格式的动态连接库,相比之下略为复杂一些,所以我们先看ReactOS代码中的这个函数,其代码在reactos/ib/ntdll\ldr/entry.S中:

[code].globl _LdrInitializeThunk@16
_LdrInitializeThunk@16:
#if defined(_M_IX86)
nop   /* breakin overwrites this with "int 3" */
jmp ___true_LdrInitializeThunk@16
#elif defined(_M_ALPHA)
. . . . . . .[/code]

    由于是汇编代码,所以在函数名前面加上了‘_’,而后缀@16则表示调用参数一共是16字节、即4个参数(不过下面的代码并未用到这4个参数)。其实这个函数的本身就是个Thunk,因为它起的只是桥梁、跳板的作用,真正起作用的是__true_LdrInitializeThunk()。
    在进入这个函数之前,目标EXE映像已经被映射到当前进程的用户空间,系统DLL ntdll.dll的映像也已经被映射,但是并没有在EXE映像与ntdll.dll映像之间建立连接(实际上EXE映像未必就直接调用ntdll.dll中的函数)。LdrInitializeThunk()是ntdll.dll中不经连接就可进入的函数,实质上就是ntdll.dll的入口。除ntdll.dll以外,别的DLL都还没有被装入(映射)。此外,当前进程(除内核中的“进程控制块”EPROCESS等数据结构外)在用户空间已经有了一个“进程环境块”PEB,以及该进程的第一个“线程环境块”TEB。这就是进入__true_LdrInitializeThunk()前的“当前形势”。

[code]VOID STDCALL
__true_LdrInitializeThunk (ULONG Unknown1, ULONG Unknown2,
                         ULONG Unknown3, ULONG Unknown4)
{
   . . . . . .

   DPRINT("LdrInitializeThunk()\n");
   if (NtCurrentPeb()->Ldr == NULL || NtCurrentPeb()->Ldr->Initialized == FALSE)
   {
       Peb = (PPEB)(PEB_BASE);
       DPRINT("Peb %x\n", Peb);
       ImageBase = Peb->ImageBaseAddress;
       . . . . . .
       /* Initialize NLS data */
       RtlInitNlsTables (Peb->AnsiCodePageData, Peb->OemCodePageData,
                         Peb->UnicodeCaseTableData, &NlsTable);
       RtlResetRtlTranslations (&NlsTable);

       NTHeaders = (PIMAGE_NT_HEADERS)(ImageBase + PEDosHeader->e_lfanew);
       . . . . . .
       /* create process heap */
       RtlInitializeHeapManager();
       Peb->ProcessHeap = RtlCreateHeap(HEAP_GROWABLE, NULL,
                            NTHeaders->OptionalHeader.SizeOfHeapReserve,
                            NTHeaders->OptionalHeader.SizeOfHeapCommit,
                            NULL, NULL);
       . . . . . .

       /* create loader information */
       Peb->Ldr = (PPEB_LDR_DATA)RtlAllocateHeap (Peb->ProcessHeap,
                                                  0,
                                                  sizeof(PEB_LDR_DATA));
       . . . . . .
       Peb->Ldr->Length = sizeof(PEB_LDR_DATA);
       Peb->Ldr->Initialized = FALSE;
       Peb->Ldr->SsHandle = NULL;
       InitializeListHead(&Peb->Ldr->InLoadOrderModuleList);
       InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList);
       InitializeListHead(&Peb->Ldr->InInitializationOrderModuleList);

       . . . . . .
       /* add entry for ntdll */
       NtModule = (PLDR_MODULE)RtlAllocateHeap (Peb->ProcessHeap,
                                                        0,
                                                        sizeof(LDR_MODULE));
       . . . . . .
       InsertTailList(&Peb->Ldr->InLoadOrderModuleList,
                             &NtModule->InLoadOrderModuleList);
       InsertTailList(&Peb->Ldr->InInitializationOrderModuleList,
                             &NtModule->InInitializationOrderModuleList);
       . . . . . .
/* add entry for executable (becomes first list entry) */
       ExeModule = (PLDR_MODULE)RtlAllocateHeap (Peb->ProcessHeap,
                                                        0,
                                                        sizeof(LDR_MODULE));
       . . . . . .
       InsertHeadList(&Peb->Ldr->InLoadOrderModuleList,
                      &ExeModule->InLoadOrderModuleList);
       . . . . . .
       EntryPoint = LdrPEStartup((PVOID)ImageBase, NULL, NULL, NULL);
       . . . . . .
   }
   /* attach the thread */
   RtlEnterCriticalSection(NtCurrentPeb()->LoaderLock);
   LdrpAttachThread();
   RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
}[/code]

    这个函数的开头一段让我们看到了“进程环境块”PEB的一部分作用。比方说,PEB中的ImageBaseAddress字段是个指针,指向EXE映像在用户空间的起点,这是由内核为其设置好的;PEB中的AnsiCodePageData、OemCodePageData、和UnicodeCaseTableData等字段则与人机界面的语言本地化有关。还有,PEB中的ProcessHeap字段指向本进程用户空间可动态分配的内存区块“堆”、即Heap,这是一个可供动态分配的内存区块队列;而函数RtlAllocateHeap(Peb->ProcessHeap, …)则从这个堆分配空间。不过此前首先要通过RtlCreateHeap()创建一个这样的堆、及其第一个区块,其初始的大小来自映像头部的建议值,其中SizeOfHeapReserve是估计的最大值,SizeOfHeapCommit是初始值,这是在编译/连接时确定的。PEB中的另一个字段Ldr是个PEB_LDR_DATA结构指针,所指向的数据结构用来为本进程维持三个“模块”队列、即InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList。这里所谓“模块”就是PE格式的可执行映像,包括EXE映像和DLL映像。前两个队列都是模块队列,第三个是初始化队列。两个模块队列的不同之处在于排列的次序,一个是按装入的先后,一个是按装入的位置(实际上目前ReactOS的代码中并未使用这个队列)。每当为本进程装入一个模块、即.exe映像或DLL映像时,就要为其分配/创建一个LDR_MODULE数据结构,并将其挂入InLoadOrderModuleList。然后,完成对这个模块的动态连接以后,就把它挂入InInitializationOrderModuleList队列,以便依次调用它们的初始化函数。相应地,LDR_MODULE数据结构中有三个队列头,因而可以同时挂在三个队列中。
    这样,有了InLoadOrderModuleList队列以后,就可以避免重复装入(映射)同一个模块。要是模块A引入(import)模块B和C(即需要调用模块B和C中的函数),而B又要求引入C;那么在装入A、B、C三个模块、并连接A与B、A与C以后,当处理B的引入时,由于C已经在这个队列中,就不需要再次装入,而只要建立B与C之间的连接就可以了。
    到此刻为止,已经装入用户空间的模块只有两个,即目标EXE映像和ntdll.dll的映像,但是尚未连接。所以这里为这两个模块分配LDR_MODULE数据结构,并将它们挂入InLoadOrderModuleList队列。不过ntdll.dll是最底层的模块,它不再引入别的DLL,也无需连接,所以这里也将其挂入了初始化队列。
PEB是用户空间的程序中常常要用到的数据结构,这里分别使用了两种手段来获取当前PEB的地址。一是使用NtCurrentPeb(),这一般是一个宏定义:

[code]#define NtCurrentPeb() (NtCurrentTeb()->Peb)[/code]

    就是说,要获取PEB的地址,先获取TEB的地址,而TEB中有个指针指向PEB。那么怎样获取TEB的地址呢?ntdll.dll为此提供了一个库函数NtCurrentTeb()。
    如果所有的应用程序和别的DLL都用由ntdll.dll提供的NtCurrentTeb()来获取TEB的地址和PEB的地址,那么TEB和PEB实际所在的位置就无关紧要,只要ntdll.dll知道TEB在哪里就行了。可是事情却并不那么理想,应用程序和别的DLL事实上还可能用别的办法来获取TEB的地址,例如,有些应用程序(或DLL)可能自己搞一个NtCurrentTeb():

[code]/* on the x86, the TEB is contained in the FS segment */
static inline struct _TEB * NtCurrentTeb(void)
{
  struct _TEB * pTeb;

  __asm mov eax, fs:0x18
  __asm mov pTeb, eax

  return pTeb;
}[/code]

    “Secrets”一书中(234页和428-430页)对此有说明:当CPU运行于用户模式时,段寄存器fs:0(通过LDT中的相应的段描述符)指向当前线程的TEB。而TEB结构中的第一个成分是“线程信息块”TIB,TIB中位移为0x18处是个指针“Self”,指向这个数据结构本身的起点。所以fs:0x18就是TIB的起点,也就是TEB的起点。可是为什么不直接获取寄存器fs的内容呢?因为段寄存器fs是一个16位的寄存器,在“保护模式”中它的内容只是一个“段选择符”,只是被用来选取LDT中的一个“段描述符”,段描述符中所含的地址才是真正的地址。这个地址被装入CPU中fs的“隐藏”部分,而隐藏部分仅供CPU自己使用,对于程序员是不可见的(详见“Intel Architecture Software Developer’s Manual”第三卷)。可想而知,不同线程的fs所指向的段描述符有着不同的内容。
    这一来,TEB和PEB实际所在的位置就不是无关紧要、而是至关重要了。而且,不光是TEB和PEB所在的位置,连fs的内容、以及相应段描述符的内容也至关重要了;因为即使TEB和PEB确实在它们应该在的地方,可是fs和相应段描述符的内容不正确,那也还是有问题。段描述符的内容只能在内核中(系统模式下)加以设置,Linux为此提供了一个系统调用modify_ldt(),让应用程序可以在用户空间通过这个系统调用设置自己的LDT。Wine就是在wine-kthread或wine-pthread的thread_init()中用modify_ldt()建立起自己的TEB和PEB的。
    再看第二种手段,那就是上面程序中所用的“Peb = (PPEB)(PEB_BASE)”,这里PEB_BASE定义为0x7FFDF000。这也意味着PEB的位置是固定的,而且必须在0x7FFDF000这个地方。显然,要对Windows软件提供较好的兼容性,就必须遵守这些规矩。

    下面就是这里的主题、即对LdrPEStartup()的调用了,除ntdll.dll以外的所有DLL的装入和(向下)连接都是由它完成的。当CPU从LdrPEStartup()返回时,EXE对象需要直接或间接引入的所有DLL均已映射到用户空间并已完成连接,对EXE模块的“启动”、即初始化也已完成。再往下就是对LdrpAttachThread()的调用,目的是调用各个DLL的初始化过程,以及对TLS、即“线程本地存储(Thread Local Storage)”的初始化。如果具体的TLS有需要在初始化时加以回调的函数,则要加以调用。同一进程中的所有线程都共享同一用户空间,一般而言只有堆栈是只属于具体线程的,所以除局部变量以外就不再有线程自己的“私房”。但是局部变量是暂时的,一旦从其所在的函数返回就不复存在。可是,有时候又确实需要让每个线程都对于同一个全局变量有一份自己的拷贝,TLS就是为此而设的。

    注意在调用LdrPEStartup()时的参数ImageBase是目标EXE映像在用户空间的位置,而不是ntdll.dll在用户空间的位置。后者的位置并没有作为参数传下去,但是在模块队列中已经有了它的LDR_MODULE数据结构。

[code][__true_LdrInitializeThunk > LdrPEStartup()]

PEPFUNC LdrPEStartup (PVOID  ImageBase, HANDLE SectionHandle,
                      PLDR_MODULE* Module, PWSTR FullDosName)
{
   . . . . . .
   DosHeader = (PIMAGE_DOS_HEADER) ImageBase;
   NTHeaders = (PIMAGE_NT_HEADERS) (ImageBase + DosHeader->e_lfanew);

   /*
    * If the base address is different from the
    * one the DLL is actually loaded, perform any
    * relocation.
    */
   if (ImageBase != (PVOID) NTHeaders->OptionalHeader.ImageBase)
     {
       DPRINT("LDR: Performing relocations\n");
       Status = LdrPerformRelocations(NTHeaders, ImageBase);
       . . . . . .
     }

   if (Module != NULL)
     {
       *Module = LdrAddModuleEntry(ImageBase, NTHeaders, FullDosName);
       (*Module)->SectionHandle = SectionHandle;
     }
   else
     {
       Module = &tmpModule;
       Status = LdrFindEntryForAddress(ImageBase, Module);
       . . . . . .
     }

   . . . . . .

   /*
    * If the DLL's imports symbols from other
    * modules, fixup the imported calls entry points.
    */
   DPRINT("About to fixup imports\n");
   Status = LdrFixupImports(NULL, *Module);
   if (!NT_SUCCESS(Status))
     {
       DPRINT1("LdrFixupImports() failed for %wZ\n", &(*Module)->BaseDllName);
       return NULL;
     }
   DPRINT("Fixup done\n");

   . . . . . .
   Status = LdrpInitializeTlsForProccess();
   . . . . . .

   /*
    * Compute the DLL's entry point's address.
    */
   . . . . . .
   if (NTHeaders->OptionalHeader.AddressOfEntryPoint != 0)
     {
        EntryPoint = (PEPFUNC) (ImageBase
                           + NTHeaders->OptionalHeader.AddressOfEntryPoint);
     }
   DPRINT("LdrPEStartup() = %x\n",EntryPoint);
   return EntryPoint;

⌨️ 快捷键说明

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