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 串加载指令

LODSSTOS 相反,从 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 串对比指令

CMPSCMP 指令类似,都是将两个操作数进行减法运算,以影响标识寄存器。CMPS 将 DS:ESI 作为数据源,同 ES:EDI 的数据进行对比:

指令 类型
cmpsb BYTE
cmpsw WORD
cmpsd DWORD

比较常见的是结合 repz / repnz 来使用。repzrepeat while zeroreperepz 具有相同的意义,在每次进行对比后,如果遇到两个数据不相等,即 ZF = 0,则停止循环操作,否则循环进行对比,直到 ECX = 0

mov ecx, 10
repe cmpsb
mov eax, ecx

repnzrepne 则同上述相反,即 repeat while not zero,当遇到两个数据相等或 ECX = 0 停止循环。

0x5 串扫描指令

SCASCMPS 类似,对数据进行对比,两个对比数分别为 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     ; 取反