Я хочу написать обработчик сигнала для перехвата 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:
Использовать асинхронную библиотеку, безопасную для сигналов.
В виртуальной машине, такой как JVM или CLR: проверьте, произошел ли SIGSEGV в JIT-компилированном коде. Если нет, то завершите код; если да, то выбросьте исключение, специфичное для языка (не исключение C++), которое работает, потому что JIT-компилятор знал, что ловушка может произойти, и сгенерировал соответствующие данные для разворачивания фрейма.
clone() и exec() отладчика (не используйте fork() — он вызывает обратные вызовы, зарегистрированные в pthread_atfork()).
Наконец, обратите внимание, что любое действие, вызывающее SIGSEGV, вероятно, является UB, поскольку это обращение к недопустимой памяти. Однако это было бы не так, если бы сигнал был, скажем, SIGFPE.
Linux