教育示例表明有时 printf 作为调试可能会隐藏错误
我记得当我在学习 C 编程课程时,一位老师曾经建议我使用 printf
来观察我试图调试的程序的执行情况。这个程序有一个分段错误,原因我现在不记得了。我听从了他的建议,分段错误消失了。幸运的是,一位聪明的助教告诉我要调试而不是使用 printf。在这种情况下,这是一件有用的事情。
所以,今天我想向某人展示使用 printf
可能会隐藏一个错误,但我找不到有这个奇怪错误的旧代码(功能?嗯)。
问题:你们中也有人遇到过这种行为吗?我怎样才能重现这样的东西?
编辑:
我发现我的问题部分将我的观点定位为“使用 printf
是错误的”。我并不是这么说的,而且我不喜欢采取极端的意见,所以我正在编辑一下这个问题。我同意 printf 是一个很好的工具,但我只是想重新创建一个例子,其中 printf 使分段错误消失,因此证明必须小心。
I remember when I was in some course of C programming, a teacher once suggested that I use printf
to watch the execution of a program that I was trying to debug. This program had a segmentation fault with a cause that I cannot remember at the moment. I followed his advice and the segmentation fault disappeared. Fortunately, a clever TA told me to debug instead of using printf
s. In this case, it was an useful thing to do.
So, today I wanted to show someone that using printf
could potentially hide a bug but I can't find that old code that had this bizarre bug (feature? hmmm).
Question: Have any of you encountered this behavior as well? How could I reproduce something like this?
Edit:
I see that my question part orients my opinion to "using printf
is wrong". I am not exactly saying that and I don't like taking extreme opinions, so I'm editing a bit the question. I agree that printf
is a good tool, but I just wanted to recreate a case where printf
s make a segmentation fault disappear and hence, prove that one must be careful.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
在某些情况下,添加
printf
调用会改变代码的行为,但在某些情况下,调试也会改变代码的行为。最突出的例子是调试多线程代码,其中停止线程的执行可能会改变程序的行为,因此您正在寻找的错误可能不会发生。所以使用 printf 语句确实有充分的理由。是调试还是 printf 应根据具体情况决定。请注意,无论如何,两者并不排斥 - 即使代码包含
printf
调用,您可以调试代码:-)There are cases when adding
printf
calls alters the behaviour of the code, but there are also cases when debugging does the same. The most prominent example is debugging multithreaded code, where stopping the execution of a thread may alter the behaviour of the program, thus the bug you are looking for may not happen.So using
printf
statements does have valid reasons. Whether to debug orprintf
should be decided on a case by case basis. Note that the two are not exclusive anyway - you can debug code even if it containsprintf
calls :-)你很难说服我不要使用日志记录(在这种情况下 printf 是一种特殊的日志记录形式)来调试。显然,要调试崩溃,首先要做的是获取回溯并使用 purify 或类似的工具,但如果原因不明显,那么日志记录是迄今为止您可以使用的最佳工具之一。调试器可以让您专注于细节,日志记录可以让您了解更大的情况。两者都有用。
You'd have a very hard time to convince me not to use logging (and printf in this situation is an had hoc form of logging) to debug. Obviously to debug a crash, the first things is to get a backtrace and use purify or a similar tool, but if the cause is not obvious logging is by far one of the best tool you can use. A debugger allows you to concentrate on details, logging give you a bigger picture. Both are useful.
听起来您正在处理 heisenbug。
我不认为使用 printf 作为调试工具有什么本质上的“错误”。但是,是的,就像任何其他工具一样,它也有其缺陷,而且,添加 printf 语句不止一次会产生 heisenbug。然而,我也遇到过由于调试器引入内存布局更改而出现的 heisenbug,在这种情况下,printf 在跟踪导致崩溃的步骤方面被证明是非常有价值的。
Sounds like you're dealing with a heisenbug.
I don't think there's anything inherently "wrong" with the use of
printf
as a debugging tool. But yes, like any other tool, it has its flaws, and yes there has been more than one occaision where the addition of printf statements created a heisenbug. However, I've also had heisenbugs show up as a result of memory layout changes introduced by a debugger, in which case printf proved invaluable in tracking the steps that lead to the crash.恕我直言,每个开发人员仍然时不时地依赖打印输出。我们刚刚学会称它们为“详细日志”。
更重要的是,我看到的主要问题是人们对待 printfs 就好像它们是无敌的。例如,在 Java 中经常会看到类似
This is Great 的情况,只不过 z 实际上参与了该方法,而其他对象却没有参与,并且可以确保您不会从 obj 的表达式中得到异常。
打印输出的另一件事是它们会带来延迟。我见过在引入打印输出时有时会“修复”具有竞争条件的代码。如果某些代码使用它,我不会感到惊讶。
IMHO Every developer still relies here and there on printouts. We just learned to call them "detailed logs".
More to the point, the main problem that I've seen is that people treat printfs like they're invincible. For instance, it is not rare in Java to see something like
This is great, except that z was actually involved in the method but that other object was not, and there's to ensure you won't get an exception from the expression on obj.
Another thing that printouts do is that they introduce delays. I've seen code with race conditions sometimes "get fixed" when printouts are introduced. I would not be surprised if some code uses that.
我记得有一次尝试在 Macintosh 上调试一个程序(大约 1991 年),其中编译器为 32K 到 64K 之间的堆栈帧生成的清理代码是错误的,因为它使用 16 位地址加法而不是 32 位地址加法(16 位地址加法)。添加到地址寄存器的位数将在 68000 上进行符号扩展。序列是这样的:
最终效果是一切都很好除了保存的寄存器已损坏,并且其中一个保存了一个常量(全局数组的地址)。如果编译器在一段代码期间将变量优化为寄存器,它会在调试信息文件中报告该变量,以便调试器可以正确输出它。当一个常量被如此优化时,编译器显然不包含此类信息,因为应该没有必要。我通过对数组的地址执行“printf”来跟踪情况,并设置断点,以便我可以查看 printf 之前和之后的地址。调试器正确报告了 printf 前后的地址,但 printf 输出了错误的值,因此我反汇编了代码,发现 printf 将寄存器 A3 压入堆栈;在 printf 之前查看寄存器 A3,发现它的值与数组的地址相当不同(printf 显示 A3 实际保存的值)。
如果我不能同时使用调试器和 printf (或者,就这一点而言,如果我不理解 68000 汇编代码),我不知道如何跟踪该代码。
I remember once trying to debug a program on the Macintosh (circa 1991) where the compiler's generated cleanup code for a stack frame between 32K and 64K was erroneous because it used a 16-bit address addition rather than a 32-bit one (a 16-bit quantity added to an address register will be sign-extended on the 68000). The sequence was something like:
The net effect was that everything was fine except that the saved registers were corrupted, and one of them held a constant (the address of a global array). If the compiler optimizes a variable to a register during a section of code, it reports that in the debug-information file so the debugger can correctly output it. When a constant is so optimized, the compiler apparently does not include such information, since there should be no need. I tracked things down by doing a "printf" of the address of the array, and set breakpoints so I could view the address before and after the printf. The debugger correctly reported the address before and after the printf, but the printf outputted the wrong value, so I disassembled the code and saw that printf was pushing register A3 onto the stack; viewing register A3 before the printf showed that it had a value rather different from the address of the array (the printf showed the value A3 actually held).
I don't know how I ever would have tracked that one down if I hadn't been able to use both the debugger and printf together (or, for that matter, if I hadn't understood 68000 assembly code).
我设法做到了这一点。我正在从平面文件中读取数据。我的错误算法如下:
我发现我的函数会可靠地抛出 seg 错误 - 除非函数主体中的某处有 printf,在这种情况下它将完全按照我的预期工作。 seg 错误的修复方法是在步骤 2 中分配文件的长度加一。
I managed to do this. I was reading data in from a flat file. My faulty algorithm went as follows:
I found that my function would reliably throw a seg fault -- unless there was a printf somewhere in the body of the function, in which case it would work exactly as I intended. The fix for the seg fault was to allocate the length of the file plus one in step 2.
我刚刚有过类似的经历。这是我的具体问题及其原因:
问题出在循环条件上 - 我使用“\n”而不是空字符“\0”。现在,我不知道 printf 是如何工作的,但从这次经验来看,我猜测它在我的变量之后使用一些内存位置作为临时/工作空间。如果 printf 语句导致在存储我的单词之后的某个位置写入“\n”字符,则 FixCap 函数将能够在某个时刻停止。如果我删除 printf,那么它会继续循环,寻找 '\n' 但永远找不到它,直到出现段错误。
所以最后,我的问题的根本原因是有时我在表示“\0”时输入“\n”。这是我以前犯过的错误,而且很可能还会再犯。但现在我知道要寻找它。
I just had a similar experience. Here's my specific problem, and the cause:
The problem is with the loop condition - I used '\n' instead of the null character, '\0'. Now, I don't know exactly how printf works, but from this experience I'm guessing that it uses some memory location after my variables as temporary / working space. If a printf statement results in a '\n' character being written at some location after where my word is stored, then the FixCap function will be able to stop at some point. If I remove the printf, then it keeps on looping, looking for a '\n' but never finding it, until it segfaults.
So in the end, the root cause of my problem is that sometimes I type '\n' when I mean '\0'. It's a mistake I've made before, and probably one I'll make again. But now I know to look for it.
好吧,也许你可以教他如何使用 gdb 或其他调试程序?
告诉他,如果 bug 仅仅由于“printf”而消失,那么它并没有真正消失,并且可能会在稍后再次出现。错误应该被修复,而不是被忽略。
Well, maybe you could teach him how to use gdb or other debugging programs ?
Tell him that if a bug disappear juste thanks to a "printf", then it didn't really disappear and could appear again latter. A bug should be fixed, not ignored.
当删除 printf 行时,这将为您提供除以 0 的结果:
This will give you a division by 0 when removing the printf line:
调试情况会怎样?在调用
exec()
之前打印一个char *[]
数组,只是为了看看它是如何标记化的 - 我认为这是printf()< 的一个非常有效的用途/代码>。
然而,如果输入到 printf() 的格式具有足够的成本和复杂性,它实际上可能会改变程序执行(主要是速度),那么调试器可能是更好的选择。不过,调试器和分析器也是有代价的。任何一个都可能会揭露在他们缺席的情况下可能不会出现的种族。
这一切都取决于您正在编写的内容以及您正在追踪的错误。可用的工具包括调试器、
printf()
(也将记录器分组到 printf 中)断言和分析器。刀片螺丝刀比其他类型的螺丝刀好吗?取决于你需要什么。请注意,我并不是说断言是好还是坏。它们只是另一个工具。
What would be the debugging case? Printing a
char *[]
array prior to callingexec()
just to see how it was tokenized - I think thats a pretty valid use forprintf()
.However, if the format fed to
printf()
is of sufficient cost and complexity that it may actually alter program execution (speed, mostly), a debugger may be the better way to go. Then again, debuggers and profilers also come at a cost. Either one may expose races that might not surface in their absence.It all depends on what you are writing and the bug you are chasing. The tools available are debuggers,
printf()
(grouping loggers into printf as well) assertions and profilers.Is a blade screwdriver better than other kinds? Depends on what you need. Note, I'm not saying assertions are good or bad. They're just another tool.
解决这个问题的一种方法是建立一个宏系统,这样可以轻松关闭 printfs,而无需在代码中删除它们。我使用这样的东西:
logging_messagef()
是一个在单独的.c
文件中定义的函数。根据消息的目的,在代码中使用 XMESSAGE(...) 宏。此设置最好的一点是它可以同时用于调试和日志记录,并且可以更改logging_messagef()
函数来执行多种不同的操作(printf 到 stderr、到日志文件、使用 syslog 或其他一些系统日志记录工具等),并且当您不需要时,可以在logging_messagef()
中忽略低于特定级别的消息。PV_DBGMESSAGE()
用于那些您肯定希望在生产中关闭的大量调试消息。One way to deal with this is to set up a system of macros which makes it easy to turn off printfs w/o having to delete them in your code. I use something like this:
logging_messagef()
is a function defined in a separate.c
file. Use the XMESSAGE(...) macros in your code depending on the purpose of the message. The best thing about this setup is that it works for debugging and logging at the same time, and thelogging_messagef()
function can be changed to do several different things (printf to stderr, to a log file, use syslog or some other system logging facility, etc.), and messages below a certain level can be ignored inlogging_messagef()
when you don't need them.PV_DBGMESSAGE()
is for those copious debug messages which you will certainly want to turn off in production.