76 votes

Ecrire un gestionnaire de signal pour attraper SIGSEGV

Je veux écrire un gestionnaire de signal pour attraper SIGSEGV. Tout d'abord, je protège un bloc de mémoire pour la lecture ou l'écriture en utilisant la fonction

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

"  mprotect(buffer,pagesize,PROT_NONE) "

Ce que cela va faire, c'est protéger la mémoire à partir du tampon jusqu'à la taille de la page pour toute lecture ou écriture.

Ensuite, je vais essayer de lire la mémoire en faisant quelque chose du genre

  p = buffer;
  a = *p 

Cela va générer un SIGSEGV et j'ai initialisé un gestionnaire pour cela. Le gestionnaire sera appelé. Jusqu'ici tout va bien. Maintenant, le problème auquel je suis confronté est qu'une fois que le gestionnaire est appelé, je veux changer l'accès en écriture de la mémoire en faisant

mprotect(buffer, pagesize,PROT_READ);

et continuer mon fonctionnement normal du code. Je ne veux pas quitter la fonction. Lors de futures écritures dans la même mémoire, je veux à nouveau capter le signal et modifier les droits d'écriture, puis tenir compte de cet événement.

Voici le code que j'essaie d'utiliser :

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

Le problème auquel je suis confronté est que seul le gestionnaire de signal est en cours d'exécution et je ne suis pas en mesure de revenir à la fonction principale après avoir attrapé le signal .

Toute aide dans ce domaine sera grandement appréciée.

Merci d'avance Aditya

2 votes

Merci Nos pour l'édition. J'apprécie. J'ai besoin de passer un peu de temps pour apprendre à éditer mes questions

1 votes

Lors de la compilation, activez toujours tous les avertissements, puis corrigez ces avertissements. (pour gcc au moins l'utiliser : -Wall -Wextra -pedantic J'utilise aussi : -Wconversion -std=gnu99 ) Le compilateur vous le dira : 1) le paramètre argc inutilisé 2) paramètre argv inutilisé (suggérer d'utiliser la signature main() de : int main( void ) 3) variable locale p utilisé dans le else sans être initialisé. 4) paramètre unused inutilisés, suggérer : ajouter une déclaration : (void)unused; comme première ligne de cette fonction. 5) variable locale a fixé mais non utilisé.

1 votes

N'utilisez JAMAIS printf() dans un gestionnaire de signaux ! La fonction write() peut être utilisé, mais le mieux est de ne pas faire d'entrées/sorties dans un gestionnaire de signaux, il suffit de définir un drapeau et de laisser la ligne de code principale vérifier ce drapeau.

77voto

Chris Dodd Points 39013

Lorsque votre gestionnaire de signaux revient (en supposant qu'il n'appelle pas exit ou longjmp ou quelque chose qui l'empêche de revenir réellement), le code continuera au point où le signal s'est produit, en réexécutant la même instruction. Étant donné qu'à ce stade, la protection de la mémoire n'a pas été modifiée, le signal sera simplement relancé, et vous serez de retour dans votre gestionnaire de signaux dans une boucle infinie.

Pour que cela fonctionne, il faut donc appeler mprotect dans le gestionnaire de signaux. Malheureusement, comme le note Steven Schansker, mprotect n'est pas async-safe, donc vous ne pouvez pas l'appeler en toute sécurité depuis le gestionnaire de signaux. Donc, en ce qui concerne POSIX, vous êtes foutu.

Heureusement, sur la plupart des implémentations (toutes les variantes modernes d'UNIX et de Linux pour autant que je sache), mprotect est un appel système, et peut donc être appelé en toute sécurité à partir d'un gestionnaire de signaux, ce qui vous permet de faire la plupart des choses que vous voulez. Le problème est que si vous voulez changer les protections après la lecture, vous devrez le faire dans le programme principal après la lecture.

Une autre possibilité est de faire quelque chose avec le troisième argument du gestionnaire de signal, qui pointe vers une structure spécifique au système d'exploitation et à l'architecture qui contient des informations sur l'endroit où le signal s'est produit. Sous Linux, il s'agit d'une structure ucontexte qui contient des informations spécifiques à la machine concernant l'adresse $PC et d'autres contenus de registre où le signal s'est produit. Si vous modifiez cette structure, vous changez l'endroit où le gestionnaire du signal reviendra, et vous pouvez donc changer l'adresse $PC pour qu'elle soit juste après l'instruction d'erreur afin qu'elle ne soit pas ré-exécutée après le retour du gestionnaire. C'est très délicat à mettre en place (et non portable aussi).

modifier

Le site ucontext est définie dans <ucontext.h> . Au sein de la ucontext le champ uc_mcontext contient le contexte de la machine, et dans que le tableau gregs contient le contexte général du registre. Donc, dans votre gestionnaire de signaux :

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

vous donnera le pc où l'exception s'est produite. Vous pouvez le lire pour découvrir quelle instruction a été l'instruction qui a fait défaut, et faire quelque chose de différent.

En ce qui concerne la portabilité de l'appel à mprotect dans le gestionnaire de signaux, tout système qui suit la spécification SVID ou la spécification BSD4 devrait être sûr -- ils permettent d'appeler n'importe quel appel système (n'importe quoi dans la section 2 du manuel) dans un gestionnaire de signaux.

0 votes

En effet, vous pouvez effectuer l'accès à la mémoire au nom du programme (comme une VM) et ensuite mettre à jour le pointeur d'instruction. Appeler mprotect est définitivement plus facile.

0 votes

Bonjour Chris, Vous m'avez donné des informations utiles. Merci pour cela. Pouvez-vous me dire comment je peux lire l'information dans la structure ucontext (3ème argument et changer le $PC) . Je suis curieux de le savoir.

0 votes

Ben Voigt, je n'ai pas compris clairement ce que vous dites, je vous demande d'être un peu plus précis.

25voto

Steven Schlansker Points 17463

Vous êtes tombé dans le piège que font tous les gens lorsqu'ils essaient pour la première fois de gérer les signaux. Le piège ? Penser que vous pouvez réellement faire quelque chose utile avec des gestionnaires de signaux. À partir d'un gestionnaire de signaux, vous ne pouvez appeler que des appels de bibliothèque asynchrones et à sécurité réentrante.

Voir cet avis du CERT pour expliquer pourquoi et une liste des fonctions POSIX qui sont sûres.

Notez que printf(), que vous appelez déjà, ne figure pas dans cette liste.

Pas plus que mprotect. Vous n'êtes pas autorisé à l'appeler depuis un gestionnaire de signaux. Il s'agit de pourrait fonctionne, mais je peux vous promettre que vous rencontrerez des problèmes par la suite. Soyez très prudent avec les gestionnaires de signaux, ils sont délicats à mettre en place !

EDIT

Puisque je suis déjà en train d'être un connard de la portabilité, je vais vous signaler que vous ne doit pas non plus écrire dans les variables partagées (c'est-à-dire globales). sans prendre les précautions nécessaires.

1 votes

Si je ne peux pas faire quoi que ce soit d'utile dans le gestionnaire de signal, je serai d'accord si je peux mettre à jour quelques compteurs à l'intérieur et revenir à main et exécuter normalement mon code, est-ce possible ?

0 votes

En citant l'avis du CERT, "ils peuvent appeler d'autres fonctions à condition que toutes les implémentations vers lesquelles le code est porté garantissent que ces fonctions sont asynchrones-sûres". Sous linux, cela inclut beaucoup plus de fonctions.

0 votes

Bien sûr, mais il faut juste être conscient du problème ! Je ne peux pas nommer de mémoire les fonctions qui sont ou ne sont pas sûres pour les signaux, et je doute que beaucoup le puissent !

13voto

Ben Voigt Points 151460

Vous pouvez récupérer les SIGSEGV sous Linux. Vous pouvez également récupérer des défauts de segmentation sous Windows (vous verrez une exception structurée au lieu d'un signal). Mais la norme POSIX ne garantit pas la récupération Votre code sera donc très peu portable.

Jetez un coup d'œil à libsigsegv .

5voto

Demetri Points 715

Vous ne devez pas retourner du gestionnaire de signal, car le comportement est alors indéfini. Il faut plutôt en sortir avec longjmp.

Cela n'est acceptable que si le signal est généré dans une fonction async-signal-safe. Sinon, le comportement est indéfini si le programme appelle un jour une autre fonction async-signal-safe. Par conséquent, le gestionnaire de signaux ne doit être établi qu'immédiatement avant qu'il ne soit nécessaire, et désétabli dès que possible.

Enfin, notez que toute action qui déclenche SIGSEGV est probablement UB, car elle accède à une mémoire non valide. Cependant, ce ne serait pas le cas si le signal était, disons, SIGFPE.

0 votes

Mmap() & mprotect() sont souvent utilisés en combinaison avec un gestionnaire SIGSEGV pour piéger les accès mémoire à certaines régions, et le comportement est défini dans ce cas car l'accès mémoire n'est pas invalide, mais protégé.

0voto

shreshtha Points 1

Il y a un problème de compilation en utilisant ucontext_t ou struct ucontext (présent dans /usr/include/sys/ucontext.h )

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X