Linux에서 분할 오류를 포착하는 방법은 무엇입니까?


84

타사 라이브러리 정리 작업에서 세분화 오류를 잡아야합니다. 이것은 때때로 내 프로그램이 종료되기 직전에 발생하며 실제 이유를 수정할 수 없습니다. Windows 프로그래밍에서 __try-__catch로이 작업을 수행 할 수 있습니다. 동일한 작업을 수행하는 교차 플랫폼 또는 플랫폼 별 방법이 있습니까? Linux, gcc에서 필요합니다.


세분화 오류는 항상 포착하기 어려운 버그로 인해 발생합니다. 무작위로 나타나는 것을 찾았습니다. 각 파일에는 5 억 개의 데이터 포인트가 있습니다. 대략 10-15 개의 파일마다이 분할 오류가 나타납니다. 나는 멀티 스레딩, 잠금없는 큐 등을 사용하고 있었다. 상당히 복잡한 작업 관리. 결국 그것은 내가 만든 객체, std :: moved ()를 다른 데이터 구조로 만듭니다. 이동 후 로컬에서이 개체를 사용하고있었습니다. 어떤 이유로 C ++는 이것으로 괜찮습니다. 그러나 segfault는 어느 시점에서 확실히 나타날 것입니다.
Kemin 저우

답변:


80

리눅스에서는 이것도 예외로 할 수 있습니다.

일반적으로 프로그램이 세그멘테이션 오류를 수행하면 SIGSEGV신호 가 전송 됩니다. 이 신호에 대한 자체 핸들러를 설정하고 결과를 완화 할 수 있습니다. 물론 상황에서 회복 할 있다는 것을 정말로 확신해야합니다 . 귀하의 경우에는 대신 코드를 디버그해야한다고 생각합니다.

주제로 돌아갑니다. 최근에 그러한 신호를 예외로 변환 하는 라이브러리 ( 짧은 매뉴얼 )를 만났 으므로 다음과 같은 코드를 작성할 수 있습니다.

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

그래도 확인하지 않았습니다. x86-64 Gentoo 상자에서 작동합니다. 플랫폼 별 백엔드 (gcc의 Java 구현에서 차용)가 있으므로 많은 플랫폼에서 작동 할 수 있습니다. 기본적으로 x86 및 x86-64 만 지원하지만 gcc 소스에있는 libjava에서 백엔드를 가져올 수 있습니다.


16
sig segfault를 잡기 전에 회복 할 수 있는지 확인하려면
Henrik Mühe 2014

16
신호 처리기에서 던지는 것은 매우 위험한 일입니다. 대부분의 컴파일러는 호출 만 예외를 생성 할 수 있다고 가정하고 그에 따라 해제 정보를 설정합니다. 하드웨어 예외를 Java 및 C #과 같은 소프트웨어 예외로 변환하는 언어는 모든 것이 발생할 수 있음을 알고 있습니다. 이것은 C ++의 경우가 아닙니다. GCC를 사용하면 최소한 -fnon-call-exceptions작동하는지 확인해야하며 이에 대한 성능 비용이 있습니다. 또한 예외 지원 (예 : C 함수)없이 함수에서 던지고 나중에 누출 / 충돌 할 위험이 있습니다.
zneak

1
나는 zneak에 동의합니다. 신호 처리기에서 던지지 마십시오.
MM.

라이브러리는 이제 github.com/Plaristote/segvcatch에 있습니다. 에 있지만 설명서를 찾거나 컴파일 할 수 없습니다. ./build_gcc_linux_release몇 가지 오류가 있습니다.
alfC

예이! 이제 저는 제가 세상에서 유일한 Gentoo 사용자가 아니라는 것을 압니다!
SS Anne

46

다음은 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;
}

9
sizeof (sigaction) ==> sizeof (struct sigaction), 그렇지 않으면 ISO C ++ 오류가 발생합니다.
Dave Dopson 2011

7
신호 처리기에서 IO를 수행하는 것은 재난의 비결입니다.
Tim Seguine 2016

6
@ TimSeguine : 그건 사실이 아닙니다. 당신이하고있는 일을 알고 있는지 확인하기 만하면됩니다. signal(7)상대적으로 거의주의를 기울이지 않고 사용할 수있는 모든 비동기 신호 안전 함수를 나열합니다. 위의 예에서는 프로그램의 다른 어떤 것도 처리기 stdoutprintf호출을 건드리지 않기 때문에 완전히 안전 합니다.
stefanct

3
@stefanct 이것은 장난감 예입니다. 사실상 장난감이 아닌 프로그램은 어느 시점에서 stdout에 잠금을 유지합니다. 이 신호 처리기를 사용하면 발생할 수있는 최악의 상황은 segfault의 교착 상태 일 수 있지만 현재 사용 사례에서 악성 프로세스를 죽일 메커니즘이없는 경우에는 충분히 나쁠 수 있습니다.
Tim Seguine 2011

3
2.4.3 Signal Actions 에 따르면 프로그램이 다중 스레드인지 여부에 관계없이 불법적 인 간접 호출의 결과로 호출되는 신호 처리기 내에서 printf를 호출하는 것은 단순한 정의되지 않은 동작 기간입니다.
Julien Villemure-Fréchette

9

이식성 std::signal을 위해 표준 C ++ 라이브러리에서 사용해야 하지만 신호 처리기가 수행 할 수있는 작업에는 많은 제한이 있습니다. 안타깝게도 사양에 다음과 같이 명시되어 있기 때문에 정의되지 않은 동작을 도입하지 않고 C ++ 프로그램 내에서 SIGSEGV를 잡을 수 없습니다 .

  1. 표준 라이브러리 함수 (매우 좁은 서브 세트 이외의 핸들러 내에서 모든 라이브러리 함수를 호출하는 정의 문제이며 abort, exit몇몇 원자 기능, 전류 신호 처리기, 다시 memcpy, memmove입력 특성`표준 : 이동, std::forward 일부 이상, ).
  2. 핸들러가 사용하는 경우 정의되지 않은 동작입니다. throw 표현식을 입니다.
  3. SIGFPE, SIGILL, SIGSEGV를 처리 할 때 핸들러가 반환하면 정의되지 않은 동작입니다.

이것은 엄격한 표준 및 이식 가능한 C ++를 사용 하는 프로그램 내에서 SIGSEGV를 포착하는 것이 불가능하다는 것을 증명합니다 . SIGSEGV는 여전히 운영 체제에 의해 포착되며 일반적으로 대기 할 때 상위 프로세스에보고됩니다. family 함수가 호출 .

2.4.3 Signal Actions에 다음 과 같은 절이 있기 때문에 POSIX 신호를 사용하는 것과 동일한 종류의 문제가 발생할 수 있습니다 .

는에 의해 생성되지 않은 SIGBUS, SIGFPE, SIGILL 또는 SIGSEGV 신호에 대한 신호 끄는 기능에서 정상적으로 반환 후 공정의 동작이 정의되어 kill(), sigqueue()또는 raise().

longjumps 에 대한 한마디 . POSIX 신호를 사용한다고 가정하면 longjump스택 해제를 시뮬레이션 하는 데 사용 하면 도움이되지 않습니다.

longjmp()는 비동기 신호 안전 함수 이지만 비동기 신호 안전이 아닌 함수 또는 동등한 기능을 중단 한 신호 처리기에서 호출되는 경우 (예 :에 exit()대한 초기 호출에서 반환 된 후 수행되는 것과 동등한 처리 main()), 비동기 신호 안전이 아닌 함수 또는 이에 상응하는 후속 호출의 동작은 정의되지 않습니다.

이는 longjump 호출에 의해 호출 된 연속이 printf, malloc또는 같은 일반적으로 유용한 라이브러리 함수를 안정적으로 호출 할 수 없음을 의미합니다.exit 정의되지 않은 동작을 유도하지 않고 main에서 리턴 . 따라서 연속은 제한된 작업 만 수행 할 수 있으며 일부 비정상적인 종료 메커니즘을 통해서만 종료 될 수 있습니다.

SIGSEGV라는 잡기, 짧은 물건을 넣으려면 휴대용에서 프로그램의 실행을 재개하는 것은 UB를 도입하지 않고 아마 불가능하다. 구조적 예외 처리에 액세스 할 수있는 Windows 플랫폼에서 작업하는 경우에도 MSDN이 하드웨어 예외를 처리하지 않도록 권장한다는 점을 언급 할 가치가 있습니다. 하드웨어 예외


하지만 SIGSEGV는 하드웨어 예외가 아닙니다. 부모가 커널에 의해 죽인 자식의 경우를 감지하고 IPC를 사용하여 우리가 떠난 곳에서 다시 시작하기 위해 관련 프로그램 상태를 공유 할 수있는 부모-자식 아키텍처를 항상 사용할 수 있습니다. 최신 브라우저는 IPC 메커니즘을 사용하여 브라우저 탭당 하나의 프로세스와 통신하기 때문에 이런 방식으로 볼 수 있다고 생각합니다. 분명히 프로세스 간의 보안 경계는 브라우저 시나리오에서 보너스입니다.
0xC0000022L

8

여기에 C ++ 솔루션이 있습니다 ( 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);
    }
}

7
나는 이것이 당신이 작성하지 않은 예일 뿐이라는 것을 알고 있지만 신호 처리기에서 IO를 수행하는 것은 재앙의 비결입니다.
Tim Seguine 2016

3
@TimSeguine : 기껏해야 오해의 소지가있는 내용을 반복하는 것은 좋은 생각이 아닙니다 (참조 : stackoverflow.com/questions/2350489/… )
stefanct

3
@stefanct 신호 처리기에서 printf를 안전하게 사용하기 위해 필요한 예방 조치는 간단하지 않습니다. 그것에 대해 오해의 소지가 없습니다. 이것은 장난감의 예입니다. 그리고이 장난감 예제에서도 SIGINT 시간을 올바르게 맞추면 교착 상태가 될 수 있습니다. 교착 상태는 드물기 때문에 정확하게 위험합니다. 이 조언이 오해의 소지가 있다고 생각한다면 1 마일 내에서 당신을 신뢰하지 않기 때문에 내 코드를 멀리하십시오.
Tim Seguine 2011

다시 말하지만, 여기서는 일반적으로 I / O에 대해 이야기했습니다. 이 실제 예제의 문제를 지적하는 대신, 실제로는 나쁜 것입니다.
stefanct

1
@stefanct 문장의 문맥을 무시하고 무시하고 싶다면 그게 문제입니다. 내가 일반적으로 I / O에 대해 이야기하고 있다고 누가 말했습니까? 당신. 나는 사람들이 어려운 문제에 대한 장난감 답변을 게시하는 데 큰 문제가 있습니다. 비동기 안전 기능을 사용하는 경우에도 여전히 생각할 것이 많으며이 답변은 사소한 것처럼 보입니다.
Tim Seguine

5

때때로 우리 SIGSEGV는 포인터가 유효한지, 즉 유효한 메모리 주소를 참조하는지 알아보기 위해 a를 잡으려고합니다 . (또는 임의의 값이 포인터인지 확인하십시오.)

한 가지 옵션은 다음으로 확인하는 것입니다 isValidPtr()(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);
}

또 다른 옵션은 메모리 보호 속성을 읽는 것입니다. 이는 좀 더 까다 롭습니다 (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);

PS DLOG()printf()Android 로그입니다. 여기FIRST_UNUSED_BIT() 에 정의되어 있습니다 .

PPS alloca () 를 호출하는 것은 좋은 생각이 아닐 수 있습니다. 루프에서 않을 수 있습니다. 함수가 반환 될 때까지 메모리가 해제되지 않을 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.