Как написать обработчик сигнала для отлова SIGSEGV?

Я хочу написать обработчик сигнала, чтобы поймать 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);

и продолжить нормальное функционирование моего кода. Я не хочу выходить из функции. При будущих записях в ту же память я хочу снова поймать сигнал и изменить права на запись, а затем записать это событие.

Вот код:

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

Проблема в том, что работает только обработчик сигнала, и я не могу вернуться к основной функции после перехвата сигнала.

6 ответов

Решение

Когда ваш обработчик сигнала возвращается (при условии, что он не вызывает exit или longjmp или что-то, что мешает его фактическому возвращению), код продолжит работу в том месте, где произошел сигнал, повторно выполнив ту же инструкцию. Поскольку в этот момент защита памяти не изменилась, он просто снова сгенерирует сигнал, и вы вернетесь в обработчик сигнала в бесконечном цикле.

Таким образом, чтобы это работало, вы должны вызвать mprotect в обработчике сигналов. К сожалению, как отмечает Стивен Шенскер, mprotect не является асинхронно безопасным, поэтому вы не можете безопасно вызывать его из обработчика сигнала. Итак, что касается POSIX, вы облажались.

К счастью, в большинстве реализаций (насколько мне известно, во всех современных вариантах UNIX и Linux) mprotect является системным вызовом, поэтому его можно безопасно вызывать из обработчика сигнала, поэтому вы можете делать большую часть того, что вам нужно. Проблема в том, что если вы хотите изменить защиту обратно после чтения, вам нужно будет сделать это в основной программе после чтения.

Другая возможность состоит в том, чтобы сделать что-то с третьим аргументом обработчику сигнала, который указывает на ОС и структуру, специфичную для арки, которая содержит информацию о том, где произошел сигнал. В 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 руководства) в сигнале обработчик.

Вы попали в ловушку, которую делают все люди, когда они впервые пытаются обработать сигналы. Ловушка? Думая, что вы действительно можете сделать что-нибудь полезное с обработчиками сигналов. Из обработчика сигнала вам разрешено вызывать только асинхронные и безопасные для повторного входа вызовы библиотеки.

См. Эту рекомендацию CERT о том, почему и список безопасных функций POSIX.

Обратите внимание, что printf(), который вы уже вызываете, отсутствует в этом списке.

Также как и mprotect. Вы не можете вызывать его из обработчика сигнала. Это может сработать, но я могу обещать, что в будущем у вас возникнут проблемы. Будьте очень осторожны с обработчиками сигналов, их сложно понять правильно!

РЕДАКТИРОВАТЬ

Поскольку в данный момент я уже работаю над переносимостью, я укажу, что вам также не следует писать в общие (то есть глобальные) переменные без принятия надлежащих мер предосторожности.

Вы можете восстановить из SIGSEGV на Linux. Также вы можете исправить ошибки сегментации в Windows (вместо сигнала вы увидите структурированное исключение). Но стандарт POSIX не гарантирует восстановления, поэтому ваш код будет очень непереносимым.

Посмотрите на libsigsegv.

Вы не должны возвращаться из обработчика сигнала, так как тогда поведение не определено. Скорее выпрыгните из него с помощью longjmp.

Это нормально, только если сигнал генерируется в асинхронно-безопасной функции. В противном случае поведение не определено, если программа когда-либо вызывает другую небезопасную асинхронную функцию. Следовательно, обработчик сигнала должен быть установлен только непосредственно перед тем, как это необходимо, и отключен как можно скорее.

На самом деле, я знаю очень мало случаев использования обработчика SIGSEGV:

  • используйте безопасную асинхронную библиотеку обратной трассировки, чтобы зарегистрировать обратную трассировку, а затем умереть.
  • в виртуальной машине, такой как JVM или CLR: проверьте, произошел ли SIGSEGV в JIT-скомпилированном коде. Если нет, умри; если это так, то генерируйте исключение для конкретного языка (не исключение C++), которое работает, потому что JIT-компилятор знал, что может произойти перехват, и генерировал соответствующие данные для разворачивания фрейма.
  • clone () и exec() отладчик (не используйте fork() - который вызывает обратные вызовы, зарегистрированные pthread_atfork()).

Наконец, обратите внимание, что любое действие, которое запускает SIGSEGV, вероятно, является UB, так как оно обращается к недействительной памяти. Однако это было бы не так, если бы сигнал был, скажем, SIGFPE.

Существует проблема компиляции с использованием ucontext_t или структура ucontext (присутствует в /usr/include/sys/ucontext.h)

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

Я думаю, разговоры о том, что из обработчиков сигналов можно вызывать только подмножество функций, — это обобщение. Это правильно, если вы не знаете, где именно произойдет недопустимый доступ к памяти — тогда это может произойти в месте, где их небезопасно вызывать (например, уже внутри одного из них). Однако если вы пишете своего рода «пробный» код и знаете, какая инструкция может дать сбой в вашем собственном коде, то я не вижу проблем с вызовом этих «запрещенных» функций, потому что вы знаете, что находитесь не в плохом месте. .

Другие вопросы по тегам