【问题标题】:exit fails to set error codeexit 设置错误码失败
【发布时间】:2015-02-01 17:42:22
【问题描述】:

我有一个无法设置退出代码的 C++ Windows 程序。该程序非常复杂,我目前无法使用简单的测试用例来重现它。我知道程序调用exit(1),因为我在那行有一个断点。在我跳过它之后,调试器 (VS2010) 立即打印 The program program.exe has exited with code 0 (0x0). 当我从 shell 运行它时,%ERRORLEVEL% 也设置为 0。

我使用 subsystem:console 和普通的旧 main(没有 WinMain)。

这只发生在 Windows Server 2008 R2 上,不会发生在我的 Windows 8.1 笔记本电脑上。我在两者上运行相同的可执行文件。

我尝试使用exit_exitExitProcessreturn(有问题的电话在main),但这些似乎都没有任何效果。我也试过返回其他代码,也没有结果。

有一个similar question,但我无法重现其中描述的结果。我的程序确实使用线程。

我该如何调试这个问题?我很困惑。

【问题讨论】:

  • UB 某处?堆栈被覆盖?没有其他看起来奇怪或不合适的地方吗?
  • @JoachimPileborg 很难说。某处可能有UB,但我怎么能找到它?堆栈似乎还可以。我知道 UB 就是 UB,但在这种情况下,我们甚至不必受 C++ 规则的约束。 WINAPI 函数如ExitProcess 无论如何都应该工作(我们可以从汇编语言程序中调用它们)。我绝对确定1 被传递给exit,因为我可以介入并在调试器中查看标准库代码,并使用调试器实际观察ExitProcess(1) 是如何被调用的。即使在机器指令级别。我无法进一步调试ExitProcess

标签: c++ windows windows-server-2008 exit-code


【解决方案1】:

我尝试过使用exit、_exit、ExitProcess和return

您已经排除了所有合理的解释,尤其是使用 ExitProcess()。只有一种可能,你需要尝试 TerminateProcess()。如果那个仍然没有设置退出代码,那么你需要把那台机器推出第四层窗口。

但期望它现在可以工作。 ExitProcess() 和 TerminateProcess() 之间的区别在于前者确保所有 DLL 都由终止通知。他们的 DllMain() 函数被 fdwReason = DLL_PROCESS_DETACH 调用。这让 DLL 有机会做一些棘手的事情,比如调用 Exit/TerminateProcess() 本身,从而搞砸退出代码。

如果您没有所有源代码,则可能很难找到这样的 DLL。也可以是注射的,这些天完全太多了。最好的办法是在系统调用上设置一个断点,这样你就可以在动作中捕捉它,你可能无论如何都想这样做。

进入 main() 后,使用 Debug > New Breakpoint > Break at Function 并输入 {,,ntdll.dll}_NtTerminateProcess@8。按 F5,调试器现在在程序终止之前停止。查看调用堆栈以找到作恶者。

【讨论】:

  • 我尝试调用 TerminateProcess 并且它似乎有效。但我无法为其设置断点。您确定{,,ntdll.dll}_NtTerminateProcess@8 语法正确吗? IDE 警告上下文无效,没有设置断点。
  • 很难猜到为什么它对你的不起作用,我对你的 VS 或 Windows 版本一无所知。另一种方法是双击调用堆栈中的 ntdll.dll 函数,忽略关于没有源的警告,然后使用 _NtTerminateProcess@8 作为函数名。最后一点是在一个小测试程序中调用 TerminateProcess() 并单步执行机器代码,以便您可以在 Windows 版本上看到 ntdll.dll 函数名称。 Anyhoo,你学到了很多,现在你知道这是一个 DLL 搞砸了。
  • @n.m.为什么不为一些可疑的 DLL 调用 FreeLibrary,看看是哪一个导致您的进程退出?在每次调用之前和之后放置跟踪行,以便您知道 FreeLibrary 是否返回。 VS调试器也没有在退出时编写卸载DLL的序列吗?也许你可以检查一下。
  • 谢谢,确实是这个问题。有一个恶意 DLL 在 DLL_PROCESS_DETACH 上调用了 exit()。用一点汇编调试发现的。
【解决方案2】:

多线程程序中涉及 exit()、_exit()、ExitProcess() 和其他程序的奇怪症状 - 特别是如果主机之间的症状不同 - 有一种变量被不同线程修改或访问的气味,而没有同步。

查看您链接到的另一个线程,您似乎正在使用 volatile 变量在线程之间进行通信,但没有使用任何形式的同步(例如,访问该变量值的代码和修改该值的代码需要通过临界区、互斥体或类似结构进行合作)。

这一点间接证据使气味更加强烈。

我怀疑的基本问题是,将变量声明为 volatile 既没有必要也不足以确保变量始终具有对您的程序有意义的值。特别是,当修改仅部分完成时,阻止正在修改变量的线程被抢占是不够的,并且另一个线程尝试访问或修改受影响的变量是不够的。

如果您查看 Herb Sutter 的一些文章(尤其是他的“本周大师”系列中与线程同步有关的文章),您会发现为什么会这样的详细解释。其他作者也描述了这样的事情,但 Sutter 的文章是我随便回忆的。

解决方案是引入一些同步方法,让程序中的每个线程在访问或修改它们之间共享的变量之前都虔诚地使用它。这避免了会导致您描述的症状的各种问题(竞争条件、操作在中途被抢占)。

此类问题很少通过调试器单步执行来发现。原因是这些症状是一种紧急特性。在不同的执行线程中,几个不太可能且通常独立的事件必须一起发生。调试器通常会更改程序中事件的时间,而时间是出现症状的关键考虑因素。

选项包括使关键变量原子化(因此特定操作不能被抢占)、临界区(线程在程序内显式协作)或互斥锁(根据定义,允许不同程序中的线程在访问之前显式协作)共享内存)。

是的,这会在您的程序中引入一个瓶颈——每个线程必须会合并可能相互等待。这会影响程序的吞吐量。有些人提倡使用 volatile 变量来避免这种担忧。通常情况下,结果是在长时间运行的程序中出现间歇性症状,就像您在这个问题和您链接到的“类似问题”中描述的那样。

无论您使用标准同步方式(例如在 C++11 中引入)还是特定于 Windows 的方式(WIN API 函数)都没有关系。重要的是您使用了一种经过深思熟虑的同步方法,而不是仅仅使变量变得易变。不同的同步选项有不同的权衡,因此您需要根据您的程序需求做出决定。

另一个考虑是向所有线程发出信号,以便它们干净地关闭,等到它们全部关闭,捕获它们的退出代码,然后退出程序。在运行 main() 的线程中执行此操作通常不太容易出错 - 它最终启动进程,因此更有可能访问正确清理所需的信息。如果另一个线程决定程序需要退出,那么最好将需要返回给 main() 来执行它。

【讨论】:

  • 我不太明白为什么会这样。授予非同步访问权限,但调用 ExitProcess C++ 规则时不再重要,它是一个 WINAPI 函数,唯一重要的是传递给它的参数。这是一个常数。
  • 我描述的是线程之间的同步问题,而不是“C++ 规则”。由于与另一个线程共享或使用的数据的不适当交互,它所需要的只是一个线程做一些意想不到的事情。
  • 没错,但ExitProcess(1) 不应该与其他线程共享任何数据。如果同步不正确,共享数据可能处于无效状态,但ExitProcess(1) 如何可能取决于任何数据的状态,无论是否共享?
  • 传递给 ExitProcess() 的值存储在某处。可能会覆盖文字值或影响传递的值的存储位置。缺乏同步是 C++ 标准所称的未定义行为的多线程贡献者。使用 win API 函数不会对此产生魔法免疫。以硬方式关闭线程/进程(如 ExitProcess() 或 TerminateThread())也可以减少干净程序关闭的机会。我不知道这些是否是您代码中唯一的问题,但它们做出贡献的机会不为零。
  • 理论上可以覆盖文字整数值,但我向您保证,这不会发生在我的程序中(我怀疑它根本不会发生在 X86 上)。文字被编码在写保护存储器中的指令中。无法覆盖它。虽然我几乎可以肯定该错误与多线程有关,但我非常怀疑确切的机制是否如您所描述的那样。
猜你喜欢
  • 2019-07-08
  • 1970-01-01
  • 1970-01-01
  • 2022-06-11
  • 1970-01-01
  • 2017-02-23
  • 2018-03-27
  • 2013-07-12
  • 1970-01-01
相关资源
最近更新 更多