Linuxでセグメンテーション違反をキャッチする方法は?


82

サードパーティのライブラリクリーンアップ操作でセグメンテーション違反をキャッチする必要があります。これは、プログラムが終了する直前に発生することがあり、この本当の理由を修正することはできません。Windowsプログラミングでは、これは__try --__ catchで実行できます。同じことを行うためのクロスプラットフォームまたはプラットフォーム固有の方法はありますか?Linux、gccでこれが必要です。


セグメンテーション違反は常に、キャッチするのが非常に難しいバグによって引き起こされます。ランダムに表示されるものを見つけました。各ファイルには5億のデータポイントがあります。およそ10〜15ファイルごとに、このセグメンテーション違反が発生します。マルチスレッド、ロックフリーキューなどを使用していました。非常に複雑なジョブ管理。結局、それは私が作成したオブジェクト、std :: moved()を別のデータ構造にしたものです。ローカルでは、移動後にこのオブジェクトを使用していました。どういうわけか、C ++はこれでOKです。しかし、セグメンテーション違反はいつか確実に現れるでしょう。
Kemin Zhou

回答:


77

Linuxでは、これらを例外として使用することもできます。

通常、プログラムがセグメンテーション違反を実行すると、SIGSEGVシグナルが送信されます。このシグナルに対して独自のハンドラーを設定し、結果を軽減することができます。もちろん、あなたは本当にあなた状況から回復できることを確信するべきです。あなたの場合、代わりにコードをデバッグする必要があると思います。

トピックに戻ります。最近このようなシグナルを例外に変換するライブラリ短いマニュアル)に遭遇しので、次のようなコードを記述できます。

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

しかし、それをチェックしませんでした。x86-64Gentooボックスで動作します。プラットフォーム固有のバックエンド(gccのJava実装から借用)があるため、多くのプラットフォームで動作できます。そのままでx86とx86-64をサポートしますが、gccソースにあるlibjavaからバックエンドを取得できます。


16
以下のための+1必ずSIGのセグメンテーションフォルトをキャッチする前に回復できるということ
ヘンリックMühe

15
シグナルハンドラーからスローすることは非常に危険なことです。ほとんどのコンパイラは、呼び出しのみが例外を生成できると想定し、それに応じてアンワインド情報を設定します。JavaやC#のように、ハードウェア例外をソフトウェア例外に変換する言語は、何でもスローできることを認識しています。これはC ++には当てはまりません。GCCを使用する場合、少なくとも-fnon-call-exceptionsそれが機能することを確認する必要があります。それにはパフォーマンスコストがかかります。また、例外サポートなしの関数(C関数など)からスローされ、後でリーク/クラッシュする危険性もあります。
zneak 2015年

1
私はzneakに同意します。シグナルハンドラーからスローしないでください。
MM。

ライブラリは現在github.com/Plaristote/segvcatchにありますが、マニュアルが見つからないか、コンパイルできませんでした。./build_gcc_linux_releaseいくつかのエラーが発生します。
alfC 2016

わーい!今、私は世界で唯一の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 ++エラーが発生します。
デイブドプソン2011

7
シグナルハンドラーでIOを実行することは、災害のレシピです。
Tim Seguine 2016年

6
@TimSeguine:それは真実ではありません。自分が何をしているのかを確認する必要があります。signal(7)比較的注意を払わずに使用できるすべての非同期シグナルセーフ関数をリストします。上記の例では、プログラム内の他の何も触れてstdoutいないのでprintf、ハンドラー内の呼び出し以外も完全に安全です。
stefanct 2017年

3
@stefanctこれはおもちゃの例です。事実上、おもちゃ以外のプログラムは、ある時点でstdoutのロックを保持します。このシグナルハンドラーを使用すると、おそらく発生する可能性のある最悪の事態はsegfaultのデッドロックですが、ユースケースで不正なプロセスを強制終了するメカニズムが現在ない場合は、それで十分です。
Tim Seguine 2017年

3
2.4.3シグナルアクションによると、プログラムがマルチスレッドであるかどうかに関係なく、不正な間接参照の結果として呼び出されるシグナルハンドラ内からprintfを呼び出すことは、単なる未定義の動作期間です。
ジュリアンVillemure、フレシェット

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 2017年

3
@stefanctシグナルハンドラでprintfを安全に使用するために必要な注意事項は簡単ではありません。それについて誤解を招くものは何もありません。これはおもちゃの例です。そして、このおもちゃの例でも、SIGINTのタイミングを正しく合わせると、デッドロックが発生する可能性があります。デッドロックはまれであるため、正確には危険です。このアドバイスが誤解を招くと思われる場合は、コードから1マイル以内で信頼できないため、コードに近づかないでください。
Tim Seguine 2017年

ここでも、I / O全般について話していました。この実際の例の問題を指摘する代わりに、それは確かに悪い例です。
stefanct 2017年

1
@stefanctステートメントのコンテキストを無視して無視したい場合は、それが問題です。I / O全般について話していると誰が言ったのですか?君は。難しい問題におもちゃの答えを投稿する人たちに大きな問題があります。非同期セーフ関数を使用する場合でも、考えることがたくさんあり、この答えはそれが些細なことのように思えます。
Tim Seguine 2017年

8

移植性のために、おそらくstd::signal標準C ++ライブラリから使用する必要がありますが、シグナルハンドラが実行できることには多くの制限があります。残念ながら、仕様には次のように記載されているため、未定義の動作を導入せずにC ++プログラム内からSIGSEGVをキャッチすることはできません。

  1. 標準ライブラリ関数の非常に狭いサブセット(以外のハンドラ内から任意のライブラリ関数を呼び出すこと未定義の動作であるabortexitいくつかの原子の機能は、現在のシグナルハンドラを再インストールし、memcpymemmove、タイプの特徴、 `のstd ::移動, std::forward、およびいくつかのより多くの)。
  2. ハンドラーがthrow式を使用する場合、これは未定義の動作です。
  3. SIGFPE、SIGILL、SIGSEGVを処理するときにハンドラーが戻る場合、これは未定義の動作です。

これは厳密に標準で移植可能なC ++を使用してプログラム内からSIGSEGVをキャッチすることは不可能であることを証明しています。SIGSEGVは引き続きオペレーティングシステムによってキャッチされ、通常、待機時に親プロセスに報告されます。ファミリ関数が呼び出されたれます。

2.4.3シグナルアクションに次のような句があるため、POSIXシグナルを使用すると同じ種類の問題が発生する可能性があります。

それによって生成されなかったSIGBUS、SIGFPE、SIGILL、またはSIGSEGV信号の信号捕捉関数から正常に戻った後、プロセスの挙動が定義されていないkill()sigqueue()またはraise()

について一言longjump。POSIXシグナルを使用longjumpしていると仮定すると、スタックの巻き戻しをシミュレートするためにを使用しても役に立ちません。

けれどもlongjmp()、それが(例えばするための処理と等価の非非同期シグナルセーフ機能または同等の中断シグナルハンドラから呼び出された場合、非同期シグナル安全関数でexit()の最初の呼び出しから復帰した後に行うとmain())、非同期シグナルセーフでない関数または同等の関数への後続の呼び出しの動作は定義されていません。

つまり、longjumpの呼び出しによって呼び出された継続は、などの通常は有用なライブラリ関数を確実に呼び出すことはできませんprintfmallocまたはexit、未定義の動作を引き起こさずにmainから戻ることはできません。そのため、継続は制限された操作のみを実行でき、何らかの異常な終了メカニズムを介してのみ終了する可能性があります。

簡単に言うと、SIGSEGVキャッチして、ポータブルでプログラムの実行を再開することは、UBを導入しないとおそらく実行不可能です。構造化例外処理にアクセスできるWindowsプラットフォームで作業している場合でも、MSDNがハードウェア例外の処理を決して試みないことを提案していることに言及する価値があります。ハードウェア例外


ただし、SIGSEGVはハードウェアの例外ではありません。親がカーネルによって殺された子のケースを検出し、IPCを使用して関連するプログラムの状態を共有して、離れたところから再開できる親子アーキテクチャをいつでも使用できます。最近のブラウザは、IPCメカニズムを使用して、ブラウザタブごとに1つのプロセスと通信するため、このように見ることができると思います。明らかに、プロセス間のセキュリティ境界は、ブラウザのシナリオではボーナスです。
0xC0000022L

5

時々私たちはキャッチしたい SIGSEGVポインタが有効かどうか、つまり、有効なメモリアドレスを参照しているかどうかを確認場合があります。(または、任意の値がポインターであるかどうかを確認することもできます。)

1つのオプションはそれをチェックすることです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);
}

もう1つのオプションは、メモリ保護属性を読み取ることです。これは少し注意が必要です(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()を呼び出すのは良い考えではないかもしれません-関数が戻るまでメモリが解放されないかもしれません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.