很多高级编程语言都提供了和字符串操作相关的函数,在汇编里也提供了一些和字符串操作相关的指令,这些字符串操作指令除了可以用于普通的字符串操作外,还可以对内存块里连续的其他类型的数据进行...

    本文由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的示例:

movl $output, %edi

    上面指令用于将output标签对应的32位内存位置加载到EDI寄存器,需要在output标签名前面加上美元符,以表示该标签所在的内存位置。

    将MOV指令换成LEA指令,则对应的示例代码如下:

leal output, %edi

    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~~
上下篇

下一篇: 汇编字符串操作 (二) 字符串操作结束篇

上一篇: 高级数学运算 (四) 高级运算结束篇

相关文章

Moving Data 汇编数据移动 (四) 结束篇 栈操作

汇编流程控制 (三) 流程控制结束篇

优化汇编指令 (三)

基本数学运算 (一)

全书结束篇 使用IA-32平台提供的高级功能 (四) SSE2、SSE3相关指令

使用内联汇编 (三) 内联汇编结束篇