上一篇介绍了将数据由内存传值到寄存器中,本篇介绍其他的传值方式...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

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

    上一篇介绍到了将数据由内存传值到寄存器中,本篇继续介绍其他的传值方式。

Moving data values from a register to memory (将数据由寄存器传值到内存中):

    下面是将值由寄存器传递到内存中的例子:

movl %ecx, value

    上面的指令会将ECX中4个字节的数据设置到value标签引用的内存位置处,下面是完整的示例:

# movtest2.s – An example of moving register data to memory
.section .data
    value:
        .int 1
.section .text
.globl _start
    _start:
        nop
        movl $100, %eax
        movl %eax, value
        movl $1, %eax
        movl $0, %ebx
        int $0x80

    将上面的movtest2.s代码使用-gstabs参数进行汇编链接,接着在调试器中运行结果如下:

$ as -o movtest2.o -gstabs movtest2.s
$ ld -o movtest2 movtest2.o
$ gdb -q movtest2

(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file movtest2.s, line 11.
(gdb) run
Starting program: /home/rich/palp/chap05/movtest2

Breakpoint 1, _start () at movtest2.s:11
11     movl $100, %eax
Current language: auto; currently asm

(gdb) x/d &value
0x804908c <value>: 1
(gdb) s
12     movl %eax, value
(gdb) s
13     movl $1, %eax
(gdb) x/d &value
0x804908c <value>: 100
(gdb)

    从上面的输出可以看出来,在执行 movl $100,%eaxmovl %eax,value 指令之前,value标签对应的内存位置的值为1 ,在指令执行后,value内存位置的值就变为100了,说明EAX寄存器里的值成功设置到value内存位置了。

    使用标签名的方式只能访问到内存中单一的数据元素,如果你的程序中有多个数据构成的数组的话,要访问数组中的某个数据就需要使用内存寻址中的索引技术。

Using indexed memory locations (使用索引的内存寻址方式):

    例如下面这个数组:

values:
    .int 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60

    上面定义了一个由values标签指向的包含11个整数的数组,数组中每个整数占4个字节,那么如果你想引用该数组中的某个数据的话,就需要使用索引的寻址模式,该模式由以下四个部分组成:
  • A base address
    一个基地址
  • An offset address to add to the base address
    一个相对于基地址的偏移地址
  • The size of the data element
    数组中单个数据元素的字节大小
  • An index to determine which data element to select
    需要访问的数据在数组中的索引
    索引模式对应的汇编表达式的格式如下:

base_address(offset_address, index, size)

    要计算出该格式对应的内存地址,可以使用下面的方法:

base_address + offset_address + index * size

    base_address(offset_address, index, size) 该格式中的四个元素,如果任何一个元素值为0,都可以留空(不过中间的逗号不可以少),另外offset_address和index必须使用寄存器的形式,size则可以是整数值,例如要访问前面的values数组中的第三个元素20,可以使用下面的指令:

movl $2, %edi
movl values(, %edi, 4), %eax

    上面的代码中使用EDI寄存器作为index索引,所以先将20在数组中的索引值2传递给EDI寄存器(数组的索引值是从0开始的,0代表第一个元素,1代表第二个,以此类推),接着使用movl values(,%edi,4),%eax来访问values数组中的第三个数据20,并将20传递给EAX寄存器,这里offset_address值为0,所以就留空,但是offset_address后面的逗号不可以少,最后一个4表示数组中每个整数都是4个字节的大小。下面是完整的示例代码:

# movtest3.s – Another example of using indexed memory locations
.section .data
output:
    .asciz "The value is %d\n"
values:
    .int 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60
.section .text
.globl _start
_start:
    nop
    movl $0, %edi
loop:
    movl values(, %edi, 4), %eax
    pushl %eax
    pushl $output
    call printf
    addl $8, %esp
    inc %edi
    cmpl $11, %edi
    jne loop
    movl $0, %ebx
    movl $1, %eax
    int $0x80

    汇编链接该程序(因为使用了C的printf函数,所以链接时需要指定C的标准库和第三方的动态库加载器):

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

$ ./movtest3
The value is 10
The value is 15
The value is 20
The value is 25
The value is 30
The value is 35
The value is 40
The value is 45
The value is 50
The value is 55
The value is 60

$

    movtest3程序循环将values数组中的每个元素打印出来,它使用EDI作为索引,每次打印完一条数据后,就通过INC指令将EDI的索引值加1,这样下一次就会将后面的数据给打印出来,代码中的指令如CMPL比较指令,JNE跳转指令等都会在后面的章节中进行详细介绍,这里只需要了解索引寻址模式的用法,其余的东东先留个印象即可。

    小提示:本例只是个演示程序,所以在循环比较时,使用了硬编码的方式,通过指定明确的数11来进行循环判断是否到达了数组的最后一个元素,在实际的应用开发中,最好是能够动态的检测出数组的元素个数,然后根据检测的结果进行操作,这样可以增加代码的灵活性。

Using indirect addressing with registers (寄存器间接寻址):

    寄存器除了可以存放具体的操作数据外,还可以存放内存地址,当寄存器中存放了内存地址时,该寄存器就可以成为访问数据的指针,在程序中就可以使用该寄存器指针来访问内存中的数据,这种内存寻址方式就叫做寄存器间接寻址。

    寄存器间接寻址技术就像C和C++中的指针技术,通过动态的改变寄存器里的内存地址,从而可以使用单一的寄存器来访问多个内存地址,增加了内存寻址的灵活性。

    下面是将内存地址传递给寄存器的例子:

movl $values, %edi

    代码中values标签名前面添加了一个美元符,表示将values引用的内存地址传递给EDI寄存器,如果没有美元符,得到的就是内存地址里存放的值,而非内存地址了。

    小提示:在平坦内存模式下,所有的内存地址都是32位的数,所以上面代码中使用的是movl以l为后缀,将32位地址值传递给32位的EDI寄存器。

    在前面的章节中写过一个cpuid.s的程序,在该程序中有两条代码就用到了寄存器间接寻址方式:

movl $output, %edi
movl %ebx, (%edi)

    上面代码中,先将output标签所在内存地址传递给EDI寄存器(因为这里的output使用了美元符为前缀),然后将EBX寄存器里的值传递到EDI引用的内存位置处,这里需要注意的是第二条代码中EDI寄存器用括号括起来了,这表示将访问的是EDI里所引用的内存位置,如果没有括号的话,那么就是直接访问EDI寄存器:movl %ebx,%edi ,左边这条指令结果就是将EBX寄存器里的值传递给EDI寄存器,而不是传递给EDI所引用的内存位置。

    在上例中,如果想访问EDI加4的内存地址,在GNU汇编器中必须写成4(%edi)的形式,即数字必须写在括号外面,如果要访问EDI减4的内存地址,则须写成-4(%edi)的形式:

movl %edx, 4(%edi)
movl %edx, -4(%edi)

    下面程序,movtest4.s是寄存器间接寻址的完整例子:

# movtest4.s – An example of indirect addressing
.section .data
values:
    .int 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60
.section .text
.globl _start
_start:
    nop
    movl values, %eax
    movl $values, %edi
    movl $100, 4(%edi)
    movl $1, %edi
    movl values(, %edi, 4), %ebx
    movl $1, %eax
    int $0x80

    使用-gstabs参数进行汇编,然后链接成可执行程序,最后放在gdb中运行:

$ gdb -q movtest4
(gdb) break *_start+1

Breakpoint 1 at 0x8048075: file movtest4.s, line 10.
(gdb) run
Starting program: /home/rich/palp/chap05/movtest4

Breakpoint 1, _start () at movtest4.s:10
10     movl values, %eax
Current language: auto; currently asm

(gdb)

    gdb在断点中断下来,在没执行实际的代码之前,我们先使用'x'命令来查看下values标签对应的内存位置里存储的值:

(gdb) x/4d &values
0x804909c <values>:     10     15     20     25

    x/4d表示将values内存位置处的前4个数以十进制的形式显示出来,values前面使用了&符号,必须使用此符号,才能将values标签所在的内存地址传给x命令,才可以查看到values内存位置里的值。接下来,我们将values里的值传递给EAX寄存器:

10     movl values, %eax
(gdb) s
11     movl $values, %edi
(gdb) print $eax
$1 = 10
(gdb)

    通过s命令单步执行前面的movl values,%eax指令,执行完后,从打印的结果可以看到,EAX寄存器里的值就变为了10,和values数组里的第一个数据是一样的。

    接着执行movl $values,%edi指令:

11     movl $values, %edi
(gdb) s
12     movl $100, 4(%edi)
(gdb) print/x $edi
$2 = 0x804909c
(gdb)

    s单步执行完后,EDI寄存器里就应该存放了values标签所引用的内存地址,通过print命令可以看到EDI寄存器里的值是0x804909c ,再和前面的x/4d &values命令输出的结果进行比对,可以看到values内存地址也是0x804909c ,所以说明EDI已经成为存放有values内存地址的指针了,接着就可以使用EDI寄存器间接寻址的方式来访问values数组里的元素了:

12     movl $100, 4(%edi)
(gdb) s
13     movl $1, %edi
(gdb) x/4d &values
0x804909c <values>:     10     100     20     25
(gdb)

    s单步执行完前面的movl $100,4(%edi)指令后,通过x/4d &values命令可以看到,values数组中的第二个元素已经由原来的15变为了100 。EDI指向的是values内存地址即第一个元素10的地址,再加上values数组中每个数都占4个字节,所以4(%edi)指向的就是values数组中的第二个元素了。

    接着我们将EDI寄存器设置为索引值1,采用索引的内存寻址方式来访问刚才设置过的values数组的第二个元素,将该元素的值传递给EBX寄存器,以设置程序退出时的退出码。

movl $1, %edi
movl values(, %edi, 4), %ebx
movl $1, %eax
int $0x80

    通过int $0x80和EAX里的功能号1来退出程序,EBX里存放有退出码,在本例中是100,在命令行下要查看程序的退出码,可以用一个特殊的环境变量即 $? 如下所示:

$ ./movtest4
$ echo $?

100
$

    从输出可以看到movtest4的退出码为100 。

Conditional Move Instructions (条件传值指令):

    普通的MOV指令已经很强大了,但是仍然有可改进的空间,多年来,英特尔不断改进IA-32平台,让汇编开发变得更容易,其中conditional move(条件传值指令)就是这些改进之一,该类指令是从奔腾处理器家族开始引入的(奔腾Pro , 奔腾2 及更新的处理器下都可用)。

    顾名思义,条件传值指令就是指在特定的条件下才会发生传值操作的指令,我们先用原来的普通传值指令来举个例子:

    dec %ecx
    jz continue
    movl $0, %ecx
continue:

    上面这段代码,先使用dec指令将ECX寄存器里的值减一,然后通过jz指令判断ECX里的值是否为0,如果已经为0了,就跳到continue标签处继续执行,反之不为0,就通过movl $0,%ecx指令将ECX寄存器的值变为0,这里就存在一个条件判断,当ECX已经为0的情况下就不执行MOV指令,在ECX不为0的情况下就执行MOV指令。

    其实这段代码完全可以用一个条件传值指令来实现,使用条件传值指令既可以减少代码量,又可以增加处理器的指令预取缓存能力,从而提高应用程序的执行速度。

    下面就详细的描述条件传值指令。

The CMOV instructions (CMOV指令):

    cmov指令格式如下:

cmovx source, destination

    上面cmovx后面的x是个通配符,可以是一到两个字符,用于表示要触发传值行为的条件,这些条件都是基于EFLAGS寄存器里的标志位来确定的,下表是EFLAGS寄存器中和这些条件传值指令相关的标志位:

EFLAGS Bit
EFLAGS寄存器位
Name
位名称
Description
描述
CF Carry flag 
进位或借位标志
A mathematical expression has 
created a carry or borrow 
判断数学表达式计算结果是否发生进位或借位
OF Overflow flag 
溢出标志
An integer value is either too large 
or too small 
当整数值过大或过小时就会发生溢出
PF Parity flag 
奇偶标志
The register contains corrupt data from 
a mathematical operation 
通过结果寄存器里包含的数据有偶数个1还是奇数个1来简单判断数据是否损坏
SF Sign flag 
符号标志
Indicates whether the result is negative 
or positive 
判断结果是正数还是负数
ZF Zero flag 
是否为零标志
The result of the mathematical operation 
is zero 
当数学计算的结果为0时,会设置该标志位

    条件传值指令从大的方向,可以分为两个集合:用于无符号操作数的条件传值指令集,和用于有符号操作数的条件传值指令集,简单的来说,无符号操作数是指用于数学运算的操作数是不考虑正负符号的,而有符号操作数则需要考虑数字的正负,无符号数和有符号数的详情在后面的章节中再进行介绍。

    对于无符号和有符号的条件传值指令集,每个指令集中又分为很多对指令,每对指令很多都有两种表达式,这是因为例如某个数大于另一个数,我们还可以说成某个数不小于等于另一个数,下表先例举无符号的条件传值指令集:

Instruction Pair 
指令对
Description 
描述
EFLAGS Condition 
EFLAGS条件标志位
CMOVA/CMOVNBE Above/not below or equal 
目标操作数大于源操作数 / 目标操作数不小于等于源操作数
(CF or ZF) = 0
CMOVAE/CMOVNB Above or equal/not below 
目标操作数大于等于源操作数 / 目标操作数不小于源操作数
CF=0
CMOVNC Not carry 
数学运算没发生进位或借位
CF=0
CMOVB/CMOVNAE Below/not above or equal 
目标操作数小于源操作数 / 目标操作数不大于等于源操作数
CF=1
CMOVC Carry 
数学运算发生了进位或借位
CF=1
CMOVBE/CMOVNA Below or equal/not above 
目标操作数小于等于源操作数 / 目标操作数不大于源操作数
(CF or ZF) = 1
CMOVE/CMOVZ Equal/zero 
两操作数相等 / 运算结果为0
ZF=1
CMOVNE/CMOVNZ Not equal/not zero 
两操作数不相等 / 运算结果不为0
ZF=0
CMOVP/CMOVPE Parity/parity even 
运算结果二进制码格式中有偶数个1时
PF=1
CMOVNP/CMOVPO Not parity/parity odd 
运算结果二进制码格式中有奇数个1时
PF=0

    从上表可以看出,无符号条件传值指令主要依据CF , ZF 和 PF 三个标志位进行条件判断,下表是有符号条件传值指令:

Instruction Pair 
指令对
Description 
描述
EFLAGS Condition 
EFLAGS条件标志位
CMOVGE/CMOVNL Greater or equal/not less 
有符号目标操作数大于等于源操作数 / 目标不小于源操作数
(SF xor OF)=0
CMOVL/CMOVNGE Less/not greater or equal 
目标小于源 / 目标不大于等于源
(SF xor OF)=1
CMOVLE/CMOVNG Less or equal/not greater 
目标小于等于源 / 目标不大于源
((SF xor OF) or ZF)=1
CMOVO Overflow 
运算结果发生溢出
OF=1
CMOVNO Not overflow 
运算结果没发生溢出
OF=0
CMOVS Sign (negative) 
最高符号位为1 (负号)
SF=1
CMOVNS Not sign (non-negative) 
最高符号位为0 (非负号)
SF=0

    从表中可以看出,有符号条件传值指令主要依据SF , OF来判断是否需要传值。

    上面这些条件传值指令主要是和一些会设置EFLAGS寄存器的数学运算指令配合使用,如下面的代码:

movl value, %ecx
cmp %ebx, %ecx
cmova %ecx, %ebx

    代码中先用value内存位置的值设置ECX寄存器,然后通过CMP指令来比较EBX和ECX寄存器的值,如果ECX寄存器的值大于EBX里的值时,CMOVA指令就会用ECX里的值覆盖掉EBX里的值。

    注意:AT&T语法中的源和目标操作数的位置是和Intel语法中的相反的,所以CMOVA之类的指令中进行条件比较的顺序也是相反的,这点容易混淆。

    下面是条件传值指令的完整示例代码:

# cmovtest.s - An example of the CMOV instructions
.section .data
output:
    .asciz "The largest value is %d\n"
values:
    .int 105, 235, 61, 315, 134, 221, 53, 145, 117, 5
.section .text
.globl _start
_start:
    nop
    movl values, %ebx
    movl $1, %edi
loop:
    movl values(, %edi, 4), %eax
    cmp %ebx, %eax
    cmova %eax, %ebx
    inc %edi
    cmp $10, %edi
    jne loop
    pushl %ebx
    pushl $output
    call printf
    addl $8, %esp
    pushl $0
    call exit

    cmovtest.s程序的作用是在values数组中找出最大的值,代码中先将values数组的第一个元素105传给EBX,然后使用索引的方式将values中的第二个元素值传给EAX,接着通过CMP指令比较EBX和EAX里的值,当EAX里的值大于EBX时,CMOVA指令就会触发条件传值,从而将EAX里的值覆盖到EBX中,这样EBX里存放的就是较大的值了,在进行一轮比较后,通过INC指令来增加EDI索引寄存器的值,然后循环判断下一组数据,继续查找更大的数,当10个数都比较完后,就结束循环,此时EBX中就存放了数组中最大的数了,最后通过printf函数将数组中这个最大的数给打印显示出来,代码中某些没涉及到的指令都会在后面的章节中进行说明,这里只需了解CMOVA之类的条件传值指令的用法即可。

    可以在gdb下进行调试分析:

(gdb) s
14     movl values(, %edi, 4), %eax
(gdb) s
15     cmp %ebx, %eax
(gdb) print $eax
$1 = 235
(gdb) print $ebx
$2 = 105
(gdb)

    从上面的输出可以看到,在没进行比较和条件传值前,EAX的值为235即values数组中的第二个元素,EBX的值为105即数组中的第一个元素,然后单步执行CMP和CMOVA指令:

(gdb) s
16     cmova %eax, %ebx
(gdb) s
17     inc %edi
(gdb) print $ebx
$3 = 235
(gdb)

    通过s命令单步执行完CMP和CMOVA指令后,EBX寄存器里的值就由原来的105变为了235即EAX里的值,从而找出了values数组中第一个数和第二个数中的较大的值。程序完整的执行完后,会将数组中最大的数给打印显示出来:

$ ./cmovtest
The largest value is 315
$

    限于篇幅本章先到这里,下节介绍汇编数据移动中的数据交换指令。

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

下一篇: Moving Data 汇编数据移动 (三) 数据交换指令

上一篇: Moving Data 汇编数据移动 (一)

相关文章

使用IA-32平台提供的高级功能 (三) SSE相关指令

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

优化汇编指令 (二)

优化汇编指令 (三)

汇编中使用文件 (二)

汇编里使用Linux系统调用 (二)