如何编写信号处理程序来捕获SIGSEGV?

时间:2010-04-18 18:44:53

标签: c linux system-calls signal-handling mprotect

我想写一个信号处理程序来捕获SIGSEGV。 我保护一块内存用于读取或写入

char *buffer;
char *p;
char a;
int pagesize = 4096;

mprotect(buffer,pagesize,PROT_NONE)

这可以保护从缓冲区开始的内存页大小字节的内存,防止任何读取或写入。

其次,我尝试阅读内存:

p = buffer;
a = *p 

这将生成一个SIGSEGV,我的处理程序将被调用。 到现在为止还挺好。我的问题是,一旦调用处理程序,我想通过执行

来更改内存的访问写入
mprotect(buffer,pagesize,PROT_READ);

并继续正常运行我的代码。我不想退出该功能。 在将来写入相同内存时,我想再次捕获信号并修改写入权限,然后记录该事件。

以下是the code

#include <signal.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

char *buffer;
int flag=0;

static void handler(int sig, siginfo_t *si, void *unused)
{
    printf("Got SIGSEGV at address: 0x%lx\n",(long) si->si_addr);
    printf("Implements the handler only\n");
    flag=1;
    //exit(EXIT_FAILURE);
}

int main(int argc, char *argv[])
{
    char *p; char a;
    int pagesize;
    struct sigaction sa;

    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = handler;
    if (sigaction(SIGSEGV, &sa, NULL) == -1)
        handle_error("sigaction");

    pagesize=4096;

    /* Allocate a buffer aligned on a page boundary;
       initial protection is PROT_READ | PROT_WRITE */

    buffer = memalign(pagesize, 4 * pagesize);
    if (buffer == NULL)
        handle_error("memalign");

    printf("Start of region:        0x%lx\n", (long) buffer);
    printf("Start of region:        0x%lx\n", (long) buffer+pagesize);
    printf("Start of region:        0x%lx\n", (long) buffer+2*pagesize);
    printf("Start of region:        0x%lx\n", (long) buffer+3*pagesize);
    //if (mprotect(buffer + pagesize * 0, pagesize,PROT_NONE) == -1)
    if (mprotect(buffer + pagesize * 0, pagesize,PROT_NONE) == -1)
        handle_error("mprotect");

    //for (p = buffer ; ; )
    if(flag==0)
    {
        p = buffer+pagesize/2;
        printf("It comes here before reading memory\n");
        a = *p; //trying to read the memory
        printf("It comes here after reading memory\n");
    }
    else
    {
        if (mprotect(buffer + pagesize * 0, pagesize,PROT_READ) == -1)
        handle_error("mprotect");
        a = *p;
        printf("Now i can read the memory\n");

    }
/*  for (p = buffer;p<=buffer+4*pagesize ;p++ ) 
    {
        //a = *(p);
        *(p) = 'a';
        printf("Writing at address %p\n",p);

    }*/

    printf("Loop completed\n");     /* Should never happen */
    exit(EXIT_SUCCESS);
}

问题是只有信号处理程序运行,并且在捕获信号后我无法返回主函数。

5 个答案:

答案 0 :(得分:64)

当你的信号处理程序返回时(假设它没有调用exit或longjmp或阻止它实际返回的东西),代码将在信号发生时继续,重新执行相同的指令。由于此时内存保护尚未更改,它只会再次抛出信号,并且您将在无限循环中返回信号处理程序。

为了使其工作,你必须在信号处理程序中调用mprotect。不幸的是,正如Steven Schansker所说,mprotect不是异步安全的,所以你不能安全地从信号处理程序中调用它。所以,就POSIX而言,你已经搞砸了。

幸运的是,在大多数实现中(据我所知,所有现代UNIX和Linux版本都是),mprotect是一个系统调用,safe to call from within a signal handler也是如此,因此您可以完成所需的大部分工作。问题是,如果你想在阅读后更改保护,你必须在阅读后在主程序中这样做。

另一种可能性是对信号处理程序的第三个参数执行某些操作,该参数指向OS和特定于结构的结构,该结构包含有关信号发生位置的信息。在Linux上,这是一个 ucontext 结构,其中包含有关$ PC地址和信号发生的其他寄存器内容的机器特定信息。如果你修改它,你可以改变信号处理程序返回的位置,这样你就可以将$ PC更改为故障指令之后,这样在处理程序返回后它就不会重新执行。这样做是非常棘手的(也是非便携式的)。

修改

ucontext结构在<ucontext.h>中定义。在ucontext字段uc_mcontext中包含机器上下文,在 中,数组gregs包含通用寄存器上下文。所以在你的信号处理程序中:

ucontext *u = (ucontext *)unused;
unsigned char *pc = (unsigned char *)u->uc_mcontext.gregs[REG_RIP];

会给你发生异常的电脑。你可以阅读它来弄清楚它是什么指令 那是故障,并做了不同的事情。

就信号处理程序中调用mprotect的可移植性而言,遵循SVID规范或BSD4规范的任何系统都应该是安全的 - 它们允许调用任何系统调用(本手册第2部分中的任何内容)在信号处理程序中。

答案 1 :(得分:24)

你陷入了所有人第一次尝试处理信号时所做的陷阱。陷阱?认为你可以用信号处理程序实际做任何有用的。从信号处理程序,您只能调用异步和可重入安全的库调用。

请参阅this CERT advisory关于为什么以及安全的POSIX函数列表。

请注意,您已经在调用的printf()不在该列表中。

也不是mprotect。你不能从信号处理程序中调用它。 可能工作,但我保证你会遇到问题。对信号处理程序要非常小心,它们很难做到正确!

修改

由于我现在已经是一个便携式douchebag,我会指出你also shouldn't write to shared (i.e. global) variables没有采取适当的预防措施。

答案 2 :(得分:12)

您可以从linux上的SIGSEGV恢复。您还可以从Windows上的分段错误中恢复(您将看到结构化异常而不是信号)。但是the POSIX standard doesn't guarantee recovery,因此您的代码将非常不便携。

看看libsigsegv

答案 3 :(得分:4)

您不应该从信号处理程序返回,因为行为未定义。相反,用longjmp跳出它。

只有在异步信号安全功能中生成信号时才可以。否则,如果程序曾调用另一个异步信号不安全函数,则行为未定义。因此,信号处理程序只应在必要之前立即建立,并尽快解除。

事实上,我知道很少使用SIGSEGV处理程序:

  • 使用异步信号安全回溯库来记录回溯,然后死掉。
  • 在诸如JVM或CLR之类的VM中:检查是否在JIT编译的代码中发生了SIGSEGV。如果没有,死;如果是这样,那么抛出一个特定于语言的异常(一个C ++异常),这是有效的,因为JIT编译器知道陷阱可能发生并生成适当的帧展开数据。
  • clone()和exec()一个调试器(使用fork() - 调用pthread_atfork()注册的回调)。

最后,请注意,触发SIGSEGV的任何操作都可能是UB,因为这是访问无效内存。但是,如果信号是SIGFPE,则不会出现这种情况。

答案 4 :(得分:0)

使用ucontext_t或结构ucontext存在编译问题(存在于/usr/include/sys/ucontext.h

http://www.mail-archive.com/arch-general@archlinux.org/msg13853.html