为了将映像被加载进内存运行,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+0x1c: ntdll32!LdrRelocateImageWithBias+0xad: ntdll32!__RtlUserThreadStart+0x25: |
现在注意上面红色粗体地方,这就是 0x77CB9D15 地址,这个地址是 helloworld.exe 调用链的最底端返回值。也就是说第1调用者是蓝色粗体标注的 call 指令,很显然是: call ntdll32!__RtlUserThreadStart (77cb9d1b),这就是 RtlUserThreadStart() 函数,主线程的启动代码
2.1 给 RtlUserThreadStart() 参数
调用 RtlUserThreadStart() 之前压入了两个值,这两个值就是 pvParam 和 pfnStartAddr 吗?
77cb9d0a ff750c push dword ptr [ebp+0Ch] |
我们先看看后面那个压入的值是不是 pfnStartAddr,我用 dd 命令来看一看 stack 情况:
0:000:x86> dd esp |
正如前面所说的:pfnStartAddr 是将会被 RtlUserThreadStart() 调用的主线程的入口代码,现在最后压入的值是 0x00b21221(上面红色粗体的值)
下面看一看 0x00b21221 是什么:
0:000:x86> uf 0xb21221 helleworld!ILT+540(_wWinMainCRTStartup): |
没错 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) |
这里的第一个环节都足够让我们去探索。