This Project Has Not Released Any Files
典型的なC言語プログラムでは、バグの約半分は メモリ管理の問題に起因されるでしょう。 メモリ管理のバグは従来の技術を使用して検出することは、悪名が知れわたるほどに、 難しいです。 多くの場合、バグの症状は実際のソースから遠く離れています。 コンパイラの最適化が行われているか、コードが異なるプラットフォームでコンパイルされるとき、 メモリ管理のバグのみがしばしば、ちらほらと現れたり、いくつかのバグのみが明らかになる場合があります。 実行時のツールはいくつかのヘルプを提供しますが、しかし、 使用するのが面倒であり、テストケース実行時に起きるエラーを検出することに制限されています。 これらのエラーを静的に検出することによって、我々は、特定のエラーのタイプが絶対に発生しないことを確信でき、 プログラムのメモリ管理動作上で検証済みのドキュメントを提供することが可能となります。
Splintは解放済みの記憶領域の使用(Section 5.2)、 メモリリーク(Section 5.2)、又は、スタックメモリに割り当てられた記憶領域へのポインタの戻し (Section 5.2.6)を含む、コンパイル時に多くのメモリ管理エラーを検知することが出来ます。
よし、おれの記憶のノート板からささいなたわごとの一切を消してしまおう、
本からとった名句もスケッチも過去の印象も、それから若い観察の記録もすべて消してしまおう
ハムレットはガベージコレクションを好みます (シェイクスピア, ハムレット. 第一幕, 第五場)
(訳注:ここの日本語訳はhttp://james.3zoku.com/shakespeare/hamlet/hamlet1.5.htmlを参考にしました。)
これらのチェックのほとんどは、 メモリ管理とポインタ値に関係したドキュメントの前提にプログラムに追加した アノテーションに依存しています。 関数インタフェース、変数、型定義、構造体のフィールド、 に対するこれらの想定をドキュメント化することにより、 メモリ管理のバグはそのソース(想定が違反しているソース)から検知することができます。 加えて、メモリ管理上の判断についての正確なドキュメントはコードを変更することを容易にします。
このsectionでは標準のC言語の用語を使用して可能なことよりも、 より正確に記憶領域の状態を記述するために、実行時の概念について説明します。 記憶領域のいくらかの用途は、プログラムのバグの兆候である様であり、 例外として報告されます。*3
SplintはCLUのようなオブジェクトメモリモデルを前提としています。*4 オブジェクトは記憶領域の型定義された部分です。 いくつかのオブジェクトは、コンパイラによって自動的に確保と解放がされる記憶領域の固定量を 使用します(訳注:スタックメモリのこと)。その他のオブジェクトはプログラムによって管理される 必要のある動的な記憶領域を使用しています(訳注:ヒープメモリのこと)。
記憶領域に値が割り当てられていない場合、それは未定義です。 値が割り当てられた後は、定義されています。 記憶領域から到達可能な全てのそれが定義されている場合、オブジェクトは完全に定義されています。 どのような記憶領域がオブジェクトから到達可能なのかは、オブジェクトの型と値に依存します。 例えば、pが構造体へのポインタの場合、pの値がNULLであるか、pが指す構造体の全てのフィールドが 完全に定義されている場合にpは完全に定義されています。
式が代入式の左側として使用されている場合、 我々は、左辺値として使用されていると言います。 メモリ内のその場所が使用されていますが、その値ではありません。 記憶領域の場所が必要となったとき以降のみ、未定義のそれは左辺値として使用されるかもしれません。 記憶領域は他の方法で使用される場合(間接演算子*を含む、基本演算のオペランド*5 や関数の引数として、代入式の右側のような) 、我々は、右辺値として使用されていると言います。 未定義の記憶領域を右辺値として使用することは、異常です。
ポインタは型指定されたメモリのアドレスです。 ポインタは生きている(live)か、死んでいる(dead)のどちらかです。 生きているポインタはNULLか、 割り当てられている記憶領域内のアドレスです。 オブジェクトを指しているポインタはオブジェクトポインタです。 オブジェクト(例えば、割り当てられたブロックの第三要素へ)の中を指すポインタは、 オフセットポインタです。 定義されていないが割り当てられている記憶領域を指すポインタは、割り当てられたポインタです。 割り当てられたポインタの先の参照の結果は未定義の記憶領域です。 従って、それを右辺値として使用することは異常です。 死んだ(あるいは「宙ぶらりんの(参照先が存在しない)」)ポインタは 割り当てられた記憶領域を指し示していません。 それが指す記憶領域が解放されている(例えば、freeライブラリ関数に渡されたポインタ)場合、 ポインタは死にます。 死んだポインタを右辺値として使用することは異常です。
特別なオブジェクトnullは、C言語プログラムではNULLポインタに対応します。 値NULLを持つ可能性のあるポインタは、 nullの可能性のある(possibly-null)ポインタです。 nullではないポインタを期待している(例えば、いくらかの関数の引数、もしくは、間接演算子)場所で nullの可能性のあるポインタを使用することは異常です。
我々が心配する解放エラーは2種類あります。 1つは、他に同じ記憶領域への生きている参照があるときの記憶領域の解放。 もう1つは、記憶領域の最後の参照が失われる前にそれを解放することによる失敗です。 解放エラーを扱うために、我々は記憶領域の返却義務の構想を導入します。 ストレージが割り当てられるたびに、ストレージを解放する義務を作成します。 この義務は割り当てられている記憶領域への参照に加えられます。*6 参照のスコープが終了するか、新しい値へ割り当てられる前に、 指している記憶領域は返却される必要があります、 アノテーションはこの義務が戻り値、関数の引数、あるいは外部参照への割り当てを通して 移されることを示すために使用されます。
お言葉は、わたくしの記憶のなかに鍵をかけて仕舞いました、
その鍵はお兄さんが持っていてください。
オフィーリアは明示的な解放を好みます (ハムレット. 第一幕, 第三場)
(訳注:ここの翻訳はhttp://james.3zoku.com/shakespeare/hamlet/hamlet1.3.htmlを参考にしました。)
onlyアノテーションは、 参照が、そのオブジェクトへの唯一のポインタであることを示すために使用されます。 我々は、この記憶領域を返却する責務を持っているものとして参照を見ることが出来ます。 この責務は以下の3つのうちの1つの方法により、いくつかの他の参照へ移すことにより果たされます。
責務の返却が移った後、元の参照は死んでいるポインタとなり、 それが指す記憶領域は使用することはできません。
記憶領域を返却する全ての責務は、 基本割り当てルーチン(例えば、malloc)に由来し、 最終的にはfreeの呼び出しによって果たされます。 標準ライブラリは基本的な割り当てと解放のルーチンを宣言しました。
基本的なメモリ割り当て関数、mallocは以下のように宣言されます。
関数の戻り値によってのみ参照されるオブジェクトを返します。
解放関数、freeは以下のように宣言されます。*7
Running Splint | |
1 extern /*@only@*/ int *glob;
/*@only@*/ int * f (/*@only@*/ int *x, int *y, int *z) /*@globals glob;@*/ { 8 int *m = (int *) 9 malloc (sizeof (int));
11 glob = y; Memory leak 12 free (x); 13 *m = *x; Use after free 14 return z; Memory leak detected } |
> splint only.c only.c:11: Only storage glob (type int *) not released before assignment: glob = y only.c:1: Storage glob becomes only only.c:11: Implicitly temp storage y assigned to only: glob = y only.c:13: Dereference of possibly null pointer m: *m only.c:8: Storage m may become null only.c:13: Variable x used after being released only.c:12: Storage x released only.c:9: Fresh storage m allocated |
図 6. メモリ管理 |
freeへの引数は 共有されていないオブジェクトへ参照している必要があります。引数がonlyアノテーションを使用して宣言されているため、 呼び出し側は読んだ後に、参照されていたオブジェクトを使用してはならず、 また、共有されているオブジェクトへの参照に渡してもいけません。 mallocとfreeについて 特別なものはありません。- それらの動作は提供されたアノテーションの条件により 完全に記述されます。
tempアノテーションは 関数によって一時的に使用される関数の引数を宣言するために使用されます。 関数がtemp仮引数に関連付けられた記憶領域を返却した場合や、 関数が戻った後でそれを表示する新しい別名を作成する場合、エラーが報告されます。 どんな記憶領域もtempとして渡される可能性があり、 関数が戻った後のその元のメモリ制約を満足します。
実際のプログラムでは、時々、いくつかの参照の可能性のあるもの同士で 共有される記憶領域を必要とする場合があります。ownedとdependentアノテーションは、少ないチェックコストで 記憶領域の管理のより柔軟な方法を提供します。 ownedは記憶領域を返却する責務を持つ参照を表します。onlyとは異なり、 しかし、dependentによりマークされた他の外部参照が このオブジェクトを共有してもかまいません。 dependent参照の生存期間が対応するowned参照の生存期間内になっていることを確実にするのはプログラマの責任です。
keepアノテーションは、 呼び出し側が、呼び出した後で参照を使用できる点を除き、onlyと同じです。 呼び出された関数はonly参照へkeep引数を割り当てるか、 別の関数のkeep引数としてそれを渡す必要があります。 呼び出し元の関数がそれが返却された後でこの参照を使用しないことを確実にするのはプログラマの責任です。 keepアノテーションは コレクション(例えばシンボルテーブル)が存在する間、開放されないことが知られている コレクションへオブジェクトを追加するときに有用です。
Splintがガーベッジコレクションの環境下で使用されるために設計されたプログラムをチェックする ときに使用される場合、1つ以上の参照によって共有されている記憶領域が存在する可能性があり、そして、 それは、決して明確には返却されません。 sharedアノテーションは勝手に共有されても良いが、しかし、 決して返却されない記憶領域を宣言します。
ローカル変数(動的に割り当てられていない)はコールスタックに保存されています。 関数から戻る際に、スタックフレームは割り当てを解除され、関数内のローカル変数に関連付けられた記憶領域は破棄されます。 この記憶領域内へのポインタが関数が戻った後も生きている場合、メモリエラーが発生します。 Splintは戻り値あるいは、グローバル変数か実引数から到達可能な参照への割り当てを介して関数の外に 出されたスタックメモリへの参照に伴うエラーを検出します。 記憶領域が関数スタック上に割り当てられている場合、それは宣言から明らかなため、 スタックメモリへの参照エラーを検知するためにアノテーションは必要ではありません。 図7はスタックに割り当てられた記憶領域に伴うエラーの例です。
stack.c |
Running Splint |
int *glob;
/*@dependent@*/ int * f (int **x) { int sa[2] = { 0, 1 }; int loc = 3;
9 glob = &loc; 10 *x = &sa[0];
12 return &loc; } |
> splint stack.c stack.c:12: Stack-allocated storage &loc reachable from return value: &loc stack.c:12: Stack-allocated storage *x reachable from parameter x stack.c:10: Storage *x becomes stack stack.c:12: Stack-allocated storage glob reachable from global glob stack.c:9: Storage glob becomes stack
dependent アノテーションが 戻り値で使用されています。戻り値は暗黙の only アノテーションを持っているため、これが無い場合、他の警告が報告されるでしょう。 |
図 7. スタックに割り当てられた記憶領域 |
アノテーションは常に記憶領域の最も外側のレベルに適用されます。例えば、
は、共有されていないintへのポインタのポインタとしてxを宣言します。onlyアノテーションはxに適用されます。*xには適用されません。型の定義が使用できる内側の記憶領域へアノテーションを適用するためには:
今、xはoip(intへのonlyポインタである)へのonlyポインタです。
アノテーションが型の定義にて使用される場合、それらはインスタンスの宣言内にて上書きされる場合があります。例えば、
は、xをintへのdependentポインタにします。 内側の記憶領域へアノテーションを適用するもうひとつの方法は、状態句を使用することです (Section 7.4参照)。
Splintがアノテーションが無いプログラムを効果的にチェックできることは 重要なことであるため、メモリアノテーションの無い宣言の持つ意味は、アノテーションの無いプログラム で便利なチェックを行うために必要とされる最小のアノテーションが選択されます。
暗黙的なメモリ管理アノテーションは 明示的なメモリ管理アノテーションが無い宣言に対して想定されているかもしれません。 エラーメッセージがそれら自身が暗黙的なアノテーションから生じることを示すことを除き、 暗黙的なアノテーションは対応する明示的なアノテーションと同様にチェックされます。 図8はいくつかの暗黙的なアノテーションを示しています。
アノテーションが無い関数の引数はtempであると仮定されます。 これは、アノテーションの無いプログラムに対してメモリチェックを行った場合、 引数によって参照されている記憶領域を返却する全ての関数、あるいは、 記憶領域に別名をつけるためにグローバル変数に割り当てる全ての関数は エラーメッセージを生成することを意味します。 ( paramimptempフラグによって制御されます。)
implicit.c | |
typedef struct { only char *name; int val; } *rec;
extern only rec rec_last ;
extern only rec rec_create (temp char *name, int val) ; 斜体のアノテーションはコードには現れませんが、フラグの設定によっては暗黙的に現れる場合があります。 |
structimponly フラグがオンになっている場合の、 可変(mutable、ミュータブル)構造体フィールド上の 暗黙の only アノテーション
globimponly フラグがオンになっている場合の、 可変グローバル変数上の暗黙の only アノテーション
retimponly フラグが設定されている場合の、 可変関数の戻り値上の暗黙の only アノテーション。 paramimptemp フラグが設定されている場合の、 可変引数上の暗黙の temp アノテーション。 |
図 8. 暗黙のアノテーション |
アノテーションの無い戻り値、構造体フィールド、そしてグローバル変数は onlyであると仮定されます。 暗黙のアノテーション(デフォルトではある)を使用して、 アノテーションの無いプログラムに対してメモリチェックを行うと、 共有されていない記憶領域や グローバル変数や構造体フィールドへの共有された記憶領域の割り当てを 返さないどのような関数に対してもエラーを発生させます。 暴露修飾子(Section 6.2参照)が使用されている場合、 暗黙のdependentアノテーションが、 より一般な暗黙のonly アノテーションの代わりに使用されます。 (retimponlyフラグ、 structimponlyフラグと globimponlyフラグによって制御されます。 allimponlyフラグは全ての暗黙のみのフラグを設定します。)
メモリ管理のもう1つのアプローチはその記憶領域への参照の数を 明示的に追跡し続けるために型にフィールドを追加することです。 参照が追加、あるいは喪失するたびに参照カウントがそれに応じて調整され、 0になったときに、記憶領域が返されます。 これは参照カウントの増加、削減を忘れやすく、これらのエラーを追跡することが 非常に難しいため、自動化無しで行うことは難しいです。
Splintは他のメモリ管理アノテーションと同様に 参照カウントの使用を宣言するためのアノテーションを使用することにより、 参照カウントをサポートします。
参照カウント型はrefcountedアノテーションを使用して宣言されます。 参照カウント記憶領域は参照を数えるためのフィールドを持っている必要があるため、 struct型へのポインタのみを refcountedとして宣言してもかまいません。 構造体(あるいは整数型)の中の1つのフィールドは、 このフィールドの値が構造体への生きている(live)参照の数であることを示す refsアノテーションが前に付きます。 例えば(rstring.hの中)、
は、抽象参照カウント型としてrstringを宣言しています。 refsフィールドは参照の数を数え、contentsフィールドは文字の中身を保持します。
rstring.c |
Running Splint |
# include "rstring.h"
static rstring rstring_ref (rstring r) { r->refs++; 6 return r; }
rstring rstring_first (rstring r1, rstring r2) { if (strcmp (r1->contents, r2->contents) < 0) 12 return r1; else 14 return rstring_ref (r2); } |
> splint rstring.c rstring.c:12: Reference counted storage returned without modifying reference count: r1
参照カウントが追加されているため、 6行目ではエラーは報告されていません。 rstring_ref が新しい参照を返すため、14行目ではエラーは報告されていません。
|
図 9. 参照カウント |
refcounted記憶領域を返す全ての関数は、返す前に参照カウントを増やす必要があります。 参照カウントが増やされた場合、Splintは判断できません。 そのため、refcounted記憶領域への参照を直接返す どんな関数もエラーを発生させます。これは新しい参照(例えば、図9のrstring_ref)を返す関数を使用することにより回避されます。
参照カウント型はtempあるいはdependent引数として 渡されてもかまいません。 only引数 として渡すことは出来ません。 その代わり、killref アノテーションは関数呼び出しによって削除された参照を持つ引数を表すために使用されます。 only引数と同様に、 killref仮引数に対応する実引数は 呼び出しの後で関数呼び出しもとの仲で使用することは出来ません。 Splintは関数の実装が killref引数として渡されたそれらを渡すことか、 あるいは、参照カウントを増やすことなくそれらを返すか割り当てることのどちらかによって、 全てのkillref引数を解放していることをチェックします。
このドキュメントはSplint(英)のサイトを元に作成しました
[PageInfo]
LastUpdate: 2014-02-11 15:05:25, ModifiedBy: daruma_kyo
[License]
Creative Commons 2.1 Attribution-ShareAlike
[Permissions]
view:all, edit:login users, delete/config:members