Làm thế nào để bắt lỗi phân đoạn trong Linux?


82

Tôi cần bắt lỗi phân đoạn trong các hoạt động dọn dẹp thư viện của bên thứ ba. Điều này đôi khi xảy ra ngay trước khi chương trình của tôi thoát và tôi không thể khắc phục lý do thực sự của việc này. Trong lập trình Windows, tôi có thể làm điều này với __try - __catch. Có cách thức đa nền tảng hoặc nền tảng cụ thể nào để làm điều tương tự không? Tôi cần cái này trong Linux, gcc.


Lỗi phân đoạn luôn do một lỗi có thể thực sự khó bắt. Tôi chỉ tìm thấy một cái xuất hiện ngẫu nhiên. Mỗi tệp có 500 triệu điểm dữ liệu. Khoảng 10-15 tệp, lỗi phân đoạn này xuất hiện. Tôi đang sử dụng đa luồng, hàng đợi không có khóa, v.v. Quản lý công việc khá phức tạp. Cuối cùng thì nó là một đối tượng mà tôi đã tạo, std :: move () vào một cấu trúc dữ liệu khác. Tại địa phương, tôi đã sử dụng đối tượng này sau khi di chuyển. Vì một số lý do, C ++ là OK với điều này. Nhưng chắc chắn một lúc nào đó segfault sẽ hiển thị.
Kemin Zhou

Câu trả lời:


77

Trên Linux, chúng ta cũng có thể có những ngoại lệ này.

Thông thường, khi chương trình của bạn thực hiện một lỗi phân đoạn, nó sẽ được gửi một SIGSEGVtín hiệu. Bạn có thể thiết lập trình xử lý của riêng mình cho tín hiệu này và giảm thiểu hậu quả. Tất nhiên, bạn thực sự nên chắc chắn rằng bạn có thể phục hồi sau tình huống này. Trong trường hợp của bạn, tôi nghĩ, bạn nên gỡ lỗi mã của mình thay thế.

Quay lại chủ đề. Gần đây tôi đã gặp một thư viện ( hướng dẫn ngắn hạn ) chuyển đổi các tín hiệu như vậy thành ngoại lệ, vì vậy bạn có thể viết mã như sau:

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

Tuy nhiên, đã không kiểm tra nó. Hoạt động trên hộp Gentoo x86-64 của tôi. Nó có chương trình phụ trợ dành riêng cho nền tảng (mượn từ việc triển khai java của gcc), vì vậy nó có thể hoạt động trên nhiều nền tảng. Nó chỉ hỗ trợ x86 và x86-64, nhưng bạn có thể nhận được phụ trợ từ libjava, nằm trong các nguồn gcc.


16
+1 để chắc chắn rằng bạn có thể phục hồi trước khi bắt được sig segfault
Henrik Mühe

15
Ném từ một người điều khiển tín hiệu là một việc làm rất nguy hiểm. Hầu hết các trình biên dịch giả định rằng chỉ có các cuộc gọi mới có thể tạo ra các ngoại lệ và thiết lập thông tin thư giãn cho phù hợp. Các ngôn ngữ chuyển đổi ngoại lệ phần cứng thành ngoại lệ phần mềm, như Java và C #, biết rằng bất cứ thứ gì cũng có thể ném ra; đây không phải là trường hợp của C ++. Với GCC, ít nhất bạn cần -fnon-call-exceptionsđảm bảo rằng nó hoạt động – và có chi phí hiệu suất cho điều đó. Cũng có một nguy cơ là bạn sẽ ném khỏi một hàm mà không có sự hỗ trợ ngoại lệ (như hàm C) và rò rỉ / sự cố sau đó.
zneak

1
Tôi đồng ý với zneak. Đừng ném từ một bộ xử lý tín hiệu.
MM.

Thư viện hiện có trong github.com/Plaristote/segvcatch , nhưng tôi không thể tìm thấy hướng dẫn sử dụng hoặc biên dịch nó. ./build_gcc_linux_releaseđưa ra một số lỗi.
alfC

Yay! Giờ tôi biết mình không phải là người dùng Gentoo duy nhất trên thế giới!
SS Anne

46

Đây là một ví dụ về cách thực hiện trong 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), nếu không bạn gặp lỗi ISO C ++ khi biên dịch thứ.
Dave Dopson

7
Thực hiện IO trong trình xử lý tín hiệu là một công thức cho thảm họa.
Tim Seguine

6
@TimSeguine: điều đó không đúng. Bạn chỉ cần chắc chắn rằng bạn biết những gì bạn đang làm. signal(7)liệt kê tất cả các chức năng không đồng bộ-tín hiệu-an toàn có thể được sử dụng tương đối ít cẩn thận. Trong ví dụ trên, nó cũng hoàn toàn an toàn vì không có gì khác trong chương trình đang chạm vào stdoutngoài printfcuộc gọi trong trình xử lý.
stefanct

3
@stefanct Đây là một ví dụ về đồ chơi. Hầu như bất kỳ chương trình không phải đồ chơi nào cũng sẽ giữ được khóa trên stdout vào một thời điểm nào đó. Với trình xử lý tín hiệu này, điều tồi tệ nhất có thể xảy ra là bế tắc trên segfault, nhưng điều đó có thể đủ tệ nếu bạn hiện không có cơ chế để giết các quy trình giả mạo trong trường hợp sử dụng của mình.
Tim Seguine

3
theo 2.4.3 Hành động tín hiệu , việc gọi printf từ bên trong trình xử lý tín hiệu được gọi là kết quả của chuyển hướng bất hợp pháp, cho dù chương trình có được đa luồng hay không chỉ là hành vi đơn giản không xác định .
Julien Villemure-Fréchette

8

Giải pháp C ++ được tìm thấy tại đây ( 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
Tôi biết đây chỉ là một ví dụ mà bạn không viết, nhưng thực hiện IO trong trình xử lý tín hiệu là một công thức cho thảm họa.
Tim Seguine

3
@TimSeguine: lặp lại nội dung mà tốt nhất là rất dễ gây hiểu lầm không phải là một ý kiến ​​hay (xem stackoverflow.com/questions/2350489/… )
stefanct

3
@stefanct Các biện pháp phòng ngừa cần thiết để sử dụng printf một cách an toàn trong trình xử lý tín hiệu không hề nhỏ. Không có gì sai lệch về điều đó. Đây là một ví dụ về đồ chơi. Và ngay cả trong ví dụ đồ chơi này, nó có thể bị bế tắc nếu bạn bấm đúng thời điểm SIGINT. Chốt lại chính xác là nguy hiểm VÌ chúng rất hiếm. Nếu bạn nghĩ rằng lời khuyên này là sai lầm, thì hãy tránh xa mã của tôi, vì tôi không tin bạn trong vòng một dặm kể từ nó.
Tim Seguine

Một lần nữa, bạn đã nói chung về I / O ở đây. Thay vì chỉ ra vấn đề với ví dụ thực tế này, ví dụ này thực sự là một điều tồi tệ.
stefanct

1
@stefanct Nếu bạn muốn kiểm soát và bỏ qua ngữ cảnh của câu lệnh, thì đó là vấn đề của bạn. Ai nói rằng tôi đang nói về I / O nói chung? Bạn. Tôi chỉ gặp một vấn đề lớn khi mọi người đăng câu trả lời về đồ chơi cho những vấn đề khó khăn. Ngay cả trong trường hợp bạn sử dụng các hàm an toàn không đồng bộ, vẫn còn rất nhiều điều phải suy nghĩ và câu trả lời này khiến nó có vẻ như là tầm thường.
Tim Seguine

8

Đối với tính di động, có lẽ nên sử dụng std::signaltừ thư viện C ++ tiêu chuẩn, nhưng có rất nhiều hạn chế về những gì một trình xử lý tín hiệu có thể làm. Thật không may, không thể bắt một SIGSEGV từ bên trong một chương trình C ++ mà không đưa ra hành vi không xác định vì đặc tả cho biết:

  1. đó là hành vi không xác định để gọi bất kỳ chức năng thư viện từ bên trong xử lý khác hơn là một tập hợp con rất hẹp trong những chức năng thư viện chuẩn ( abort, exit, một số chức năng nguyên tử, cài đặt lại hiện xử lý tín hiệu, memcpy, memmove, đặc điểm loại, `std :: di chuyển , std::forward, và một số chi tiết ).
  2. nó là hành vi không xác định nếu trình xử lý sử dụng một throwbiểu thức.
  3. đó là hành vi không xác định nếu trình xử lý trả về khi xử lý SIGFPE, SIGILL, SIGSEGV

Điều này chứng tỏ rằng không thể bắt SIGSEGV từ bên trong một chương trình sử dụng C ++ tiêu chuẩn và di động. SIGSEGV vẫn bị hệ điều hành bắt và thường được báo cáo cho tiến trình mẹ khi chờ đợi hàm họ được gọi.

Bạn có thể sẽ gặp phải sự cố tương tự khi sử dụng tín hiệu POSIX vì có một điều khoản nói trong 2.4.3 Hành động tín hiệu :

Các hành vi của một quá trình là undefined sau nó sẽ trả về bình thường từ một hàm tín hiệu bắt cho một SIGBUS, SIGFPE, SIGILL, hoặc tín hiệu SIGSEGV mà không được tạo ra bởi kill(), sigqueue()hoặc raise().

Một từ về longjumps. Giả sử chúng tôi đang sử dụng tín hiệu POSIX, việc sử dụng longjumpđể mô phỏng việc tháo cuộn ngăn xếp sẽ không giúp ích gì:

Mặc dù longjmp()là một hàm không đồng bộ-tín hiệu-an toàn, nhưng nếu nó được gọi từ một bộ xử lý tín hiệu làm gián đoạn một hàm không-không-tín-hiệu-an toàn hoặc chức năng tương đương (chẳng hạn như xử lý tương đương được exit()thực hiện sau khi trả về từ lệnh gọi ban đầu đến main()), hành vi của bất kỳ lệnh gọi tiếp theo nào đến một hàm không phải là tín hiệu an toàn không đồng bộ hoặc chức năng tương đương là không xác định.

Điều này có nghĩa là phần tiếp tục được gọi bởi lệnh gọi longjump không thể gọi một cách đáng tin cậy hàm thư viện thường hữu ích như printf, mallochoặc exithoặc trả về từ hàm chính mà không tạo ra hành vi không xác định. Như vậy, việc tiếp tục chỉ có thể thực hiện các hoạt động bị hạn chế và chỉ có thể thoát ra thông qua một số cơ chế kết thúc bất thường.

Nói một cách ngắn gọn, việc bắt một SIGSEGV tiếp tục thực thi chương trình trong thiết bị di động có lẽ là không khả thi nếu không giới thiệu UB. Thậm chí nếu bạn đang làm việc trên một nền tảng Windows mà bạn có thể truy cập để xử lý ngoại lệ có cấu trúc, nó là đáng nói đến là MSDN gợi ý để không bao giờ cố gắng ngoại lệ phần cứng xử lý: Trường hợp ngoại lệ phần cứng


Tuy nhiên, SIGSEGV hầu như không phải là một ngoại lệ phần cứng. Người ta luôn có thể sử dụng kiến ​​trúc cha-con trong đó cha mẹ có thể phát hiện trường hợp con bị hạt nhân giết và sử dụng IPC để chia sẻ trạng thái chương trình có liên quan nhằm tiếp tục lại nơi chúng ta đã rời khỏi. Tôi tin rằng các trình duyệt hiện đại có thể được nhìn thấy theo cách này, vì chúng sử dụng cơ chế IPC để giao tiếp với một quy trình đó trên mỗi tab trình duyệt. Rõ ràng ranh giới bảo mật giữa các quy trình là một phần thưởng trong kịch bản trình duyệt.
0xC0000022L

5

Đôi khi chúng tôi muốn bắt một SIGSEGV con trỏ để tìm xem một con trỏ có hợp lệ hay không, nghĩa là nếu nó tham chiếu đến một địa chỉ bộ nhớ hợp lệ. (Hoặc thậm chí kiểm tra xem một số giá trị tùy ý có thể là một con trỏ hay không.)

Một tùy chọn là kiểm tra nó với isValidPtr()(hoạt động trên 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);
}

Một tùy chọn khác là đọc các thuộc tính bảo vệ bộ nhớ, phức tạp hơn một chút (hoạt động trên 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()nhật ký Android. FIRST_UNUSED_BIT()được định nghĩa ở đây .

PPS Có thể không phải là ý kiến ​​hay khi gọi hàm alloca () trong một vòng lặp - bộ nhớ có thể không được giải phóng cho đến khi hàm trả về.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.