汇编语言学习笔记(八)-字符串

字符串处理可能是编程语言中最耗时以及最繁琐的操作,比较一个整数只需要比较32位,但是比较字符串却需要循环比较字符串的每一个字符,相关的复制 追加操作则需要很多的内存拷贝操作,可能各种语言都对应有一系列的字串处理库,对汇编来说,当然没有类似的库可用,但是Intel提供了一系列字串处理相 关的指令,可以相对方便的处理字符串。

先来看几个简单的拷贝操作:

	;AT&T 语法格式
	movsb 传递一个字节
	movsw 传递两个字节
	movsl 传递四个字节

因为这组操作没有操作数,所以MASM语法相同,但是movsl不同,l在AT&T语言中表示long,是4字节,但是在MASM中,4字节是dword,所以最后一个指令被写做 movsd

虽说这里movsb没有操作数,但是没有操作数如何拷贝字符?其实这有关字串处理的操作,有两个操作数,源字符串和目标字符串,这里的操作数是隐含操作数,记得寄存器那篇说过,esi为源操作数,edi为目标操作数,所体现的就是这里了。

另外一个问题是,如果仅仅移动一个字符,mov指令完全可以胜任,那还要movsb干什么呢?的确如此,movsb本身来讲并不见得方便,但是结合一个指令前缀rep,就不同了。

rep是一个指令前缀,就是再其他指令之前表示一种特性,rep为repeat,重复之意,就是他可以不停的执行,直到ecx为0,与之相关的还有两个指令,一起总结如下:

指令 说明
rep ecx > 0时重复
repz/repe 零标志置位 并且 ecx > 0时重复
repnz/repne 零标志为清零 并且 ecx > 0时重复

如果还记得loop和loopz指令,应该熟悉汇编中的这种后缀规律。

如果movsb能够重复执行,那另外一个问题就出来了,重复复制一个字节有什么意思呢?其实movsb指令不单复制,同时还修改esi和edi的值。至于如何修改,是加还是减,需要看DF标志,方向标识。当DF清零,则esi和edi递增;反之递减。

为了方便操作DF标志,Intel还专门提供了两个清零和置位的指令:

cld 	DF 清零 	esi 和 edi 递增
std 	DF 置位 	esi 和 edi 递减

与这两个指令相类似的还有cli, sti来控制中断标志,clc, stc控制CF标志,其中的规律一看便知。

回头来看字串的复制步骤,先填充源目的寄存器,再用cld/std设置方向,之后设置ecx控制循环次数,再rep movsb即可。 现举一个memcpy函数的例子:

	char src[100] = "test data";
	char dest[100];
	
	int main()
	{
		memcpy(dest, src, 100);
		return 0;
	}

对应的MASM语法格式的汇编如下:

	.386
	.model flat,stdcall
	option casemap:none
	
	include    windows.inc
	include    kernel32.inc
	includelib kernel32.lib
	
	.data
	src	db "test data", 91 dup(0)
	dest db 100 dup(0)
	
	.code
	start:
		cld
		mov esi, offset src
		mov edi, offset dest
		mov ecx, 100
		rep movsb
	    invoke ExitProcess, NULL
	end start

这里代码很少,操作也非常明了,对于AT&T语法,这里就不单写一个了,不过,从原理上讲,不管是AT&T还是 MASM语法,最终都得编译成intel cpu可以识别的机器码,所以我们把这段MASM写成的程序用AT&T语法的反汇编器反汇编即可得到AT&T的写法,Ollydbg 2.0 可以反汇编程序到多种汇编语法,不过我感觉翻译AT&T翻译的不大好看,还是使用gcc套件里的工具吧,这个工具是objdump,参数-D 为反汇编参数,直接使用命令行 objdump -D strcpy.exe 即可输出AT&T语法的汇编,这里简单摘要一些:

	401000:       fc                      cld
	401001:       be 00 30 40 00          mov    $0x403000,%esi
	401006:       bf 64 30 40 00          mov    $0x403064,%edi
	40100b:       b9 64 00 00 00          mov    $0x64,%ecx
	401010:       f3 a4                   rep movsb %ds:(%esi),%es:(%edi)
	401012:       6a 00                   push   $0x0
	401014:       e8 01 00 00 00          call   0x40101a

其中大致流程还是看的很清楚的。

介绍了这么多,仅仅只介绍了一个字符拷贝命令movsb,其他的一些诸如比较、加载的指令,但是其他指令与movsb类似,没有什么难点,这里仅仅做一些列举:

指令 操作 源操作数 目的操作数
lodsb 加载字节 esi al
lodsw 加载双字节 esi ax
lodsl(MASM lodsd) 加载四字节 esi eax
stosb 保存字节到目的 al edi
stosw 保存双字节到目的 ax edi
stosl(MASM stosd) 保存四字节到目的 eax edi
cmpsb 比较字节 esi edi
cmpsw 比较双字节 esi edi
cmpsl(MASM cmpsd) 比较四字节 esi edi
scasb 比较内存和AL al edi
scasw 比较内存和AX ax edi
scasl(MASM scasd) 比较内存和EAX eax edi

这里举一个简单的strlen例子:

	#include <string.h>
	
	char ss[100] = "test data";
	int len = 0;
	
	int main()
	{
		len = strlen(ss);
		return 0;
	}

对应的MASM汇编如下:

	.386
	.model flat,stdcall
	option casemap:none
	
	include    windows.inc
	include    kernel32.inc
	includelib kernel32.lib
	
	.data
	string	db "test data", 91 dup(0)
	len dd 0
	
	.code
	start:
		cld
		mov edi, offset string
		mov al, 0
		mov ecx, 100
		repne scasb
		sub ecx, 100
		neg ecx
		dec ecx
		mov len, ecx
	    invoke ExitProcess, NULL
	end start

这里把al设置为0,使用scasb指令,即在edi指向的字符中查找0,找到以后ecx所减少的数字就是移动的次数,注意其中是包含0的,最后经过运算便可得到字串长度。

使用此类指令操作字符串,比用c语言好的地方是不需要循环,这个再之前提到,由于预载入指令的问题,如果进行跳转(循环)将会是一件很损耗效率的事情,使用此类指令可以避免这个问题。

Built with Hugo
主题 StackJimmy 设计