malloc
とfree
の仕組みを知りたいです。
int main() {
unsigned char *p = (unsigned char*)malloc(4*sizeof(unsigned char));
memset(p,0,4);
strcpy((char*)p,"abcdabcd"); // **deliberately storing 8bytes**
cout << p;
free(p); // Obvious Crash, but I need how it works and why crash.
cout << p;
return 0;
}
可能であれば、答えがメモリレベルで詳細に説明されていれば、本当に感謝しています。
OK、mallocに関するいくつかの回答はすでに投稿されています。
より興味深い部分は、フリーの仕組みです(この方向では、mallocもよりよく理解できます)。
多くのmalloc/free実装では、freeは通常、オペレーティングシステムにメモリを返しません(少なくともまれな場合のみ)。その理由は、ヒープにギャップができるため、ギャップが発生する可能性があり、2 GBまたは4 GBの仮想メモリをギャップで終了するだけだからです。仮想メモリが終了するとすぐに大きな問題が発生するため、これは避ける必要があります。もう1つの理由は、OSは特定のサイズとアライメントのメモリチャンクしか処理できないことです。具体的には:通常、OSは仮想メモリマネージャーが処理できるブロック(ほとんどの場合、512バイトの倍数、たとえば4KB)のみを処理できます。
したがって、OSに40バイトを返すことは機能しません。それで、無料は何をしますか?
Freeは、メモリブロックを独自の空きブロックリストに入れます。通常、アドレス空間内の隣接するブロックを一緒に結合しようとします。フリーブロックリストは、先頭に管理データがあるメモリチャンクの循環リストです。これは、標準のmalloc/freeを使用して非常に小さなメモリ要素を管理することが効率的でない理由でもあります。すべてのメモリチャンクには追加のデータが必要で、サイズが小さくなると断片化が発生します。
空きリストは、新しいメモリチャンクが必要なときにmallocが最初に調べる場所でもあります。 OSから新しいメモリを呼び出す前にスキャンされます。必要なメモリより大きいチャンクが見つかると、2つの部分に分割されます。 1つは呼び出し元に返され、もう1つはフリーリストに戻されます。
この標準の動作にはさまざまな最適化があります(たとえば、メモリの小さな塊)。ただし、mallocとfreeは非常に普遍的でなければならないため、代替が使用できない場合の標準動作は常にフォールバックです。フリーリストの処理には最適化もあります。たとえば、サイズでソートされたリストにチャンクを保存します。ただし、すべての最適化にも独自の制限があります。
コードがクラッシュする理由:
理由は、4文字のサイズの領域に9文字(末尾のヌルバイトを忘れないでください)を書き込むことで、データのチャンクの「背後」にある別のメモリチャンクに保存されている管理データを上書きする可能性があるためです(このデータはほとんどの場合、メモリチャンクの「前」に格納されるためです。 freeがあなたのチャンクをfreeリストに追加しようとすると、この管理データに触れることができるため、上書きされたポインターにつまずきます。これにより、システムがクラッシュします。
これはかなり優雅な動作です。また、暴走ポインターがメモリーフリーリストのデータを上書きし、システムがすぐにクラッシュせず、一部のサブルーチンが後でクラッシュする状況も見ました。中程度の複雑さのシステムでさえ、このような問題はデバッグするのが非常に難しい場合があります!私が関与した1つのケースでは、メモリダンプで示された場所とはまったく異なる場所にあったため、クラッシュの原因を見つけるのに(より大きな開発者グループ)数日かかりました。それは時限爆弾のようなものです。次の「free」または「malloc」はクラッシュしますが、理由はわかりません。
これらは最悪のC/C++の問題の一部であり、ポインターがそれほど問題になる理由の1つです。
Aluserが このフォーラムスレッド で述べているように:
プロセスには、ヒープと呼ばれるアドレスxからアドレスyまでのメモリ領域があります。すべてのmallocされたデータはこの領域に存在します。 malloc()は、ヒープ内のすべての空き領域のデータ構造、たとえばリストを保持します。 mallocを呼び出すと、リストから十分な大きさのチャンクが検索され、そのポインターが返され、空きがなくなったという事実とその大きさが記録されます。同じポインターを使用してfree()を呼び出すと、free()はそのチャンクの大きさを検索し、フリーチャンクのリストに追加します。 malloc()を呼び出して、ヒープ内に十分な大きさのチャンクが見つからない場合は、brk()syscallを使用してヒープを拡大します。つまり、アドレスyを増やし、古いyと新しいyの間のすべてのアドレスを有効なメモリであること。 brk()はシステムコールでなければなりません。ユーザースペースから同じことを完全に行う方法はありません。
malloc()はシステム/コンパイラに依存しているため、具体的な答えを出すのは困難です。しかし基本的には、どのメモリが割り当てられているかを追跡し、その方法に応じて、freeの呼び出しが失敗または成功する可能性があります。
malloc() and free() don't work the same way on every O/S.
Malloc/freeの実装の1つは次のことを行います。
あなたのサンプルコードは本質的にサンプルプログラムがトラップしない理由を尋ねます、そして答えはメモリ保護はカーネル機能であり、ページ全体にのみ適用されますが、メモリアロケータはライブラリ機能であり、強制的に管理されます..任意多くの場合、ページよりもはるかに小さいサイズのブロック。
メモリは、ページ単位でプログラムからのみ削除できますが、それも見られないでしょう。
calloc(3)およびmalloc(3)は、必要に応じて、カーネルと対話してメモリを取得します。ただし、free(3)のほとんどの実装は、カーネルにメモリを返しません。1、彼らはcalloc()とmalloc()が解放されたブロックを再利用するために後で調べるフリーリストにそれを追加するだけです。
Free()がシステムにメモリを返したい場合でも、実際に領域を保護するためにカーネルを取得するには少なくとも1つの連続したメモリページが必要になるため、小さなブロックを解放すると保護の変更が発生しますlastページ内の小さなブロック。
あなたのブロックはそこにあり、フリーリストに載っています。ほとんどの場合、まるでまだ割り当てられているかのように、いつでもそれにアクセスできます。 Cは、マシンコードに直接コンパイルします。特別なデバッグの準備がなければ、ロードとストアの健全性チェックは行われません。ここで、フリーブロックにアクセスしようとすると、ライブラリ実装者に不当な要求を行わないために、動作は標準によって定義されていません。割り当てられたブロックの外で解放されたメモリまたはメモリにアクセスしようとすると、さまざまな問題が発生する可能性があります。
したがって、あなたの例から全体的な理論までさかのぼると、malloc(3)は必要に応じてカーネルからメモリを取得します。通常はページ単位です。これらのページは、プログラムの必要に応じて分割または統合されます。 Mallocとfreeは協力してディレクトリを管理します。大きなブロックを提供できるように、可能であれば隣接する空きブロックを合体します。ディレクトリは、リンクされたリストを形成するために、解放されたブロックでメモリを使用することを含む場合と含まない場合があります。 (代替手段はもう少し共有メモリでページングしやすく、特別にメモリをディレクトリに割り当てる必要があります。)Mallocとfreeには、特別なオプションのデバッグコードがコンパイルされている場合でも、個々のブロックへのアクセスを強制する機能はほとんどありませんプログラム。
1. free()のほとんどの実装がシステムにメモリを返そうとしないという事実は、必ずしも実装者が緩んでいるためではありません。カーネルとの対話は、単にライブラリコードを実行するよりもはるかに遅く、メリットはわずかです。ほとんどのプログラムは定常状態または増加するメモリフットプリントを持っているため、リターナブルメモリを探してヒープの分析に費やす時間は完全に無駄になります。他の理由には、内部フラグメンテーションにより、ページに配置されたブロックが存在する可能性が低くなり、ブロックを返すとブロックがどちらかの側にフラグメント化される可能性が高いという事実が含まれます。最後に、大量のメモリを返すいくつかのプログラムは、malloc()をバイパスして、とにかくページを単に割り当て、解放する可能性があります。
理論的には、mallocはこのアプリケーションのオペレーティングシステムからメモリを取得します。ただし、必要なのは4バイトだけであり、OSはページ単位で動作する必要がある(多くの場合4k)ので、mallocはそれ以上のことを行います。ページを取得し、そこに独自の情報を入れて、そのページから割り当てたものと解放したものを追跡できるようにします。
たとえば、4バイトを割り当てると、mallocは4バイトへのポインターを提供します。気づかないかもしれませんが、8〜12バイトのメモリbeforeは、割り当てたすべてのメモリのチェーンを作成するためにmallocによって4バイトが使用されています。 freeを呼び出すと、ポインタが取得され、データのある場所に戻り、その上で動作します。
メモリを解放すると、mallocはそのメモリブロックをチェーンから外し、そのメモリをオペレーティングシステムに返す場合と返さない場合があります。その場合、OSはその場所にアクセスするためのアクセス権を奪うため、そのメモリへのアクセスはおそらく失敗します。 mallocがメモリを保持している場合(そのページに他の要素が割り当てられているため、または何らかの最適化のために)、アクセスが機能します。まだ間違っていますが、うまくいくかもしれません。
免責事項:私が説明したのは、mallocの一般的な実装ですが、決して唯一の可能な実装ではありません。
NULターミネーターのため、strcpy行は8バイトではなく9バイトを格納しようとします。未定義の動作を呼び出します。
Freeの呼び出しは、クラッシュする場合としない場合があります。 4バイトの割り当ての「後」のメモリは、CまたはC++の実装によって他の何かに使用される可能性があります。それが他の何かに使用される場合、それをくまなく落書きすると、その「他の何か」がうまくいかなくなりますが、それが他の何かに使用されない場合、たまたまそれで逃げることができます。 「それで逃げる」というのは良いように聞こえるかもしれませんが、実際には悪いのです。これは、コードが正常に実行されているように見えるが、将来の実行ではうまくいかない可能性があるためです。
デバッグスタイルのメモリアロケーターを使用すると、そこに特別なガード値が書き込まれ、その値を無料でチェックし、見つからない場合はパニックに陥ることがあります。
そうしないと、次の5バイトに、まだ割り当てられていない他のメモリブロックに属するリンクノードの一部が含まれていることがあります。ブロックを解放するには、使用可能なブロックのリストにブロックを追加する必要があります。また、リストノードで落書きをしているため、無効な値でポインターを逆参照してクラッシュを引き起こす可能性があります。
それはすべてメモリアロケータに依存します-異なる実装は異なるメカニズムを使用します。
Malloc()およびfree()の動作方法は、使用するランタイムライブラリによって異なります。通常、malloc()はオペレーティングシステムからヒープ(メモリブロック)を割り当てます。 malloc()への各リクエストは、このメモリの小さなチャンクを割り当て、呼び出し元へのポインタを返します。メモリ割り当てルーチンは、ヒープ上の使用済みメモリと空きメモリを追跡できるように、割り当てられたメモリブロックに関する追加情報を保存する必要があります。この情報は、多くの場合、malloc()によって返されるポインターの直前の数バイトに格納され、メモリブロックのリンクリストにすることができます。
Malloc()によって割り当てられたメモリブロックを超えて書き込むことで、残りの未使用のメモリブロックである可能性がある次のブロックの簿記情報の一部を破壊する可能性が高くなります。
プログラムをクラッシュさせる可能性のある場所の1つは、バッファーにコピーする文字が多すぎる場合です。余分な文字がヒープ外にある場合、存在しないメモリに書き込もうとしているときにアクセス違反が発生する可能性があります。
これは、mallocおよびfreeとは特に関係ありません。文字列をコピーした後、プログラムは未定義の動作を示します。その時点またはその後の任意の時点でクラッシュする可能性があります。これは、mallocとfreeを一度も使用したことがなく、char配列をスタック上または静的に割り当てた場合でも当てはまります。
mallocとfreeは実装に依存します。典型的な実装では、利用可能なメモリを「空きリスト」(利用可能なメモリブロックのリンクリスト)に分割します。多くの実装は、人工的にそれを小さなオブジェクトと大きなオブジェクトに分割します。空きブロックは、メモリブロックの大きさや次のブロックの位置などに関する情報から始まります。
Mallocを実行すると、空きリストからブロックが取得されます。解放すると、ブロックは空きリストに戻されます。ポインタの末尾を上書きすると、空きリストのブロックのヘッダーに書き込みが行われる可能性があります。メモリを解放すると、free()は次のブロックを調べようとし、おそらくバスエラーの原因となるポインタをヒットすることになります。
まあ、それはメモリアロケータの実装とOSに依存します。
たとえば、ウィンドウの下では、プロセスは1ページ以上のRAMを要求できます。次に、OSはこれらのページをプロセスに割り当てます。ただし、これはアプリケーションに割り当てられるメモリではありません。 CRTメモリアロケータは、メモリを連続した「利用可能な」ブロックとしてマークします。 CRTメモリアロケーターは、空きブロックのリストを実行し、使用可能な最小ブロックを見つけます。次に、そのブロックを必要なだけ使用して、「割り当てられた」リストに追加します。実際のメモリ割り当ての先頭に添付されるのはヘッダーです。このヘッダーには、さまざまな情報が含まれます(たとえば、リンクリストを形成するために、次および前の割り当て済みブロックを含めることができます。ほとんどの場合、割り当てのサイズが含まれます)。
その後、Freeはヘッダーを削除し、それを空きメモリリストに追加します。周囲の空きブロックと大きなブロックを形成する場合、これらは一緒に追加されて大きなブロックになります。現在、ページ全体が空いている場合、アロケーターはおそらくページをOSに返します。
単純な問題ではありません。 OSアロケーター部分は完全に制御できません。 Doug LeaのMalloc(DLMalloc)などを読んで、かなり高速なアロケーターがどのように機能するかを理解することをお勧めします。
編集:あなたのクラッシュは、割り当てよりも大きく書き込むことにより、次のメモリヘッダーを上書きしたという事実によって引き起こされます。このように、それが解放されると、それが何を解放しているか、そして次のブロックにどのようにマージするかについて非常に混乱します。これは、常に無料ですぐにクラッシュを引き起こすとは限りません。後でクラッシュする可能性があります。一般的に、メモリの上書きは避けてください!
自分に属していないメモリを使用したため、プログラムがクラッシュします。他の人が使用するかどうかに関係なく、ラッキーな場合はクラッシュします。そうでない場合は、問題が長期間隠されたままになり、後で戻ってきて噛みつきます。
Malloc/free実装に関する限り、本全体がこのトピックに当てられています。基本的に、アロケーターはOSからより大きなメモリチャンクを取得し、それらを管理します。アロケーターが対処しなければならない問題のいくつかは次のとおりです。
実際の動作は異なるコンパイラ/ランタイム間で異なるため、言うのは困難です。デバッグ/リリースビルドでさえ、異なる動作をします。 VS2005のデバッグビルドは、メモリ破損を検出するために割り当ての間にマーカーを挿入するため、クラッシュの代わりにfree()でアサートします。
brk
およびsbrk
を使用してプログラムブレークポインターを移動するだけでは、実際にはallocateメモリではなく、アドレス空間を設定するだけであることを認識することも重要です。たとえば、Linuxでは、そのアドレス範囲にアクセスすると実際の物理ページによってメモリが「バッキング」され、ページフォールトが発生し、最終的にカーネルがページアロケーターを呼び出してバッキングページを取得します。