SBB:借位减法运算指令。计算出操作数间的差值之后,如果CF标识位为1,则将差值递减。SBB指令常见于大型数据的减法运算。例如,在32位系统上计算2个64位数据的差值时,编译器通常组合使用SUB和SBB指令:

; 计算两个 64位数的差值;val1=val1-val2
; .lo 代表低 32 位; .hi 代表高32位
SUB val1.lo, val2.lo
SBB val1.hi, val2.hi ; 使用了前一个指令设置的CF标识位

本书第24章还有更详细的介绍。

SCASB/SCASW/SCASD/SCASQ(M):比较8位单字节数据(Byte)/16位Word数据(Word)/32位双字型数据(Dword)/64位四字型数据(Qword)的指令。它将AX/EAX/RAX寄存器里的值当作源操作数,另一个操作数则取自DI/EDI/RDI寄存器。在比较结果之后再设置标识位,设置标识位的方式和CMP指令相同。

这些指令通常与REPNE指令前缀组合使用。在这种情况下,组合指令将把寄存器AX/EAX/RAX里存储的值当作关键字,在缓冲区里进行搜索。此时,REPNE里的NE就意味着:如果值不相等,则继续进行比较(搜索)。

这种指令常常用于实现strlen() 函数,以判明ASCIIZ型字符串的长度。例如:

lea      edi, string
mov      ecx, 0FFFFFFFFh ; 扫描232−1 bytes, 即, 接近“无限”
xor      eax, eax        ; 0作终止符
repne scasb
add      edi, 0FFFFFFFFh ; 修正

; 现在,EDI寄存器的值指向里ASCIIZ字符串的最后一个字符

; 接下来将计算字符串的长度
; 目前 ECX = -1-strlen

not      ecx
dec      ecx

; 此后,ECX的值是字符串的长度值

如果AX/EAX/RAX里存储的值不同,那么这个函数的功能就和标准C函数memchr() 一致,即搜索特定byte。

SHL:逻辑左移指令。

..\tu\a01.tif

SHR:逻辑右移指令。

..\tu\a02.tif

指令常用于乘以/除以2n乘除运算。此外,它们还常见于字段处理的各种实践方法(请参见本书第19章)。

SHRD op1,op2,op3:双精度右移指令。把op2右移op3位,移位引起的空缺位由op1的相应位进行补足。详细介绍,请参见本书第24章。

STOSB/STOSW/STOSD/STOSQ:8位单字节数据(Byte)/16位Word数据(Word)/32位双字型数据(Dword)/64位四字型数据(Qword)的输出指令。它把AX/EAX/RAX的值当作源操作数,存储到DI/EDI/RDI为目的串地址指针所寻址的存储器单元中去。指针DI将根据DF的值进行自动调整。

在与REP前缀组合使用时,它将使用CX/ECX/RCX寄存器作循环计数器、进行多次循环;工作方式与C语言到memset() 函数相似。如果编译器能够在早期阶段确定区间大小,那么将通过REP MOVSx指令实现memset() 函数,从而减少代码碎片。

例如,memset(EDI,0xAA,15)对应的汇编指令是:

;在EDI中存储15个 0xAA
CLD                     ; set direction to "forward"
MOV EAX, 0AAAAAAAAh
MOV ECX, 3
REP STOSD              ; write 12 bytes
STOSW                  ; write 2 more bytes
STOSB                  ; write remaining byte

在复制15字节的内容时,从寄存器读取的操作效率来看,上述代码的效率要高于15次数据读写(REP STOSB)的操作效率。

SUB:减法运算指令。常见的“SUB寄存器,寄存器”指令可进行寄存器清零。

TEST:测试指令。在设置标识位方面,它和AND指令相同,但是它不存储逻辑与的运算结果。详细介绍请参见本书的第19章。

XCHG:数据交换指令。

XOR op1,op2:逻辑异或运算指令。常见的“XOR reg,reg”用于寄存器清零。

XOR指令普遍用于“翻转”特定比特位。无论以哪个操作符作为源数据,只要另外一个操作符(参照数据)的指定位为1,那么运算结果里的那一位数据都是源数据相应位的非值。

输入A

输入B

运 算 结 果

0

0

0

0

1

1

1

0

1

1

1

0

另外,如果参照数据的对应位为0,那么运算结果里的那一位数据都是源数据相应位的原始值。这是XOR操作非常重要的特点,应当熟练掌握。

A.6.3 不常用的汇编指令

BSF:顺向位扫描指令。详情请参见本书25.2节。

BSR:逆向位扫描指令。

BSWAP:重新整理字节次序的指令。它以字节为单位逆序重新排列字节序,用于更改数据的字节序。

BTC:位测试并取反的指令。

BTR:位测试并清零的指令。

BTS:位测试并置位的指令。

BT:位测试指令。

CBW/CWD/CWDE/CDQ/CDQE:signed型数据的类型转换指令:

上述五个指令均能正确处理signed型数据中对符号位、对高位进行正确的填补。详情请参见本书的第24章第5节。

CLD清除DF标识位。

CLI(M):清除IF标识位。

CMC(M):变换CF标识位。

CMOVcc:条件赋值指令。如果满足相应的条件代码(cc),则进行赋值。有关条件代码cc的各种代表意,请参见前文A.6.2对Jcc的详细说明。

CMPSB/CMPSW/CMPSD/CMPSQ(M):比较8位单字节数据(Byte)/16位Word数据(Word)/32位双字型数据(Dword)/64位四字型数据(Qword)的指令。它将SI/ESI/RSI寄存器里的值当作源操作数的地址,另一个操作数的地址则取自DI/EDI/RDI寄存器。在比较结果之后再设置标识位,设置标识位的方式和CMP指令相同。

这些指令通常与指令前缀REP组合使用。在这种情况下,组合指令将CX/ECX/RCX寄存器的值当作循环计数器进行多次循环比较,直至ZF标识位为零。也就是说,它也常用作字符比较(或搜索)。

它的工作方式和C语言的memcmp() 函数相同。

以Windows NT内核(WindowsResearchKernel v1.2)为例,base\ntos\rtl\i386\ movemem.asm代码如下所示。

指令清单A.3 base\ntos\rtl\i386\movemem.asm

; ULONG
; RtlCompareMemory (
;     IN PVOID Source1,
;     IN PVOID Source2,
;     IN ULONG Length
;     )
;
; Routine Description:
;
;     This function compares two blocks of memory and returns the number
;     of bytes that compared equal.
;
; Arguments:
;
;     Source1 (esp+4) - Supplies a pointer to the first block of memory to
;         compare.
;
;     Source2 (esp+8) - Supplies a pointer to the second block of memory to
;         compare.
;
;     Length (esp+12) - Supplies the Length, in bytes, of the memory to be
;         compared.
;
; Return Value:
;
;     The number of bytes that compared equal is returned as the function
;     value. If all bytes compared equal, then the length of the original
;     block of memory is returned.
;
;--

RcmSource1       equ      [esp+12]
RcmSource2       equ      [esp+16]
RcmLength        equ      [esp+20]

CODE_ALIGNMENT
cPublicProc _RtlCompareMemory,3
cPublicFpo 3,0

         push   esi                           ; save registers
         push   edi
         cld                                  ; clear direction
         mov    esi,RcmSource1                ; (esi) -> first block to compare
         mov    edi,RcmSource2                ; (edi) -> second block to compare

;
;     Compare dwords, if any.
;

rcm10:  mov     ecx,RcmLength                ; (ecx) = length in bytes
        shr     ecx,2                        ; (ecx) = length in dwords
        jz      rcm20                        ; no dwords, try bytes
        repe    cmpsd                        ; compare dwords
        jnz     rcm40                        ; mismatch, go find byte

;
;  Compare residual bytes, if any.
;

rcm20:  mov     ecx,RcmLength               ; (ecx) = length in bytes
        and     ecx,3                       ; (ecx) = length mod 4
        jz      rcm30                       ; 0 odd bytes, go do dwords
        repe    cmpsb                       ; compare odd bytes
        jnz     rcm50                       ; mismatch, go report how far we got

;
;  All bytes in the block match.
;

rcm30:  mov      eax,RcmLength              ; set number of matching bytes
        pop      edi                        ; restore registers
        pop      esi                        ;
        stdRET   _RtlCompareMemory

;
;  When we come to rcm40, esi (and edi) points to the dword after the
;  one which caused the mismatch.  Back up 1 dword and find the byte.
;  Since we know the dword didn't match, we can assume one byte won't.
;

rcm40:  sub     esi,4                       ; back up
        sub     edi,4                       ; back up
        mov     ecx,5                       ; ensure that ecx doesn't count out
        repe    cmpsb                       ; find mismatch byte

;
;  When we come to rcm50, esi points to the byte after the one that
;  did not match, which is TWO after the last byte that did match.
;

rcm50:  dec     esi                         ; back up
        sub     esi,RcmSource1              ; compute bytes that matched
        mov     eax,esi                     ;
        pop     edi                         ; restore registers
        pop     esi                         ;
        stdRET  _RtlCompareMemory

stdENDP _RtlCompareMemory

当内存块的尺寸是4的倍数时,这个函数将使用32位字型数据的比较指令CMPSD,否则使用逐字比较指令CMPSB。

CPUID:返回CPU信息的指令。详情请参见本书的21.6.1节。

DIV:无符号型数据的除法指令。

IDIV:有符号型数据的除法指令。

INT(M):INT x的功能相当于16位系统中的PUSHF;CALL dwordptr[x*4]。在MS-DOS中,INT指令普遍用于系统调用(syscall)。它调用AX/BX/CX/DX/SI/DI寄存器里的中断参数,然后跳转到中断向量表[2]。因为INT的opcode很短(2字节),而且通过中断调用MS-DOS服务的应用程序不必去判断系统服务的入口地址,所以INT指令曾盛行一时。中断处理程序通过使用IRET指令即可返回程序的控制流。

最常被调用的MS-DOS中断是第0x21号中断,它负责着大量的API接口。有关MS-DOS各中断的完整列表,请参见Ralf Brown撰写的《The x86 Interrupt List》[3]

在MS-DOS之后,早期的Linux和Windows(参见本书第66章)系统仍然使用INT指令进行系统调用。近些年来,它们逐渐使用SYSENTER或SYSCALL指令替代了INT指令。

INT 3(M):这条指令有别于其他的INT指令。它的opcde只有1个字节,即(0xCC),普遍用于程序调试。一般来说,调试程序就是在需要进行调试的断点地址写上0xCC(opcode替换)。当被调试程序执行 INT3 指令而导致异常时,调试器就会捕捉这个异常从而停在断点处,然后将断点处的指令恢复成原来指令。

在Windows NT系统里,当CPU执行这条指令时,系统将会抛出EXCEPTION_BREAKPOINT异常。如果运行了主机调试程序/ host debugger,那么这个调试事件将会被主机调试程序拦截并处理;否则,Windows系统将会调用系统上注册了的某个调试器/system debugger进行响应。如果安装了MSVS(Microsoft Visual Studio),在执行INT3时,Windows可能会启动MSVS的debugger,继而调试这个进程。这种调试方法改变了原程序的指令,容易被软件检测到。人们开发出了很多反调试技术,通过检查加载代码的完整性防止他人进行逆向工程的研究。

MSVC编译器有INT3对应的编译器内部函数——__debugbreak()。[4]

Kernel32.dll里还有win32的系统函数DebugBreak(),专门执行INT 3。[5]

IN(M):数据输入指令,用于从外设端口读取数据。这个指令常用于OS驱动程序和MS-DOS的应用程序。详细介绍请参见本书的78.3节。

IRET:在MS-DOS环境中调用INT中断之后,IRET指令负责返还中断处理程序(interrupt handler)。它相当于POP tmp;POPF;JMP tmp。

LOOP(M):递减计数器CX/ECX/RCX,在计数器不为零的情况下进行跳转。

OUT(M):数据输出指令,用于向外设端口传输数据。这个指令常用于OS驱动程序和MS-DOS的应用程序。详细介绍请参见本书的78.3节。

POPA(M):从数据栈中读取(恢复)(R|E)DI、(R|E)SI、(R|E)BP、(R|E)BX、(R|E)DX、(R|E)CX、(R|E)AX寄存器的值。

POPCNT:它的名称是“population count”的缩写。该指令一般翻译为“位1计数”。既是说,它负责统计有多少个“为1的位”。它的英文外号称为“hamming weight”和“NSA指令”。这种外号来自于下述轶闻(Bruce Schneier《Applied Cryptography:Protocols,Algorithms,and Source Code in C》1994.):

This branch of cryptography is fast-paced and very politically charged. Most designs are secret; a majority of military encryptions systems in use today are based on LFSRs. In fact, most Cray computers (Cray 1, Cray X-MP, Cray Y-MP) have a rather curious instruction generally known as “population count.” It counts the 1 bits in a register and can be used both to efficiently calculate the Hamming distance between two binary words and to implement a vectorized version of a LFSR. I’ve heard this called the canonical NSA instruction, demanded by almost all computer contracts.

更多信息请参阅参考资料[Sch94]。

POPF:从数据栈中读取标识位,即恢复EFLAGS寄存器。

PUSHA(M):把(R|E)AX、(R|E)CX、(R|E)DX、(R|E)BX、(R|E)BP、(R|E)SI、(R|E)DI寄存器的值,依次保存在数据栈里。

PUSHF:把标识位保存到数据栈里,即存储EFLAGS寄存器的值。

RCL(M):带进位的循环左移指令,通过CF标识位实现。

..\tu\a03.tif

RCR(M):带进位的循环右移指令,通过CF标识位实现。

..\tu\a04.tif

ROL/ROR(M):循环左/右移。

..\tu\a05.tif
..\tu\a06.tif

几乎所有的CPU都有这些循环位移指令。但是C/C++语言里没有相应的操作指令,所以它们的编译器不会生成这些指令。

为了便于编程人员使用这些指令,至少MSVC提供了相应的伪函数(complier intrinsics)_rotl() 和_rotr(),可直接翻译这些指令。详情请参见:http://msdn.microsoft.com/en-us/library/ 5cc576c4.aspx

SAL:算术左移指令,等同于逻辑左移SHL指令。

SAR:算术右移指令。

..\tu\a07.tif

本指令通常用于对带符号数减半的运算中,因而在每次右移时,它保持最高位(符号位)不变,并把最低位右移至CF中。

SETcc op:在条件表达式cc为真的情况下,将目标操作数设置为1;否则设置目标操作数为0。这里的目标操作数指向一个字节寄存器(也就是8位寄存器)或内存中的一个字节。状态码后缀(cc)指代条件表达式,可参见附录A.6.2的有关介绍。

STC(M):设置CF标识位的指令。

STD(M):设置DF标识位的指令。编译器不会生成这种指令,因此十分罕见。我们可以在Windows的内核文件ntoskrnl.exe中找到这条指令,也可以在手写的内存复制的汇编代码里看到它。

STI(M):设置IF标识位的指令。

SYSCALL:(AMD)系统调用指令(参见本书第66章)。

SYSENTER:(Intel)系统调用指令(参见本书第66章)。

UD2(M):未定义的指令,会产生异常信息,多用于软件测试。

A.6.4 FPU指令

FPU指令有很多带有“-R”或“-P”后缀的派生指令。带有R后缀的指令,其操作数的排列顺序与常规指令相反。带有P后缀的指令,在运行计算功能后,会从栈里抛出一个数据;而带有PP后缀的指令则最后抛出两个数据。P/PP后缀的指令可在计算后释放栈里存储的计算因子。

FABS:计算ST(0)绝对值的指令。ST(0)=fabs(ST(0))。

FADD op:单因子加法运算指令。ST(0)=op+ST(0)。

FADD ST(0),STi:加法运算指令。ST(0)=ST(0)+ST(i)。

FADDP ST(1):相当于ST(1)=ST(0)+ST(1);pop。求和之后,再从数据栈里抛出1个因子。即,使用计算求得的“和”替换计算因子。

FCHS:求负运算指令。ST(0)= −1xST(0)

FCOM:比较ST(0)和ST(1)。

FCOM op:比较ST(0)和op。

FCOMP:比较ST(0)和ST(1);然后执行1次出栈操作。

FCOMPP:比较ST(0)和ST(1);然后执行2次出栈操作。

FDIVR op:ST(0)=op/ST(0)。

FDIVR STi),STj:ST(i)=ST(j)/ST(i)。

FDIVRP op:ST(0)=op/ST(0),然后执行1次出栈操作。

FDIVRPP STi),STj:ST(i)=ST(j)/ST(i),然后执行2次出栈操作。

FDIV op:ST(0)=ST(0)/op。

FDIV STi),STj:ST(i)=ST(i)/ST(j)。

FDIVP:ST(1)=ST(0)/ST(1),然后执行1次出栈操作。即,被除数替换为商。

FILD op:将整数转化为长精度数据,并存入ST(0)的指令。

FIST op:将st(0)以整数保存到op。

FISTP op:将st(0)以整数保存到op,再从栈里抛出ST(0)。

FLD1:把1推送入栈。

FLDCW op:从16位的操作数op里提取FPU控制字(参见附录A.3)。

FLDZ:把0推送入栈。

FLD op:把op推送入栈。

FMUL op:ST(0)=ST(0)*op。

FMUL STi),STj:ST(i)=ST(i)*ST(j)。

FMULP op:ST(0)=ST(0)*op;然后执行1次出栈操作。

FMULP STi),STj:ST(i)=ST(i)*ST(j);然后执行1次出栈操作。

FSINCOS:一次计算Sine和Cosine结果的指令。

调用指令时,ST(0)存储着角度参数tmp。ST(0)=sin(tmp);PUSH ST(0)(即ST1存储Sin值);之后再计算ST(0)=cos(tmp)。

FSQRT ST(0)=\sqrt{ST(0)}

FSTCW op:检查尚未处理的、未被屏蔽的浮点异常,再将FPU的控制字(参见附录A.3)保存到op。

FNSTCW op:将FPU的控制字(参见附录A.3)直接保存到op。

FSTSW op:检查尚未处理的、未被屏蔽的浮点异常,再将FPU的状态字(参见附录A.3)保存到op。

FNSTSW op:将状态字(参见附录A.3)直接保存到op。

FST op:保存实数ST(0) 到op。

FSTP op:将ST(0) 复制给op,然后执行1次出栈操作(ST(0))。

FSUBR op:ST(0)=op−ST(0)。

FSUBR ST0),STi:ST(0)=ST(i)−ST(0)。

FSUBRP:ST(1)=ST(0)−ST(1),然后执行1次出栈操作。即被减数替换为差。

FSUB op:ST(0)=ST(0)−op。

FSUB ST0),STi:ST(0)=ST(0)−ST(i)。

FUSBP ST1:ST(1)=ST(1)−ST(0)。

FUCOM STi:比较ST(0)和ST(i)。

FUCOM:比较ST(0)和ST(1)。

FUCOMP:比较ST(0)和ST(1),然后执行1次出栈操作。

FUCOMPP:比较ST(0)和ST(1),然后从栈里抛出2个数据。

上述两个指令与FCOM的功能相似,但是它们在处理QNaN型数据时不会报错,仅在处理SNaN时进行异常汇报。

FXCH STi:交换ST(0)和ST(i)的数据。

FXCH:交换ST(0)和ST(1)的数据。

A.6.5 可屏显的汇编指令(32位)

在构建Shellcode时(参见本书第82章),可能会用到下面这个速查表。

ASCII字符

16进制码

x86指令

0

30

XOR

1

31

XOR

2

32

XOR

3

33

XOR

4

34

XOR

5

35

XOR

7

37

AAA

8

38

CMP

9

39

CMP

:

3A

CMP

;

3B

CMP

<

3C

CMP

=

3D

CMP

?

3F

AAS

@

40

INC

A

41

INC

B

42

INC

C

43

INC

D

44

INC

E

45

INC

F

46

INC

G

47

INC

H

48

DEC

I

49

DEC

J

4A

DEC

K

4B

DEC

L

4C

DEC

M

4D

DEC

N

4E

DEC

O

4F

DEC

P

50

PUSH

Q

51

PUSH

R

52

PUSH

S

53

PUSH

T

54

PUSH

U

55

PUSH

V

56

PUSH

W

57

PUSH

X

58

POP

Y

59

POP

Z

5A

POP

[

5B

POP

\

5C

POP

]

5D

POP

^

5E

POP

_

5F

POP

`

60

PUSHA

a

61

POPA

f

66

32位运行模式下,把操作数切换为16位

g

67

32位运行模式下,把操作数切换为16位

h

68

PUSH

i

69

IMUL

j

6a

PUSH

k

6b

IMUL

p

70

JO

q

71

JNO

r

72

JB

s

73

JAE

t

74

JE

u

75

JNE

v

76

JBE

w

77

JA

x

78

JS

y

79

JNS

z

7A

JP

总之,存在对应ASCII字符的指令有AAA、AAS、CMP、DEC、IMUL、INC、JA、JAE、JB、JBE、JE、JNE、JNO、JNS、JO、JP、JS、POP、POPA、PUSH、PUSHA和XOR。


[1] 请注意,Tag(x)描述的不是FPU逻辑寄存器ST(x)的状态。

[2] Interrupt Vector Table。它位于地址空间的开头部分,是实模式中断机制的重要组成部分,表中记录所有中断号对应的中断服务程序的内存地址。

[3] http://www.cs.cmu.edu/~ralf/files.html。

[4] 编译器内部函数指compiler intrinsic,本书有详细介绍。它属于编译器有关的内部函数,基本不会被常规库函数调用。编译器为其生成特定的机械码,并非直接调用它的函数。内部函数常用于实现与特定CPU有关的伪函数。_debugbreak的介绍请参见http://msdn.microsoft.com/en-us/ library/f408b4et.aspx。

[5] 请参见http://msdn.microsoft.com/en-us/library/windows/desktop/ms679297%28v= vs.85%29.aspx。