REP / REPZ / REPE / REPNZ/ REPNE 操作指令
0x0 概述
重复指令主要用于进行循环操作,通过每次递减 (E)CX 寄存器,并结合相关条件,来完成循环操作。
重复指令一般配合串指令一起工作,常见串指令包括 MOVS、LODS、STOS、CMPS 和 SCAS。
当运行串指令后,SI / DI 的值会自动递增或递减,取决于标识寄存器的 DF 标识。
为了简单表述,下面相关的寄存器都是用 32 位进行表示,同时 DF = 0
,表示正反向增长。
0x1 串传送指令
MOVS
作为串传送指令,主要是将数据从 DS:ESI 复制到 ES:EDI,之后递增 ESI 和 EDI 寄存器值。
串传送指令所复制的数据类型大小,都有对应类型的指令:
指令 | 类型 |
---|---|
movsb | BYTE |
movsw | WORD |
movsd | DWORD |
使用 rep
指令,循环拷贝:
mov ecx, 10h
rep movsb
0x2 串存储指令
STOS
用于串存储,同 MOVS
类似,将源数据复制到 ES:EDI。区别在于,STOS
将 EAX / AX / AL 作为数据源,同样类型大小取决于对应的指令:
指令 | 类型 | 源数据寄存器 |
---|---|---|
stosb | BYTE | AL |
stosw | WORD | AX |
stosd | DWORD | EAX |
Windows 程序中常见的栈初始化:
mov ecx, 4
mov eax, 0CCCCCCCCh
rep stosd
0x3 串加载指令
LODS
同 STOS
相反,从 DS:ESI 加载数据到 EAX / AX / AL 寄存器:
指令 | 类型 | 目的寄存器 |
---|---|---|
lodsb | BYTE | AL |
lodsw | WORD | AX |
lodsd | DWORD | EAX |
对于循环加载 rep lods
的意义不是很大,因为每次都会覆盖上一次寄存器的值,通常 LODS
会配合 LOOP
来使用,在每次复制后,对 EAX / AX / AL 依次进行处理:
lodsb
mov bl, al
0x4 串对比指令
CMPS
同 CMP
指令类似,都是将两个操作数进行减法运算,以影响标识寄存器。CMPS
将 DS:ESI 作为数据源,同 ES:EDI 的数据进行对比:
指令 | 类型 |
---|---|
cmpsb | BYTE |
cmpsw | WORD |
cmpsd | DWORD |
比较常见的是结合 repz / repnz 来使用。repz
即 repeat while zero
,repe
和 repz
具有相同的意义,在每次进行对比后,如果遇到两个数据不相等,即 ZF = 0
,则停止循环操作,否则循环进行对比,直到 ECX = 0
。
mov ecx, 10
repe cmpsb
mov eax, ecx
repnz
和 repne
则同上述相反,即 repeat while not zero
,当遇到两个数据相等或 ECX = 0
停止循环。
0x5 串扫描指令
SCAS
同 CMPS
类似,对数据进行对比,两个对比数分别为 EAX / AX / AL 和 DS:EDI。
指令 | 类型 |
---|---|
scasb | BYTE |
scasw | WORD |
scasd | DWORD |
常见的使用 repne scas
来计算 null-terminated 的字符串长度:
mov ecx, -1
mov al, 0
repne scasb
mov eax, -2
sub eax, ecx
首先初始化 ECX = -1
,并设置 AL = 0
,开始循环后,ECX 依次递减(-2、-3、-4)。当到达字符串结尾符时,循环结束。
最后,使用 -2
减去 ECX 值,ECX 为负数:
$$
x = [ECX],x < 0\
-2 - (-x) = -2 + x
$$
最终得到字符串长度。对于为啥使用负数初始化 ECX,原因在于每次 rep 运行后,都会递减 ECX 的值,所以不能使用递增的方式来存储 ECX。
同时也不能用零,否则 nepne
就停止循环了 (=゚Д゚=)
另一种计算字符串长度的方法:
mov ecx, -1
mov ebx, edi
mov al, 0
repne scasb
sub edi, ebx
还有类似的:
mov ecx, -1
repne scasb
add ecx, 2 ; -x + 2 => -(-x + 2) = -2 + x
neg ecx ; 取反