SP是栈指针Stack Pointer的缩写。在初始化之后,它是当前栈地址的指针。

A.2.16 RBP/EBP/BP/BPL
7
6
5
4
3
2
1
0
RBP
EBP
BP
BPL

帧指针(Frame Pointer),通常是局部变量的指针。在调用函数时,它也常常用来传递参数。有关这个寄存器的详细介绍,请参照本书的7.1.2节。

A.2.17 RIP/EIP/IP
7
6
5
4
3
2
1
0
RIPx64
EIP
IP

指令指针instruction pointer应当总是指向接下来将要执行的那条指令。正常情况下,无法直接干预它的值。但是,下述指令可以等效地实现调整指令指针的功能:

MOV EAX,  ...
JMP EAX
或者:
PUSH Value
RET
A.2.18 段地址寄存器CS/DS/ES/SS/FS/GS

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章。

A.2.19 标识寄存器

标识寄存器即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

其余的标识位都是保留标识位。

A.3 FPU寄存器

FPU栈由8个80位寄存器构成,这8个寄存器分别叫作ST(0)~ST(7)。IDA把ST(0)显示为ST。FPU寄存器用于存储符合IEEE 754标准的long double型数据。这种数据的格式如下表所示。

第79位

第78-64位

第63位

第62-0位

符号位

指数位

整数位

尾数(小数)位

A.3.1 控制字寄存器(16位)

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)
01:保留
10:IEEE双精度53位(REAL8)
11:IEEE扩展双精度64位(REAL10)

10,11

RC(舍入控制)

00:就近舍入(默认)
01:向−∞舍入
10:向+∞舍入
11:截断(向零舍入)

12

IC(无限/∞控制位)

0:按照unsigned处理±∞(初始态)
1:按照signed处理∞

若PM、UM、OM、ZM、DM、IM字段(第0~5位)设置为1,则由FPU处理异常信息(对软件屏蔽了错误信息);若某位设置为0,则FPU将会在遇到相应异常时进行中断、释放异常信息给应用程序,程序在处理之后再把控制权返还给FPU。

A.3.2 状态字寄存器(16位)

FPU的状态寄存器又称Fstate,属于只读寄存器。

位序

缩写(及含义)

描 述

15

B(忙)

1:FPU正在进行运算
0: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的模(余数),以此确定栈指针的物理寄存器地址。

A.3.3 标记字寄存器(16位)

标志字寄存器总共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]的状态码。

其各值的代表含义是:

A.4 SIMD寄存器

A.4.1 MMX寄存器

MMX寄存器由8个64位寄存器(MM0~MM7)组成。

A.4.2 SSE与AVX寄存器

SSE都有XMM0~XMM7这8个128位寄存器,x86-64系统还有额外的8个寄存器(XMM8~XMM15)。

而支持AVX指令集的CPU,它们把XMM*寄存器扩充为256位寄存器。

A.5 FPU调试寄存器

调试寄存器(Debugging registers)用于实现基于硬件的断点控制。

A.5.1 DR6规格

位序(掩码)

描  述

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标识,即可实现单步调试。

A.5.2 DR7规格

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的触发条件又分为:

可见,FPU断点的触发条件里没有“读取数据”这一项。

FPU断点长度的规格如下:

A.6 指令

标记为(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节),否则没那种必要。

A.6.1 指令前缀

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标识位决定了它的操作方向。

A.6.2 常见指令

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