私にとっては、ファンキーなMOVのようです。その目的は何ですか、いつ使用する必要がありますか?
私にとっては、ファンキーなMOVのようです。その目的は何ですか、いつ使用する必要がありますか?
回答:
他の人が指摘したように、LEA(ロード実効アドレス)は特定の計算を行うための「トリック」としてよく使用されますが、それはその主な目的ではありません。x86命令セットは、PascalやCなどの高水準言語をサポートするように設計されており、配列(特に、intまたは小さな構造体の配列)が一般的です。たとえば、(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
。
mov
命令を拡張して括弧を省くほうがきれいではなかっただろうか?MOV EDX, EBX + 8*EAX + 4
MOV
、間接ソースのみの場合と似ていますが、を行わず、間接ソースのみを行う点が異なりMOV
ます。実際に計算されたアドレスから読み取るのではなく、計算するだけです。
Abrash の"Zen of Assembly"から:
LEA
、メモリアドレッシング計算を実行するが、実際にはメモリをアドレッシングしない唯一の命令。LEA
標準のメモリアドレッシングオペランドを受け入れますが、計算されたメモリオフセットを指定されたレジスタに格納するだけです。これは、任意の汎用レジスタである場合があります。それは私たちに何を与えますか?
ADD
提供されない2つのこと:
- 2つまたは3つのオペランドを使用して加算を実行する機能、および
- 結果を任意のレジスタに保存する機能。ソースオペランドの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 による)。差:他のユースケースは、ループに便利であるLEA EAX, [ EAX + 1 ]
とINC EAX
後者変更することであるEFLAGS
が、前者はありません。これはCMP
状態を保持します。
LEA EAX, [ EAX + EBX + 1234567 ]
の合計を計算しEAX
、EBX
そして1234567
(3つのオペランドだという)。結果を上書きせずにLEA EAX, [ EBX + ECX ]
計算EBX + ECX
します。3つ目は、LEA
(フランクによって記載されていない)のために使用されている定数による乗算あなたのようにそれを使用する場合、(2、3、5または9による)LEA EAX, [ EBX + N * EBX ]
(N
1,2,4,8することができます)。差:他のユースケースは、ループに便利であるLEA EAX, [ EAX + 1 ]
とINC EAX
後者変更することであるEFLAGS
が、前者はありません。これはCMP
状態を保持します
LEA
を使用できるかが明確になります...(上記のIJケネディの人気のある回答で「LEA(実効アドレスのロード)は、特定の計算を行うための「トリック」としてよく使用されます)
LEA
命令のもう1つの重要な機能は、またはのような算術命令によってアドレスを計算するときに、CF
andのような条件コードを変更しないことです。この機能により、命令間の依存関係のレベルが低下し、コンパイラまたはハードウェアスケジューラによるさらなる最適化の余地が生まれます。ZF
ADD
MUL
lea
コンパイラ(または人間のコーダー)がフラグの結果を壊さずに数学を実行するのに役立つ場合があります。しかしlea
、よりも速くはありませんadd
。ほとんどのx86命令はフラグを書き込みます。高性能のx86実装では、EFLAGSの名前を変更するか、通常のコードが高速に実行されるように書き込み後の危険を回避する必要があるため、フラグの書き込みを回避する命令の方が適していません。(部分的なフラグのものは問題を作成する可能性があります。INC命令とADD 1を参照してください。それは問題ですか?)
すべての説明にもかかわらず、LEAは算術演算です。
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
その名前が、shift + add操作に対して非常に愚かであるというだけです。その理由は、すでに上位の回答で説明されています(つまり、高レベルのメモリ参照を直接マップするように設計されています)。
LEA
、AGUではなく通常の整数ALUで実行するように選択したCPUもあります。最近、CPU仕様をよく読んで、「どこで実行されるか」を見つけなければなりません...
LEA
メモリ関連のアドレッシングモードから生じるアドレスを提供します。これはシフトおよび追加操作ではありません。
LEA命令については、もう1つだけかもしれません。LEAを使用して、レジスタを3、5、または9で高速乗算することもできます。
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 EAX, [EAX*3]
か?
shl
レジスタを2,4,8,16倍する命令のように左シフトを使用できます...高速で短いです。しかし、2の累乗の異なる数を乗算する場合、通常mul
はより大げさで遅い命令を使用します。
lea eax,[eax*3]
はに相当しlea eax,[eax+eax*2]
ます。
lea
「ロード実効アドレス」の略です。ソースオペランドによるロケーション参照のアドレスをデスティネーションオペランドにロードします。たとえば、次の目的で使用できます。
lea ebx, [ebx+eax*8]
1つの命令でebx
ポインターeax
項目をさらに(64ビット/要素配列で)移動します。基本的に、ポインタを効率的に操作するために、x86アーキテクチャでサポートされている複雑なアドレッシングモードを利用できます。
あなたが使用することを最大の理由LEA
の上にはMOV
、あなたがアドレスを計算するために使用していることをレジスタに算術演算を実行する必要がある場合です。効果的には、いくつかのレジスタでポインタ演算に相当するものを効果的に組み合わせて「無料」で実行できます。
それについて本当に混乱しているのは、通常LEA
aと同じように書いても、MOV
実際にはメモリを逆参照していないということです。言い換えると:
MOV EAX, [ESP+4]
これにより、ESP+4
ポイントするコンテンツがに移動しEAX
ます。
LEA EAX, [EBX*8]
これにより、有効なアドレスEBX * 8
がEAXに移動します。その場所にあるものではありません。ご覧のとおり、a MOV
は加算/減算に制限されていますが、2の係数(スケーリング)を乗算することもできます。
LEA
しているかを理解するのがとても難しい理由の一つだと思います。
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
Intelがこの命令を含める価値があると思った理由については、はっきりとはわかりませんが、実装するのが安かったという事実が大きな要因だったでしょう。別の要因は、インテルのアセンブラーがシンボルをBPレジスターに関連して定義することを許可したという事実でした。fnord
がBP相対シンボル(例:BP + 8)として定義されている場合、次のように言うことができます。
mov ax,fnord ; Equivalent to "mov ax,[BP+8]"
stoswのようなものを使用してデータを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ではなく場所[BP + 8]のコンテンツがDIに追加されることに注意してください。おっとっと。
既存の回答が述べたように、LEA
メモリにアクセスせずにメモリアドレッシング演算を実行するという利点があり、単純な形式のadd命令ではなく、演算結果を別のレジスタに保存します。本当の根本的なパフォーマンスの利点は、最新のプロセッサが効果的なアドレス生成(LEA
およびその他のメモリ参照アドレスを含む)のために独立したLEA ALUユニットとポートをLEA
備えていることです。芯。
LEAユニットの詳細については、Haswellアーキテクチャのこの記事を確認してください。http: //www.realworldtech.com/haswell-cpu/4/
他の回答で言及されていないもう1つの重要な点は、LEA REG, [MemoryAddress]
命令ですMemoryAddress
。この命令のPC相対アドレスを参照するようにエンコードするPIC(位置独立コード)です。これは、MOV REG, MemoryAddress
相対仮想アドレスをエンコードするものとは異なり、最新のオペレーティングシステムでは再配置/パッチが必要です(ASLRは一般的な機能です)。したがって、そのLEA
ような非PICをPICに変換するために使用できます。
lea
は、他の算術命令を実行する1つ以上の同じALUで実行されます(ただし、通常、他の算術命令よりも数は少なくなります)。例えば、ハズウエルCPUが実行することができる言及add
またはsub
またはで他のほとんどの基本的な算術演算四つの異なるのALU、のみ実行することができるlea
一方(複合上lea
)または2つ(単純lea
)。さらに重要なことに、これらの2つのlea
ALUは、他の命令を実行できる4つのうちの2つにすぎないため、主張されているような並列処理の利点はありません。
LEA命令を使用すると、CPUによる有効アドレスの計算に時間がかかることを回避できます。アドレスが繰り返し使用される場合は、使用されるたびに実効アドレスを計算するのではなく、レジスタに格納する方が効果的です。
[esi]
ため、言うよりめったに安く[esi + 4200]
はなく、めったに安くはありません[esi + ecx*8 + 4200]
。
[esi]
は[esi + ecx*8 + 4200]
。よりも安くはありません。しかし、なぜ比較するのか。それらは同等ではありません。前者が後者と同じメモリ位置を指定するようにしたい場合は、追加の指示が必要です。8 esi
をecx
掛けた値に追加する必要があります。次に、4200を追加する必要があります。これらの追加の命令は、コードサイズに追加されます(命令キャッシュのスペースを占有し、フェッチするサイクル)。
[esi + 4200]
一連の命令で繰り返しのようなものを使用する場合、最初に実効アドレスをレジスターにロードしてそれを使用する方がよいと言っています。たとえばadd eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]
、を書くのlea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi]
ではなく、を優先することをお勧めします。少なくともそれはこの答えの明白な解釈です。
[esi]
および[esi + 4200]
(または[esi + ecx*8 + 4200]
同じ複雑なアドレスを持つN個の命令は、単純な(1つのREG)のアドレス指定、プラスワンでN個の命令に変換されていること:これはOP単純化であるということですが、(私はそれを理解したように)提案していますlea
、複雑なアドレッシングは「時間がかかる」ため、実際、最新のx86でも遅くなりますが、同じアドレスの連続する命令ではレイテンシが問題になる可能性は低い
lea
なるため、その場合のプレッシャーが増大します。一般的に、中間体を保存することはレジスター圧力の原因であり、解決策ではありませんが、ほとんどの場合それは洗浄であると思います。@カズ
LEA(実効アドレスのロード)命令は、インテルプロセッサーのメモリアドレス指定モードから発生するアドレスを取得する方法です。
つまり、次のようなデータ移動がある場合、
MOV EAX, <MEM-OPERAND>
指定されたメモリ位置の内容をターゲットレジスタに移動します。
MOV
by を置き換える場合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
これは非常によく似たエンコーディングです。ただ8d
のは、LEA
に変更されました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 and move命令がどのように異なる動作をするかを示すサンプルコードをもう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
=d
して、結果がEDXにあることをコンパイラーに伝えることもできますmov
。また、出力の初期クローバー宣言も省略しました。これは、あなたが説明しようとしていることを示していますが、他のコンテキストで使用すると壊れるインラインasmの誤解を招く悪い例でもあります。それはスタックオーバーフローの答えとしては悪いことです。
%%
Extended asmでこれらすべてのレジスタ名に書き込みたくない場合は、入力制約を使用します。のようにasm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));
。コンパイラをレジスタに初期化させることで、クロバーを宣言する必要もなくなります。mov-immediateがレジスタ全体を上書きする前に、xor-zeroingによって複雑化しています。
mov 4(%ebx, %eax, 8), %edx
が無効だと誰が言うのですか?とにかく、はい、64ビットの値があることをコンパイラに伝えるmov
ために書き込むのは理にかなって"a"(1ULL)
いるので、レジスタ全体を埋めるように拡張されていることを確認する必要があります。実際にはmov $1, %eax
、コンパイラーがRAX = 0xff00000001
または何かを知っているコードを囲む奇妙な状況でない限り、EAXをゼロ拡張してRAXに書き込むため、引き続きを使用します。の場合lea
、まだ32ビットのオペランドサイズを使用しているため、入力レジスタの迷子の上位ビットは32ビットの結果に影響を与えません。
LEA:単なる「算術」命令。
MOVはオペランド間でデータを転送しますが、leaは計算中です
mov eax, offset GLOBALVAR
代わりに使用してください。あなたはできる LEAを使用しますが、それはよりもわずかに大きいコードサイズだmov r32, imm32
と、少数のポート上で動作し、それはまだアドレス計算プロセスを通過しているため。 lea reg, symbol
PICおよび/または下位32ビット外のアドレスが必要な場合、RIP相対LEAの64ビットでのみ役立ちます。32ビットまたは16ビットのコードでは、利点はありません。LEAは、アドレッシングモードをデコード/計算するCPUの機能を公開する算術命令です。
imul eax, edx, 1
が計算しないと言うことができます:それは単にedxをeaxにコピーします。しかし実際には、3サイクルのレイテンシで乗算器を介してデータを実行します。または、rorx eax, edx, 0
単にコピーします(ゼロ回転)。
乗算の追加、排他的、またはゼロ、符号などのステータスフラグの設定など、すべての通常の「計算」命令。複雑なアドレスを使用する場合AX xor:= mem[0x333 +BX + 8*CX]
、フラグはxor操作に従って設定されます。
これで、アドレスを複数回使用することができます。このようなアドレスをレジスタにロードすることは、ステータスフラグを設定することを意図したものではなく、幸いにもそうではありません。「実効アドレスをロードする」というフレーズは、プログラマにそれを認識させます。それが奇妙な表現の源です。
プロセッサが複雑なアドレスを使用してそのコンテンツを処理できるようになると、他の目的のためにそれを計算できるようになることは明らかです。実際x <- 3*x+1
、1つの命令で変換を実行するために使用できます。これは、アセンブリプログラミングの一般的な規則です。ボートを揺さぶる方法を使用してください。
重要なのは、命令によって具体化される特定の変換がユーザーにとって役立つかどうかだけです。
ボトムライン
MOV, X| T| AX'| R| BX|
そして
LEA, AX'| [BX]
AXには同じ効果がありますが、ステータスフラグには影響しません。(これはciasdis表記です。)
call lbl
lbl: pop rax
の値を取得する方法として技術的に「機能している」などの理由により、私は個人的にそのアドバイスを提供しrip
ませんが、ブランチ予測を非常に不幸にします。必要に応じて手順を使用しますが、トリッキーなことを実行しても予期しない結果になったとしても驚かないでください