intel lea指令中SIB模式的极限在哪里
一、问题的由来
在汇编代码中,经常可以看到lea这个指令,它的出现频率比它“应该出现”的频率高得多,因为很多时候,它甚至可以用来做简单的乘法运算。
tsecer@harry: cat mult.cpp
int foo(int x)
{
return x * 9;
}
tsecer@harry: gcc -O2 -c mult.cpp
tsecer@harry: objdump -d mult.o
mult.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3fooi>:
0: 8d 04 ff lea (%rdi,%rdi,8),%eax
3: c3 retq
tsecer@harry:
这个指令初看起来比较复杂,尽管它最原始的设计意图是为了对应高级语言中对结构数组中特定成员取值的指令。例如下面的情况
tsecer@harry: cat typicallea.cpp
struct S
{
int x;
int y;
};
void *foo(S *ps, int i)
{
return &ps[i].y;
}
tsecer@harry: gcc -O2 -c typicallea.cpp
tsecer@harry: objdump -d typicallea.o
typicallea.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3fooP1Si>:
0: 48 63 f6 movslq %esi,%rsi
3: 48 8d 44 f7 04 lea 0x4(%rdi,%rsi,8),%rax
8: c3 retq
tsecer@harry:
现在的问题是,这个复杂指令的极限在哪里?比方说,其中的8可以修改为100、7这种数值吗?外面的0x4可以修改为任意的32位绝对地址吗?
二、intel手册对指令的说明
ModR/M 1 byte (if required) 其中 Mod为7-6bit,Reg/OpCode为5-3bit,R/M为2-0bit
SIB 1 byte (if required) 其中Scale为7-6bit、Index为5-3bit,Base为2-0bit。
2.1.3 ModR/M and SIB Bytes
Many instructions that refer to an operand in memory have an addressing-form specifier byte (called the ModR/M byte) following the primary opcode. The ModR/M byte contains three fields of information:
• The mod field combines with the r/m field to form 32 possible values: eight registers and 24 addressing modes.
• The reg/opcode field specifies either a register number or three more bits of opcode information. The purpose
of the reg/opcode field is specified in the primary opcode.
• The r/m field can specify a register as an operand or it can be combined with the mod field to encode an
addressing mode. Sometimes, certain combinations of the mod field and the r/m field is used to express
opcode information for some instructions.
Certain encodings of the ModR/M byte require a second addressing byte (the SIB byte). The base-plus-index and
scale-plus-index forms of 32-bit addressing require the SIB byte. The SIB byte includes the following fields:
• The scale field specifies the scale factor.
• The index field specifies the register number of the index register.
• The base field specifies the register number of the base register.
See Section 2.1.5 for the encodings of the ModR/M and SIB bytes
综合这些信息,我们比较关心的问题是:
scale的范围:scale为7-6两个bit,所以它的范围为0-3。但是,这个factor事实上是表示2^factor中的factor(尽管我在手册搜索“scale”没有找到明确说明),也就是说在汇编中看到的disp(scale, index, base)中的scale的取值只能为1、2、4、8。
Table 2-2. 32-Bit Addressing Forms with the ModR/M Byte
[--][--]1、[--][--]+disp8、[--][--]+disp32
并且在注释中说明
NOTES:
1. The [--][--] nomenclature means a SIB follows the ModR/M byte.
2. The disp32 nomenclature denotes a 32-bit displacement that follows the ModR/M byte (or the SIB byte if one is present) and that is added to the index.
3. The disp8 nomenclature denotes an 8-bit displacement that follows the ModR/M byte (or the SIB byte if one is present) and that is sign-extended and added to the index.
从这里可以看到,当使用SIB模式的时候,偏移量可以为8bit的常量,也可以是32bit的常量。
这样算下来,其实lea中的偏移量可以表达的形式既丰富又受限,可以认为是带着脚镣跳舞。
三、验证一些例子
1、关于scale大小的例子
可以看到,当结构大于8的时候(例如struct B),一条lea指令已经无法满足,而需要转换为多条lea指令。以struct B为例,结构大小为12=4*3,所以首先使用lea(esi, esi, 2)获得esi * 3,然后在lea(edi,esi,4)获得esi*4。
tsecer@harry: cat lea.cpp
struct S
{
int x;
int y;
};
struct B
{
S s;
int y;
};
#pragma pack(1)
struct A
{
int x;
int y;
short s;
}
;
void* fooS(S *ps, int i)
{
return &ps[i].y;
}
void* fooB(B *pb, int i)
{
return &pb[i].y;
}
void* fooA(A *pa, int i)
{
return &pa[i].y;
}
tsecer@harry: gcc -O2 -c lea.cpp
tsecer@harry: objdump -d lea.o
lea.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z4fooSP1Si>:
0: 48 63 f6 movslq %esi,%rsi
3: 48 8d 44 f7 04 lea 0x4(%rdi,%rsi,8),%rax
8: c3 retq
9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
0000000000000010 <_Z4fooBP1Bi>:
10: 48 63 f6 movslq %esi,%rsi
13: 48 8d 04 76 lea (%rsi,%rsi,2),%rax
17: 48 8d 44 87 08 lea 0x8(%rdi,%rax,4),%rax
1c: c3 retq
1d: 0f 1f 00 nopl (%rax)
0000000000000020 <_Z4fooAP1Ai>:
20: 48 63 f6 movslq %esi,%rsi
23: 48 8d 04 b6 lea (%rsi,%rsi,4),%rax
27: 48 8d 44 47 04 lea 0x4(%rdi,%rax,2),%rax
2c: c3 retq
tsecer@harry:
2、关于disp大小的例子
下面的例子看到,虽然127和128只差了1,但是由于它的指令长度相差了3个bytes,导致最后的dips从8个bit直接提升为32bit,也就是
3: 48 8d 44 f7 7f lea 0x7f(%rdi,%rsi,8),%rax
和
13: 48 8d 84 f7 80 00 00 lea 0x80(%rdi,%rsi,8),%rax
1a: 00
的差别。
tsecer@harry: cat lea.disp.cpp
struct S
{
int x;
int y;
};
void* foo127(S *ps, int i)
{
return (void*)&ps[i].x + 127;
}
void* foo128(S *ps, int i)
{
return (void*)&ps[i].x + 128;
}
tsecer@harry: gcc -O2 -c lea.disp.cpp
lea.disp.cpp: 在函数‘void* foo127(S*, int)’中:
lea.disp.cpp:10:27: 警告:‘void *’型指针用在了算术表达式中 [-Wpointer-arith]
return (void*)&ps[i].x + 127;
^
lea.disp.cpp: 在函数‘void* foo128(S*, int)’中:
lea.disp.cpp:15:27: 警告:‘void *’型指针用在了算术表达式中 [-Wpointer-arith]
return (void*)&ps[i].x + 128;
^
tsecer@harry: objdump -d lea.disp.o
lea.disp.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z6foo127P1Si>:
0: 48 63 f6 movslq %esi,%rsi
3: 48 8d 44 f7 7f lea 0x7f(%rdi,%rsi,8),%rax
8: c3 retq
9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
0000000000000010 <_Z6foo128P1Si>:
10: 48 63 f6 movslq %esi,%rsi
13: 48 8d 84 f7 80 00 00 lea 0x80(%rdi,%rsi,8),%rax
1a: 00
1b: c3 retq
tsecer@harry: