字符串处理可能是编程语言中最耗时以及最繁琐的操作,比较一个整数只需要比较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语言好的地方是不需要循环,这个再之前提到,由于预载入指令的问题,如果进行跳转(循环)将会是一件很损耗效率的事情,使用此类指令可以避免这个问题。