LoginSignup
7

More than 3 years have passed since last update.

posted at

SIGSEGVをハンドリングした時のsi_addrは時として信用できない

まずは以下のCプログラムを実行してみましょう。

#include <stdio.h>
#include <string.h>

int main() {
    void* ptr = (void *)(1UL<<48);
    fprintf(stderr, "ptr=%p\n", ptr);
    memset(ptr, 'a', 1);
    return 0;
}

もちろん、以下のようにSEGVで落ちることでしょう。

ptr=0x800000000000
Segmentation fault

さて、このプログラムに対してsigaction()システムコールを使ってSIGSEGVをハンドリングするコードを書き加えてみます。
ハンドリングの具体的な方法については割愛します。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void sigac(int signo, siginfo_t *siginfo, void *ptr) {
    fprintf(stderr, "si_addr=%p\n", siginfo->si_addr);
    exit(1);
}

int main() {
    void* ptr = (void *)(1UL<<47);
    struct sigaction act;
    act.sa_handler = SIG_DFL;
    act.sa_sigaction = sigac;
    act.sa_flags = SA_SIGINFO | SA_RESTART | SA_NODEFER;
    sigprocmask(SIG_BLOCK, NULL, &act.sa_mask);
    sigaction(SIGSEGV, &act, NULL);

    fprintf(stderr, "ptr=%p\n", ptr);
    memset(ptr, 'a', 1);
    return 0;
}

このコードを実行した時に、多くの人は以下の出力を期待するでしょう。

ptr=0x800000000000
si_addr=0x800000000000

しかし現行(2018年10月現在)のほとんど(全て?)のIntel製64ビットCPUでは以下のような出力になると思われます。

ptr=0x800000000000
si_addr=(nil)

(nil)ということは、siginfo->si_addrには0x800000000000ではなく0x0が代入されているということです。

void* ptr = (void *)(1UL<<47);

この挙動のヒントはここにあります。ここでピンときた人はおそらく正解までたどり着いているでしょう。
この値を1つ小さくしてみましょう。つまり、当該部分のコードを以下のように変更します。

void* ptr = (void *)((1UL<<47)-1);

このようにすると当初の想定通り、

ptr=0x7fffffffffff
si_addr=0x7fffffffffff

と出力されます。

これらの事象の原因は現行のIntel製64ビットCPUでサポートされている仮想メモリ空間が64ビットでなく48ビットであることにあります。
48ビットを超える範囲の仮想メモリアドレスで何かをやろうとすると、範囲外アクセスによってSEGVが発生します。しかし、これをハンドリングしようとするとヌルポインタ操作を行った場合と同様の挙動を示してしまうようです。
(この挙動についてIntel側のマニュアル等に書いてあったらどなたか教えてください……)
しかしながらC言語上では何も問題がないため、メイン関数内の方のfprintfは普通に出力してくれます。

ちなみに、

void* ptr = (void *)((1UL<<47)+1);

とすると

ptr=0x800000000001
si_addr=(nil)

のようになります。せめて0x1になって欲しいところではありますが。

ちなみにARM64をはじめとする、他の64ビットプロセッサの多くも仮想メモリ空間が48ビットらしいのですが、手元にARM64環境がないので検証できず。

結論としては si_addr0であっても、アクセスしようとしていた仮想メモリアドレスが0x0であるとは限らない ということです。
もし理解不能なヌルポインタ系のエラーで引っかかったら、si_addrの値を調べる以外の方法で、アクセスしようとしていた仮想メモリアドレスが0x800000000000以上でないかを確認しましょう。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7