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