• 汇编浮点指令


    浮点数如何存储

    浮点数的运算完全不同于整数,从寄存器到指令,都有一套独特的处理流程,浮点单元也称作x87 FPU。

    现在看浮点数的表示方式,我们所知道的,计算机使用二进制存储数据,所表示的数字都具有确定性,那是如何表示浮点这种具有近似效果的数据呢,答案是通过科学计数,科学计数由符号,尾数和指数表示,这三部分都是一个整数值,具体来看一下IEEE二进制浮点标准:

    格式说明
    单精度 32位:符号占1位,指数占8位,尾数中的小数部分占23位
    双精度 64位:符号占1位,指数占11位,尾数中的小数部分占52位
    扩展精度 80位:符号占1位,指数占16位,尾数中的小数部分占63位

    以单精度为例,在内存中的储存格式如下(左边为高位):

    	| 1位符号 | 8位指数 | 23位尾数 |
    

    其中符号位1表示负数,0表示正数,这与整数形式的符号位意义相同; 科学计数法表示形式如 m * (b ^ e),m为尾数,b为基数,e是指数,再二进制中,基数毫无疑问是2,对单精度,指数为中间8位二进制表示的数字,其中的尾数是形如1.1101 小数点后面的整数值。

    关于指数,由于需要表示正负两种数据,IEEE标准规定单精度指数以127为分割线,实际存储的数据是指数加127所得结果,127为高位为零,后7位为1所得,其他双精度也以此方式计算。

    为了解释内存中浮点数的存储方式,举一个浮点数的例子说明:

    float test = 123.456;
    
    int main()
    {
        return 0;
    }
    

    例子再简单不过了,仅仅定义了一个全局的float类型,我们通过gcc -S test.c来生成汇编,看看123.456是如何存储的,打开反汇编后的文件,看到符号_test后定义的数字是 1123477881(这里gcc定义成了long类型,不过没有关系,因为都是四字节数字,具体的类型还得看如何使用)。可以使用计算器把十进制数字转化为二进制:0 10000101 11101101110100101111001,这里根据单精度的划分方式把32位划分成三部分,符号位为0,为正数,指数为 133,减去127得6,尾数加上1.,形式为1.11101101110100101111001,扩大2 ^ 23次方为111101101110100101111001,十进制16181625,后除以2 ^ (23 – 6) = 131072,结果为123.45600128173828125,与我们所定义的浮点数正好相符。

    浮点寄存器

    这里介绍了浮点数的二进制表示,前面说过浮点单元计算使用独立的寄存器,在寄存器那篇也稍有提及,这里详细说明一下浮点单元的寄存器设施。

    FPU有 8 个独立寻址的80位寄存器,名称分别为r0, r1, …, r7,他们以堆栈形式组织在一起,统称为寄存器栈,编写浮点指令时栈顶也写为st(0),最后一个寄存器写作st(7)。

    FPU另有3个16位的寄存器,分别为控制寄存器、状态寄存器、标记寄存器,现一一详细说明此三个寄存器的作用:

    状态寄存器,为用户记录浮点计算过程中的状态,其中各位的含义如下:

    0 —— 非法操作异常
    1 —— 非规格化操作数异常
    2 —— 除数为0异常
    3 —— 溢出标志异常
    4 —— 下溢标志异常
    5 —— 精度异常标志
    6 —— 堆栈错误
    7 —— 错误汇总状态
    8 —— 条件代码位0(c0)
    9 —— 条件代码位1(c1)
    10 —— 条件代码位2 (c2)
    11-13 —— 堆栈顶指针
    14 —— 条件代码位3(c3)
    15 —— 繁忙标志
    

    其中读取状态寄存器内容可使用 fstsw %ax

    控制寄存器的位含义如下:

    0 —— 非法操作异常掩码 
    1 —— 非法格式化异常掩码 
    2 —— 除数为0异常掩码 
    3 —— 溢出异常掩码 
    4 —— 下溢异常掩码 
    5 —— 精度异常亚曼 
    6-7 —— 保留 
    8-9 —— 精度控制(00单精度,01未使用,10双精度,11扩展精度) 
    10-11 —— 舍入控制(00舍入到最近,01向下舍入,10向上舍入,11向0舍入) 
    12 —— 无穷大控制 
    13–15 —— 保留
    

    其中读取控制寄存器和设置控制寄存器的指令如下:

    # 加载到内存
    fstcw control
    # 加载到控制器
    fldcw control
    

    最后的标志寄存器最为简单,分别0-15位分别标志r0-r7共8个寄存器,每个寄存器占2位,这两位的含义如下:

    11 —— 合法扩展精度 
    01 —— 零 
    10 —— 特殊浮点 
    11 —— 无内容
    

    另外对浮点寄存器的一些控制指令如下:

    # 初始化fpu,控制、状态设为默认值,但不改变fpu的数据
    finit
    
    # 恢复保存环境
    fldenv buffer
    fstenv buffer
    
    #清空浮点异常
    fnclex
    
    #fpu状态保存
    fssave
    

    fstenv 保存控制寄存器、状态寄存器、标记寄存器、FPU指令指针偏移量、FPU数据指针,FPU最后执行的操作码到内存中。

    浮点数指令

    接下来将要详细说明其计算过程,要计算数据首先得看如何从内存中加载数据到寄存器,同时把结果从寄存器取出到内存,除了加载内存中的浮点数据指令,另外还有一些常量的加载,现列举如下:

    指令说明
    finit 初始化控制和状态寄存器,不改变fpu数据寄存器
    fstcw control 将控制寄存器内容放到内存control处
    fstsw status 将状态寄存器内容放到内存status处
    flds value 加载内存中的单精浮点到fpu寄存器堆栈
    fldl value 加载内存中的双精浮点到fpu寄存器堆栈
    fldt value 加载内存中的扩展精度点到fpu寄存器堆栈
    fld %st(i) 将%st(i)寄存器数据压入fpu寄存器堆栈
    fsts value 单精度数据保存到value,不出栈
    fstl value 双精度数据保存到value,不出栈
    fstt value 扩展精度数据保存到value,不出栈
    fstps value 单精度数据保存到value,出栈
    fstpl value 双精度数据保存到value,出栈
    fstpt value 扩展精度数据保存到value,出栈
    fxch %st(i) 交换%st(0)和%st(i)
    fld1 把 +1.0 压入 FPU 堆栈中
    fldl2t 把 10 的对数(底数2)压入 FPU 堆栈中
    fldl2e 把 e 的对数(底数2)压入 FPU 堆栈中
    fldpi 把 pi 的值压入 FPU 堆栈中
    fldlg2 把 2 的对数(底数10)压入 FPU 堆栈中
    fldln2 把 2 的对数(底数e) 压入堆栈中
    fldz 把 +0.0 压入压入堆栈中

    以上指令虽多,但是还是很有规律,前缀f表示fpu操作,ld加载,st保存设置,p后缀弹出堆栈,s、l、t后缀表示单精度,双精度,扩展精度,c后缀表 示控制寄存器,s后缀表示状态寄存器。当然这仅仅是对AT&T语法而言,对MASM语法没有s,l,t之分,需要使用type ptr来指明精度,即内存大小。

    学会灵活的加载弹出数据堆栈后,接下来就要看一些基本的计算:

    fadd    浮点加法
    fdiv    浮点除法
    fdivr   反向浮点除法
    fmul    浮点乘法
    fsub    浮点减法
    fsubr   反向浮点减法
    

    对于以上的每种指令,有几种指令格式,以fadd为例,列举如下:

    # 内从中的32位或者64位值和%st(0)相加
    fadd source
    
    # 把%st(x)和%st(0)相加,结果存入%st(0)
    fadd %st(x), %st(0)
    
    # 把%st(0)和%st(x)相加,结果存入%st(x)
    fadd %st(0), %st(x)
    
    # 把%st(0)和%st(x)相加,结果存入%st(x),弹出%st(0)
    faddp %st(0), %st(x)
    
    # 把%st(0)和%st(1)相加,结果存入%st(1),弹出%st(0)
    faddp
    
    # 把16位或32位整数与%st(0)相加,结果存入%st(0)
    fiadd source
    

    这仅仅是对AT&T语法而言,对MASM源操作数与目的操作数相反!另外,对AT&T,与内存相关指令可加s、l指定内存精度。其中反向加法和反向除法是计算过程中目的与源反向计算。

    浮点计算例子

    接下来举一个AT&T语法的例子,来计算表达式的值 ( 12.34 * 13 ) + 334.75 ) / 17.8 :

    # ( 12.34 * 13 ) + 334.75 ) / 17.8
    .section .data
        values: .float 12.34, 13, 334.75, 17.8
        result: .double 0.0
    
        outstring: .asciz "result is %f
    "
    .section .text
    .globl _main
    _main:
        leal values, %ebx
        flds 12(%ebx)
        flds 8(%ebx)
        flds 4(%ebx)
        flds (%ebx)
    
        fmulp
        faddp
        fdivp %st(0), %st(1)
    
        fstl result
    
        leal result, %ebx
        pushl 4(%ebx)
        pushl (%ebx)
        pushl $outstring
        call _printf
    end:
        pushl $0
        call _exit
    

    前四个flds加载所有的数据到寄存器堆栈,可以单步运行并是用gdb的print $st0打印堆栈寄存器的值,可以看到为什么是堆栈寄存器。需要说明的是由于printf的%f是double类型的输出,所以最后要把一个8字节浮点放 到栈中传递,最终结果为27.818541,可以看到与计算器计算的结果近似相等。

    浮点高级运算

    除了基本的浮点计算,x87还提供了一些诸如余弦运算等高级计算功能:

    指令说明
    f2xm1 计算2的乘方(次数为st0中的值,减去1
    fabs 计算st0中的绝对值
    fchs 改变st0中的值的符号
    fcos 计算st0中的值的余弦
    fpatan 计算st0中的值的部分反正切
    fprem 计算st0中的值除以st1的值的部分余数
    fprem1 计算st0中的值除以st1的值的IEEE部分余弦
    fptan 计算st0中的值的部分正切
    frndint 把st0中的值舍入到最近的整数
    fscale 计算st0乘以2的st1次方
    fsin 计算st0中的值的正弦
    fsincos 计算st0中的值的正弦和余弦
    fsqrt 计算st0中的值的平方根
    fyl2x 计算st1*log st0 以2为底
    fyl2xp1 计算st1*log (st0 + 1) 以2为底

    下面来看一下浮点条件分支,浮点数的比较不像整数,可以容易的使用cmp指令比较,判断eflags的值,关于浮点数比较,fpu提供独立的比较机制和指令,现对这组比较指令进行说明:

    指令说明
    fcom 比较st0和st1寄存器的值
    fcom %st(x) 比较st0和stx寄存器的值
    fcom source 比较st0和32/64位内存值
    fcomp 比较st0和st1寄存器的值,并弹出堆栈
    fcomp %st(x) 比较st0和stx寄存器的值,并弹出堆栈
    fcomp source 比较st0和32/64位内存值,并弹出堆栈
    fcompp 比较st0和st1寄存器的值,并两次弹出堆栈
    ftst 比较st0和0.0

    浮点数比较的结果放入状态寄存器的c0,c2,c3条件代码位中,其值如下:

    结果c3c2c0
    st0 > source 0 0 0
    st0 < source 0 0 1
    st0 = source 1 0 0

    如此倘若直接判断c0,c2,c3的值比较繁琐,所以可以使用一些技巧,首先使用fstsw指令获得fpu状态寄存器的值并存入ax,再使用sahf指令把 ah寄存器中的值加载到eflags寄存器中,sahf指令把ah寄存器的第0、2、4、6、7分别传送至进位、奇偶、对准、零、符号位,不影响其他标 志,ah寄存器中这些位刚好包含fpu状态寄存器的条件代码值,所以通过fstsw和sahf指令组合,可以传送如下值:

    把c0位传送到eflags的进位标志 
    把c2位传送到eflags的奇偶校验标志 
    把c3位传送到eflags的零标志
    

    传送完毕后,可以用条件跳转使用不同的结果值,另外需要说明的是浮点数相等判断,因为浮点数本身存储结构决定了它仅仅是一个近似值,所以不能直接判断是否相 等,这样可能与自己预期的结果不同,应该判断两个浮点数之差是否在一个很小的误差范围内,来决定这两个浮点数是否相等。

    根据上面的技巧,使用fstsw和fpu指令组合,可以方便的使用浮点判断结果,这对我们是一种便利,而intel的工程师又为我们设计了一个组合指令,fcomi指令执行浮点比较结果并把结果存放到eflags寄存器的进位,奇偶,和零标志。

    指令说明
    fcomi 比较st0和stx寄存器的值
    fcomip 比较st0和stx寄存器,并弹出堆栈
    fucomi 比较之前检查无序值
    fucomip 比较之前检查无序值,之后弹出堆栈

    判断结束后eflags的标志设置如下:

    结果ZFPFCF
    st0 > st(x) 0 0 0
    st0 < st(x) 0 0 1
    st0 = st(x) 1 0 0

    CMOV移动指令

    最后介绍的是类似cmov的指令,根据判断结果决定是否需要移动数据,其AT&T格式为 fcmovxx source, destination,其中source是st(x)寄存器,destination是st(0)寄存器。

    指令说明
    fcmovb 如果st(0)小于st(x),则进行传送
    fcmove 如果st(0)等于st(x),则进行传送
    fcmovbe 如果st(0)小于或等于st(x),则进行传送
    fcmovu 如果st(0)无序,则进行传送
    fcmovnb 如果st(0)不小于st(x),则进行传送
    fcmovne 如果st(0)不等于st(x),则进行传送
    fcmovnbe 如果st(0)不小于或等于st(x),则进行传送
    fcmovnu 如果st(0)非无序,则进行传送

    以上可以看出,无论从寄存器的操作,还是计算过程,都比整数运算要繁琐的多,而且看似很简单的一个表达式,转化成浮点汇编需要做很多工作,由于其复杂性,同 一个表达式可以有多种运算过程,当然其中的效率相差很大,这依赖于对浮点汇编的理解程度,好在有高级语言处理相关工作,编写浮点指令的情况比较少见。

  • 相关阅读:
    1022. 从根到叶的二进制数之和
    剑指 Offer 54. 二叉搜索树的第k大节点
    枚举--百练2811--熄灯问题
    UVA 572 BFS 图论入门
    百练1088 DP+DFS 迷宫问题
    poj 1661 动态规划 拯救老鼠
    入坑动态规划!POJ 1458字符串最大公共子序列
    文件后缀批处理
    奇妙的算法--UVA 679(二叉树的编号)
    栈_uva514
  • 原文地址:https://www.cnblogs.com/DeeLMind/p/7366512.html
Copyright © 2020-2023  润新知