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 * scaledisp を足し合わせたメモリアドレスを取得します。lea の行っていることは次の2つのコードで違いはありませんが、

leal 0x20(%esp, %ecx), %eax
movl %esp, %eax
addl %ecx, %eax
addl $20, %eax

lea は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, %eax

7倍のときは base が省略されており、このときは0として計算されます。shl 命令は2のn乗倍するだけなら最適な命令で、シフトされるレジスタは自由に選べますが、シフト数は即値か cl レジスタ( ecx の8bit分)しか使用できないのが難点です。

lea 命令はオペランドの自由度も高く、 base に8 つの汎用レジスタを取ることができ、index にはスタックポインタである esp を除く7つのレジスタを指定できます。indexesp を設定すると特殊なゼロレジスタ eiz になり、0として計算する様なトリッキーなこともできます。

3. nop としてのlea

lea はフラグレジスタに影響を与えないので、レジスタを実質書き換えない記述をすれば nop として使用可能です。よく見るニーモニックは次の様なものです。

leal 0x00(%esi), %esi

esi に一切足し合わせない結果を esi に代入しているので、事実上 nop と同じ動作をしていることになります。

leaの仕組み

lea の機械語は 0x8d から始まり、その後x86の汎用マシン命令フォーマットが続きます。

+-------------+-----------+----------+---------------+
| Opcode(1-2) | ModR/M(1) | SIB(0-1) | Disp(0, 1, 4) |
+-------------+-----------+----------+---------------+

オペコードは必須で、ModR/Mバイトが続く場合があります。SIBバイトとディスプレースメントバイトはオプションです。ModR/MSIB はさらにビットフィールドで、

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  |
+--+--+--+--+--+--+--+--+

という構造になっています。moddisp のありなしを決めます。

  • 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), %eax

ebpによるアドレス指定

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 = 0b101index = 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 |
+-------+------------------+

参考文献