ダングリングポインタ
無効なメモリ領域を指すポインタはダングリングポインタ(dangling pointer)と呼ばれる。とりわけ、本来有効だったメモリ領域が解放処理などによって無効化されたにもかかわらず、そのメモリ領域を参照し続けているポインタのことを、ダングリングポインタと呼ぶ。
例えば、free関数等によって解放されたオブジェクトを参照し続けているポインタ変数などは、無効なメモリアドレスを指していることから、ダングリングポインタの状態にあると言える。このような無効な領域を指すポインタは、誤って利用されると重大なバグを引き起こすことにも繋がる。そのため、そのようなポインタ変数に対しては、空ポインタ(NULL)を代入することで、ダングリングポインタの発生やその影響を回避することなどが求められる。
int *pointer = malloc(8); // メモリ確保
free(pointer); // メモリ解放
pointer; // ダングリングポインタ
free(pointer); // ダングリングポインタへの二重解放はクラッシュ(危険)
pointer = NULL; // ダングリングポインタの影響を回避
free(pointer); // 空ポインタへの二重解放は無効化(安全)
*pointer = 99; // 空ポインタへの操作はクラッシュ(セキュリティリスクの回避)
なお、ダングリングポインタは「ぶら下がりポインタ」と訳されることもある。ダングリングポインタと似たような用語として、ワイルドポインタ(wild pointer)というものもある(wild = 野生)。こちらは不正なメモリ領域を指すポインタのことを言い、未初期化のポインタ変数や、誤ったポインタ演算によって発生したポインタ等に対して使われる。
目次
ダングリングポインタの例
解放済みのオブジェクトポインタはダングリングポインタの良い例である。
char *p = malloc(1); // メモリ確保
free(p); // メモリ解放
p; // ポインタ変数`p`は既にダングリングポインタ
*p = 'c'; // ダングリングポインタへのアクセス(危険)
また、寿命を迎えたオブジェクトを参照し続けているポインタも、ダングリングポインタとなる。
char *p = NULL;
{
// 自動変数`c`の生存期間はスコープ内のみ
char c = 'c';
p = &c; // ローカル変数の参照を保持
}
// ローカル変数`c`の寿命は既に尽きている
p; // よってポインタpはダングリングポインタ
printf("%c", *p); // 不定な動作を引き起こすため危険
int *f() {
int i = 99; // 変数`i`の生存期間は関数内のみ
return &i; // 受け取り先ではダングリングポインタとなる
}
ローカル変数の生存期間に起因するダングリングポインタは、関数へのポインタ渡しを行う際にも問題になるため注意しなければならない。次のような、引数として受け取ったポインタを永続的なオブジェクト領域(グローバル変数、構造体メンバ変数、等)に格納するような関数に対して、ローカル変数へのポインタを渡してしまうと、ダングリングポインタの影響を受ける危険性がある。
int *p = NULL;
void f(int *i) { p = i; }
void g() { if (p) printf("%d", *p); }
int main() {
{ int i = 99;
f(&i); // ローカル変数のエスケープが発生
g(); // 出力結果: "99"
}
p; // ダングリングポインタ
g(); // 結果は不定
}
他にもrealloc関数使用時にメモリブロックの変更が発生してしまうと、元のポインタがダングリングポインタに変化する。
char *a = malloc(1); // メモリ領域確保
char *b = realloc(a, 2); // メモリ領域拡張
if (a != b) {
b; // メモリ領域の再確保によりアドレスが変化している
a; // ダングリングポインタ
a = b; // 対応策: 新しいアドレスで上書きする
}
free(a); // 安全
参考: realloc関数|正しい使い方と注意点/メモリの断片化について
ワイルドポインタの例
なお、未初期化のポインタ変数はワイルドポインタとなりうる。
int *p; // 未定義動作によりNULLで初期化されない可能性がある
p; // ワイルドポインタの可能性あり
*p = 9; // 危険
誤ったポインタ演算によって、不正なメモリ領域を指すようになったポインタ変数はワイルドポインタとなる。
const char *s = "abc"; // 4byte分のメモリ領域を利用({'a', 'b', 'c', '\0'})
s = s + 2; // ポインタ演算
*s; // 'c' (正常)
s = s + 4; // ワイルドポインタに変化
*s; // '-' (不定な値)
// ポインタ変数sは不正な領域を指している
// ポインタ変数sは文字列`"abc"`の記憶領域から外れた領域を指している
ダングリングポインタの危険性
二重解放
ダングリングポインタの存在によって、解放済みのオブジェクトを二重に解放してしまう危険性を生み出す。オブジェクトの二重解放はプログラムのエラーやクラッシュの原因となるため、細心の注意を払う必要がある。
void* ptr = malloc(1);
free(ptr); // 正常
free(ptr); // エラー発生
// malloc: *** error for object 0x100700000: pointer being freed was not allocated
// *** set a breakpoint in malloc_error_break to debug
不正領域へのアクセス
不定なメモリ領域へのアクセスによって、本来プログラムが意図しないような動作を引き起こす危険性がある。
char *a = malloc(1);
free(a); // 解放
char *b = malloc(1);
*b = 'b';
*a = 'a'; // ダングリングポインタへの操作
printf("%c", *b); // "a" ("b"ではない!?)
このように全く関係のないメモリ領域を書き換えてしまう危険性がある。これはソフトウェアの脆弱性やセキュリティーの問題にも繋がるため、大変重大な問題と言える。
加えて、このような不正領域へのデリファレンスや書き換えは、構造体ポインタへのメンバアクセス時においても、プログラムのクラッシュや強制終了を引き起こさずにそのまま動いてしまうことが多く、問題の発見も遅れやすいという厄介さがある。
struct Number { /* C++言語による例 */
int value;
void print() { printf("%d", value); }
};
Number *ptr = new Number; // オブジェクトの動的確保
delete ptr; // オブジェクトの解放
ptr->value = 9; // クラッシュしない
printf("%d", ptr->value); // クラッシュしない
ptr->print(); // クラッシュしない(C++言語ではメンバ関数もそのまま呼ばれる)
// 出力結果: "99"
ダングリングポインタの回避
ダングリングポインタによる危険性や問題を回避するするためには、ポインタの初期化作業の徹底や、ソフトウェア設計による回避が必要になる。
ポインタ変数の初期化
使われなくなったポインタ変数を明示的に空ポインタ(NULL
, ヌルポインタ)で初期化することで、ダングリングポインタによる影響を回避できる。
char *a = malloc(1);
free(a);
a = NULL; // ダングリングポインタへの参照を断ち切る
free(a); // 空ポインタに対する解放処理は無効化される
*a = 'a'; // 実行時エラー発生(問題の早期発見に繋がる)
C言語の場合はヌルポインタに対する解放処理は無効化されるため、ポインタの二重解放の問題にも対処することができる。
またヌルポインタへの操作はアクセス違反やセグメンテーション違反によって、プログラムの強制終了やクラッシュを引き起こすことがあり、このことが逆に、脆弱性の早期発見に繋がったり、不正領域へのアクセスに伴うセキュリティーホールの問題を抑えることに繋がる場合がある。もっともこれはメモリ保護機能が有効な実行環境での話である。
ソフトウェア設計による回避
また、ダングリングポインタの問題を回避するためには、複数のポインタが同一のオブジェクトを無闇に参照しないような設計を意識する必要がある。または生のポインタを直接やり取りするのではなく、カプセル化の技法やアクセサ、デストラクタの仕組みを用いてオブジェクトのやり取りや操作をより抽象化したり制限することが求められる。先程のrealloc関数の例ような、副作用を伴うAPI設計も極力回避する必要がある。
スマートポインタやARC(特殊なメモリ管理機構。半自動の参照カウンタ方式を実現する)における弱参照と強参照の概念をソフトウェア設計に取り入れることも有効的である。例えばARCで弱参照指定されたポインタは、ダングリングポインタになる前に自動的にヌルポインタによる初期化が行われるため、ダングリングポインタの問題が起こらないようになっている。
ARCの実現は難しいが、スマートポインタなら一部の言語で比較的容易に実装ができる。中にはスマートポインタを標準でサポートしている言語も存在する(C++等)。
ただしスマートポインタを用いた設計には、自前によるガード処理やアンラップ処理が必要になるため、利用に若干の手間が掛かるという欠点もある。代替案としては、ARCやガベージコレクタを取り入れた言語への移行が考えられる。
スマートポインタとARCの問題点
なおARCとスマートポインタを用いた方法には、循環参照によるメモリリークが起こりやすいという危険性もある。これは、強参照同士のオブジェクトがお互いを参照し合うような形態が発生することによって、両オブジェクトの開放処理が働かなくなってしまうという問題である。これらは手違いによって発生し、とりわけ弱い型付けの言語では注意していなければ防げない問題となっているため、安全性や信頼性の観点ではガベージコレクタに劣る面がある。
プログラミング言語による回避
ガーベッジコレクションを採用したJavaやC#、エスケープ解析が強力なGoなどの言語を用いることで、ダングリングポインタとは無縁のプログラミング環境を得ることができる。またARCを採用した言語(Swift, Objective-C)や次世代のメモリ管理方式(Ownershipモデル等)を採用したRust等の言語に移行する手もある。
ポインタを直接的に扱わない次世代のプログラミング言語やスクリプト言語(Ruby, Python, JavaScript)の利用も適切である。