私にとっては、ファンキーなMOVのようです。その目的は何ですか?またいつ使用しますか?
他の人が指摘したように、LEA(load effective address)は特定の計算をするための「トリック」としてよく使われますが、それはその主な目的ではありません。 x86命令セットは、PascalやCなどの高水準言語をサポートするように設計されています。ここでは、配列、特に整数の配列または小さな構造体が一般的です。たとえば、(x、y)座標を表す構造体を考えます。
struct Point
{
int xcoord;
int ycoord;
};
では、次のような文を想像してください。
int y = points[i].ycoord;
points[]
はPoint
の配列です。配列の基底が既にEBX
にあり、変数i
がEAX
にあり、xcoord
とycoord
がそれぞれ32ビットであると仮定すると(したがってycoord
は構造体のオフセット4バイトにあります)、
MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address"
これはy
にEDX
を上陸させます。 8の倍率は、各Point
のサイズが8バイトであるためです。それでは、 "address of"演算子&と同じ式を使ってみましょう。
int *p = &points[i].ycoord;
この場合、ycoord
の値は必要ありませんが、そのアドレスは必要です。それがLEA
(実効アドレスのロード)が入ってくるところです。MOV
の代わりに、コンパイラは次のものを生成することができます。
LEA ESI, [EBX + 8*EAX + 4]
これはアドレスをESI
にロードします。
"議会の禅" Abrash著:
LEA
、メモリアドレッシング計算を実行するが実際にはメモリをアドレス指定しない唯一の命令。LEA
は標準的なメモリアドレッシングオペランドを受け入れますが、計算されたメモリオフセットを指定されたレジスタに格納することに他なりません。それは私たちに何を与えるのでしょうか?
ADD
が提供しない2つのこと:
- 2つまたは3つのオペランドのいずれかで加算を実行する機能
- any registerに結果を格納する機能。ソースオペランドの1つだけではありません。
そしてLEA
はフラグを変更しません。
例
LEA EAX, [ EAX + EBX + 1234567 ]
はEAX + EBX + 1234567
を計算します(これは3つのオペランドです)。LEA EAX, [ EBX + ECX ]
は、結果をオーバーライドせずにEBX + ECX
を計算します。LEA EAX, [ EBX + N * EBX ]
のように使用する場合は、定数による乗算(2、3、5、または9による乗算)(Nは1,2,4,8)。LEA EAX, [ EAX + 1 ]
とINC EAX
の違いは、後者はEFLAGS
を変更しますが、前者は変更しないということです。これはCMP
状態を維持します。
LEA
命令のもう1つの重要な特徴は、CF
やZF
のような算術命令でアドレスを計算しながら、ADD
やMUL
などの条件コードを変更しないことです。この機能により、命令間の依存性のレベルが下がるため、コンパイラまたはハードウェアスケジューラによるさらなる最適化の余地があります。
すべての説明にもかかわらず、LEAは算術演算です。
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
その名前がshift + add操作にとって非常に愚かだということです。その理由はすでにトップクラスの回答で説明されています(つまり、高レベルのメモリ参照を直接マッピングするように設計されています)。
たぶんLEA命令についてのもう一つのこと。 3、5、または9でレジスタを高速乗算するためにLEAを使用することもできます。
LEA EAX, [EAX * 2 + EAX] ;EAX = EAX * 3
LEA EAX, [EAX * 4 + EAX] ;EAX = EAX * 5
LEA EAX, [EAX * 8 + EAX] ;EAX = EAX * 9
lea
は「load実効アドレス」の略語です。ソースオペランドによるロケーション参照のアドレスをデスティネーションオペランドにロードします。たとえば、あなたはそれを使用することができます:
lea ebx, [ebx+eax*8]
1つの命令でebx
ポインタeax
項目を(64ビット/要素の配列内で)さらに移動する。基本的に、ポインタを効率的に操作するために、x86アーキテクチャでサポートされている複雑なアドレッシングモードの恩恵を受けることができます。
LEA
に対してMOV
を使用する最大の理由は、アドレスを計算するために使用しているレジスタに対して算術演算を実行する必要がある場合です。効果的には、 "free"のために効果的に組み合わせていくつかのレジスタでポインタ演算に相当する量を実行することができます。
それについて本当に混乱しているのは、あなたが通常LEA
と同じようにMOV
を書くのですが、実際にはメモリを間接参照していないということです。言い換えると:
MOV EAX, [ESP+4]
これはESP+4
が指し示す内容をEAX
に移動します。
LEA EAX, [EBX*8]
これは実効アドレスEBX * 8
をEAXに移動させます、その場所にあるものではありません。ご覧のとおり、MOV
は加減算に限定されていますが、2倍(スケーリング)で乗算することも可能です。
8086は、レジスタオペランドおよび有効アドレスを受け入れ、その有効アドレスのオフセット部分を計算するためにいくつかの計算を実行し、計算されたアドレスによって参照されるレジスタおよびメモリを含むいくつかの演算を実行する命令の大ファミリーを有する。その実際のメモリ操作をスキップすることを除いて、そのファミリの命令の1つを上記のように動作させることは非常に簡単でした。これ、指示:
mov ax,[bx+si+5]
lea ax,[bx+si+5]
内部的にはほとんど同じように実装されていました。違いはスキップされたステップです。どちらの指示も、次のように機能します。
temp = fetched immediate operand (5)
temp += bx
temp += si
address_out = temp (skipped for LEA)
trigger 16-bit read (skipped for LEA)
temp = data_in (skipped for LEA)
ax = temp
インテルがこの命令を含める価値があると思った理由については、よくわかりませんが、実装するのが安価であるという事実が大きな要因となっていたでしょう。もう1つの要因は、IntelのアセンブラがBPレジスタに対してシンボルの定義を許可していたことです。 fnord
がBP相対シンボル(BP + 8など)として定義されている場合、次のようになります。
mov ax,fnord ; Equivalent to "mov ax,[BP+8]"
BP相対アドレスにデータを格納するためにストーのようなものを使いたいとしたら、
mov ax,0 ; Data to store
mov cx,16 ; Number of words
lea di,fnord
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative Word ptr
より便利でした:
mov ax,0 ; Data to store
mov cx,16 ; Number of words
mov di,bp
add di,offset fnord (i.e. 8)
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative Word ptr
世界の「オフセット」を忘れると、値8ではなくlocation [BP + 8]の内容がDIに追加されることになります。おっとっと。
既存の回答で述べたように、LEA
には、メモリにアクセスせずにメモリアドレッシング演算を実行し、単純な形式のadd命令ではなく、異なるレジスタに演算結果を保存するという利点があります。本当の根本的なパフォーマンス上の利点は、最新のプロセッサが効果的なアドレス生成(LEA
およびその他のメモリ参照アドレスを含む)のために別個のLEA ALUユニットとポートを持っていることです。これはLEA
ALUの操作は、1つのコアで並行して実行できます。
LEAユニットの詳細については、Haswellアーキテクチャの次の記事を参照してください。 http://www.realworldtech.com/haswell-cpu/4/
他の回答で言及されていないもう1つの重要な点は、LEA REG, [MemoryAddress]
命令がPIC(位置独立コード)であり、この命令のPC相対アドレスをエンコードしてMemoryAddress
を参照することです。これは、相対仮想アドレスをエンコードし、最新のオペレーティングシステムでの再配置/パッチ適用が必要なMOV REG, MemoryAddress
とは異なります(ASLRは一般的な機能です)。したがって、LEA
を使用して、このような非PICをPICに変換できます。
LEA命令は、CPUによる有効アドレスの時間のかかる計算を回避するために使用することができる。アドレスが繰り返し使用される場合は、使用されるたびに実効アドレスを計算するのではなく、アドレスをレジスタに格納する方が効果的です。
LEA(Load Effective Address)命令は、Intelプロセッサのメモリアドレッシングモードのいずれかから発生するアドレスを取得する方法です。
つまり、次のようにデータを移動するとします。
MOV EAX, <MEM-OPERAND>
指定されたメモリ位置の内容をターゲットレジスタに移動します。
MOV
をLEA
に置き換えると、メモリ位置のアドレスは<MEM-OPERAND>
アドレス式によってまったく同じ方法で計算されます。しかし、メモリロケーションの内容の代わりに、ロケーション自体を宛先に取得します。
LEA
は特定の算術命令ではありません。これは、プロセッサのメモリアドレッシングモードのいずれかから発生する実効アドレスを傍受する方法です。
たとえば、単純な直接アドレスにLEA
を使用できます。算術演算はまったく必要ありません。
MOV EAX, GLOBALVAR ; fetch the value of GLOBALVAR into EAX
LEA EAX, GLOBALVAR ; fetch the address of GLOBALVAR into EAX.
これは有効です。 Linuxプロンプトでテストすることができます。
$ as
LEA 0, %eax
$ objdump -d a.out
a.out: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 8d 04 25 00 00 00 00 lea 0x0,%eax
ここでは、スケーリングされた値の加算もオフセットもありません。ゼロがEAXに移動しました。 MOVを即値オペランドで使用しても可能です。
これが、LEA
の括弧が不必要であると考える人々がひどく誤っている理由です。大括弧はLEA
構文ではありませんが、アドレッシングモードの一部です。
LEAはハードウェアレベルで本物です。生成された命令は実際のアドレッシングモードをエンコードし、プロセッサはそれをアドレスを計算するポイントまで実行します。その後、メモリ参照を生成するのではなく、そのアドレスを宛先に移動します。 (他の命令のアドレッシングモードのアドレス計算はCPUフラグには影響しないため、LEA
はCPUフラグには影響しません。)
アドレス0から値をロードするのとは対照的に、
$ as
movl 0, %eax
$ objdump -d a.out | grep mov
0: 8b 04 25 00 00 00 00 mov 0x0,%eax
これは非常によく似たエンコーディングです。 LEA
の8d
だけが8b
に変わりました。
もちろん、このLEA
エンコーディングは即値ゼロをEAX
に移動するよりも長くなります。
$ as
movl $0, %eax
$ objdump -d a.out | grep mov
0: b8 00 00 00 00 mov $0x0,%eax
短い選択肢があるからといって、LEA
がこの可能性を排除する理由はありません。それはただ利用可能なアドレッシングモードと直交する方法で結合しています。
これが一例です。
// compute parity of permutation from lexicographic index
int parity (int p)
{
assert (p >= 0);
int r = p, k = 1, d = 2;
while (p >= k) {
p /= d;
d += (k << 2) + 6; // only one lea instruction
k += 2;
r ^= p;
}
return r & 1;
}
コンパイラオプションとして-O(最適化)を指定すると、gccは指定されたコード行のlea命令を見つけます。
LEA:単なる "算術"命令です。
MOVはオペランド間でデータを転送しますが、leaは計算中です
たくさんの答えがすでに完成しているようですが、同じ表現形式を使用した場合に、リーと移動の命令の動作が異なることを示すコード例をもう1つ追加します。
簡単に言えば、lea命令とmov命令の両方を、命令のsrcオペランドを囲む括弧とともに使用することができます。それらが () で囲まれている場合、 () 内の式は同じ方法で計算されます。ただし、2つの命令がsrcオペランドの計算値を異なる方法で解釈します。
式がleaとmovのどちらで使用されている場合でも、src値は次のように計算されます。
D(Rb、Ri、S) => (Reg [Rb] + S * Reg [Ri] + D)
ただし、mov命令と共に使用すると、上記の式で生成されたアドレスが指す値にアクセスし、それを宛先に格納しようとします。
それに対して、lea命令が上記の式で実行されると、生成された値がそのままデスティネーションにロードされます。
以下のコードは、同じパラメータを使用してlea命令とmov命令を実行します。ただし、違いを理解するために、mov命令の結果として誤ったアドレスにアクセスすることによって発生するセグメンテーション違反を検出するためのユーザーレベルのシグナルハンドラを追加しました。
コード例
#define _GNU_SOURCE 1 /* To pick up REG_RIP */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
uint32_t
register_handler (uint32_t event, void (*handler)(int, siginfo_t*, void*))
{
uint32_t ret = 0;
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
ret = sigaction(event, &act, NULL);
return ret;
}
void
segfault_handler (int signum, siginfo_t *info, void *priv)
{
ucontext_t *context = (ucontext_t *)(priv);
uint64_t rip = (uint64_t)(context->uc_mcontext.gregs[REG_RIP]);
uint64_t faulty_addr = (uint64_t)(info->si_addr);
printf("inst at 0x%lx tries to access memory at %ld, but failed\n",
rip,faulty_addr);
exit(1);
}
int
main(void)
{
int result_of_lea = 0;
register_handler(SIGSEGV, segfault_handler);
//initialize registers %eax = 1, %ebx = 2
// the compiler will emit something like
// mov $1, %eax
// mov $2, %ebx
// because of the input operands
asm("lea 4(%%rbx, %%rax, 8), %%edx \t\n"
:"=d" (result_of_lea) // output in EDX
: "a"(1), "b"(2) // inputs in EAX and EBX
: // no clobbers
);
//lea 4(rbx, rax, 8),%edx == lea (rbx + 8*rax + 4),%edx == lea(14),%edx
printf("Result of lea instruction: %d\n", result_of_lea);
asm volatile ("mov 4(%%rbx, %%rax, 8), %%edx"
:
: "a"(1), "b"(2)
: "edx" // if it didn't segfault, it would write EDX
);
}
実行結果
Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed