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

发布时间 2023-08-22 16:16:14作者: lyshark

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

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

  • 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

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/f6603608.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!