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

Ale*_*x F 67 c++ try-catch segmentation-fault

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

P S*_*ved 65

在Linux上,我们也可以将这些作为例外.

通常,当程序执行分段故障时,会发送一个SIGSEGV信号.您可以为此信号设置自己的处理程序并减轻后果.当然,你应该确定你可以从这种情况中恢复过来.在您的情况下,我认为,您应该调试代码.

回到主题.我最近遇到了一个库 (简短手册),它将这些信号转换为异常,因此您可以编写如下代码:

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

但是没有检查.适用于我的x86-64 Gentoo盒子.它有一个特定于平台的后端(借用gcc的java实现),因此它可以在许多平台上运行.它只支持开箱即用的x86和x86-64,但你可以从libjava获得后端,它位于gcc源代码中.

  • 从信号处理程序中投掷是一件非常危险的事情.大多数编译器都假设只有调用才能生成异常,并相应地设置展开信息.将硬件异常转换为软件异常(如Java和C#)的语言意识到任何东西都可以抛出; 这不是C++的情况.使用GCC,您至少需要`-fnon-call-exceptions`以确保它正常工作 - 并且存在性能成本.还存在一种危险,即您将从函数中抛出无异常支持(如C函数)以及稍后泄漏/崩溃. (12认同)
  • __的+1确保你可以在捕获sig segfault之前恢复 (11认同)
  • 耶!现在我知道我不是世界上唯一的 Gentoo 用户! (2认同)

Jay*_*ayM 35

这是一个如何在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;
}
Run Code Online (Sandbox Code Playgroud)

  • sizeof(sigaction)==> sizeof(struct sigaction),否则您将收到编译该事物的ISO C++错误. (9认同)
  • 在信号处理程序中执行IO是一种灾难. (5认同)
  • @TylerDurden *首先,我总是打印出异常。*所以你对你编写的代码的标准很低,并且你公开地将你的名字附加到这个事实上。我不知道你为什么为此感到自豪,但是好吧。*其次,您认为 printf 使用堆的想法是完全错误的,如果实际阅读典型 printf 函数的源代码,您就会知道。* [ORLY?!?!](https://github.com/lattera/glibc/ blob/895ef79e04a953cac1493863bcae29ad85657ee1/stdio-common/vfprintf.c#L1025) 您可能只想在发布内容之前就阅读源代码提出自己的建议。 (5认同)
  • @stefanct 这是一个玩具示例。实际上,任何非玩具程序都会在某个时候锁定 stdout。使用这个信号处理程序,可能发生的最坏情况是段错误上的死锁,但如果您目前没有机制在您的用例中杀死流氓进程,这可能已经够糟糕了。 (3认同)
  • 根据 [2.4.3 Signal Actions](http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03),从信号处理程序中调用 printf,该处理程序因非法间接调用而被调用,程序是否是多线程只是简单的_未定义行为_周期。 (3认同)
  • @TimSeguine:事实并非如此。您只需要确保您知道自己在做什么即可。`signal(7)`列出了所有异步信号安全功能,这些功能可以使用得很少。在上面的示例中,它也是完全安全的,因为程序中的其他任何东西都不会触及“ stdout”,而是会调用处理程序中的“ printf”调用。 (2认同)
  • 你说的是一般的 I/O;没有限制; 根本没有给出任何理由。创建新文件(流)并在那里输出信息没有任何错误或危险,也不会直接写入`STDOUT_FILENO`。因此,您的评论具有误导性,这就是我所批评的。 (2认同)
  • @stefanct 您忽略了上下文。我没有说任何关于通用 I/O 的事情。但是既然你提出来了:读写有同步问题。它们在异步代码中的使用非常重要,并且从一个有缺陷的玩具示例开始,基本上说“看看这有多容易”,这确实是灾难的秘诀。我不明白你会如何期望有人从货物崇拜信号处理代码神奇地变成领域专家并考虑到每一件小事。我想传达“不要复制这个例子”的信息。如果没有遇到,那是我唯一的遗憾。 (2认同)
  • 哈哈,“灾难”已经发生了。您不妨尝试告诉用户发生了什么。此外,99% 的段错误都是尝试取消引用 NULL 指针,因此打印出相关指针的值(如果您的处理程序是本地化的)在大多数情况下都可以识别导致问题的特定变量。打印出来是关键,因为一旦软件发布,您就无法访问用户的计算机,因此您必须依赖他们告诉您的内容。如果他们告诉您他们收到一条消息,指出指针 XYZ 为空,那么这就是关键信息。 (2认同)

Jul*_*tte 12

为了可移植性,可能应该使用std::signal标准的 C++ 库,但是信号处理程序可以做什么有很多限制。不幸的是,不引入未定义行为的情况下从 C++ 程序中捕获 SIGSEGV 是不可能的,因为规范说:

  1. 除了标准库函数的一个非常狭窄的子集(abort, exit,一些原子函数,重新安装当前信号处理程序,memcpymemmove,类型特征,std::movestd::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 的调用所调用的延续不能可靠地调用通常有用的库函数,例如printf,mallocexit从 main 返回,而不会引起未定义的行为。因此,continuation 只能进行受限操作,并且只能通过某种异常终止机制退出。

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

最后但并非最不重要的是,在取消引用空值指针(或无效值指针)时是否会引发任何 SIGSEGV 不是标准的要求。因为通过空值指针或任何无效值指针的间接性是未定义行为,这意味着编译器假定您的代码永远不会在运行时尝试这样的事情,编译器可以自由地进行代码转换以消除此类未定义行为。例如,从 cppreference,

int foo(int* p) {
    int x = *p;
    if(!p)
        return x; // Either UB above or this branch is never taken
    else
        return 0;
}
 
int main() {
    int* p = nullptr;
    std::cout << foo(p);
}
Run Code Online (Sandbox Code Playgroud)

在这里,if编译器可以完全忽略的真实路径作为优化;只能else保留一部分。否则,编译器推断foo()永远不会在运行时收到空值指针,因为它会导致未定义的行为。使用空值指针调用它,您可能会观察到0打印到标准输出的值并且没有崩溃,您可能会观察到 SIGSEG 的崩溃,实际上您可以观察到任何事情,因为没有对没有未定义行为的程序强加合理的要求.


rev*_*evo 7

这里找到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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我知道这只是一个你没有写的例子,但是在信号处理程序中执行IO是一种灾难. (6认同)
  • @TimSeguine:重复最具有误导性的内容不是一个好主意(参见 /sf/ask/164534261/#comment81651055_2436368) (3认同)
  • @stefanct 在信号处理程序中安全使用 printf 所需的预防措施并非微不足道。没有任何误导。这是一个玩具示例。即使在这个玩具示例中,如果您对 SIGINT 进行正确计时,也可能会出现死锁。死锁之所以危险,正是因为它们很少见。如果您认为此建议具有误导性,请远离我的代码,因为我不信任您。 (3认同)
  • @stefanct如果你想挑剔并忽略语句的上下文,那么这就是你的问题。谁说我在谈论一般性的 I/O?你。我只是对人们发布难题的玩具答案有一个大问题。即使在使用异步安全函数的情况下,仍然有很多事情需要考虑,这个答案让它看起来微不足道。 (2认同)

184*_*615 5

有时我们想抓住一个 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);
}
Run Code Online (Sandbox Code Playgroud)

另一种选择是读取内存保护属性,这有点棘手(适用于 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;
}
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

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

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