0x01起因:
工作时,稳定的老应用代码移植到新平台后,出现的错误,一段复杂类型的计算得到的值和vxworks上、以及windows下用vs x86-64编译调试出的结果不同,再仔细核对代码以及其汇编后发现有该问题,不知是bug还是就是如此设计。
0x02分析:
上图中的d就是代码出问题的运算的结果,我将这段代码重新命名,方便记录。
前提是:当在其它环境进行运算时,d的结果是‘'000000000000000c’(10进制的12),但是当在qnx用aarch64le编译后运算的结果是'ffffffff0000000c'
(本来想附个图片,但是一直上传失败)
分析:
1、看代码,首先当运算量的类型不同,则先转换成同一类型,然后进行运算。(转换按数据长度增加的方向进行,以保证精度不降低),那最后d运算中的a,b都是扩展到了unsigned long进行的运算。
2、之后去看生成的汇编代码如图所示。(图片上传一直失败,我直接copy过来了)
int main(void) {
0: d10083ff sub sp, sp, #0x20
/*- 初始化入口 */
unsigned char a = 8;
4: 52800100 mov w0, #0x8 // #8
8: 39002fe0 strb w0, [sp,#11]
unsigned int b = 5754 - a * 3600;
c: 39402fe1 ldrb w1, [sp,#11]
10: 1281c1e0 mov w0, #0xfffff1f0 // #-3600
14: 1b007c20 mul w0, w1, w0
18: 11400400 add w0, w0, #0x1, lsl #12
1c: 1119e800 add w0, w0, #0x67a
20: b9000fe0 str w0, [sp,#12]
unsigned long c = 5766;
24: d282d0c0 mov x0, #0x1686 // #5766
28: f9000be0 str x0, [sp,#16]
unsigned long d = (c - a * 3600 - b);
2c: 39402fe1 ldrb w1, [sp,#11]
30: 2a0103e0 mov w0, w1
34: 531c6c00 lsl w0, w0, #4
38: 4b010000 sub w0, w0, w1
3c: 531c6c01 lsl w1, w0, #4
40: 4b000021 sub w1, w1, w0
44: 531c6c20 lsl w0, w1, #4
48: 2a0003e1 mov w1, w0
4c: 2a0103e0 mov w0, w1
50: 93407c00 sxtw x0, w0
54: f9400be1 ldr x1, [sp,#16]
58: cb000021 sub x1, x1, x0
5c: b9400fe0 ldr w0, [sp,#12]
60: cb000020 sub x0, x1, x0
64: f9000fe0 str x0, [sp,#24]
return EXIT_SUCCESS;
0x03原因:
那分析完汇编后,其实就找到了问题所在(看了很长时间),看下上述汇编中的5c行和60行,就是原因所在。
首先大概说一下寄存器中的值,和这块的操作。
ldr操作:首先w0获得的就是值b,因为b是int类型的,所以用的是32位寄存器w0来保存b的值。
sub操作:x1是(c-a*3600)的结果,x0是w0寄存器的64位扩展(这块我解释一下,相当于x0是64位的,它的低32位就是w0),所以做的操作就是x0 = x1 - x0.
好的问题终于来了
当w0为正数时,x0与w0相等,当w0为负数时,w0的最高位为1,那么在保证值不变的情况下,x0想与w0的值相同,应该用命令sxtw让w0这个负数做有符号位的扩展,之后扩展后的值给到x0,这样才能保证w0和x0的值相同,但根据汇编代码,发现汇编器并没有做这一步,直接进行的相减。
x1的值为:1111111111111111111111111111111111111111111111111010011000000110
理想中x0的值:1111111111111111111111111111111111111111111111111010010111111010
实际x0的值:0000000000000000000000000000000011111111111111111010010111111010
x1-x0理想中的值 = ‘000000000000000c’
x1-x0在arch64中实际的值为 = ‘ffffffff0000000c’
0x04验证猜想:
但我不觉得这是bug,因为我定义的b是一个unsigned int类型,编译器可能认为是一个正数,所以扩展不扩展是无所谓的,为了验证我这个猜想,我将unsigned int 转为了 int 再一次进行了测试,发现在5c行路由不同,具体如下
5c: b9800fe0 ldrsw x0, [sp,#12]
当我用int定义b时,那么汇编器认为我可能是个有符号的数,所以在从栈帧上那的时候,就给它做了有符号扩展,扩展的数为x0。这么运算就对了。
所以我就在猜想为什么在vs和vxworks上的结果是不同的那,猜想是这样的:
在qnx上arch64拿到一个无符号的值进行寄存器扩展时,那么汇编器对寄存器的扩展恒为无符号扩展
在vsx86上拿到一个无符号的值进行寄存器扩展时,那么汇编器对寄存器的扩展是恒为有符号扩展
之后我那vs和qnx下分别写了一个测试用例证明我的猜想,代码如下 :
unsigned long a = 500;
unsigned int b = 0xffffffff;
unsigned long c = a + b;
printf("%p", c);
在vs x86-64的环境下给出的结果是:00000000000001F3
解释:因为vs进行的是有符号扩展,所以0xffffffff->0xffffffffffffffff等于-1,500+-1=499,最后的结果就是1F3。
在qnx aarch64le的环境下给出的结果是:1000001f3
解释:因为qnx进行的是无符号扩展,所以0xffffffff->0x00000000ffffffff等于4294967295,500+4,294,967,295=4,294,967,795最后的结果就是1000001f3
和我猜想的一样。
我又将long int 变为 int short做了一次测试,但是结果和我上面的结论有些不同,根据产生的汇编文件研究发现,因为short类型的变量,汇编还是拿的32位寄存器存的,所以不存在对寄存器的扩展,避免了上述的情况。
0x05总结:
在qnx上arch64拿到一个无符号的值进行寄存器扩展时,那么汇编器对寄存器的扩展恒为无符号扩展
在vsx86上拿到一个无符号的值进行寄存器扩展时,那么汇编器对寄存器的扩展是恒为有符号扩展
在现实应用中,这种情况大多出现在32位变量到64位变量的转化的场景,因为根据测试发现(64位的环境)小于等于32位的变量汇编语言大多都用的是32位寄存器,就不存在上述的情况。