逆向工程——汇编基础(三)

[TOC]

参考文档:《简明 x86 汇编语言教程》作者:司徒彦南
##操作内存
我们可以将内存想象为一个顺序的字节流。操作内存是,首先需要的就是他的地址。

1
mov ax,[0]

方括号表示,里面的表达式指定的不是立即数,而是偏移量。在是模式中,DS:0中的那个会被装入AX。

1
mov [0],ax

mov指令也可以把数据保存到内存中。
###描述内存宽度的操作符

操作符 意义
byte ptr 一个字节(8-bit,1 byte)
word ptr 一个字(16-bit)
dword ptr 一个双字(32-bit)

例如,在DS:100h处保存1234h,以字存放:

1
mov word ptr [100h],01234h

于是,我们将mov指令扩展为:

1
2
3
mov reg(8,16,32),mem(8,16,32)
mov mem(8,16,32),reg(8,16,32)
mov mem(8,16,32),imm(8,16,32)

需要说明的是,加减同样也可以在[](取地址操作)中使用,例如:

1
2
3
mov ax,[bx+10]
mov ax,[bx+si]
mov ax,es:[di+bp]

###串操作
前面说到,内存可以和寄存器交换数据,也可以被赋予立即数。问题是,如果我们需要把内存的某部分内容复制到另一个地址,要怎么操作呢?
设想将DS:SI处的连续512字节内容复制到ES:DI(不考虑可能的重叠):

1
2
3
4
5
6
			mov cx,512			;循环次数
NextByte: mov al,ds:[si]
mov es:[si],al
inc si
inc di
loop NextByte

这是一种不太完美的解决方法,因为效率不好。
Intel的CPU的强项是串操作。所谓串操作就是由CPU去完成某一数量的重复的内存操作。需要说明的是,我们常用的KMP算法(用于匹配字符串的模式)的改进——Boyer算法,由于没有利用到串操作,因此在Intel的CPU上的效率并非最优。好的编译器往往可以利用Intel CPU的这一特性优化代码,然而,并非所有的时候他都能产生最好的代码。

某些指令可以加上REP前缀,这些指令通常被叫做串操作指令
举例来说,STOSD指令将EAX的内容保存到ES:DI,同时在DI上加/减4STOSBSTOSW分别作1字节1字的操作,在DI上加/减的数是12
在DI上是加还是减,取决于DF标志,如果DF被复位,则加;反之则减。

置位复位的指令分别是STDCLD

除了REP,常用的前缀还包括REPNEREPZREPEREPNZ等。

操作码 指令 说明
F3 6C REP INS r/m8,DX 将 (E)CX个字节从端口输入到ES:[(E)DI]
F3 6D REP INS r/m16,DX 将 (E)CX 个字从端口 DX 输入到 ES:[(E)DI]
F3 6D REP INS r/m32,DX 将 (E)CX 个双字从端口 DX 输入到 ES:[(E)DI]
F3 A4 REP MOVS m8,m8 将 (E)CX 个字节从 DS:[(E)SI] 移到 ES:[(E)DI]
F3 A5 REP MOVS m16,m16 将 (E)CX 个字从 DS:[(E)SI] 移到 ES:[(E)DI]
F3 A5 REP MOVS m32,m32 将 (E)CX 个双字从 DS:[(E)SI] 移到 ES:[(E)DI]
F3 6E REP OUTS DX,r/m8 将 (E)CX 个字节从 DS:[(E)SI] 输出到端口 DX
F3 6F REP OUTS DX,r/m16 将 (E)CX 个字从 DS:[(E)SI] 输出到端口 DX
F3 6F REP OUTS DX,r/m32 将 (E)CX 个双字从 DS:[(E)SI] 输出到端口 DX
F3 AC REP LODS AL 将 (E)CX 个字节从 DS:[(E)SI] 加载到 AL
F3 AD REP LODS AX 将 (E)CX 个字从 DS:[(E)SI] 加载到 AX
F3 AD REP LODS EAX 将 (E)CX 个双字从 DS:[(E)SI] 加载到 EAX
F3 AA REP STOS m8 使用 AL 填写位于 ES:[(E)DI] 的 (E)CX 个字节
F3 AB REP STOS m16 使用 AX 填写位于 ES:[(E)DI] 的 (E)CX 个字
F3 AB REP STOS m32 使用 EAX 填写位于 ES:[(E)DI] 的 (E)CX 个双字
F3 A6 REPE CMPS m8,m8 在 ES:[(E)DI] 与 DS:[(E)SI] 中查找不匹配的字节
F3 A7 REPE CMPS m16,m16 在 ES:[(E)DI] 与 DS:[(E)SI] 中查找不匹配的字
F3 A7 REPE CMPS m32,m32 在 ES:[(E)DI] 与 DS:[(E)SI] 中查找不匹配的双字
F3 AE REPE SCAS m8 从 ES:[(E)DI] 开始查找非 AL 字节
F3 AF REPE SCAS m16 从 ES:[(E)DI] 开始查找非 AX 字
F3 AF REPE SCAS m32 从 ES:[(E)DI] 开始查找非 EAX 双字
F2 A6 REPNE CMPS m8,m8 在 ES:[(E)DI] 与 DS:[(E)SI] 中查找匹配字节
F2 A7 REPNE CMPS m16,m16 在 ES:[(E)DI] 与 DS:[(E)SI] 中查找匹配字
F2 A7 REPNE CMPS m32,m32 在 ES:[(E)DI] 与 DS:[(E)SI] 中查找匹配双字
F2 AE REPNE SCAS m8 从 ES:[(E)DI] 开始查找 AL
F2 AF REPNE SCAS m16 从 ES:[(E)DI] 开始查找 AX
F2 AF REPNE SCAS m32 从 ES:[(E)DI] 开始查找 EAX

####说明:
按计数寄存器 ((E)CX) 中指定的次数重复执行字符串指令,或是重复到 ZF 标志不再满足指定的条件。REP(重复)、REPE(相等时重复)、REPNE(不相等时重复)、REPZ(为零时重复)及 REPNZ(不为零时重复)助记符都是可以添加到一些字符串指令中的前缀。REP 前缀可以添加到 INS、OUTS、MOVS、LODS 及 STOS 指令,REPE、REPNE、REPZ 及 REPNZ 前缀可以添加到 CMPS 与 SCAS 指令。(REPZ 与 REPNZ 前缀分别是 REPE 与 REPNE 前缀的同义形式)。同非字符串指令一起使用时,REP 前缀的行为未定义。

REP 前缀一次只能应用于一条字符串指令。要重复指令块,请使用 LOOP 指令或其它循环结构。

所有这些重复前缀都会使关联的指令重复执行,直到寄存器 (E)CX 中的计数递减到 0(请参阅下表)。(如果当前地址大小属性为 32,则将寄存器 ECX 用作计数器;如果大小属性为 16,则将 CX 寄存器用作计数器)。在每次迭代之后,REPE、REPNE、REPZ 及 REPNZ 前缀还会检查 ZF 标志的状态,如果 ZF 标志未处于指定的状态,则终止重复循环。同时测试两个终止条件时,终止重复的原因可以通过使用 JECXZ 指令来测试 (E)CX 寄存器进行确定,也可以通过使用 JZ、JNZ 及 JNE 指令来测试 ZF 标志进行确定。

重复前缀 终止条件1 终止条件2
REP ECX=0
REPE/REPZ ECX=0 ZF=0
REPNE/REPNZ ECX=0 ZF=1
使用 REPE/REPZ 与 REPNE/REPNZ 前缀时,由于 CMPS 与 SCAS 指令都会根据它们的比较结果设置 ZF 标志,因此 ZF 标志不需要初始化。

所以,我们可以把上面的程序改写为如下:

1
2
3
cld			;复位DF
mov cx,128 ;512/4=128,共128个双字
rep movsd