Debug-compiled executable:为什么不在无效写入NULL时正常中止?

时间:2009-02-15 01:15:56

标签: c++ error-handling debugging segmentation-fault

我对C / C ++不了解的是:

是的,每个人都使用它来获得超快速的可执行文件,因此它们会在启用优化的情况下进行编译。

但是对于打开调试信息的编译,我们并不关心速度。那么为什么不在该编译模式中包含更多信息,例如在它们发生之前检测一些段错误?实际上,在每次访问指针assert(ptr != NULL)之前插入ptr。为什么编译器不能这样做?同样,默认情况下应该关闭,但我认为应该有这种可能性。

编辑:有些人说我建议的检测没有意义或没有做segmentation fault的报告不会做的任何事情。但我想到的只是一个更优雅和信息丰富的中止,它打印出违规代码的文件名和行号,就像assert()一样。

9 个答案:

答案 0 :(得分:9)

您的建议存在一些主要问题:

您希望编译器检测哪些条件?在Linux / x86上,未对齐访问可能导致SIGBUS并且堆栈溢出可能导致SIGSEGV,但在这两种情况下,技术上都可以编写应用程序来检测这些条件并“正常”失败。可以检测到NULL指针检查,但最隐蔽的错误是由于无效的指针访问,而不是NULL指针。

C和C ++编程语言提供了足够的灵活性,因此如果给定的随机地址是任意类型的有效指针,运行时无法100%成功确定。

在检测到这种情况时,您希望运行时环境做什么?它无法纠正行为(除非你相信魔法)。它只能继续执行或退出。但是等一下......这就是信号传递时已经发生的事情!程序退出,生成核心转储,应用程序开发人员可以使用该核心转储来确定程序崩溃时的状态。

您所倡导的实际上听起来像是想要在调试器(gdb)中运行应用程序或通过某种形式的虚拟化(valgrind)。这已经成为可能,但默认情况下这样做是没有意义的,因为它对非开发人员没有任何好处。

更新以回复评论:

没有理由修改调试版本的编译过程。如果您需要应用程序的“温和”调试版本,则应在调试器内部运行它。将您的可执行文件包装在一个透明地为您执行此操作的脚本中非常容易。

答案 1 :(得分:8)

在这种情况下,该计划应该做些什么?如果它通知用户一个错误,那就是segfault所做的。

如果它应该继续下去并避免错误,它怎么知道该怎么办?

更不用说如果它以某种方式神奇地知道如何继续正确,那么你的发布版本中就有一个错误(调试版本旨在帮助你识别和修复错误 - 而不是隐藏错误)。


回应问题中添加的其他信息(我想我误解了你的意图):

  

我想到的只是一个更优雅和信息丰富的中止,它打印出违规代码的文件名和行号,就像assert()一样。

这是编译器可以做的事情 - 正如你所说,编译器基本上会在指针被解除引用的任何地方自动插入assert()。这可能会大大增加调试版本的大小,但对于许多(或大多数)目的而言,它可能仍然可以接受。我认为这对于编译器来说是一个合理的选择。

我不确定编译器供应商会说什么......也许在Microsoft's Connect site for the VC++ product上发布请求,看看他们说了什么。

答案 2 :(得分:1)

我同意迈克尔伯尔的观点,认为这并没有真正做到或有所帮助。

此外,这仍然不适用于悬空指针,这些指针往往比空指针更加隐蔽和难以追踪。

至少对于空指针,它很简单,以确保它们在你反驳它们之前是有效的。

答案 3 :(得分:1)

我认为原始海报希望应用程序停止在调试器中。您可以访问所有堆栈变量和堆栈,这样您就有机会找出程序处于此状态的原因。

如果您使用C / C ++进行开发,调试内存管理器可以为您节省大量时间。缓冲区溢出,访问已删除的内存,内存泄漏等很容易找到并修复。市场上有几种,或者您可以花2到3天时间自己编写并获得90%的所需功能。如果你在没有它们的情况下编写应用程序,那么你的工作就会比你需要的更困难。

答案 4 :(得分:1)

在解除引用指针之前,简单的assert(ptr != NULL)将无法工作还有另外一个原因:并非每个无效指针(即使那些以NULL开头的生命指针)实际上都等于0。

首先考虑一下你有一个包含多个成员的结构的情况:

struct mystruct {
    int first;
    int second;
    int third;
    int fourth;
};

如果指针ptrmystruct并且您尝试访问ptr->second,编译器将生成代码广告4(假设32位整数)到{{ 1}}并访问该内存位置。如果ptr为0,则访问的实际内存位置将为4.这仍然无效但不会被简单的断言捕获。 (可以合理地期望编译器在添加4之前检查ptr的地址,在这种情况下断言会捕获它。)

其次,考虑你有一个ptr数组并将任意元素传递给另一个函数的情况。如果您尝试访问数组的第二个元素,它将从第一个指针之外的16个字节开始。在没有捕获合法指针算法的情况下,编译器无法合理地期望在所有情况下可靠地执行您想要的操作。

您真正想要做的是使用操作系统和硬件来捕获无效和未对齐的内存访问并终止您的应用程序,然后找出如何获取所需的调试信息。最简单的方法就是在调试器中运行。如果您在Linux上使用gcc,请参阅how to generate a stacktace when my C++ app crashes。我假设有类似的方法可以与其他编译器做同样的事情。

答案 5 :(得分:0)

所以你说在系统抛出错误之前,它应该抛出一个错误......警告你即将出现的错误?

有什么意义?当我得到段错误时,我知道这意味着我遇到了段错误。我首先不需要单独的消息说“你现在会得到一个段错误。”

我完全忽略了这一点吗? :P

编辑: 我看到你在编辑中的意思,但实施起来并不容易。问题是,如果您访问错误的指针,则不是编译器或语言或运行时决定应该发生什么。该语言对此没有任何承诺或保证。相反,操作系统会在不知道这是一个调试可执行文件的情况下触发错误,而不知道哪个行号触发了问题,或其他任何内容。 这个错误所说的唯一一件事是“你试图访问地址X,我不能允许这样做。死”。编译器应该怎么做呢?

那么谁应该生成这个有用的错误消息?如何? 编译器可以这样做,当然,但是在错误处理中包装每个单指针访问,以确保如果发生段错误/访问冲突,我们捕获它,并触发断言。问题是,这将是非常慢的。不仅“发布太慢”,而且“太慢而无法使用”。它还假定编译器可以访问您调用的所有代码。如果您在第三方库中调用函数怎么办?内部的指针访问无法包含在错误处理代码中,因为编译器不会为该库生成代码。

OS 可以这样做,假设它愿意/能够加载相关的符号文件,以某种方式检测你是否正在运行调试可执行文件等等......就这样吧可以打印出行号。谈论过度工程。这不是操作系统的工作。

最后,这样做你会得到什么? 为什么不简单地启动调试器?当发生类似情况时,它会自动中断,为您提供精确的行号和其他所有内容。

可以完成,但它非常复杂,并且涉及编译器和操作系统,并且好处非常小。你会得到一个弹出框,告诉你调试器已经能够告诉你的信息。有了这些信息,你就可以......启动你的调试器 以找出问题所在。

答案 6 :(得分:0)

已经有相当于断言(prt!= NULL)作为操作系统的一部分。这就是为什么你得到一个段错误,而不是只是覆盖0地址的重要重要数据,然后真的弄乱了系统。

答案 7 :(得分:0)

鉴于您有可执行文件的符号文件,可以将崩溃的位置映射到行号。如果您在调试器中运行它,调试器会为您执行此操作,正如其他人所提到的那样。 Visual C ++甚至提供“即时”调试,当程序崩溃时,您可以将调试器附加到崩溃的进程以查看问题所在。

但是,如果要在未安装Visual C ++的计算机上使用此功能,仍可以使用某些编码进行此操作。您可以使用SetUnhandledExceptionFilter设置异常处理程序,该程序将在程序崩溃时调用。在处理程序中,您可以查看异常记录并使用SymGetLineFromAddr64来确定正在执行的源代码行。 “调试帮助”库中有许多功能,可以提取各种信息。请参阅the articles on MSDN以及www.debuginfo.com上的文章。

答案 8 :(得分:0)

好主意,但仅限于一个特定情况。那是在您取消引用funciton指针之前。原因是调试器将始终启动,但在取消引用空函数指针后,堆栈将被启动。因此,您在查找违规代码时遇到问题。如果调用者在调用之前进行了检查,则调试器将能够为您提供完整的堆栈。

更通用的检查是查看指针是否指向可执行的内存。 NULL不是,但许多操作系统也可以使用CPU功能使特定的内存段不可执行。