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

Какой самый простой способ отправить зашифрованное письмо?

Ubuntu Touch: определение, преимущества и правила установки вместо Android
Linux

Ubuntu Touch: определение, преимущества и правила установки вместо Android

Linux

Запуск jmap, получение которого невозможно без открытия файла сокета

×