本文由zengl.com站长对
http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
另外再附加一个英特尔英文手册的共享链接地址:
http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)
本篇翻译对应汇编教程英文原著的第304页到第314页,对应原著第10章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为314 / 577)。
很多高级编程语言都提供了和字符串操作相关的函数,在汇编里也提供了一些和字符串操作相关的指令,这些字符串操作指令除了可以用于普通的字符串操作外,还可以对内存块里连续的其他类型的数据进行操作,下面就依次介绍这些指令的用法。
Moving Strings 字符串移动传值指令:
前面的章节我们介绍过MOV指令,不过该指令的源操作数与目标操作数不能同时都为内存位置,所以不方便在两内存位置之间直接拷贝字符串数据。幸运的是,IA-32平台提供了MOVS指令,该指令就可以直接将某内存里的数据拷贝传值到另一个内存位置。
The MOVS instruction MOVS指令:
MOVS指令提供了一种简单的方式,可以直接将某内存里的字符串数据拷贝到另一个内存位置,该指令的格式如下:
-
MOVSB: Moves a single byte
MOVSB指令:拷贝一个字节的数据
-
MOVSW: Moves a word (2 bytes)
MOVSW指令:拷贝一个字(即两个字节)的数据
-
MOVSL: Moves a doubleword (4 bytes)
MOVSL指令:拷贝一个双字(即4个字节)的数据
注意:在英特尔汇编语法里用的是MOVSD指令来拷贝双字大小的数据,而GNU汇编语法里则用的是MOVSL指令。
MOVS指令的源操作数与目标操作数都是隐式声明的,其中,隐式的源操作数为ESI寄存器,用于指向需要拷贝的源字符串的内存位置,隐式的目标操作数为EDI寄存器,用于指向字符串需要拷贝到的目标内存位置。有个简单的方法来记住这两个隐式的操作数,源操作数Source的首字母为S,对应ESI寄存器,目标操作数Destination的首字母为D,对应EDI寄存器。
在GNU汇编里,要将内存位置加载到ESI和EDI里,有两种方法:一种是使用MOV指令,一种则是使用LEA指令。
下面是MOV指令加载内存位置到EDI的示例:
上面指令用于将output标签对应的32位内存位置加载到EDI寄存器,需要在output标签名前面加上美元符,以表示该标签所在的内存位置。
将MOV指令换成LEA指令,则对应的示例代码如下:
LEA指令直接就可以得到源操作数所在的内存位置,所以这里output标签名前面就不需要加美元符了。
下面的movstest1.s程式演示了MOVS指令的用法:
# movstest1.s - An example of the MOVS instructions
.section .data
value1:
.ascii "This is a test string.\n"
.section .bss
.lcomm output, 23
.section .text
.globl _start
_start:
nop
leal value1, %esi
leal output, %edi
movsb
movsw
movsl
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面的代码里,先将value1的内存位置通过LEA指令设置到ESI寄存器,再将output所在内存位置设置到EDI寄存器,这样就为接下来的MOVS指令准备好了源操作数和目标操作数,最后依次测试MOVSB,MOVSW及MOVSL指令。
movstest1.s程式经过汇编链接后,在调试器里的输出情况如下:
$ as -gstabs -o movstest1.o movstest1.s
$ ld -o movstest1 movstest1.o
$ gdb -q movstest1
Reading symbols from /home/zengl/Downloads/asm_example/movs/movstest1...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file movstest1.s, line 11.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/movs/movstest1
Breakpoint 1, _start () at movstest1.s:11
11 leal value1, %esi
(gdb) s
12 leal output, %edi
(gdb) s
13 movsb
(gdb) s
14 movsw
(gdb) x/s &output
0x80490b0 <output>: "T"
(gdb) s
15 movsl
(gdb) x/s &output
0x80490b0 <output>: "Thi"
(gdb) s
16 movl $1, %eax
(gdb) x/s &output
0x80490b0 <output>: "This is"
(gdb) |
由于output标签位于bss段,里面的数据默认都是清零的,所以在MOVS指令执行后,output指向的字符串就自动是以0结尾了,这样x/s命令就可以查看到output里完整的字符串信息。
在执行完MOVSB指令后,该指令将value1字符串里的第一个字节(即第一个字符)拷贝到output内存位置,所以该指令执行后,output里的字符串就为"T"。
在MOVSW指令执行后,output里的字符串值是"Thi",而不是"Th",这是因为每次执行完MOVS指令后,ESI和EDI寄存器里的值会自动递增或递减,至于是递增还是递减,是由EFLAGS寄存器里的DF标志来决定的,当DF标志位被清零时,MOVS指令执行后,ESI和EDI寄存器的值就会根据拷贝的字节数进行递增,当DF标志位被设置时,MOVS执行后,ESI和EDI的值就会递减。
上面的movstest1.s程式里由于没有手动设置DF标志位,所以该标志位就是默认的零。所以在MOVS指令执行后,ESI和EDI的值就会递增。
如果需要手动设置DF标志位,则可以使用下面的两条指令:
-
CLD to clear the DF flag
CLD指令:将DF标志位清零
-
STD to set the DF flag
STD指令:设置DF标志位
由于STD指令在设置DF标志位后,ESI和EDI的值在MOVS指令执行后会递减,所以这种情况下,就需要将ESI和EDI指向源字符串和目标内存的结束位置,下面就用movstest2.s程式来进行演示:
# movstest2.s - A second example of the MOVS instructions
.section .data
value1:
.ascii "This is a test string.\n"
.section .bss
.lcomm output, 23
.section .text
.globl _start
_start:
nop
leal value1+22, %esi
leal output+22, %edi
std
movsb
movsw
movsl
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面的代码将value1加22的值即源字符串的最后一个字符的位置加载到ESI寄存器,同时将output加22即要拷贝到的目标内存的结束位置加载到EDI寄存器,然后使用STD指令设置DF标志位,这样MOVS指令在执行后,ESI和EDI的值就会递减,最后依次测试MOVSB,MOVSW,MOVSL这三条指令。
在三条MOVS指令执行完后,gdb调试器里的输出结果如下:
(gdb) x/23bx &output
0x80490b0 <output>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x80490b8 <output+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x80490c0 <output+16>: 0x00 0x00 0x00 0x6e 0x67 0x2e 0x0a
(gdb)
|
可以看到最后一条MOVSL指令拷贝的四个字节的数据覆盖掉了前两个MOVSB和MOVSW指令拷贝的数据,这是因为无论DF标志位是设置状态还是清零状态,MOVS指令始终是从当前的ESI和EDI位置开始向前(本例中就是向高地址)方向进行拷贝的,只是在MOVS指令执行后,会根据拷贝的字节数来递增或递减ESI和EDI的值。MOVSB指令只会拷贝和递减一个字节,所以MOVSB指令拷贝的结果会被MOVSW指令覆盖掉,同理MOVSW只会拷贝和递减两个字节,所以MOVSW拷贝的结果会被MOVSL指令覆盖掉,如下图所示:
图1
所以如果你三条MOVS指令都使用MOVSB或都使用MOVSW,又或者都使用的MOVSL指令的话,就不会出现相互覆盖的情况。
如果要拷贝一段完整的字符串的话,首先可以想到的方法就是将MOVS指令放在一个loop循环里,将字符串的长度信息放在ECX里,这样就可以控制循环拷贝操作,下面的movstest3.s程式就演示了这种方法:
# movstest3.s - An example of moving an entire string
.section .data
value1:
.ascii "This is a test string.\n"
.section .bss
.lcomm output, 23
.section .text
.globl _start
_start:
nop
leal value1, %esi
leal output, %edi
movl $23, %ecx
cld
loop1:
movsb
loop loop1
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面代码将value1源字符串的内存位置加载到ESI,将output目标内存位置加载到EDI,然后通过movl $23, %ecx指令将ECX设置为源字符串的有效长度,接着手动调用CLD指令将DF标志位清零,这样就能以递增的方式拷贝字符串,loop1对应的循环体会循环执行MOVSB指令,直到将源字符串里的23个字符都拷贝到output目标内存才退出循环,在循环结束后,gdb调试器里的输出结果如下:
(gdb) x/s &output
0x80490b0 <output>: "This is a test string.\n"
(gdb) |
输出结果和预期的一致,尽管可以使用loop循环的方式来完成字符串的拷贝,但英特尔还是提供了一种更为简单的方式:即REP指令前缀。
The REP prefix REP指令前缀:
REP指令前缀会根据ECX里的值,反复执行紧跟在REP后面的指令,从而可以产生loop循环的效果。
Moving a string byte by byte 逐字节的拷贝字符串:
下面通过reptest1.s程式来演示REP加MOVSB指令逐字节的拷贝字符串的方式:
# reptest1.s - An example of the REP instruction
.section .data
value1:
.ascii "This is a test string.\n"
.section .bss
.lcomm output, 23
.section .text
.globl _start
_start:
nop
leal value1, %esi
leal output, %edi
movl $23, %ecx
cld
rep movsb
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面代码先将ECX的值设为23,这样REP指令就会将MOVSB指令反复执行23次,从而将value1源字符串里的23个字符都拷贝到output目标内存里,不过,和loop循环指令不同的是,在调试器里,只要单步执行一次,REP就可以在内部,一次完成所有的23次拷贝操作,可以从下面调试器的输出结果里查看到:
$ as -gstabs -o reptest1.o reptest1.s
$ ld -o reptest1 reptest1.o
$ gdb -q reptest1
Reading symbols from /home/zengl/Downloads/asm_example/movs/reptest1...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file reptest1.s, line 11.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/movs/reptest1
Breakpoint 1, _start () at reptest1.s:11
11 leal value1, %esi
(gdb) s
12 leal output, %edi
(gdb) s
13 movl $23, %ecx
(gdb) s
14 cld
(gdb) s
15 rep movsb
(gdb) s
16 movl $1, %eax
(gdb) x/s &output
0x80490b0 <output>: "This is a test string.\n"
(gdb) |
从上面的输出里可以看到,gdb中只用s命令单步执行了一次rep movsb指令,就将源字符串里所有23个字节的数据都拷贝到output目标内存里了。
Moving strings block by block 逐块的拷贝字符串:
除了可以使用MOVSB指令逐字节的拷贝字符串外,还可以使用MOVSW或MOVSL指令以2个字节或4个字节的方式,逐块的拷贝字符串,例如,拷贝一个含有8个字节的字符串,可以将ECX设为8,使用MOVSB指令来完成,也可以将ECX设为4,使用MOVSW指令来完成,还可以将ECX设为2,通过MOVSL指令来完成。
不过在使用MOVSW或MOVSL指令进行逐块拷贝时,需要注意的是,不要超出了字符串的有效范围。
下面的reptest2.s程式在拷贝时,就超出了字符串的有效范围:
# reptest2.s - An incorrect example of using the REP instruction
.section .data
value1:
.ascii "This is a test string.\n"
value2:
.ascii "Oops"
.section .bss
.lcomm output, 23
.section .text
.globl _start
_start:
nop
leal value1, %esi
leal output, %edi
movl $6, %ecx
cld
rep movsl
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面的代码里,value1源字符串只有23个有效字符,但是ECX被设置为了6,所以rep movsl指令就会将movsl执行6次,每次拷贝4个字节,结果就会拷贝24个字节,这样拷贝操作就会超过value1字符串的有效范围,从而将value2里的首字母也拷贝到output目标内存里,调试器里的输出结果如下:
$ as -gstabs -o reptest2.o reptest2.s
$ ld -o reptest2 reptest2.o
$ gdb -q reptest2
Reading symbols from /home/zengl/Downloads/asm_example/movs/reptest2...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file reptest2.s, line 13.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/movs/reptest2
Breakpoint 1, _start () at reptest2.s:13
13 leal value1, %esi
(gdb) s
14 leal output, %edi
(gdb) s
15 movl $6, %ecx
(gdb) s
16 cld
(gdb) s
17 rep movsl
(gdb) s
18 movl $1, %eax
(gdb) x/s &output
0x80490b8 <output>: "This is a test string.\nO"
(gdb) |
可以看到,output目标内存里将value2字符串里的首字母也包含了进去,这并不是我们所期望的结果。
Moving large strings 拷贝大的字符串数据:
在拷贝大的字符串数据时,最好能尽可能多的使用MOVSL指令,因为该指令一次可以拷贝4个字节的数据,从而可以提高拷贝的效率,为了避免出现上面reptest2.s程式的问题,可以使用一个技巧:将字符串的有效长度除以4,得到的商作为REP MOVSL指令的ECX计数器值,而余数部分则使用MOVSB指令(该余数最多不会超过3)。下面的reptest3.s程式就演示了这种作法:
# reptest3.s - Moving a large string using MOVSL and MOVSB
.section .data
string1:
.asciz "This is a test of the conversion program!\n"
length:
.int 43
divisor:
.int 4
.section .bss
.lcomm buffer, 43
.section .text
.globl _start
_start:
nop
leal string1, %esi
leal buffer, %edi
movl length, %ecx
shrl $2, %ecx
cld
rep movsl
movl length, %ecx
andl $3, %ecx
rep movsb
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面代码还是先将string1源字符串的内存位置加载到ESI,并将buffer目标内存的位置加载到EDI,接着将length源字符串的有效长度设置到ECX,再通过shrl $2, %ecx指令,将ECX里的值右移两位,从而产生除以4的效果,右移后的结果表示除以4的商,存储到ECX里,这样rep movsl就可以根据ECX里的商值将字符串里大部分的数据拷贝到buffer,然后再由movl length, %ecx和andl $3, %ecx两条指令得到length除以4的余数(这里是用and指令来完成取余操作),得到余数后,剩下的字符就可以用MOVSB指令拷贝到buffer目标内存。
当rep movsl指令执行完后,buffer里的值如下:
20 rep movsl
(gdb) s
21 movl length, %ecx
(gdb) x/s &buffer
0x80490d8 <buffer>: "This is a test of the conversion program"
(gdb) |
可以看到MOVSL将源字符串里的前40个字节都拷贝到了buffer里,在rep movsb指令执行后,buffer里的值如下:
23 rep movsb
(gdb) s
24 movl $1, %eax
(gdb) x/s &buffer
0x80490d8 <buffer>: "This is a test of the conversion program!\n"
(gdb) |
可以看到,MOVSB指令将最后两个字符也拷贝到了buffer中,从而将字符串完整的拷贝到目标内存里,读者可以将代码里的字符串内容及length字符串长度进行修改,然后进行更多的测试。
Moving a string in reverse order 反向拷贝字符串:
我们可以通过设置DF标志位,来实现反向拷贝字符串,即从字符串的末尾拷贝到字符串的开头,下面的reptest4.s程式就实现了反向拷贝:
# reptest4.s - An example of using REP backwards
.section .data
value1:
.asciz "This is a test string.\n"
.section .bss
.lcomm output, 24
.section .text
.globl _start
_start:
nop
leal value1+22, %esi
leal output+22, %edi
movl $23, %ecx
std
rep movsb
movl $1, %eax
movl $0, %ebx
int $0x80
|
上面代码将value1和output的结束位置依次加载到ESI和EDI,将ECX设置为value1源字符串的有效长度,接着通过STD指令设置DF标志位,这样MOVSB指令每执行一次,就会将ESI和EDI的值进行递减,在REP前缀的作用下,就可以反向将value1里的23个字符全部拷贝到output内存位置,下面是调试器里的输出情况:
13 movl $23, %ecx
(gdb) s
14 std
(gdb) s
15 rep movsb
(gdb) s
16 movl $1, %eax
(gdb) x/s &output
0x80490b0 <output>: "This is a test string.\n"
(gdb) |
可以看到这里的rep movsb指令也是单步执行一次就完成了,output里的结果和预期的一致。
Other REP instructions 其他的REP指令:
上面介绍的REP指令只会监测ECX的值,而下表显示的几个REP指令除了会监测ECX的值外,还会监测ZF标志位:
Instruction 指令 |
Description 描述 |
REPE |
Repeat while equal
当相等时则重复执行REPE后面的指令 |
REPNE |
Repeat while not equal
当不相等时则重复执行REPNE后面的指令 |
REPNZ |
Repeat while not zero
当结果不为零时则重复执行REPNZ后面的指令 |
REPZ |
Repeat while zero
当结果为零时则重复执行REPZ后面的指令 |
|
REPE和REPZ指令都是当CX为0或ZF标志为0时结束迭代操作,所以REPE和REPZ指令有相同的效果,REPNE和REPNZ指令则都是当CX为0或ZF标志为1时结束迭代操作,所以REPNE和REPNZ指令具有相同的效果。
这些REPE之类的指令主要用于字符串的比较和查找操作,字符串的比较和查找相关的内容将在后面的章节中进行介绍。
OK,本篇就到这里,下一篇介绍其他的和字符串操作相关的汇编指令。
转载请注明来源:www.zengl.com
休息,休息一下 o(∩_∩)o~~