Windows 在调用 Main() 之前做了什么?

What Does Windows Do Before Main() is Called?

Windows 必须做一些事情来解析 PE header,将可执行文件加载到内存中,并将命令行参数传递给 main()

使用 OllyDbg 我已将调试器设置为在 main() 上中断,以便我可以查看调用堆栈:

好像缺少符号所以我们无法获得函数名,只能看到它的内存地址。但是我们可以看到main的调用者是kernel32.767262C4,也就是ntdll.77A90FD9的被调用者。在堆栈底部,我们看到 RETURN 到 ntdll.77A90FA4,我认为这是第一个被调用到 运行 可执行文件的函数。传递给该函数的值得注意的参数似乎是 Windows' 结构化异常处理程序地址和可执行文件的入口点。

那么这些函数究竟是如何最终将程序加载到内存中并准备好执行入口点的呢?调试器显示的是main()之前OS执行的整个过程?

如果您调用 CreateProcess system internally call ZwCreateThread[Ex] 来创建进程中的第一个线程

当你创建线程时 - 你(如果你直接调用 ZwCreateThread) or system initialize the CONTEXT record for new thread - here Eip(i386) or Rip(amd64) the entry point of thread. if you do this - you can specify any address. but when you call say Create[Remote]Thread[Ex] - how i say - the system fill CONTEXT 并将自身例程设置为线程入口点。你的原始入口点保存在 Eax(i386)Rcx(amd64) 寄存器中.

此例程的名称取决于 Windows 版本。

早期这是 BaseThreadStartThunkBaseProcessStartThunk(如果从 CreateProcess 调用)来自 kernel32.dll

但现在系统从 ntdll.dll 指定 RtlUserThreadStartRtlUserThreadStart 通常从 kernel32.dll 调用 BaseThreadInitThunk(本机(引导执行)应用程序除外,例如 smss.exechkdsk.exe 自身没有 kernel32.dll地址 space )。 BaseThreadInitThunk 已经调用了您的原始线程入口点,并且在 (if) 它之后调用了 return - RtlExitUserThread

这个公共线程启动包装器的主要目标 - 设置顶层 SEH filter. only because this we can call SetUnhandledExceptionFilter function. if thread start direct from your entry point, without wrapper - the functional of Top level Exception Filter 变得不可用。

但是无论线程入口点是什么 - 用户 space 中的线程 - 永远不会 从该点开始执行!

早于用户模式线程开始执行时 - 系统将 APC 插入到线程 LdrInitializeThunk 作为 Apc-routine - 这是通过复制(保存)线程 CONTEXT 到用户堆栈,然后调用 KiUserApcDispatcher,后者调用 LdrInitializeThunk。当 LdrInitializeThunk 完成时 - 我们 return 到 KiUserApcDispatcher 调用 NtContinue 并保存线程 CONTEXT - 只有在这个线程入口点开始执行之后。

但现在系统在此过程中做了一些优化 - 它将线程 CONTEXT 复制(保存)到用户堆栈并直接调用 LdrInitializeThunk。在此函数结束时 NtContinue 调用 - 线程入口点正在执行。

所以每个 线程从LdrInitializeThunk 开始以用户模式执行。 (这个同名的函数在从 nt4 到 win10 的所有 windows 版本中都存在并被调用)

这个功能是做什么的?这是什么?您可能会听到 DLL_THREAD_ATTACH notification ? when new thread in process begin executed (with exception for special system worked threads, like LdrpWorkCallback)- he walk by loaded DLL list, and call DLLs entry points with DLL_THREAD_ATTACH notification (of course if DLL have entry point and DisableThreadLibraryCalls 未为此 DLL 调用)。但这是如何实施的?感谢 LdrInitializeThunk 调用 LdrpInitialize -> LdrpInitializeThread -> LdrpCallInitRoutine(对于 DLL EP)

当进程中的第一个线程启动时——这是一种特殊情况。需要为进程初始化做很多额外的工作。此时只有两个模块正在加载 - EXEntdll.dllLdrInitializeThunk 为此工作致电 LdrpInitializeProcess。如果非常简短:

  1. 初始化不同的进程结构
  2. 静态加载EXE的所有DLL(及其依赖) 链接 - 但不称他们为 EP!
  3. 调用LdrpDoDebuggerBreak - 这个函数看起来 - 是调试器 附加到进程,如果是 - int 3 调用 - 所以调试器 接收异常消息 - STATUS_BREAKPOINT - 大多数调试器可以 begin UI 调试只从这一点开始。然而存在 从 LdrInitializeThunk 作为调试过程的调试器 - 我所有来自这种调试器的屏幕截图
  4. 重要的一点 - 直到在进程中执行的代码仅来自 ntdll.dll(可能来自 kernel32.dll)- 来自另一个的代码 DLL,任何尚未在进程中执行的 third-party 代码。
  5. 可选加载的 shim dll 进行处理 - 已初始化 Shim 引擎。但 这是可选的
  6. 遍历已加载的 DLL 列表并调用其 EPs DLL_PROCESS_DETACH
  7. TLS 调用的初始化和 TLS 回调(如果存在)

  8. ZwTestAlert 被调用 - 这个调用检查线程中是否存在 APC 队列,并执行它。这一点存在于从NT4到所有版本 win 10. 这让我们以挂起状态创建进程 然后将 APC 调用 ( QueueUserAPC ) 插入到它的线程中 (PROCESS_INFORMATION.hThread) - 因此这个调用将是 进程将完全初始化后执行,所有 DLL_PROCESS_DETACH 已调用,但在 EXE 入口点之前。在上下文中 第一个进程线程。

  9. 并最终调用了 NtContinue - 这恢复了保存的线程上下文 我们最终跳转到线程 EP

另请阅读 Flow of CreateProcess