跳到主要内容

字符串处理指令

x86 架构提供了专门的字符串处理指令,可以高效地操作内存中的数据块。本章介绍这些指令的使用方法。

字符串指令概述

字符串指令用于处理内存中的连续数据,包括:

  • 数据传送:MOVS
  • 比较操作:CMPS
  • 扫描操作:SCAS
  • 存储操作:STOS
  • 加载操作:LODS

这些指令通常与重复前缀(REP、REPE、REPNE)配合使用。

方向标志

DF 标志的作用

方向标志(DF)决定字符串操作的方向:

  • DF = 0:递增方向(ESI/EDI 增加)
  • DF = 1:递减方向(ESI/EDI 减少)

设置方向标志

cld                    ; DF = 0,递增方向
std ; DF = 1,递减方向

数据传送指令

MOVS 指令

MOVS 指令在内存之间传送数据:

movsb                  ; 传送字节:[ES:DI/RDI] = [DS:SI/RSI]
movsw ; 传送字(16 位)
movsd ; 传送双字(32 位)
movsq ; 传送四字(64 位)

; 根据方向标志更新指针
; DF=0:RSI++, RDI++
; DF=1:RSI--, RDI--

REP 前缀

REP 前缀重复执行指令,直到 RCX/ECX 为 0:

; 内存复制
mov rsi, source ; 源地址
mov rdi, dest ; 目标地址
mov rcx, count ; 字节数
cld ; 递增方向
rep movsb ; 复制 count 个字节

; 复制双字(更快)
mov rcx, count / 4 ; 双字数
rep movsd ; 复制 count/4 个双字

内存复制示例

; void memcpy(void *dest, const void *src, size_t n)
; 参数:RDI = dest, RSI = src, RDX = n
memcpy:
mov rcx, rdx ; 字节数
test rcx, rcx
jz .done

cld
rep movsb

.done:
ret

比较指令

CMPS 指令

CMPS 指令比较两个内存区域:

cmpsb                  ; 比较字节:[RSI] - [RDI]
cmpsw ; 比较字
cmpsd ; 比较双字
cmpsq ; 比较四字

; 设置标志位,然后更新指针

REPE/REPZ 前缀

当相等时继续重复:

; 比较两个字符串
mov rsi, str1
mov rdi, str2
mov rcx, length
cld
repe cmpsb ; 比较直到不相等或计数结束

; 检查结果
ja str1_greater ; str1 > str2
jb str2_greater ; str1 < str2
; 相等

REPNE/REPNZ 前缀

当不相等时继续重复:

; 查找不同的字符
mov rsi, str1
mov rdi, str2
mov rcx, length
cld
repne cmpsb ; 比较直到相等或计数结束

内存比较示例

; int memcmp(const void *s1, const void *s2, size_t n)
; 返回:<0, 0, >0
memcmp:
mov rcx, rdx ; 字节数
test rcx, rcx
jz .equal

cld
repe cmpsb

ja .greater
jb .less

.equal:
xor eax, eax
ret

.greater:
mov eax, 1
ret

.less:
mov eax, -1
ret

扫描指令

SCAS 指令

SCAS 指令在字符串中扫描特定值:

scasb                  ; 比较 AL 与 [RDI],更新 RDI
scasw ; 比较 AX 与 [RDI]
scasd ; 比较 EAX 与 [RDI]
scasq ; 比较 RAX 与 [RDI]

字符串长度计算

; size_t strlen(const char *s)
; 参数:RDI = 字符串地址
; 返回:RAX = 长度
strlen:
xor eax, eax ; 计数器
xor ecx, ecx
dec ecx ; ECX = 0xFFFFFFFF

cld
repne scasb ; 扫描直到找到 0

not ecx ; 取反得到长度
dec ecx ; 减 1(不包括 null)
mov eax, ecx
ret

字符查找

; char *strchr(const char *s, int c)
; 参数:RDI = 字符串地址,ESI = 要查找的字符
; 返回:RAX = 指向字符的指针,或 NULL
strchr:
mov al, sil ; 要查找的字符
mov rcx, -1 ; 无限计数

cld
repne scasb

jne .not_found

lea rax, [rdi - 1] ; 指向找到的字符
ret

.not_found:
xor eax, eax
ret

存储指令

STOS 指令

STOS 指令将寄存器值存储到内存:

stosb                  ; [RDI] = AL,更新 RDI
stosw ; [RDI] = AX
stosd ; [RDI] = EAX
stosq ; [RDI] = RAX

内存填充

; void *memset(void *s, int c, size_t n)
; 参数:RDI = 目标地址,ESI = 填充值,RDX = 字节数
memset:
mov rcx, rdx ; 字节数
test rcx, rcx
jz .done

mov al, sil ; 填充值
cld
rep stosb

.done:
mov rax, rdi ; 返回目标地址
sub rax, rdx
ret

清零内存

; 清零 1024 字节缓冲区
mov rdi, buffer
mov rcx, 1024 / 8 ; 128 个四字
xor eax, eax ; 填充 0
cld
rep stosq

加载指令

LODS 指令

LODS 指令从内存加载到寄存器:

lodsb                  ; AL = [RSI],更新 RSI
lodsw ; AX = [RSI]
lodsd ; EAX = [RSI]
lodsq ; RAX = [RSI]

使用示例

; 将字符串转换为大写
mov rsi, str
mov rdi, str
mov rcx, length

cld
.upper_loop:
lodsb ; 加载字符到 AL
cmp al, 'a'
jb .store
cmp al, 'z'
ja .store
sub al, 32 ; 转大写
.store:
stosb ; 存储字符
loop .upper_loop

重复前缀总结

前缀条件用途
REPRCX != 0重复执行
REPE/REPZRCX != 0 且 ZF = 1相等时重复
REPNE/REPNZRCX != 0 且 ZF = 0不相等时重复

字符串指令组合

字符串复制并转换

; 复制字符串并转换为大写
; 参数:RSI = 源,RDI = 目标
strcpy_upper:
cld
.copy_loop:
lodsb ; 加载字符
test al, al ; 检查是否为 null
jz .done

cmp al, 'a'
jb .store
cmp al, 'z'
ja .store
sub al, 32 ; 转大写

.store:
stosb ; 存储字符
jmp .copy_loop

.done:
stosb ; 存储 null 终止符
ret

字符串拼接

; char *strcat(char *dest, const char *src)
; 参数:RDI = dest,RSI = src
strcat:
push rdi

; 找到 dest 的末尾
xor eax, eax
mov rcx, -1
cld
repne scasb
dec rdi ; 指向 null

; 复制 src 到 dest 末尾
.copy_loop:
lodsb
stosb
test al, al
jnz .copy_loop

pop rax ; 返回 dest
ret

字符串查找子串

; char *strstr(const char *haystack, const char *needle)
; 参数:RDI = haystack,RSI = needle
strstr:
push rbx
push r12
push r13

mov r12, rdi ; 保存 haystack
mov r13, rsi ; 保存 needle

; 空 needle 返回 haystack
mov al, [rsi]
test al, al
jz .found

.search:
; 在 haystack 中查找 needle 的第一个字符
mov rdi, r12
mov al, [r13]
mov rcx, -1
cld
repne scasb
jne .not_found

; 检查是否匹配整个 needle
mov r12, rdi ; 更新 haystack 位置
dec r12

mov rsi, r13 ; needle
inc rsi ; 跳过第一个字符(已匹配)

.compare_loop:
mov al, [rsi]
test al, al
jz .found ; needle 结束,找到匹配

mov bl, [rdi]
cmp al, bl
jne .search ; 不匹配,继续搜索

inc rsi
inc rdi
jmp .compare_loop

.found:
mov rax, r12
jmp .done

.not_found:
xor eax, eax

.done:
pop r13
pop r12
pop rbx
ret

性能考虑

字符串指令 vs 普通指令

现代处理器上,字符串指令的性能特点:

  • 短数据块:普通循环可能更快
  • 长数据块:字符串指令通常更快
  • 对齐数据:对齐的数据处理更快

优化建议

; 优化:对齐后使用更大的数据单位
; 假设复制大量数据

; 先复制未对齐的部分
mov rcx, rdi
and rcx, 7 ; 检查低 3 位
jz .aligned
sub rcx, 8
neg rcx ; 需要复制的字节数
sub rdx, rcx ; 更新剩余字节数
rep movsb

.aligned:
; 以四字为单位复制
mov rcx, rdx
shr rcx, 3 ; 四字数
rep movsq

; 复制剩余字节
mov rcx, rdx
and rcx, 7
rep movsb

小结

本章介绍了 x86 的字符串处理指令:

  • MOVS:内存块复制
  • CMPS:内存块比较
  • SCAS:字符串扫描
  • STOS:内存填充
  • LODS:内存加载
  • 重复前缀:REP、REPE/REPZ、REPNE/REPNZ

这些指令在处理字符串和内存块时非常高效,是系统编程的重要工具。下一章将学习系统调用。