PE 映像运行机理(2)--- 入口

为了将映像被加载进内存运行,windows 为每个映像建立一个进程内核对象,为进程创建虚拟地址空间,加载映像所有的数据到进程空间,然而系统还要为进程建立一个主线程。

主线程是执行线程,它开始运行会调用 C/C++ 的启动例程,这个 C/C++ 启动例程是由链接器添加到我们的代码里的,C/C++ 启动例程最终会调用我们的程序入口 WinMain/wWinMain 或main/wmain 函数

 

1. 主线程的启动例程

这是 Jeffrey Richter 在 《Windows via C/C++》 一书里说的:

Once the kernel object has been created, the system allocates memory, which is used for the thread's stack. This memory is allocated from the process' address space because threads don't have an address space of their own. The system then writes two values to the upper end of the new thread's stack. (Thread stacks always build from high memory addresses to low memory addresses.) The first value written to the stack is the value of the pvParam parameter that you passed to CreateThread. Immediately below it is the pfnStartAddr value that you also passed to CreateThread.

Each thread has its own set of CPU registers, called the thread's context. The context reflects the state of the thread's CPU registers when the thread last executed. The set of CPU registers for the thread is saved in a CONTEXT structure (defined in the WinNT.h header file). The CONTEXT structure is itself contained in the thread's kernel object.

The instruction pointer and stack pointer registers are the two most important registers in the thread's context. Remember that threads always run in the context of a process. So both these addresses identify memory in the owning process' address space. When the thread's kernel object is initialized, the CONTEXT structure's stack pointer register is set to the address of where pfnStartAddr was placed on the thread's stack. The instruction pointer register is set to the address of an undocumented function called RtlUserThreadStart, which is exported by the NTDLL.dll module. Figure 6-1 shows all of this.

Here is what RtlUserThreadStart basically does:

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
   __try {
      ExitThread((pfnStartAddr)(pvParam));
   }
   __except(UnhandledExceptionFilter(GetExceptionInformation())) {
      ExitProcess(GetExceptionCode());
   }
   // NOTE: We never get here.
}
      

其实我手上有中文版书籍,贴出来是让大家看原味的话 :)

他是说:一旦创建了线程内核对象后,windows 会在 线程的 stack 的顶端写入两个值:

  • 传给 CreateThread 函数的 pvParam 参数
  • 传给 CreateThread 函数的 pfnStartAddr 值

先压入 pvParam 值,然后压入 pfnStartAddr 值,这两个值都是传给 CreateThread() 时的参数。

每个线程都有自已的 CONTEXT(上下文环境),它是一组 CPU 寄存器值,那么压入这两个值后,CONTEXT 里的 esp 指针为压入后的 stack 顶,也就是说 esp 里的值是 pfnStartAddr 值,eip 指令指针指向 RtlUserThreadStart() 函数。

这样控制权转到 ntdll 里的 RtlUserThreadStart() 函数,然后再调用 pfnStartAddr 指向的线程函数。

 

2. RtlUserThreadStart() 的工作机理

下面来看一看是不是 Jeffrey Ricther 所说的那样,在我的 x64 win7 上的例子。

当用 Visual Studio 2010 调试运行时,在 helloworld.exe 的主线程的调用栈中看到,在调用链的最底端值是:0x77CB9D15,大家不妨实验一下。

看一看这个 0x77CB9D15 是何方神圣,在哪里?这里要借助 windbg 来调试,其实我不会用 windbg,但是没办法不用了 :(

我觉得 windbg 挺难用,一直没用心去学,所以一直没学会用,只能看着帮助来玩玩 :(

我使用 uf 命令来反汇编 RtlUserThreadStart() 的代码:

0:000:x86> uf ntdll32!_RtlUserThreadStart
ntdll32!_RtlUserThreadStart:
77cb9cfa 8bff            mov     edi,edi
77cb9cfc 55              push    ebp
77cb9cfd 8bec            mov     ebp,esp
77cb9cff 51              push    ecx
77cb9d00 51              push    ecx
77cb9d01 8d45f8          lea     eax,[ebp-8]
77cb9d04 50              push    eax
77cb9d05 e8d5ffffff      call    ntdll32!RtlInitializeExceptionChain (77cb9cdf)
77cb9d0a ff750c          push    dword ptr [ebp+0Ch]
77cb9d0d ff7508          push    dword ptr [ebp+8]
77cb9d10 e806000000     call    ntdll32!__RtlUserThreadStart (77cb9d1b)
77cb9d15
 cc              int     3
77cb9d16 90              nop
77cb9d17 90              nop
77cb9d18 90              nop
77cb9d19 90              nop
77cb9d1a 90              nop
77cb9d1b 6a14            push    14h
77cb9d1d 6890c3ca77      push    offset ntdll32! ?? ::FNODOBFM::`string'+0xb5e (77cac390)
77cb9d22 e8fd3fffff      call    ntdll32!_SEH_prolog4 (77cadd24)
77cb9d27 8365fc00        and     dword ptr [ebp-4],0
77cb9d2b a12442d877      mov     eax,dword ptr [ntdll32!Kernel32ThreadInitThunkFunction (77d84224)]
77cb9d30 ff750c          push    dword ptr [ebp+0Ch]
77cb9d33 85c0            test    eax,eax
77cb9d35 0f84d2d20400    je      ntdll32!__RtlUserThreadStart+0x25 (77d0700d)

ntdll32!__RtlUserThreadStart+0x1c:
77cb9d3b 8b5508          mov     edx,dword ptr [ebp+8]
77cb9d3e 33c9            xor     ecx,ecx
77cb9d40 ffd0            call    eax
77cb9d42 c745fcfeffffff  mov     dword ptr [ebp-4],0FFFFFFFEh
77cb9d49 e81b40ffff      call    ntdll32!_SEH_epilog4 (77cadd69)
77cb9d4e c20800          ret     8

ntdll32!LdrRelocateImageWithBias+0xad:
77cf57cf 5f              pop     edi
77cf57d0 5e              pop     esi
77cf57d1 c9              leave
77cf57d2 c21c00          ret     1Ch

ntdll32!__RtlUserThreadStart+0x25:
77d0700d ff5508          call    dword ptr [ebp+8]
77d07010 50              push    eax
77d07011 e84ab2fcff      call    ntdll32!RtlExitUserThread (77cd2260)
77d07016 cc              int     3
77d07017 8b4520          mov     eax,dword ptr [ebp+20h]
77d0701a e9b0e7feff      jmp     ntdll32!LdrRelocateImageWithBias+0xad (77cf57cf)

现在注意上面红色粗体地方,这就是 0x77CB9D15 地址,这个地址是 helloworld.exe 调用链的最底端返回值。也就是说第1调用者是蓝色粗体标注的 call 指令,很显然是: call    ntdll32!__RtlUserThreadStart (77cb9d1b),这就是 RtlUserThreadStart() 函数,主线程的启动代码


2.1 给 RtlUserThreadStart() 参数

调用 RtlUserThreadStart() 之前压入了两个值,这两个值就是 pvParam 和 pfnStartAddr 吗?

77cb9d0a ff750c push dword ptr [ebp+0Ch]
77cb9d0d ff7508 push dword ptr [ebp+8]

我们先看看后面那个压入的值是不是 pfnStartAddr,我用 dd 命令来看一看 stack 情况

0:000:x86> dd esp
002cfd64  00b21221 7efde000 00000000 00000000
002cfd74  00000000 00000000 00b21221 7efde000     <------ 系统硬写入值
002cfd84  00000000 00000000 00000000 00000000
002cfd94  00000000 00000000 00000000 00000000
002cfda4  00000000 00000000 00000000 00000000
002cfdb4  00000000 00000000 00000000 00000000
002cfdc4  00000000 00000000 00000000 00000000
002cfdd4  00000000 00000000 00000000 00000000

正如前面所说的:pfnStartAddr 是将会被 RtlUserThreadStart() 调用的主线程的入口代码,现在最后压入的值是 0x00b21221(上面红色粗体的值)

下面看一看 0x00b21221 是什么:

0:000:x86> uf 0xb21221
helleworld!wWinMainCRTStartup [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 361]:
  361 00b22090 8bff            mov     edi,edi
  361 00b22092 55              push    ebp
  361 00b22093 8bec            mov     ebp,esp
  368 00b22095 e8c5efffff      call    helleworld!ILT+90(___security_init_cookie) (00b2105f)
  370 00b2209a e811000000      call    helleworld!__tmainCRTStartup (00b220b0)
  371 00b2209f 5d              pop     ebp
  371 00b220a0 c3              ret

helleworld!ILT+540(_wWinMainCRTStartup):
00b21221 e96a0e0000     jmp     helleworld!wWinMainCRTStartup (00b22090)

没错 0x00b21221 处就是 helloworld.exe 的 C/C++ 启动例程入口,在上一篇文章里已经介绍,这里是一个跳转表。这里还看到接下来会调用 _tmainCRTStartup() 进程实际的启动例程

因此,后面压入的值就是 pfnStartAddr 参数,那么 0x7efde000 是不是 pvParam 参数呀?

实际上 0x77cb9cfa 才是 Jeffrey Ricther 所说的 ntdll32!_RtlUserThreadStart() 注意和 ntdll32!__RtlUserThreadStart() 的区别:前者是 1 个下划线,后者是 2 个下划线,前者调用后者

根据 Jeffrey Ricther 所说,RtlUserThreadStart() 是系统调用,没有 caller,系统会往线程的 stack 上硬写入两个值,因此不存在调用者的返回地址,这从上面的 stack 显示可以看出,并没返回地址被保存在 stack 里。

0x7efde000 是不是 pvParam 参数比较难考证,pvParam 是传给 CreateThread() 的参数,而后又传给 RtlUserThreadStart(),它只是一个值,不作别的事情。

现在,我们知道它们之间的调用关系,也就是 helloword.exe 的入口:

ntdll32!_RtlUserThreadStart --> ntdll32!__RtlUserThreadStart --> helleworld!wWinMainCRTStartup --> helleworld!__tmainCRTStartup --> helleworld!wWinMain(HINSTANCE__ *, HINSTANCE__ *, wchar_t *, int)

这里的第一个环节都足够让我们去探索。


版权 mik 所有

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值