按键精灵 如何给xmmx64 xmm 寄存器器赋值,用大漠dm.AsmAdd "movsd xmm0,[eax]" 提示书写格式错误,跪求大佬指点

当然,在增加了x64扩展这个特性之后,FPU在x86兼容处理器中还是存在的。但是同事,SIMD扩展(SSE, SSE2等)已经有了,他们也可以处理浮点数。数字格式依然相同(使用IEEE754标准)。

所以,x86-64编译器通常都使用SIMD指令。可以说这是一个好消息,因为这让我们可以更容易的使用他们。 24.1 简单的例子

输入的浮点数被传入了XMM0-XMM3寄存器,其他的通过栈来传递。 a被传入了XMM0,b则是通过XMM1。 XMM寄存器是128位的(可以参考SIMD22一节),但是我们的类型是double型的,也就意味着只有一半的寄存器会被使用。

函数处理double的结果将保存在XMM0寄存器中。

这是无优化的MSVC编译器的结果:

有一些繁杂,输入参数保存在“shadow space”(影子空间,7.2.1节),但是只有低一半的寄存器,也即只有64位存了这个double的值。

GCC编译器生成了几乎一样的代码。

24.2 通过参数传递浮点型变量

他们通过XMM0-XMM3的低一半寄存器传递。

在Intel和AMD的手册中(见14章和1章)并没有MOVSDX这个指令,而只有MOVSD一个。所以在x86中有两个指令共享了同一个名字(另一个见B.6.2)。显然,微软的开发者想要避免弄得一团糟,所以他们把它重命名为MOVSDX,它只是会多把一个值载入XMM寄存器的低一半中。 pow()函数从XMM0和XMM1中加载参数,然后返回结果到XMM0中。 然后把值移动到RDX中,因为接下来printf()需要调用这个函数。为什么?老实说我也不知道,也许是因为printf()是一个参数不定的函数?

GCC让结果更清晰,printf()的值传入到了XMM0中。顺带一提,这是一个因为printf()才把1写入EAX中的例子。这意味着参数会被传递到向量寄存器中,就像标准需求一样(见21章)。

只有低一半的XMM寄存器会被使用,一组IEEE754格式的数字也会被存在这里。 显然,所有的指令都有SD后缀(标量双精度数),这些操作数是可以用于IEEE754浮点数的,他们存在XMM寄存器的低64位中。 比FPU更简单的是,显然SIMD扩展并不像FPU以前那么混乱,栈寄存器模型也没使用。 如果你像试着将例子中的double替换成float的话,它们还是会使用同样的指令,但是后缀是SS(标量单精度数),例如MOVSS,COMISS,ADDSS等等。 标量(Scalar)代表着SIMD寄存器会包含仅仅一个值,而不是所有的。可以在所有类型的值中生效的指令都被“封装”成同一个名字。


另一个在初学者的编程书中常见的例子是温度转换程序,例如将华氏度转为摄氏度,或者反过来。

我也添加了一个简单的错误处理: 1)我们应该检查用户是否输入了正确的数字 2)我们应该检查摄氏度是否低于-273゜C,因为这比绝对零度还低,学校物理课上的东西应该都还记得。 exit()函数将立即终止程序,而不会回到调用者函数。

关于这个我们可以说的是:

  • printf()的地址先被载入了ESI寄存器中,所以printf()调用的序列会被CALL ESI处理,这是一个非常著名的编译器技术,当代码中存在多个序列调用同一个函数的时候,并且/或者有空闲的寄存器可以用上的时候,编译器就会这么做。
  • 用乘法做除法的技巧也会在这儿用上。
  • 虽然我们没有指定,但是main()函数依然会返回0。C99规范告诉我们[15章, 5.1.2.2.3] main()将在没有return时也会照常返回0。 这个规则仅仅对main()函数有效。 虽然MSVC并不支持C99,但是这么看说不好他还是做到了一部分呢?

生成的代码几乎一样,但是我发现每个exit()调用之后都有INT 3。

INT 3是一个调试器断点。 可以知道的是exit()是永远不会return的函数之一。所以如果他“返回”了,那么估计发生了什么奇怪的事情,也是时候启动调试器了。

但是MSVC从2012年开始又改成了使用SIMD指令:

当然,SIMD在x86下也是可用的,包括这些浮点数的运算。使用他们计算起来也确实方便点,所以微软编译器使用了他们。 我们也可以注意到 -273 这个值会很早的被载入XMM0。这个没问题,因为编译器并不一定会按照源代码里面的顺序产生代码。

sse提供了xmm寄存器,xmm一组8个128位的寄存器,分别名为xmm0-xmm7,sse构架提供对打包单精度浮点数的SIMD支持。

sse提供了两个版本的指令,其一以后缀ps结尾,这组指令对打包单精度浮点值执行类似mmx操作运算,而第二种后缀ss,这些指令对一个量标单精度浮点 值进行运算操作,这些指令不对打包值中的所有浮点值操作,而只对打包值中的低位双字节执行操作,源操作数中剩余的3个值直接传送给结果。

把4个对准的单精度值传送到xmm寄存器或者内存
把4个不对准的单精度值传送到xmm寄存器或者内存
把1个单精度值传送到内存或者寄存器的低位双字
把2个单精度值传送到内存或者寄存器的低四字
把2个单精度值传送到内存或者寄存器的高四字
把2个单精度值从低四字传送到高四字
把2个单精度值从高四字传送到低四字

其中对准操作movaps要求数据在内存中对准16字节的边界,以提交效率,否则应使用movups传送数据。

计算打包值的平方根倒数
计算两个打包值中的最大值
计算两个打包值中的最小值
计算两个打包值的按位逻辑与
计算两个打包值的按位逻辑非
计算两个打包值的按位逻辑或
计算两个打包值的按位逻辑异或

以上指令都是用两个操作数:源操作数可以是128位内存或者xmm寄存器,目标操作数必须是xmm寄存器。

可以看到,调用加法指令之后,四组和都存储在xmm1寄存器中,gdb查看时由于不知道如何解析xmm1寄存器的内容,因为可能是单精度,也可能是双精度或者不同宽度的整数,所以只能按不同的解析方式全部显示,查看v4_float即四个单精度浮点数的显示。

下面介绍一下sse构架下的比较指令,sse的比较指令单独比较128位打包单精度浮点的每个元素,结果是一个掩码,满足比较条件的结果全为1值,不满足结果的全为0值(量标只对最低的双字执行)。

比较标量值并且设置eflags寄存器
比较标量值(包括非法值)并设置eflags寄存器

看到这里,仅仅有一个比较指令,并没有说明大小,何为满足条件全1,不满足全0呢,这样说一下指令的使用:

其中多出来的imp是一个无符号整数,这个整数表示的含义就是条件,这个条件值如下表所示:

0

如果需要比较两个数是否相等,传imp为0即可作为条件,满足条件结果全1,这是sse的比较方式。这里说明一下条件中的无序,因为是浮点比较,寄存器或内存中的有些值并不符合规定的浮点存储格式,相互比较是没有意义的,称为无序。

除了对浮点数的支持,sse指令集也有指令对mmx提供的功能进行扩展,他们对mmx寄存器中的数据执行操作:

计算打包无符号字节整数的平均值
计算打包无符号字整数的平均值
把一个字从mmx寄存器复制到通用寄存器
把一个字从通用寄存器复制到mmx寄存器
计算打包无符号字节整数的最大值
计算打包有符号字整数的最大值
计算打包无符号字节整数的最小值
计算打包有符号字整数的最小值
将打包无符号字整数相乘并且存储高位结果
计算无符号字节整数的绝对差的总和

SSE2 指令集又对 SSE 指令集做了很多扩充,主要对操作双精度浮点数和128位打包整数值执行数学操作,下面介绍SSE2的使用,先来看数据传送指令:

把2个对准的双精度值传送到xmm寄存器或者内存
把2个不对准的双精度值传送到xmm寄存器或者内存
把2个对准的四字节整数传送到xmm寄存器或者内存
把2个不对准的四字节整数传送到xmm寄存器或者内存
把1个双精度值传送到内存或者寄存器的低四字
把1个双精度值传送到内存或者寄存器的高四字
把1个双精度值传送到内存或者寄存器的低四字

SSE2指令集提供处理打包双精度浮点数,打包字整数,打包双字整数和打包四字整数值的数学指令,这里列举SSE2的加法指令来说明这一系列指令格式:

将打包双精度浮点值相加
将量标双精度浮点值相加
将打包带符号字节整数相加
将打包带符号字整数相加
将打包带符号双字整数相加
将打包带符号四字整数相加

这里虽然只列举add系列指令,这些选项也存在于乘法和除法操作中(mulpd, mulsd, divpd, divsd等)。
另外同sse指令集,sse2指令集也提供专门的数学操作,sqrt, max, min。

最后我们来看SSE3指令集,SSE3构架并没有提供任何新的数据类型,仅仅添加了几条指令,用于更快的执行标准函数,下面是新指令的列表:

把第一个fpu寄存器的值转换为整数(舍入)并且从fpu堆栈弹出
快速从内存加载128位不对准的数据值
传送128位值,复制第2个和第4个32位数据元素
传送128位值,复制第1个和第3个32位数据元素
传送64位值,赋值值,使之成为128位值
对于打包单精度浮点数,对第2个和第4个32位执行加法,第1和第3个32位执行减法
对于打包单精度浮点数,对第2对64位值执行加法,第1对位执行减法
对操作数的相邻的元素执行单精度浮点加法操作
对操作数的相邻的元素执行双精度浮点加法操作
对操作数的相邻的元素执行单精度浮点减法操作
对操作数的相邻的元素执行双精度浮点减法操作

SSE指令繁多,这里举得例子却很少,以后我会在此文继续附加一些说明例子,方便理解


参考资料

 

随机推荐