更新記録
(2004.04.12)
3/6、
3/11、
3/13 の日記をまとめて作成。
(2004.05.07) 文章を修正。サンプルコードを追加。
(2005.01.20) alternative → alterante に修正。
(2005.02.13) 追記を記述。
(2006.02.17) linux_stack_info.cpp の実装に誤りがあったので修正。
(2006.02.19) BSD 系OS でのスタック領域情報の取得の仕方を追加
初めに
C/C++ でプログラムをしているとつい忘れてしまうのが
スレッドのスタックオーバーフローの問題。
最近の OS はスレッド当たり 2〜8MB のスタック領域を持っているため、
よほどのことがない限りスタックが溢れてしまうことはない。
だが、再帰や alloca を積極的に使うような
プログラムではスタックオーバーフローを気にすべきだ。
スタックのオーバーフローが起きた場合でもプログラム中で事故処理を行い、
適切にリカバリーできるようにしたい。
一般的な OS では
スレッドが生成された時に
所定のサイズの仮想メモリ空間が割り付けられる。
これがスタックが取りうる最大サイズになる。
実際には、
仮想メモリの仕組みを利用してスタックの最初のページにだけ実メモリが割り付けられる。
実行が進みスタックが伸びてくると
メモリが割り当てられていないメモリページへアクセスが起きる。
CPU はアクセスバイオレーションの例外を発効し、
それを OS がハンドリングして
スタックの未マップ領域にメモリを割り当てることになる。
このスタック管理は
Windows、Solaris、Linux では
ユーザープロセスからは
隠蔽されている。
そしてスレッドに割り当てられたスタックの最大サイズを越えてアクセスが発生しそうになった場合には、
UNIX 系では SEGV シグナル、
Win32 API では EXCEPTION_STACK_OVERFLOW 構造化例外がスローされる。
この文書では主に UNIX 系 OS (Linux、Solaris) で スタックオーバーフローをハンドリングする方法について述べる。 WindowsNT 系についても少しだけ書く。
- 代替シグナルスタック (Alternate Signal Stack)
スタックオーバーのハンドリングのためには代替シグナルスタックを設定する必要がある。
- スタック領域情報の取得
SEGV シグナルを補足した場合、シグナルハンドラにバイオレーションが発生したアドレスが渡される。
このアドレスがスタック内なのか外なのかを知るために スタックの存在するアドレス範囲を把握する必要がある。
- スタックのユーザー制御
スタックオーバーフロー後に追加の処理を行うためには スタックを本当に使い切ってしまうとまずい。 そのためスタックに「余白」を作る必要がある。
- スタックオーバーフローからの強制復帰
スタックオーバーフローが発生した時に、 あらかじめ登録された復帰ポイントに強制的に戻る方法について述べる。
- Windows NT 系の場合
以下に出てくるサンプルは C++ 言語です。
1. 代替シグナルスタック (Alternate Signal Stack)
UNIX 系 OS でスタックオーバーフローをハンドリングのためには
SEGV シグナルを補足する必要がある。
現在の UNIX 系 OS はスタックオーバーフローを起こしたスレッド自身が SEGV シグナルを補足するようにできている(ものがほとんどだ)。
しかし、シグナルハンドラ処理はシグナルを補足したスレッドのスタックを利用するため、
いざ SEGV 例外が発生した場合には、シグナルハンドラを呼び出すスタックフレームを置くメモリがないということになる。
この問題に対処するために UNIX 系 OS では
代替シグナルハンドラ (alternate signal stack) の機構を用意している。
これはシグナルハンドラが
予め用意された通常とは異なるスタックを使用する機構である。
Linux / Solaris では代替シグナルスタックは明示的に設定を行った時にのみ利用される。
Windows では同様の機構がデフォルトで用意されている。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#define ALT_STACK_SIZE (4096*8)
#define ALLOCA_SIZE (1024*2)
/* シグナルハンドラ */
static void signal_handler(int sig, siginfo_t* sig_info, void* sig_data) {
if( sig == SIGSEGV ) {
fprintf(stderr, "SEGV: %p\n", sig_info->si_addr );
fflush(stderr);
exit(1);
}
/* ここで位置で以前に登録されたシグナルハンドラを
呼び出す処理を行ってもよい。 */
}
/* わざとスタックオーバーフローを発生させる */
static void cause_stack_overflow() {
cause_stack_overflow();
}
/* メイン関数 */
int main(int argc, char** argv) {
/* 代替シグナルスタックのセット */
stack_t ss ;
/* alternative signal stack としてメモリを確保 */
ss.ss_sp = malloc(ALT_STACK_SIZE);
ss.ss_size = ALT_STACK_SIZE;
ss.ss_flags = 0;
if( sigaltstack(&ss, NULL)) {
/* error */
fprintf(stderr, "Failed sigaltstack\n" );
exit(1);
}
/* シグナルハンドラのセット */
struct sigaction newAct, oldAct;
/* 古いシグナルハンドラを退避 */
if( sigaction( SIGSEGV, NULL, &oldAct ) == -1 ){
/* error */
fprintf(stderr, "Failed to acquire the current sigaction_t.\n" );
exit(1);
}
/* 新しいシグナルハンドラをセット */
sigemptyset(&newAct.sa_mask);
sigaddset(&newAct.sa_mask, SIGSEGV);
newAct.sa_sigaction = signal_handler;
newAct.sa_flags = SA_SIGINFO|SA_RESTART|SA_ONSTACK;
if( sigaction( SIGSEGV, &newAct, NULL ) == -1 ){
/* error */
fprintf(stderr, "Failed to set my signal handler.\n" );
exit(1);
}
/* わざとスタックオーバーフローを発生させる */
cause_stack_overflow();
return 0;
}
この時点ではまだシグナルをハンドラしただけで、 スタックオーバーフローに対処したとはいえない。 以下のような問題がある。
- SEGV はスタックオーバーフロー以外でも送出されるので、
スタックオーバーフローとそうでない場合の切り分けが必要。
(Windows ではEXCEPTION_STACK_OVERFLOWとEXCEPTION_ACCESS_VIOLATIONは 区別される) - 実際のプログラムでは
シグナルハンドラ内ですべての問題に対処するわけには行かないので、
もう一度スタックオーバーフローを起こしたコンテキストに戻る必要がある。
だが、 元のコンテキストに戻ると代替シグナルスタックは効かないので スタックオーバーフローのままである。
この問題は Windows でも存在する (が、Windows では少しだけ楽をして対処可能)。 - スタックオーバーフローが発生した場所とは 異なる場所から実行を再開したい場合がある。
- 特定のバージョンの Linux ではこのコードは正しく動作しない。 その原因を Part 2 で述べる。
2. スタック領域情報の取得
スレッドのスタックがメモリ上のどこにあるか判定する方法について述べる。
2.1 Linux
Linux のスタック事情は大変複雑だ。
Linux のスレッドの特性は、
使用されるスレッドライブラリによって大きく変わってくる。
現在までに以下のようなライブラリが使用されてきた (はず)。
種類 特徴 ディストリビューション libc5 (筆者はこの時代のことは良く知らない) 古い Slackware など glibc2 LinuxThreads
(fixed stack)スレッドを同一メモリ空間を共有する 複数のプロセスとして実装。 管理スレッドを必要とする。
マルチスレッド使用時の スタックサイズが 2 MB に固定。RedHat Linux v7.0 以前
Debian
Vine Linux 2.6 以前LinuxThreads
(floating stack)スレッドを同一メモリ空間を共有する 複数のプロセスとして実装。 管理スレッドを必要とする。
マルチスレッド使用時の スタックサイズを自由に選択できる。RedHat Linux v7.1 〜 v8
RedHat Linux AS v2.1 以前NPTL カーネルレベルでのスレッドのサポート。
マルチスレッド使用時の スタックサイズを自由に選択できる。
- 管理スレッドが不要に
- シグナルが POSIX 準拠に
- 同期オブジェクトのカーネルサポート
- 同一プロセスのスレッドが同じプロセスIDを持つようになった。
RedHat Linux v9 以降
RedHat Enterprise Linux v3 以降
現在では libc5 は使われなくなっているので、
さすがに気にしなくてもよいと思われる。
glic2 では大きく分けて 2 種類
Linux スレッド (LinuxThreads) と
Native POSIX スレッドライブラリ (NPTL) がある。
NPTL は最近になって出てきた ちゃんとした POSIX スレッドライブラリ で比較的問題は少ない。 よって NPTL では Part1 のコードは正常に動作する。
LinuxThreads もバージョン間でずいぶん仕様が異なっている。 まず、LinuxTheads のバージョンによって fixed stack と floating stack に二分される。 fixed stack は、スタックのサイズが 2MB に固定されていて変更不能である。 floating stack は、スタックサイズを変更することが可能になっている。
Linux のスレッドライブラリの最大の問題は、
現在システムにインストールされているスレッドライブラリが
どのようなバージョン・種類なのか明示的に確認する方法がないことだ
(例えばスレッドライブラリをバージョン番号を返す API などがあればよいのだが)。
スレッドライブラリの特定には、
いささか ad hock な手法を使う必要がある。
- スレッドを使用しないプロセス
-
まずスレッドを使用しない Linux プロセスは、 プロセスに 1 つだけのスレッドを持っている。
この場合のプロセスのスタックは、 仮想メモリ空間のアドレス 0xC0000000 から始まり下向きに伸びてゆく。 プロセスのスタックサイズはリソースによって決定され、 tcsh ではlimit stacksize <n>、 bash ではulimit -s <n>を指定することで変更できる。
デフォルトのスタックサイズは 8192K なので[0xB800000,0xC0000000)が スタックサイズになる。リソースリミットからスタックサイズを取得するには 以下のように
getrlimitを用いる。
rlim.rlim_curにスタックサイズがバイト数で格納される。struct rlimit rlim; getrlimit(RLIMIT_STACK, &rlim);
- fixed stack の LinuxThreads
-
プロセスがスレッドを使う場合、libpthread.so をアタッチする。 この共有ライブラリがプロセスにアタッチされると、 スタックの使用の仕方が変わる。
- libpthread.so がアタッチされると、
プロセス起動時にあったスレッド(?) がメインスレッドとなる。
そしてメインスレッドのサイズの上限が 2MB に制限される。
ただしリソースリミットを指定することで、 メインスレッドのスタックサイズを 2MB から減らすことは可能。 pthread_createを使って新しいスレッドを作成すると、 0xC0000000 から下のアドレス空間に 2MB のメモリを確保し、 新しいスレッドのスタックとする。
新たに作られたスレッド (以後、派生スレッドと呼ぶことにする) は リソースリミットのスタックサイズ指定の影響を受けない。
fixed stack は古い Linux スレッドの設計で、 スタックのサイズを 2MB に固定し 2MB 境界に揃えて配置する。 そのため fixed stack では (SP レジスタの値) & (2MB -1) を 計算することでスタック領域を識別することができる。 この 「スタックレジスタからスタック領域判定でき、 そこからスレッドも識別もできる」という 特性を利用してしまったライブラリがある (らしい)。 そのため SP レジスタを固定の 2MB の以外の場所に変更すると エラーとなる可能性がある。
この制約は 代替シグナルスタックの使用方法とバッディング する。
これは このような hack で回避することが可能。 - libpthread.so がアタッチされると、
プロセス起動時にあったスレッド(?) がメインスレッドとなる。
そしてメインスレッドのサイズの上限が 2MB に制限される。
- floating stack の LinuxThreads
-
floating stack の LinuxThreads も libpthread.so の形で提供されている。 プロセスが libpthread.so をアタッチすることでスレッドの使用が始まる。
- floating stack の場合には、
リソースリミットで制限したプロセスのスタックサイズが
デフォルトのスタックサイズとなる
(デフォルトは 8192K のことが多い)。
そのため libpthread.so 前のプロセススタックが、 メインスレッドのスタックになると考えてよい。 pthread_createを使って新しいスレッドを作成すると、 0xC0000000 から下のアドレス空間の中に指定サイズのメモリを取って 新しいスレッドのスタックとする。
指定がない場合にはデフォルト値が採用される。
floating stack ではスタックを 2MB 境界に置かねばならないという制限はない。
- floating stack の場合には、
リソースリミットで制限したプロセスのスタックサイズが
デフォルトのスタックサイズとなる
(デフォルトは 8192K のことが多い)。
- NPTL
-
NPTL はスタック周辺の動作は floating stack と同様。
このような複数のスタックシステムを区別しながら、 動的にスタックのサイズを取得するためには 以下のような方法を用いればよい。
pthread_getattr_npを使用したスタック領域情報の取得-
POSIX にはスレッドのアトリビューションから スタックの位置とサイズを読み取る API
pthread_attr_getstackaddr、pthread_attr_getstacksizeが用意されている。 ただスレッドアトリビューションは スレッドを新規に作成するときに与えるものであって、 実行中に動的に取得することは (POSIX の仕様上は) できない。ただし glibc の特定バージョンからは、 POSIX の仕様外の API として 動作中のスレッドからスレッドアトリビューションを取得する
pthread_getattr_npが用意された。 これを用いてスタック領域の取得にせまる。 - fixed stack と floating stack の判別方法
-
まず floating stack が実装されたのは LinuxThreads v0.9 以降である。
pthread_getattr_npAPI は、 このバージョンから実装されている。
そこで共有ライブラリ中からdlsymAPI を用いてシンボルを探すことで、 現在稼動しているシステムにpthread_getattr_npが存在するかどうかをチェックする。pthread_getattr_npがなければ fixed stack であると断定できる。typedef int (*PTHREAD_GETATTR_NP_FUNC)(pthread_t, pthread_attr_t *); PTHREAD_GETATTR_NP_FUNC pthread_getattr_np_func = NULL; pthread_getattr_np_func = (PTHREAD_GETATTR_NP_FUNC) dlsym(RTLD_DEFAULT, "pthread_getattr_np" ); if( pthread_getattr_np_func != NULL ) { // floating stack } else { // fixed stack }ただし LinuxThreads をビルドする際に、 オプションで fixed stack を選ぶことも可能である。
そのためpthread_getattr_npが存在した場合に必ず floating stack であるとは限らない。 Debian や Vine Linux 2.6 は 新しい glibc/LinuxThreads を採用していても fixed stack になっている。今のところシステムのスレッドライブラリが floating stack なのか、 それとも fixed stack なのかを見分ける確実な方法はないようだ。
pthread_getattr_npの問題-
pthread_getattr_npAPI が存在する場合、 以下のようなプログラムを使用すると スタックの開始ポイント(最下位アドレス)とサイズを取得できる。pthread_attr_t attr; if( pthread_getattr_np_func(pthread_self(), &attr) ){ fprintf(stderr, "Failed pthread_getattr_np().\n" ); fflush(stderr); exit(2); } void* stack_base = 0; size_t stack_size = 0; // Method 1. pthread_attr_getstackaddr(&attr, &stack_base); pthread_attr_getstacksize(&attr, &stack_size); // Method 2. pthread_attr_getstack(&attr, &stack_base, &stack_size);ただし、 この
pthread_getattr_npによってアトリビューションが正しく取得できるのは派生スレッドのみで、 メインスレッドpthread_getattr_npを使っても不正確な値しか手に入らない。
メインスレッドだけはリソースリミットからスタックサイズを得る必要がある。注意!
スタックハンドラ内でpthread_getattr_npを使用した場合、 代替シグナルスタックを使っている場合でも (代替でない) ノーマルなスタック情報が手に入る。 - メインスレッドのスタックサイズ
-
メインスレッドのスタックサイズを取得するには、 プロセススタックと同様に
getrlimitAPI によって リソースリミットを取得する。floating stack の場合には プロセススタックサイズ = メインスレッドスタックサイズとなるため
rlim.rlim_curの値がそのまま使える (リソースリミットで 8192KB を指定すれば 8192 * 1024 が入っている)。fixed stack の場合には、 スタックサイズからガード領域のサイズが引いた値が返ってくる。 これは libpthread.so アタッチ後のスタックサイズに近い値となる。
スタックガード領域は Part 3 で説明するが、 通常 1 仮想記憶ページ。x86/Linux では 4kB なためgetrlimitから取得できるスタックサイズは 2044KB と出る。メインスレッドのスタック開始位置は固定 (0xC0000000) である。
以上をまとめると、 Linux のスタックの位置とサイズを求めるプログラムは 以下のようになる。
サンプルプログラム1
linux_stack_info.cpp
2.2 Solaris
Solaris の場合は thr_stksegment を使用する。
#include <thread.h>
#include <sys/signal.h>
/* スタック領域情報の格納先となる*/
stack_t st;
if (thr_stksegment(&st) == 0) {
/* スタックの天井 (スタック領域の上限となるアドレス) */
char* stack_base = (char*)st.ss_sp;
/* スタックの底 (スタック領域の下限となるアドレス) */
char* stack_end = (char*)st.ss_sp - st.ss_size;
}
バージョン 8 以前の Solaris には、 メインスレッドで thr_stksegment を複数回呼ぶと正しく結果を返さないバグがあったようだ。 メインスレッド内で thr_stksegment を呼ぶのは 1 回だけに抑えた方がよい。
2.3 Windows
Windows の場合は、 スタック情報は各スレッド毎に存在する thread information block (TIB) に記述されている。 以下のようなコードでアクセス可能(ソースコードは VC を想定)。
/* TIB を取得する関数 */
NT_TIB* getTIB(void) {
NT_TIB* pTib;
__asm {
mov eax, dword ptr FS:[18H];
mov pTib, eax;
}
return pTib;
}
/* スタック領域の判定 */
NT_TIB* pTIB = getTIB();
printf("[%08x %08x]\n", pTIB->StackBase, pTIB->StackLimit);
NT_TIB の詳細は
プラットフォーム SDK や Visual Studio の
インクルードファイル中にある WinNT.h を参考に。
2.4 FreeBSD、OpenBSD、MacOS X
BSD 系 OS でもスタック領域情報の取得の仕方はバラバラのようだ。 以下のような _np のサフィックスのついた API が用意されている。
| OS | API | インクルードファイル |
|---|---|---|
| FreeBSD | pthread_attr_get_np | pthread_np.h |
| OpenBSD | pthread_stackseg_np(3) | pthread_np.h & sys/signal.h |
| MacOS X | pthread_get_stacksize_np pthread_get_stackaddr_np | pthread.h |
以下は MacOS X と FreeBSD でのコード例。
#include <pthread.h>
int get_macosx_stack_info(void** stackaddr, size_t* stacksize) {
size_t size = pthread_get_stacksize_np(pthread_self());
void * addr = pthread_get_stackaddr_np(pthread_self());
*stackaddr = (char*)addr - size;
*stacksize = size;
return 0;
}
#include <pthread_np.h>
int get_freebsd_stack_info(void** stackaddr, size_t* stacksize) {
int ret = 0;
pthread_attr_t attr;
if ((ret = pthread_attr_init(&attr)) == 0) {
ret = pthread_attr_get_np(pthread_self(), &attr) ||
pthread_attr_getstackaddr(&attr, &stackaddr) ||
pthread_attr_getstacksize(&attr, &stacksize);
pthread_attr_destroy(&attr);
}
return ret;
}
3. スタックのユーザー制御
Part 1 の代替シグナルスタックを設定し、 SEGV シグナルを補足したシグナルハンドラ内で Part 2 の判定を行えば、 スタックオーバーフローを補足できる。 この後、 シグナルハンドラ内で exit/abort するような終了処理を行うのであれば、 スタックオーバーフローハンドリングは Part 2 までの処理でおしまい。 しかし、 スタックオーバーフローが発生した時に何らかの処理をした後に元のコンテキストに戻りたい場合には、 さらに工夫が必要になる。
シグナル発生後の処理の継続方法としては、 以下の二通りの方法が考えられる。
- (a) シグナルハンドラー内で 「スタックオーバーフローが発生しましたよ」という警告フラグを立てておいてシグナルハンドラを脱出。 元のコンテキストに戻って処理を再開。
- (b) 通常のコンテキストの再開ポイントを
sigsetjmpを使って記録しておき、 シグナルハンドラ内でsiglongjmpを 使ってそこにジャンプすることで再開する。
まず (a) の実現方法について説明する。 だが、その前にスタックオーバーフローが発生する機構を再考してみる。
スタックオーバーフローは「スタック領域が溢れて(越えて)しまった」というニュアンスがあるが、 大概の OS ではスタックオーバーフローの不正アクセス例外はスタック領域の中で発生するように実装されている。 OS またはスレッドライブラリは スタック領域の最後 (普通はスタック領域の最下位アドレス) に 仮想記憶 1ページ分の ガード領域 を設けていて、 このページの属性を読み込み・書き込み・実行に設定する。 スタックが伸びようとしてガード領域と接触した場合SEGV シグナルが発生し、 「もうこれ以上はスタックは伸ばせません」ということが通知される。
独自のガード領域の設定
(a) の実現方法のために、 ユーザーがガード領域を自由に設定する方法を考える。
- (1) システム定義ガード領域のサイズの確認
-
まず POSIX に準拠するスレッドライブラリの場合、 スタックの最終ページをガード領域として設定しているはずである。 このような システム定義ガード領域 と呼ぶことにする。 このシステム定義ガード領域の大きさを何らかの方法によって予め調べておく。
# Pthread ライブラリのバージョンによっては、pthread_attr_getguardsizeAPI を用いて動的に取得できる。 - (2) ユーザー定義ガード領域の設定
-
ユーザー独自のガード領域は、 システム定義ガード領域の直上の領域に 仮想記憶ページの整数倍となるように設定する。 i486/Linux の場合、 スタック領域は上位アドレスから下位アドレスに向けて 実メモリを貼り付けていくので、 この時点ではユーザーガード領域と定めた領域には 実メモリが貼り付けられていない。
ここでmmapAPI を用いて強制的に実メモリをり付けてしまう。 この時、 ページの属性をPROT_NONE(読み・書き・実行禁止) に設定してしまう。スタックとして使える領域 ユーザー定義ガード領域 システム定義ガード領域 - (3) ユーザーガード領域内でのアクセスバイオレーション
-
実行がはじまりスタックが激しく消費されると、 ユーザー定義ガード領域までスタックが伸びる。
ユーザー定義ガード領域は すでに実メモリが貼り付けてあるが、 属性が読み・書き・実行禁止のため SEGV シグナルが発生する。
- (4) シグナルハンドラで補足
-
シグナルハンドラ内で SEGV シグナルを確認し、 アクセスエラーの発生アドレスから ユーザー定義ガード領域内でアクセスバイオレーションが発生したことを確認する。
確認ができればmprotectAPI で ユーザー定義領域のアクセス権をPROT_READ | PROT_WRITEなど 必要な属性に変更する。また、 この SEGV シグナルは真のスタックオーバーフローの予兆的なシグナルなので、 ここからユーザー独自のスタックオーバーフロー処理を開始する。
- (5) 処理の再開
-
シグナルハンドラを脱出すれば アクセスバイオレーションが発生した位置から処理が再開する。
アクセスページの権限を変更したので、 ユーザー定義ガード領域は通常のスタック領域に戻っていて 実行を再開することが可能である。
以上のような流れで、 ユーザー定義ガード領域を使ったシグナルハンドリングを行えばよい。 ただし、 この方法ではユーザー定義ガード領域は通常のスタック領域に戻ったままなので、 どこかのポイントでユーザー定義ガード領域は再設定する必要がある。
下の例では元々 スレッドが持っていたスタック下半分を独自に制御している (完全なコードは StackOverflowHandling_linux.cpp を参照のこと)。
/* スタック領域を取得する */
get_linux_stack_info((void**)&stackaddr, &stacksize);
/*
スタックの中に独自のガード領域を設定する
ここではオリジナルのスタックの下半分をガード領域とする。
*/
start_new_guard = stackaddr + SYSTEM_GUARD_PAGE_SIZE;
end_new_guard = stackaddr + (stacksize / 2);
/* アライメントを合わせる */
start_new_guard = (char*)(((intptr_t)start_new_guard + VM_PAGE_SIZE -1) &(~(VM_PAGE_SIZE - 1)));
end_new_guard = (char*)(((intptr_t)end_new_guard) &(~(VM_PAGE_SIZE - 1)));
/* スタックの中に実メモリをコミットする。この際、アクセス権限を PROT_NONE にする */
if (mmap(start_new_guard, end_new_guard - start_new_guard,
PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) == (void*)-1 ){
fprintf(stderr, "faield mmap\n");
fflush(stderr);
exit(1);
}
printf( "New guard area: [%p, %p)\n", start_new_guard, end_new_guard);
/* わざとスタックオーバーフローを発生させる */
cause_stack_overflow();
/* シグナルハンドラ */
static void signal_handler(int sig, siginfo_t* sig_info, void* sig_data) {
if (sig == SIGSEGV & sig_info->si_code == SEGV_ACCERR) {
char* address = (char*)sig_info->si_addr;
/* ガードエリア内で発生した SIGSEGV か */
if (start_new_guard <= address &address < end_new_guard) {
/* スタックオーバーフローをハンドリング */
if (address < start_new_guard + VM_PAGE_SIZE) {
/* ガード領域内の最後の 1 ページなのでエラーメッセージを出して停止 */
fprintf(stderr, "Stack overflow!!\n");
fflush(stderr);
exit(1);
} else {
/* ユーザー制御スタック部分までスタックが伸びる */
char* start = (char*)(((intptr_t)address) &(~(VM_PAGE_SIZE - 1)));
printf("New memory mapped: [%p,%p", start, start + VM_PAGE_SIZE);
fflush(stdout);
/* アクセス権を替えてユーザー制御スタックを使用可能にする */
if (mprotect(start, VM_PAGE_SIZE, PROT_READ|PROT_WRITE) == -1) {
fprintf(stderr, "faield mprotect\n");
fflush(stderr);
exit(1);
}
}
}
}
}
Linux の代替シグナル問題の解決
Fixed stack の LinuxThreads で代替シグナルハンドラがまともに使えない問題は、
これは最初に取られた 2MB のスタック領域の中に
代替シグナルハンドラも入れてしまえば解決する。
つまりスタックの構成を以下のように変更してしまえばよい。
Fixed floating のスタック (2MB) スタックとして使える領域 Glibc が用意するガード領域
Alt. siganl stack を 2MB 内に挿入 スタックとして使える領域 ユーザー定義のガード領域 代替シグナルスタックを設定 Glibc が用意するガード領域
代替シグナルスタックは mmap を使って
実メモリを無理矢理貼り付ける。
この方法では Glibc が用意した通常のガード領域が使えなくなるので、
代替シグナルスタックの上に新しいガード領域を作って自分で管理する必要がある。
以上を実現したサンプルプログラムは以下のようになる。
サンプルプログラム2
StackOverflowHandling_linux.cpp
Part 4 スタックオーバーフローからの強制復帰
Part 3 で述べた (b) 案のように、 スタックオーバーフローが発生した時に 予め登録しておいた場所にジャンプし、 そこから再開させる方法もある。 >
このジャンプは
sigsetjmp/siglongjmp を用いて実現する。
スタックオーバーフローが起こる前の場所で
sigsetjmp を用いて場所を戻り場所を登録しておき、
スタックオーバーフローが起きた場合
シグナルハンドラ内で siglongjmp を
呼び出して元に戻る。
このプログラムの概略は次のコードのようになる。
#include <setjmp.h>
#include <unistd.h>
static sigjmp_buf return_point;
int main(int argc, char** argv) {
/* (ここにシグナルハンドラを登録するコードを) */
if (sigsetjmp(return_point, 1) == 0) {
/* メインの処理 (この中でスタックオーバーフローする) */
} else {
/* スタックオーバーフローからの戻ってきた場合の処理をここに書く */
}
return 0;
}
/* Signal Handler */
static void signal_handler(int sig, siginfo_t* sig_info, void* sig_data) {
if (sig == SIGSEGV) {
if ( /*スタックオーバーフローと判定されたら */ ){
siglongjmp(return_point, 1 /* ここは 0 でない任意の整数 */ );
}
}
}
いくつか注意点がある。
- sigsetjmp/siglongjmp の代わりに
setjmp/longjmpを使わないこと。
POSIX の規約では setjmp/longjmp は シグナルマスクなどのシグナルコンテキストを 保存・復帰することを保証されない。 Linux では setjmp/longjmp ではシグナルコンテキストが保存・復帰されるが、 Solaris などの SYSV 系では シグナルマスクの処理が放置される。
その結果、以下のようなことが起こる。- SEGV シグナルを補足したシグナルハンドラでは、 SEGV シグナルがブロック (マスク) される。
- シグナルハンドラ内から longjmp で戻ると シグナルマスクがそのままになる。
- 再度、スタックオーバーフローが起きると シグナルハンドラに飛べず Segmentation Fault で落ちる。
- siglongjmp 呼び出し時には代替シグナルスタックの 切り替えは自動的に行われる (放置してよい)。
- アクセスバイオレーションの発生したアドレス (仮想記憶ページ) も放置してよい。
Windows NT 系の場合
Windows NT 系では構造化例外(SEH) を用いることで、
スタックオーバーフローのハンドリングは簡単に処理することができる。
例をあげる。
/* 本文 */
__try {
/* メインの処理 (この中でスタックオーバーフローする) */
} __except(ExceptionHandler((_EXCEPTION_POINTERS*)_exception_info())) {
// 例外発生時に行う特別な処理
}
/* 構造化例外ハンドラ */
LONG WINAPI ExceptionHandler(struct _EXCEPTION_POINTERS* pExceptionInfo) {
DWORD dwExceptionCode = pExceptionInfo->ExceptionRecord->ExceptionCode;
switch( dwExceptionCode ){
// スタックオーバーフローと(考えられる)アクセス
case EXCEPTION_STACK_OVERFLOW:
{
PEXCEPTION_RECORD pExceptionRecord = pExceptionInfo->ExceptionRecord;
void* pAddr = (void*) pExceptionRecord->ExceptionInformation[1];
/*
pAddr へアクセスしたときに例外が発生している。
これに対処した後、戻り値として下の 3 つの値のいずれかを返す。
戻り値によって復帰動作が変わる。
EXCEPTION_CONTINUE_EXECUTION 例外をキャンセルし、例外を発生させた命令からやり直す。
EXCEPTION_CONTINUE_SEARCH この例外ハンドラでは対処せず、親の例外ハンドラに処理を移す。
EXCEPTION_EXECUTE_HANDLER この例外ハンドラで例外に対処する。__except 節の処理を実行した後、
*/
}
break;
// スタックオーバーフロー以外の不正アクセス
case EXCEPTION_ACCESS_VIOLATION:
{
// 不正アクセスがあったメモリアドレスの位置は EXCEPTION_STACK_OVERFLOW と
// 同様に補足できる。
}
break;
default:
break;
}
// 外側の _try のハンドラを探す。
return EXCEPTION_CONTINUE_SEARCH;
}
__try 〜 __except は入れ子に構造にしても構わない。
入れ子にした場合には最初に一番内側の __except 節から評価される。
そこで EXCEPTION_CONTINUE_SEARCH を返せば、
より外側の __try〜__except を探しに行く。
構造化例外をハンドリングするコンテキストは、 原則として例外を受けたスレッドのスタックを使用する。 ただし EXCEPTION_STACK_OVERFLOW の場合だけは 別スタックを用いるように 切り替えられるようである (つまり Alternative Signal Stack が不要)。
追記
- (2004/12/5) Windows NT 系の
PAGE_GUARD属性の話 -
Windows の VirtualProtect API は、
PAGE_READONLY、PAGE_READWRITE、PAGE_EXECUTE_READWRITEなど他にPAGE_GUARD属性が存在する。 この属性を付けたページに初めてメモリアクセスするとSTATUS_GUARD_PAGE構造化例外が発生する。 この例外はトラップしてもトラップしなくてもプログラムが続行する無害な例外で、 いったん例外を発効したページはPAGE_GUARD属性を落したノーマルな属性へと戻る。Windows NT 系はスレッドスタックの実装のために この
PAGE_GUARD属性を使用しており、 スタックトップにPAGE_GUARD属性ページをおいてSTATUS_GUARD_PAGE例外をトラップするごとに スタックを伸張するということを行っている。 - (2004/12/22、2005/1/17) Linux で sigaltstack と posix スレッドを併用したときの問題点
-
Linux で
sigaltstack()をセットしたスレッドが、pthread_create()を使ってpthread_attr指定なしで子スレッドを生成すると、 親スレッドの代替シグナルスタック設定がそのままコピーされる。 このため二つのスレッドが同一の代替シグナルスタックを共有してしまい、 二つのスレッドが同時に例外を補足するとスタックを上書きしあう。 こうなると動作はまったく不定となり非常に高い確率で abort することになる。 この問題は、バージョン・ディストリビューションを問わず広く Linux 全般に存在するようだ。これは Linux のスレッド実装の根底にある
cloneシステムコールが、 スレッド構造体中の代替シグナルスタックに関するメンバ変数を単純コピーしてしまうことに起因している。 POSIX 仕様としては新しいスレッドを生成した場合、 代替シグナルスタックの属性は継承してはいけない(shall not be inherited)とあるので、 この動作は POSIX 仕様に準拠していない。