X86处理器汇编技术系列2
高级数学运算 (一) FPU寄存器介绍
之前的章节简单的介绍过FPU(浮点运算单元),在80486之前,是通过软件模拟或购买特殊的数学协处理器来处理浮点数的,在80486出现后,Intel处理器就内置了FPU浮点单元,下面就具体介绍下FPU的结构,...
http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
另外再附加一个英特尔英文手册的共享链接地址:
http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)
本篇翻译对应汇编教程英文原著的第266页到第276页,对应原著第9章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为276/ 577)。
之前的章节简单的介绍过FPU(浮点运算单元),在80486之前,是通过软件模拟或购买特殊的数学协处理器来处理浮点数的,在80486出现后,Intel处理器就内置了FPU浮点单元,下面就具体介绍下FPU的结构,以及和浮点操作相关的指令。
The FPU Environment FPU结构:
FPU浮点单元由8个数据寄存器,1个control控制寄存器(也可以叫做control word控制字),1个status状态寄存器(status word状态字),1个tag标记寄存器(tag word标记字)组成,下面对这些寄存器一一进行介绍。
The FPU register stack 8个数据寄存器构成的FPU寄存器栈:
前面的章节,我们就接触过FPU寄存器栈,只不过当时只有个宏观的了解,其实FPU寄存器栈是由8个80位的数据寄存器构成的,这8个寄存器依次被命名为R0、R1、R2 .... R7,但是在使用这8个寄存器时,并不像EAX之类的通用寄存器那样直接用这些名字,它们被连在一起构成了一个寄存器栈,通过一个栈顶指针来访问这些寄存器,栈顶指针指向的寄存器被命名为ST(0),栈顶指针往上走,依次又可以得到ST(1)、ST(2).....ST(7),它们依次对应一个上面的R寄存器,下面通过一个简单的例子来说明:
.section .data v1: .float 1 v2: .float 2 v3: .float 3 v4: .float 4 v5: .float 5 v6: .float 6 v7: .float 7 v8: .float 8 v9: .float 9 .section .text .globl _start _start: nop flds v1 flds v2 flds v3 flds v4 flds v5 flds v6 flds v7 flds v8 flds v9 movl $0x1,%eax movl $0,%ebx int $0x80 |
上面代码中通过FLD指令将v1到v9里的9个数依次压入FPU寄存器栈,在第一个FLD指令执行前,FPU里8个数据寄存器的情况如下:
$ as -gstabs -o fputest.o fputest.s $ ld -o fputest fputest.o $ gdb -q fputest (gdb) break *_start+1 Breakpoint 1 at 0x8048075: file fputest.s, line 26. (gdb) r Starting program: /home/zengl/Downloads/asm_example/fpu/fputest Breakpoint 1, _start () at fputest.s:26 24 flds v1 (gdb) info float R7: Empty 0x00000000000000000000 (st7) R6: Empty 0x00000000000000000000 (st6) R5: Empty 0x00000000000000000000 (st5) R4: Empty 0x00000000000000000000 (st4) R3: Empty 0x00000000000000000000 (st3) R2: Empty 0x00000000000000000000 (st2) R1: Empty 0x00000000000000000000 (st1) =>R0: Empty 0x00000000000000000000 (st0) Status Word: 0x0000 TOP: 0 |
栈顶寄存器游标ST(0)对应的R寄存器索引值存储在status状态寄存器的3个二进制位里(下面会提到),TOP 0就是那三个二进制位的值,用以表示ST(0)栈顶寄存器此时对应R0,其余的R1对应ST(1),R2对应ST(2),以此类推,当flds v1执行后,寄存器栈的情况如下:
(gdb) info float =>R7: Valid 0x3fff8000000000000000 +1 (st0) R6: Empty 0x00000000000000000000 (st7) R5: Empty 0x00000000000000000000 (st6) R4: Empty 0x00000000000000000000 (st5) ............................................ Status Word: 0x3800 TOP: 7 |
ST(0)栈顶游标往下走,循环到顶部R7(Status Word状态寄存器也显示TOP: 7),将1存入R7里,R6对应ST(7),R5对应ST(6),以此类推。
执行完flds v2,将2压入栈后,FPU寄存器栈情况如下:
(gdb) info float R7: Valid 0x3fff8000000000000000 +1 (st1) =>R6: Valid 0x40008000000000000000 +2 (st0) R5: Empty 0x00000000000000000000 (st7) R4: Empty 0x00000000000000000000 (st6) R3: Empty 0x00000000000000000000 (st5) ............................................ Status Word: 0x3000 TOP: 6 |
ST(0)栈顶游标继续往下由R7移动到R6,将2存入R6中,原来的R7此时对应ST(1),R5对应ST(7),R4对应ST(6),以此类推。
当前8个浮点数都通过FLD指令压入寄存器栈后,调试情况如下:
(gdb) info float R7: Valid 0x3fff8000000000000000 +1 (st7) R6: Valid 0x40008000000000000000 +2 (st6) R5: Valid 0x4000c000000000000000 +3 (st5) R4: Valid 0x40018000000000000000 +4 (st4) R3: Valid 0x4001a000000000000000 +5 (st3) R2: Valid 0x4001c000000000000000 +6 (st2) R1: Valid 0x4001e000000000000000 +7 (st1) =>R0: Valid 0x40028000000000000000 +8 (st0) Status Word: 0x0000 TOP: 0 .......................................... (gdb) info all .......................................... st0 8 (raw 0x40028000000000000000) st1 7 (raw 0x4001e000000000000000) st2 6 (raw 0x4001c000000000000000) st3 5 (raw 0x4001a000000000000000) st4 4 (raw 0x40018000000000000000) st5 3 (raw 0x4000c000000000000000) st6 2 (raw 0x40008000000000000000) st7 1 (raw 0x3fff8000000000000000) .......................................... |
ST0游标转了一圈又回到R0,此时8个数据寄存器R0到R7就全部填入了Valid有效的浮点数,当再继续执行flds v9时,就会出现栈错误(因为没有更多的R寄存器可以用来存放数据了),执行完flds v9后,调试输出情况如下:
(gdb) info float =>R7: Special 0xffffc000000000000000 Real Indefinite (QNaN) (st0) R6: Valid 0x40008000000000000000 +2 (st7) R5: Valid 0x4000c000000000000000 +3 (st6) R4: Valid 0x40018000000000000000 +4 (st5) ...................... Status Word: 0x3a41 IE SF C1 TOP: 7 |
ST(0)向下循环游走到R7,并将R7里原来有效的浮点数覆盖成为了一个Special特殊的QNaN类型,并在status word状态寄存器对应的二进制位上设置了IE, SF , C1的浮点栈异常标志,简单的说就是产生了浮点异常。
以上就是FPU寄存器栈的压栈原理,以及ST0栈顶指针的游走原理,还有R数据寄存器与ST栈寄存器的对应原理,当然在平时使用FPU时,FPU内部会自动帮你处理这种对应关系,你只需了解原理即可。
在向FPU加载数据时,除了可以使用FLD来加载浮点数外,还可以使用FILD指令来加载整数,或者使用FBLD指令来加载BCD码到FPU寄存器中,不同的数据类型在加载到FPU的数据寄存器里时,都会被自动转为浮点格式,后面还会提到一些指令用于将FPU里的数据以指定的数据格式存储到内存位置。
The FPU status, control, and tag registers FPU状态、控制以及标记寄存器:
由于FPU是独立于主处理器的一部分,所以它不通过常规的EFLAGS寄存器来检测结果,它是通过status状态寄存器、control控制寄存器以及tag标记寄存器来检测结果和FPU的状态,下面对这三个寄存器一一进行介绍。
The status register 状态寄存器:
status状态寄存器是一个16位的寄存器,它里面每个二进制位的含义如下表所示:
|
8、9、10及14这四个条件代码位用于配合浮点异常标志来提供一些额外的错误信息。
Error summary status(错误摘要状态位)用于当某异常发生时,如果control控制寄存器中对应的异常掩码位没被设置即没有屏蔽对应的异常,则该异常就会交由FPU执行默认处理,FPU默认处理时就会设置Error summary status标志位,并且丢弃掉错误的结果,同时FPU会产生一些异常信号来终止程序的继续执行,如果control里对应的异常掩码被设置时,对应的异常发生时就会被屏蔽掉,不会交由FPU执行默认处理,指令执行的异常结果将存储到对应的数据寄存器里,不会影响程序的继续执行,例如前面的fputest例子,默认情况下控制寄存器的掩码都是设置状态(屏蔽状态),异常的结果QNaN就会被存储在数据寄存器里而不会被FPU丢弃掉。
status状态寄存器的前6个标志位都用于指示FPU发生的异常,当FPU计算过程中发生浮点异常时,对应的异常标志位就会被设置,这些异常标志一旦被设置,就会一直保持设置状态,除非你手动清理掉它们(比如通过重新初始化FPU的方式),另外Error summary status位被设置时也会一直保留下去除非你手动清理掉它。
当FLD之类的指令导致寄存器栈溢出时,Stack fault栈错误标志就会被设置。
11到13位的栈顶指针用于表示当前的ST(0)栈顶寄存器对应哪个R寄存器,这个在前面fputest例子里,在gdb调试时info float命令输出的TOP值就存储在这三个标志位里。
在汇编程序中可以通过FSTSW指令来将status状态寄存器里的值读取到内存或读取到AX寄存器,下面的getstatus.s程式就演示了这个指令的用法:
# getstatus.s - Get the FPU Status register contents .section .bss .lcomm status, 2 .section .text .globl _start _start: nop fstsw %ax fstsw status movl $1, %eax movl $0, %ebx int $0x80 |
上面的代码通过FSTSW指令将FPU状态寄存器的值分别加载到AX寄存器和status内存位置,在汇编链接后,在调试器里的输出情况如下:
(gdb) x/x &status 0x804908c <status>: 0x00000000 (gdb) print/x $eax $1 = 0x0 (gdb) info all ...................... fctrl 0x37f 895 fstat 0x0 0 ftag 0xffff 65535 ...................... |
从上面输出可以看到,status和EAX里的值都和info all显示的fstat状态寄存器的值一样,默认情况下status状态寄存器的值为0 。
The control register FPU控制寄存器:
控制寄存器用于控制FPU的精度,舍入方式等,控制寄存器也是一个16位的寄存器,该寄存器的各二进制位的含义如下表所示:
|
头6位为异常掩码,当某个mask异常掩码被设置时,那么对应的异常发生时,就会屏蔽掉该异常,这里屏蔽的意思只是相对于FPU默认异常处理程序而言的,也就是不将异常交由FPU执行默认的处理操作,浮点的异常结果就不会被忽略掉。如果某mask异常掩码没被设置,就不会屏蔽该异常,异常发生时就会交由FPU执行默认的处理例程,默认处理例程中就会将发生异常的指令和产生的异常结果给丢弃掉,同时设置Error summary status错误摘要标志以表示默认例程捕获并处理了一个异常,还会产生异常信号来终止程序的继续执行。
初始状态下,所有的异常掩码默认都是设置即屏蔽状态,这样即便发生了对应的浮点异常也不会产生异常信号来终止程序的继续执行。
8到9位的Precision control精度控制位用于设置FPU内部数学计算时的浮点精度,可用的精度值如下:
- 00 — single-precision (24-bit significand)
单精度(24位有效二进制位) - 01 — not used
没有使用 - 10 — double-precision (53-bit significand)
双精度(53位有效二进制位) - 11 — double-extended-precision (64-bit significand)
双精度扩展(64位有效二进制位)
10到11位的Rounding control舍入控制位用于设置FPU如何对浮点计算的结果进行舍入操作,可用的舍入控制如下:
- 00 — round to nearest
舍入到最接近的值 - 01 — round down (toward negative infinity)
向下舍入(向负无穷大方向进行舍入) - 10 — round up (toward positive infinity)
向上舍入(向正无穷大方向进行舍入) - 11 — round toward zero
向零舍入
初始状态下,control控制寄存器的默认值为0x037F,你可以使用FSTCW指令将控制寄存器的值加载到内存里。你也可以使用FLDCW指令来改变控制寄存器的值,FLDCW指令会将某内存位置里16位的值加载到控制寄存器,下面的setprec.s程式就演示了这些指令的用法:
# setprec.s - An example of setting the precision bits in the Control Register .section .data newvalue: .byte 0x7f, 0x00 .section .bss .lcomm control, 2 .section .text .globl _start _start: nop fstcw control fldcw newvalue fstcw control movl $1, %eax movl $0, %ebx int $0x80 |
上面的代码里,先用fstcw control指令将control控制寄存器里的初始值存储到control内存位置,再由fldcw newvalue指令将newvalue内存里的0x007f(注意内存定义时用的小字节序)加载到control控制寄存器,这样就将control寄存器里的8和9的精度位设置为00即单精度,最后再由fstcw control指令将修改后的控制寄存器值存储到control内存里。
下面是程序调试运行的输出情况:
$ as -gstabs -o setprec.o setprec.s $ ld -o setprec setprec.o $ gdb -q setprec Reading symbols from /home/zengl/Downloads/asm_example/fpu/setprec...done. (gdb) break *_start+1 Breakpoint 1 at 0x8048075: file setprec.s, line 11. (gdb) r Starting program: /home/zengl/Downloads/asm_example/fpu/setprec Breakpoint 1, _start () at setprec.s:11 11 fstcw control (gdb) x/x &control 0x804909c <control>: 0x00000000 (gdb) s 12 fldcw newvalue (gdb) x/x &control 0x804909c <control>: 0x0000037f (gdb) s 13 fstcw control (gdb) s 14 movl $1, %eax (gdb) x/x &control 0x804909c <control>: 0x0000007f (gdb) info all .......................... fctrl 0x7f 127 .......................... (gdb) |
可以看到control控制寄存器里的值一开始是0x037f,经过fldcw newvalue指令后,FPU控制寄存器里的值就被成功修改为0x7f了,从而将FPU的计算精度由双精度扩展改为单精度。
FPU设置为单精度后并不一定会加快所有的浮点计算,主要是在一些除法和平方根计算里会提升性能。
The tag register FPU标记寄存器:
标记寄存器用于标识FPU的8个数据寄存器里存储的是什么样的值,tag标记寄存器也是16位的寄存器,如下图所示:
图1
tag寄存器里每2位对应一个R数据寄存器,这两位可以表示的数据内容如下:
- A valid double-extended-precision value (code 00)
一个有效的双精度扩展值(对应二进制值为00) - A zero value (code 01)
一个0值(对应二进制值为01) - A special floating-point value (code 10)
一个特殊的浮点值(对应二进制值为10) - Nothing (empty) (code 11)
没有存放值即初始状态时的空值(对应二进制值为11)
Using the FPU stack 使用FPU寄存器栈:
[zengl pagebreak]
Using the FPU stack 使用FPU寄存器栈:
下面通过stacktest.s例子来说明如何使用FPU寄存器栈:
# stacktest.s - An example of working with the FPU stack .section .data value1: .int 40 value2: .float 92.4405 value3: .double 221.440321 .section .bss .lcomm int1, 4 .lcomm control, 2 .lcomm status, 2 .lcomm result, 4 .section .text .globl _start _start: nop finit fstcw control fstsw status filds value1 fists int1 flds value2 fldl value3 fst %st(4) fxch %st(1) fstps result movl $1, %eax movl $0, %ebx int $0x80 |
上面代码中一开始就使用FINIT指令来初始化FPU,该指令可以将FPU的control控制寄存器和status状态寄存器的值初始化为默认值,但它不会修改FPU的数据寄存器,在使用FPU的程序里用FINIT指令进行初始化是个好的编程习惯。
接着代码里通过FSTCW和FSTSW指令将控制寄存器和状态寄存器里的值分别存储到control和status内存位置,在gdb调试器下可以用x命令来查看这些存储在内存里的值:
(gdb) x/2bx &control 0x80490c8 <control>: 0x7f 0x03 (gdb) x/2bx &status 0x80490ca <status>: 0x00 0x00 (gdb) |
上面输出中,control内存里的值为0x037f,说明控制寄存器里的默认值就是0x037f,status内存里为0,说明状态寄存器的默认值就是0 。
接下来,代码通过filds value1将value1内存里的整数加载到ST0栈顶寄存器,再由fists int1指令将ST0栈顶寄存器的值存储到int1内存位置,这里FILDS和FISTS指令都是S后缀,表示操作数是一个32位的数据,这里表示操作的是32位的整数值,执行完这两条指令后,gdb调试器的输出如下:
(gdb) info all ....................... st0 40 (raw 0x4004a000000000000000) ....................... (gdb) x/d &int1 0x80490c4 <int1>: 40 (gdb) x/4bx &int1 0x80490c4 <int1>: 0x28 0x00 0x00 0x00 (gdb) |
从上面的输出可以看到,ST0栈顶寄存器和int1内存里的值都为40,和value1里的整数值一致,另外,从info all的输出可以看到st0里的40对应的raw原始二进制值是双精度扩展浮点格式,也就是说FILDS指令在将整数加载到FPU后,在数据寄存器里会自动转为双精度扩展格式,而从x/4bx &int1命令的输出可以看到,在FISTS指令将FPU数据寄存器里的值存储到内存位置时,又会自动将双精度扩展浮点格式转为32位整数的二进制格式。
再接下来,代码通过flds value2和fldl value3指令将value2里的单精度浮点数和value3里的标准双精度浮点数依次加载到FPU寄存器栈里,这两条指令执行完后,调试器的输出情况如下:
(gdb) info all ........................ st0 221.44032100000001150874595623463392 (raw 0x4006dd70b8e086bdf800) st1 92.44049835205078125 (raw 0x4005b8e1890000000000) st2 40 (raw 0x4004a000000000000000) ........................ |
st0栈顶寄存器始终指向最后一次压入栈的数据,FLDS指令使用S后缀表示要加载的数据是32位的单精度浮点数,FLDL指令使用L后缀表示要加载的数据是64位的双精度浮点数。
在将value1、value2及value3三个内存里的值加载到FPU寄存器栈后,代码通过fst %st(4)指令来将ST0栈顶寄存器的值拷贝到ST4,在GNU汇编里引用ST寄存器时,数字需要用括号括起来,该指令执行后,调试情况如下:
(gdb) info all ........................... st0 221.44032100000001150874595623463392 (raw 0x4006dd70b8e086bdf800) st1 92.44049835205078125 (raw 0x4005b8e1890000000000) st2 40 (raw 0x4004a000000000000000) st3 0 (raw 0x00000000000000000000) st4 221.44032100000001150874595623463392 (raw 0x4006dd70b8e086bdf800) ........................... |
从info all的输出可以看到ST0的值成功拷贝到ST4中。接着,代码使用fxch %st(1)指令将ST0和ST1里的值进行交换,交换的结果如下:
(gdb) info all .......................... st0 92.44049835205078125 (raw 0x4005b8e1890000000000) st1 221.44032100000001150874595623463392 (raw 0x4006dd70b8e086bdf800) st2 40 (raw 0x4004a000000000000000) st3 0 (raw 0x00000000000000000000) st4 221.44032100000001150874595623463392 (raw 0x4006dd70b8e086bdf800) .......................... |
最后,程序通过fstps result指令将ST0的值弹出到result内存位置,FSTPS指令和前面的FST指令的区别在于:FST在获取FPU数据时,ST0栈顶的值保持不变,而FSTPS指令在将ST0栈顶的值存储到目标位置后,还会将ST0栈顶的值弹出寄存器栈,所有ST寄存器栈里的数据都会往上挪一格,如下所示:
(gdb) info all ............................ st0 221.44032100000001150874595623463392 (raw 0x4006dd70b8e086bdf800) st1 40 (raw 0x4004a000000000000000) st2 0 (raw 0x00000000000000000000) st3 221.44032100000001150874595623463392 (raw 0x4006dd70b8e086bdf800) st4 0 (raw 0x00000000000000000000) ............................ (gdb) x/fw &result 0x80490cc <result>: 92.4404984 (gdb) x/4bx &result 0x80490cc <result>: 0x89 0xe1 0xb8 0x42 (gdb) |
可以看到原来的栈顶值被成功弹出,并存储到result内存位置。
有关FPU寄存器栈和FPU内部寄存器的结构就介绍到这里,下一篇开始介绍FPU里的基础数学运算指令。
上一篇介绍了FPU的寄存器结构,本篇就介绍和FPU浮点计算相关的加减乘除之类的基础运算指令。
Basic Floating-Point Math 基础浮点运算:
下表显示了FPU提供的用于进行基础浮点运算的指令:
|
上表中每条指令都有好几个格式,例如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)
另外,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 (至此计算出整个表达式的最终结果)
图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寄存器栈的值)命令进行更加详细的调试分析。
本篇基础浮点运算指令的介绍就到这里,下一篇介绍其他的浮点运算指令。
上一篇介绍了浮点数的加减乘除运算指令,如果你的汇编程序是用于科学计算或者某些工程应用的话,就有必要了解更多的高级浮点运算指令,可用的高级浮点指令如下表所示:
|
上面这些浮点指令很多都可以直接从字面上得出它的含义,下面就具体的介绍下这些指令的意义和用法。
Floating-point functions 浮点数学运算:
FABS, FCHS, FRNDINT 以及 FSQRT指令用于完成一些简单的浮点运算,其中FABS指令用于计算ST(0)的绝对值,FCHS指令用于改变ST(0)值的符号位,FSQRT指令则用于计算ST(0)的平方根,下面的fpmath2.s程式就演示了这几个指令的用法:
# fpmath2.s - An example of the FABS, FCHS, and FSQRT instructions .section .data value1: .float 395.21 value2: .float -9145.290 value3: .float 64.0 .section .text .globl _start _start: nop finit flds value1 fchs flds value2 fabs flds value3 fsqrt movl $1, %eax movl $0, %ebx int $0x80 |
该程序调试输出的结果如下:
$ as -gstabs -o fpmath2.o fpmath2.s $ ld -o fpmath2 fpmath2.o $ gdb -q fpmath2 Reading symbols from /home/zengl/Downloads/asm_example/fpuAdv/fpmath2...done. (gdb) break 20 Breakpoint 1 at 0x8048090: file fpmath2.s, line 20. (gdb) r Starting program: /home/zengl/Downloads/asm_example/fpuAdv/fpmath2 Breakpoint 1, _start () at fpmath2.s:20 20 movl $1, %eax (gdb) info all .................... st0 8 (raw 0x40028000000000000000) st1 9145.2900390625 (raw 0x400c8ee5290000000000) st2 -395.209991455078125 (raw 0xc007c59ae10000000000) .................... (gdb) |
这里计算结果的顺序是和FLDS指令压栈的顺序相反的,ST0里的8是FSQRT指令对value3压入栈顶的值64进行平方根运算的结果,ST1为FABS指令对value2压入的值取绝对值的结果,ST2里的值则为FCHS指令修改符号位后的结果。
FRNDINT指令用于将ST0里的值舍入为整数值,具体舍入的方式是由FPU控制寄存器里的Rounding control(10到11位的舍入控制位)来进行控制的(这个在前面FPU寄存器介绍篇里讲解过),一共有4种舍入方式,默认为00即舍入到最接近的值,还有01向下舍入(向负无穷大方向进行舍入),10向上舍入(向正无穷大方向进行舍入),以及11向零舍入。
下面的roundtest.s程式就演示了FRNDINT指令的用法:
# roundtest.s - An example of the FRNDINT instruction .section .data value1: .float 3.65 rdown: .byte 0x7f, 0x07 rup: .byte 0x7f, 0x0b .section .bss .lcomm result1, 4 .lcomm result2, 4 .lcomm result3, 4 .section .text .globl _start _start: nop finit flds value1 frndint fists result1 fldcw rdown flds value1 frndint fists result2 fldcw rup flds value1 frndint fists result3 movl $1, %eax movl $0, %ebx int $0x80 |
上面代码中,一开始FINIT指令初始化FPU后,FPU控制寄存器里的默认值为0x037f,对应的10到11位的舍入控制位为00即舍入到最接近的值,然后代码会使用rdown和rup里的值来修改控制寄存器,其中rdown里的0x077f值对应的10到11位为01即向下舍入,rup里的0x0b7f值对应的10到11位为10即向上舍入,最后将不同舍入方式下的计算结果依次存储到result1、result2及result3的内存位置。
在gdb调试器里可以通过info float命令来查看FPU控制寄存器的值,如下所示:
(gdb) info float R7: Empty 0x00000000000000000000 R6: Empty 0x00000000000000000000 R5: Empty 0x00000000000000000000 R4: Empty 0x00000000000000000000 R3: Empty 0x00000000000000000000 R2: Empty 0x00000000000000000000 R1: Empty 0x00000000000000000000 =>R0: Empty 0x00000000000000000000 Status Word: 0x0000 TOP: 0 Control Word: 0x037f IM DM ZM OM UM PM PC: Extended Precision (64-bits) RC: Round to nearest Tag Word: 0xffff ..................................... |
上面输出里,Control Word控制字部分当为默认的0x037f时,对应的舍入方式就是Round to nearest(舍入到最接近的值)。
在调试完所有浮点指令后,result1里存储的是舍入到最接近的结果:
(gdb) x/d &result1 0x80490c4 <result1>: 4 (gdb) |
由于需要舍入的浮点数3.65从四舍五入的角度最接近4,所以result1的值就为4。
result2里存储的是向下舍入(向负无穷大方向舍入)的结果:
(gdb) x/d &result2 0x80490c8 <result2>: 3 (gdb) |
result3里存储的是向上舍入(向正无穷大方向舍入)的结果:
(gdb) x/d &result3 0x80490cc <result3>: 4 (gdb) |
Partial remainders 浮点数取余运算:
先来了解下浮点数取余的运算原理,例如求解20.65除以3.97的余数,就可以通过下面的几步来完成:
- 20.65 / 3.97 = 5.201511335, 进行舍入后得到的整数5就是商
- 5 * 3.97 = 19.85
- 20.65 – 19.85 = 0.8 (这里的计算结果0.8就是20.65除以3.97的余数)
在之前的 汇编数据处理 (三) 浮点数 章节里,介绍过浮点数在内存里的二进制科学计算形式,如标准单精度浮点数的格式如下:
图1
上图里23到30位的Exponent部分就是指数部分,该指数的有效值范围为-126到128,当FPREM取余指令运算后,ST0里值的Exponent递减幅度超过63时,就只会得到一个中间余数。例如,我们将上例里的20.65替换为一个很大的单精度浮点数1288382893898492849284942.323294929492442,当然这个单精度浮点数会严重失真,这里只是做个简单的说明,在执行完一次FPREM指令后就会得到一个中间余数14863237120保存在ST0里,再次执行FPREM后,才会得到最终的余数1.36649.... 。
既然会生成中间余数,这里就涉及到如何判断ST0里的值是中间余数还是最终余数的问题,前面介绍FPU寄存器时,提到过FPU状态寄存器里有个condition code bit 2(条件代码位2),如果该位处于set设置状态,则表示ST0里的值是个中间余数,需要继续求解,当该位清零时,则表示ST0里的值就是最终的余数,在汇编里可以通过TEST指令来测试该位的设置状态。
现在再来看下FPREM和FPREM1两个指令的不同之处。英特尔最开始引入的是FPREM指令,该指令在进行舍入求商的过程中使用的舍入方式是向零舍入的方式,例如20.65 / 4.34 = 4.75806451613,4.75806451613按向零舍入的方式得到的商就是4,然后使用这个商去求解余数,得到的余数为3.29。
不幸的是,后来IEEE(电气电子工程师学会)又发布了一个标准,他使用的舍入方式是舍入到最接近的值的方式,因此,英特尔在保留原来FPREM指令的基础上,又引入了一个FPREM1指令,该指令在浮点数取余运算时,就采用IEEE标准来进行舍入求商的操作,例如上面的20.65 / 4.34 = 4.75806451613,4.75806451613按IEEE标准舍入到最接近的整数的话,商就会是5,使用5去求解余数,得到的结果就是 20.65 - 5 * 4.34 = -1.05 (-1.05就是余数)。
至于是使用FPREM指令还是使用FPREM1指令,需要根据你自己的实际需求来决定,例如,GCC 4.3就是用FPREM指令来求解ST0 / ST1的余数的。
下面的premtest.s程式就演示了使用FPREM1指令求解浮点余数的方法:
# premtest.s - An example of using the FPREM1 instruction .section .data value1: .float 20.65 value2: .float 3.97 .section .bss .lcomm result, 4 .section .text .globl _start _start: nop finit flds value2 flds value1 loop: fprem1 fstsw %ax testb $4, %ah jnz loop fsts result movl $1, %eax movl $0, %ebx int $0x80 |
上面代码里,先将value2里的3.97除数压入栈,再将value1里的20.65被除数压入栈,这样ST0里就是20.65,ST1里就是3.97,接着使用FPREM1指令进行取余运算,取余的结果将保存在ST0,由于需要判断该结果是否只是一个中间余数,所以紧接着就用fstsw %ax将状态寄存器(状态字)里的值设置到AX寄存器,再由TEST指令测试condition code bit 2(条件代码位2),如果该位处于设置状态,说明是个中间余数,就通过jnz loop跳转到loop标签处,继续执行FPREM1指令,直到求解出最终余数为止,在得到最终余数后,将该余数通过fsts result指令存储到result内存位置。
在汇编链接程序后,在gdb调试器里的输出结果如下:
(gdb) x/fw &result 0x80490a8 <result>: 0.799999475 (gdb) |
在得到ST0里的余数的时候,计算过程中通过舍入得到的商的低三位存储在状态寄存器的另外三个condition code bit(条件代码位)里,如下所示:
- 商的位0存储在condition bit 1(条件代码位1)
- 商的位1存储在condition bit 3(条件代码位3)
- 商的位2存储在condition bit 0(条件代码位0)
(gdb) info float R7: Valid 0x4000fe147b0000000000 +3.970000028610229492 =>R6: Valid 0x3ffeccccc40000000000 +0.7999994754791259766 R5: Empty 0x00000000000000000000 R4: Empty 0x00000000000000000000 R3: Empty 0x00000000000000000000 R2: Empty 0x00000000000000000000 R1: Empty 0x00000000000000000000 R0: Empty 0x00000000000000000000 Status Word: 0x3300 C0 C1 TOP: 6 ................................................. (gdb) |
上面输出里C0和C1处于设置状态,C3没有显示也就是清零状态,说明商的低三位为101即5 。
FPREM和FPREM1指令只得到商的低三位,这看起来有点古怪,但这是有历史原因的,在过去旧的80287协处理器时代,FPTAN计算角度正切值的指令不能处理大于pi / 4即45度的角度,因此,在执行FPTAN指令前,就需要先用FPREM指令将角度针对pi / 4进行取余运算,得到的商的低三位作为坐标系里的象限值,而余数就可以进行FPTAN的正切函数运算,我们将360度的平面坐标系按pi / 4即45度可以划分为8个部分即8个象限,FPREM指令得到的商的低三位刚好可以表示8个数,每个数对应一个象限。
到了80387协处理器后,FPTAN指令就没有这方面的限制了,所以现在FPREM得到的商值就很少被使用了。
Trigonometric functions 三角函数:
FPU浮点计算的另一个重要功能就是计算三角函数,例如正弦,余弦及正切函数等,下面就具体的介绍下和这些运算相关的指令。
The FSIN and FCOS instructions FSIN和FCOS指令:
这些基本的三角函数都有一个隐示的源操作数位于ST0里,当指令运算完后,结果存储在ST0中。
这里需要注意的是,这些三角函数都是使用radians(弧度)作为源操作数的单位,如果你的应用程序里的数据是用的degrees(度)为单位的话,就必须在使用三角函数前,先将度转为弧度,可以使用下面的公式来进行转换:
radians = (degrees * pi) / 180 |
上面这个公式可以使用下面的代码片段来实现:
flds degree1 # 将degree1内存里以degrees(度)为单位的值加载到ST0 fidivs val180 # 将ST0除以val180内存位置里的值180 fldpi # 将pi加载到ST0,这样之前的degrees / 180的结果就会移入ST1 fmul %st(1), %st(0) # 将ST0里的pi乘以ST1里degrees / 180的值,得到对应的弧度值保存在ST0里 fsin # 对ST0里的弧度值进行正弦函数运算 |
下面的trigtest1.s程式完整的演示了FSIN,FCOS的用法以及度转弧度的方法:
# trigtest1.s - An example of using the FSIN and FCOS instructions .section .data degree1: .float 90.0 val180: .int 180 .section .bss .lcomm radian1, 4 .lcomm result1, 4 .lcomm result2, 4 .section .text .globl _start _start: nop finit flds degree1 fidivs val180 fldpi fmul %st(1), %st(0) fsts radian1 fsin fsts result1 flds radian1 fcos fsts result2 movl $1, %eax movl $0, %ebx int $0x80 |
上面代码在将degree1里的90度转为弧度值后,将转换后的弧度值存储在radian1内存处,这样后面执行FCOS时就可以直接加载这个转换好的弧度值,代码中将FSIN指令计算的正弦值存储在result1处,将FCOS指令计算的余弦值存储在result2位置。
trigtest1程序汇编链接后,在调试器里的输出结果如下:
$ as -gstabs -o trigtest1.o trigtest1.s $ ld -o trigtest1 trigtest1.o $ gdb -q trigtest1 Reading symbols from /home/zengl/Downloads/asm_example/fpuAdv/trigtest1...done. ........................................................... (gdb) x/fw &result1 0x80490bc <result1>: 1 (gdb) x/fw &result2 0x80490c0 <result2>: -4.37113883e-08 (gdb) |
从输出结果可以看到result1里存储的是90度的正弦值1,result2里为余弦值0,由于弧度在保存和加载的过程中存在精度丢失,所以result2得到的结果是一个很接近0的值。
小提示:可以预先将pi /180的值计算并存储到FPU,这样度转弧度时速度要快很多。
The FSINCOS instruction FSINCOS指令:
如果你需要同时计算出弧度的正弦和余弦值的话,可以使用FSINCOS指令,该指令运算完后,会先将sin正弦值替换掉ST0里的源弧度值,再将cos余弦值压入栈,这样结果就是cos余弦值将存储在ST0,sin正弦值将存储在ST1,下面的trigtest2.s程式就演示了该指令的用法:
# trigtest2.s - An example of using the FSINCOS instruction .section .data degree1: .float 90.0 val180: .int 180 .section .bss .lcomm sinresult, 4 .lcomm cosresult, 4 .section .text .globl _start _start: nop finit flds degree1 fidivs val180 fldpi fmul %st(1), %st(0) fsincos fstps cosresult fsts sinresult movl $1, %eax movl $0, %ebx int $0x80 |
trigtest2程式的结果将分别存储在cosresult和sinresult内存位置,如下所示:
(gdb) x/fw &cosresult 0x80490b0 <cosresult>: -2.71050543e-20 (gdb) x/fw &sinresult 0x80490ac <sinresult>: 1 (gdb) |
cosresult里的值并非精确的0,但也非常接近了,sinresult里为正确的值1 。
The FPTAN and FPATAN instructions FPTAN和FPATAN指令:
[zengl pagebreak]
The FPTAN and FPATAN instructions FPTAN和FPATAN指令:
FPTAN用于计算tanST0即ST0的正切函数值,它在计算完后,会先将tanST0的值压入寄存器栈,再将1压入栈,之所以这么做,是为了兼容80287的FPU协处理器,因为那时FSIN和FCOS指令还不可用,要计算这些值就需要用到tan正切值的倒数,FPTAN在计算后,只要再使用FDIV指令就可以得到这个倒数了(ST0里的1除以ST1里的tanST0值)。
FPATAN指令用于计算反正切值,它需要的隐示源操作数有两个,一个ST0,一个ST1,该指令会计算arctan(ST1/ST0)即ST1与ST0的商的反正切函数值,并将结果存储在ST1,然后再将ST0弹出寄存器栈,这样结果就会由ST1往上挪入ST0,之所以要计算ST1与ST0商的反正切值,是因为这种形式下,当ST0为0时,ST1除以ST0就会是无穷大,这样就可以得到无穷大的反正切值。标准的ANSI C函数atan2(double x, double y)的内部实现也是类似的做法。
Logarithmic functions 对数函数:
FPU里通过FYL2X和FYL2XP1指令来计算log以2为底的对数,其中FYL2X指令的运算方式如下:
ST(1) * log2(ST(0)) |
FYL2XP1指令的运算方式如下:
ST(1) * log2(ST(0) + 1.0) |
FYL2XP1这个指令在Log后的数很接近一时,比FYL2X有较好的准确度。
另外,还有个FSCALE指令可以对ST0的值进行缩放,该指令的运算方式如下:
ST(0) <-- ST(0) * 2RoundTowardZero(ST(1)) |
它会将ST0乘以2的ST1次方,结果存储在ST0,当ST1为正数时,ST0的值就会扩大,当ST1为负数时,ST0的值就会缩小,下面的fscaletest.s程式就演示了FSCALE指令的用法:
# fscaletest.s - An example of the FSCALE instruction .section .data value: .float 10.0 scale1: .float 2.0 scale2: .float -2.0 .section .bss .lcomm result1, 4 .lcomm result2, 4 .section .text .globl _start _start: nop finit flds scale1 flds value fscale fsts result1 flds scale2 flds value fscale fsts result2 movl $1, %eax movl $0, %ebx int $0x80 |
上面代码先将scale1里的2.0压入栈,再将value内存里的10.0压入栈,这样ST0就为10,ST1就为2,经过FSCALE指令后,ST0里的结果就会变为10 * 22 = 40,该结果通过fsts result1指令存储到result1的内存位置,接着将scale2里的-2.0和value里的10依次压入栈,再执行一次FSCALE指令,ST0的结果就会变为10 * 2-2 = 2.5 ,该结果会被保存在result2的内存位置。
fscaletest.s程式经过汇编链接后,调试输出的结果如下:
(gdb) x/fw &result1 0x80490b8 <result1>: 40 (gdb) x/fw &result2 0x80490bc <result2>: 2.5 (gdb) |
该输出结果和预期的一致。
小提示:FSCALE指令的效果类似于整数运算里左移、右移指令的效果。
尽管FYL2X和FYL2XP1指令只支持log以2为底的对数运算,但是你可以通过下面的公式来计算log以其他数为底的对数:
logbX = (1/log2b) * log2X |
上面的公式表示log以b为底X的对数,等于log以2为底b的对数的倒数,乘以log以2为底X的对数。
下面的logtest.s程式就演示了计算log以10为底12的对数的方法:
# logtest.s - An example of using the FYL2X instruction .section .data value: .float 12.0 base: .float 10.0 .section .bss .lcomm result, 4 .section .text .globl _start _start: nop finit fld1 flds base fyl2x fld1 fdivp flds value fyl2x fsts result movl $1, %eax movl $0, %ebx int $0x80 |
上面的代码先将1和base里的10压入栈,此时ST0就为10,ST1就为1,执行FYL2X时,就会得到1 * log210即log210的结果,然后再将1压入栈,通过FDIVP除法指令就可以得到log210的倒数,最后将value里的12压入栈,此时ST0为12,ST1为log210的倒数,再执行FYL2X指令,就能得到log210的倒数与log212的乘积,根据前面的公式可知,该结果就是log1012的值。
Floating-Point Conditional Branches 浮点条件分支指令:
要判断一个整数是否大于,等于或小于另一个整数,可以通过CMP指令,再加上EFLAGS寄存器里的各种标志来进行判断,但是,要比较两个浮点数的大小就没有这么简单,在FPU里提供了一些专门用于浮点数比较的指令。
The FCOM instruction family FCOM指令集:
FCOM指令集可以将ST0里的浮点值与其他FPU寄存器或内存里的浮点值进行比较,该指令集中可用的指令如下表所示:
|
上面这些指令比较的结果会被设置到FPU状态寄存器的C0、C2和C3这三个条件代码位里,可能的比较结果如下表所示:
|
下面的fcomtest.s程式就演示了FCOM比较指令的用法:
# fcomtest.s - An example of the FCOM instruction .section .data value1: .float 10.923 value2: .float 4.5532 .section .text .globl _start _start: nop flds value1 fcoms value2 fstsw sahf ja greater jb lessthan movl $1, %eax movl $0, %ebx int $0x80 greater: movl $1, %eax movl $2, %ebx int $0x80 lessthan: movl $1, %eax movl $1, %ebx int $0x80 |
上面代码先通过flds value1指令将value1里的10.923加载到ST0,然后使用fcoms value2指令将ST0里的10.923与value2内存里的4.5532进行比较。
接着,代码里使用了一个技巧来检测浮点比较的结果:先用fstsw指令将FPU状态寄存器里的值设置到AX寄存器,再通过sahf指令将AH里的值加载到EFLAGS寄存器,之所以这么做,是因为AH寄存器的0 , 2 , 4 , 6 及 7位依次对应EFLAGS寄存器里的carry进位,parity奇偶,aligned对齐,zero是否为零,及sign符号标志位,而当我们通过fstsw指令将状态寄存器的值设置到AX后,AH里的0 , 2 及 6位正好就是C0,C2和C3的值,这样经过fstsw,sahf两条指令后,FPU状态寄存器和EFLAGS寄存器就会有如下的对应关系:
- FPU状态寄存器里的C0对应EFLAGS寄存器里的carry进位标志
- 状态寄存器里的C2对应EFLAGS寄存器里的parity奇偶标志
- 状态寄存器里的C3对应EFLAGS寄存器里的zero是否为零标志
之所以可以使用这一技巧来检测浮点的比较结果,都要归功于英特尔工程师的巧妙设计。
fcomtest.s程式会根据浮点数的比较结果产生不同的退出码,在linux命令行下可以使用echo $?命令来进行查看:
$ ./fcomtest $ echo $? 2 $ |
上面输出结果为2,表示代码里value1内存里的值大于value2内存里的值。你也可以修改代码,使用不同的值来进行测试。
在检测两个浮点数是否相等时,要注意的是:浮点数在加载到FPU寄存器后,会被转为双精度扩展的浮点格式,在这一转化过程里,有可能会产生一些舍入上的误差值,这样,标准单精度或标准双精度浮点数在加载到FPU寄存器后,不一定会完全等于原来的值,所以,在这种情况下,进行等于比较时,最好是通过它们的差值是否在一定的误差范围内来进行判断。
The FCOMI instruction family FCOMI指令集:
前面的fcomtest.s例子是通过fstsw和sahf两条指令来建立起FPU状态寄存器和EFLAGS标志寄存器之间的映射关系的,那么是否存在一种指令,能够在比较后,自动完成这一映射呢?答案是:可以的。
从Pentium Pro处理器开始,就可以使用FCOMI指令在比较浮点数后,自动将FPU状态寄存器里C0,C2和C3的值设置到EFLAGS寄存器的carry进位,parity奇偶,及zero是否为零标志位。
下表显示了FCOMI指令集里可用的指令:
|
上面这些FCOMI指令只能比较两个FPU寄存器里的值,不能直接和内存里的浮点数进行比较,所以如果要对内存里的值进行FCOMI的比较,就必须先将内存里的值加载到FPU寄存器。
上表中,最后两条FUCOMI和FUCOMIP指令在进行比较前,会先利用FPU的tag标记寄存器来判断需要比较的值是否是有效的浮点数,如果不是有效的浮点数,就会抛出异常。
FCOMI指令的比较结果可以直接使用EFLAGS寄存器里的各种标志来进行判断,如下表所示:
|
下面的fcomitest.s程式就演示了FCOMI指令的用法:
# fcomitest.s - An example of the FCOMI instruction .section .data value1: .float 10.923 value2: .float 4.5532 .section .text .globl _start _start: nop flds value2 flds value1 fcomi %st(1), %st(0) ja greater jb lessthan movl $1, %eax movl $0, %ebx int $0x80 greater: movl $1, %eax movl $2, %ebx int $0x80 lessthan: movl $1, %eax movl $1, %ebx int $0x80 |
上面代码里依次将value2和value1加载到FPU寄存器栈,这样加载后,value1里的10.923就会位于ST0,而value2里的4.5532就会位于ST1,然后使用FCOMI指令进行比较后,EFLAGS寄存器里就保存了比较的结果,接着就可以使用JA和JB指令来进行大于,小于的判断操作。
fcomitest.s经过汇编链接后,测试结果如下:
$ as -gstabs -o fcomitest.o fcomitest.s $ ld -o fcomitest fcomitest.o $ ./fcomitest $ echo $? 2 $ |
可以看到FCOMI指令的比较结果和之前提到的FCOM指令的比较结果一致,你也可以使用其他的值进行测试。
The FCMOV instruction family FCMOV指令集:
类似于整数的CMOV指令,FCMOV指令集可以让你根据条件来进行浮点数的传值,该指令集里的每个指令都是根据EFLAGS标志寄存器里的各种标志来决定是否需要将ST(x)里的值传值到ST0 。
FCMOV指令通常会配合FCOMI指令来一起使用,因为FCOMI在完成浮点数的大小比较后,会自动设置EFLAGS里对应的标志,而这些标志就可以让FCMOV决定是否需要进行传值操作。
下表显示了FCMOV指令集里可用的各种指令:
|
FCMOV指令集的指令格式如下:
fcmovxx source, destination |
上面格式里,source源操作数是ST(x)寄存器,而destination目标操作数是ST(0)寄存器。
下面的fcmovtest.s程式演示了FCMOV指令的用法:
# fcmovtest.s - An example of the FCMOVxx instructions .section .data value1: .float 20.5 value2: .float 10.90 .section .text .globl _start _start: nop finit flds value1 flds value2 fcomi %st(1), %st(0) fcmovb %st(1), %st(0) movl $1, %eax movl $0, %ebx int $0x80 |
上面的代码先将value1和value2依次压入FPU寄存器栈,完成压栈操作后,ST0里为10.90,ST1里则为20.5,接着通过FCOMI指令比较ST0和ST1的大小,并根据比较的结果设置EFLAGS里的各种标志,由于ST0小于ST1,所以EFLAGS里的carry标志就会被设置,最后执行FCMOVB指令时,就会将ST1里的20.5传值给ST0,结果就是ST0和ST1里的值都为20.5,该程序调试输出的情况如下:
$ as -gstabs -o fcmovtest.o fcmovtest.s $ ld -o fcmovtest fcmovtest.o $ gdb -q fcmovtest Reading symbols from /home/zengl/Downloads/asm_example/fcmov/fcmovtest...done. .................................... 15 fcmovb %st(1), %st(0) (gdb) s 16 movl $1, %eax (gdb) info all .................................... eflags 0x203 [ CF IF ] .................................... st0 20.5 (raw 0x4003a400000000000000) st1 20.5 .................................... (gdb) |
提示:FCMOV指令集只能用于Pentium Pro及以后的处理器,之前的处理器则不能使用这些指令。
Saving and Restoring the FPU State 保存和恢复FPU的状态:
之前的章节提到过,FPU的数据寄存器还可以用作MMX的寄存器,用于存放80位的压缩整数,所以如果你的程序既要使用MMX的指令,又要使用FPU的浮点指令的话,就容易出错,庆幸的是,英特尔提供了一些特殊的指令来保存和恢复FPU的的当前工作状态,这样当你进行MMX的工作流程之前,可以先将FPU的当前状态保存到内存里,等MMX相关运算完毕,再切换回之前的工作状态,这样程序就不容易出错了。
下面就介绍保存和恢复FPU工作状态相关的指令。
Saving and restoring the FPU environment 保存和恢复FPU的环境:
我们可以使用FSTENV指令来将FPU的当前环境保存到内存里,该指令会将FPU里下面这些寄存器的内容进行保存:
- Control register 控制寄存器
- Status register 状态寄存器
- Tag register 标记寄存器
- FPU instruction pointer offset FPU指令指针偏移值
- FPU data pointer FPU数据指针
- FPU last opcode executed FPU里最后执行过的操作码
# fpuenv.s - An example of the FSTENV and FLDENV instructions .section .data value1: .float 12.34 value2: .float 56.789 rup: .byte 0x7f, 0x0b .section .bss .lcomm buffer, 28 .section .text .globl _start _start: nop finit flds value1 flds value2 fldcw rup fstenv buffer finit flds value2 flds value1 fldenv buffer movl $1, %eax movl $0, %ebx int $0x80 |
fpuenv.s程式在初始化FPU后,将value1和value2依次加载到FPU寄存器栈,然后修改了控制寄存器,将控制寄存器里的舍入方式设为向上舍入,接着使用FSTENV指令将FPU当前的环境保存到buffer内存位置,在gdb调试器里,可以通过x命令来查看buffer里保存的数据:
|
可以看到buffer内存里的0x7f 0x0b对应控制寄存器里的值,0x00 0x30对应状态寄存器里的值。在数据保存到内存中后,代码再通过finit指令对FPU重新初始化,并重新加载了两个值到寄存器栈,可以通过info float及info all命令来查看重新初始化FPU后寄存器里的值。
最后,代码使用fldenv buffer指令将buffer里保存的数据恢复到FPU中,FPU里的控制寄存器将恢复为之前设置的向上舍入方式,但是FPU数据寄存器里的值并不会被恢复,因为FSTENV和FLDENV指令只能备份和恢复除了数据寄存器以外的其他寄存器,如果想将数据寄存器里的值也备份到内存,则可以使用下面要介绍的指令。
Saving and restoring the FPU state 保存和恢复FPU的完整工作状态:
前面的FSTENV和FLDENV指令不能保存数据寄存器,如果想将数据寄存器也一起进行保存和恢复的话,可以使用FSAVE和FRSTOR指令,其中,FSAVE指令用于将FPU里所有的寄存器包括数据寄存器里的值保存到一个108字节的内存块里,然后重新初始化FPU。而FRSTOR指令则用于将FSAVE指令保存的数据恢复到FPU寄存器。
下面的fpusave.s程式就演示了FSAVE和FRSTOR指令的用法:
# fpusave.s - An example of the FSAVE and FRSTOR instructions .section .data value1: .float 12.34 value2: .float 56.789 rup: .byte 0x7f, 0x0b .section .bss .lcomm buffer, 108 .section .text .globl _start _start: nop finit flds value1 flds value2 fldcw rup fsave buffer flds value2 flds value1 frstor buffer movl $1, %eax movl $0, %ebx int $0x80 |
在FLDS指令加载数据到寄存器栈,及FLDCW指令将控制寄存器设为向上舍入后,就使用FSAVE指令将FPU里的所有数据保存到buffer内存里,在FSAVE指令执行之前,FPU寄存器的情况如下:
(gdb) info all ................................. st0 56.78900146484375 (raw 0x4004e327f00000000000) st1 12.340000152587890625 (raw 0x4002c570a40000000000) st2 0 (raw 0x00000000000000000000) st3 0 (raw 0x00000000000000000000) st4 0 (raw 0x00000000000000000000) st5 0 (raw 0x00000000000000000000) st6 0 (raw 0x00000000000000000000) st7 0 (raw 0x00000000000000000000) fctrl 0xb7f 2943 fstat 0x3000 12288 ftag 0xfff 4095 ................................. (gdb) info float R7: Valid 0x4002c570a40000000000 +12.34000015258789062 =>R6: Valid 0x4004e327f00000000000 +56.78900146484375 R5: Empty 0x00000000000000000000 R4: Empty 0x00000000000000000000 R3: Empty 0x00000000000000000000 R2: Empty 0x00000000000000000000 R1: Empty 0x00000000000000000000 R0: Empty 0x00000000000000000000 Status Word: 0x3000 TOP: 6 Control Word: 0x0b7f IM DM ZM OM UM PM PC: Extended Precision (64-bits) RC: Round up Tag Word: 0x0fff (gdb) |
可以看到value1和value2里的两个值被加载到寄存器栈,同时fctrl控制寄存器的值为0xb7f即舍入方式被设为Round up向上舍入。
当FSAVE指令执行完后,FPU寄存器的情况如下:
(gdb) info all ................................. st0 0 (raw 0x00000000000000000000) st1 0 (raw 0x00000000000000000000) st2 0 (raw 0x00000000000000000000) st3 0 (raw 0x00000000000000000000) st4 0 (raw 0x00000000000000000000) st5 0 (raw 0x00000000000000000000) st6 56.78900146484375 (raw 0x4004e327f00000000000) st7 12.340000152587890625 (raw 0x4002c570a40000000000) fctrl 0x37f 895 fstat 0x0 0 ftag 0xffff 65535 ................................. (gdb) info float R7: Empty 0x4002c570a40000000000 R6: Empty 0x4004e327f00000000000 R5: Empty 0x00000000000000000000 R4: Empty 0x00000000000000000000 R3: Empty 0x00000000000000000000 R2: Empty 0x00000000000000000000 R1: Empty 0x00000000000000000000 =>R0: Empty 0x00000000000000000000 Status Word: 0x0000 TOP: 0 Control Word: 0x037f IM DM ZM OM UM PM PC: Extended Precision (64-bits) RC: Round to nearest Tag Word: 0xffff ................................. (gdb) |
可以看到,FSAVE指令会将FPU进行重新初始化,控制寄存器的值恢复为默认的0x037f,此时舍入方式为默认的Round to nearest(舍入到最接近的值)。之所以value1和value2的值56.789和12.34会跑到栈底ST6及ST7的位置,是因为Status状态寄存器由于被重新初始化为默认值,TOP为0,即此时的栈顶ST0对应的是R0寄存器(在FSAVE执行前,ST0对应的是R6寄存器),ST6对应R6,ST7对应R7,所以之前加载的两个数就跑到栈底去了,另外R6和R7显示Empty,是因为Tag标记寄存器也初始化为默认值,所有的数据寄存器此时都被认为是空的,即使R6和R7里有数据,也会被FPU认为是空的,之后再FLDS加载数据时,R7和R6的数据就会被覆盖掉(FPU重新初始化时不会将数据寄存器里的数据清空,只会将其他寄存器初始化为默认值)。
另外,FSAVE指令还会将FPU之前的数据都保存到buffer内存位置:
|
此时buffer内存里的值不仅包括FPU控制,状态及标记寄存器里的值,还包括所有数据寄存器里的值。在执行完FRSTOR指令后,FPU寄存器的情况如下:
(gdb) info all ............................... st0 56.78900146484375 (raw 0x4004e327f00000000000) st1 12.340000152587890625 (raw 0x4002c570a40000000000) st2 0 (raw 0x00000000000000000000) st3 0 (raw 0x00000000000000000000) st4 0 (raw 0x00000000000000000000) st5 0 (raw 0x00000000000000000000) st6 0 (raw 0x00000000000000000000) st7 0 (raw 0x00000000000000000000) fctrl 0xb7f 2943 fstat 0x3000 12288 ftag 0xfff 4095 ............................... (gdb) |
从上面的输出可以看到,数据寄存器和控制寄存器里的值都恢复为FSAVE执行之前的值了。
Waiting versus Nonwaiting Instructions 等待与非等待指令:
在之前的FPU寄存器介绍篇里,曾提到过,状态寄存器里有6个浮点异常标志位,如果浮点指令执行时发生了异常(如除零异常),就会设置对应的异常标志位,大部分的浮点指令,如前面提到的FSAVE指令,在执行前都会先检测之前的浮点指令是否产生了异常,如果产生了异常,就必须等待异常处理完毕,再继续执行当前的指令。这些指令就属于等待指令。
FPU里还存在一种非等待指令,可以不处理之前指令产生的异常,而直接执行,可用的非等待指令如下表所示:
|
Optimizing Floating-Point Calculations 优化浮点计算:
浮点计算经常是汇编程序里最耗时的部分,英特尔提供了一些建议,可以帮助优化你的汇编浮点计算:
- 确保要操作的浮点数没有超过数据寄存器可以容纳的尺寸
- 没有很高精度要求的计算中,将精度控制位设为单精度
- 对于简单的三角函数计算,尽量使用查询表的方法
- 尽可能打破依赖链,例如可以将z = a + b + c + d表达式替换为
x = a + b; y = c + d; z = x + y.这三个表达式。 - 尽可能的将方程式里的值保存在FPU寄存器里
- 当整数和浮点数一起计算时,最好将整数加载到FPU寄存器,再进行计算,
这样比直接和整数进行浮点运算的速度要快,例如FIDIV指令可以替换为:
先用FILD加载整数到FPU寄存器,再使用FDIVP指令对FPU寄存器里的值进行运算。 - 尽可能使用FCOMI指令来代替FCOM指令。
logtest.s程式经过汇编链接后,调试输出的结果如下:
$ as -gstabs -o logtest.o logtest.s $ ld -o logtest logtest.o $ gdb -q logtest Reading symbols from /home/zengl/Downloads/asm_example/fpuAdv/logtest...done. ..................................... (gdb) x/fw &result 0x80490a8 <result>: 1.07918119 (gdb) |
输出情况和预期的值一致。
第13部分-Linux x86 64位汇编 独立函数文件
将各个函数字自包含在自己的文件中,并且连接在一起成为最终产品。
函数的文件不需要使用_start段。不过需要将函数名字声明为全局标签,以便其他程序能够访问,使用.globl命令来完成。
例如:
.section .text
.type area,&function
.globl area
area:
示例
函数文件:
文件为area.s
-
.section .text
-
.type area, @function;//定义函数area
-
.globl area
-
area:
-
push %rbp;//压栈ebp寄存器
-
mov %rsp, %rbp;//将esp赋值为ebp
-
subq $8, %rsp;//设置rsp网上走,这里其实没有实际作用,64位系统中一个压栈就是8个字节
-
fldpi;//加载π到st0
-
filds 16(%rbp) ;//加载ebp+8就是调用函数前压栈的参数到st0,π移到st1
-
fmul %st(0), %st(0) ;//st0和st0相乘,保存在st0
-
fmulp %st(0), %st(1) ;//st0和st1相乘,结果在栈顶st0
-
fstps -8(%rbp) ;//保存结果到栈中, 8个字节,也就是挡墙rsp所指的位置。
-
movl -8(%rbp), %eax;//最后将结果移动到eax寄存器,是单精度值
-
-
mov %rbp, %rsp;//恢复esp寄存器
-
pop %rbp;//恢复ebp寄存器
-
ret;//返回函数
-
-
- 主程序文件
-
主程序不包含area函数的任何代码。
文件为funcmain.s
-
.extern printf ;//调用外部的printf函数
-
.section .data
-
precision:
-
.byte 0x7f, 0x00
-
resultstr:
-
.ascii "Area result is %f.\n"
-
-
.section .bss
-
.lcomm bresult, 4
-
-
.section .text
-
.globl _start
-
_start:
-
nop
-
finit;//FPU初始化
-
fldcw precision;//加载FPU控制器
-
push $10;//加载10到栈中
-
call area;//调用area函数
-
addq $8, %rsp;//增加8个字节,即跳过压栈的参数
-
movl %eax, bresult;//保存结果到result
-
-
movq bresult,%xmm0;//结果复制到xmm0寄存器
-
cvtps2pd %xmm0,%xmm0;//转化单精度为双精度
-
movq $resultstr,%rdi
-
call printf
-
-
push $2
-
call area
-
addq $8, %rsp
-
movl %eax, bresult
-
movq $resultstr,%rdi
-
movq bresult,%xmm0
-
cvtps2pd %xmm0,%xmm0
-
call printf
-
-
push $120
-
call area
-
addq $8, %rsp
-
movl %eax, bresult
-
movq $resultstr,%rdi
-
movq bresult,%xmm0
-
cvtps2pd %xmm0,%xmm0
-
call printf
-
-
mov $60,%rax
-
syscall
-
进行编译连接:
as -g -o area.o area.s
as -g -o funcmain.o funcmain.s
ld -o funcmain funcmain.o area.o -lc -I /lib64/ld-linux-x86-64.so.2
第14部分-Linux x86 64位汇编 命令行参数
以上篇中编译的的funcmain为例。
使用gdb进行调试。
#gdb -q funcmain
(gdb) b *_start+1
(gdb)r
(gdb) print /x $rsp
$3 = 0x7fffffffde90
(gdb) x /20x 0x7fffffffde90
0x7fffffffde90: 0x00000001 0x00000000 0xffffe268 0x00007fff
0x7fffffffdea0: 0x00000000 0x00000000 0xffffe29d 0x00007fff
0x7fffffffdeb0: 0xffffe889 0x00007fff 0xffffe8a4 0x00007fff
0x7fffffffdec0: 0xffffe8b8 0x00007fff 0xffffe8e9 0x00007fff
0x7fffffffded0: 0xffffe8fb 0x00007fff 0xffffe91d 0x00007fff
第一个8个字节0x00000001 0x00000000组合在一起是0x0000000000000001。就是参数的数量了。
第二个8个字节将0xffffe268和0x00007fff组合在一起,8个字节的地址是0x7fffffffe268:
(gdb) x /s 0x7fffffffe268
0x7fffffffe268: "/root/funcmain"
看到这个是程序名字。
第三个8个字节是0。用于分割参数和指向环境变量的指针。
后面开始都是环境变量的指针了。
第四个8个字节0x00007fffffffe29d
(gdb) x /s 0x00007fffffffe29d
0x7fffffffe29d: "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...
第五个8个字节: 0x00007fffffffe889
(gdb) x /s 0x00007fffffffe889
0x7fffffffe889: "LC_MEASUREMENT=zh_CN.UTF-8"
第六个8个字节: 0x00007fffffffe8a4
(gdb) x /s 0x00007fffffffe8a4
0x7fffffffe8a4: "laq47=172.16.11.105"
第七个8个字节: 0x00007fffffffe8b8
(gdb) x /s 0x00007fffffffe8b8
0x7fffffffe8b8: "SSH_CONNECTION=XX.XX.XX.XX 58792 30.54.166.64 22"
第八个8个字节: 0x00007fffffffe8e9
(gdb) x /s 0x00007fffffffe8e9
0x7fffffffe8e9: "hg1=192.168.1.101"
第九个8个字节: 0x00007fffffffe8fb
(gdb) x /s 0x00007fffffffe8fb
0x7fffffffe8fb: "LESSCLOSE=/usr/bin/lesspipe %s %s"
第十个8个字节: 0x00007fffffffe91d
(gdb) x /s 0x00007fffffffe91d
0x7fffffffe91d: "hg3=XX.XX.X.XX"
第15部分-Linux x86 64位汇编 命令行参数
程序启动时,一些应用程序需要在命令行中指定输入参数。
不同的操作系统使用不同的方法把命令行参数传递给程序。
程序启动时候,Linux把如下4种类型的信息存放到程序堆栈中。
命令行参数的数目/shell提示符执行的程序的名称/命令行中包含的任何命令行参数/程序启动时所有当前Linux的环境变量。
而且这些元素的指针加载到堆栈中可以方便定位。
程序启动时候,堆栈的一般布局如下:
查看命令行参数示例
-
.extern printf ;//调用外部的printf函数
-
.section .data
-
output1:
-
.asciz "There are %d parameters! \n"
-
output2:
-
.asciz "%s\n"
-
count:
-
.int 0
-
resultstr:
-
.ascii "Area result is %f.\n"
-
-
.section .text
-
.globl _start
-
_start:
-
movq (%rsp), %rcx;//保存esp指向的第一个参数给rcs,就是参数数目
-
-
movq $output1,%rdi;// output1字符串地址
-
movq %rcx,%rsi;
-
call printf;//调用printf函数,打印参数数量
-
-
movq %rsp, %rbp;//移动rsp移动rbp,保存rbp
-
add $8, %rbp;//rbp往下走一个字节,到程序名字
-
movq (%rsp), %rcx;//rsp指向的第一个参数rcs,就是参数数目
-
dec %rcx;//减去1,去掉程序本身这个参数
-
cmp $0,%rcx;//如果只有一个文件名参数则直接退出,无需打印。
-
je exit;
-
-
loop1:
-
mov %rcx,count
-
add $8, %rbp;//rbp加8,继续往下走。使得Rbp寄存器指向第一个参数。
-
movq $output2,%rdi;//output2字符串地址
-
movq (%rbp),%rsi;//栈指针
-
call printf;//调用printf函数,打印参数数量
-
mov count, %rcx
-
loop loop1;//rcx-1,继续循环,直到rcx=0打印完毕所有参数。
-
exit:
-
mov $60,%rax
-
syscall
as -g -o paramtest.o paramtest.s
ld -o paramtest paramtest.o -lc -I /lib64/ld-linux-x86-64.so.2
第16部分-Linux x86 64位汇编 命令行参数
查看环境变量示例
-
.extern printf ;//调用外部的printf函数
-
.section .data
-
output:
-
.asciz "%s\n"
-
resultstr:
-
.ascii "Area result is %f.\n"
-
-
.section .text
-
.globl _start
-
_start:
-
mov %rsp, %rbp;//rsp移动到rbp
-
add $24, %rbp;//指向第一个环境变量
-
mov $0,%rcx
-
loop1:
-
cmpl $0, (%rbp)
-
je endit
-
movq $output,%rdi;//output2字符串地址
-
movq (%rbp),%rsi;//栈指针
-
call printf;//调用printf函数,打印参数数量
-
add $8, %rbp
-
loop loop1
-
endit:
-
mov $60,%rax
-
syscall
as -g -o paramtest.o paramtest2.s
ld -o paramtest paramtest.o -lc -I /lib64/ld-linux-x86-64.so.2
可以看到所有的环境变量的输出,这个和在系统中使用env命令是一样的。
第17部分-Linux x86 64位汇编 命令行参数
使用命令行参数示例
在堆栈中命令行参数被存储为字符串值。需要进行转换工作,转换为数字。
可以使用C库函数的:
atoi:把ASCII字符串转换为短整数值
atol:把ASCII字符串转换为长整数值
atof:把ASCII字符串转换为双精度浮点值
这些函数需要将字符串位置的指针必须放置堆栈中。
atoi函数结果返回到EAX寄存器中。Atol的结果存放在EDX:EAX寄存器中,需要64位。函数atof结果返回到fpu的st0寄存器中。
-
-
- 示例
-
读取命令行参数把它转换为整数值,然后计算圆面积。
-
.extern printf ;//调用外部的printf函数
-
.section .data
-
output:
-
.asciz "The area is: %f\n"
-
.section .bss
-
.lcomm result, 8
-
.section .text
-
.globl _start
-
_start:
-
nop
-
finit;//初始化FPU
-
-
mov 16(%rsp),%rdi;//指向第一个参数
-
call atoi;//调用atoi为短整数
-
movq %rax, result
-
fldpi;//加载π到st0
-
filds result;//加载result到st0,π移动到st1
-
fmul %st(0), %st(0);//result相等并存放于st0
-
fmul %st(1), %st(0);//将st0和st1相乘,结果存放于st0
-
fstpl (%rsp);//将st0存放于堆栈中,覆盖了指向程序名的指针。
-
movq $output,%rdi;//output2字符串地址
-
movq (%rsp),%xmm0;//栈指针
-
call printf;//调用printf函数,打印参数数量
-
movq $0,%rdi
-
call exit
as -g -o paramtest.o paramtest3.s
ld -o paramtest paramtest.o -lc -I /lib64/ld-linux-x86-64.so.2
然后执行
./paramtest 20
即可得到输出:
The area is: 1256.637061
这里要注意的是,如果不增加一个参数是会报段错误的。
第18部分-Linux x86 64位汇编 AT&T实现栈
我们根据上篇中AT&T汇编语法改造之前栈篇的代码如下。
主要好处是通过as编译后,可以通过gdb进行debug。
-
.section .data
-
.equ SYS_WRITE,1
-
.equ STD_IN,1
-
.equ SYS_EXIT,60
-
.equ EXIT_CODE,0
-
-
.equ NEW_LINE,0xa
-
WRONG_ARGC: .ascii "Must be two command line argument\n"
-
-
.section .text
-
.global _start
-
-
_start:
-
pop %rcx ;//栈的第一个保存的是参数的数量,数量不为3则跳转到argcError处退出。
-
cmp $3, %rcx ;//两个参数,外加一个程序名,程序名也是参数。
-
jne argcError
-
-
addq $8, %rsp;//[rsp+8]保存的是第一个参数argv[0],跳过程序名这个变量。
-
popq %rsi;//将第一个参数赋值给rsi。
-
call str_to_int;//调用函数str_to_int,将参数转换为整型,保存于rax。
-
movq %rax, %r10; //保存到r10寄存器中
-
popq %rsi; //将第二个参数赋值为rsi
-
call str_to_int; //调用函数str_to_int,字符串保存的,例如“123\0”
-
movq %rax,%r11; //保存到r11寄存器中
-
addq %r11, %r10; //完成加法。
-
movq %r10, %rax
-
xor %r12, %r12
-
jmp int_to_str; //调用函数int_to_str函数,整型转换为字符串
-
-
argcError:
-
movq $1,%rax
-
movq $1,%rdi
-
movq $WRONG_ARGC, %rsi
-
movq $34, %rdx
-
syscall
-
jmp exit
-
-
str_to_int:; //负责将字符串转换为整型
-
xor %rax, %rax; //清空rax寄存器
-
movq $10,%rcx; //赋值rcx为10
-
next:
-
cmpb $0,(%rsi); //对比参数的低位字节,是否为0,字符串最后一个为’\0’。
-
je return_str; //为0,则调用函数return_str返回。
-
movb (%rsi),%bl; //否则将低8位赋值给bl。
-
subb $48, %bl; //参数减去48,ASCII码中,字符和数字相差48。
-
mulq %rcx; //乘以10,将已处理的字符往高位挪。
-
addq %rbx, %rax;
-
inc %rsi; //增加rsi,即变为下一个字节。
-
jmp next; //调到函数next
-
-
return_str:
-
ret; //直接返回。
-
-
int_to_str:
-
movq $0, %rdx
-
movq $10, %rbx
-
divq %rbx; //除以10,获取个位数余数在rdx,商在rax。
-
addq $48,%rdx
-
;addq $0x0,%rdx
-
pushq %rdx
-
inc %r12; //首次迭代为0,记录字符的个数,用于后续输出。
-
cmp $0x0,%rax; //商是否为0,为0则退出,跳转到print进行输出。
-
jne int_to_str; //商不为0,则继续输出。
-
jmp print
-
-
print:
-
movq $1,%rax
-
mulq %r12
-
movq $8,%rax;//计算长度,因为入栈是8个字节一次的,所以每个字符输出都是8个字节了
-
mulq %r12
-
movq %rax,%rdx
-
-
movq $SYS_WRITE,%rax
-
movq $STD_IN,%rdi
-
movq %rsp,%rsi
-
syscall; //调用sys_write输出结果
-
jmp exit
-
-
exit:
-
movq $SYS_EXIT,%rax
-
movq $EXIT_CODE,%rdi
-
syscall
编译:
# as -g -o addsum_att.o addsum_att.s
#ld -o addsum_att addsum_att.o
第19部分- Linux x86 64位汇编GDB单步调试
本篇我们使用gdb来调试上篇中的汇编代码。
gdb调试
使用gdb进行调试。
#gdb ./addsum_arg
设置参数:
(gdb) set args 1 1
#启动程序
(gdb) r
#查看寄存器
(gdb) i r rcx
可以看到启动进程时候rcs为0.
查看rsp寄存器对应的地址.
(gdb) i r rsp
rsp 0x7fffffffe3d0 0x7fffffffe3d0
查看rsp寄存器地址所多对应的内容字符串,命令是x /16s,使用16进制显示可以使用x /16x ,这个rsp是栈的指针,可以看到当前指向的是”\003”也是程序的参数量3个。
(gdb) x /16s $rsp
0x7fffffffe3b0: "\003"
0x7fffffffe3b2: ""
0x7fffffffe3b3: ""
0x7fffffffe3b4: ""
0x7fffffffe3b5: ""
0x7fffffffe3b6: ""
0x7fffffffe3b7: ""
0x7fffffffe3b8: "]\346\377\377\377\177"
0x7fffffffe3bf: ""
0x7fffffffe3c0: "y\346\377\377\377\177"
0x7fffffffe3c7: ""
0x7fffffffe3c8: "}\346\377\377\377\177"
0x7fffffffe3cf: ""
0x7fffffffe3d0: ""
0x7fffffffe3d1: ""
0x7fffffffe3d2: ""
通过16进制显示如下:
(gdb) x /16x $rsp
0x7fffffffe3b0: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffe3b8: 0x5d 0xe6 0xff 0xff 0xff 0x7f 0x00 0x00
设置断点
(gdb)b 15
(gdb)c
然后开始执行,停止在代码15行:cmp $3, %rcx;
此时rcx已经弹出,查看rcx得到
(gdb) i r rcx
rcx 0x3 3
rsp寄存器也发生了变化,
(gdb) i r rsp
rsp 0x7fffffffe3b8 0x7fffffffe3b8
从0x7fffffffe3d0变成了0x7fffffffe3b8
(gdb) c
popq %rsi;//将第一个参数赋值给rsi。
(gdb) i r rsp
rsp 0x7fffffffe3c0 0x7fffffffe3c0
这个栈地址保存的内容为:
(gdb) x /16x $rsp
0x7fffffffe3c0: 0xffffe67a 0x00007fff 0xffffe67c 0x00007fff
前八个字节为0xffffe67a 0x00007fff,组合在一起就是
0x00007fffffffe67a就是一个新的地址,就是要赋值为rsi的。
查看这个地址内容如下:
(gdb) x /16s 0x00007fffffffe67a
0x7fffffffe67a: "1"
0x7fffffffe67c: "1"
0x7fffffffe67e: "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...
0x7fffffffe746: "=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=0"...
0x7fffffffe80e: "1;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.al"...
0x7fffffffe8d6: "z=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;"...
0x7fffffffe99e: "35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*."...
0x7fffffffea66: "m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;"...
0x7fffffffeb2e: "35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka="...
0x7fffffffebf6: "00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:"
0x7fffffffec6a: "LC_MEASUREMENT=zh_CN.UTF-8"
0x7fffffffec85: "SSH_CONNECTION=XX.XX.XX.XX 52057 XX.XX.XX.XX 22"
0x7fffffffecb9: "LESSCLOSE=/usr/bin/lesspipe %s %s"
0x7fffffffecdb: "LC_PAPER=zh_CN.UTF-8"
0x7fffffffecf0: "LC_MONETARY=zh_CN.UTF-8"
0x7fffffffed08: "_=/usr/bin/gdb"
这个地址空间保存的环境变量。
继续使用s进行单步往前走
(gdb) s
call str_to_int;//调用函数str_to_int,将参数转换为整型,保存于rax。
(gdb)s
直接跳到了函数str_to_int处了。
…
当到达代码:je return_str; //为0,则调用函数return_str返回。
(gdb) i r eflags
eflags 0x246 [ PF ZF IF ]
置位zf,所以跳转到return_str.
可以单步往下走进行观察。
第20部分-Linux x86 64位汇编 AT&T语法深入
字符串汇编语法
Intel汇编和AT&T的语法基本一致。
有如下:
(1) lodsb、lodsw:把DS:SI指向的存储单元中的数据装入AL或AX,然后根据DF标志增减SI
(2) stosb、stosw:把AL或AX中的数据装入ES:DI指向的存储单元,然后根据DF标志增减DI
(3) movsb、movsw:把DS:SI指向的存储单元中的数据装入ES:DI指向的存储单元中,然后根据DF标志分别增减SI和DI
(4) scasb、scasw:把AL或AX中的数据与ES:DI指向的存储单元中的数据相减,影响标志位,然后根据DF标志分别增减SI和DI
(5) cmpsb、cmpsw:把DS:SI指向的存储单元中的数据与ES:DI指向的存储单元中的数据相减,影响标志位,然后根据DF标志分别增减SI和DI
(6) rep:重复其后的串操作指令。重复前先判断CX是否为0,为0就结束重复,否则CX减1,重复其后的串操作指令。主要用在MOVS和STOS前。一般不用在LODS前。
上述指令涉及的寄存器:段寄存器DS和ES、变址寄存器SI和DI、累加器AX、计数器CX
涉及的标志位:DF、AF、CF、OF、PF、SF、ZF
汇总后如下,看上去更加,五个字串基本操作指令
指令 功能 說 明
-------- ------ ----------------------------------
CMPSB 比较 比较来源字串与目的字串一个字节
CMPSW 一个字
CMPSD 一个双字
LODSB 载入 将来源字串一个指定的字元载入AL
LODSW 字载入AX
LODSD 双字载入EAX
MOVSB 搬移 从来源字串搬移一个字节至目的字串
MOVSW 字至目的字串
MOVSD 双字至目的字串
SCASB 扫描 扫描来源字串一个指定的字元
SCASW 字
SCASD 双字
STOSB 存储 将AL存至字串一个指定的字节位址
STOSW AX 字
STOSD EAX 双字
字串指令所使用的运算元
指令 第一个运算元 第二个运算元 搭配虚拟指令
------ ------------ ------------ ------------
MOVSB DS:SI ES:DI REP
MOVSW DS:SI ES:DI REP
MOVSD DS:SI ES:DI REP
CMPSB DS:SI ES:DI REPE,REPNE
CMPSW DS:SI ES:DI REPE,REPNE
CMPSD DS:SI ES:DI REPE,REPNE
SCASB AL ES:DI REPE,REPNE
SCASW AX ES:DI REPE,REPNE
SCASD EAX ES:DI REPE,REPNE
LODSB AL DS:SI
LODSW AX DS:SI
LODSD EAX DS:SI
STOSB ES:DI AL REP
STOSW ES:DI AX REP
STOSD ES:DI EAX REP
有些REP指令除监视ECX外还监视ZF(零标志)的状态。
--------------------------------------------------
指令 描述
-------------------------------------------
REPE 等于时重复
REPNE 不等于时重复
REPNZ 不为0时重复
REPZ 为0时重复
标记位
CF(carry flag):进位标志 描述了最近操作是否发生了进位(可以检查无符号操作是否溢出)
ZF(zero flag):零标志 最近操作结果为0(列如 逻辑操作 等)
SF(sign flag):符号标志最近操作结果为负数
OF(overflow flag):溢出标志最近操作导致一个补码溢出 补码溢出通常有两种结果(正溢出或者负溢出)
PF 奇偶校验(parity)标志 寄存器包含数学操作造成的错误操作
汇编跳转
JE ;等于则跳转
JNE ;不等于则跳转
JZ ;为 0 则跳转
JNZ ;不为 0 则跳转
JS ;为负则跳转
JNS ;不为负则跳转
JC ;进位则跳转
JNC ;不进位则跳转
JO ;溢出则跳转
JNO ;不溢出则跳转
JA ;无符号大于则跳转
JNA ;无符号不大于则跳转
JAE ;无符号大于等于则跳转
JNAE ;无符号不大于等于则跳转
JG ;有符号大于则跳转
JNG ;有符号不大于则跳转
JGE ;有符号大于等于则跳转
JNGE ;有符号不大于等于则跳转
JB ;无符号小于则跳转
JNB ;无符号不小于则跳转
JBE ;无符号小于等于则跳转
JNBE ;无符号不小于等于则跳转
JL ;有符号小于则跳转
JNL ;有符号不小于则跳转
JLE ;有符号小于等于则跳转
JNLE ;有符号不小于等于则跳转
JP ;奇偶位置位则跳转
JNP ;奇偶位清除则跳转
JPE ;奇偶位相等则跳转
JPO ;奇偶位不等则跳转
寄存器相关
-
-
- cld 和sld
-
cld相对应的指令是std,二者均是用来操作方向标志位DF(Direction Flag)。cld使DF 复位,即是让DF=0,std使DF置位,即DF=1.这两个指令用于串操作指令中。通过执行cld或std指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)。
数据交换指令
指令 描述
XCHG 在两个寄存器之间或者寄存器和内存器之间交换值
BSWAP 反转一个32位寄存器中的字节顺序
XADD 交换来嗯改制并且把总和存储在目标操作数中
CMPXCHG 把一个值和一个外部值进行比较,并且交换它和另一个值
CMPXCHG8B 比较两个64位值并且交换他们