代码之家  ›  专栏  ›  技术社区  ›  YuppieNetworking

一个教育性的例子,显示有时打印作为调试可能隐藏一个错误

  •  7
  • YuppieNetworking  · 技术社区  · 15 年前

    我记得当我在C编程的某个课程中,一位老师曾经建议我使用 printf 观察我试图调试的程序的执行。这个程序有一个分段错误,原因我现在记不起来了。我听从他的建议,分割错误消失了。幸运的是,一个聪明的助教告诉我调试而不是使用 普林特 在这种情况下,这是一件有用的事情。

    所以,今天我想向大家展示 普林特 可能隐藏了一个bug,但我找不到有这个奇怪bug(特性?HMM)。

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

    编辑:

    我看到我的问题部分将我的观点导向“使用 普林特 是错的。我不是这么说的,我不喜欢采取极端的意见,所以我在编辑一些问题。我同意 普林特 是一个很好的工具,但我只是想重新创建一个案例 普林特 S使分割故障消失,从而证明必须小心。

    11 回复  |  直到 14 年前
        1
  •  18
  •   Péter Török    15 年前

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

    所以使用 普林特 声明确实有正当理由。是否调试或 普林特 应该根据具体情况来决定。请注意,这两个不是唯一的-你 可以 调试代码,即使它包含 普林特 呼叫:-)

        2
  •  7
  •   AProgrammer    15 年前

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

        3
  •  4
  •   torak    15 年前

    Sounds like you're dealing with a heisenbug .

    我认为使用 printf 作为调试工具。但是,是的,和其他工具一样,它也有缺陷,而且是的,有不止一种情况下,添加printf语句会创建一个heisenbug。但是,我也让HeisenBugs出现在调试器引入的内存布局更改的结果中,在这种情况下,printf在跟踪导致崩溃的步骤方面被证明是非常宝贵的。

        4
  •  2
  •   Uri    15 年前

    imho每个开发人员仍然依赖打印输出。我们刚学会称之为“详细日志”。

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

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

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

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

        5
  •  2
  •   supercat    15 年前

    我记得有一次我试着在Macintosh上调试一个程序(大约1991年),其中编译器为32K到64K之间的堆栈帧生成的清除代码是错误的,因为它使用了16位地址加法而不是32位加法(添加到地址寄存器的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 act保持正常状态)。

    如果我不能同时使用调试器和printf(或者说,如果我不理解68000个汇编代码的话),我不知道我会如何跟踪到它。

        6
  •  1
  •   Cyan    15 年前

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

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

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

        7
  •  1
  •   Peter O. Manuel Pinto    14 年前

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

    // 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是如何工作的,但是根据这个经验,我猜想它在变量之后使用了一些内存位置作为临时/工作空间。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. 如果我删除了printf,那么它会继续循环,寻找一个'\n',但永远找不到它,直到它出错为止。

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

        8
  •  0
  •   Pik'    15 年前

    嗯,也许你可以教他如何使用gdb或其他调试程序? 告诉他,如果一个臭虫因为“printf”而消失,那么它不会真正消失,可能会再次出现在后者。错误应该被修复,而不是被忽略。

        9
  •  0
  •   Andy    15 年前

    这将在删除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();
    }
    
        10
  •  0
  •   Tim Post Samir J M Araujo    15 年前

    调试的情况如何?打印A char *[] 调用前的数组 exec() 只是想看看它是如何被标记化的-我认为这是一个非常有效的用途 printf() .

    但是,如果将格式发送到 Primff() 它的成本和复杂性足以改变程序的执行(速度,主要是速度),调试器可能是更好的方法。另外,调试人员和分析人员也要付出代价。任何一个都可能暴露出在他们缺席的情况下可能不会出现的种族。

    这完全取决于你在写什么和你在追的虫子。可用的工具是调试程序, Primff() (将记录器分组为printf)断言和配置文件。

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

        11
  •  0
  •   Tim Schaeffer    15 年前

    解决这个问题的一种方法是建立一个宏系统,这样可以很容易地关闭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(…)宏。这个安装程序最好的一点是它同时用于调试和日志记录,并且 日志记录_messagef() 可以将函数更改为执行几个不同的操作(printf到stderr、日志文件、使用syslog或其他系统日志记录工具等),并且可以在中忽略低于某个级别的消息。 日志记录_messagef() 当你不需要它们的时候。 PV_DBGMESSAGE() 对于那些大量的调试消息,您肯定希望在生产中关闭它们。