g++でのスタック使用量の動的解析

Linux Memory Overcommitment の話とも関係するが、組み込み機器向けのプログラムを設計・実装する際は、たとえターゲットがMMU/仮想記憶を利用できるモノであるとしても、使用するスタック量の見積もりくらいはしておきたいと思っている。


UNIXではsetrlimit(2)で、スタック使用量をunlimitedにしてしまえば、スタック使用量の自主制限にひっかかってsegvすることはなくなる。ちなみに ulimit -s での制限は、Linuxであってもstrictに効く。しかし、物理的な限界は確実に存在するわけで、スタックのある領域にアクセスした時に物理ページに空きがなければ、プロセスはSIGSEGVで死ぬかSIGKILLで殺されてしまう。


C言語で書かれたプログラムであれば、再帰や関数ポインタさえ上手に処理(コーディング標準で制限など)できれば、コードを静的に解析して最大スタック使用量を推測することはさほど難しいわけではない。私も以前、そういうツールをいくつか書いたことがある。


しかしC++となると、静的な解析は不可能に思える。少なくとも私の想像力では無理だ。で、動的に使用量を測るにはどうしたらいいか考えました。


長い上にツマラナイので、先に結論を書いておきます。組み込み機器を想定してます。

可能であるなら設計フェーズでスタック最大使用量を特定すべし、それができないのなら、案3の方法で、おおまかなスタック使用量くらいは把握しておくべし。
把握したなら、同じく案3で示した方法でプロセス開始時にすぐスタックをmlock(2)すべし。さもないとメモリ不足時にプロセスが突然死しますよ。

測定方法: 案1

小規模の組み込みソフトウェアで、固定長のスタック領域を静的に確保する場合には、その固定領域を特定のビットパターンで塗りつぶしておき、実行後にそのパターンがどこまで破壊されたかでスタックの使用量を調べたりするが、普通のUNIXを組み込み用途に使用する場合はそれはちょっと難しい(特権が必要)。案1は特権が不要な方法。


スタック使用量をモニタするようなValgrindskinを作成する。


書くのが大変なのが問題。そもそもskinで本当に実現可能かどうか調べるのだけでも結構な手間。書いても現状、valgrind自身が Linux/x86 でしか動かないから、non-x86な組み込み機器のスタック使用量は測定できず(私はMIPSやARMを相手にすることのほうが多い)。以下アルナシ表。

スレッドのサポート
正確さ(ライブラリ内関数のスタック使用量測定) 正確、可能
最大スタック消費位置の特定 可能(backtraceも可能)
測定対象コードの書き換え 不要
測定対象コードの再コンパイル 不要
対応プラットフォーム Linux/x86のみ
計測時の実効速度低下 非常に大きい

測定方法: 案2

g++のプロファイラサポート機能(-finstrument-functions)を利用する。

extern "C" {
 void __attribute__((__no_instrument_function__))
  __cyg_profile_func_enter (void *this_fn,  void *call_site);
 void __attribute__((__no_instrument_function__))
  __cyg_profile_func_exit (void *this_fn,  void *call_site);
}

関数の入退場時に決められた名前/署名の関数(上記のもの)が呼ばれるようになるので、その関数内で現在のスタックフレームを alloca(1); あるいは __builtin_frame_address(0); で得れば良い。main関数の先頭やスレッドの実行開始位置でも同様にフレームアドレスを得ておき、毎回それとの差を取る。プログラムの実行終了時点でのその差の最大値がおよそのスタック使用量である。

  • マルチスレッド対応が面倒。各スレッドは固有のスタックを持つが、そのあたりのサポートは?
  • 実行速度はそれなりに低下するでしょう。
  • 最大使用位置(関数名)くらいはわかるが、backtraceまでは取れないね。

あたりが難儀な点か。__builtin_frame_address() については、Ruby Hacking GuideThreadを参照。exploitで良く使われる

unsigned long get_sp(void) { 
  __asm__ __volatile__ ("mov %esp %eax"); 
}

でも良いけど、CPU依存のコードはイマイチ。スタック使用量の概算が目的であるから、厳密なアドレスを得る必要はなく、正直なんでもいい。allocaでいい。おなじく以下アルナシ表。

スレッドのサポート 一応可能
正確さ(ライブラリ内関数のスタック使用量測定) ライブラリ内部のみ計測不可
最大スタック消費位置の特定 不可能、最大消費位置の関数名のみ特定可能
測定対象コードの書き換え __cyg_XXX関数の追記
測定対象コードの再コンパイル
対応プラットフォーム GCCが動作する任意のプラットフォーム
計測時の実効速度低下 大きい

用意する冒頭の2関数についての。詳細はGCCのinfoか、この投稿が詳しいが、簡単に説明すると this_fnは現在の関数のアドレス、all_siteはthis_fnを呼び出した関数のアドレスとなる(ようだ)。


この他の -finstrument-functions についての日本語の資料は、Web上にはフリーソフトウェア徹底活用講座(14)くらいしかなさそうだ。Interface誌に連載された記事ですね。

測定方法: 案3

動的解析方法の提案その3です。一部特権が必要。以下、swapは存在しないことを仮定します。


まず、メインスレッドを含む各スレッドの使用するスタック量をざっくりと見積もっておきます。これだけあればまず溢れないだろうという粒度でOKです。で、pthread_create関数で生成するスレッドについては、それ用のスタック領域をフリーストアから確保(new[])し、マジックナンバーで塗りつぶし、pthread_attr_setstack関数を用いてそのアドレスを教えておきます。スレッドをjoinした後に、マジックナンバーがどこまで壊れたか調べればスタック使用量がわかるでしょう。


pthread_attr_setstack()を用いたのは、これを使わない場合のスタックの位置とサイズはpthreadの実装依存であるからです(よね?)。LinuxではLinuxThreadsでもNPTLでも、マシンスタック(OSの提供するスタック領域)は使用されませんね。多分。

スレッドのサポート
正確さ(ライブラリ内関数のスタック使用量測定) 正確、可能
最大スタック消費位置の特定 不可能
測定対象コードの書き換え
測定対象コードの再コンパイル
対応プラットフォーム 任意のプラットフォーム
計測時の実効速度低下 小さい

OSに暗に生成される、main関数を実行するスレッド(上ではメインスレッドと呼びました)については、pthread_setstack()ができませんから、main関数の直後で別スレッドを明示的にpthread_create()してそちらだけに実質的な処理を行なわせるか、さもなければ次の方法でスタックをマジックナンバーで充たせば良いと思います。

#define STK_SIZE (1024*1024*8)  // 8MB
static char* stk_start = 0; 
void stack_init(void)
{
  char x[STK_SIZE]; 
  mlock(x, sizeof(x));
  for (int i = 0; i < sizeof(x); ++i) {
    x[i] = MAGIC_NUMBER;
  }
  stk_start = x;
}
void stack_check(void)
{
  char x[STK_SIZE]; 
  // xをどこまで潰したかの検査
}
int main(void)
{
  stack_init();
  // 処理
  stack_check();
}

stack_init()のmlock(2)を実行するのにroot権限が必要です。正確には、CAP_IPC_LOCKケーパビリティ(だけ)が必要です。蛇足までに書いておくと、mlockのあと配列にかきこまなくても、プロセスのRSSは8MBになります。


上記コードでmlockしない場合にRSSが8MBよりちいさくなり得るかについては微妙で、私にはよくわからないのですが、RH7.3では実際に小さくなるような気が...。mlockしたほうが安全に思います。


なお、特に組み込みではmlockには別の効果を期待できたりもします。それについては別に。


pthread_setstack()しない場合のデフォルトのスタックの確保方法についても今度。

OOMとの関係

おなじみの linux/Documentations/vm/overcommit-accounting を再度開いてみると、次のような記述を見つけることが出来ます。

The C language stack growth does an implicit mremap. If you want absolute guarantees and run close to the edge you MUST mmap your stack for the largest size you think you will need. For typical stack usage is does not matter much but its a corner case if you really really care

上記は逆に言うと、特に何も考えない場合にはスタック領域を(setrlimitの範囲内で!)拡張しようとした場合に物理的なページフレーム不足でプロセスが即死する可能性があるということです。これはOSに与えられたスタックを使用する場合の話ですが、newしたメモリをpthread_attr_setstackaddr()する場合も当然同様です。


この現象は、一部の組み込み機器では困るでしょうから、もし困るのなら案3に書いたような方法で、スタックをmlockしてしまえば良いのでしょう。newしたスレッド用スタックについてもmlockしてしまってOKです。mlockしておけばswap領域にすら移動しなくなります。


ただ、確実かつ固定的にページフレームを消費してしまいますから、各スレッドのスタック使用量がそこそこの精度で見積もられている場合にしか使えません :-)