¿Cómo detectar una falla de segmentación en Linux?

83

Necesito detectar fallas de segmentación en operaciones de limpieza de bibliotecas de terceros. Esto sucede a veces justo antes de que salga mi programa y no puedo solucionar la verdadera razón de esto. En la programación de Windows, podría hacer esto con __try - __catch. ¿Existe una forma multiplataforma o específica de plataforma para hacer lo mismo? Necesito esto en Linux, gcc.

Alex F
fuente
La falla de segmentación siempre es causada por un error que podría ser realmente difícil de detectar. Solo encuentro uno que aparece al azar. Cada archivo tiene 500 millones de puntos de datos. Aproximadamente cada 10-15 archivos, aparece este error de segmentación. Estaba usando subprocesos múltiples, cola sin bloqueos, etc. Gestión de trabajos bastante complicada. Al final, es un objeto que creé, el std :: moved () en otra estructura de datos. Localmente estaba usando este objeto después de la mudanza. Por alguna razón, C ++ está bien con esto. Pero el segfault aparecerá con seguridad en algún momento.
Kemin Zhou

Respuestas:

78

En Linux también podemos tener estos como excepciones.

Normalmente, cuando su programa realiza una falla de segmentación, se envía una SIGSEGVseñal. Puede configurar su propio controlador para esta señal y mitigar las consecuencias. Por supuesto, debe estar realmente seguro de que puede recuperarse de la situación. En su caso, creo que debería depurar su código.

Volvamos al tema. Recientemente encontré una biblioteca ( manual breve ) que transforma tales señales en excepciones, por lo que puede escribir código como este:

try
{
    *(int*) 0 = 0;
}
catch (std::exception& e)
{
    std::cerr << "Exception caught : " << e.what() << std::endl;
}

Sin embargo, no lo comprobé. Funciona en mi caja Gentoo x86-64. Tiene un backend específico de la plataforma (tomado de la implementación de java de gcc), por lo que puede funcionar en muchas plataformas. Solo es compatible con x86 y x86-64 de fábrica, pero puede obtener backends de libjava, que reside en gcc sources.

P Shved
fuente
16
+1 para asegurarse de que puede recuperarse antes de atrapar sig segfault
Henrik Mühe
15
Lanzar desde un manejador de señales es algo muy peligroso. La mayoría de los compiladores asumen que solo las llamadas pueden generar excepciones y configuran la información de desenrollado en consecuencia. Los lenguajes que transforman excepciones de hardware en excepciones de software, como Java y C #, son conscientes de que cualquier cosa puede arrojar; este no es el caso de C ++. Con GCC, al menos debe -fnon-call-exceptionsasegurarse de que funcione, y eso tiene un costo de rendimiento. También existe el peligro de que se lance desde una función sin soporte de excepción (como una función C) y se produzca una fuga / falla más tarde.
zneak
1
Estoy de acuerdo con Zneak. No lance desde un manejador de señales.
MM.
La biblioteca está ahora en github.com/Plaristote/segvcatch , pero no pude encontrar el manual ni compilarlo. ./build_gcc_linux_releaseda varios errores.
alfC
¡Hurra! ¡Ahora sé que no soy el único usuario de Gentoo en el mundo!
SS Anne
46

Aquí hay un ejemplo de cómo hacerlo en C.

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void segfault_sigaction(int signal, siginfo_t *si, void *arg)
{
    printf("Caught segfault at address %p\n", si->si_addr);
    exit(0);
}

int main(void)
{
    int *foo = NULL;
    struct sigaction sa;

    memset(&sa, 0, sizeof(struct sigaction));
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = segfault_sigaction;
    sa.sa_flags   = SA_SIGINFO;

    sigaction(SIGSEGV, &sa, NULL);

    /* Cause a seg fault */
    *foo = 1;

    return 0;
}
JayM
fuente
9
sizeof (sigaction) ==> sizeof (struct sigaction), o de lo contrario obtendrá un error ISO C ++ al compilar la cosa.
Dave Dopson
7
Hacer IO en un manejador de señales es una receta para el desastre.
Tim Seguine
6
@TimSeguine: eso no es cierto. Solo necesita asegurarse de saber lo que está haciendo. signal(7)enumera todas las funciones seguras para señales asíncronas que se pueden utilizar con relativamente poco cuidado. En el ejemplo anterior también es completamente seguro porque nada más en el programa está tocando stdoutexcepto la printfllamada en el controlador.
stefanct
3
@stefanct Este es un ejemplo de juguete. Prácticamente cualquier programa que no sea de juguetes mantendrá el bloqueo en la salida estándar en algún momento. Con este manejador de señales, lo peor que probablemente puede suceder es un punto muerto en la falla de seguridad, pero eso puede ser suficientemente malo si actualmente no tiene un mecanismo para eliminar procesos no autorizados en su caso de uso.
Tim Seguine
3
de acuerdo con 2.4.3 Acciones de señales , llamar a printf desde un manejador de señales que se llama como resultado de una indirección ilegal, ya sea que el programa sea multiproceso o no, es simplemente un período de comportamiento no definido .
Julien Villemure-Fréchette
8

Solución C ++ que se encuentra aquí ( http://www.cplusplus.com/forum/unices/16430/ )

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig)
{
    printf("OUCH! - I got signal %d\n", sig);
}
int main()
{
    struct sigaction act;
    act.sa_handler = ouch;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);
    while(1) {
        printf("Hello World!\n");
        sleep(1);
    }
}
revo
fuente
7
Sé que esto es solo un ejemplo que no escribiste, pero hacer IO en un manejador de señales es una receta para el desastre.
Tim Seguine
3
@TimSeguine: repetir cosas que, en el mejor de los casos, son muy engañosas no es una buena idea (cf. stackoverflow.com/questions/2350489/… )
stefanct
3
@stefanct Las precauciones necesarias para utilizar printf de forma segura en un gestor de señales no son triviales. No hay nada engañoso en eso. Este es un ejemplo de juguete. E incluso en este ejemplo de juguete es posible interbloquear si cronometra el SIGINT correctamente. Los interbloqueos son peligrosos precisamente PORQUE son raros. Si cree que este consejo fue engañoso, manténgase alejado de mi código, porque no confío en usted a una milla de él.
Tim Seguine
Nuevamente, aquí estaba hablando de E / S en general. En lugar de señalar el problema con este ejemplo real, que es realmente malo.
stefanct
1
@stefanct Si quieres ser quisquilloso e ignorar el contexto de la declaración, ese es tu problema. ¿Quién dijo que estaba hablando de E / S en general? Usted. Solo tengo un gran problema con la gente que publica respuestas de juguetes a problemas difíciles. Incluso en el caso de que use funciones asincrónicas seguras, todavía hay mucho en qué pensar y esta respuesta hace que parezca trivial.
Tim Seguine
8

Para la portabilidad, probablemente se debería usar std::signalde la biblioteca estándar de C ++, pero hay muchas restricciones sobre lo que puede hacer un manejador de señales. Desafortunadamente, no es posible capturar un SIGSEGV desde un programa C ++ sin introducir un comportamiento indefinido porque la especificación dice:

  1. es un comportamiento indefinido para llamar a cualquier función de la biblioteca dentro del manejador que no sea un subconjunto muy estrecho de las funciones de la librería estándar ( abort, exit, algunas funciones atómicas, vuelva a instalar actual manejador de señales, memcpy, memmove, caracteres de tipo, `std :: movimiento , std::forward, y algunos más ).
  2. es un comportamiento indefinido si el manejador usa una throwexpresión.
  3. es un comportamiento indefinido si el controlador regresa al manejar SIGFPE, SIGILL, SIGSEGV

Esto prueba que es imposible capturar SIGSEGV desde un programa que utilice C ++ estrictamente estándar y portátil. SIGSEGV todavía es detectado por el sistema operativo y normalmente se informa al proceso padre cuando se llama a una función de familia de espera .

Probablemente se encontrará con el mismo tipo de problema al usar la señal POSIX porque hay una cláusula que dice en 2.4.3 Acciones de señal :

El comportamiento de un proceso no está definido después de que vuelve normalmente a partir de una función de captura de señal para un SIGBUS, SIGFPE, SIGILL, o la señal de SIGSEGV que no se genera por kill(), sigqueue()o raise().

Unas palabras sobre el longjumps. Suponiendo que estamos usando señales POSIX, usar longjumppara simular el desenrollado de la pila no ayudará:

Aunque longjmp()es una función segura para señales asíncronas, si se invoca desde un manejador de señales que interrumpió una función segura para señales no asíncronas o equivalente (como el procesamiento equivalente a exit()realizado después de un retorno de la llamada inicial a main()), el El comportamiento de cualquier llamada posterior a una función segura de señal no asíncrona o equivalente no está definido.

Esto significa que la continuación invocado por la llamada a longjump no se puede llamar de forma fiable por lo general la función de utilidad de la biblioteca como printf, malloco exito devolución del principal sin inducir un comportamiento indefinido. Como tal, la continuación solo puede realizar operaciones restringidas y solo puede salir a través de algún mecanismo de terminación anormal.

En pocas palabras, capturar un SIGSEGV y reanudar la ejecución del programa en un portátil probablemente no sea factible sin introducir UB. Incluso si está trabajando en una plataforma Windows para la que tiene acceso al manejo estructurado de excepciones, vale la pena mencionar que MSDN sugiere no intentar nunca manejar las excepciones de hardware: Excepciones de hardware

Julien Villemure-Fréchette
fuente
Sin embargo, SIGSEGV no es una excepción de hardware. Siempre se puede usar una arquitectura padre-hijo en la que el padre sea capaz de detectar el caso de un hijo que fue asesinado por el kernel y usar IPC para compartir el estado del programa relevante para continuar donde lo dejamos. Creo que los navegadores modernos pueden verse de esta manera, ya que utilizan mecanismos IPC para comunicarse con ese proceso por pestaña del navegador. Obviamente, el límite de seguridad entre procesos es una ventaja en el escenario del navegador.
0xC0000022L
5

A veces queremos capturar a SIGSEGVpara averiguar si un puntero es válido, es decir, si hace referencia a una dirección de memoria válida. (O incluso verifique si algún valor arbitrario puede ser un puntero).

Una opción es verificarlo con isValidPtr()(funcionó en Android):

int isValidPtr(const void*p, int len) {
    if (!p) {
    return 0;
    }
    int ret = 1;
    int nullfd = open("/dev/random", O_WRONLY);
    if (write(nullfd, p, len) < 0) {
    ret = 0;
    /* Not OK */
    }
    close(nullfd);
    return ret;
}
int isValidOrNullPtr(const void*p, int len) {
    return !p||isValidPtr(p, len);
}

Otra opción es leer los atributos de protección de la memoria, que es un poco más complicado (funcionó en Android):

re_mprot.c:

#include <errno.h>
#include <malloc.h>
//#define PAGE_SIZE 4096
#include "dlog.h"
#include "stdlib.h"
#include "re_mprot.h"

struct buffer {
    int pos;
    int size;
    char* mem;
};

char* _buf_reset(struct buffer*b) {
    b->mem[b->pos] = 0;
    b->pos = 0;
    return b->mem;
}

struct buffer* _new_buffer(int length) {
    struct buffer* res = malloc(sizeof(struct buffer)+length+4);
    res->pos = 0;
    res->size = length;
    res->mem = (void*)(res+1);
    return res;
}

int _buf_putchar(struct buffer*b, int c) {
    b->mem[b->pos++] = c;
    return b->pos >= b->size;
}

void show_mappings(void)
{
    DLOG("-----------------------------------------------\n");
    int a;
    FILE *f = fopen("/proc/self/maps", "r");
    struct buffer* b = _new_buffer(1024);
    while ((a = fgetc(f)) >= 0) {
    if (_buf_putchar(b,a) || a == '\n') {
        DLOG("/proc/self/maps: %s",_buf_reset(b));
    }
    }
    if (b->pos) {
    DLOG("/proc/self/maps: %s",_buf_reset(b));
    }
    free(b);
    fclose(f);
    DLOG("-----------------------------------------------\n");
}

unsigned int read_mprotection(void* addr) {
    int a;
    unsigned int res = MPROT_0;
    FILE *f = fopen("/proc/self/maps", "r");
    struct buffer* b = _new_buffer(1024);
    while ((a = fgetc(f)) >= 0) {
    if (_buf_putchar(b,a) || a == '\n') {
        char*end0 = (void*)0;
        unsigned long addr0 = strtoul(b->mem, &end0, 0x10);
        char*end1 = (void*)0;
        unsigned long addr1 = strtoul(end0+1, &end1, 0x10);
        if ((void*)addr0 < addr && addr < (void*)addr1) {
            res |= (end1+1)[0] == 'r' ? MPROT_R : 0;
            res |= (end1+1)[1] == 'w' ? MPROT_W : 0;
            res |= (end1+1)[2] == 'x' ? MPROT_X : 0;
            res |= (end1+1)[3] == 'p' ? MPROT_P
                 : (end1+1)[3] == 's' ? MPROT_S : 0;
            break;
        }
        _buf_reset(b);
    }
    }
    free(b);
    fclose(f);
    return res;
}

int has_mprotection(void* addr, unsigned int prot, unsigned int prot_mask) {
    unsigned prot1 = read_mprotection(addr);
    return (prot1 & prot_mask) == prot;
}

char* _mprot_tostring_(char*buf, unsigned int prot) {
    buf[0] = prot & MPROT_R ? 'r' : '-';
    buf[1] = prot & MPROT_W ? 'w' : '-';
    buf[2] = prot & MPROT_X ? 'x' : '-';
    buf[3] = prot & MPROT_S ? 's' : prot & MPROT_P ? 'p' :  '-';
    buf[4] = 0;
    return buf;
}

re_mprot.h:

#include <alloca.h>
#include "re_bits.h"
#include <sys/mman.h>

void show_mappings(void);

enum {
    MPROT_0 = 0, // not found at all
    MPROT_R = PROT_READ,                                 // readable
    MPROT_W = PROT_WRITE,                                // writable
    MPROT_X = PROT_EXEC,                                 // executable
    MPROT_S = FIRST_UNUSED_BIT(MPROT_R|MPROT_W|MPROT_X), // shared
    MPROT_P = MPROT_S<<1,                                // private
};

// returns a non-zero value if the address is mapped (because either MPROT_P or MPROT_S will be set for valid addresses)
unsigned int read_mprotection(void* addr);

// check memory protection against the mask
// returns true if all bits corresponding to non-zero bits in the mask
// are the same in prot and read_mprotection(addr)
int has_mprotection(void* addr, unsigned int prot, unsigned int prot_mask);

// convert the protection mask into a string. Uses alloca(), no need to free() the memory!
#define mprot_tostring(x) ( _mprot_tostring_( (char*)alloca(8) , (x) ) )
char* _mprot_tostring_(char*buf, unsigned int prot);

PD DLOG()es printf()el registro de Android. FIRST_UNUSED_BIT()se define aquí .

PPS Puede que no sea una buena idea llamar a alloca () en un bucle; es posible que la memoria no se libere hasta que la función regrese.

18446744073709551615
fuente