これを確認するために、さまざまなタイプの変数を作成し、それらを値、参照、およびポインタによって関数に渡した、この単純なコードを書きました。
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の慣習について学びました。つまり、呼び出し元が保存したレジスターを保持し、パラメーターをスタックにプッシュすることに関係する何かが起こっていると思います。このため、レジスタにロードされ、どこにでも "プッシュ"されることが予想されますが、mov
sとlea
sで何が起こっているのかわかりません。また、DWORD PTR
が何かわかりません。
私はレジスターeax, ebx, ecx, edx, esi, edi, esp
とebp
についてしか学んでいないので、XMM0
やDL
のようなものを見ただけでも混乱します。メモリアドレスを使用するため、参照/ポインタによる受け渡しではlea
を表示するのが理にかなっていると思いますが、実際には何が起こっているのかわかりません。値渡しについては、多くの命令があるように思われるため、値をレジスターにコピーする必要があります。配列をパラメーターとして渡し、パラメーターとしてアクセスする方法については、わかりません。
誰かがアセンブリの各ブロックで何が起こっているのかについての一般的な考えを私に説明できたら、私はそれを高く評価します。
引数を渡すために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
。
上記の例では、それらすべてを分析するには多すぎます。代わりに、passByValue
について説明します。これが最も興味深いようです。その後、残りの部分を理解できるはずです。
コードの海で完全に迷子にならないように、逆アセンブリを研究するときに最初に覚えておくべき重要なポイント:
mov [ebp - 44], [ebp - 36]
は正当な命令ではありません。最初にデータを保存し、次にメモリの宛先にコピーするために、中間レジスタが必要です。[]
をmov
と組み合わせて使用すると、計算されたメモリアドレスからデータにアクセスできます。これは、C/C++でポインターを逆参照することに似ています。lea x, [y]
が表示された場合、通常はyのアドレスを計算し、xに保存することを意味します。これは、C/C++で変数のアドレスを取得することに似ています。上記を念頭に置いて、ここでは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
あなたが探しているのは ABI呼び出し規約 です。異なるプラットフォームには異なる規則があります。例えばx86-64上のWindowsには、x86-64上のUnix/Linuxとは異なる規則があります。
http://www.agner.org/optimize/ には、x86/AMD64のさまざまな仕様を詳述した呼び出し規約に関するドキュメントがあります。
ASMで好きなことを行うコードを書くことができますが、他の関数を呼び出してそれらから呼び出す場合は、ABIに従ってパラメーター/戻り値を渡します。
標準のABIを使用せず、代わりに呼び出し側の関数が割り当てるレジスターの値を使用する、内部使用専用のヘルパー関数を作成すると便利です。これは特にです。メインプログラムをASM以外の何かで作成していて、ASMのほんの一部を使用している場合にそうです。次に、asmの部分は、メインプログラムから呼び出されるために、独自の内部ではなく、異なるABIを持つシステムに移植可能であることだけを気にする必要があります。