x86 Load Effetive Address

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の部分は scale と呼び、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. 乗算をする

一般的に数値を2のn乗倍するときは add 命令や shl 命令が用いられます。

addl %eax, %eax     # 2 倍
shll %eax           # 2 倍
shll $4, %eax       # 16 倍

かける数が2のn乗倍であれば効率が良いニーモニックが生成されることになりますが、 2のn乗倍でない場合には、その後足したり引いたりといった調整が必要です。

しかし 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 と同じ動作をしていることになります。 binutilsにおけるx86マルチバイト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 |
+-------+------------------+