C/C++ 技術

setjmp/longjmpによるメモリリークパターン

setjmp/longjmpによるメモリリークパターンを紹介します。
次のコードを見てください。

main.c

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf env;

void sub(void)
{
  longjmp(env, 1);
}

int main(int argc, char** argv)
{

  char* buf1 = NULL;
  char* buf2 = NULL;

  buf1 = (char*)malloc(100);
  printf("1:buf1 = %p\n", buf1);

  if(setjmp(env)){
    printf("3:buf1 = %p\n", buf1);
    printf("4:buf2 = %p\n", buf2);
    free(buf1);
    free(buf2);
    return 0;
  }

  buf2 = (char*)malloc(200);
  printf("2:buf2 = %p\n", buf2);

  sub();

  return 0;
}

開始直後に確保したメモリをbuf1に、setjmp後に確保したメモリをbuf2に結び付けています。
その後sub()呼び出しによりlongjmpが行われ、遷移先(setjmp戻り真の場合の処理)で各メモリを解放する、というものです。
このコードをUbuntu Linux (x86)上でコンパイルし、実行すると

$ gcc main.c -O0
$ ./a.out 
1:buf1 = 0x83a9008
2:buf2 = 0x83a9070
3:buf1 = 0x83a9008
4:buf2 = 0x83a9070
$ 

となります。特に問題なく、メモリ解放できていそうです。
しかし、これを最適化ありでコンパイルすると、

$ gcc main.c -O2
$ ./a.out 
1:buf1 = 0x8170008
2:buf2 = 0x8170070
3:buf1 = 0x8170008
4:buf2 = (nil)
$

となります。
buf2がNULLになっているため、buf2の解放ができていません。
理由は、最適化されていない場合、buf1、buf2はスタックポインタ(レジスタ)が"指し示す"スタック上の1データにすぎないため、setjmpコール時点もlongjm後も変化しない。しかし、最適化が行われると、このコードの場合、buf1やbuf2そのものがレジスタに割り当てられようです。setjmpでコール時点でのそのレジスタ状態(buf2はNULLだという状態)が保持され、longjmpで遷移した時にsetjmpで保持されたもの(buf2はNULLだという状態)がレジスタとして復元されるためです。

http://linuxjm.sourceforge.jp/html/LDP_man-pages/man3/longjmp.3.html
に、

以下の条件が全て成立する場合、 longjmp() の呼び出しが行われた後の自動変数の値は未定義 (unspecified) となる。

  • その自動変数が、対応する setjmp(3) 呼び出しを行った関数のローカル変数である。
  • 自動変数の値が setjmp(3) と longjmp() の間で変更されている。
  • volatile として宣言されていない。

とあります。
また、

    longjmp() や siglongjmp() を使うと、プログラムは理解しづらく、保守しにくいものになる。別の方法が可能なら、それを使うべきである。

また、setjmpのほうにも

    setjmp() や sigsetjmp() を使うと、プログラムは理解しづらく、保守しにくいものになる。別の方法が可能なら、それを使うべきである。

と記載があります。

setjmp/longjmpの利用には細心の注意が必要です。

-C/C++, 技術
-

© 2024 BLuE AND PuRE