C语言 如何编写一个信号处理程序来捕获SIGSEGV?

wwtsj6pe  于 2023-04-05  发布在  其他
关注(0)|答案(6)|浏览(198)

我想写一个信号处理程序来捕获SIGSEGV。我使用

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

mprotect(buffer,pagesize,PROT_NONE)

这可以保护从buffer开始的内存的pagesize字节不受任何读取或写入的影响。
第二,我试着读一读记忆:

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);
}

问题是,只有信号处理程序运行,我不能在捕获信号后返回到主函数。

disho6za

disho6za1#

当你的信号处理器返回时(假设它没有调用exit或longjmp或其他阻止它实际返回的东西),代码将在信号发生的地方继续,重新执行相同的指令。由于此时内存保护没有改变,它将再次抛出信号,你将在无限循环中回到信号处理器。
因此,要使其工作,您必须在信号处理程序中调用mprotect。不幸的是,正如Steven Schansker所指出的,mprotect不是异步安全的,因此您不能安全地从信号处理程序调用它。因此,就POSIX而言,您完蛋了。
幸运的是,在大多数实现中(据我所知,都是现代的UNIX和Linux变体),mprotect是一个系统调用,safe to call from within a signal handler也是,所以你可以做你想做的大部分事情。问题是,如果你想在读取后修改保护,你必须在读取后在主程序中这样做。
另一种可能性是对信号处理程序的第三个参数做一些事情,该参数指向包含信号发生位置信息的OS和arch特定结构。在Linux上,这是一个 ucontext 结构,它包含有关$PC地址和信号发生位置的其他寄存器内容的机器特定信息。如果修改此参数,则更改信号处理程序将返回的位置,所以你可以将$PC修改为刚好在出错指令之后,这样它就不会在处理程序返回后重新执行。

编辑

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

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

将给予你发生异常的pc。你可以读它来弄清楚是什么指令出错了,然后做一些不同的事情。
就在信号处理程序中调用mprotect的可移植性而言,任何遵循SVID规范或BSD 4规范的系统都应该是安全的--它们允许在信号处理程序中调用任何系统调用(手册第2节中的任何内容)。

eoxn13cs

eoxn13cs2#

你已经落入了所有人在第一次尝试处理信号时都会遇到的陷阱。陷阱?认为你可以用信号处理程序做任何有用的事情。从信号处理程序中,你只允许调用异步和可重入安全的库调用。
请参阅this CERT advisory以了解原因以及安全的POSIX函数列表。
请注意,您已经调用的printf()不在该列表中。
mprotect也不是。你不允许从信号处理程序调用它。它可能工作,但我可以保证你会遇到问题的道路上。要真正小心信号处理程序,他们是棘手的得到正确的!

编辑

因为我现在已经是一个便携性的混蛋了,我会指出你没有采取适当的预防措施。

hgc7kmma

hgc7kmma3#

你可以在linux上从SIGSEGV中恢复。你也可以在Windows上从分段错误中恢复(你会看到一个结构化异常而不是信号)。但是the POSIX standard doesn't guarantee recovery,所以你的代码将是非常不可移植的。
看看libsigsegv

rsl1atfo

rsl1atfo4#

你不应该从信号处理器返回,因为那样的话行为是未定义的。相反,用longjmp跳出它。
只有当信号是在async-signal-safe函数中生成时才可以。否则,如果程序调用另一个async-signal-unsafe函数,则行为是未定义的。因此,信号处理程序应该只在需要之前立即建立,并尽快取消。
事实上,我知道SIGSEGV处理程序的用途很少:

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

最后,请注意,任何触发SIGSEGV的动作都可能是UB,因为这是在访问无效内存。然而,如果信号是SIGFPE,情况就不是这样了。

jqjz2hbq

jqjz2hbq5#

使用ucontext_t或结构ucontext(存在于/usr/include/sys/ucontext.h中)时存在编译问题
http://www.mail-archive.com/arch-general@archlinux.org/msg13853.html

bqujaahr

bqujaahr6#

我认为,关于只能从信号处理程序调用函数的子集的讨论是一种概括。如果你不知道无效内存访问将在哪里发生,那么它可能发生在调用它们不安全的地方,这是正确的。但是,如果您正在编写某种“探测”代码,并且您知道哪个指令可能会失败,那么在您自己的代码中,那么我认为调用那些“禁止的”函数没有问题,因为你知道你并没有处于一个糟糕的位置。

相关问题