逆向工程——汇编基础(三)
[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 | mov reg(8,16,32),mem(8,16,32) |
需要说明的是,加减同样也可以在[]
(取地址操作)中使用,例如:
1 | mov ax,[bx+10] |
###串操作
前面说到,内存可以和寄存器交换数据,也可以被赋予立即数。问题是,如果我们需要把内存的某部分内容复制到另一个地址,要怎么操作呢?
设想将DS:SI处的连续512字节内容复制到ES:DI(不考虑可能的重叠):
1 | mov cx,512 ;循环次数 |
这是一种不太完美的解决方法,因为效率不好。
Intel的CPU的强项是串操作
。所谓串操作就是由CPU去完成某一数量的重复的内存操作
。需要说明的是,我们常用的KMP算法
(用于匹配字符串的模式)的改进——Boyer算法,由于没有利用到串操作,因此在Intel的CPU上的效率并非最优。好的编译器往往可以利用Intel CPU的这一特性优化代码,然而,并非所有的时候他都能产生最好的代码。
某些指令可以加上REP
前缀,这些指令通常被叫做串操作指令
。
举例来说,STOSD
指令将EAX的内容保存到ES:DI,同时在DI上加/减4
;STOSB
和STOSW
分别作1字节
或1字
的操作,在DI上加/减的数是1
或2
。
在DI上是加还是减,取决于DF
标志,如果DF被复位
,则加;反之则减。
置位
、复位
的指令分别是STD
、CLD
。
除了REP,常用的前缀还包括REPNE
、REPZ
、REPE
、REPNZ
等。
操作码 | 指令 | 说明 |
---|---|---|
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 | cld ;复位DF |