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

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

问题: 你们中也有人遇到过这种行为吗?我怎样才能重现这样的东西?

编辑:

我看到我的问题部分将我的观点定位为“使用 printf 是错的”。我并不是这么说的,而且我不喜欢采取极端的意见,所以我正在编辑一下这个问题。我同意 printf 是一个很好的工具,但我只是想重新创建一个案例 printf这使得分段错误消失,因此证明人们必须小心。

有帮助吗?

解决方案

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

所以使用 printf 语句确实有正当的理由。是调试还是 printf 应逐案决定。请注意,两者无论如何都不是独家的 - 您 能够 调试代码即使包含 printf 电话:-)

其他提示

您将很难说服我不要使用伐木(在这种情况下printf是一种事实的伐木形式)进行调试。显然,要调试崩溃,第一件事是获得倒退并使用净化或类似工具,但是如果原因不明显,则记录是迄今为止您可以使用的最佳工具之一。调试器允许您专注于详细信息,记录可为您提供更大的了解。两者都是有用的。

听起来您正在处理 海森伯格.

我认为使用本质上没有任何固有的“错误” printf 作为调试工具。但是,是的,像其他任何工具一样,它都有缺陷,是的,添加printf语句创建了一个heisenbug,已经有不止一个ercaision。但是,由于调试器引入的内存布局更改,我也出现了Heisenbugs,在这种情况下,Printf在跟踪导致崩溃的步骤方面非常宝贵。

恕我直言,每个开发人员仍然在这里和那里都依靠打印输出。我们刚刚学会称它们为“详细日志”。

更重要的是,我看到的主要问题是人们将printf对待,就像无敌。例如,在Java中看到类似的东西并不罕见

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

这很棒,除了Z实际上参与了该方法,但其他对象没有,并且可以确保您不会从OBJ上的表达中获得例外。

打印输出所做的另一件事是他们引入延迟。当引入打印输出时,我看到有种族条件的代码有时“修复”。如果某些代码使用它,我不会感到惊讶。

我记得曾经尝试过在Macintosh(大约1991年)上调试程序,其中编译器生成的32K和64K之间堆栈框架的清理代码是错误的,因为它使用了16位地址添加而不是32位(A 16) - 添加到地址寄存器中的数量将在68000上登录)。序列就像:

  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

网络效果是一切都很好 除了 保存的寄存器被破坏了,其中一个人保持了不变(全球阵列的地址)。如果编译器在代码部分期间优化了寄存器的变量,则报告说在调试信息文件中,调试器可以正确输出它。当常量如此优化时,编译器显然不包含此类信息,因为不需要。我通过执行数组地址的“ printf”来跟踪事物,并设置断点,以便我可以在printf之前和之后查看地址。调试器正确地报告了printf之前和之后的地址,但是printf输出了错误的值,因此我拆开了代码,并看到printf将寄存器A3推到堆栈上;在printf之前查看寄存器A3显示其具有与数组地址大不相同的值(printf显示了实际持有的值A3)。

我不知道如果我不能一起使用调试器和printf(或者,如果我不了解68000的汇编代码),我将如何跟踪这一点。

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

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

我发现我的功能将可靠地抛出SEG故障 - 除非功能正文中的某个地方有一个printf,在这种情况下,它将完全按照我的意图工作。 SEG故障的修复是分配文件的长度加上步骤2中的一个。

我只是有类似的经历。这是我的特定问题,也是原因:

// 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',而不是null字符' 0'。现在,我不知道printf是如何工作的,但是从这种经验中,我猜想它在变量作为临时 /工作空间后使用了一些内存位置。如果printf语句导致在存储我的单词之后的某个位置写入“ n”字符,则FixCap函数将能够在某个时候停止。如果我删除了printf,那么它会继续循环,寻找' n',但永远不会找到它,直到它被击退为止。

因此,最终,我问题的根本原因是有时我的意思是' 0'时我键入' n'。这是我以前犯的一个错误,也许我会再次犯一个错误。但是现在我知道要寻找它。

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

删除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();
}

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

但是,如果格式为 printf() 具有足够的成本和复杂性,它实际上可能会改变程序执行(大多数情况下),调试器可能是更好的方法。再说一次,辩论者和剖道师也付出了代价。任何一个人都可能在缺席的情况下暴露可能不会浮出水面的种族。

这完全取决于您正在写的内容以及您正在追逐的错误。可用的工具是调试器, printf() (也将记录器分组为printf)主张和剖道师。

刀片螺丝刀比其他类型更好吗?取决于您的需求。请注意,我并不是说断言是好是坏。它们只是另一个工具。

解决此问题的一种方法是设置一个宏系统,该系统可以轻松关闭必须在代码中删除它们的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() 是针对那些大量的调试消息,您肯定希望在生产中关闭。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top