CやC++などのプログラミング言語では、多くの場合、静的および動的メモリ割り当てを参照します。私はこの概念を理解していますが、「コンパイル時にすべてのメモリが割り当てられた(予約された)」というフレーズは常に混乱します。
私が理解しているように、コンパイルは高レベルのC/C++コードを機械語に変換し、実行可能ファイルを出力します。コンパイルされたファイルでメモリはどのように「割り当て」られますか? RAMのメモリは、すべての仮想メモリ管理スタッフと共に常に割り当てられているのではありませんか?
定義によるメモリ割り当てはランタイムの概念ではありませんか?
C/C++コードで1KBの静的に割り当てられた変数を作成すると、実行可能ファイルのサイズが同じ量だけ増えますか?
これは、「静的割り当て」という見出しの下でフレーズが使用されるページの1つです。
コンパイル時に割り当てられるメモリとは、コンパイラがプロセスメモリマップ内で特定のものが割り当てられるコンパイル時に解決することを意味します。
たとえば、グローバル配列を考えてみましょう。
int array[100];
コンパイラーはコンパイル時に配列のサイズとint
のサイズを知っているため、コンパイル時に配列のサイズ全体を知っています。また、グローバル変数にはデフォルトで静的な保存期間があります。これはプロセスメモリ空間の静的メモリ領域(.data/.bssセクション)に割り当てられます。その情報が与えられると、コンパイラーはコンパイル中に、その静的メモリー領域のどのアドレスに配列を配置するかを決定します。
もちろん、メモリアドレスは仮想アドレスです。プログラムは、独自のメモリ空間全体(たとえば、0x00000000から0xFFFFFFFF)を持っていると仮定します。これが、コンパイラーが「わかりました、配列はアドレス0x00A33211にあります」などの仮定を行うことができる理由です。実行時に、アドレスはMMUおよびOSによって実アドレス/ハードウェアアドレスに変換されます。
値が初期化された静的ストレージは少し異なります。例えば:
int array[] = { 1 , 2 , 3 , 4 };
最初の例では、コンパイラーは配列の割り当て場所のみを決定し、その情報を実行可能ファイルに保存しました。
[。これらの値で。
コンパイラー(x86ターゲットのGCC4.8.1)によって生成されたアセンブリーの2つの例を次に示します。
C++コード:
int a[4];
int b[] = { 1 , 2 , 3 , 4 };
int main()
{}
出力アセンブリ:
a:
.zero 16
b:
.long 1
.long 2
.long 3
.long 4
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
popq %rbp
ret
ご覧のとおり、値はアセンブリに直接注入されます。配列a
では、コンパイラは静的に保存されたものをデフォルトでゼロに初期化する必要があると規格が規定しているため、16バイトのゼロ初期化を生成します。
8.5.9(イニシャライザー)[注]:
静的ストレージ期間のすべてのオブジェクトは、他の初期化が行われる前に、プログラムの起動時にゼロで初期化されます。場合によっては、後で追加の初期化が行われます。
コンパイラーが実際にC++コードで何をするかを見るために、コードを逆アセンブリすることを常にお勧めします。これは、ストレージクラス/期間(この質問のような)から高度なコンパイラ最適化に適用されます。コンパイラにアセンブリを生成するように指示することもできますが、これをインターネット上で友好的な方法で行うためのすばらしいツールがあります。私のお気に入りは GCC Explorer です。
コンパイル時に割り当てられたメモリは、実行時にそれ以上割り当てられないことを意味します。malloc、new、またはその他の動的割り当てメソッドの呼び出しはありません。すべてのメモリを常に必要としない場合でも、メモリ使用量は固定されます。
定義によるメモリ割り当てはランタイムの概念ではありませんか?
メモリは使用中ではなく、実行前ではなく、実行の直前に割り当てがシステムによって処理されます。
C/C++コードで1KBの静的に割り当てられた変数を作成すると、実行可能ファイルのサイズが同じ量だけ増えますか?
静的を宣言するだけでは、実行可能ファイルのサイズが数バイトを超えることはありません。ゼロ以外の初期値で宣言すると、(その初期値を保持するために)なります。むしろ、リンカは実行の直前にシステムのローダーが作成するメモリ要件にこの1KBの量を単に追加します。
コンパイル時に割り当てられるメモリとは、プログラムをロードすると、メモリの一部がすぐに割り当てられ、この割り当てのサイズと(相対)位置がコンパイル時に決定されることを意味します。
char a[32];
char b;
char c;
これらの3つの変数は「コンパイル時に割り当てられます」。つまり、コンパイラーはコンパイル時にそれらのサイズ(固定)を計算します。変数a
はメモリ内のオフセットになります。たとえば、アドレス0を指している場合、b
はアドレス33を指し、c
は34を指します(アライメントの最適化がない場合)。そのため、1Kbの静的データを割り当ててもコードのサイズは増加しません。これは内部のオフセットを変更するだけです。 実際のスペースはロード時に割り当てられます。
カーネルはそれを追跡し、内部データ構造(各プロセス、ページなどに割り当てられるメモリの量)を更新する必要があるため、実メモリ割り当ては常に実行時に行われます。違いは、使用する各データのサイズをコンパイラがすでに認識しており、プログラムが実行されるとすぐに割り当てられることです。
また、相対アドレスについて話していることも忘れないでください。変数が配置される実際のアドレスは異なります。ロード時に、カーネルはプロセス用にメモリを予約します。たとえば、アドレスx
で、実行可能ファイルに含まれるすべてのハードコーディングされたアドレスはx
バイトずつ増加するため、例の変数a
はアドレスx
bアドレスx+33
など。
Nバイトを占有する変数をスタックに追加しても、ビンのサイズは(必ずしも)Nバイト増加しません。実際、ほとんどの場合、数バイトを追加します。
1000文字をコードに追加する方法の例から始めましょうwillビンのサイズを直線的に増加させます。
1kが1000文字の文字列の場合、次のように宣言されます
const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end
その後、vim your_compiled_bin
を実行すると、実際にはどこかのビンにその文字列が表示されます。その場合、はい:実行可能ファイルは文字列全体を含むため、1 k大きくなります。
ただし、int
s、char
s、またはlong
sの配列をスタックに割り当て、ループで割り当てる場合、これらの行に沿って何か
int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);
その後、いいえ:ビンを増やすことはありません... by 1000*sizeof(int)
コンパイル時の割り当てとは、コメントに基づいて理解できるようになったことを意味します。コンパイル済みビンには、実行時に関数/ブロックが必要とするメモリ量をシステムが知るために必要な情報が含まれます。 、アプリケーションに必要なスタックサイズに関する情報とともに。それはあなたのビンを実行するときにシステムが割り当てるものであり、あなたのプログラムはプロセスになります(あなたのビンの実行は...私が言っていることを得るプロセスです)。
もちろん、ここでは全体像を描いていません。ビンには、ビンが実際に必要とするスタックの大きさに関する情報が含まれています。 (特に)この情報に基づいて、システムはスタックと呼ばれるメモリのチャンクを予約し、プログラムが一種の自由な支配権を獲得します。プロセス(実行されているビンの結果)が開始されるとき、スタックメモリはまだシステムによって割り当てられます。その後、プロセスはスタックメモリを管理します。関数またはループ(任意のタイプのブロック)が呼び出される/実行されると、そのブロックにローカルな変数がスタックにプッシュされ、削除されます(スタックメモリは"freed" so to他の機能/ブロックが使用する)。したがって、int some_array[100]
を宣言すると、数バイトの追加情報のみがビンに追加されます。これは、機能Xが100*sizeof(int)
+追加のブックキーピングスペースを必要とすることをシステムに伝えます。
多くのプラットフォームでは、各モジュール内のすべてのグローバルまたは静的割り当ては、コンパイラによって3つ以下の統合割り当て(未初期化データ(「bss」と呼ばれることが多い)、初期化された書き込み可能データ(「データ」と呼ばれる) )、および定数データ(「const」)の1つ)、およびプログラム内の各タイプのすべてのグローバルまたは静的割り当ては、リンカによって各タイプの1つのグローバルに統合されます。たとえば、int
が4バイトであると仮定すると、モジュールの唯一の静的割り当てとして次のものがあります。
int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;
bssには208バイト、「data」には16バイト、「const」には28バイトが必要であることをリンカに伝えます。さらに、変数への参照はエリアセレクタとオフセットに置き換えられるため、a、b、c、d、およびeは、bss + 0、const + 0、bss + 4、const + 24、dataに置き換えられます。それぞれ+0、またはbss + 204。
プログラムがリンクされると、すべてのモジュールのすべてのbssエリアが連結されます。同様に、データおよびconstエリア。各モジュールについて、すべてのbss相対変数のアドレスは、先行するすべてのモジュールのbss領域のサイズだけ増加します(これも同様にdataおよびconstを使用)。したがって、リンカが完了すると、すべてのプログラムに1つのbss割り当て、1つのデータ割り当て、および1つのconst割り当てが割り当てられます。
プログラムがロードされると、通常、プラットフォームに応じて次の4つのいずれかが発生します。
実行可能ファイルは、各種類のデータに必要なバイト数と、初期コンテンツが見つかる初期化されたデータ領域に必要なバイト数を示します。また、bss-、data-、またはconst-相対アドレスを使用するすべての命令のリストも含まれます。オペレーティングシステムまたはローダーは、各領域に適切な量のスペースを割り当て、その領域の開始アドレスを必要な各命令に追加します。
オペレーティングシステムは、3種類のデータすべてを保持するためにメモリチャンクを割り当て、アプリケーションにそのメモリチャンクへのポインタを与えます。静的データまたはグローバルデータを使用するコードは、そのポインターに関連してそれを逆参照します(多くの場合、ポインターはアプリケーションの有効期間中はレジスターに格納されます)。
オペレーティングシステムは、バイナリコードを保持しているものを除き、最初はアプリケーションにメモリを割り当てませんが、アプリケーションが最初に行うことは、オペレーティングシステムから適切な割り当てを要求することであり、それは永遠にレジスタに保持されます。
オペレーティングシステムは最初はアプリケーションにスペースを割り当てませんが、アプリケーションは起動時に適切な割り当てを要求します(上記を参照)。アプリケーションには、メモリが割り当てられた場所を反映するために更新する必要があるアドレスの命令のリストが含まれます(最初のスタイルと同様)が、OSローダーによってアプリケーションにパッチを適用するのではなく、アプリケーション自体にパッチを適用するのに十分なコードが含まれます。
4つのアプローチにはすべて長所と短所があります。ただし、どの場合でも、コンパイラーは任意の数の静的変数を固定された少数のメモリー要求に統合し、リンカーはそれらすべてを少数の統合された割り当てに統合します。アプリケーションはオペレーティングシステムまたはローダーからメモリのチャンクを受信する必要がありますが、その大きなチャンクから個々のピースをそれを必要とするすべての個々の変数に割り当てるのは、コンパイラとリンカーです。
あなたの質問の核心はこうです:「コンパイルされたファイルでメモリはどのように「割り当てられますか?」メモリは常にRAMにすべての仮想メモリ管理のものと共に割り当てられますか?定義によりメモリ割り当てされませんか?ランタイムの概念ですか?」
問題は、メモリ割り当てに関係する2つの異なる概念があることだと思います。基本的に、メモリ割り当ては「このデータ項目はこの特定のメモリチャンクに格納される」というプロセスです。現代のコンピューターシステムでは、これには2段階のプロセスが含まれます。
後者のプロセスは純粋に実行時ですが、データが既知のサイズを持ち、それらの固定数が必要な場合、前者はコンパイル時に実行できます。基本的な仕組みは次のとおりです。
コンパイラは、次のような行を含むソースファイルを確認します。
int c;
これは、変数 'c'のメモリを予約するように指示するアセンブラーの出力を生成します。これは次のようになります。
global _c
section .bss
_c: resb 4
アセンブラが実行されると、メモリ「セグメント」(または「セクション」)の先頭から各アイテムのオフセットを追跡するカウンタが保持されます。これは、ファイル全体のすべてを含む非常に大きな「構造体」の一部に似ています。この時点では、実際のメモリは割り当てられておらず、どこにでも配置できます。 _c
には特定のオフセット(セグメントの先頭から510バイトなど)があり、そのカウンターが4増加することがテーブルで示されているため、そのような変数は(たとえば)514バイトになります。 _c
のアドレスを必要とするコードについては、510を出力ファイルに配置し、出力に_c
を含むセグメントのアドレスが必要であるというメモを追加します。
リンカは、アセンブラのすべての出力ファイルを取得して検査します。セグメントが重複しないように各セグメントのアドレスを決定し、命令が正しいデータ項目を参照するように必要なオフセットを追加します。 c
によって占有されているような初期化されていないメモリの場合(アセンブラは、コンパイラが初期化されていないメモリ用に予約された名前である '.bss'セグメントにメモリを置くことでメモリが初期化されないことを通知されました)、オペレーティングシステムに予約する必要がある量を伝えるヘッダーフィールドが出力に含まれます。再配置される可能性があります(通常はそうなります)が、通常は特定のメモリアドレスでより効率的にロードされるように設計されており、OSはこのアドレスでロードを試みます。この時点で、c
によって使用される仮想アドレスが何であるかについて、かなり良いアイデアが得られました。
物理アドレスは、プログラムが実行されるまで実際には決定されません。ただし、プログラマの観点から見ると、物理アドレスは実際には無関係です。OSは通常、だれにもわからないので、それが何であるかを知ることすらありません。 OSの主な目的は、とにかくこれを抽象化することです。
実行可能ファイルは、静的変数に割り当てるスペースを記述します。この割り当ては、実行可能ファイルを実行するときにシステムによって行われます。したがって、1kBの静的変数は、1kBで実行可能ファイルのサイズを増加させません。
static char[1024];
もちろん、初期化子を指定しない限り:
static char[1024] = { 1, 2, 3, 4, ... };
そのため、「マシン言語」(つまり、CPU命令)に加えて、実行可能ファイルには必要なメモリレイアウトの説明が含まれています。
メモリはさまざまな方法で割り当てることができます。
ここでの質問は、「コンパイル時に割り当てられるメモリ」とは何ですか。間違いなく、それはバイナリセグメント割り当てまたはスタック割り当てのいずれか、または場合によってはヒープ割り当てを指すことになっていますが、その場合割り当ては目に見えないコンストラクター呼び出しによってプログラマの目から隠されています。またはおそらく、メモリはヒープに割り当てられていないが、スタックまたはセグメントの割り当てについては知らないと言っただけだと言った人(または、その種の詳細については知りたくなかった)。
しかし、ほとんどの場合、人は割り当てられているメモリの量はコンパイル時にわかっていると言いたいだけです。
バイナリサイズは、メモリがアプリのコードまたはデータセグメントで予約されている場合にのみ変更されます。
あなたが正しいです。メモリは、ロード時に、つまり実行可能ファイルが(仮想)メモリに持ち込まれるときに、実際に割り当てられます(ページングされます)。その瞬間にメモリを初期化することもできます。コンパイラはメモリマップを作成するだけです。 [ところで、スタックおよびヒープスペースもロード時に割り当てられます!]
いくつかの図を使用してこれらの概念を説明します。
確かに、コンパイル時にメモリを割り当てることはできません。しかし、それではコンパイル時に実際に何が起こるか。
ここに説明があります。たとえば、プログラムに4つの変数x、y、z、kがあるとします。現在、コンパイル時にメモリマップが作成され、これらの変数の相互の位置が確認されます。この図はそれをより良く説明します。
今、メモリで実行されているプログラムがないことを想像してください。これは、大きな空の長方形で示しています。
次に、このプログラムの最初のインスタンスが実行されます。次のように視覚化できます。これは、実際にメモリが割り当てられる時間です。
このプログラムの2番目のインスタンスが実行されている場合、メモリは次のようになります。
そして3番目..
などなど。
この視覚化がこの概念をうまく説明することを願っています。
アセンブリプログラミングを学ぶと、データ、スタック、コードなどのセグメントを作成する必要があることがわかります。データセグメントは、文字列と数値が存在する場所です。コードセグメントは、コードが存在する場所です。これらのセグメントは、実行可能プログラムに組み込まれています。もちろん、スタックサイズも重要です... スタックオーバーフロー!
したがって、データセグメントが500バイトの場合、プログラムには500バイトの領域があります。データセグメントを1500バイトに変更すると、プログラムのサイズは1000バイト大きくなります。データは実際のプログラムに組み込まれます。
これは、高レベルの言語をコンパイルするときに行われます。実際のデータ領域は、実行可能プログラムにコンパイルされるときに割り当てられ、プログラムのサイズが増加します。プログラムは、オンザフライでメモリを要求することもできます。これは動的メモリです。 RAMからメモリを要求すると、CPUが使用するメモリを提供し、手放すことができ、ガベージコレクタがメモリを解放してCPUに戻します。必要に応じて、優れたメモリマネージャーによってハードディスクにスワップすることもできます。これらの機能は、高水準言語が提供するものです。
少し後退する必要があると思います。コンパイル時に割り当てられるメモリ....それはどういう意味ですか?まだ設計されていないコンピューターのために、まだ製造されていないチップ上のメモリーが何らかの形で予約されているということですか?いいえ、タイムトラベル、宇宙を操作できるコンパイラはありません。
そのため、コンパイラは実行時に何らかの方法でそのメモリを割り当てるための命令を生成することを意味する必要があります。しかし、正しい角度から見た場合、コンパイラーはすべての命令を生成するため、違いは何でしょうか。違いは、コンパイラが決定し、実行時にコードがその決定を変更または変更できないことです。コンパイル時に、実行時に50バイトが必要であると判断した場合、60を割り当てることを決定することはできません-その決定は既に行われています。