调试编译的可执行文件:为什么不在无效写入 NULL 时优雅地中止?

发布于 2024-07-14 02:19:53 字数 420 浏览 4 评论 0原文

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

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

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

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

What I don't understand about C/C++ is:

Yes, everyone uses it to get blazingly fast executables, so they compile with optimization turned on.

But for compilation with debug information turned on, we don't care about speed. So why not include more information in that compile mode, for example detect some segfaults before they happen? Effectively, insert an assert(ptr != NULL) before every access to a pointer ptr. Why can't the compiler do that? Again, that should be off by default, but there should be such a possibility, I think.

EDIT: Some people said that the detection I suggested doesn't make sense or doesn't do anything that the report of segmentation fault wouldn't already do. But what I have in mind is just a more graceful and informative abort, which prints the file name and line number of the offending code, just like an assert() would do.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

九命猫 2024-07-21 02:19:53

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

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

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


回应添加到问题中的附加信息(我想我误解了您的意图):

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

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

我不确定编译器供应商会说什么...也许在 Microsoft 的 VC++ 产品 Connect 站点上发布请求< /a> 看看他们怎么说。

What should the program do in that case? If it informs the user of a bug, then that's what the segfault does.

If it's supposed to keep going and avoid the bug, how does it know what to do?

Not to mention that if it did somehow magically know how to continue properly, then you have a bug in your release build (debug builds are intended to help you identify and fix bugs - not hide them).


In response to the additional information added to the question (I guess I misunderstood your intent):

what I have in mind is just a more graceful and informative abort, which prints the file name and line number of the offending code, just like an assert() would do.

This is something the compiler could do - as you say, the compiler would essentially be automatically inserting an assert() anywhere a pointer was dereferenced. This might add pretty significantly to the size of a debug build, but it would probably still be acceptable for many (or most) purposes. I think this would be a reasonable option for a compiler to provide.

I'm not sure what compiler vendors would say... Maybe post a request on Microsoft's Connect site for the VC++ product and see what they say.

甜中书 2024-07-21 02:19:53

您的建议存在几个主要问题:

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

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

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

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

更新以回应评论:

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

There are a few major problems with your suggestion:

What conditions do you want the compiler to detect? On Linux/x86, unaligned access can cause SIGBUS and stack overflow can cause SIGSEGV, but in both cases it technically is possible to write the application to detect those conditions and fail "gracefully". NULL pointer checks can be detected, but the most insidious bugs are due to invalid pointer access, rather than NULL pointers.

The C and C++ programming languages provide enough flexibility so it is impossible for a runtime to determine with 100% success if a given random address is a valid pointer of an arbitrary type.

What would you like the runtime environment to do when it detects this situation? It can't correct the behavior (unless you believe in magic). It can only continue executing or exit. But wait a minute... that's what already happens when a signal is delivered! The program exits, a core dump is generated, and that core dump can be used by application developers to determine the state of the program when it crashed.

What you're advocating actually sounds like you want to run your application in a debugger (gdb) or through some form of virtualization (valgrind). This is already possible, but it makes no sense to do it by default, because it provides no benefit to non-developers.

Update to respond to comments:

There's no reason to modify the compilation process for debug versions. If you need a "gentle" debug version of the application, you should run it inside of a debugger. It's very easy to wrap your executable in a script that does this for you transparently.

垂暮老矣 2024-07-21 02:19:53

我同意迈克尔·伯尔的观点,即这并没有真正起到任何作用或有任何帮助。

此外,这对于悬空指针仍然不起作用,悬空指针往往比空指针更加阴险且难以追踪。

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

I agree with Michael Burr that this doesn't really do or help anything.

Furthermore, this still wouldn't work for dangling pointers which tend to be far more insidious and difficult to track down than null pointers.

At least with null pointers it's simple enough to ensure they are valid before you deref them.

巡山小妖精 2024-07-21 02:19:53

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

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

I think the original poster wants the app to stop in the debugger. You would have access to all of the stack variables and stack so you would have a chance to figure out why your program is in this state.

If you are developing in C/C++, a debugging memory manager can save you a ton of time. Buffer overruns, accessing deleted memory, memory leaks, and so on are pretty easy to find and fix. There are several on the market or you can spend 2 or 3 days to write your own and get 90% of the needed functionality. If you are writing apps without them, you are making your job much more difficult than it needs to be.

梦中楼上月下 2024-07-21 02:19:53

在取消引用指针之前,简单的 assert(ptr != NULL) 不起作用还有一个原因:并非每个无效指针(即使是那些最初为 NULL 的指针)实际上都是相等的 首先考虑这样

的情况:您有一个包含多个成员的结构:

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

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

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

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

There's one more reason a simple assert(ptr != NULL) won't work before dereferencing a pointer won't work: Not every invalid pointer (even those that began life as NULL) is in fact equal to 0.

First consider the case where you have a struct with several members:

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

If you have a pointer ptr to mystruct and you try to access ptr->second, the compiler is going to generate code that ads 4 (assuming 32-bit integers) to ptr and access that memory location. If ptr is 0, the actual memory location accessed will be 4. That's still invalid but wouldn't be caught by a simple assertion. (The compiler could reasonably be expected to check the address of ptr before adding 4, in which case the assertion would catch it.)

Second, consider the case where you have an array of struct mystruct and you pass an arbitrary element to another function. If you try to access the second element of the array, it will begin at 16 bytes beyond the first pointer. There's no way the compiler could reasonably be expected to do what you want reliably in all cases, without catching legitimate pointer arithmetic.

What you really want to do is use the operating system and hardware to catch invalid and unaligned memory access and kill your application, then figure out how to get the debugging information you need. The easiest way is simply to run inside a debugger. If you're using gcc on Linux, see how to generate a stacktace when my C++ app crashes. I assume there are similar ways to do the same thing with other compilers.

悲凉≈ 2024-07-21 02:19:53

作为操作系统的一部分,已经有相当于assert(prt != NULL) 的东西。 这就是为什么你会遇到段错误,而不是仅仅覆盖 0 地址处的重要数据,然后真正搞乱系统。

There is already the equivalent of a assert(prt != NULL) as a part of the OS. That's why you get a segfault instead of just overwriting important important data at the 0 address and then really messing up the system.

¢好甜 2024-07-21 02:19:53

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

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

Given that you have a symbol file for your executable, it is possible to map the location of the crash to a line number. The debugger does this for you if you are running it in the debugger, as others have mentioned. Visual C++ even offers "just-in-time" debugging where, when the program crashes, you can attach a debugger to the crashed process to see where the problem is.

However, if you want to have this functionality on machines where Visual C++ is not installed, it is still possible to do it with some coding. You can set up an exception handler using SetUnhandledExceptionFilter that will be called when your program crashes. In the handler, you can look at the exception record and use SymGetLineFromAddr64 to determine what source line that was executing. There are many functions in the "debug help" library that lets you extract all sorts of information. See the articles on MSDN, and also the articles on www.debuginfo.com.

ぺ禁宫浮华殁 2024-07-21 02:19:53

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

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

我完全没有抓住要点吗? :p

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

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

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

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

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

So you're saying that before the system throws an error, it should throw an error to.... warn you of the impending error?

What'd be the point? When I get a segfault, I know it means I got a segfault. I don't need a separate message first saying "you will now get a segfault".

Am I completely missing the point here? :p

Edit:
I see what you mean in your edit, but it's not easy to implement. The problem is that it's not the compiler or the language or the runtime that decides what should happen if you access a bad pointer. The language officially makes no promises or guarantees about this. Instead, the OS fires off an error, without knowing that this is a debug executable, without knowing which line number triggered the problem, or anything else.
The only thing this error says is "you tried to access address X, and I can't allow that. Die". What should the compiler do with this?

So who should generate this helpful error message? And how?
The compiler could do it, sure, but wrapping every single pointer access in error handling, to ensure that if a segfault/access violation occurs, we catch it, and trigger an assert instead. The problem is, this would be ridiculously slow. Not just "too slow for release", but "too slow to be usable". It also assumes that the compiler has access to all code you call into. What if you call a function in a third-party library? Pointer accesses inside that can't be wrapped in error handling code, because the compiler doesn't generate code for that library.

The OS could do it, assuming it was willing/able to load the relevant symbol files, somehow detect whether or not you're running a debug executable and so on... Just so it can print out a line number. Talk about overengineering. This is hardly the OS's job.

And finally, what would you gain by doing this?
Why not simply fire up your debugger? It automatically breaks when something like this happens, giving you precise line number and everything else.

It could be done, but it'd be awfully complicated, and involve both the compiler and the OS, and the benefit would be extremely small. You'd get a popup box telling you information that your debugger is already able to tell you. And with that information, you'd then... fire up your debugger anyway to find out what went wrong.

極樂鬼 2024-07-21 02:19:53

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

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

Good idea, but only in one particular case. That is before you're dereferencing a funciton pointer. The reason is that the debugger will always kick in, but after dereferencing a null function pointer the stack is shot. Hence, you have problems finding the offending code. If the caller checked before calling, the debugger would be able to give you a full stack.

A more generic check would be to see if the pointer points to memory which is executable. NULL isn't, but many Operating Systems also can use CPU features to make specific memory segments non-executable.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文