Як виявити помилку сегментації в Linux?


84

Мені потрібно виявити помилку сегментації в операціях очищення бібліотеки сторонніх виробників. Це трапляється іноді безпосередньо перед виходом моєї програми, і я не можу визначити справжню причину цього. У програмуванні Windows я міг зробити це за допомогою __try - __catch. Чи існує крос-платформенний або специфічний для платформи спосіб зробити те саме? Мені це потрібно в Linux, gcc.


Помилка сегментації завжди спричинена помилкою, яку дуже важко вловити. Я просто знаходжу такий, який з’являється випадково. Кожен файл має 500 мільйонів точок даних. Приблизно кожні 10-15 файлів з’являється ця помилка сегментації. Я використовував багатопоточність, чергу без замків тощо. Досить складне управління роботою. Врешті-решт це об’єкт, який я створив, std :: preselio () в іншу структуру даних. Місцево я використовував цей об’єкт після переїзду. З якоїсь причини з C ++ це нормально. Але сегментація з’явиться напевно в якийсь момент.
Кемін Чжоу

Відповіді:


80

У Linux ми можемо мати це як виняток.

Зазвичай, коли ваша програма виконує помилку сегментації, їй надсилається SIGSEGVсигнал. Ви можете налаштувати власний обробник для цього сигналу та пом'якшити наслідки. Звичайно, ви дійсно повинні бути впевнені, що зможете виправити ситуацію. У вашому випадку, я думаю, вам слід замість цього налагодити ваш код.

Повернутися до теми. Нещодавно я зіткнувся з бібліотекою ( короткий посібник ), яка перетворює такі сигнали на винятки, тому ви можете написати такий код:

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

Однак не перевіряв. Працює на моєму x86-64 Gentoo box. У нього є серверна база для платформи (запозичена з реалізації Java gcc), тому вона може працювати на багатьох платформах. Він просто підтримує x86 та x86-64 нестандартно, але ви можете отримати серверні файли з libjava, який знаходиться у джерелах gcc.


16
+1 для впевненості, що ви можете відновитись, перш ніж ловити sig segfault
Хенрік Мюе 02

16
Кидання з обробника сигналу - дуже небезпечна справа. Більшість компіляторів припускають, що лише виклики можуть генерувати винятки, і відповідно встановлюють інформацію про розмотування. Мови, які перетворюють апаратні винятки на програмні винятки, такі як Java та C #, усвідомлюють, що все може кинути; це не стосується C ++. З GCC вам, принаймні, потрібно -fnon-call-exceptionsпереконатися, що він працює - і це вимагає продуктивності. Існує також небезпека того, що ви викинете функцію без виняткової підтримки (як функція C) і пізніше вийде / виллється.
zneak

1
Я згоден із zneak. Не кидайте з обробника сигналу.
ММ.

Зараз бібліотека знаходиться в 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 ++ при компіляції речі.
Дейв Допсон,

7
Введення вводу-виводу в обробнику сигналів - це рецепт катастрофи.
Тім Сегін,

6
@TimSeguine: це неправда. Вам просто потрібно переконатися, що ви знаєте, що робите. signal(7)перераховані всі функції, захищені від асинхронних сигналів, які можна використовувати з відносно невеликою обережністю. У наведеному вище прикладі це також повністю безпечно, оскільки ніщо інше в програмі не торкається, stdoutкрім printfвиклику в обробнику.
stefanct

3
@stefanct Це приклад іграшки. Практично будь-яка неіграшкова програма в якийсь момент утримує замок на stdout. За допомогою цього обробника сигналів найгіршим, що, можливо, може статися, є глухий кут на сегментарі, але це може бути досить погано, якщо у вас наразі немає механізму для вбивства неправдивих процесів у вашому випадку використання.
Тім Сегін

3
згідно з 2.4.3 Сигнальні дії , виклик printf зсередини обробника сигналу, який викликається в результаті незаконного опосередкування, незалежно від того, є програма багатопотоковою чи ні, - це просто невизначений період поведінки .
Julien Villemure-Fréchette

9

Для переносимості, мабуть, слід використовувати std::signalстандартну бібліотеку C ++, але існує багато обмежень щодо того, що може зробити обробник сигналу. На жаль, неможливо зловити SIGSEGV із програми C ++ без введення невизначеної поведінки, оскільки в специфікації сказано:

  1. це невизначене поведінку для виклику будь-бібліотечної функції з обробника, крім дуже вузького підмножини стандартних бібліотечних функцій ( abort, exit, деякі атомарні функцій, переустановите поточний обробник сигналу memcpy, memmove, риси типу, `ЗОГО :: рухатися , std::forward, і ще трохи ).
  2. це невизначена поведінка, якщо обробник використовує throwвираз.
  3. це невизначена поведінка, якщо обробник повертається при обробці SIGFPE, SIGILL, SIGSEGV

Це доводить, що неможливо зловити SIGSEGV із програми використовуючи строго стандартний та портативний C ++. SIGSEGV все ще перехоплюється операційною системою і зазвичай повідомляється батьківському процесу при виклику функції сімейства очікування .

Можливо, ви зіткнетеся з такою ж проблемою із використанням сигналу POSIX, оскільки в пункті 2.4.3 „Сигнальні дії” є пункт :

Поведінка процесу є невизначеним після того, як він повертається , як правило з функції сигналу виділяються для SIGBUS, SIGFPE, SIGILL, або сигналу SIGSEGV , який не був генеруються kill(), sigqueue()абоraise() .

Слово про longjumpс. Припускаючи, що ми використовуємо сигнали POSIX, використовуючиlongjump для імітації розмотування стека не допоможе:

Хоча longjmp()це функція, безпечна для асинхронного сигналу, якщо вона викликається від обробника сигналу, який перервав функцію, що не захищає асинхронний сигнал, або еквівалент (наприклад, обробка, еквівалентна exit()виконуваній після повернення з початкового виклику до main()), поведінка будь-якого наступного виклику функції, що не захищає асинхронний сигнал, або еквівалента не визначена.

Це означає , що продовження викликається при виклику LongJump не може надійно називають зазвичай корисну бібліотеку функції , такими як printf, mallocабо exitчи повернення з основних , не викликаючи невизначений поведінка. Таким чином, продовження може виконувати лише обмежені операції і може виходити лише через якийсь ненормальний механізм припинення.

Якщо коротко сказати, зловити 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
Я знаю, що це лише приклад, якого ви не писали, але робити введення-введення в обробнику сигналу - це рецепт катастрофи.
Тім Сегін,

3
@TimSeguine: повторювати матеріал , який в кращому випадку дуже оману , це не дуже гарна ідея (див stackoverflow.com/questions/2350489 / ... )
stefanct

3
@stefanct Заходи безпеки, необхідні для безпечного використання printf в обробнику сигналу, не є тривіальними. У цьому немає нічого оманливого. Це приклад іграшки. І навіть у цьому прикладі іграшок можна зайти в глухий кут, якщо ви правильно встановите час на SIGINT. Тупикові ситуації небезпечні саме тому, що вони рідкісні. Якщо ви вважаєте, що ця порада вводить в оману, то тримайтеся подалі від мого коду, тому що я не довіряю вам за милю від нього.
Тім Сегін

Знову ж таки, ви тут говорили про I / O. Замість того, щоб вказати на проблему на цьому фактичному прикладі, який насправді є поганим.
stefanct

1
@stefanct Якщо ви хочете зачепити і проігнорувати контекст твердження, то це ваша проблема. Хто сказав, що я взагалі говорив про введення / виведення? Ти. У мене просто велика проблема з тим, що люди розміщують іграшкові відповіді на складні проблеми. Навіть у тому випадку, якщо ви використовуєте асинхронні безпечні функції, є над чим задуматись, і ця відповідь здається тривіальною.
Тім Сегін

5

Іноді ми хочемо зловити a SIGSEGV щоб з’ясувати, чи є покажчик дійсним, тобто якщо він посилається на дійсну адресу пам’яті. (Або навіть перевірити, чи може якесь довільне значення бути покажчиком.)

Один із варіантів - перевірити його 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 () у циклі - пам’ять може не звільнятися, поки функція не повернеться.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.