web-dev-qa-db-ja.com

アセンブリはどのようにパラメーターを渡しますか:値、参照、さまざまなタイプ/配列のポインターによって?

これを確認するために、さまざまなタイプの変数を作成し、それらを値、参照、およびポインタによって関数に渡した、この単純なコードを書きました。

int i = 1;
char c = 'a';
int* p = &i;
float f = 1.1;
TestClass tc; // has 2 private data members: int i = 1 and int j = 2

パラメータの受け渡し方法を確認しているだけなので、関数の本文は空白のままにしました。

passByValue(i, c, p, f, tc); 
passByReference(i, c, p, f, tc); 
passByPointer(&i, &c, &p, &f, &tc);

これが配列でどのように異なるのか、またパラメータがどのようにアクセスされるのかを確認したいと考えました。

int numbers[] = {1, 2, 3};
passArray(numbers); 

アセンブリ:

passByValue(i, c, p, f, tc)

mov EAX, DWORD PTR [EBP - 16]
    mov DL, BYTE PTR [EBP - 17]
    mov ECX, DWORD PTR [EBP - 24]
    movss   XMM0, DWORD PTR [EBP - 28]
    mov ESI, DWORD PTR [EBP - 40]
    mov DWORD PTR [EBP - 48], ESI
    mov ESI, DWORD PTR [EBP - 36]
    mov DWORD PTR [EBP - 44], ESI
    lea ESI, DWORD PTR [EBP - 48]
    mov DWORD PTR [ESP], EAX
    movsx   EAX, DL
    mov DWORD PTR [ESP + 4], EAX
    mov DWORD PTR [ESP + 8], ECX
    movss   DWORD PTR [ESP + 12], XMM0
    mov EAX, DWORD PTR [ESI]
    mov DWORD PTR [ESP + 16], EAX
    mov EAX, DWORD PTR [ESI + 4]
    mov DWORD PTR [ESP + 20], EAX
    call    _Z11passByValueicPif9TestClass


passByReference(i, c, p, f, tc)

    lea EAX, DWORD PTR [EBP - 16]
    lea ECX, DWORD PTR [EBP - 17]
    lea ESI, DWORD PTR [EBP - 24]
    lea EDI, DWORD PTR [EBP - 28]
    lea EBX, DWORD PTR [EBP - 40]
    mov DWORD PTR [ESP], EAX
    mov DWORD PTR [ESP + 4], ECX
    mov DWORD PTR [ESP + 8], ESI
    mov DWORD PTR [ESP + 12], EDI
    mov DWORD PTR [ESP + 16], EBX
    call    _Z15passByReferenceRiRcRPiRfR9TestClass

passByPointer(&i, &c, &p, &f, &tc)

    lea EAX, DWORD PTR [EBP - 16]
    lea ECX, DWORD PTR [EBP - 17]
    lea ESI, DWORD PTR [EBP - 24]
    lea EDI, DWORD PTR [EBP - 28]
    lea EBX, DWORD PTR [EBP - 40]
    mov DWORD PTR [ESP], EAX
    mov DWORD PTR [ESP + 4], ECX
    mov DWORD PTR [ESP + 8], ESI
    mov DWORD PTR [ESP + 12], EDI
    mov DWORD PTR [ESP + 16], EBX
    call    _Z13passByPointerPiPcPS_PfP9TestClass

passArray(numbers)

    mov EAX, .L_ZZ4mainE7numbers
    mov DWORD PTR [EBP - 60], EAX
    mov EAX, .L_ZZ4mainE7numbers+4
    mov DWORD PTR [EBP - 56], EAX
    mov EAX, .L_ZZ4mainE7numbers+8
    mov DWORD PTR [EBP - 52], EAX
    lea EAX, DWORD PTR [EBP - 60]
    mov DWORD PTR [ESP], EAX
    call    _Z9passArrayPi

    // parameter access
    Push    EAX
    mov EAX, DWORD PTR [ESP + 8]
    mov DWORD PTR [ESP], EAX
    pop EAX

それぞれの最後に呼び出しがあるので、パラメーターの受け渡しに関連する正しいアセンブリを見ていると思います!

しかし、私のアセンブリに関する非常に限られた知識のため、ここで何が行われているのかわかりません。私はccallの慣習について学びました。つまり、呼び出し元が保存したレジスターを保持し、パラメーターをスタックにプッシュすることに関係する何かが起こっていると思います。このため、レジスタにロードされ、どこにでも "プッシュ"されることが予想されますが、movsとleasで何が起こっているのかわかりません。また、DWORD PTRが何かわかりません。

私はレジスターeax, ebx, ecx, edx, esi, edi, espebpについてしか学んでいないので、XMM0DLのようなものを見ただけでも混乱します。メモリアドレスを使用するため、参照/ポインタによる受け渡しではleaを表示するのが理にかなっていると思いますが、実際には何が起こっているのかわかりません。値渡しについては、多くの命令があるように思われるため、値をレジスターにコピーする必要があります。配列をパラメーターとして渡し、パラメーターとしてアクセスする方法については、わかりません。

誰かがアセンブリの各ブロックで何が起こっているのかについての一般的な考えを私に説明できたら、私はそれを高く評価します。

14
atkayla

引数を渡すためにCPUレジスタを使用すると、メモリ、つまりスタックを使用するよりも高速です。ただし、CPU(特にx86互換CPU)にはレジスタの数に制限があるため、関数に多くのパラメーターがある場合、CPUレジスタの代わりにスタックが使用されます。あなたの場合、5つの関数引数があるので、コンパイラーはレジスターの代わりに引数のスタックを使用します。

原則として、コンパイラはPush命令を使用して実際のcallが機能する前に引数をスタックにプッシュできますが、多くのコンパイラ(gnu c ++を含む)はmovを使用して引数をスタックにプッシュします。この方法は、関数を呼び出すコードの一部でESPレジスタ(スタックの最上部))を変更しないので便利です。

passByValue(i, c, p, f, tc)の場合、引数の値はスタックに配置されます。メモリの場所からレジスタへ、そしてレジスタからスタックの適切な場所へ、多くのmov命令を見ることができます。この理由は、x86アセンブリは、あるメモリ位置から別のメモリ位置への直接移動を禁止しているためです(例外は、ある配列(または文字列)から別の配列に値を移動するmovsです)。

passByReference(i, c, p, f, tc)の場合、CPUレジスタに引数のaddressesをコピーする5つのlea命令が多数表示され、これらのレジスタの値がスタックに移動されます。

passByPointer(&i, &c, &p, &f, &tc)の場合はpassByValue(i, c, p, f, tc)に似ています。内部的には、アセンブリレベルでは、参照渡しはポインターを使用しますが、上位のC++レベルでは、プログラマーは参照で&および*演算子を明示的に使用する必要はありません。

パラメータがスタックに移動した後、callが発行され、プログラムの実行をサブルーチンに転送する前に、命令ポインタEIPがスタックにプッシュされます。 moves命令の後にスタックで来るEIPのスタックアカウントへのパラメーターのすべてのcall

18
Igor Popov

上記の例では、それらすべてを分析するには多すぎます。代わりに、passByValueについて説明します。これが最も興味深いようです。その後、残りの部分を理解できるはずです。

コードの海で完全に迷子にならないように、逆アセンブリを研究するときに最初に覚えておくべき重要なポイント:

  • あるmemの場所から別のmemの場所にデータを直接コピーする手順はありません。例えば。 mov [ebp - 44], [ebp - 36]正当な命令ではありません。最初にデータを保存し、次にメモリの宛先にコピーするために、中間レジスタが必要です。
  • ブラケット演算子[]movと組み合わせて使用​​すると、計算されたメモリアドレスからデータにアクセスできます。これは、C/C++でポインターを逆参照することに似ています。
  • lea x, [y]が表示された場合、通常yのアドレスを計算し、xに保存することを意味します。これは、C/C++で変数のアドレスを取得することに似ています。
  • コピーする必要があるが、大きすぎてレジスターに収まらないデータとオブジェクトは、断片的にスタックにコピーされます。 IOW、オブジェクト/データを表すすべてのバイトがコピーされるまで、ネイティブマシンのWordを一度にコピーします。通常、最新のプロセッサでは4バイトまたは8バイトのいずれかを意味します。
  • 通常、コンパイラーはinterleave命令を一緒にインターリーブして、プロセッサーのパイプラインをビジーに保ち、ストールを最小限に抑えます。コードの効率は良いですが、逆アセンブルを理解しようとすると悪いです。

上記を念頭に置いて、ここではpassByValue関数の呼び出しを少しわかりやすくするために少し並べ替えました。

.define arg1  esp
.define arg2  esp + 4
.define arg3  esp + 8
.define arg4  esp + 12
.define arg5.1  esp + 16
.define arg5.2  esp + 20


; copy first parameter
mov EAX, [EBP - 16]
mov [arg1], EAX

; copy second parameter
mov DL, [EBP - 17]
movsx   EAX, DL
mov [arg2], EAX

; copy third
mov ECX, [EBP - 24]
mov [arg3], ECX

; copy fourth
movss   XMM0, DWORD PTR [EBP - 28]
movss   DWORD PTR [arg4], XMM0

; intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI

;copy fifth
lea ESI, [EBP - 48]
mov EAX, [ESI]
mov [arg5.1], EAX
mov EAX, [ESI + 4]
mov [arg5.2], EAX
call    passByValue(int, char, int*, float, TestClass)

上記のコードは複雑ではなく、実際に何が起こっているのかを明確にするために命令の混合が取り消されていますが、説明が必要なものもあります。まず、charはsignedで、サイズは1バイトです。ここでの指示:

; copy second parameter
mov DL, [EBP - 17]
movsx   EAX, DL
mov [arg2], EAX

[ebp - 17](スタックのどこか)からバイトを読み取り、edxの下位の最初のバイトに格納します。次に、そのバイトは、符号拡張移動を使用してeaxにコピーされます。 eaxの完全な32ビット値は、最終的にpassByValueがアクセスできるスタックにコピーされます。 レジスタレイアウトを参照 詳細が必要な場合。

4番目の引数:

movss   XMM0, DWORD PTR [EBP - 28]
movss   DWORD PTR [arg4], XMM0

SSE movss命令を使用して、浮動小数点値をスタックからxmm0レジスタにコピーします。簡単に言えば、SSE命令複数のデータに対して同じ操作を同時に実行できるようにしますが、ここではコンパイラが浮動小数点値をスタックにコピーするための中間ストレージとしてそれを使用しています。

最後の引数:

; copy intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI

TestClassに対応します。どうやらこのクラスのサイズは8バイトで、スタックには[ebp - 40]から[ebp - 33]まであります。オブジェクトが単一のレジスターに収まらないため、ここでのクラスは一度に4バイトずつコピーされています。

call passByValueの前のスタックは次のようになります。

lower addr    esp       =>  int:arg1            <--.
              esp + 4       char:arg2              |
              esp + 8       int*:arg3              |    copies passed
              esp + 12      float:arg4             |    to 'passByValue'
              esp + 16      TestClass:arg5.1       |
              esp + 20      TestClass:arg5.2    <--.
              ...
              ...
              ebp - 48      TestClass:arg5.1    <--   intermediate copy of 
              ebp - 44      TestClass:arg5.2    <--   TestClass?
              ebp - 40      original TestClass:arg5.1
              ebp - 36      original TestClass:arg5.2
              ...
              ebp - 28      original arg4     <--.
              ebp - 24      original arg3        |  original (local?) variables
              ebp - 20      original arg2        |  from calling function
              ebp - 16      original arg1     <--.
              ...
higher addr   ebp           prev frame
8
greatwolf

あなたが探しているのは ABI呼び出し規約 です。異なるプラットフォームには異なる規則があります。例えばx86-64上のWindowsには、x86-64上のUnix/Linuxとは異なる規則があります。

http://www.agner.org/optimize/ には、x86/AMD64のさまざまな仕様を詳述した呼び出し規約に関するドキュメントがあります。

ASMで好きなことを行うコードを書くことができますが、他の関数を呼び出してそれらから呼び出す場合は、ABIに従ってパラメーター/戻り値を渡します。

標準のABIを使用せず、代わりに呼び出し側の関数が割り当てるレジスターの値を使用する、内部使用専用のヘルパー関数を作成すると便利です。これは特にです。メインプログラムをASM以外の何かで作成していて、ASMのほんの一部を使用している場合にそうです。次に、asmの部分は、メインプログラムから呼び出されるために、独自の内部ではなく、異なるABIを持つシステムに移植可能であることだけを気にする必要があります。

3
Peter Cordes