【问题标题】:What Does Windows Do Before Main() is Called?在调用 Main() 之前 Windows 会做什么?
【发布时间】:2017-05-14 12:07:59
【问题描述】:

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

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

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

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

【问题讨论】:

  • 加载器对此负责。它是操作系统的一个组件,它的工作方式使得您不会在调用堆栈中看到太多关于它的证据。否则,在单个 Stack Overflow 答案中解释起来太复杂了。如果您想了解...嗯,Windows 的内部结构,可以考虑购买一本关于 Windows 内部结构的书。 :-)
  • 此外,Windows 会加载 EXE 所需的 DLL 并执行重定位。这些 DLL 中的一些代码可能会在 main() 之前执行。
  • 相关:Linux 上的进程启动:dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html。还有how to create a tiny but still runnable Linux executable。与 Windows 相比,Linux 在内核中完成了大部分进程启动工作,但动态二进制文件仍然在 C main() 函数之前在新执行的用户空间进程的上下文中运行动态链接器,然后运行 ​​CRT 启动代码。
  • 请注意,操作系统不会将命令行参数传递给可执行文件。 C 运行时库 (CRT) 调用 GetCommandLine 以在调用 main 之前获取参数。

标签: windows assembly x86 portable-executable


【解决方案1】:

如果你调用CreateProcess系统内部调用ZwCreateThread[Ex]来创建进程中的第一个线程

当你创建线程时——你(如果你直接调用ZwCreateThread)或系统为新线程初始化CONTEXT记录——这里Eip(i386)Rip(amd64)是线程的入口点。如果你这样做 - 你可以指定任何地址。但是当你打电话说Create[Remote]Thread[Ex] - 我怎么说 - 系统填充CONTEXT 并将自身例程设置为线程入口点。您的原始入口点保存在Eax(i386)Rcx(amd64) 寄存器中。

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

早期这是来自kernel32.dllBaseThreadStartThunkBaseProcessStartThunk(以防CreateProcess 调用)。

但现在系统从 ntdll.dll 指定 RtlUserThreadStartRtlUserThreadStart 通常从kernel32.dll 调用BaseThreadInitThunk(除了本机(启动执行)应用程序,如smss.exechkdsk.exe,它们在自身地址空间中根本没有kernel32.dll)。 BaseThreadInitThunk 已经调用了您的原始线程入口点,并且在(如果)它返回之后 - 调用了 RtlExitUserThread

这个公共线程启动包装器的主要目标 - 设置顶级SEH 过滤器。仅仅因为这个我们可以调用SetUnhandledExceptionFilter函数。如果线程直接从您的入口点开始,没有包装器 - Top level Exception Filter 的功能将不可用。

但无论线程入口点是什么——用户空间中的线程——绝不从这一点开始执行!

当用户模式线程开始执行的早期-系统将APC插入到线程中,LdrInitializeThunk作为Apc例程-这是通过将线程CONTEXT复制(保存)到用户堆栈然后调用KiUserApcDispatcher来完成的LdrInitializeThunk。当LdrInitializeThunk 完成时——我们返回到KiUserApcDispatcher,它调用了NtContinue,并保存了线程CONTEXT——只有在这个已经线程入口点开始执行之后。

但是现在系统在这个过程中做了一些优化——它将线程CONTEXT复制(保存)到用户堆栈并直接调用LdrInitializeThunk。在这个函数的末尾NtContinue 被调用 - 并且正在执行线程入口点。

所以 EVERY 线程从 LdrInitializeThunk 开始在用户模式下执行。 (这个名字完全正确的函数存在并在从 nt4 到 win10 的所有 windows 版本中调用

这个功能是做什么的?这是为了什么?您可能正在收听DLL_THREAD_ATTACH 通知?当进程中的新线程开始执行时(特殊系统工作线程除外,如LdrpWorkCallback)-他通过加载的DLL列表,并使用DLL_THREAD_ATTACH通知调用DLL入口点(当然如果DLL有入口点和@987654337 @ 未调用此 DLL)。但是这是如何实现的?感谢LdrInitializeThunk 调用LdrpInitialize -> LdrpInitializeThread -> LdrpCallInitRoutine(用于 DLL EP)

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

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

【讨论】:

  • 可能是个愚蠢的问题:这一切都是在编译器包含在典型可执行文件中的 CRT 代码运行之前吗?在 Linux 上,用户空间进程入口点通常称为_start,由 CRT 提供。在用户空间进程的上下文中,只有动态链接器代码在此之前运行。我很惊讶没有人提到 CRT 代码作为 C main 函数之前运行的一部分,因为这就是问题标题所要求的(不仅仅是到达 CRT 入口点)。或者这些东西是正常 MSVCRT 的一部分吗?
  • @PeterCordes - 我写的所有内容都与c/c++ CRT 无关。这适用于 Windows 中的所有 exe。然而,并非所有写在c/c++ 甚至c/c++ 上的代码都不能使用CRT。如果您使用CRT,您的进程“入口点”是 wWinMainCRTStartup 或wmainMainCRTStartup。所以代码流将是LdrInitializeThink -> NtContinue -> BaseThreadInitThunk -> wWinMainCRTStartup -> WinMain。所有这些都只是与 Windows 相关的。关于 Linux - 我所知道的就是操作系统。没有了
  • @PeterCordes - 所以我在下面/在调用用户定义的进程或线程的 EP 之前描述进程。说我不使用CRT - 结果这里user defined EP 是我的函数(它是任何名称) - 它直接从BaseThreadInitThunk 调用。如果你使用CRT - 你的user defined EPCRT 代码中是[w]WinMainCRTStartup[w]mainMainCRTStartup,它已经最终调用了你的[w]WinMain[w]main(你必须使用这个名字,当我有空时选择)
  • @PeterCordes - “这一切都是在编译器包含在典型可执行文件中的 CRT 代码运行之前吗?” - 是的,当然之前
  • 谢谢,这回答了我的问题。我只提到 Linux 是因为 知道,所以我可以问一下 Windows 有何不同。我很困惑,因为问题标题说“在 main() 之前”,但是没有人提到 CRT 代码是 OP 在堆栈中找到的一些东西的来源。
猜你喜欢
  • 2020-08-09
  • 1970-01-01
  • 2011-06-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-27
  • 2023-03-21
相关资源
最近更新 更多