如何在Linux中捕获分段错误?


82

我需要在第三方库清理操作中捕获分段错误。有时会在我的程序退出之前发生这种情况,而我无法解决此问题的真正原因。在Windows编程中,我可以使用__try-__catch做到这一点。是否有跨平台或特定于平台的方法来做到这一点?我在Linux中需要这个,gcc。


细分错误通常是由可能很难捕获的错误引起的。我只是发现一个随机出现的。每个文件有5亿个数据点。大约每10-15个文件出现一次此分段错误。我正在使用多线程,无锁队列等。非常复杂的作业管理。最后,它是我创建的一个对象,将std :: moved()放入另一个数据结构中。移动后,我在本地使用此对象。由于某种原因,C ++可以做到这一点。但是段错误肯定会在某个时刻出现。
周克敏

Answers:


77

在Linux上,我们也可以将它们作为例外。

通常,当您的程序执行分段错误时,会向其发送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,但您可以从libjava获得后端,libjava位于gcc源中。


16
一为确保您可以捕捉SIG段错误,然后才能恢复
亨里克木盒

15
从信号处理程序中抛出是非常危险的事情。大多数编译器假定只有调用才能生成异常,并相应地设置展开信息。诸如Java和C#之类的将硬件异常转换为软件异常的语言都知道任何东西都可能抛出。C ++并非如此。使用GCC,您至少需要-fnon-call-exceptions确保它能正常工作,并且为此付出了性能成本。还有一种危险,您可能会从没有异常支持的功能(例如C函数)中抛出该错误,并且稍后泄漏/崩溃。
zneak

1
我同意zneak。不要抛出信号处理程序。
MM。

该库现在位于github.com/Plaristote/segvcatch中,但是我找不到该手册或对其进行编译。./build_gcc_linux_release给出了几个错误。
alfC

好极了!现在我知道我不是世界上唯一的Gentoo用户!
SS安妮

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
在信号处理程序中执行IO可能会带来灾难。
蒂姆·塞吉

6
@TimSeguine:事实并非如此。您只需要确保您知道自己在做什么。signal(7)列出了所有可以很少使用的异步信号安全功能。在上面的示例中,它也是完全安全的,因为除了处理程序中stdoutprintf调用外,程序中没有其他任何触摸。
stefanct

3
@stefanct这是一个玩具示例。实际上,任何非玩具程序都会在某个时候将其锁定在stdout上。使用此信号处理程序,可能发生的最坏情况是segfault的死锁,但是如果您当前没有在用例中杀死流氓进程的机制,这可能已经够糟了。
Tim Seguine

3
根据2.4.3 Signal Actions的规定,从信号处理程序内部调用printf(由于非法间接调用而导致),无论程序是否是多线程的,这只是未定义的行为期。
JulienVillemure-Fréchette18年

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可能会导致灾难。
蒂姆·塞吉恩

3
@TimSeguine:重复的东西,充其量是非常误导是不是一个好主意(参见stackoverflow.com/questions/2350489/...
stefanct

3
@stefanct在信号处理程序中安全使用printf所必需的预防措施并不简单。对此没有任何误导。这是一个玩具示例。而且即使在这个玩具示例中,如果您将SIGINT设置为正确的时间,也可能会死锁。死锁很危险,因为它们很少发生。如果您认为此建议具有误导性,请不要使用我的代码,因为我不信任您。
Tim Seguine

同样,您在这里通常是在谈论I / O。并没有指出这个实际示例的问题,这确实是一个不好的例子。
stefanct

1
@stefanct如果您想nitpick并忽略该语句的上下文,那么这就是您的问题。谁说我总体上在谈论I / O?您。我只是遇到一个主要问题,人们发布玩具难题的答案。即使在使用异步安全功能的情况下,仍然有很多需要考虑的问题,而且这个答案似乎显得微不足道。
蒂姆·塞吉因

8

为了实现可移植性,应该std::signal从标准C ++库中使用它,但是对信号处理程序的功能有很多限制。不幸的是,如果不引入未定义的行为,就不可能从C ++程序中捕获SIGSEGV 因为该规范指出:

  1. 它是未定义的行为从比的标准库函数(一个非常窄的子集以外的处理程序中调用任何库函数abortexit一些原子功能,重新安装电流信号处理程序,memcpymemmove,型性状,`的std ::移动, std::forward,并且更一些)。
  2. 如果处理程序使用 throw表达式。
  3. 如果处理程序在处理SIGFPE,SIGILL,SIGSEGV时返回,则它是未定义的行为

这证明使用严格的标准和可移植的C ++从程序中捕获SIGSEGV是不可能的。SIGSEGV仍被操作系统捕获,通常在等待时报告给父进程调用系列函数。

使用POSIX信号可能会遇到同样的麻烦,因为在2.4.3 Signal Actions中有一个子句说:

它从一个信号捕获函数通常返回为不是由产生的SIGBUS,SIGFPE,SIGILL或SIGSEGV信号之后的过程的行为是未定义的kill()sigqueue()raise()

关于longjumps的一个字。假设我们使用的是POSIX信号,那么使用longjump模拟堆栈展开将无济于事:

尽管这longjmp()是异步信号安全函数,但如果它是通过中断非异步信号安全函数或等效函数的信号处理程序调用的(例如等效于exit()从初始调用返回之后执行的处理main()),后续对非异步信号安全函数或等效函数的任何调用的行为均未定义。

这意味着对longjump的调用所调用的继续不能可靠地调用通常有用的库函数(例如)printfmallocexit从main返回而不会引起未定义行为。因此,继续只能执行受限制的操作,并且只能通过某种异常终止机制退出。

简而言之,如果不引入UB ,捕获SIGSEGV在便携式计算机中恢复程序执行可能是不可行的。即使您在可以访问结构化异常处理的Windows平台上工作,也值得一提的是MSDN建议不要尝试处理硬件异常:硬件异常


但是,SIGSEGV几乎不是硬件例外。人们总是可以使用父子体系结构,其中父级能够检测被内核杀死的孩子的情况,并使用IPC共享相关的程序状态,以便继续我们的工作。我相信现代浏览器可以这样看,因为它们使用IPC机制与每个浏览器选项卡的一个进程进行通信。显然,在浏览器方案中,进程之间的安全边界是一个好处。
0xC0000022L

5

有时,我们想捕获aSIGSEGV来找出指针是否有效,也就是说,它是否引用了有效的内存地址。(或者甚至检查某个任意值是否可能是指针。)

一种选择是进行检查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);

PSDLOG()printf()Android日志。在这里FIRST_UNUSED_BIT()定义。

PPS循环调用alloca()可能不是一个好主意-直到函数返回之前,内存可能不会释放。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.