アセンブラについて読んでいると、プロセッサの特定のレジスタをプッシュし、後で再度ポップして以前の状態に戻すと書いている人によく出くわします。
- どうすればレジスターをプッシュできますか? それはどこに押し付けられていますか?なぜこれが必要なのですか?
- これは単一のプロセッサ命令に要約されますか、それともより複雑ですか?
アセンブラについて読んでいると、プロセッサの特定のレジスタをプッシュし、後で再度ポップして以前の状態に戻すと書いている人によく出くわします。
値をプッシュする (必ずしもレジスタに格納されているとは限りません) とは、スタックに書き込むことを意味します。
ポッピングとは、スタックの一番上にあるものをレジスタに復元することを意味します。これらは基本的な指示です:
push 0xdeadbeef ; push a value to the stack
pop eax ; eax is now 0xdeadbeef
; swap contents of registers
push eax
mov eax, ebx
pop ebx
レジスタをプッシュする方法は次のとおりです。x86について話していると思います。
push ebx
push eax
スタックにプッシュされます。ESP
x86 システムでは、スタックが下方に成長するにつれて、レジスターの値はプッシュされた値のサイズまで減分されます。
値を保持するために必要です。一般的な使い方は
push eax ; preserve the value of eax
call some_method ; some method is called which will put return value in eax
mov edx, eax ; move the return value to edx
pop eax ; restore original eax
Apush
は x86 の単一の命令であり、内部で 2 つのことを行います。
ESP
プッシュされた値のサイズだけレジスタをデクリメントします。ESP
レジスタの現在のアドレスに格納します。それはどこに押し付けられていますか?
esp - 4
. より正確に:
esp
4減算されますesp
pop
これを反転します。
System V ABI はrsp
、プログラムの実行開始時に適切なスタックの場所を示すように Linux に指示します。これは、通常使用する必要があるものです。
どうすればレジスターをプッシュできますか?
最小限の GNU GAS の例:
.data
/* .long takes 4 bytes each. */
val1:
/* Store bytes 0x 01 00 00 00 here. */
.long 1
val2:
/* 0x 02 00 00 00 */
.long 2
.text
/* Make esp point to the address of val2.
* Unusual, but totally possible. */
mov $val2, %esp
/* eax = 3 */
mov $3, %ea
push %eax
/*
Outcome:
- esp == val1
- val1 == 3
esp was changed to point to val1,
and then val1 was modified.
*/
pop %ebx
/*
Outcome:
- esp == &val2
- ebx == 3
Inverses push: ebx gets the value of val1 (first)
and then esp is increased back to point to val2.
*/
なぜこれが必要なのですか?
mov
これらの命令は、 、 、add
およびを介して簡単に実装できることは事実ですsub
。
それらが存在する理由は、これらの命令の組み合わせが非常に頻繁に発生するため、Intel がそれらを提供することにしたからです。
これらの組み合わせが非常に頻繁にある理由は、レジスタの値を一時的にメモリに保存して復元し、上書きされないようにするためです。
この問題を理解するには、いくつかの C コードを手動でコンパイルしてみてください。
主な困難は、各変数をどこに格納するかを決定することです。
理想的には、すべての変数がレジスタに収まるのが理想的です。これはアクセスが最も高速なメモリです (現在、RAMよりも約100 倍高速です)。
しかしもちろん、特にネストされた関数の引数の場合、レジスタよりも多くの変数を簡単に使用できるため、唯一の解決策はメモリに書き込むことです。
任意のメモリ アドレスに書き込むことができますが、関数呼び出しと戻り値のローカル変数と引数は適切なスタック パターンに適合し、メモリの断片化を防ぐため、これが対処する最善の方法です。それを、ヒープ アロケータを作成することの狂気と比較してください。
次に、コンパイラーにレジスター割り当てを最適化させます。これは NP 完全であり、コンパイラーを作成する上で最も難しい部分の 1 つであるためです。この問題はレジスタ割り当てと呼ばれ、グラフの色付けと同型です。
コンパイラのアロケータがレジスタだけでなくメモリに格納することを強制された場合、これはスピルと呼ばれます。
これは単一のプロセッサ命令に要約されますか、それともより複雑ですか?
私たちが確かに知っているのは、Intel が apush
とpop
命令を文書化しているということだけです。したがって、それらはその意味で 1 つの命令です。
内部的には、複数のマイクロコードに拡張でき、1 つは変更用esp
、もう 1 つはメモリ IO を実行し、複数のサイクルが必要です。
しかし、より具体的であるため、単一push
の命令が他の命令の同等の組み合わせよりも高速である可能性もあります。
これはほとんど文書化されていません:
push
おり、pop
単一のマイクロ操作を行うことを述べています。 レジスタのプッシュとポップは、これと同等の舞台裏で行われます。
push reg <= same as => sub $8,%rsp # subtract 8 from rsp
mov reg,(%rsp) # store, using rsp as the address
pop reg <= same as=> mov (%rsp),reg # load, using rsp as the address
add $8,%rsp # add 8 to the rsp
これは x86-64 At&t 構文であることに注意してください。
ペアとして使用すると、レジスタをスタックに保存し、後で復元できます。他にも使い道があります。
ほとんどすべての CPU がスタックを使用します。プログラム スタックは、ハードウェアでサポートされた管理を使用したLIFO手法です。
スタックは、通常、CPU メモリ ヒープの最上部に割り当てられるプログラム (RAM) メモリの量であり、逆方向に増加します (PUSH 命令ではスタック ポインタが減少します)。スタックへの挿入の標準的な用語はPUSHで、スタックからの削除の標準的な用語はPOPです。
スタックは、スタック ポインターとも呼ばれるスタックを意図した CPU レジスターを介して管理されるため、CPU がPOPまたはPUSHを実行すると、スタック ポインターはレジスターまたは定数をスタック メモリにロード/ストアし、スタック ポインターはプッシュされたワード数に応じて自動的に減少または増加します。またはスタックに(から)ポップしました。
アセンブラ命令を介して、スタックに保存できます。