SP是栈指针Stack Pointer的缩写。在初始化之后,它是当前栈地址的指针。
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
|
RBP
|
||||||||
EBP
|
||||||||
BP
|
||||||||
BPL
|
帧指针(Frame Pointer),通常是局部变量的指针。在调用函数时,它也常常用来传递参数。有关这个寄存器的详细介绍,请参照本书的7.1.2节。
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
|
RIPx64
|
||||||||
EIP
|
||||||||
IP
|
指令指针instruction pointer应当总是指向接下来将要执行的那条指令。正常情况下,无法直接干预它的值。但是,下述指令可以等效地实现调整指令指针的功能:
MOV EAX, ...
JMP EAX
或者:
PUSH Value
RET
CS/DS/SS/ES分别代表Code Segment代码段寄存器、Data Segment数据段寄存器、Stack Segment堆栈段寄存器和Extra Segment附加段寄存器。
在Win32系统里,FS附加段寄存器(Extra Segment Register)承担TLS(线程本地存储/Thread Local Storage)的角色;而在Linux系统里,GS(另一个附加段寄存器)承担这个角色。早期,这两个寄存器用于实现段式寻址;而现在,它们用于提供更为快速的TLS和TIB(线程信息块/ThreadInformationBlock)功能。有关段地址寄存器的详细介绍,请参见本书第94章。
标识寄存器即Eflags。
Bit位(及掩码) |
缩写(及含义) |
描 述 |
---|---|---|
0(1) |
CF(进/借位) |
除了常规计算指令之外,专门操作CF的指令还有CLC/STC/CMC |
2(4) |
PF(奇偶标识位) |
参见17.7.1节 |
4(0x10) |
AF(辅助进/借位标识) |
|
6(0x40) |
ZF(零标识位) |
ZF用来反映运算结果是否为0。如果运算结果为0,则其值为1,否则其值为0 |
7(0x80) |
SF(符号位) |
|
8(0x100) |
TF(追踪标识) |
当追踪标志TF被置为1时,CPU进入单步执行方式,即每执行一条指令,产生一个单步中断请求。这种方式主要用于程序的调试 |
9(0x200) |
IF(中断允许标识) |
中断允许标志用来决定CPU是否响应CPU外部的可屏蔽中断发出的中断请求。CLI/STI指令可对它进行赋值 |
10(0x400) |
DF(方向标识) |
决定在执行串操作指令(REP MOVSx、REP CMPSx、REP LODSx和REP SCASx)时有关指针寄存器发生调整的方向。CLD/STD指令可对它进行赋值 |
11(0x800) |
OF(溢出标识) |
|
12,13(0x3000) |
IOPL(I/O特权标识)80286 |
|
14(0x4000) |
NT(嵌套任务标志)80286 |
|
16(0x10000) |
RF(重启标识)80386 |
重启动标识用来控制是否接受调试。如果它的值为1,那么CPU将忽略DRx中的硬件断点调试功能 |
17(0x20000) |
VM(虚拟8086方式标志)80386 |
|
18(0x40000) |
AC(对准校验方式位)80486 |
|
19(0x80000) |
VIF(虚拟中断标志)Pentium |
|
20(0x100000) |
VIP(虚拟中断未决标志)Pentium |
|
21(0x200000) |
ID(标识标志)Pentium |
其余的标识位都是保留标识位。
FPU栈由8个80位寄存器构成,这8个寄存器分别叫作ST(0)~ST(7)。IDA把ST(0)显示为ST。FPU寄存器用于存储符合IEEE 754标准的long double型数据。这种数据的格式如下表所示。
第79位 |
第78-64位 |
第63位 |
第62-0位 |
---|---|---|---|
符号位 |
指数位 |
整数位 |
尾数(小数)位 |
FPU的控制字(Control Word)用于控制FPU的行为。
位 |
缩写(及含义) |
描 述 |
---|---|---|
0 |
IM(无效操作掩码) |
|
1 |
DM(操作数规格异常掩码) |
|
2 |
ZM(除数为0的掩码) |
|
3 |
OM(上溢/溢出掩码) |
|
4 |
UM(下溢/溢出掩码) |
|
5 |
PM(精度异常掩码) |
|
7 |
IEM(异常中断位/软件处理控制位) |
第0~5位掩码控制功能的总开关,现在的FPU已经不可对其赋值。若IEM为0,则由FPU处理所有的异常信息,从而对软件屏蔽了所有的错误信息。默认值为1 |
8,9 |
PC(精度控制) |
00:IEEE单精度24位(REAL4) |
10,11 |
RC(舍入控制) |
00:就近舍入(默认) |
12 |
IC(无限/∞控制位) |
0:按照unsigned处理±∞(初始态) |
若PM、UM、OM、ZM、DM、IM字段(第0~5位)设置为1,则由FPU处理异常信息(对软件屏蔽了错误信息);若某位设置为0,则FPU将会在遇到相应异常时进行中断、释放异常信息给应用程序,程序在处理之后再把控制权返还给FPU。
FPU的状态寄存器又称Fstate,属于只读寄存器。
位序 |
缩写(及含义) |
描 述 |
---|---|---|
15 |
B(忙) |
1:FPU正在进行运算 |
14 |
C3(条件代码位C3) |
|
13,12,11 |
TOP(栈顶指针) |
ST(0)使用的物理寄存器 |
10 |
C2(条件代码位C2) |
|
9 |
C1(条件代码位C1) |
|
8 |
C0(条件代码位C0) |
|
7 |
IR(中断请求) |
|
6 |
SF(栈异常) |
|
5 |
P(精度) |
|
4 |
U(下溢) |
|
3 |
O(上溢) |
|
2 |
Z(运算结果为零) |
|
1 |
D(操作数规格异常) |
|
0 |
I(无效操作) |
状态位SF、P、U、O、Z、D、I用于异常反馈。
有关C3、C2、C1、C0的更详细介绍,请参见本书的17.7.1节。
在软件使用st(x)时,FPU会计算x与栈顶指针序号的和,必要的时候还会再计算8的模(余数),以此确定栈指针的物理寄存器地址。
标志字寄存器总共16位。每2位为一组,表示FPU数据寄存器的使用情况。
位 序 |
描 述 |
---|---|
15,14 |
Tag(7) |
13,12 |
Tag(6) |
11,10 |
Tag(5) |
9,8 |
Tag(4) |
7,6 |
Tag(3) |
5,4 |
Tag(2) |
3,2 |
Tag(1) |
1,0 |
Tag(0) |
Tag(x)存储着FPU物理寄存器R(x)[1]的状态码。
其各值的代表含义是:
00:该寄存器存储着非零的值。
01:该寄存器存储的值为零。
10:寄存器的值为特殊的值,NAN、∞或者无效操作数。
11:寄存器为空。
MMX寄存器由8个64位寄存器(MM0~MM7)组成。
SSE都有XMM0~XMM7这8个128位寄存器,x86-64系统还有额外的8个寄存器(XMM8~XMM15)。
而支持AVX指令集的CPU,它们把XMM*寄存器扩充为256位寄存器。
调试寄存器(Debugging registers)用于实现基于硬件的断点控制。
DR0为第1个断点的地址(线性地址)。
DR1为第2个断点的地址。
DR2为第3个断点的地址。
DR3为第4个断点的地址。
DR6为调试状态寄存器。在调试过程异常时,它负责报告产生异常的原因。
DR7为用于控制断点调试。
位序(掩码) |
描 述 |
---|---|
0(1) |
B0:触发了断点DR0 |
1(2) |
B1:触发了断点DR1 |
2(4) |
B2:触发了断点DR2 |
3(8) |
B3:触发了断点DR3 |
13(0x2000) |
BD:仅在DR7的GD为1的情况下有效。只有当下一条指令要访问到某一个调试寄存器的时候,BD位才被置位(1) |
14(0x4000) |
BS:当进行单步调试的时候,即EFLAGS的TF标识位被置位的时候,BS才被置位。单步调试具有最高的调试优先级,不受其他标识位影响 |
15(0x8000) |
BT:任务切换标识位 |
单步调试断点是在执行一条指令之后发生的断点。设置EFLAGS(附录A.2.19)的TF标识,即可实现单步调试。
DR7用于控制断点类型。
位序(掩码) |
描 述 |
---|---|
0(1) |
L0:在当前任务的DR0处设置断点 |
1(2) |
G0:在所有任务中都设置DR0的断点 |
2(4) |
L1:在当前任务的DR1处设置断点 |
3(8) |
G1:在所有任务中都设置DR1的断点 |
4(0x10) |
L2:在当前任务的DR2处设置断点 |
5(0x20) |
G2:在所有任务中都设置DR2的断点 |
6(0x40) |
L3:在当前任务的DR3处设置断点 |
7(0x80) |
G3:在所有任务中都设置DR3的断点 |
8(0x100) |
LE:P6以及P6以后的处理器不支持这个标识位。如果被置位,那么FPU将会在当前任务中追踪精确的数据断点 |
9(0x200) |
GE:P6以及P6以后的处理器不支持这个标识位。如果被置位,那么FPU将会在所有任务中追踪精确的数据断点 |
13(0x2000) |
GD:如果置位,那么当MOV指令修改DRx寄存器的值时,FPU将进行进行异常处理 |
16,17(0x3000) |
断点DR0的触发条件 |
18,19(0xC000) |
断点DR0的断点长度 |
20,21(0x30000) |
断点DR1的触发条件 |
22,23(0xC0000) |
断点DR1的断点长度 |
24,25(0x300000) |
断点DR2的触发条件 |
26,27(0xC00000) |
断点DR2的断点长度 |
28,29(0x3000000) |
断点DR3的触发条件 |
30,31(0xC000000) |
断点DR3的断点长度 |
其中,断点DRx的触发条件又分为:
00:执行指令。
01:数据的写操作。
10:读写I/O(User mode 下不可用)。
11:读写数据。
可见,FPU断点的触发条件里没有“读取数据”这一项。
FPU断点长度的规格如下:
00:1个字节。
01:2个字节。
10:32位系统中未定义,64位系统中代表8字节。
11:4个字节。
标记为(M)的指令通常都不是编译器生成的指令。这种指令或许属于手写出来的汇编代码,或许属于编译器的内部指令(参见本书第90章)。
本节仅列举那些常见指令。如果需要查看完整的指令说明,请参见《Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes:1,2A,2B,2C,3A,3B,and 3C》和《AMD64 Architecture Programmer’s Manual》。
我们是不是也要记住指令的opcode呢?除非您专门从事给代码打补丁的工作(参见本书第89章第2节),否则没那种必要。
LOCK:数据总线封锁前缀。在执行LOCK作前缀的汇编指令时,它可起到独占数据总线的作用。简单地说,在执行这种指令时,多处理器的其他CPU都将停下来、等该指令执行结束。这种指令常见于各种关键系统、(硬件)信号量和互斥锁。
禁止协处理器修改数据总线上的数据,起到独占总线的作用。该指令的执行不影响任何标志位。它常作为ADD、AND、BTR、BTS、CMPXCHG、OR、XADD、XOR指令的前缀。本书的第68章第4节详细介绍了这种指令。
REP:与MOVSx和STOSx指令结合使用,以循环的方式进行数据复制及数据存储。在执行REP指令时,CX/ECX/RCX寄存器里存储的值将作为隐含的循环计数器。有关MOVSx和STOSx指令的详细说明,请参见附录A.6.2。
REP指令属于DF敏感指令。DF标识位决定了它的操作方向。
REPE/REPNE:(又称为REPZ/REPNZ)与CMPSx和SCASx指令结合使用,以循环的方式进行数值比较。在执行这种指令时,CX/ECX/RCX 寄存器里存储的值将作为隐含的循环计数器。当ZF标识位为0(REPE),或ZF标识位为1(REPNE)时,它将终止循环过程。
有关CMPSx和SCASx的详细描述,请参见附录A.6.2和A.6.3。
REPE/REPNE指令属于DF敏感指令。DF标识位决定了它的操作方向。
ADC(进位加法运算):在进行加法运算时,会把 CF 标识位代表的进位加入和中。它常见于较大数值的加法运算。例如,在32位系统进行64位数值的加法运算时,会组合使用ADD和ADC指令,如下所示:
; 64位值的运算:val2= val1 + val2.
; .lo 代表低32位,.hi代表高32位。
ADD val1.lo, val2.lo
ADC val1.hi,val2.hi;会使用上一条指令设置的CF
本书的第24章有更为详细的使用案例。
ADD:加法运算指令。
AND:逻辑“与”运算指令。
CALL:调用其他函数。相当于“PUSH(CALL之后的返回地址);JMP label”。
CMP:比较数值、设置标识位。虽然它的运算过程确是减法运算,但是SUB指令保存运算结果(差)、而CMP指令不保存运算结果。
DEC:递减运算。它不影响CF标识位。
IMUL:有符号数的乘法运算指令。
INC:递增运算。它不影响CF标识位。
JCXZ,JECXZ,JRCXZ(M):当CX/ECX/RCX=0时跳转。
JMP:跳转到指定地址。相应的opcode中含有转移偏移量(jump offset)。
Jcc:条件转移指令。cc是condition code的缩写。
JAE即JNC:(unsigned)在大于或等于的情况下进行跳转;转移条件是CF=0。
JA即JNBE:(unsigned)在大于的情况下进行跳转;转移条件是CF=0且ZF=0。
JBE:(unsigned)在小于或等于的条件下进行跳转;转移条件是CF=1或ZF=1。
JB即JC:(unsigned)在小于的情况下进行跳转;转移条件是CF=1
JC即JB:在小于的情况下进行跳转;转移条件是CF=1。
JE即JZ:在相等的情况下进行跳转;转移条件是ZF=1。
JGE:(signed)在大于或等于的情况下进行跳转;转移条件是SF=OF。
JG:(signed)在大于的情况下进行跳转;转移条件是ZF=0且SF=OF。
JLE:(signed)在小于或等于的情况下进行跳转;转移条件是ZF=1或SF≠OF。
JL:(signed)在小于的条件下进行跳转;转移条件是SF≠OF。
JNAE即JC:(unsigned)在小于(不大于且不相等)的情况下进行跳转;转移条件是CF=1。
JNA:(unsigned)在不大于的情况下进行跳转;转移条件是CF=1或ZF=1。
JNBE:(unsigned)在大于的情况下进行转移;转移条件是CF=0且ZF=0。
JNB即JNC:(unsigned)在不小于的情况下进行跳转;转移条件是CF=0。
JNC即JAE:等同于JNB;转移条件是CF=0。
JNE即JNZ:在不相等的情况下进行跳转;转移条件是ZF=0。
JNGE:(signed)在不大于且不等于的情况下进行跳转;转移条件是SF≠OF。
JNG:(signed)在不大于的情况下进行跳转;转移条件是ZF=1或SF≠OF。
JNLE:(signed)在不大于且不相等的情况下进行跳转;转移条件是ZF=0且SF=OF。
JNL:(signed)在不小于的情况下进行跳转;转移条件是SF=OF。
JNO:在不溢出的情况下进行跳转;转移条件是OF=0。
JNS:在SF标识位为0的情况下进行跳转。
JNZ即JNE:在不大于且不等于的情况下进行跳转;转移条件是ZF=0
JO:在溢出的情况下进行跳转;转移条件是OF=1。
JPO:在PF标识位为零的情况下进行跳转。
JP即JPE:在PF标识位为1的情况下进行跳转。
JS:在SF标识位为1的情况下进行跳转。
JZ即JE:在操作数相等的情况下进行跳转;转移条件是ZF=1。
LAHF:标识位读取指令。它把标识位复制到AH寄存器。数权关系如下表所示。
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
SF
|
ZF
|
AF
|
PF
|
CF
|
LEAVE:等效于“MOV ESP,EBP”“POP EBP”指令的组合。即,这条指令释放当前子程序在堆栈中的局部变量,恢复栈指针(stack pointer/ESP)和EBP寄存器的初始状态。
LEA:有效(偏移)地址传送指令。
这个指令并非调用寄存器的值,也不会进行地址以外的求值运算。它可利用数组地址、元素索引号和元素空间进行混合运算,求得某个元素的有效地址。
所以,MOV和LEA指令有巨大的差别:MOV指令会把操作数的值当作地址、而后对这个地址的值进行读写操作;而LEA就对操作数的地址进行直接处理。
因此,LEA指令也经常用于各种常规计算。
LEA指令有一个重要的特点——它不会影响CPU标识位的状态。对于OOE(乱序方式执行的指令)处理器来说,这一特性有利于大幅度降低数据依赖性。
int f(int a, int b)
{
return a*8+b;
};
使用MSVC 2010(启用优化功能)编译上述程序,可得到:
指令清单A.1 优化MSVC 2010
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_f PROC
mov eax, DWORD PTR _b$[esp-4]
mov ecx, DWORD PTR _a$[esp-4]
lea eax, DWORD PTR [eax+ecx*8]
ret 0
_f ENDP
Intel C++编译器生成的汇编代码更为烦琐。例如,在编译下述源代码时:
int f1(int a)
{
return a*13;
};
Intel C++生成的汇编代码为:
指令清单A.2 Intel C++2011
_f1 PROC NEAR
mov ecx, DWORD PTR [4+esp] ; ecx = a
lea edx, DWORD PTR [ecx+ecx*8] ; edx = a*9
lea eax, DWORD PTR [edx+ecx*4] ; eax = a*9 + a*4 = a*13
ret
即使如此,两条LEA指令的执行效率仍然超过了单条IMUL指令。
MOVSB/MOVSW/MOVSD/MOVSQ:复制8位单字节数据(Byte)/16位Word数据(Word)/32位双字型数据(Dword)/64位四字型数据(Qword)的指令。默认情况下,它将把SI/ESI/RSI寄存器里的值当作源操作数的地址,目标操作数的地址将取自DI/EDI/RDI寄存器。
在与REP前缀组合使用时,它会把CX/ECX/RCX作为循环控制变量进行循环操作。这种情况下,它就像C语言中的memcpy() 函数那样工作。如果编译器在编译阶段能够确定每个模块的大小,编译器通常使用REP MOVSx指令以内连函数的形式实现memcpy()。
例如,memcpy(EDI,ESI,15)等效于:
; copy 15 bytes from ESI to EDI
CLD ; set direction to "forward"
MOV ECX, 3
REP MOVSD ; copy 12 bytes
MOVSW ; copy 2 more bytes
MOVSB ; copy remaining byte
在复制15字节的内容时,从寄存器读取的操作效率来看,上述代码的效率要高于15次数据读写(MOVSB)的操作效率。
MOVSX:以符号扩展的方法实现signed型数据的类型转换(参见本书15.1.1节)。
MOVZX:以用零扩展的方法实现unsigned型数据的类型转换(参见本书15.1.1节)。
MOV:数据传送指令。“MOV”这个名字与“MOVE”(移动)拼写相似,不过它的功能是复制数据而非移动数据。在某些平台上,这条指令的名字是“LOAD”或者某个类似的名字。
值得一提的是,当使用MOV指令给32位寄存器的低16位赋值时,寄存器的高16位不会发生变化。而使用MOV指令给64位寄存器的低32位赋值时,寄存器的高32位会被清零。
64位寄存器的高32位被自动清零的特性,可能是为了在x86-64系统上兼容32位程序而有意这样设计的。
MUL:unsigned型数据的乘法运算指令。
NEG:求补指令(并非补码计算指令)。NEG op可得到–op。
NOP:NOP指令。在x86平台上的opcode是0x90。这个opcode和XCHG EAX,EAX的空操作指令相同。这即是说,x86平台没有NOP专用的汇编指令,而RISC平台上NOP有专用的汇编指令。有关这个指令的详细介绍,请参见本书的第88章。
编译器可能会使用 NOP 指令进行 16 字节边界对齐。此外,在手工修改程序时,人们也会使用 NOP指令进行指令替换,用于屏蔽条件转移之类的汇编指令。
NOT:求反指令/逻辑“非”运算指令。
OR:逻辑“或”运算指令。
POP:出栈指令。它从SS:[ESP]中取值,再执行ESP=ESP+4(或8)的操作。
PUSH:入栈指令。它先进行ESP=ESP–4(或8),再向地址SS:[ESP]存储数据。
RET:子程序返回函数,相当于POP tmp或JMP tmp。
实际上RET是汇编语言的宏。在Windows和*NIX环境中,它会被解释为RETN(“return near”);在MS-DOS的寻址方式里,它被解释为RETF(参见本书第94章)。
RET指令可以有操作数。在这种情况下,它等同于POP tmp、ADD ESP op1及JMP tmp。在符合调用约定stdcall的程序里,每个函数最后的RET指令通常都有相应的操作数。有关细节请参见本书的64.2。
SAHF:标识传送指令。它把AH寄存器的值,复制到CPU到标识上。对应的数权关系如下表所示。
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
SF
|
ZF
|
AF
|
PF
|
CF
|