web-dev-qa-db-ja.com

ローカル変数のメモリはその範囲外でアクセスできますか?

次のようなコードがあります。

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

そして、コードは実行時例外なしで実行されているだけです。

出力は58でした

どうすることができますか?ローカル変数のメモリは、その関数外ではアクセスできませんか?

953
Avraham Shukron

どうして?ローカル変数のメモリは、その関数の外ではアクセスできませんか?

ホテルの部屋を借ります。ベッドサイドテーブルの一番上の引き出しに本を置いて、寝ます。翌朝チェックアウトしますが、キーを返すのを「忘れる」。あなたは鍵を盗みます!

1週間後、ホテルに戻り、チェックインせずに、盗まれた鍵で古い部屋に忍び込み、引き出しを覗き込みます。あなたの本はまだそこにあります。驚くべき!

それはどういうことですか?部屋を借りていない場合、ホテルの部屋の引き出しの内容にアクセスできませんか?

まあ、明らかに、このシナリオは現実の世界で問題なく発生する可能性があります。部屋に入室する権限がなくなったときに、本が消える不思議な力はありません。盗まれた鍵で部屋に入ることを妨げる不思議な力もありません。

ホテルの管理はrequiredではなく、本を削除します。あなたは彼らと契約を結んでいませんでした。あなたが物を置き去りにすれば、彼らはあなたのためにそれを細断します盗まれた鍵を使って部屋に不法に再入室した場合、ホテルのセキュリティスタッフは必須ではなく、あなたがこっそり入るのをキャッチしません。後で部屋に忍び込むように試みます、あなたは私を止める必要があります。」むしろ、あなたは彼らと契約を結び、「後で部屋に忍び込まないことを約束します」、あなたが破ったの契約を結びました。

この状況では何でも起こります。本はそこにあります-あなたは幸運になりました。他の誰かの本がそこにあり、あなたの本がホテルの炉にあるかもしれません。入ってすぐに誰かがそこにいて、本をばらばらに破ります。ホテルでは、テーブルと本を完全に取り外して、ワードローブに交換することもできました。ホテル全体が取り壊され、フットボールスタジアムに置き換わろうとしているかもしれません。そして、あなたがこっそりしている間に爆発で死ぬでしょう。

何が起こるかわかりません。ホテルをチェックアウトし、後で違法に使用するためのキーを盗んだとき、yoはシステムの規則を破ることを選択したため、予測可能で安全な世界に住む権利を放棄しました。

C++は安全な言語ではありません。これにより、システムのルールを破ることができます。あなたが入室を許可されていない部屋に戻ったり、もうそこにさえいないかもしれない机を調べたりするような違法で愚かなことをしようとしても、C++はあなたを止めようとしません。 C++より安全な言語は、たとえばキーをより厳密に制御することにより、権限を制限することでこの問題を解決します。

更新

聖なる良さ、この答えは多くの注目を集めています。 (理由はわかりませんが、それは単なる「楽しい」小さなアナロジーであると考えましたが、何でも。)

これをもう少し技術的な考えで少し更新することは密接な関係があると思いました。

コンパイラは、そのプログラムによって操作されるデータのストレージを管理するコードを生成するビジネスです。メモリを管理するためのコードを生成する方法はたくさんありますが、時間の経過とともに2つの基本的な手法が確立されてきました。

1つ目は、ストレージ内の各バイトの「寿命」、つまり、あるプログラム変数と有効に関連付けられている期間を簡単に予測できない、ある種の「長期」ストレージ領域を持つことです。時間の。コンパイラーは、必要に応じてストレージを動的に割り当て、不要になったときにストレージを再利用する方法を知っている「ヒープマネージャー」への呼び出しを生成します。

2番目の方法は、各バイトの寿命がよく知られている「短命の」ストレージ領域を持つことです。ここでは、ライフタイムは「ネスト」パターンに従います。これらの短命の変数の中で最も長命の変数は、他の短命の変数の前に割り当てられ、最後に解放されます。寿命の短い変数は、寿命の最も長い変数の後に割り当てられ、それらの前に解放されます。これらの寿命の短い変数の寿命は、寿命の長い変数の寿命内に「ネスト」されます。

ローカル変数は後者のパターンに従います。メソッドに入ると、そのローカル変数が有効になります。そのメソッドが別のメソッドを呼び出すと、新しいメソッドのローカル変数が有効になります。最初のメソッドのローカル変数が無効になる前に、それらは無効になります。ローカル変数に関連付けられたストレージのライフタイムの開始と終了の相対的な順序は、事前に解決できます。

このため、ローカル変数は通常、「スタック」データ構造のストレージとして生成されます。スタックには、最初にプッシュされるものが最後にポップされるというプロパティがあるためです。

まるでホテルが部屋を連続して貸し出すことを決めたようで、チェックアウトした部屋番号よりも高い部屋番号を持つ全員がチェックアウトするまでチェックアウトできません。

それでは、スタックについて考えてみましょう。多くのオペレーティングシステムでは、スレッドごとに1つのスタックを取得し、スタックは特定の固定サイズに割り当てられます。メソッドを呼び出すと、ものがスタックにプッシュされます。次に、元のポスターが行うように、メソッドへスタックへのポインタを渡すと、それは完全に有効な100万バイトのメモリブロックの中央へのポインタにすぎません。私たちの例えでは、ホテルからチェックアウトします。あなたがするとき、あなたは最も高い番号の占有された部屋からチェックアウトしました。他の人があなたの後にチェックインせず、あなたが不法にあなたの部屋に戻った場合、すべてのものはまだそこにあることが保証されますこの特定のホテルで

一時ストアにはスタックが非常に安価で簡単なので、スタックを使用します。 C++の実装は、ローカルのストレージにスタックを使用する必要はありません。ヒープを使用できます。それはプログラムを遅くするからです。

C++の実装は、スタックに残したゴミをそのままにしておく必要はありません。そうすれば、後で違法に戻ってくることができます。コンパイラが、空にした「部屋」のすべてをゼロに戻すコードを生成することは完全に合法です。再び、それは高価になるからです。

C++の実装は、スタックが論理的に縮小しても、以前は有効だったアドレスが引き続きメモリにマップされるようにするために必要ではありません。実装は、オペレーティングシステムに「このスタックのページの使用はこれで終わりです。別の言い方をするまで、以前に有効なスタックページに触れた場合、プロセスを破壊する例外を発行する」ことができます。繰り返しますが、実装は実際にはそれを行いません。なぜなら、それは遅くて不必要だからです。

代わりに、実装によりミスを犯して逃げることができます。ほとんどの時間。ある日まで本当にひどいものがうまくいかず、プロセスが爆発します。

これには問題があります。多くのルールがあり、それらを誤って破ることは非常に簡単です。確かに何度もあります。さらに悪いことに、問題は、メモリが破損した後に数十億ナノ秒で破損していることが検出され、誰がそれを台無しにしたかを把握することが非常に困難な場合にのみ表面化します。

より多くのメモリセーフな言語は、電力を制限することでこの問題を解決します。 「通常の」C#では、ローカルのアドレスを取得してそれを返したり、後で使用するために保存したりする方法はありません。地元の住所を取得することはできますが、言語は巧妙に設計されているため、現地の寿命が切れると使用できなくなります。ローカルのアドレスを取得して戻すには、コンパイラを特別な「安全でない」モードに設定する必要があります。andプログラムに「安全でない」という単語を入れて、あなたはおそらくルールを破る可能性のある危険な何かをしているという事実。

さらに読むには:

4724
Eric Lippert

ここでやっていることは、単にそれを読み書きすることです。 慣れている aのアドレスになります。 fooの外側にいるので、これは単にランダムなメモリ領域へのポインタです。あなたの例では、そのメモリ領域が存在していて、他に何も現在それを使用していないということが起こります。あなたはそれを使い続けることによって何かを壊すことはありません、そしてそれ以外のものはまだそれを上書きしていません。したがって、5はまだそこにあります。実際のプログラムでは、そのメモリはほとんどすぐに再利用され、これを行うことで何かを壊すことになります(ただし、症状はそれ以降には現れないかもしれません)

fooから戻ったとき、あなたはもうそのメモリを使用していないことをOSに伝え、それを他のものに再割り当てすることができます。運が良ければ、二度と再割り当てされず、OSが再びそれを使用していることをキャッチしないのであれば、うそをつきません。たぶんあなたはあなたがそのアドレスで終わる他の何でも上書きすることになるでしょうが。

コンパイラが文句を言わない理由を疑問に思っているのであれば、おそらくfooが最適化によって取り除かれたからでしょう。それは通常この種のことについてあなたに警告します。 Cはあなたが自分のしていることを知っていると仮定し、技術的にはここでスコープに違反していない(a以外のfoo自体への参照はありません)。

一言で言えば、これは通常はうまくいきませんが、時々偶然にはうまくいきます。

271
Rena

ストレージスペースがまだ足を踏み入れていないからです。その振る舞いを当てにしないでください。

148
msw

すべての答えに少し追加:

あなたがそのような何かをするならば:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

出力はおそらく次のようになります。7

これは、foo()から戻った後、スタックが解放されてからboo()によって再利用されるためです。実行可能ファイルを逆アセンブルすると、それが明確にわかります。

79
Michael

C++では、にアクセスできますが、にするべきではありません。アクセスしているアドレスは無効です。 fooが戻った後に他に何もメモリをスクランブルしなかったので、それは works ですが、多くの状況下でクラッシュする可能性があります。 Valgrind を使用してプログラムを分析するか、最適化してコンパイルしてみてください。

68
Charles Brunet

無効なメモリにアクセスしてC++例外をスローすることは決してありません。あなたは単に任意のメモリ位置を参照するという一般的な考え方の例を挙げているだけです。私はこのように同じことをすることができます:

unsigned int q = 123456;

*(double*)(q) = 1.2;

ここでは、123456をダブルアドレスとして扱い、それに書き込みます。いろいろなことが起こり得ます:

  1. qは実際には本当にdoubleの有効なアドレスかもしれません。 double p; q = &p;
  2. qは割り当てられたメモリのどこかを指している可能性があり、その場合は8バイトだけ上書きします。
  3. qは割り当てられたメモリの外を指しており、オペレーティングシステムのメモリマネージャは私のプログラムにセグメンテーションフォルトシグナルを送り、ランタイムにそれを終了させます。
  4. あなたは宝くじに勝ちます。

設定方法としては、返されるアドレスが有効なメモリ領域を指すことがもう少し合理的です。おそらくスタックのもう少し先にあるはずですが、それでもまだアクセスできない無効な場所です決定論的なファッション。

通常のプログラム実行中に、そのようなメモリアドレスの意味的妥当性を自動的にチェックする人は誰もいません。しかし、valgrindのようなメモリデバッガはこれをうまくやるので、プログラムを実行してエラーを確認する必要があります。

65
Kerrek SB

オプティマイザーを有効にしてプログラムをコンパイルしましたか? foo()関数は非常に単純で、結果のコードでインライン化または置き換えられた可能性があります。

しかし、私はマークBに同意します。結果として生じる振る舞いは未定義です。

28
gastush

あなたの問題は scope とは無関係です。表示されているコードでは、関数mainは関数foo内の名前を認識しないため、fooのaに直接 this nameをfooの外側から使用することはできません。

あなたが抱えている問題は、違法メモリを参照するときにプログラムがエラーを通知しない理由です。これは、C++標準では、違法メモリと合法メモリとの間の明確な境界が規定されていないためです。飛び出したスタックで何かを参照するとエラーが発生することがあります。場合によります。この動作を頼りにしないでください。プログラムすると常にエラーが発生すると想定しますが、デバッグするとエラーが発生することはないと想定します。

22
Chang Peng

あなたはただメモリアドレスを返しているだけです、それは許されますがおそらくエラーです。

そのメモリアドレスを間接参照しようとすると、未定義の動作になります。

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
17
Brian R. Bondy

スタックがそこに置かれてから(まだ)変更されていないので、それはうまくいきます。 aに再度アクセスする前に、他のいくつかの関数(他の関数も呼び出しています)を呼び出してください。おそらくもうこれ以上ラッキーではないでしょう... ;-)

16
Adrian Grigore

それは古典的な 未定義の動作 2日前にはここで説明されていました - サイト内で少し検索します。一言で言えば、あなたはラッキーでしたが、何かが起こった可能性があり、あなたのコードはメモリへの不正なアクセスをしています。

16
Kerrek SB

Alexが指摘したように、この振る舞いは未定義です - 実際、ほとんどのコンパイラはこれをしないように警告します。クラッシュを起こすのは簡単な方法だからです。

このような不気味な振る舞いの例として、 おそらく を得るには、このサンプルを試してください。

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

これは "y = 123"を出力しますが、あなたの結果は異なるかもしれません(本当に!)。あなたのポインタは、他の、無関係なローカル変数を破壊しています。

16
AHelps

すべての警告に注意してください。エラーを解決するだけではいけません。
GCCはこの警告を示しています

警告:ローカル変数 'a'のアドレスが返されました

これがC++の力です​​。あなたは記憶を気にするべきです。 -Werrorフラグを使うと、この警告はエラーになりましたので、今度はデバッグする必要があります。

16
sam

あなたは実際に未定義の動作を呼び出しました。

一時的な作品のアドレスを返すが、関数の終わりに一時的なものが破壊されるので、それらにアクセスした結果は未定義になるでしょう。

だからあなたはaを修正したのではなく、aがかつてあったメモリ位置を修正しました。この違いは、クラッシュした場合とクラッシュしなかった場合の違いと非常によく似ています。

15

典型的なコンパイラの実装では、コードは「で占められていたというアドレスでメモリブロックの値を出力する」と考えることができます。また、ローカルのintを制約する関数に新しい関数呼び出しを追加すると、aの値(またはaが指すために使用されていたメモリアドレス)が変わる可能性があります。これは、異なるデータを含む新しいフレームでスタックが上書きされるためです。

しかしながら、これは 未定義 の振る舞いであり、あなたはそれを当てにするべきではありません!

13
larsmoa

aはその有効期間中、一時的に割り当てられる変数であるためです(foo関数)。 fooから戻った後、メモリは解放され上書きされる可能性があります。

あなたがしていることは 未定義の振舞い として説明されています。結果は予測できません。

13
littleadv

:: printfを使用してcoutを使用しないと、正しい(?)コンソール出力を持つものが劇的に変わる可能性があります。以下のコード内でデバッガを試すことができます(x86、32ビット、MSVisual Studioでテスト済み)。

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}
11
Mykola

関数から戻った後、メモリ位置に保持された値の代わりにすべての識別子が破壊され、識別子を持たずに値を見つけることはできません。ただし、その位置には前の関数によって格納された値が含まれます。

そのため、ここでは関数foo()aのアドレスを返し、aはそのアドレスを返した後に破棄されます。そして、あなたはその返されたアドレスを通して修正された値にアクセスすることができます。

現実世界の例を見てみましょう。

男がある場所でお金を隠し、その場所を教えているとします。しばらくすると、お金の場所を言っていた男が死にます。しかし、それでもあなたはその隠されたお金にアクセスすることができます。

それはメモリアドレスを使うための「汚い」方法です。あなたがアドレス(ポインタ)を返すとき、それが関数のローカルスコープに属しているかどうかわかりません。住所です。 'foo'関数を呼び出したので、 'a'のアドレス(メモリ位置)はすでに(安全に、少なくとも今のところ)あなたのアプリケーション(プロセス)のアドレス可能なメモリに割り当てられています。 'foo'関数が戻った後、 'a'のアドレスは 'ダーティ'と見なすことができますが、クリーンアップされたり、プログラムの他の部分の式によって妨害/変更されたりすることはありません。 C/C++コンパイラでは、そのような「ダーティ」なアクセスを防ぐことはできません(気になるなら警告するかもしれません)。何らかの方法でアドレスを保護しない限り、プログラムインスタンス(プロセス)のデータセグメント内にある任意のメモリ位置を安全に使用(更新)できます。

2
Ayub

あなたのコードは非常に危険です。ローカル変数を作成していて(関数の終了後に破棄されたと見なされます)、破棄された後にその変数のメモリのアドレスを返します。

これは、メモリアドレスが有効かどうかを示す可能性があり、コードはメモリアドレスの問題(たとえば、セグメンテーションフォルト)に対して脆弱になる可能性があることを意味します。

これは、あなたがとても悪いことをしているということを意味します。あなたがメモリアドレスをポインタに渡しているからです。

代わりにこの例を考えて、テストしてください。

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

あなたの例とは異なり、この例であなたは:

  • int用のメモリをローカル関数に割り当てる
  • 関数が期限切れになっても、そのメモリアドレスはまだ有効です(誰によっても削除されません)。
  • メモリアドレスは信頼できます(そのメモリブロックは空きとは見なされないため、削除されるまで上書きされません)。
  • 使用しない場合はメモリアドレスを削除する必要があります。 (プログラムの最後にある削除を参照)
0
Nobun