3

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

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

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

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

4

9 回答 9

8

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

如果它应该继续运行并避免错误,它怎么知道该怎么做?

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


针对添加到问题中的其他信息(我想我误解了您的意图):

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

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

我不确定编译器供应商会说什么……也许在Microsoft 的 Connect 网站上发布一个关于 VC++ 产品的请求,看看他们怎么说。

于 2009-02-15T01:17:58.377 回答
7

你的建议有几个主要问题:

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

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

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

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

更新以回应评论:

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

于 2009-02-15T01:27:55.937 回答
1

我同意 Michael Burr 的观点,即这并没有真正起到任何作用或帮助任何事情。

此外,这仍然不适用于比空指针更隐蔽且难以追踪的悬空指针。

至少对于空指针来说,在取消引用它们之前确保它们是有效的很简单。

于 2009-02-15T01:24:03.683 回答
1

我认为原始发布者希望应用程序在调试器中停止。您将有权访问所有堆栈变量和堆栈,因此您将有机会弄清楚您的程序为何处于这种状态。

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

于 2009-02-15T02:38:47.043 回答
1

assert(ptr != NULL)在取消引用指针不起作用之前,还有一个简单的不起作用的原因:并非每个无效指针(即使是那些以 NULL 开头的指针)实际上都等于 0。

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

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

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

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

您真正想要做的是使用操作系统和硬件来捕获无效和未对齐的内存访问并终止您的应用程序,然后弄清楚如何获取您需要的调试信息。最简单的方法就是在调试器中运行。如果您在 Linux 上使用 gcc,请查看当我的 C++ 应用程序崩溃时如何生成堆栈信息。我假设有类似的方法可以用其他编译器做同样的事情。

于 2009-02-15T20:00:52.817 回答
0

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

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

我完全错过了这里的重点吗?:p

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

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

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

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

可以做到,但会非常复杂,涉及编译器和操作系统,收益也非常小你会得到一个弹出框,告诉你调试器已经能够告诉你的信息。有了这些信息,您就可以……无论如何启动调试器以找出问题所在。

于 2009-02-15T01:39:53.737 回答
0

作为操作系统的一部分,已经有一个等价的 assert(prt != NULL) 。这就是为什么你会得到一个段错误,而不是仅仅覆盖 0 地址的重要数据,然后真正搞砸系统。

于 2009-02-15T02:30:13.117 回答
0

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

但是,如果您想在未安装 Visual C++ 的机器上拥有此功能,仍然可以通过一些编码来实现。SetUnhandledExceptionFilter您可以使用将在程序崩溃时调用的异常处理程序来设置。在处理程序中,您可以查看异常记录并用于SymGetLineFromAddr64确定正在执行的源代码行。“调试帮助”库中有许多函数可以让您提取各种信息。请参阅MSDN 上的文章,以及www.debuginfo.com上的文章。

于 2009-02-15T19:39:39.543 回答
0

好主意,但仅限于一种特殊情况。那是在您取消引用函数指针之前。原因是调试器总是会启动,但在取消引用空函数指针后,堆栈会被拍摄。因此,您很难找到有问题的代码。如果调用者在调用之前进行了检查,调试器将能够为您提供完整的堆栈。

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

于 2009-02-16T09:48:25.400 回答