【问题标题】:Finding bugs in C code查找 C 代码中的错误
【发布时间】:2013-03-23 06:28:09
【问题描述】:

更新:确实,依赖某些程序的段错误是错误的。但我很感激并从我在这里收到的答案中学到了很多东西。

segfault.c

#include<stdio.h>
#include<stdlib.h>


/**
  *
  * This binary receives 2 numbers, an L and an R
  *
  * It will intentionally hit Segmentation Fault(actually SIGSEGV) whenever 30 is in the interval [L,R]
  *
  * It will be used to test the binary search segfault finder
  *
  */
int main(int argc, int **argv) {
  char exists;
  char *Q[3000];
  int i;
  int L = atoi((char*)argv[1]);
  int R = atoi((char*)argv[2]);

  printf("L=%d R=%d",L,R);
  /*exit(0);*/
  for(i=0;i<3000;i++)
    Q[i] = &exists;

  Q[30] = NULL; // <==== I want to cause a SIGSEGV through this !

  for(i=L;i<=R;i++) {
    int T = *Q[i]; // <== will segfault when i == 30 because I said so :)
  };
};

automate.pl

#!/usr/bin/env perl
use strict;
use warnings;



my $segfaulting_file = "s";
my $L = 0;
my $R = 7266786; #`cat $segfaulting_file | wc -l`;
my $M;
my $binary = "./filter";

while ($L < $R) {
  $M     = int(($L+$R)/2);
  # head argument for right side
  my $HL = $M;
  # tail argument for right side
  my $TL = $M-$L;
  # head argument for left  side
  my $HR = $R;
  # tail argument for left  side
  my $TR = $R-$M;

  print "M=$M L=$L R=$R\n";
  my $go_left ;
  my $go_right;
  my $cmd_R = "cat $segfaulting_file | head -$HR | tail -$TR | $binary > /dev/null;";
  my $cmd_L = "cat $segfaulting_file | head -$HL | tail -$TL | $binary > /dev/null;";

  print "\nRunning $cmd_R\n";
  `$cmd_R`;
  #`./a.out $M $R`;
  print "RETVAL=$?\n";
  $go_right = ($? > 30000); # right side caused SEGFAULT
  `rm core`;
  print "\nRunning $cmd_L\n";
  `$cmd_L`;
  print "RETVAL=$?\n";
  #`./a.out $L $M`;
  $go_left  = ($? > 30000); # left  side caused SEGFAULT
  `rm core`;

  if(  $L == $R ) {
    last;
  }elsif ( $go_left  ) {
    print "GO left  L=$L R=$R\n";
    $R = $M  ;
  }elsif ( $go_right ) {
    print "GO right L=$L R=$R\n";
    $L = $M+1;
  };
};


# the loop stopped because $L==$R==$M , so we just print out $M
print "Segfault caused by line $M\n";

【问题讨论】:

    标签: c linux perl segmentation-fault


    【解决方案1】:

    你问 Z 是为了解决 Y 是为了解决 X...停止!回到 X。你不需要日志行来找出它出现段错误的原因。您需要 源代码行 来确定它出现段错误的原因,我想你知道如何获得。从该行返回,沿着它可能采用的任何分支并战略性地放置assert()ions 以确定错误输入的来源。当您继续“消除错误”时,将重复的代码分离为函数以供重用,并且您将同时重构代码以实现模块化和稳定性。

    编辑:如果你真的想继续这些路线,你不需要 perl 脚本。您所需要的只是代码中的一个计数器,用于在解析行时对行进行计数,以及 signal 的帮助。运行以下代码时,ideone 在计数为 65535 之前不会出现段错误。希望您能看到依赖段错误来处理缓冲区溢出是多么愚蠢...

    #include <assert.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <limits.h>
    
    void segfault_handler(int sig);
    
    int counter = 0;
    
    int main(void) {
        int *fubar = malloc(0); // allocate ZERO bytes!
    
        assert(signal(SIGSEGV, segfault_handler) != SIG_ERR);
        assert(signal(SIGILL, segfault_handler) != SIG_ERR);
        assert(signal(SIGFPE, segfault_handler) != SIG_ERR);
    
        for (;;) {
            printf("Assigning to fubar[%d]\n", counter);
            fubar[counter] = INT_MIN;
            counter = counter * 2 + 1;
        }
    }
    
    void segfault_handler(int sig) {
        /* NOTE: Using printf inside a signal handler is also undefined behaviour */
        printf("Fault when counter is %d\n", counter);
        exit(EXIT_FAILURE);
    }
    

    【讨论】:

    • 这也称为XY Problem。您提出的关于 XY 问题的建议是有道理的。
    • 您过于依赖未定义行为可能导致分段违规的事实;它也可能不会导致分段违规。这不是检测 C 中错误的可靠方法。
    • @average 在这种情况下,该术语既没有被滥用也没有被滥用。与其寻求“实际解决问题”的建议,不如寻求解决发现问题的建议;您是在寻求重新发明轮子的建议吗?问问自己这个问题:其他项目如何挂钩 SIGSEGV 并转储错误消息?我会把答案放在我的答案中。
    【解决方案2】:

    检测 seg 故障最可靠的方法可能是使用forkexecwait。你 fork Perl 脚本;孩子执行二进制文件。父级从二进制文件中收集退出状态。退出状态有两个部分——如果孩子在控制下退出,则退出的值,或者如果它死于信号,则它接收到的信号。您只需检查信号分量是否为零;如果不为零,则假设这是一个段错误(尽管您可以根据需要验证其编号)。剩下的技巧是获取二进制数据以在标准输入上读取可用。您可以让孩子启动一个管道,它从中读取并将管道复制到标准输入,然后关闭管道。复杂多于真正的困难。


    概述 Perl 代码

    我需要一个崩溃的程序,所以我创建了一个 Perl 脚本crash_after_reading

    #!/usr/bin/env perl
    use POSIX;
    
    $| = 1;
    print "." while (<>);
    print "\n";
    abort;
    

    然后,您的 Perl 脚本的模拟器如下所示 (forkwait.pl):

    #!/usr/bin/env perl
    use strict;
    use warnings;
    use POSIX;
    
    my $pipeline = "cat /etc/group";
    my $pid;
    
    die "$!" if (($pid = fork) < 0);
    
    if ($pid == 0)
    {
        # Child
        open STDIN, "-|", $pipeline or die "open";
        exec "crash_after_reading";
        die "failed to exec crash_after_reading";
    }
    
    my $corpse = waitpid($pid, 0);
    printf "PID = %d; status = 0x%.4X\n", $corpse, $?;
    

    示例输出

    ..................................................................................
    PID = 92413; status = 0x0006
    

    低 8 位非零,因此子程序因信号 6 (SIGABRT) 而崩溃。如果低8位全为零,则子程序以高8位状态退出成功。

    如果您决定使用它,则需要根据您的代码进行调整。

    【讨论】:

    • 感谢您的精彩解释。你能发布一个小例子吗?谢谢
    • 我推荐采用lxop的方法。这可能会更容易。
    【解决方案3】:

    既然您可以编辑过滤程序的源代码,为什么不直接编辑它以在处理之前将每个日志文件行写入另一个文件。然后在段错误之后,查看新文件以查看当时正在处理的行。这也将比您的二进制搜索更快,后者每次都需要处理 N-1 行,而这种方法将平均处理 N/2 行(取决于违规行的位置),其中有 N日志文件中的行。

    【讨论】:

    • +1 替代 :) 二进制搜索非常快。它在大约 3-4 分钟内完成了一个 500MB 的 .gz 文件。是的,我可以将输出写入磁盘,这样我就不必再做更多的| head | tail。是的,我可以打印这些行并准确查看它何时发生段错误。这也是个好主意,可能比我的要好得多。
    • 我认为这种方法是有原因的。问题是它打印了很多行,所以我必须等待很多时间。但即便如此,它仍然可能比我使用二分搜索更快。
    • @average 您可以通过在处理一行之前每次简单地写一个点来加速它,然后计算写入的点数(使用wc 或其他),这将给出行号在日志文件中。写更少的字符可能会更快。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-31
    相关资源
    最近更新 更多