web-dev-qa-db-ja.com

x86アセンブリのレジスタで使用されるプッシュ/ポップ命令の機能は何ですか?

アセンブラーについて読むとき、私はしばしばPushプロセッサーの特定のレジスターpopそれを以前の状態に復元するために再び書くという人々に出くわします。

  • どうすればレジスタをプッシュできますか?どこにプッシュされますか?なぜこれが必要ですか?
  • これは単一のプロセッサ命令に要約されますか、それともより複雑ですか?
72
Ars emble

pushing値(必ずしもレジスターに格納されるとは限らない)は、スタックに書き込むことを意味します。

poppingは、スタックの一番上にあるものを復元することを意味しますintoレジスタ。基本的な手順は次のとおりです。

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
118
Linus Kleen

レジスタをプッシュする方法は次のとおりです。 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

Pushはx86の単一の命令で、内部で2つのことを行います。

  1. プッシュされた値をESPレジスタの現在のアドレスに保存します。
  2. プッシュされた値のサイズまでESPレジスタをデクリメントします。
37
Madhur Ahuja

どこにプッシュされますか?

esp - 4。より正確に:

  • espは4で減算されます
  • 値はespにプッシュされます

popはこれを逆にします。

System V ABIは、プログラムの実行開始時にrspが適切なスタック位置を指すようにLinuxに指示します。 プログラム起動時のデフォルトのレジスタ状態は何ですか(asm、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.
    */

上記 実行可能なアサーションを含むGitHubで

なぜこれが必要なのですか?

これらの命令は、movadd、およびsubを介して簡単に実装できることは事実です。

それらが存在する理由は、これらの命令の組み合わせが非常に頻繁であるため、インテルがそれらを提供することを決定したためです。

これらの組み合わせが頻繁に行われる理由は、レジスタの値を一時的にメモリに保存および復元して、上書きされないようにすることです。

問題を理解するには、いくつかのCコードを手作業でコンパイルしてみてください。

主な困難は、各変数を保存する場所を決定することです。

理想的には、すべての変数がレジスタに収まり、これがアクセスするのに最も速いメモリです(現在はRAMの約 100倍 )。

しかしもちろん、特にネストされた関数の引数のために、レジスタよりも多くの変数を簡単に持つことができるため、唯一の解決策はメモリに書き込むことです。

任意のメモリアドレスに書き込むことができますが、関数呼び出しと戻り値のローカル変数と引数はNiceスタックパターンに収まるため、 メモリの断片化 を防ぐことができます。これが最善の対処方法です。それを、ヒープアロケータを記述することの狂気と比較してください。

次に、NPが完全であり、コンパイラーを記述する上で最も難しい部分の1つであるため、コンパイラーにレジスター割り当てを最適化させます。この問題は レジスタ割り当て と呼ばれ、 グラフの色付け と同型です。

コンパイラーのアロケーターが単にレジスターではなくメモリーに物事を保存することを強制されるとき、それはspillとして知られています。

これは単一のプロセッサ命令に要約されますか、それともより複雑ですか?

確かにわかっているのは、IntelがPushおよびpop命令を文書化することだけです。したがって、それらはその意味での1つの命令です。

内部的には、複数のマイクロコードに拡張でき、1つはespを変更し、もう1つはメモリIOを実行し、複数のサイクルを実行します。

しかし、単一のPushは、より具体的であるため、他の命令の同等の組み合わせよりも高速である可能性もあります。

これはほとんど文書化されていません:

レジスタのプッシュとポップは、これと同等の裏側です。

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構文であることに注意してください。

ペアとして使用すると、スタックにレジスタを保存して後で復元できます。他の用途もあります。

16
gowrath

ほとんどすべてのCPUがスタックを使用します。プログラムスタックは LIFO ハードウェアサポート付きの管理手法です。

スタックは、通常、CPUメモリヒープの最上部に割り当てられたプログラム(RAM)メモリの量であり、反対方向に成長します(プッシュ命令ではスタックポインタが減少します)。スタックへの挿入の標準用語はPushであり、スタックからの削除の標準用語はPOP

スタックはスタックポインターとも呼ばれるスタックを意図したCPUレジスタを介して管理されるため、CPUがPOPまたはPushを実行するとスタックポインターは、レジスタまたは定数をスタックメモリにロード/保存し、スタックポインターは、スタックに(から)プッシュまたはポップされたワード数に従って自動的に減少または増加します。

スタックに保存できるアセンブラ命令を介して:

  1. CPUレジスタと定数。
  2. 関数またはプロシージャのアドレスを返します
  3. 関数/手順の入力/出力変数
  4. 関数/プロシージャのローカル変数。
11
GJ.