Warning
この記事は2019年に書かれたもので、情報として残しておくためにObsidianに移したものです。現在は64bitのOSが主流なので、ここに書かれている内容は時代遅れかもしれません。
lea (Load Effective Address)はx86の命令のひとつです。普段、私たちがx86上でC言語のプログラムを書いてコンパイルしたとき、この命令の存在をまったく感じませんし、アセンブラをわざわざ覚えようとしないならば、気づきもせず一生を終えるでしょう。しかしx86プログラムはこの命令の大いなる恩恵を受けています。今回は lea の3 つの用途について紹介します。
1. 効率的にアドレスを取得する
x86には単純な命令が多い傾向にありますが、 lea はその中でも複雑な命令で、次のことを一度におこないます。
dst = src + index * [ 1 | 2 | 4 | 8 ] + disp
1, 2, 4, 8の部分はスケールと呼び、1, 2, 4, 8のどれかを指定します。1を指定したときニーモニック上では省略されます。これをGNU Assembler (GAS)に乗っ取った記法で表すと以下のニーモニックに対応します。
lea disp(src, index, scale), dst
lea 命令は src にアドレス値を持つレジスタを指定し( base と呼ぶ)、それに index * scale と disp を足し合わせたメモリアドレスを取得します。lea の行っていることは次の2つのコードで違いはありませんが、
leal 0x20(%esp, %ecx), %eaxmovl %esp, %eax
addl %ecx, %eax
addl $20, %eaxlea は1クロックサイクルで動作するため、 mov + add + add = 3クロックサイクルよりも効率的で、さらにはフラグレジスタを一切書き換えません。
2. 乗算をする
一般的に を計算するときは add 命令や shl 命令が用いられます。
addl %eax, %eax # 2 倍
shll %eax # 2 倍
shll $4, %eax # 16 倍かける数が であれば効率が良いニーモニックが生成されることになりますが、
倍でない場合には、その後足したり引いたりといった調整が必要です。
しかし lea を使用すれば、少ない命令数で計算できることがあります。 lea はオペランドにメモリを取るもののメモリアクセスはしないので、一般的な数値演算に流用可能です。
leal (%eax, %eax, 2), %eaxこのニーモニックでは eax <- eax + eax * 2 = 3eax を計算します。また、
3倍して50を足すには、
leal 50(%eax, %eax, 2), %eaxと書けます。コンパイラでは乗算するコードを自動的に lea を使ったコードに変換します。参考に2倍から10倍するときのコードは次の様になります。
addl %eax, %eax # 2
leal (%eax, %eax, 2), %eax # 3
shll $2, %eax # 4
leal (%eax, %eax, 4), %eax # 5
leal (%eax, %eax, 2), %eax # 6
addl %eax, %eax
leal (, %eax, 8), %edx # 7
subl %eax, %edx
shll $3, %eax # 8
leal (%eax, %eax, 8), %eax # 9
leal (%eax, %eax, 4), %eax # 10
addl %eax, %eax7倍のときは base が省略されており、このときは0として計算されます。shl 命令は2のn乗倍するだけなら最適な命令で、シフトされるレジスタは自由に選べますが、シフト数は即値か cl レジスタ( ecx の8bit分)しか使用できないのが難点です。
lea 命令はオペランドの自由度も高く、 base に8 つの汎用レジスタを取ることができ、index にはスタックポインタである esp を除く7つのレジスタを指定できます。index に esp を設定すると特殊なゼロレジスタ eiz になり、0として計算する様なトリッキーなこともできます。
3. nop としてのlea
lea はフラグレジスタに影響を与えないので、レジスタを実質書き換えない記述をすれば nop として使用可能です。よく見るニーモニックは次の様なものです。
leal 0x00(%esi), %esiesi に一切足し合わせない結果を esi に代入しているので、事実上 nop と同じ動作をしていることになります。
leaの仕組み
lea の機械語は 0x8d から始まり、その後x86の汎用マシン命令フォーマットが続きます。
+-------------+-----------+----------+---------------+
| Opcode(1-2) | ModR/M(1) | SIB(0-1) | Disp(0, 1, 4) |
+-------------+-----------+----------+---------------+
オペコードは必須で、ModR/Mバイトが続く場合があります。SIBバイトとディスプレースメントバイトはオプションです。ModR/M と SIB はさらにビットフィールドで、
ModR/M
8 7 6 5 4 3 2 1 0
+--+--+--+--+--+--+--+--+
| mod | reg | mem |
+--+--+--+--+--+--+--+--+
SIB
8 7 6 5 4 3 2 1 0
+--+--+--+--+--+--+--+--+
|scale| index | base |
+--+--+--+--+--+--+--+--+
という構造になっています。mod は disp のありなしを決めます。
mod = 0b00のとき、dispはありません。mod = 0b01のとき、8bitのdispを指定します。mod = 0b10のとき、32bitのdispを指定します。mod = 0b11は設定禁止です。
mem にソースレジスタ、 reg にデスティネーションレジスタを指定します。
+-------+-------------------------------------+-----+
| bit | mem | reg |
+-------+-------------------------------------+-----+
| 0b000 | eax |
| 0b001 | ecx |
| 0b010 | edx |
| 0b011 | ebx +-----+
| 0b100 | (SIB) | esp |
| 0b101 | 32bit addr / ebp(mod = 0b01, 0b10) | ebp |
| 0b110 | esi +-----+
| 0b111 | edi |
+-------+-------------------------------------------+
+-------+--------+---------+---------+
| bit | base | index | scale |
+-------+--------+---------+---------+
| 0b000 | eax | 1 |
| 0b001 | ecx | 2 |
| 0b010 | edx | 4 |
| 0b011 | ebx | 8 |
| 0b100 | esp | (eiz) | - |
| 0b101 | (0) | ebp | - |
| 0b110 | esi | - |
| 0b111 | edi | - |
+-------+------------------+---------+
ちゃんと動くのか興味がある方はgccを使って試してみるのも手です。例えば、 leal (%eax), %eax は、 mod = 0b00 , reg = 0b000 , mem = 0b000 とすれば作れます。
# test.s
.globl main
main:
.byte 0x8d, 0b0000000とアセンブラソースに記述して、
$ gcc test.s -m32とコンパイルします。できあがったファイルに対して objdump をすれば、書いた命令の確認ができます。
$ objdump -d a.out | less
80483f0: 8d 00 lea (%eax), %eaxebpによるアドレス指定
mem = 0b101 のときは mod によって動作が変わります。
mod = 0b00では32bitのアドレスを指定します。mod = 0b01, 0b10ではほかのレジスタ指定時同様にebpに対するdispを指定します。
# leal 0xdeadbeef, %eax
.byte 0x8d, 0b00000101, 0xef, 0xbe, 0xad, 0xde
# leal 0x20(%ebp), %eax
.byte 0x8d, 0b01000101, 0x20
# leal 0x40302010(%ebp), %eax
.byte 0x8d, 0b10000101, 0x10, 0x20, 0x30, 0x40
SIB拡張
mem = 0b100 のときはSIB拡張と呼ばれる追加の1バイトを指定します。ビットフィールド上ではソースに esp を指定したことになりますが、実際のソースは base となります。
ここでも試しに簡単な leal (%eax, %ecx, 4), %edx を作ってみます。SIB拡張をするため、 mod = 0b00 , reg = 0b010 , mem = 0b100 , base = 0b000 , index = 0b001 , scale = 0b10 です。
# leal (%eax, %ecx, 4), %edx
.byte 0x8d, 0b00010100, 0b10001000先ほど常に0として扱われる特殊なレジスタ eiz を挙げましたが、 base = 0b101 や index = 0b100 とすると eiz を指定できます。
# leal 0x00(, %eax, 1), %eax
.byte 0x8d, 0b00000100, 0b00000101
.long 0x00000000
# leal (%eax, %eiz, 1), %eax
.byte 0x8d, 0b00000100, 0b00100000
# leal 0x00(, %eiz, 1), %eax
.byte 0x8d, 0b00000100, 0b00100101
.long 0x00000000この様な拡張があるため、単純なニーモニックも機械語にすると長くなることがあります。
# leal (%esp), %esp => leal (%esp, %eiz, 1), %esp
.byte 0x8d, 0b00100100, 0b00100100
# leal (%ebp), %ebp => leal 0x00(%ebp), %ebp
.byte 0x8d, 0b01101101, 0b00000000おまけ: 16bit lea
data16 を付けると lea 命令も16bitになります。32bitと違うところはデスティネーションレジスタが16bit( ax , cx , dx , bx , sp , bp , si , di )になるということだけです。
# leal (%eax), %eax
.byte 0x8d, 0b00000000
# leaw (%eax), %ax
.byte 0x66, 0x8d, 0b00000000また、 addr16 ではソースと disp が16bitになります。
+-------+------------+-----+
| bit | mem | reg |
+-------+------------+-----+
| 0b000 | bx + si | eax |
| 0b001 | bx + di | ecx |
| 0b010 | bp + si | edx |
| 0b011 | bp + di | ebx |
| 0b100 | si | esp |
| 0b101 | di | ebp |
| 0b110 | 16bit addr | esi |
| 0b111 | bx | edi |
+-------+------------------+