8

我记得当我在学习 C 编程课程时,一位老师曾经建议我用它printf来观察我正在尝试调试的程序的执行情况。这个程序有一个分段错误,原因我现在不记得了。我听从了他的建议,分段错误消失了。幸运的是,一位聪明的 TA 告诉我要调试而不是使用printfs。在这种情况下,这是一件有用的事情。

所以,今天我想向某人展示使用printf可能会隐藏一个错误,但我找不到那个有这个奇怪错误的旧代码(功能?嗯)。

问题:你们中的任何人是否也遇到过这种行为?我怎么能重现这样的东西?

编辑:

我看到我的问题部分将我的观点导向“使用printf是错误的”。我并不完全这么说,我不喜欢采取极端的意见,所以我正在编辑这个问题。我同意这printf是一个很好的工具,但我只是想重新创建一个printfs 使分段错误消失的情况,因此证明必须小心。

4

11 回答 11

19

在某些情况下,添加printf调用会改变代码的行为,但也有在调试时会发生同样情况的情况。最突出的例子是调试多线程代码,其中停止线程的执行可能会改变程序的行为,因此您正在寻找的错误可能不会发生。

所以使用printf语句确实有正当理由。是否调试或printf应根据具体情况决定。请注意,这两者并不是排他性的——即使它包含调用,您也可以printf调试代码:-)

于 2010-06-24T14:13:55.447 回答
7

您很难说服我不要使用日志记录(在这种情况下 printf 是一种特殊的日志记录形式)来调试。显然,要调试崩溃,首先要做的是获取回溯并使用 purify 或类似工具,但如果原因不明显,日志记录是迄今为止您可以使用的最佳工具之一。调试器可以让您专注于细节,而日志记录可以为您提供更大的视野。两者都很有用。

于 2010-06-24T14:18:01.753 回答
4

听起来你正在处理一个heisenbug

我认为printf作为调试工具使用并没有任何本质上的“错误”。但是,是的,就像任何其他工具一样,它也有其缺陷,而且是的,添加 printf 语句会产生 heisenbug 的情况不止一次。但是,由于调试器引入的内存布局更改,我也出现了 heisenbugs,在这种情况下 printf 在跟踪导致崩溃的步骤方面被证明是无价的。

于 2010-06-24T14:17:45.420 回答
2

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:

  copy stack pointer to some register
  push some other registers on stack
  subtract about 40960 from stack pointer
  do some stuff which leaves saved stack-pointer register alone
  add -8192 (signed interpretation of 0xA000) to stack pointer
  pop registers
  reload stack pointer from that other register

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).

于 2010-06-24T15:38:30.310 回答
2

恕我直言,每个开发人员仍然到处依赖打印输出。我们刚刚学会称它们为“详细日志”。

更重要的是,我看到的主要问题是人们将 printfs 视为无敌。例如,在 Java 中看到类似的东西并不罕见

System.out.println("The value of z is " + z + " while " + obj.someMethod().someOtherMethod());

这很好,除了 z 实际上参与了该方法而其他对象没有参与,并且确保您不会从 obj 上的表达式中得到异常。

打印输出的另一件事是它们会引入延迟。我见过在引入打印输出时有时会“修复”具有竞争条件的代码。如果某些代码使用它,我不会感到惊讶。

于 2010-06-24T14:14:33.663 回答
1

我刚有过类似的经历。这是我的具体问题和原因:

// Makes the first character of a word capital, and the rest small
// (Must be compiled with -std=c99)
void FixCap( char *word )
{
  *word = toupper( *word );
  for( int i=1 ; *(word+i) != '\n' ; ++i )
    *(word+i) = tolower( *(word+i) );
}

问题在于循环条件 - 我使用了“\n”而不是空字符“\0”。现在,我不确切知道 printf 是如何工作的,但是根据这次经验,我猜测它在我的变量之后使用了一些内存位置作为临时/工作空间。如果 printf 语句导致在存储我的单词之后的某个位置写入一个 '\n' 字符,那么 FixCap 函数将能够在某个点停止。如果我删除 printf,那么它会继续循环,寻找 '\n' 但从未找到它,直到它出现段错误。

所以最后,我的问题的根本原因是有时我输入'\n',而我的意思是'\0'。这是我以前犯过的一个错误,而且我可能会再犯一次。但现在我知道去寻找它了。

于 2012-02-20T18:13:11.667 回答
1

我设法做到了。我正在从平面文件中读取数据。我的错误算法如下:

  1. 获取输入文件的长度(以字节为单位)
  2. 分配一个可变长度的字符数组作为缓冲区
    • 文件很小,所以我不担心堆栈溢出,但是零长度的输入文件呢?哎呀!
  3. 如果输入文件长度为 0,则返回错误代码

我发现我的函数会可靠地抛出一个段错误——除非函数体中的某处有 printf,在这种情况下它会完全按照我的预期工作。段错误的修复是在步骤 2 中分配文件的长度加一。

于 2011-03-27T06:21:36.410 回答
0

调试案例是什么?char *[]在调用之前打印一个数组exec()只是为了看看它是如何被标记化的——我认为这是一个非常有效的用途printf()

但是,如果输入的格式printf()具有足够的成本和复杂性,以至于它实际上可能会改变程序的执行(主要是速度),那么调试器可能是更好的选择。再说一次,调试器和分析器也是有代价的。任何一个都可能暴露在他们缺席时可能不会出现的种族。

这一切都取决于您正在编写的内容和您正在追逐的错误。可用的工具是调试器,printf()(也将记录器分组到 printf 中)断言和分析器。

一字螺丝刀是否比其他类型的更好?取决于你需要什么。请注意,我并不是说断言是好是坏。它们只是另一种工具。

于 2010-06-24T14:42:27.207 回答
0

好吧,也许你可以教他如何使用 gdb 或其他调试程序?告诉他,如果一个错误仅仅由于“printf”而消失,那么它并没有真正消失,以后可能会再次出现。一个错误应该被修复,而不是被忽略。

于 2010-06-24T14:14:41.197 回答
0

删除 printf 行时,这将为您除以 0:

int a=10;
int b=0;
float c = 0.0;

int CalculateB()
{
  b=2;
  return b;
}
float CalculateC()
{
  return a*1.0/b;
}
void Process()
{
  printf("%d", CalculateB()); // without this, b remains 0
  c = CalculateC();
}
于 2010-06-24T14:23:22.027 回答
0

解决这个问题的一种方法是建立一个宏系统,它可以很容易地关闭 printfs 而无需在代码中删除它们。我使用这样的东西:

#define LOGMESSAGE(LEVEL, ...) logging_messagef(LEVEL, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__);

/* Generally speaking, user code should only use these macros.  They
 * are pithy. You can use them like a printf:
 *
 *    DBGMESSAGE("%f%% chance of fnords for the next %d days.", fnordProb, days);
 *
 * You don't need to put newlines in them; the logging functions will
 * do that when appropriate.
 */
#define FATALMESSAGE(...) LOGMESSAGE(LOG_FATAL, __VA_ARGS__);
#define EMERGMESSAGE(...) LOGMESSAGE(LOG_EMERG, __VA_ARGS__);
#define ALERTMESSAGE(...) LOGMESSAGE(LOG_ALERT, __VA_ARGS__);
#define CRITMESSAGE(...) LOGMESSAGE(LOG_CRIT, __VA_ARGS__);
#define ERRMESSAGE(...) LOGMESSAGE(LOG_ERR, __VA_ARGS__);
#define WARNMESSAGE(...) LOGMESSAGE(LOG_WARNING, __VA_ARGS__);
#define NOTICEMESSAGE(...) LOGMESSAGE(LOG_NOTICE, __VA_ARGS__);
#define INFOMESSAGE(...) LOGMESSAGE(LOG_INFO, __VA_ARGS__);
#define DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#if defined(PAINFULLY_VERBOSE)
#   define PV_DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#else
#   define PV_DBGMESSAGE(...) ((void)0);
#endif

logging_messagef()是在单独.c文件中定义的函数。根据消息的用途,在代码中使用 XMESSAGE(...) 宏。这个设置最好的一点是它可以同时用于调试和日志记录,并且logging_messagef()可以更改函数以执行多种不同的操作(printf 到 stderr,到日志文件,使用 syslog 或其他一些系统日志记录工具等.),logging_messagef()当您不需要它们时, 可以忽略低于特定级别的消息。PV_DBGMESSAGE()适用于那些您肯定希望在生产中关闭的大量调试消息。

于 2010-06-24T15:02:34.990 回答