【问题标题】:Coming back to life after Segmentation Violation在分割违规后恢复生机
【发布时间】:2011-03-18 12:14:16
【问题描述】:

如果出现 Segmentation Fault 错误,是否可以恢复 C 程序的正常执行流程?

struct A {
    int x;
};
A* a = 0;

a->x = 123; // this is where segmentation violation occurs

// after handling the error I want to get back here:
printf("normal execution");
// the rest of my source code....

我想要一种类似于 Java、C# 等中存在的 NullPointerException 的机制。

注意:请不要告诉我 C++ 中有异常处理机制,因为我知道,不要告诉我应该在赋值之前检查每个指针等。

我真正想要实现的是回到上面示例中的正常执行流程。我知道可以使用 POSIX 信号执行一些操作。它应该是什么样子?其他想法?

【问题讨论】:

  • 遇到“空指针异常”后,您希望将控制权返回到哪里?
  • 最好将控制权返回到导致 sigsegv 的指令之后的第一条指令,但任何其他“安全”的地方都值得赞赏。
  • SIGSEGV 后没有“正常执行流程”。错误可能发生在 if (*p > 12) 之类的地方,并且由于 *p 未定义,您的程序将继续运行,就像您编写了 if (random() & 1 == 0)
  • @Marc,你不应该这样做的原因是你真的不知道你的程序在段错误之后的状态是什么。您的堆栈可能已损坏;您的堆可能已损坏;您的全局变量可能会被压缩;你的指令指针可能在梦幻岛;您的安全数据可能在互联网上传播;你的 CPU 可能着火了。你会后悔在一个段错误之后继续尝试,好像什么都没发生一样。处理调用有缺陷的第三方代码的正确方法是生成一个新进程来调用它——需要做更多的工作,但唯一可靠的是远程。
  • 我猜你希望“你不能重写的函数”中的错误对你自己的代码影响很小或没有影响。这种推理的问题在于,正如你所说,你没有消息来源。据您所知,它们可能正在您自己分配的内存中写入,擦除您自己的数据甚至您的堆栈。如果发生这种情况,即使使用您自己的处理程序也不会将您的进程内存恢复到正确的状态。因此,忽略 他们的 代码产生的分段错误只会延迟到从 your 代码引发的下一个错误......我很高兴我没有遇到你的情况...

标签: c exception-handling signals segmentation-fault


【解决方案1】:

您可以使用信号处理程序捕获分段错误,并决定继续执行程序(风险自负)。

信号名称是SIGSEGV

您必须使用 sigaction() 函数,来自 signal.h 标头。

基本上,它的工作方式如下:

struct sigaction sa1;
struct sigaction sa2;

sa1.sa_handler = your_handler_func;
sa1.sa_flags   = 0;
sigemptyset( &sa1.sa_mask );

sigaction( SIGSEGV, &sa1, &sa2 );

这是处理函数的原型:

void your_handler_func( int id );

如您所见,您无需返回。程序的执行将继续,除非您决定自己从处理程序中停止它。

【讨论】:

  • 我知道,但它应该是什么样子?简单地说: void my_handler() { return; } ??
  • 简单地返回无济于事;相同的指令将再次执行并再次崩溃。一种更可靠的做事方式(但仍然很难看,不推荐!)是从信号处理程序中将longjmp(或者更好的是siglongjump)转移到已知安全的位置。
  • @R。我认为执行相同的insn或下一个insn是否取决于CPU + OS?至少,SIGFPE IIRC 是这样的。
  • @ninjalj,在未定义的行为领域中,这是非常糟糕的,但我说的是它在实践中是如何完成的。
【解决方案2】:

调用它,当发生段错误时,您的代码将执行 segv_handler 然后继续回到原来的位置。

void segv_handler(int)
{
  // Do what you want here
}

signal(SIGSEGV, segv_handler);

【讨论】:

  • 我可以让身体空着吗?会不会回到出错的地方?
  • 我刚刚编辑添加了 cmets。是的,它可以是空的,并且会返回到异常发生的地方。
  • 它将继续回到原来的位置,这是导致段错误的指令,而不是段错误的原因之后。所以段错误会再次发生,你的处理程序会被再次调用,依此类推。
  • 哦,是的......因此,不要出现段错误,应该没问题;-)
【解决方案3】:

在 POSIX 中,当您执行此操作时,您的进程将收到 SIGSEGV。默认处理程序只会使您的程序崩溃。您可以使用 signal() 调用添加自己的处理程序。您可以通过自己处理信号来实现您喜欢的任何行为。

【讨论】:

    【解决方案4】:

    “所有事情都是允许的,但并非所有事情都是有益的” - 通常一个段错误是有充分理由的游戏结束......比选择保持数据持久化(数据库或在至少一个文件系统)并使其能够以这种方式从中断的地方继续。这将为您提供更好的数据可靠性。

    【讨论】:

    • 虽然这通常是正确的,但在某些特殊情况下,处理 SIGSEGV 的黑客可以说是值得的。我能想到的一个例子是,当一个极其紧凑的、性能关键的循环使用从潜在不受信任的来源中提取的数字用作写入索引并且无法进行边界检查时。只要你能限定潜在的越界写入范围,你就可以mmap数据并映射与之相邻的只读页面,让cpu为你捕获并报告越界写入。是的,它很丑,但我知道 MPlayer 的 libmpeg2 变体曾经这样做过。 :-)
    • 我肯定会承认这一点,但你是对的,它很丑!如果有错误,修复起来会很痛苦。
    【解决方案5】:

    这本glib 手册让您清楚地了解如何编写信号处理程序。

    A signal handler is just a function that you compile together with the rest
    of the program. Instead of directly invoking the function, you use signal 
    or sigaction to tell the operating system to call it when a signal arrives.
    This is known as establishing the handler.
    

    在您的情况下,您将不得不等待 SIGSEGV 指示分段错误。其他信号列表见here

    信号处理程序大致分为两大类

    1. 您可以让处理函数注意到信号到达通过调整一些 全局数据结构,然后正常返回。
    2. 您可以让处理函数终止程序或转移 控制到可以从引起信号的情况中恢复的程度。

    SIGSEGV 出现程序错误信号

    【讨论】:

      【解决方案6】:
      #include <unistd.h>
      #include <stdio.h>
      #include <sys/types.h>
      #include <sys/mman.h>
      #include <signal.h>
      #include <stdlib.h>
      #include <ucontext.h>
      
      void safe_func(void)
      {
          puts("Safe now ?");
          exit(0); //can't return to main, it's where the segfault occured.
      }
      
      void
      handler (int cause, siginfo_t * info, void *uap)
      {
        //For test. Never ever call stdio functions in a signal handler otherwise*/
        printf ("SIGSEGV raised at address %p\n", info->si_addr);
        ucontext_t *context = uap;
        /*On my particular system, compiled with gcc -O2, the offending instruction
        generated for "*f = 16;" is 6 bytes. Lets try to set the instruction
        pointer to the next instruction (general register 14 is EIP, on linux x86) */
        context->uc_mcontext.gregs[14] += 6; 
        //alternativly, try to jump to a "safe place"
        //context->uc_mcontext.gregs[14] = (unsigned int)safe_func;
      }
      
      int
      main (int argc, char *argv[])
      {
        struct sigaction sa;
        sa.sa_sigaction = handler;
        int *f = NULL;
        sigemptyset (&sa.sa_mask);
        sa.sa_flags = SA_SIGINFO;
        if (sigaction (SIGSEGV, &sa, 0)) {
            perror ("sigaction");
            exit(1);
        }
        //cause a segfault
        *f = 16; 
        puts("Still Alive");
        return 0;
      }
      
      $ ./a.out
      SIGSEGV raised at address (nil)
      Still Alive
      

      如果我在生产代码中看到这样的东西,我会用球棒击败某人,但这是一个丑陋的,有趣的黑客。您将不知道段错误是否损坏了您的某些数据,您将无法恢复并且知道现在一切正常,没有可移植的方式来执行此操作。您可以做的唯一稍微理智的事情是尝试记录错误(直接使用 write(),而不是任何 stdio 函数 - 它们不是信号安全的),也许重新启动程序。对于这些情况,您最好编写一个监控子进程退出、记录并启动新子进程的超级监督进程。

      【讨论】:

      • 您应该修改 EIP (x86 32bit) 或 RIP 寄存器 (x86_64)。它们位于 uc_mcontext.gregs[] 中的不同偏移处。因此,如果在#ifdef __WORDSIZE == 64 下,请使用 gregs[REG_RIP] 而不是 gregs[14],否则使用 gregs[REG_EIP]
      • 至少我的 GCC 编译了代码: //cause a segfault *f = 16; puts("还活着");进入以下程序集: movl $0, 0 ud2 因此编译器假定空指针写入将变坏并且永远不会返回。此后它甚至完全优化了“看跌期权”!这可以通过替换来解决: int *f = NULL;通过函数调用,(编译器无法预测)返回一个空指针。例如,使用: int *f = (int *) fopen("/nonexisting/path", "bad-mode");
      【解决方案7】:

      查看 R. 对 MacMade 答案的评论。

      扩展他所说的内容,(在处理 SIGSEV,或者,对于这种情况,SIGFPE,CPU+OS 可以将您返回到有问题的 insn)这里是我对除以零处理的测试:

      #include <stdio.h>
      #include <limits.h>
      #include <string.h>
      #include <signal.h>
      #include <setjmp.h>
      
      static jmp_buf  context;
      
      static void sig_handler(int signo)
      {
          /* XXX: don't do this, not reentrant */
          printf("Got SIGFPE\n");
      
          /* avoid infinite loop */
          longjmp(context, 1);
      }
      
      int main()
      {
          int a;
          struct sigaction sa;
      
          memset(&sa, 0, sizeof(struct sigaction));
          sa.sa_handler = sig_handler;
          sa.sa_flags = SA_RESTART;
          sigaction(SIGFPE, &sa, NULL);
      
          if (setjmp(context)) {
                  /* If this one was on setjmp's block,
                   * it would need to be volatile, to
                   * make sure the compiler reloads it.
                   */
                  sigset_t ss;
      
                  /* Make sure to unblock SIGFPE, according to POSIX it
                   * gets blocked when calling its signal handler.
                   * sigsetjmp()/siglongjmp would make this unnecessary.
                   */
                  sigemptyset(&ss);
                  sigaddset(&ss, SIGFPE);
                  sigprocmask(SIG_UNBLOCK, &ss, NULL);
      
                  goto skip;
          }
      
          a = 10 / 0;
      skip:
          printf("Exiting\n");
      
          return 0;
      }
      

      【讨论】:

      • sigemptysetsigprocmask 等也不是 ISO C。一旦你接触到花哨的信号技巧,你就已经超出了普通 C 的领域并进入了 POSIX,所以你最好继续使用sigsetjmp 并省去麻烦。
      【解决方案8】:

      不,从任何逻辑意义上讲,都不可能在分段错误后恢复正常执行。您的程序只是试图取消引用空指针。如果您的程序期望的东西没有,您将如何正常进行?这是一个编程错误,唯一安全的做法就是退出。

      考虑分段错误的一些可能原因:

      • 您忘记为指针分配合法值
      • 指针已被覆盖,可能是因为您正在访问已释放的堆内存
      • 一个错误损坏了堆
      • 一个错误损坏了堆栈
      • 恶意第三方正在尝试利用缓冲区溢出漏洞
      • malloc 返回 null,因为内存不足

      只有在第一种情况下,你才可能有任何合理的期望

      如果您有一个要取消引用的指针,但它可能合法地为空,您必须在尝试取消引用之前对其进行测试。我知道你不想让我告诉你,但这是正确的答案,太难了。

      编辑:这里有一个例子来说明为什么你在取消引用空指针后绝对不想继续执行下一条指令:

      void foobarMyProcess(struct SomeStruct* structPtr)
      {
          char* aBuffer = structPtr->aBigBufferWithLotsOfSpace; // if structPtr is NULL, will SIGSEGV
          //
          // if you SIGSEGV and come back to here, at this point aBuffer contains whatever garbage was in memory at the point
          // where the stack frame was created
          //
          strcpy(aBuffer, "Some longish string");  // You've just written the string to some random location in your address space
                                                   // good luck with that!
      
      }
      

      【讨论】:

      • +1 用于提出想要做错事的问题。 :-)
      • 感谢示例,但​​是会执行此复制操作吗?我声称操作系统不允许这样做,而是发送 SIGSEGV。更重要的是,赋值并不是唯一违反内存的操作。阅读记忆不会造成这种威胁。
      • @MarcAnderson:这不是真的。读操作也可能导致 SIGSEGV。
      • 我说过它们不会导致 SIGSEGV 吗?我提到的威胁适用于覆盖无效的内存位置。
      • @MarcAnderson:可能会或可能不会执行复制操作。从第一个 SIGSEGV 恢复后,aBuffer 将包含一个随机值。如果该随机值指向受保护的内存,例如为 0 或指向只读段,它将引发另一个 SIGSEGV。但是,如果它恰好指向堆栈,您将覆盖一两个堆栈帧,或者如果它指向堆,您将破坏堆。
      【解决方案9】:

      您可以使用 SetUnhandledExceptionFilter() 函数(在 Windows 中),但即使要能够跳过“非法”指令,您也需要能够解码一些汇编器操作码。而且,正如glowcoder所说,即使它会在运行时“注释掉”产生段错误的指令,原始程序逻辑会留下什么(如果可以这样调用的话)? 一切皆有可能,但不代表一定要做。

      【讨论】:

        【解决方案10】:

        没有有意义的方法可以从 SIGSEGV 中恢复,除非您确切知道是什么原因造成的,而且在标准 C 中也没有办法做到这一点。在诸如 C-VM 之类的仪表化环境中(可以想象)可能是可能的( ?)。对于所有程序错误信号也是如此;如果您尝试阻止/忽略它们,或建立正常返回的处理程序,您的程序可能会在它们发生时严重中断,除非它们可能是由raisekill 生成的。

        帮自己一个忙,并考虑错误情况。

        【讨论】:

          【解决方案11】:

          很遗憾,在这种情况下您不能这样做。有缺陷的函数具有未定义的行为,可能会破坏程序的状态。

          您可以做的是在新进程中运行这些函数。如果此进程终止并返回指示 SIGSEGV 的返回码,则您知道它已失败。

          你也可以自己重写函数。

          【讨论】:

            【解决方案12】:

            我可以看到从分段违规中恢复的案例,如果您在循环中处理事件并且其中一个事件导致分段违规,那么您只想跳过此事件,继续处理剩余的事件。在我看来,Segmentation Violation 与 Java 中的 NullPointerExceptions 非常相似。是的,在其中任何一个之后,状态都会不一致且未知,但是在某些情况下,您希望处理这种情况并继续。例如,在 Algo 交易中,您可以暂停订单的执行并允许交易者手动接管,而不会导致整个系统崩溃并破坏所有其他订单。

            【讨论】:

              【解决方案13】:

              最好的解决方案是以这种方式收件箱每个不安全的访问:

              #include <iostream>
              #include <signal.h>
              #include <setjmp.h>
              static jmp_buf buf;
              int counter = 0;
              void signal_handler(int)
              {
                   longjmp(buf,0);
              }
              int main()
              {
                  signal(SIGSEGV,signal_handler);
                  setjmp(buf);
                  if(counter++ == 0){ // if we did'nt try before
                  *(int*)(0x1215) = 10;  // access an other process's memory
                  }
                  std::cout<<"i am alive !!"<<std::endl; // we will get into here in any case
                  system("pause");
               return 0;   
              }
              

              您的程序在几乎所有操作系统中都不会崩溃

              【讨论】:

              • 这是 C,不是 C++
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-05-05
              • 1970-01-01
              • 2020-12-24
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多