除了可以在内存之间拷贝字符串数据外,还可以将内存里的字符串值加载到寄存器,或者反向将寄存器里的值存储回内存位置。下面要介绍的STOS和LODS指令就可以实现这些功能...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第314页到第327页,翻译完前10章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为327 / 577)。

Storing and Loading Strings 存储和加载字符串:

    除了可以在内存之间拷贝字符串数据外,还可以将内存里的字符串值加载到寄存器,或者反向将寄存器里的值存储回内存位置。下面要介绍的STOS和LODS指令就可以实现这些功能。

The LODS instruction LODS指令:

    LODS指令用于将内存里的字符串值加载到EAX寄存器,LODS指令有如下三种指令格式:
  • LODSB: Loads a byte into the AL register
    LODSB指令:加载一个字节的数据到AL寄存器
  • LODSW: Loads a word (2 bytes) into the AX register
    LODSW指令:加载一个字(即2个字节)的数据到AX寄存器
  • LODSL: Loads a doubleword (4 bytes) into the EAX register
    LODSL指令:加载一个双字(即4个字节)的数据到EAX寄存器
    注意:英特尔汇编语法里是使用LODSD指令来加载4个字节的数据到EAX寄存器的,而GNU汇编语法里用的是LODSL指令。

    LODS指令的隐式操作数是ESI寄存器,所以在使用LODS指令时,ESI寄存器必须包含将要加载的字符串数据的内存位置,LODS指令执行后,会根据加载的字节数,自动递增或递减ESI寄存器的值(和MOVS指令一样是根据DF标志位来决定是递增还是递减)。

    稍候要介绍的STOS和SCAS指令的操作数是位于EAX寄存器里的,而LODS指令正好就可以为这些指令加载所需的数据到EAX里,尽管你可以对LODS指令使用REP指令前缀,但是这样做的话,并没有多大的含义,因为EAX最多只能存放4个字节的数据,通过REP重复执行LODS指令只会造成EAX里的数据被重复覆盖。

The STOS instruction STOS指令:

    和LODS指令的操作刚好相反,STOS指令用于将EAX寄存器里的值存储回内存位置,STOS指令根据操作的字节数有三种指令格式:
  • STOSB: Stores a byte of data from the AL register
    STOSB指令:将AL寄存器里的一个字节的数据存储到目标内存位置
  • STOSW: Stores a word (2 bytes) of data from the AX register
    STOSW指令:将AX寄存器里的一个字(即2个字节)的数据存储到目标内存位置
  • STOSL: Stores a doubleword (4 bytes) of data from the EAX register
    STOSL指令:将EAX寄存器里的一个双字(即4个字节)的数据存储到目标内存位置
    STOS指令的隐式操作数是EDI寄存器,当STOS指令执行后,会自动根据存储的字节数,递增或递减EDI寄存器的值。

    单个STOS指令可能作用没那么大,但是当STOS指令和REP指令配合时,就可以用AX里的值来填充一大块内存,例如,下面的stostest1.s程式就演示了如何用REP加STOS指令来将一块256字节的内存都填充为空格字符(对应ASCII码为0x20):

# stostest1.s - An example of using the STOS instruction
.section .data
space:
    .ascii " "
.section .bss
    .lcomm buffer, 256
.section .text
.globl _start
_start:
    nop
    leal space, %esi
    leal buffer, %edi
    movl $256, %ecx
    cld
    lodsb
    rep stosb
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码里,先将space内存里的空格字符加载到AL寄存器,由于ECX设为了256,所以rep stosb指令会将AL里的空格拷贝256次到buffer内存里,从而将buffer里的256字节的数据都填充为空格字符。gdb调试器里的输出情况如下:

15        lodsb
(gdb) s
16        rep stosb
(gdb) print/x $eax
$1 = 0x20
(gdb) x/10bx &buffer
0x80490a0 <buffer>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x80490a8 <buffer+8>:    0x00    0x00
(gdb) s
17        movl $1, %eax
(gdb) x/10bx &buffer
0x80490a0 <buffer>:    0x20    0x20    0x20    0x20    0x20    0x20    0x20    0x20
0x80490a8 <buffer+8>:    0x20    0x20
(gdb)

    上面输出显示,在lodsb指令执行后,EAX里的值就变为0x20即空格字符的ASCII码,在rep stosb指令执行前,buffer里的数据都是默认的0x00(因为buffer在bss段,该段里的数据默认由零填充),在rep stosb执行后,buffer所在的内存就被全部填充为0x20的空格字符。

Building your own string functions 组建你自己的字符串操作函数:

    LODS和STOS指令配合在一起就可以组建自己的字符串操作函数,例如可以将ESI和EDI指向同一个字符串,先用LODS将字符串里的字符加载到AL寄存器,然后对AL进行加减乘除之类的操作,接着就可以用STOS指令将结果写回字符串,这样再在loop循环指令的作用下,就可以对字符串里的所有字符执行相同的操作。

    下面的convert.s程式就演示了如何利用LODS和STOS指令将字符串里的所有字符都转为大写字母:

# convert.s - Converting lower to upper case
.section .data
string1:
    .asciz "This is a TEST, of the conversion program!\n"
length:
    .int 43
.section .text
.globl _start
_start:
    nop
    leal string1, %esi
    movl %esi, %edi
    movl length, %ecx
    cld
loop1:
    lodsb
    cmpb $'a', %al
    jl skip
    cmpb $'z', %al
    jg skip
    subb $0x20, %al
skip:
    stosb
    loop loop1
end:
    pushl $string1
    call printf
    addl $4, %esp
    pushl $0
    call exit
 

    在convert.s程式里,先将string1要操作的字符串的内存位置加载到ESI和EDI,这样ESI与EDI就指向同一个字符串,并且将length字符串的长度值加载到ECX,这样后面的loop1循环体就可以操作字符串里的每个字符,在loop1循环体中,代码先通过LODSB指令将ESI指向的字符加载到AL寄存器,然后通过CMP指令将AL里字符的ASCII码与'a'及'z'进行比较,当AL里的字符的ASCII码位于'a'与'z'之间时,就说明该字符是一个小写字母,接着就可以用subb $0x20, %al指令将小写字母的ASCII值减去0x20,结果就是对应的大写字母,最后由STOSB指令将转换后的结果写回string1字符串,对string1字符串里的每个字符循环执行这种操作,就可以将所有小写字母都转为大写字母。

    这里需要注意的是:无论AL里的字符是否被处理,每个LODS指令都必须对应有一个STOS指令,这样才能确保ESI和EDI的值能够同步递增。

    在汇编链接程序后,在命令行里的输出结果如下:

$ as -gstabs -o convert.o convert.s
$ ld --dynamic-linker /lib/ld-linux.so.2 -lc -o convert convert.o
$ ./convert

THIS IS A TEST, OF THE CONVERSION PROGRAM!
$

    由于convert.s程式里调用了printf的C库函数,所以ld命令需要加上--dynamic-linker /lib/ld-linux.so.2 -lc (这个在之前的章节里多次提到过)。convert程式的输出结果和预期的一致,你可以修改string1测试字符串的内容,然后进行测试,不过修改了字符串的内容后,要对应修改length即字符串的长度值。

Comparing Strings 字符串的比较操作:

    除了上面介绍的字符串的加载和拷贝指令外,汇编里还提供了一些指令可以用于完成字符串的比较操作,下面就介绍下这些指令。

The CMPS instruction CMPS指令:

    CMPS指令集用于比较字符串里的值,CMPS指令同样有三种指令格式:
  • CMPSB: Compares a byte value
    CMPSB指令:比较一个字节的值
  • CMPSW: Compares a word (2 bytes) value
    CMPSW指令:比较一个字(即2个字节)的值
  • CMPSL: Compares a doubleword (4 bytes) value
    CMPSL指令:比较一个双字(即4个字节)的值
    CMPS指令的隐式源操作数为ESI,隐式目标操作数为EDI,每执行一次CMPS指令,ESI和EDI的值都会自动根据比较的字节数递增或递减(同样是由DF标志位来决定是递增还是递减)。

    CMPS指令执行时,会用ESI指向的源字符串里的值减去EDI指向的目标字符串里的值,然后根据结果来设置EFLAGS寄存器里的carry、sign、overflow、zero、parity或adjust标志,所以在CMPS指令执行后,就可以使用条件跳转指令来根据标志位里的值进行一些跳转操作。

    下面的cmpstest1.s程式就演示了CMPS指令的用法:

# cmpstest1.s - A simple example of the CMPS instruction
.section .data
value1:
    .ascii "Test"
value2:
    .ascii "Test"
.section .text
.globl _start
_start:
    nop
    movl $1, %eax
    leal value1, %esi
    leal value2, %edi
    cld
    cmpsl
    je equal
    movl $1, %ebx
    int $0x80
equal:
    movl $0, %ebx
    int $0x80
 

    上面cmpstest1.s程式在开头将EAX设为1,这个1是系统调用号,这样最后一条中断指令int $0x80执行时,就会根据EAX里的1值来退出程序,这里将movl $1, %eax放在开头,这样equal标签处就不需要再给EAX赋值了。接着代码将value1字符串的内存位置加载到ESI作为CMPS指令的源字符串,将value2字符串的内存位置加载到EDI作为CMPS的目标字符串,由于value1和value2里的字符串的有效长度都是4,所以只需一条CMPSL指令就可以一次完成这四个字符的比较操作,由于CMPS指令执行后会设置对应的EFLAGS寄存器里的标志位,因此接着就可以使用 je equal 条件跳转指令判断两字符串是否相等,如果相等即ZF标志位被设置时就跳转到equal标签处,将EBX即程序的退出码设为0,否则就继续执行,将EBX退出码设为1。

    在汇编链接程序后,你可以通过检查程序执行后的退出码来判断字符串比较的结果:

$ as -gstabs -o cmpstest1.o cmpstest1.s
$ ld -o cmpstest1 cmpstest1.o
$ ./cmpstest1
$ echo $?

0
$

    上面输出的退出码为0,表示代码里value1和value2里的字符串是相等的,你可以试着将value1或value2里的4个字符改为不同的值,再进行测试,会发现结果为1以表示两个字符串不相等。

    上面的程式只对4个字符的字符串进行了比较操作,如果字符串的长度超过4的话,就需要用到下面要介绍的REP指令。

Using REP with CMPS 使用REP配合CMPS来进行字符串的比较:

    REP指令前缀虽然可以完成重复比较操作,但是REP只能检测ECX里的值,而CMPS指令还会设置ZF标志位,所以如果在重复比较时还要检测ZF标志位的话,就需要用到上一篇提到的其他REP指令:REPE、REPNE、REPZ或REPNZ指令,这些指令在执行重复比较操作时,除了会检测ECX的值外,还会检测ZF标志位的值。例如:repe cmpsb指令,当cmpsb的比较结果发现字符不相等时,就会将ZF标志位清零,这样repe就不会再继续重复执行cmpsb比较操作了。

    下面的cmpstest2.s程式就演示了REPE和CMPS指令配合完成字符串比较的方式:

# cmpstest2.s - An example of using the REPE CMPS instruction
.section .data
value1:
    .ascii "This is a test of the CMPS instructions"
value2:
    .ascii "This is a test of the CMPS Instructions"
.section .text
.globl _start
_start:
    nop
    movl $1, %eax
    lea value1, %esi
    leal value2, %edi
    movl $39, %ecx
    cld
    repe cmpsb
    je equal
    movl %ecx, %ebx
    int $0x80
equal:
    movl $0, %ebx
    int $0x80
 

    上面代码里,先将value1源字符串的内存位置加载到ESI,将value2目标字符串的内存位置加载到EDI,再将比较字符串的长度39设置到ECX,然后就可以使用repe cmpsb指令进行逐字节的比较操作,当ECX计数器的值减到0,或者ZF标志位被清零时(即两字符串里某个字符不相等时),repe指令就会停止重复执行cmpsb的操作,在repe指令执行后,如果两字符串的所有字符的ASCII码都完全相同则ZF标志位会处于设置状态,否则ZF标志位就会处于清零状态,这样接下来的je equal条件跳转指令就可以根据ZF的值来进行跳转操作。

    如果两字符串不相等,则je equal指令就不会发生跳转,此时39减去ECX计数器里的值就可以得到不相等的字符所在的位置。

    该程式经过汇编链接后,在命令行的输出结果如下:

$ ./cmpstest2
$ echo $?

11
$

    退出码11其实就是ECX计数器的值,用39减去11得到28,说明value1和value2两字符串里的第28个字符是不相同的。

String inequality 字符串大于,等于,小于之类的不等于比较操作:

    前面的例子都只是简单的判断两个字符串是否相等还是不相等,但是当两个字符串不相等时,又如何进一步判断其中一个字符串是大于另一个字符串,还是小于另一个字符串呢?

    字符串和整数不同,在整数里,100大于10这个好理解,但是一个字符串大于另一个字符串就不那么好理解了,最常用的字符串比较方法被称作字典顺序比较法,即在字典里排在越后面的字符,其值越大,下面是字典顺序比较法里的两个基本规则:
  • Alphabetically lower letters are less than alphabetically higher letters
    字母排序较低的字符小于字母排序较高的字符
  • Uppercase letters are less than lowercase letters
    大小字母小于小写字母
   可以看出来,这些规则其实就是ASCII码表里的字符顺序,可以很轻松的将这些规则应用到相同长度的字符串比较里,例如"test"字符串小于"west"字符串,是因为'test'里的't'的ASCII码小于'west'里的'w'的ASCII码。

    如果两个字符串的长度不相同时,则先对两字符串前面相同长度的部分进行比较,如果这部分不相等,则该部分的比较结果就是最后的结果,如果该部分相等,则哪个字符串的原始长度长,哪个就大。

    下面的strcomp.s程式就演示了上面的字符串比较规则(下面的代码和英文原著的代码不相同,不同的地方用红色标注了出来,英文原著的逻辑顺序刚好弄反了,原著的结果虽然正确,但是那个结果纯属巧合,读者可以自己用gdb调试器对原著的代码进行调试分析):

# strcomp.s - An example of comparing strings
.section .data
string1:
    .ascii "test"
length1:
    .int 4
string2:
    .ascii "test1"
length2:
    .int 5
.section .text
.globl _start
_start:
    nop
    lea string1, %esi
    lea string2, %edi
    movl length1, %ecx
    movl length2, %eax
    cmpl %eax, %ecx
    jbe longer
    xchg %ecx, %eax
longer:
    cld
    repe cmpsb
    je equal
    jg greater
less:
    movl $1, %eax
    movl $255, %ebx
    int $0x80
greater:
    movl $1, %eax
    movl $1, %ebx
    int $0x80
equal:
    movl length1, %ecx
    movl length2, %eax
    cmpl %ecx, %eax
    jb greater
    ja less
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的代码里,定义了string1和string2两个要比较的字符串,同时还定义了两个字符串的长度length1和length2,通过退出码的值来判断两个字符串的大小关系,退出码与字符串大小比较的关系如下表所示:

Result Code 退出码 Description 描述
255 string1 is less than string2 
string1小于string2
0 string1 is equal to string2 
string1等于string2
1 string1 is greater than string2 
string1大于string2

    在程式代码的开头部分,一开始先将string1的长度length1设置到ECX,将string2的长度length2设置到EAX,然后对这两个长度值进行比较和交换操作,确保ECX里的值为较短的字符串的长度值,这样后面longer标签处的repe cmpsb指令执行时,就会对string1和string2两字符串前面相同长度的部分进行比较,如果在前面相同长度部分,string1大于string2则就跳转到greater标签处,将退出码设为1,表示第一个字符串大于第二个字符串,如果小于,则进入less标签处,将退出码设为255,如果前面相同长度部分里的字符都相等,则跳转进入equal标签处,再次比较两字符串的长度值,如果哪个字符串长度长,则该字符串就大,如果长度一样,则说明两字符串完全相同,就将退出码设为0 。

    在程序汇编链接后,命令行的输出结果如下:

$ as -gstabs -o strcomp.o strcomp.s
$ ld -o strcomp strcomp.o
$ ./strcomp
$ echo $?

255
$

    退出码为255,说明第一个字符串'test'小于第二个字符串'test1' 。

Scanning Strings 扫描查找字符串:

    汇编里还提供了一种SCAS指令,该指令配合LODS指令,可以完成单个字符或一段字符串的查找工作。

The SCAS instruction SCAS指令:

    SCAS指令也有三种指令格式:
  • SCASB: Compares a byte in memory with the AL register value
    SCASB指令:将内存里一个字节的数据与AL寄存器里的值进行比较
  • SCASW: Compares a word in memory with the AX register value
    SCASW指令:将内存里一个字(即2个字节)的数据与AX里的值进行比较
  • SCASL: Compares a doubleword in memory with the EAX register value
    SCASL指令:将内存里一个双字(即4个字节)的数据与EAX里的值进行比较
    SCAS指令的隐式目标操作数为EDI寄存器,该指令执行时,会用AX里的值与EDI指向的字符串里的值相减,然后根据减法运算的结果来设置EFLAGS里的各种标志位,同时根据比较的字节数,将EDI的值进行递增或递减(还是由DF标志位来决定递增还是递减)。

    SCAS指令需要配合REPE或REPNE指令来完成字符串的搜索工作,REPE和REPNE的搜索效果刚好相反:
  • REPE: Scans the string characters looking for a character that does not match the search character
    SCAS配合REPE指令:会在EDI指向的目标字符串里一直搜索,直到发现某个与AL(或AX或EAX)里的要搜索的值不相等的字符为止
  • REPNE: Scans the string characters looking for a character that matches the search character
    SCAS配合REPNE指令:会在目标字符串里一直搜索,直到发现某个与AL(或AX或EAX)里的要搜索的值相等的字符为止
    多数情况下,都会使用REPNE来查找需要搜索的字符。

    下面的scastest1.s程式就演示了如何使用repne scasb指令来搜索指定的字符:

# scastest1.s - An example of the SCAS instruction
.section .data
string1:
    .ascii "This is a test - a long text string to scan."
length:
    .int 44
string2:
    .ascii "-"
.section .text
.globl _start
_start:
    nop
    leal string1, %edi
    leal string2, %esi
    movl length, %ecx
    lodsb
    cld
    repne scasb
    jne notfound
    subw length, %cx
    neg %cx
    movl $1, %eax
    movl %ecx, %ebx
    int $0x80
notfound:
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码的作用是从string1字符串里查找出 '-' 字符的位置。代码开头先用LEA指令将string1的内存位置加载到EDI,同时将string2的内存位置加载到ESI,这样接下来的LODSB指令就可以将ESI指向的string2里的'-'字符的ASCII码加载到AL里,后面的scasb指令就可以根据AL里的值进行查找操作。

    repne scasb指令会重复将AL里的'-'字符与EDI指向的string1字符串里的字符进行比较,直到找到'-'字符为止,在找到该字符时,会对应设置ZF标志位,这样jne notfound就不会发生跳转,此时string1字符串的长度length减去CX计数器里的值就可以得到string1字符串里'-'字符的位置了,代码里subw length, %cx指令是用CX减去length,之所以是CX减length,是因为length立即数不能作为SUB指令的目标操作数,减法的结果是一个负值,所以还需要用neg指令将CX变为正值,最后就可以将该值作为退出码返回,如果没有找到所需的字符,则jne notfound就会跳转到notfound标签处,退出码就会是0 。

    下面是程式运行的结果:

$ as -gstabs -o scastest1.o scastest1.s
$ ld -o scastest1 scastest1.o
$ ./scastest1
$ echo $?

16
$

    上面输出结果显示,退出码为16,说明string1字符串里的第16个字符就是需要搜索的'-'字符。

Scanning for multiple characters 搜索多个字符:

    我们可以使用SCASW指令来每次搜索2个字符,或者使用SCASL指令来每次搜索4个字符,但是,这两个指令在实际使用时,可能并不会产生预期的效果,例如下面的scastest2.s程式:

# scastest2.s - An example of incorrectly using the SCAS instruction
.section .data
string1:
    .ascii "This is a test - a long text string to scan."
length:
    .int 11
string2:
    .ascii "test"
.section .text
.globl _start
_start:
nop
    leal string1, %edi
    leal string2, %esi
    movl length, %ecx
    lodsl
    cld
    repne scasl
    jne notfound
    subw length, %cx
    neg %cx
    movl $1, %eax
    movl %ecx, %ebx
    int $0x80
notfound:
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的scastest2.s程式会尝试在string1里查找"test"字符串,和之前的scastest1.s程式整体结构上没多大改动,只是这里用的是SCASL指令,一次可以搜索4个字符,所以length长度值是11即string1有效长度44的四分之一,下面是程式实际运行的结果:

$ ./scastest2
$ echo $?

0
$

    结果显示退出码为0,说明该程式并没有在string1里找到"test"字符串,但是string1里又确实存在"test",这是为什么呢?下面这幅图就说明了原因:


图1

    上图显示,REPNE SCASL指令是以4个字节为单位进行查找的,第一次比较的是"This",然后EDI的值会递增4,所以第二次比较的就会是" is ",第三次比较的是"a te",第四次比较的是"st -",所以也就自然找不到test了。

    虽然SCASW与SCASL指令不能很好的完成字符串的查找工作,但是这两个指令在搜索非字符串类型的数组成员时,却很有用,SCASW指令适合于在每个成员都为2字节的数组里搜索2字节的数据,SCASL指令则适合于4字节成员的数组里搜索4字节的数据。

Finding a string length 找出字符串的有效长度:

    SCAS指令的另一个重要的作用是可以检测出字符串的有效长度值,这里的字符串指的是以零结尾的字符串,这种字符串是C语言里最常用的字符串类型,在GNU汇编里,可以通过.asciz伪指令来声明一个以零结尾的字符串。下面的strsize.s程式就演示了SCAS指令检测字符串有效长度的方法:

# strsize.s - Finding the size of a string using the SCAS instruction
.section .data
string1:
    .asciz "Testing, one, two, three, testing.\n"
.section .text
.globl _start
_start:
    nop
    leal string1, %edi
    movl $0xffff, %ecx
    movb $0, %al
    cld
    repne scasb
    jne notfound
    subw $0xffff, %cx
    neg %cx
    dec %cx
    movl $1, %eax
    movl %ecx, %ebx
    int $0x80
notfound:
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码将要检测的目标字符串string1的内存位置加载到EDI,同时将ECX设为0xffff,表示该程式只能检测有效长度小于65,535的字符串,在repne scasb指令执行后,0xffff减去CX的值就可以得到结尾零的位置,再用该位置减1就可以得到字符串的有效长度值,下面是程式的运行结果:

$ ./strsize
$ echo $?

35
$

    输出退出码为35,说明string1里字符串的有效长度值就为35 。

    剩下就是一些原著第10章的总结部分,限于篇幅就不多说了。

    下一篇开始介绍汇编里函数的定义和使用。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 汇编函数的定义和使用 (一)

上一篇: 汇编字符串操作 (一)

相关文章

汇编数据处理 (二)

汇编开发示例 (一)

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

汇编开发示例 (二) 示例介绍结束篇

优化汇编指令 (三)

汇编数据处理 (三) 浮点数