爱技术 & 爱分享
爱蛋蛋 & 爱生活

字符串处理

传送字符串

MOVS指令

MOVS指令向程序员提供了把字符串从一个内存位置传送到另一个内存位置的简单途径。MOVS指令有3 种格式:

  • MOVSB: 传送单一字节
  • MOVSW: 传送一个字(2字节)
  • MOVSL: 传送一个双字(4字节)

Intel 文档使用MOVSD 传送双字。GNU 汇编器决定使用MOVSL 。

MOVS 指令使用隐含的源和目标操作数。隐含的源操作数是ESI寄存器。它指向源字符串的内存位置。隐含的目标操作数是EDI寄存器。它指向字符串要被复制到的目标内存位置。记住操作数顺序的好方法是ESI 中的S代表源(source) ,而EDI 中的D代表目标(destination) 。

使用GNU 汇编器时,有两种方式加载ESI和EDI值。第一种方式是使用间接寻址。通过在内存位置标签前面添加美元符号($),内存位置的地址被加载到了ESI 或者EDI寄存器中:

movl $output, %edi

这条指令把output标签的32位内存位置传送给EDI寄存器。

指定内存位置的另一种方式是LEA指令。LEA指令加载一个对象的有效地址。因为Linux 使用32位值引用内存位置,所以对象的内存地址必须存储在32位的目标值中。源操作数必须指向一个内存位置, 比如data段中使用的标签。指令leal output , %edi把output标签的32位内存位四加载到EDI 寄存器中。

每次执行MOVS 指令时, 数据传送后, ESI和EDI寄存器会自动改变,为另一次传送做准备。

通常这是件好事儿,但是有时候会变得有些难以处理。这一操作难以处理的部分之一就是寄存器向哪个方向改变。ESI 和EDI寄存器可能自动地递增,也可能自动地递减, 这取决于EFLAGS寄存器中的DF标志。

如果DF标志被清零,那么每条MOVS指令执行之后ESI和EDI寄存器就会递增。如果DF标志被设置,那么每条MOVS 指令执行之后ESI和EDI寄存器就会递减。

为了确保DF标志被设置为正确的方向, 可以使用下面的命令:

  • CLD用于将DF标志清零
  • STD用于设置DF标志

使用STD指令时, ESI 和EDI寄存器在每条MOVS 指令执行之后递减,所以它们应该指向字符串的末尾, 而不是开头。

要记住, 如果使用STD指令向后处理字符串, MOVSW 和MOVSL指令仍旧向前荻取内存位置,这一点很重要。

REP前缀

REP指令的特殊之处在于它自己不执行什么操作。这条指令用于按照特定次数重复执行字符串指令,由ECX 寄存器中的值进行控制。这和使用循环类似,但是不需要额外的LOOP 指令。

REP指令重复地执行紧跟在它后面的字符串指令,直到ECX寄存器中的值为零。这就是为什么称它为前缀的原因。

虽然REP指令很方便,在处理字符串时,也可以使用它的几个其他版本。除了监视ECX寄存器的值之外,还有监视零标志(ZF) 的状态的REP指令。下表介绍可以使用的其他REP指令。

指令 描述
REPE 等于时重复
REPNE 不等于时重复
REPNZ 不为零时煎复
REPZ 为零时项复

REPE和REPZ指令是相同指令的同义词, REPNE和REPNZ指令是同义词。

LOOS指令

LODS 指令用于把内存中的字符串值传送到EAX寄存器中。和MOVS 指令一样, LODS 指令有3 种不同格式:

  • LODSB: 把一个字节加载到AL寄存器中
  • LODSW: 把一个字(2字节)加载到AX寄存器中
  • LODSL: 把一个双字(4字节)加载到EAX寄存器中

Intel 文档使用LODSD加载双字。GNU 汇编器使用LODSL 。

LODS指令使用ESI寄存器作为隐含的源操作数。ESI寄存器必须包含要加载的字符串所在的内存地址。数据传送完成之后, LODS指令按照加载的数据的数量递增或者递减(取决于DF标志状态) ESI寄存器。

虽然可以使用REP指令重复执行LODS指令,但是很可能永远都不会这么做,因为能够加载到EAX寄存器中的数据最多是4个字节,这可以通过单一LODSL指令完成。

STOS指令

使用LODS指令把字符串值存放到EAX寄存器之后,可以使用STOS指令把它存放到另一个内存位置中。和LODS指令类似,根据要传送的数据的数量, STOS指令有3种格式:

  • STOSB: 存储AL寄存器中一个字节的数据
  • STOSW: 存储AX寄存器中一个字(2字节)的数据
  • STOSL: 存储EAX寄存器中一个双字(4个字节)的数据

STOS指令使用EDI寄存器作为隐含的目标操作数。执行STOS指令时,它按照使用的数据长度递增或者递减EDI寄存器的值。

STOS指令本身没有什么太令人兴奋的地方。仅仅把单一字节、字或者双字的字符串值存放到内存位置中不是很难的工作。STOS指令真正能够提供的方便是和REP指令一起使用,多次把一个字符串值复制到大型字符串值中的时候。

CMPS指令

CMPS指令系列用于比较字符串值。和其他字符串指令一样, CMPS 指令有3种格式:

  • CMPSB: 比较字节值
  • CMPSW: 比较字(2字节)值
  • CMPSL: 比较双字(4字节)值

和其他字符串指令一样,隐含的源和目标操作数的位置同样存储在ESI和EDI寄存器中。每次执行CMPS指令时,根据DF标志的设置, ESI和EDI寄存器按照被比较的数据的长度递增或者递减。

CMPS 指令从源字符串中减去目标字符串,并且适当地设置EFLAGS 寄存器的进位、符号、溢出、零、奇偶校验和辅助进位标志。CMPS 指令执行之后,可以根据字符串的值,使用一般的条件跳转指令跳转到分支。

REP指令可以用于跨越多个字节重复地进行字符串比较,但是这里有个问题,REP指令不在两个重复的过程之间检查标志的状态,它只关心ECX寄存器中的计数值。

解决方案是使用REP指令系列中的其他指令: REPE 、REPNE 、REPZ和REPNZ 。这些指令在每次重复过程中检查零标志,如果零标志被设置,就停止重复。这使得可以逐字节地检查字符串以便确定它们是否匹配。只要遇到不匹配的字符对, REP指令就会停止重复。

扫描字符串

SCAS指令

SCAS 指令系列用于扫描字符串搜索一个或者多个字符。和其他字符串指令一样, SCAS 指令有3 个版本:

  • SCASB 比较内存中的一个字节和AL寄存器的值
  • SCASW 比较内存中的一个字和AX寄存器的值
  • SCASL: 比较内存中的一个双字和EAX寄存器的值

SCAS 指令使用EDI寄存器作为隐含的目标操作数。EDI寄存器必须包含要扫描的字符串的内存地址。和其他字符串指令一样,当执行SCAS 指令时, EDI寄存器的值按照搜索字符的数据长度递增或者递减(这取决于DF标志的值) 。

进行比较时,会相应地设置EFLAGS 的辅助进位、进位、奇偶校验、溢出、符号和零标志。

可以使用标准的条件分支指令检查扫描的结果。SCAS 指令本身没有什么令人兴奋的地方。它仅仅是把EDI寄存器当前指向的字符和AL寄存
器中的字符进行比较,这和CMPS 指令类似。把SCAS 与REPE和REPNE前缀一起使用时,它的方便性才显现出来。

这两个前缀可以扫描整个字符串,查找特定的搜索字符(或者字符序列)。REPE和REPNE指令常常用于在找到搜索字符时停止扫描。但是,使用这两个指令时要谨慎,因为它们的行为也许和你想像的相反:

  • REPE: 扫描字符串的字符,查找不匹配搜索字符的字符
  • REPNE: 扫描字符串的字符,查找匹配搜索字符的字符

对于大多数字符串扫描,使用REPNE指令,因为它将在字符串中找到搜索字符时停止扫描。

当找到字符时,EDI寄存器包含紧跟在定位到的字符后面的内存地址。这是因为REPNE指令在执行SCAS 指令之后递增EDI 寄存器。ECX寄存器包含搜索字符距离字符串末尾的位置。使用这个值时要谨慎,因为它是从字符串的末尾开始计数的。为了得到距离字符串开头的位置,要从这个值减去字符串长度并且反转符号。

搜索多个字符

虽然SCASW 和SCASL指令可以用于搜索2个或者4个字符的序列,但是使用它们的时候必须要谨慎。它们可能不像读者期望的那样执行。

SCASW 和SCASL指令扫描字符串,查找AX 或者EAX 寄存器中的字符序列,但是它们并不进行逐字符的比较。而是每次比较之后, EDI寄存器要么递增2 (对于SCASW) ,要么递增4 (对于SCASL) ,而不是递增l 。这就是说字符序列也必须按照适当的顺序出现在字符串中。

计算字符串长度

SCAS 指令的一个非常有用的功能是确定零结尾(也称为空结尾)的字符串的长度。这些字符串经常在C程序中使用,但是也通过使用asciz声明在汇编语言程序中使用。对于零结尾的字符串,要搜索的显然是零的位置,并且计数找到零经过了多少个字符。

上述笔记内容学习自 AT&T Professional Assembly Language — Chapter X

赞(0)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。墨影 » 字符串处理