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:逻辑左移指令。
SHR:逻辑右移指令。
指令常用于乘以/除以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操作非常重要的特点,应当熟练掌握。
BSF:顺向位扫描指令。详情请参见本书25.2节。
BSR:逆向位扫描指令。
BSWAP:重新整理字节次序的指令。它以字节为单位逆序重新排列字节序,用于更改数据的字节序。
BTC:位测试并取反的指令。
BTR:位测试并清零的指令。
BTS:位测试并置位的指令。
BT:位测试指令。
CBW/CWD/CWDE/CDQ/CDQE:signed型数据的类型转换指令:
CBW:把AL中的字节(byte)型数据转换为字(word)型数据,存储于AX寄存器。
CWD:把AX中的字(word)型数据转换为双字(Dword)型数据、存储于DX-AX寄存器对。
CWDE:把AX中的字(word)型数据转换为双字字(Dword)型数据,存储于DAX寄存器。
CDQ:把EAX中的双字(word)型数据转换为四字(Qword)型数据,存储于EDX:EAX寄存器对。
CDQE(x64指令):把EAX中对双字(Dword)型数据转换为四字(Qword)型数据,并存储于RAX寄存器。
上述五个指令均能正确处理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标识位实现。
RCR(M):带进位的循环右移指令,通过CF标识位实现。
ROL/ROR(M):循环左/右移。
几乎所有的CPU都有这些循环位移指令。但是C/C++语言里没有相应的操作指令,所以它们的编译器不会生成这些指令。
为了便于编程人员使用这些指令,至少MSVC提供了相应的伪函数(complier intrinsics)_rotl() 和_rotr(),可直接翻译这些指令。详情请参见:http://msdn.microsoft.com/en-us/library/ 5cc576c4.aspx
。
SAL:算术左移指令,等同于逻辑左移SHL指令。
SAR:算术右移指令。
本指令通常用于对带符号数减半的运算中,因而在每次右移时,它保持最高位(符号位)不变,并把最低位右移至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):未定义的指令,会产生异常信息,多用于软件测试。
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),ST(i):加法运算指令。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 ST(i),ST(j):ST(i)=ST(j)/ST(i)。
FDIVRP op:ST(0)=op/ST(0),然后执行1次出栈操作。
FDIVRPP ST(i),ST(j):ST(i)=ST(j)/ST(i),然后执行2次出栈操作。
FDIV op:ST(0)=ST(0)/op。
FDIV ST(i),ST(j):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 ST(i),ST(j):ST(i)=ST(i)*ST(j)。
FMULP op:ST(0)=ST(0)*op;然后执行1次出栈操作。
FMULP ST(i),ST(j):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:。
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 ST(0),ST(i):ST(0)=ST(i)−ST(0)。
FSUBRP:ST(1)=ST(0)−ST(1),然后执行1次出栈操作。即被减数替换为差。
FSUB op:ST(0)=ST(0)−op。
FSUB ST(0),ST(i):ST(0)=ST(0)−ST(i)。
FUSBP ST(1):ST(1)=ST(1)−ST(0)。
FUCOM ST(i):比较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 ST(i):交换ST(0)和ST(i)的数据。
FXCH:交换ST(0)和ST(1)的数据。
在构建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。