スタックオーバーフローのハンドリング (Stack Overflow Handling)

作成日:2004.04.12
更新日:2006.02.19

更新記録
(2004.04.12) 3/63/113/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 系についても少しだけ書く。

  1. 代替シグナルスタック (Alternate Signal Stack)
    スタックオーバーのハンドリングのためには代替シグナルスタックを設定する必要がある。

  2. スタック領域情報の取得
    SEGV シグナルを補足した場合、シグナルハンドラにバイオレーションが発生したアドレスが渡される。
    このアドレスがスタック内なのか外なのかを知るために スタックの存在するアドレス範囲を把握する必要がある。

  3. スタックのユーザー制御
    スタックオーバーフロー後に追加の処理を行うためには スタックを本当に使い切ってしまうとまずい。 そのためスタックに「余白」を作る必要がある。

  4. スタックオーバーフローからの強制復帰
    スタックオーバーフローが発生した時に、 あらかじめ登録された復帰ポイントに強制的に戻る方法について述べる。

  5. 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;
}

この時点ではまだシグナルをハンドラしただけで、スタックオーバーフローに対処したとはいえない。 以下のような問題がある。

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 stackfloating 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 で回避することが可能。

floating stack の LinuxThreads

floating stack の LinuxThreads も libpthread.so の形で提供されている。 プロセスが libpthread.so をアタッチすることでスレッドの使用が始まる。

  • floating stack の場合には、 リソースリミットで制限したプロセスのスタックサイズが デフォルトのスタックサイズとなる (デフォルトは 8192K のことが多い)。
    そのため libpthread.so 前のプロセススタックが、 メインスレッドのスタックになると考えてよい。
  • pthread_create を使って新しいスレッドを作成すると、 0xC0000000 から下のアドレス空間の中に指定サイズのメモリを取って 新しいスレッドのスタックとする。
    指定がない場合にはデフォルト値が採用される。

floating stack ではスタックを 2MB 境界に置かねばならないという制限はない。

NPTL

NPTL はスタック周辺の動作は floating stack と同様。



このような複数のスタックシステムを区別しながら、動的にスタックのサイズを取得するためには以下のような方法を用いればよい。

pthread_getattr_np を使用したスタック領域情報の取得

POSIX にはスレッドのアトリビューションから スタックの位置とサイズを読み取る API pthread_attr_getstackaddrpthread_attr_getstacksize が用意されている。 ただスレッドアトリビューションは スレッドを新規に作成するときに与えるものであって、 実行中に動的に取得することは (POSIX の仕様上は) できない。

ただし glibc の特定バージョンからは、 POSIX の仕様外の API として 動作中のスレッドからスレッドアトリビューションを取得する pthread_getattr_np が用意された。 これを用いてスタック領域の取得にせまる。

fixed stack と floating stack の判別方法

まず floating stack が実装されたのは LinuxThreads v0.9 以降である。
pthread_getattr_np API は、 このバージョンから実装されている。
そこで共有ライブラリ中から dlsym API を用いてシンボルを探すことで、 現在稼動しているシステムに 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_np API が存在する場合、 以下のようなプログラムを使用すると スタックの開始ポイント(最下位アドレス)とサイズを取得できる。

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 を使用した場合、 代替シグナルスタックを使っている場合でも (代替でない) ノーマルなスタック情報が手に入る。

メインスレッドのスタックサイズ

メインスレッドのスタックサイズを取得するには、 プロセススタックと同様に getrlimit API によって リソースリミットを取得する。

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 が用意されている。

OSAPIインクルードファイル
FreeBSDpthread_attr_get_nppthread_np.h
OpenBSDpthread_stackseg_np(3)pthread_np.h & sys/signal.h
MacOS Xpthread_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) の実現方法について説明する。 だが、その前にスタックオーバーフローが発生する機構を再考してみる。

スタックオーバーフローは「スタック領域が溢れて(越えて)しまった」というニュアンスがあるが、大概の OS ではスタックオーバーフローの不正アクセス例外はスタック領域の中で発生するように実装されている。 OS またはスレッドライブラリはスタック領域の最後 (普通はスタック領域の最下位アドレス) に仮想記憶 1ページ分の ガード領域 を設けていて、このページの属性を読み込み・書き込み・実行に設定する。 スタックが伸びようとしてガード領域と接触した場合SEGV シグナルが発生し、「もうこれ以上はスタックは伸ばせません」ということが通知される。

独自のガード領域の設定

(a) の実現方法のために、ユーザーがガード領域を自由に設定する方法を考える。

(1) システム定義ガード領域のサイズの確認

まず POSIX に準拠するスレッドライブラリの場合、 スタックの最終ページをガード領域として設定しているはずである。 このような システム定義ガード領域 と呼ぶことにする。 このシステム定義ガード領域の大きさを何らかの方法によって予め調べておく。
# Pthread ライブラリのバージョンによっては、 pthread_attr_getguardsize API を用いて動的に取得できる。

(2) ユーザー定義ガード領域の設定

ユーザー独自のガード領域は、 システム定義ガード領域の直上の領域に 仮想記憶ページの整数倍となるように設定する。 i486/Linux の場合、 スタック領域は上位アドレスから下位アドレスに向けて 実メモリを貼り付けていくので、 この時点ではユーザーガード領域と定めた領域には 実メモリが貼り付けられていない。
ここで mmap API を用いて強制的に実メモリをり付けてしまう。 この時、 ページの属性を PROT_NONE (読み・書き・実行禁止) に設定してしまう。

スタックとして使える領域
ユーザー定義ガード領域
システム定義ガード領域
(3) ユーザーガード領域内でのアクセスバイオレーション

実行がはじまりスタックが激しく消費されると、 ユーザー定義ガード領域までスタックが伸びる。
ユーザー定義ガード領域は すでに実メモリが貼り付けてあるが、 属性が読み・書き・実行禁止のため SEGV シグナルが発生する。

(4) シグナルハンドラで補足

シグナルハンドラ内で SEGV シグナルを確認し、 アクセスエラーの発生アドレスから ユーザー定義ガード領域内でアクセスバイオレーションが発生したことを確認する。
確認ができれば mprotect API で ユーザー定義領域のアクセス権を 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 < 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 でない任意の整数 */ ); 
    }
  }
}

いくつか注意点がある。

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_READONLYPAGE_READWRITEPAGE_EXECUTE_READWRITE など他に PAGE_GUARD 属性が存在する。 この属性を付けたページに初めてメモリアクセスすると STATUS_GUARD_PAGE 構造化例外が発生する。 この例外はトラップしてもトラップしなくてもプログラムが続行する無害な例外で、 いったん例外を発効したページは PAGE_GUARD 属性を落したノーマルな属性へと戻る。

Windows NT 系はスレッドスタックの実装のために この PAGE_GUARD 属性を使用しており、 スタックトップに PAGE_GUARD 属性ページをおいて STATUS_GUARD_PAGE 例外をトラップするごとに スタックを伸張するということを行っている。

参考
Ask Dr. GUI #49

(2004/12/222005/1/17) Linux で sigaltstack と posix スレッドを併用したときの問題点

Linux で sigaltstack() をセットしたスレッドが、 pthread_create() を使って pthread_attr 指定なしで子スレッドを生成すると、 親スレッドの代替シグナルスタック設定がそのままコピーされる。 このため二つのスレッドが同一の代替シグナルスタックを共有してしまい、 二つのスレッドが同時に例外を補足するとスタックを上書きしあう。 こうなると動作はまったく不定となり非常に高い確率で abort することになる。 この問題は、バージョン・ディストリビューションを問わず広く Linux 全般に存在するようだ。

これは Linux のスレッド実装の根底にある clone システムコールが、 スレッド構造体中の代替シグナルスタックに関するメンバ変数を単純コピーしてしまうことに起因している。 POSIX 仕様としては新しいスレッドを生成した場合、 代替シグナルスタックの属性は継承してはいけない(shall not be inherited)とあるので、 この動作は POSIX 仕様に準拠していない。

コメント

コメントを書き込む
[mune]
fixed stackの場合、メインスレッドのみulimitの設定で4--16MBのサイズのスタックを利用できるようです。

TOP    掲示板    戻る
Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp