实验3 字符串/块处理程序设计
CPU提供了一些系列的指令,用于对内存中的连续数据单元进行处理。利用这些指令,可以编写出高效的程序,完成对字符串、数据块的操作处理。
3.1 搜索字符
块扫描指令SCASB、SCASW、SCASD在EDI指向的目标数据块中查找AL、AX或EAX,然后EDI自动增加或减小1、2或4。块扫描指令像CMP指令那样设置FLAGS寄存器中的标志位。
块扫描指令的几个关键特征为:
1. 自动修改指针
块操作指令会自动地修改ESI和EDI,使它们指向下一个源操作数和目的操作数。CPU的EFLAGS中有一个标志位DF,由DF来决定ESI和EDI是增加还是减小。DF=0时,地址增加;DF=1时,地址减小。SCASB、SCASW、SCASD只修改EDI。
2. 方向标志
CLD指令将DF标志设为0,代表Clear Direction Flag;STD指令将DF标志设为1,代表Set Direction Flag。
3. 操作数大小
AL、AX或EAX称为匹配数,扫描就是对目标数据块的每一个单元和匹配数相比较。
操作数的大小决定增加或减小的单位。对于字节操作数,增减量为1,指向下一个字节;对于字操作数,增减量为2,指向下一个字;对于双字操作数,增减量为4,指向下一个双字。
4. 重复前缀
SCAS指令可以和REPZ或REPNZ前缀一起使用,ECX是最大扫描次数,即数据块中单元的个数。
SCAS指令一般与REPNZ前缀配合使用。使用REPNZ前缀时,只有比较结果为不等(ZF=0)时,表示没有找到匹配数,继续进行下一次比较(扫描)。如果比较结果为相等(ZF=1),表示已经找到了匹配数,不再继续比较。
扫描完成后,根据ZF标志位来决定是否找到。ZF=1时,找到了匹配数,此时EDI指向匹配数位置的下一个单元,ECX是剩下的比较次数(ECX也有可能为0);ZF=0时,没有找到匹配数,ECX一定为0。
下面的程序片断在字符串中查找一个字符。EDI指向目标字符串,ECX设为字符串所占的字节数。AL为要查找的字符,以字节为单位进行比较(SCASB),当发现相等时停止比较(前缀为REPNZ)。
szStr byte 'How do you do!', 0
lea edi, szStr ; 指向目标字符串
mov ecx, 15 ; szStr占15个字节
cld ; 地址由低至高
mov al, 'y'
repnz scasb ; ZF=1则停止扫描
前面7个字符都不等于AL,直到比较第8个字符时,才找到匹配数,ZF=1,不再继续扫描。注意:EDI指向字符’o’,而不是’y’。ECX=7,表示后面还有7个字节未扫描。如图3-1所示。
| +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +10 | +11 | +12 | +13 | +14 | |
szStr | H | o | w |
| d | o |
| y | o | u |
| d | o | ! | 00H | |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图3-1 在字符串中查找一个字符
在下面的程序中,首先求出字符串'How do you do!'的长度。接着,再将字符串中出现的第1个字符’o’替换为’O’,采用地址从低到高的顺序扫描,扫描结束时,EDI指向字符’w’; 最后,将字符串中出现的最后1个字符’ ’替换为’*’,采用地址从高到低的顺序扫描,扫描结束时,EDI指向字符’u’。执行结果为:
HOw do you*do!
nLen = 15
;程序清单:scanchar.asm(字符扫描与替换)
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C :dword,:vararg
time PROTO C :dword
.data
szStr byte 'How do you do!', 0
nLen dword 0 ; 字符串长度
szLen byte 0ah, 'nLen = %d', 0ah, 0
.code
start:
lea edi, szStr ; 指向目标字符串
mov al, 0
repnz scasb ; ZF=1则停止扫描
sub edi, offset szStr
mov nLen, edi
lea edi, szStr ; 指向目标字符串
mov ecx, nLen ; szStr占15个字节
cld ; 地址由低至高
mov al, 'o'
repnz scasb ; ZF=1则停止扫描
jnz c10
mov byte ptr [edi-1], 'O' ; 替换第1个o为O
c10:
lea edi, szStr ; 指向目标字符串
add edi, nLen
dec edi ; 指向最后1个字符'!'
mov ecx, nLen ; szStr占15个字节
std ; 地址由低至高
mov al, ' '
repnz scasb ; ZF=1则停止扫描
jnz c20
mov byte ptr [edi+1], '*' ; 替换最后1个空格为*
c20:
invoke printf, offset szStr ; 显示字符串
invoke printf, offset szLen, nLen ; 显示字符串长度
ret
end start
3.2 内存块复制
MOVSB/W/D将操作数从一个内存单元传送到另一个内存单元。它经常和REP前缀同时使用,将一个内存块(DS:ESI指向的源数据块)复制到另一个内存块(ES:EDI指向的目标数据块)。使用MOVSB时,传送单位为字节;使用MOVSW时,传送单位为字;使用MOVSD时,传送单位为双字。
在Windows环境中,DS、ES已由操作系统设置了,程序中不必考虑DS、ES的赋值。
1. 数组的复制
下面的程序将数组Array1复制给数组Array2。
Array1 dword 1, 10, 100, 1000, 10000
Array2 dword 5 dup (0)
lea esi, Array1
lea edi, Array2
cld
mov ecx, 5
rep movsd
MOVSD每次传送一个双字,传送一次后,ESI、EDI自动加4,指向下一个双字。由于有REP前缀,每次传送后,ECX自动减1。传送5次后,ECX=0时,传送结束,此时Array2和Array1中的5个元素完全相等。如图3-2所示。
ESI=00402144 | 00000001 |
| 00402144 | 00000001 |
00402148 | 0000000A |
| 00402148 | 0000000A |
0040214C | 00000064 | 双字传送 | 0040214C | 00000064 |
00402150 | 000003E8 | ECX=5 | 00402150 | 000003E8 |
00402154 | 00002710 | DF=0 | 00402154 | 00002710 |
EDI=00402158 | 00000000 |
| ESI=00402158 | 00000001 |
0040215C | 00000000 |
| 0040215C | 0000000A |
00402160 | 00000000 |
| 00402160 | 00000064 |
00402164 | 00000000 |
| 00402164 | 000003E8 |
00402168 | 00000000 |
| 00402168 | 00002710 |
0040216C |
|
| EDI=0040216C |
|
开始传送前的状态 |
| 传送完成后的状态 |
图3-2 数据块传送(源数据块和目标数据块没有重叠)
在这个例子中,如果要使用MOVSW,按字为单位传送,则后面两条指令应修改为:
mov ecx, 10
rep movsw
如果要使用MOVSB,按字节为单位传送,则后面两条指令应修改为:
mov ecx, 20
rep movsb
按双字、字、字节都可以完成数据块的传送,应尽量使用按双字的传送方式,其效率最高。特别是源地址和目标地址应设置为4的倍数,即地址按4字节对齐时,完成数据块传送所花的时间最短。
2. 从字符串中删除一个字符
假设一个编辑软件把用户输入的一行字符存储在缓冲区InBuffer中:
InBuffer byte 'Hellox World!', 0
这里的字符’x’是多余的,要把它删掉。如果用块传送指令,指令代码为:
lea esi, InBuffer+6 ; ESI指向字符' '
lea edi, InBuffer+5 ; EDI指向字符'x'
cld ; 地址由低至高
mov ecx, 8 ; 传送8次
rep movsb ; 以字节为单位传送
块传送之前的准备过程和传送完成后的结果如图3-3所示。
InBuffer | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +10 | +11 | +12 | +13 |
| ||
传送前 | H | e | l | l | o |
|
| W | o | r | l | d | ! | 00H |
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送1次后 | H | e | l | l | o | W |
| o | r | l | d | ! | 00H | 00H |
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送2次后 | H | e | l | l | o | W | o |
| r | l | d | ! | 00H | 00H |
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送8次后 | H | e | l | l | o |
| W | o | r | l | d | ! | 00H |
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图3-3 数据块传送(源数据块和目标数据块有重叠,ESI>EDI)
“lea esi, InBuffer+6”也可以写做“mov esi, offset InBuffer+6”,将InBuffer字符串的第6个字符(即’x’)的地址赋给ESI。
从数组中删除一个元素时,同样可以使用这种传送方式。
3. 向字符串中插入一个字符
要插入一个字符到字符串中,需将插入点后面的所有字符向后移动,给这个字符留出空间。这个字符串为:
InBuffer byte 'Hello Wrld!', 0
要把’o’插入字符’W’和’r’中间。需将“rld!”和0向后移动,如果用块传送指令,指令代码为:
lea esi, InBuffer+7 ; ESI指向字符'x'
lea edi, InBuffer+8 ; EDI指向字符' '
cld ; 地址由低至高
mov ecx, 5 ; 传送5次
rep movsb ; 以字节为单位传送
上面指令执行后,InBuffer字符串变为:
Hello Wrrrrrr
因此,这个程序是错误的。数据块的传送过程如图3-4所示。
InBuffer | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +10 | +11 | +12 | +13 |
| ||
传送前 | H | e | l | l | o |
| W |
| l | d | ! | 00H |
|
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送1次后 | H | e | l | l | o |
| W | r |
| d | ! | 00H |
|
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送2次后 | H | e | l | l | o |
| W | r | r |
| ! | 00H |
|
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送5次后 | H | e | l | l | o |
| W | r | r | r | r | r |
|
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图3-4 数据块传送(源数据块和目标数据块有重叠,DF=0,ESI<EDI)
之所以发生这种状况,是因为在传送的过程中,源数据在还没有进行传送就被覆盖了。比如字符’l’应该向后移动一个字节,但该字符在没有传送之前就在第1次传送中被字符’r’覆盖了。
为避免这种问题,首先将字符串的最后一个字节00H向后移动一个字节,再依次传送’!’、’d’、’l’、’r’。正确的传送过程如图4-4所示。
InBuffer | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +10 | +11 | +12 | +13 |
| ||
传送前 | H | e | l | l | o |
| W | r | l | d | ! |
|
|
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送1次后 | H | e | l | l | o |
| W | r | l | d |
| 00H | 00H |
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送2次后 | H | e | l | l | o |
| W | r | l |
| ! | ! | 00H |
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送3次后 | H | e | l | l | o |
| W | r |
| d | d | ! | 00H |
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送4次后 | H | e | l | l | o |
| W |
| l | l | d | ! | 00H |
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
传送5次后 | H | e | l | l | o |
|
| r | r | l | d | ! | 00H |
|
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图4-4 数据块传送(源数据块和目标数据块有重叠,DF=1,ESI<EDI)
传送完成后,“rld!”和0向后移动了1个字节。InBuffer+7就可以用来存放新的字符’o’了。
这样的传送方式,首先传送的是地址最高的源数据,即由高向低的传送方式。DF必须设置为1,每次传送后,ESI和EDI自动地减少。
InBuffer byte 'Hello Wrld!', 0, ?
lea esi, InBuffer+11 ; ESI指向字符00H
lea edi, InBuffer+12 ; EDI指向?所在的位置
std ; 地址由高至低
mov ecx, 5 ; 传送5次
rep movsb ; 以字节为单位传送
cld ; 恢复为"地址由低至高"
mov InBuffer+7, 'o' ; 插入字符'o'
完成传送后,用CLD将DF设置为0。最后,将’o’存入到InBuffer+7中。
向数组中插入一个元素时,也需要使用这种方式。
4. 块传送的3种情况
根据源数据块和目标数据块是否重叠,以及数据块的地址前后顺序,将数据块的传送分为3种情况。如图4-5所示,源数据块用虚线框表示,而目标数据块用实线框表示。
(1) 源数据块和目标数据块不重叠。DF=0或DF=1均可。
(2) 源数据块和目标数据块重叠,目标数据块的地址较小。只能设置DF=0,ESI和EDI分别执行源数据块和目标数据块的第1个单元的地址。
(3) 源数据块和目标数据块重叠,目标数据块的地址较大。只能设置DF=1,ESI和EDI指向源数据块和目标数据块的最后一个传送单位。如果是字节传送,指向最后一个字节;如果是字传送,指向最后一个字;如果是双字传送,指向最后一个双字。
EDI → |
| EDI → |
|
|
|
|
|
| 目标数据块 |
|
|
|
|
|
|
|
| 目标数据块 |
| 源数据块 |
|
| ESI → |
|
|
|
|
ESI → |
|
|
| ESI → |
|
|
源数据块 |
| 源数据块 |
|
|
|
|
|
|
|
| 目标数据块 |
|
|
|
|
|
| EDI → |
|
|
|
| ESI>EDI, DF=0 | ESI<EDI, DF=1 |
| ||
| 不重叠 | 重叠 | 重叠 |
|
图4-5 块传送的3种情况
带REP前缀的块传送的次数为ECX。传送完成后,ECX=0,ESI指向源数据块外的下一个传送单位的地址,EDI指向目标数据块外的下一个传送地址。因为在传送完最后一个单位时,ESI和EDI继续增加(DF=0)或减小(DF=1)。
下面的程序,进行了2次内存块复制操作,对应于上述的第2、3种情况。
;程序清单:memfunc.asm(内存块处理)
.386
.model flat,stdcall
includelib msvcrt.lib
printf PROTO C :dword,:vararg
.data
szFmt byte 'Array[%2d]=%4d', 0ah, 0
Array dword 1, 2, 4, 8, 16, 16, 32, 64, 128, 512, 1024
.code
start:
; +0 +4 +8 +12 +16 +20 +24 +28 +32 +36 +40
;Array dword 1, 2, 4, 8, 16, 16, 32, 64, 128, 512,1024
; 最后5个元素向前移动4个字节, 变为:
;Array dword 1, 2, 4, 8, 16, 32, 64, 128, 512,1024,1024
mov edi, offset Array+20 ; EDI是目标数据块的首地址
mov esi, offset Array+24 ; ESI是源数据块的首地址
mov ecx, 20 ; 数据块的长度
cld ; 地址由低至高
rep movsb ; 传送数据
cmp edi, esi ; 比较源地址和目标地址
jbe f10 ; EDI<=ESI, 由低至高传送
std ; EDI>ESI, 由高至低传送
add esi, ecx ;
dec esi ; ESI指向源数据块的最后1个字节
add edi, ecx
dec edi ; EDI指向目标数据块的最后1个字节
f10:
rep movsb ; 传送数据
; +0 +4 +8 +12 +16 +20 +24 +28 +32 +36 +40
;Array dword 1, 2, 4, 8, 16, 32, 64, 128, 512,1024,1024
; ; 最后2个元素向后移动4个字节
;Array dword 1, 2, 4, 8, 16, 32, 64, 128, 512, 512,1024
mov edi, offset Array+36 ; EDI是目标数据块的首地址
mov esi, offset Array+32 ; ESI是源数据块的首地址
mov ecx, 8 ; 数据块的长度
std
rep movsb ; 传送数据
; +0 +4 +8 +12 +16 +20 +24 +28 +32 +36 +40
;Array dword 1, 2, 4, 8, 16, 32, 64, 128, 512, 512,1024
; ^^ 替换为256
mov Array+32, 256
lea esi, Array ; ESI指向数组的第1个元素
xor ebx, ebx ; EBX为数组下标i
cld ; 地址由低至高
f20:
lodsd ; 取出1个元素至AX, ESI加4.
; printf(szFmt, i, Array[i]);
invoke printf, offset szFmt, ebx, eax
inc ebx ; 数组下标加1
cmp ebx, 11 ; 数组中共有11个元素, 最大下标=10
jb f20 ; 继续处理下一个元素
ret
end start
3.3 字符串插入
字符串就是特殊的数据块,以00H字符结尾。每个字符占1字节,存放的是它的ASCII码值。
下面的程序将一个字符串插入到另一个字符串的中间,两个字符串和插入位置都从键盘输入。
程序中,调用了strlen函数求出字符串的长度,在实验中可以尝试用块操作指令替换strlen函数。还可以增加对输入参数进行检查的条件,如nPos必须小于nLen1等。
;程序清单:inserts.asm(字符串插入)
.386
.model flat,stdcall
includelib msvcrt.lib
printf PROTO C :dword,:vararg
scanf PROTO C :dword,:vararg
strlen PROTO C :dword
.data
szFmt byte 'result = "%s"', 0ah, 0
szStr1 byte 80 dup(0) ; 第1个字符串
szStr2 byte 80 dup(0) ; 第2个字符串
nLen1 dword 0 ; 第1个字符串的长度
nLen2 dword 0 ; 第2个字符串的长度
nPos dword 0 ; 插入位置
szStr byte 160 dup(0) ; 结果字符串
szInFormat byte '%s %s %d', 0
.code
start:
; 输入3个参数, szStr1, szStr2, nPos
invoke scanf, offset szInFormat,
offset szStr1, offset szStr2, offset nPos
; 求第1个字符串的长度
invoke strlen, offset szStr1
mov nLen1, eax
; 求第2个字符串的长度
invoke strlen, offset szStr2
mov nLen2, eax
; 复制第1个字符串szStr1到结果字符串szStr中
lea esi, szStr1
lea edi, szStr
mov ecx, nLen1
cld
rep movsb
; szStr中nPos之后的部分字符串向后移动nLen2个字节
; 为第2个字符串留出位置
mov ecx, nLen1
sub ecx, nPos
lea esi, szStr
add esi, nLen1
dec esi
mov edi, esi
add edi, nLen2
std
rep movsb
; 复制第2个字符串szStr2到结果字符串szStr+nPos处
inc esi
mov edi, esi
lea esi, szStr2
mov ecx, nLen2
cld
rep movsb
invoke printf, offset szFmt, offset szStr
ret
end start
程序执行结果如下,其中第1行的内容为键盘输入,按回车键结束。
abcghijklmnopqrstuvwxyz def 3
result = "abcdefghijklmnopqrstuvwxyz"
3.4 实验题:多个字符串的排序
将多个字符串按升序排序,字符串直接定义在程序的数据区中,并将排序后的结果输出。
要求:
1. 10个字符串定义如下:
str1 byte 'Use', 0
str2 byte 'this', 0
str3 byte 'guide', 0
str4 byte 'with', 0
str5 byte 'ATADRVR', 0
str6 byte 'versions', 0
str7 byte '14', 0
str8 byte 'and', 0
str9 byte '15', 0
str10 byte '.', 0
2. 数据区还需要定义哪些其他的内容,使程序易于编写。比较程序的代码长度(程序大小、源程序长度等)。
3. 如何最大程度地降低字符串的比较次数?
4. 记录程序运行时间,并考虑如何缩短程序运行时间。