本文由zengl.com站长对
http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
另外再附加一个英特尔英文手册的共享链接地址:
http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)
本篇翻译对应汇编教程英文原著的第276页到第279页,对应原著第9章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为279/ 577)。
上一篇介绍了FPU的寄存器结构,本篇就介绍和FPU浮点计算相关的加减乘除之类的基础运算指令。
Basic Floating-Point Math 基础浮点运算:
下表显示了FPU提供的用于进行基础浮点运算的指令:
Instruction 指令 |
Description 描述 |
FADD |
Floating-point addition 浮点加法指令 |
FDIV |
Floating-point division 浮点除法指令 |
FDIVR |
Reverse floating-point division
反向浮点除法指令(FDIVR的被除数和除数与FDIV指令刚好相反) |
FMUL |
Floating-point multiplication
浮点乘法指令 |
FSUB |
Floating-point subtraction 浮点减法指令 |
FSUBR |
Reverse floating-point subtraction
反向浮点减法指令(FSUBR的被减数和减数与FSUB指令刚好相反) |
|
上表中每条指令都有好几个格式,例如FADD指令就可以有如下几种格式:
-
FADD source: Add a 32- or 64-bit value from memory to the ST0 register
FADD source格式:将内存里32位或64位的值和ST0的值相加,结果存储到ST0
-
FADD %st(x), %st(0): Add st(x) to st(0) and store the result in st(0)
FADD %st(x), %st(0)格式:将ST(X)寄存器和ST(0)相加,结果存储到ST0
-
FADD %st(0), %st(x): Add st(0) to st(x) and store the result in st(x)
FADD %st(0), %st(x)格式:将ST(X)寄存器和ST(0)相加,结果存储到ST(X)
-
FADDP %st(0), %st(x): Add st(0) to st(x), store the result in st(x), and pop st(0)
FADDP %st(0), %st(x)格式:将ST(X)寄存器和ST(0)相加,结果存储到ST(X),并且将ST(0)弹出寄存器栈
-
FADDP: Add st(0) to st(1), store the result in st(1), and pop st(0)
FADDP格式:将ST(0)和ST(1)的值相加,结果存储到ST(1),并且将ST(0)弹出寄存器栈
-
FIADD source: Add a 16- or 32-bit integer value to st(0) and store the result in st(0)
FIADD source格式:将内存里16位或32位的整数值和ST(0)相加,结果存储到ST(0)
这些FADD、FDIV之类的浮点指令都是将ST(0)和另一个ST寄存器或内存里的值进行加减乘除运算,结果存储到ST(0)或某个指定的ST寄存器里,所以这些浮点运算指令都是在和ST(0)栈顶寄存器的值进行运算。
另外,FSUB减法指令的被减数始终是ST(0),所以无论是fsub %st(0),%st(1)格式还是fsub %st(1),%st(0)格式,运算过程都是用ST(0)减去ST(1),如果ST(0)原来是3,ST(1)原来是12,两个格式计算结果都会是-9,只不过fsub %st(0),%st(1)它的结果存储在ST(1)里,而fsub %st(1),%st(0)则存储在ST(0)里,但是减法的结果都是一样的,这点是译者经过实验数据得出的结果,英文原著还有很多其他汇编网站上有关FSUB指令的解释都和实验数据不符合,读者可以自行做测试。
同时这也就解释了为什么要有FSUBR指令了,FSUBR指令就刚好和FSUB相反,它始终将ST(0)作为减数,另一个操作数作为被减数,所以无论是fsubr %st(0),%st(1)还是fsubr %st(1),%st(0),计算结果都是ST(1)减去ST(0),只不过前者结果存储在目标操作数ST(1),而后者则存储在ST(0)里。如果按照其他汇编网站或原著的说法,即目标操作数减去源操作数的话,就完全没必要弄出FSUB和FSUBR两条指令出来,因为程序中只需要根据情况调整两操作数的位置即可,这些结论读者可以再自行测试进行验证。
同理,FDIV指令的被除数始终是ST(0),也就是该指令始终是用ST(0)去除以另一个操作数,例如ST(0)为12,另一个操作数为3,则FDIV就是12除以3,结果为4,FDIVR指令则刚好相反,它始终将ST(0)作为除数,即该指令始终是用另一个操作数去除以ST(0)。
至于FADD和FMUL,加法与乘法指令,正向反向操作都是一样的结果,所以就没有像FSUBR之类的反向操作指令。
译者并不是对原著做简单的直译,所有的结论都是基于自己的实验数据,包括上一篇有关FPU的control控制寄存器里mask异常掩码的解释都是基于实验数据得出来的,上一篇里有关control寄存器的解释与原著是不一样的,英文原著的说法并不符合实验结果。
当然,尽管英文原著在本段有关浮点反向操作指令的解释在某些地方有误,但是后面的测试例子中,这些反向指令的用法还是很到位的。
另外在GNU汇编里的浮点运算指令经常需要在指令助记符后面添加尺寸后缀以表示操作数的大小(s 后缀表示操作数是32位的单精度浮点数,l 后缀表示操作数是64位的双精度浮点数),还有GNU汇编指令的源操作数和目标操作数与Intel汇编语法是相反的。
下面是一些浮点运算指令的简单例子:
fadds data1 #将ST(0)和data1内存里的单精度浮点数相加,结果存储在ST(0)
fmull data1 #将ST(0)和data1内存里的双精度浮点数相乘,结果存储在ST(0)
fidiv data1 #将ST(0)除以data1内存里的整数,结果存储在ST(0)
fsub %st, %st(1) #将ST(0)减去ST(1),结果存储在ST(1),%st就表示%st(0)
fsub %st(0), %st(1) #将ST(0)减去ST(1),结果存储在ST(1)
fsub %st(1), %st(0) #还是将ST(0)减去ST(1),结果存储在ST(0)
fsubr %st(0),%st(1) #fsubr反向减法指令,将ST(1)减去ST(0),结果存储在目标操作数ST(1) |
由于FSUB和FDIV指令始终是指定ST(0)作为被减数和被除数,所以如果没有反向操作指令的话,如果想将其他ST寄存器作为被除数的话,就要通过交换或传值指令,将数据设置到ST(0),再进行运算,有了FSUBR和FDIVR反向操作指令后,就可以方便的将其他的ST寄存器或内存作为被减数或被除数进行相关运算了。
为了更好的演示上面提到的浮点运算指令的用法,下面就用汇编来计算下面的数学表达式:
((43.65 / 22) + (76.34 * 3.1)) / ((12.43 * 6) – (140.2 / 94.21)) |
求解复杂的表达式,最好先规划好计算的步骤,然后根据规划来编写具体的代码,该表达式的主要计算步骤如下:
-
将43.65加载到ST0寄存器
-
将ST0里的值除以22,结果存储到ST0
-
加载76.34到ST0 (上面第2步的计算结果就自动移入ST1)
-
加载3.1到ST0 (上面第3步的值移入ST1,第2步的结果移入ST2)
-
将ST0里的3.1乘以ST1里的76.34,结果保存在ST0
-
将ST0和ST2相加,结果保存在ST0 (至此计算完左侧((43.65 / 22) + (76.34 * 3.1))的表达式)
-
加载12.43到ST0 (上面第6步的结果移入ST1)
-
将ST0里的12.43乘以6,结果保存在ST0
-
加载140.2到ST0 (上面第8步的结果移入ST1,第6步的结果移入ST2)
-
加载94.21到ST0 (上面第8步的结果移入ST2,第6步的结果移入ST3)
-
将ST1里的140.2除以ST0里的94.21,结果存储在ST1,同时将ST0的值弹出寄存器栈,这样ST1的结果就自动往上挪一格到了ST0 (上面第8步的结果也往上挪到ST1,第6步的结果则挪到ST2)
-
将ST1里第8步计算的结果减去ST0里的值,结果保存在ST0 (从而完成右侧((12.43 * 6) – (140.2 / 94.21))表达式的计算)
-
将ST2里的值除以ST0,结果保存在ST0 (至此计算出整个表达式的最终结果)
在上面的过程中,FPU寄存器栈里值的加载和计算情况如下图所示:
图1
下面的fpmath1.s程式就将上面的计算步骤转为具体的汇编代码:
# fpmath1.s - An example of basic FPU math
.section .data
value1:
.float 43.65
value2:
.int 22
value3:
.float 76.34
value4:
.float 3.1
value5:
.float 12.43
value6:
.int 6
value7:
.float 140.2
value8:
.float 94.21
output:
.asciz "The result is %f\n"
.section .text
.globl _start
_start:
nop
finit
flds value1
fidiv value2
flds value3
flds value4
fmul %st(1), %st(0)
fadd %st(2), %st(0)
flds value5
fimul value6
flds value7
flds value8
fdivrp
fsubr %st(1), %st(0)
fdivr %st(2), %st(0)
subl $8, %esp
fstpl (%esp)
pushl $output
call printf
add $12, %esp
pushl $0
call exit
|
上面的代码中,将数学表达式里要计算的值都存储到data段定义的内存里,然后根据每一步的计算需要,加载指定的数据到FPU寄存器栈,并进行加减乘除运算,这里需要留意的浮点运算指令是fdivrp,fsubr及fdivr这几个反向指令。
fdivrp隐示使用ST1作为被除数,ST0作为除数,即用ST1除以ST0,结果先存储在ST1,接着将ST0弹出寄存器栈,这样保存在ST1里的结果就会自动往上挪入ST0。
fsubr反向减法指令使用ST0作为减数,另一个操作数作为被减数,例如上面代码里fsubr %st(1), %st(0)指令就是用ST1的值减去ST0的值,结果存储在目标操作数ST0里。
fdivr %st(2), %st(0)指令则使用ST2作为被除数,ST0作为除数,计算时就会用ST2的值去除以ST0,结果存储到目标操作数ST0里。
你可以试着将fsubr %st(1), %st(0)的两操作数调换位置,即变为fsubr %st(0), %st(1),那么在gdb调试器里,可以看到两个fsubr指令的运行结果都是一样的,只不过前者将结果存储在ST0,后者将结果存储在ST1,当然虽然该指令两个格式的结果一样,但是由于结果的存储位置不一样,所以如果你真的在fpmath1里做了这样的调整的话,原来ST0里的结果就会跑到ST1,整个程式的计算结果肯定也就不一样。fdivr反向除法指令也是同理。
由于最终的计算结果存储在ST0栈顶寄存器里,所以代码最后就通过fstpl (%esp)指令将ST0栈顶寄存器里的值弹出到ESP指向的内存栈位置(该位置是printf函数接受参数的位置),这样就可以将整个表达式的计算结果作为printf函数的参数,从而将结果显示出来。
下面是程序调试运行的情况:
$ as -gstabs -o fpmath1.o fpmath1.s
$ ld -dynamic-linker /lib/ld-linux.so.2 -lc -o fpmath1 fpmath1.o
$ ./fpmath1
The result is 3.264907
$ |
计算结果和预期的一致,你还可以在gdb调试器里通过s单步执行命令和info all查看寄存器(包括FPU寄存器栈的值)命令进行更加详细的调试分析。
本篇基础浮点运算指令的介绍就到这里,下一篇介绍其他的浮点运算指令。
转载请注明来源:www.zengl.com
OK,休息,休息一下 o(∩_∩)o~~