Linux

Как написать обработчик сигнала для перехвата 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("Выполняет обработчик 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;

 

    /* Выделите буфер, выровненный по границе страницы;

       начальная защита - 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("Это приходит сюда перед чтением памяти\n");

        a = *p; //попытка прочитать память

        printf("Это приходит сюда после чтения памяти\n");

    } else {

        if (mprotect(buffer + pagesize * 0, pagesize,PROT_READ) == -1)

        handle_error("mprotect");

        a = *p;

        printf("Теперь я могу читать память\n");

    }

/*  for (p = buffer;p<=buffer+4*pagesize ;p++ )  {

        //a = *(p);

        *(p) = 'a';

        printf("Запись по адресу %p\n",p);

    }*/

    printf("Цикл завершен\n"); /* Этого никогда не должно произойти */

    exit(EXIT_SUCCESS);

}

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

 

Ответ 1

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

Поэтому, чтобы заставить его работать, вы должны вызвать mprotect в обработчике сигнала. К сожалению, mprotect не является async-safe, поэтому вы не можете безопасно вызвать его из обработчика сигналов. Так что, с точки зрения 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];

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

Что касается переносимости вызова mprotect в обработчике сигналов, то любая система, которая следует спецификации SVID или BSD4, должна быть безопасной они позволяют вызывать любой системный вызов (все, что указано в руководстве, 2 раздел) в обработчике сигналов.

 

Ответ 2

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

 О причинах и списке безопасных функций POSIX читайте в списке CERT.

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

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

 

Ответ 3

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

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

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

  1. Использовать асинхронную библиотеку, безопасную для сигналов.

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

  3. clone() и exec() отладчика (не используйте fork() он вызывает обратные вызовы, зарегистрированные в pthread_atfork()).

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

 

Схожие статьи

Linux

Определение переменной с экспортом или без него

Linux

Как получить информацию о только что запущенном процессе

Linux

Как лучше всего послать сигнал всем членам группы процессов

Мониторинг процессов Linux: топовые инструменты и какие сложности могут быть
Linux

Мониторинг процессов Linux: топовые инструменты и какие сложности могут быть