5.3 汇编语言:字符串操作指令

汇编语言是一种面向机器的低级语言,用于编写计算机程序。汇编语言与计算机机器语言非常接近,汇编语言程序可以使用符号、助记符等来代替机器语言的二进制码,但最终会被汇编器编译成计算机可执行的机器码。

本章将深入研究字符串操作指令,这些指令在汇编语言中具有重要作用,用于处理字符串数据。我们将重点介绍几个关键的字符串操作指令,并详细解释它们的功能和用法。通过清晰的操作示例和代码解析,读者将了解如何使用这些指令进行字符串比较、复制、填充等常见操作。我们还将探讨不同指令之间的区别,并提供实际的示例程序,展示字符串操作指令在实际场景中的应用。通过学习本章,读者将能够拓展汇编技能,为处理字符串数据提供高效而精确的解决方案。

常见的字符串操作指令包括:

  • MOVSB / MOVSW / MOVSX:在两个存储器地址之间复制一个字节、一个字或一个双字。其中 MOVSB 复制一个字节,MOVSW 复制一个字,MOVSX 复制一个双字。
  • CMPSB / CMPSW / CMPSD:比较两个存储器地址中的一个字节、一个字或一个双字,并将比较结果存储在条件码寄存器中。其中 CMPSB 比较一个字节,CMPSW 比较一个字,CMPSD 比较一个双字。
  • LODSB / LODSW / LODSD:从存储器中读取一个字节、一个字或一个双字,并将其存储在累加器中。其中 LODSB 读取一个字节,LODSW 读取一个字,LODSD 读取一个双字。
  • STOSB / STOSW / STOSD:将一个字节、一个字或一个双字写入存储器,并将累加器的值相应地更新。其中 STOSB 写入一个字节,STOSW 写入一个字,STOSD 写入一个双字。
  • SCASB / SCASW / SCASD:在存储器地址中扫描一个字节、一个字或一个双字,并将扫描结果存储在条件码寄存器中。其中 SCASB 扫描一个字节,SCASW 扫描一个字,SCASD 扫描一个双字。

这些字符串操作指令通常是通过累加器(即 AH、AL、AX 或 EAX 等寄存器)来控制读取或写入的数据大小,同时还需要通过 DF 标志位来控制是向存储地址增加还是减小。在使用字符串操作指令时,需要仔细理解这些指令的语法和操作方式,以便正确地处理字符串数据。

3.1 MOVSB/MOVSW/MOVSD

移动串指令包括了MOVSB、MOVSW、MOVSD这三条指令,该指令的原理为从ESIEDI中,执行后将ESI地址里面的内容移动到EDI指向的内存空间中,该指令常用于对特定字符串的复制操作。

  • MOVSB指令:将一个字节从ESI地址指向的内存单元复制到EDI地址指向的内存单元,同时增加或减少ESI和EDI(取决于方向标志位的状态)。
  • MOVSW指令:将两个字节从ESI地址指向的内存单元复制到EDI地址指向的内存单元,
  • MOVSD指令:将四个字节从ESI地址指向的内存单元复制到EDI地址指向的内存单元。这些指令都可用于复制字符串或移动缓冲区。
  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
; 逐字节拷贝
SrcString BYTE "hello lyshark",0h ; 源字符串
SrcStringLen EQU $ - SrcString - 1 ; 计算出原始字符串长度
DstString BYTE SrcStringLen dup(?),0h ; 目标内存地址
szFmt BYTE '字符串: %s 长度: %d ',0dh,0ah,0

; 四字节拷贝
ddSource DWORD 10h,20h,30h ; 定义三个四字节数据
ddDest DWORD lengthof ddSource dup(?) ; 得到目标地址

.code
main PROC
; 第一种情况: 实现逐字节拷贝
cld ; 清除方向标志
mov esi,offset SrcString ; 取源字符串内存地址
mov edi,offset DstString ; 取目标字符串内存地址
mov ecx,SrcStringLen ; 指定循环次数,为原字符串长度
rep movsb ; 逐字节复制,直到ecx=0为止

lea eax,dword ptr ds:[DstString]
mov ebx,sizeof DstString
invoke crt_printf,addr szFmt,eax,ebx

; 第二种情况: 实现4字节拷贝
lea esi,dword ptr ds:[ddSource]
lea edi,dword ptr ds:[ddDest]
cld
rep movsd

; 使用loop循环逐字节复制
lea esi,dword ptr ds:[SrcString]
lea edi,dword ptr ds:[DstString]
mov ecx,SrcStringLen
cld ; 设置方向为正向复制
@@: movsb ; 每次复制一个字节
dec ecx ; 循环递减
jnz @B ; 如果ecx不为0则循环

lea eax,dword ptr ds:[DstString]
mov ebx,sizeof DstString
invoke crt_printf,addr szFmt,eax,ebx

invoke ExitProcess,0
main ENDP
END main

3.2 CMPSB/CMPSW/CMPSD

比较串指令包括CMPSB、CMPSW、CMPSD比较ESI、EDI执行后将ESI指向的内存操作数同EDI指向的内存操作数相比较,其主要从ESI指向内容减去EDI的内容来影响标志位。这些指令通常用于比较字符串中的字符,可影响方向标志、零标志和符号标志位的状态。

CMPSB指令是将ESI和EDI地址指向的内存单元中的一个字节进行比较,同时增加或减少ESI和EDI(取决于方向标志位的状态)。

  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
; 逐字节比较
SrcString BYTE "hello lyshark",0h
DstStringA BYTE "hello world",0h
.const
szFmt BYTE '字符串: %s',0dh,0ah,0
YES BYTE "相等",0
NO BYTE "不相等",0

.code
main PROC
; 实现字符串对比,相等/不相等输出
lea esi,dword ptr ds:[SrcString]
lea edi,dword ptr ds:[DstStringA]
mov ecx,lengthof SrcString
cld
repe cmpsb
je L1
jmp L2

L1: lea eax,YES
invoke crt_printf,addr szFmt,eax
jmp lop_end

L2: lea eax,NO
invoke crt_printf,addr szFmt,eax
jmp lop_end
lop_end:
int 3

invoke ExitProcess,0
main ENDP
END main

CMPSW 是对比一个字类型的数组,指令是将ESI和EDI地址指向的内存单元中的两个字节进行比较,只有当数组中的数据完全一致的情况下才会返回真,否则为假。

  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
Array1 WORD 1,2,3,4,5 ; 必须全部相等才会清空ebx
Array2 WORD 1,3,5,7,9
.const
szFmt BYTE '数组: %s',0dh,0ah,0
YES BYTE "相等",0
NO BYTE "不相等",0

.code
main PROC
lea esi,Array1
lea edi,Array2
mov ecx,lengthof Array1

cld
repe cmpsw
je L1
lea eax,NO
invoke crt_printf,addr szFmt,eax
jmp lop_end

L1: lea eax,YES
invoke crt_printf,addr szFmt,eax
jmp lop_end

lop_end:
int 3

invoke ExitProcess,0
main ENDP
END main

CMPSD则是比较双字数据,指令将ESI和EDI地址指向的内存单元中的四个字节进行比较,同样可用于比较数组,这里就演示一下比较单数的情况。

  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
var1 DWORD 1234h
var2 DWORD 5678h
.const
szFmt BYTE '两者: %s',0dh,0ah,0
YES BYTE "相等",0
NO BYTE "不相等",0

.code
main PROC
lea esi,dword ptr ds:[var1]
lea edi,dword ptr ds:[var2]

cmpsd
je L1
lea eax,dword ptr ds:[YES]
invoke crt_printf,addr szFmt,eax
jmp lop_end

L1: lea eax,dword ptr ds:[NO]
invoke crt_printf,addr szFmt,eax
jmp lop_end

lop_end:
int 3

invoke ExitProcess,0
main ENDP
END main

3.3 SCASB/SCASW/SCASD

扫描串指令包括SCASB、SCASW、SCASD其作用是把AL/AX/EAX中的值同EDI寻址的目标内存中的数据相比较,这些指令在一个长字符串或者数组中查找一个值的时候特别有用。

  • SCASB指令:将AL寄存器中的值与EDI地址指向的内存单元中的一个字节进行比较,同时增加或减少EDI(取决于方向标志位的状态)。
  • SCASW指令:将AX寄存器中的值与EDI地址指向的内存单元中的两个字节进行比较。
  • SCASD指令:将EAX寄存器中的值与EDI地址指向的内存单元中的四个字节进行比较。这些指令通常用于在一个长字符串或数组中查找一个特定值的位置,可影响方向标志、零标志和符号标志位的状态。
  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
szText BYTE "ABCDEFGHIJK",0
.const
szFmt BYTE '字符F所在位置: %d',0dh,0ah,0

.code
main PROC
; 寻找单一字符找到会返回第几个字符
lea edi,dword ptr ds:[szText]
mov al,"F"
mov ecx,lengthof szText -1
cld
repne scasb ; 如果不相等则重复扫描
je L1
xor eax,eax ; 如果没找到F则清空eax
jmp lop_end

L1: sub ecx,lengthof szText -1
neg ecx ; 如果找到输出第几个字符
invoke crt_printf,addr szFmt,ecx

lop_end:
int 3

main ENDP
END main

如果我们想要对数组中某个值是否存在做判断,则可以使用SCASD指令扫描一个数组中是否存在一个特定的值,通过循环指令(如LOOP或JECXZ)逐个4字节扫描,来检查EAX寄存器中的值是否与目标数组中的值匹配。如果匹配成功,则方向标志位将被设置为与扫描方向相反的方向,如果没有找到匹配项,方向标志位将保持不变。

在使用循环指令时,需要在每次循环中比较数组当前位置的值是否与目标值相等,如果相等就跳出循环,如果没有找到匹配项,就继续循环指令知道数组的最后元素。

  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
MyArray DWORD 65,88,93,45,67,89,34,67,89,22
.const
szFmt BYTE '数值: %d 存在',0dh,0ah,0
.code
main PROC
lea edi,dword ptr ds:[MyArray]
mov eax,34
mov ecx,lengthof MyArray - 1
cld
repne scasd
je L1
xor eax,eax
jmp lop_end

L1: sub ecx,lengthof MyArray - 1
neg ecx
invoke crt_printf,addr szFmt,ecx,eax
lop_end:
int 3

main ENDP
END main

3.4 STOSB/STOSW/STOSD

存储指令主要包括STOSB、STOSW、STOSD其作用是把AL/AX/EAX中的数据储存到EDI给出的地址中,执行后EDI的值根据方向标志的增加或减少,该指令常用于初始化内存或堆栈。

  • STOSB指令:将AL寄存器中的值存储到EDI地址指向的内存单元中,同时增加或减少EDI(取决于方向标志位的状态)。
  • STOSW指令:将AX寄存器中的值存储到EDI地址指向的两个字节内存单元中。
  • STOSD指令:将EAX寄存器中的值存储到EDI地址指向的四个字节内存单元中。这些指令常用于初始化内存、堆栈和缓冲区。
  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
Count DWORD 100
String BYTE 100 DUP(?),0

.code
main PROC

; 利用该指令初始化字符串
mov al,0ffh ; 初始化填充数据
lea di,byte ptr ds:[String] ; 待初始化地址
mov ecx,Count ; 初始化字节数
cld ; 初始化:方向=前方
rep stosb ; 循环填充

; 存储字符串: 使用A填充内存
lea edi,dword ptr ds:[String]
mov al,"A"
mov ecx,Count
cld
rep stosb

int 3

main ENDP
END main

3.5 LODSB/LODSW/LODSD

载入指令主要包括LODSB、LODSW、LODSD起作用是将ESI指向的内存位置向AL/AX/EAX中装载一个值,同时ESI的值根据方向标志值增加或减少,如下分别完成加法与乘法计算,并回写到内存中。

  • LODSB指令:将ESI地址指向的一个字节复制到AL寄存器中,同时增加或减少ESI(取决于方向标志位的状态)。
  • LODSW指令:将ESI地址指向的两个字节复制到AX寄存器中
  • LODSD指令:将ESI地址指向的四个字节复制到EAX寄存器中。
  .386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

include msvcrt.inc
includelib msvcrt.lib

.data
ArrayW WORD 1,2,3,4,5,6,7,8,9,10
ArrayDW DWORD 1,2,3,4,5
ArrayMulti DWORD 10

szFmt BYTE '计算结果: %d ',0dh,0ah,0

.code
main PROC
; 利用载入命令计算数组加法
lea esi,dword ptr ds:[ArrayW]
mov ecx,lengthof ArrayW
xor edx,edx
xor eax,eax
@@: lodsw ; 将输入加载到EAX
add edx,eax
loop @B

mov eax,edx ; 最后将相加结果放入eax
invoke crt_printf,addr szFmt,eax

; 利用载入命令(LODSD)与存储命令(STOSD)完成乘法运算
mov esi,offset ArrayDW ; 源指针
mov edi,esi ; 目的指针
cld ; 方向=向前

mov ecx,lengthof ArrayDW ; 循环计数器
L1: lodsd ; 加载[esi]至EAX
mul ArrayMulti ; 将EAX乘以10
stosd ; 将结果从EAX存储至[EDI]
loop L1

; 循环读取数据(存在问题)
mov esi,offset ArrayDW ; 获取基地址
mov ecx,lengthof ArrayDW ; 获取长度
xor eax,eax
@@: lodsd
invoke crt_printf,addr szFmt,eax
dec ecx
loop @B

int 3

main ENDP
END main