コンピューターが変数を保存するとき、プログラムが変数の値を取得する必要があるとき、コンピューターはどのようにしてその変数の値をメモリ内で探すべきかを知るのでしょうか?
コンピューターが変数を保存するとき、プログラムが変数の値を取得する必要があるとき、コンピューターはどのようにしてその変数の値をメモリ内で探すべきかを知るのでしょうか?
回答:
コンパイラ構築の素晴らしい世界をご覧になることをお勧めします!答えは、少し複雑なプロセスだということです。
あなたに直観を与えようとするために、変数名はプログラマのためだけに存在することを覚えておいてください。コンピューターは最終的にすべてをアドレスに変換します。
ローカル変数は(一般に)スタックに保存されます。つまり、関数呼び出しを表すデータ構造の一部です。関数が(おそらく)使用する変数の完全なリストは、その関数を調べることで決定できるため、コンパイラーは、この関数に必要な変数の数と各変数が使用するスペースの量を確認できます。
スタックポインターと呼ばれるちょっとした魔法があります。これは、現在のスタックが始まるアドレスを常に保存するレジスタです。
各変数には、「スタックオフセット」が与えられます。これは、スタックのどこに格納されるかです。次に、プログラムが変数にアクセスする必要がある場合x
、コンパイラはに置き換えx
られSTACK_POINTER + x_offset
、メモリに格納されている実際の物理的な場所を取得します。
これが、CまたはC ++ を使用するとき、malloc
またはnew
CまたはC ++でポインタを戻す理由です。ヒープに割り当てられた値がメモリのどこにあるのかを正確に判断することはできません。そのため、ポインタを保持する必要があります。そのポインターはスタック上にありますが、ヒープを指します。
コンピューターが変数を保存するとき、プログラムが変数の値を取得する必要があるとき、コンピューターはどのようにしてその変数の値をメモリ内で探すべきかを知るのでしょうか?
プログラムはそれを伝えます。コンピューターには本来「変数」という概念がありません。これは完全に高レベルの言語です!
Cプログラムを次に示します。
int main(void)
{
int a = 1;
return a + 3;
}
そして、ここにコンパイルするアセンブリコードがあります:(で始まるコメント;
)
main:
; {
pushq %rbp
movq %rsp, %rbp
; int a = 1
movl $1, -4(%rbp)
; return a + 3
movl -4(%rbp), %eax
addl $3, %eax
; }
popq %rbp
ret
「int a = 1;」の場合 CPUは「アドレスに値1を格納する(レジスタrbpの値から4を引いた値)」という命令を確認します。プログラムが値1を保存するので、値1を保存する場所を知っています。
同様に、次の命令は「アドレスの値(レジスタrbpの値から4を引いた値)をレジスタeaxにロードする」というものです。コンピューターは変数のようなことを知る必要はありません。
%rsp
が、CPUのスタックポインターです。%rbp
現在の関数が使用するスタックのビットを参照するレジスタです。2つのレジスタを使用すると、デバッグが簡単になります。
正確な方法は、具体的に何を話しているか、どの程度深くしたいかによって異なります。たとえば、ファイルをハードドライブに保存することは、メモリに何かを保存することや、データベースに何かを保存することとは異なります。概念は似ていますが。また、プログラミングレベルでそれを行う方法は、コンピューターがI / Oレベルで行う方法とは異なる説明です。
ほとんどのシステムでは、何らかの種類のディレクトリ/インデックス/レジストリメカニズムを使用して、コンピューターがデータを検索してアクセスできるようにします。このインデックス/ディレクトリには、1つ以上のキーと、データが実際に配置されているアドレス(ハードドライブ、RAM、データベースなど)が含まれます。
コンピュータープログラムの例
コンピュータプログラムは、さまざまな方法でメモリにアクセスできます。通常、オペレーティングシステムはプログラムにアドレススペースを与え、プログラムはそのアドレススペースで必要な処理を実行できます。メモリ空間内の任意のアドレスに直接書き込むことができ、その方法を追跡できます。これは、プログラミング言語やオペレーティングシステムによって、またはプログラマの好みの技術によっても異なる場合があります。
他の回答のいくつかで述べたように、使用される正確なコーディングまたはプログラミングは異なりますが、通常は背後でスタックのようなものを使用します。現在のスタックが開始するメモリ位置を保存するレジスタがあり、そのスタックのどこに関数または変数があるかを知る方法があります。
多くの高レベルのプログラミング言語では、それはすべてあなたのために面倒を見ます。あなたがしなければならないのは、変数を宣言し、その変数に何かを保存することだけです。そうすれば、必要なスタックと配列がバックグラウンドで作成されます。
しかし、プログラマーはいつでも割り当てられたスペース内の任意のアドレスに直接書き込むことを選択できるので、多目的なプログラミングがいかに優れているかを考えると、実際には1つの答えはありません(それを可能にするプログラミング言語を使用していると仮定)。次に、その場所を配列に格納するか、プログラムにハードコードするだけで済みます(つまり、変数 "alpha"は常にスタックの先頭に格納されるか、割り当てられたメモリの最初の32ビットに格納されます)。
概要
そのため、基本的には、データの保存場所をコンピューターに伝えるメカニズムが背後で必要になります。最も一般的な方法の1つは、キーとメモリアドレスを含む何らかのインデックス/ディレクトリです。これはあらゆる種類の方法で実装され、通常はユーザーからカプセル化されます(プログラマからカプセル化されることもあります)。
テンプレートとフォーマットのおかげです。
プログラム/機能/コンピューターは実際には何がどこにあるかを知りません。何かが特定の場所にあることを期待しています。例を使用しましょう。
class simpleClass{
public:
int varA=58;
int varB=73;
simpleClass* nextObject=NULL;
};
新しいクラス「simpleClass」には、3つの重要な変数が含まれています。必要なときにデータを格納できる2つの整数と、別の「simpleClassオブジェクト」へのポインターです。簡単にするために、32ビットマシン上にいると仮定しましょう。「gcc」または別の「C」コンパイラは、データを割り当てるために使用するテンプレートを作成します。
単純型
まず、「int」などの単純なタイプのキーワードを使用すると、コンパイラによって実行可能ファイルの「.data」または「.bss」セクションにメモが作成されるため、オペレーティングシステムによって実行されると、データはプログラムで利用できます。'int'キーワードは4バイト(32ビット)を割り当て、 'long int'は8バイト(64ビット)を割り当てます。
セルごとに、変数がメモリにロードされるはずの命令の直後に変数が来る場合があるため、擬似アセンブリでは次のようになります。
...
clear register EAX
clear register EBX
load the immediate (next) value into EAX
5
copy the value in register EAX to register EBX
...
これは、EAXおよびEBXで値「5」で終了します。
プログラムの実行中、「5」以外のすべての命令が実行されます。即時ロードがそれを参照し、CPUがそれをスキップするためです。
この方法の欠点は、コードの中央に配列/バッファ/文字列を保持することは非現実的であるため、定数に対して実際にしか実用的ではないということです。したがって、一般的に、ほとんどの変数はプログラムヘッダーに保持されます。
これらの動的変数の1つにアクセスする必要がある場合、即値をポインターであるかのように扱うことができます。
...
clear register EAX
clear register EBX
load the immediate value into EAX
0x0AF2CE66 (Let's say this is the address of a cell containing '5')
load the value pointed to by EAX into EBX
...
これは、レジスタEAXの値「0x0AF2CE66」およびレジスタEBXの値「5」で終了します。レジスタに値を一緒に追加することもできるため、このメソッドを使用して配列または文字列の要素を見つけることができます。
もう1つの重要な点は、同様の方法でアドレスを使用するときに値を保存できるため、後でそれらのセルの値を参照できることです。
複合型
このクラスの2つのオブジェクトを作成する場合:
simpleClass newObjA;
simpleClass newObjB;
次に、最初のオブジェクトで使用可能なフィールドに2番目のオブジェクトへのポインターを割り当てることができます。
newObjA.nextObject=&newObjB;
これで、プログラムは最初のオブジェクトのポインタフィールド内で2番目のオブジェクトのアドレスを見つけることができるようになります。メモリでは、これは次のようになります。
newObjA: 58
73
&newObjB
...
newObjB: 58
73
NULL
ここで注意すべき非常に重要な事実は、「newObjA」と「newObjB」はコンパイル時に名前を持たないということです。これらは、何らかのデータがあると予想される場所にすぎません。したがって、&newObjAに2つのセルを追加すると、「nextObject」として機能するセルが見つかります。したがって、「newObjA」のアドレスと「nextObject」セルの相対位置がわかっている場合、「newObjB」のアドレスを知ることができます。
...
load the immediate value into EAX
&newObjA
add the immediate value to EAX
2
load the value in EAX into EBX
これは、「EAX」の「2 +&newObjA」および「EBX」の「&newObjB」で終わります。
テンプレート/フォーマット
コンパイラーがクラス定義をコンパイルするとき、フォーマットを作成する方法、フォーマットに書き込む方法、およびフォーマットから読み取る方法を実際にコンパイルしています。
上記の例は、2つの「int」変数を持つ単一リンクリストのテンプレートです。これらの種類の構成は、バイナリおよびnツリーと共に、動的メモリ割り当てにとって非常に重要です。n-aryツリーの実用的なアプリケーションは、ファイル、ディレクトリ、またはドライバ/オペレーティングシステムによって認識される他のインスタンスを指すディレクトリで構成されるファイルシステムです。
すべての要素にアクセスするために、構造体の上下に働きかけるインチワームについて考えてください。このように、プログラム/機能/コンピューターは何も知らず、単にデータを移動する命令を実行します。