一、问题引入
最近在项目中遇到一个问题,当使用double类型数据时,在进行jce编解码后会出现乱数据问题,比如encode一个数据.
代码语言:txt复制Encode:
{
"index": 10,
"score": 10.12,
......
}
再decode出来,会发现与原来encode进去的数据不一样,看起来像是未定义的一个值
代码语言:txt复制Decode:
{
"index": 10,
"score": -1.53533e 267,
......
}
二、问题定位
项目之前也有相同的应用场景,但是没有出现问题,所以首先怀疑jce版本是否有升级过,但发现jce版本没有被改动过,可以排除是jce的问题(实际上也是jce的问题,后面解释)。想到最近项目在编译时加了-O2的优化选项,故验证之,果然是-O2搞的鬼。但是为什么加了-O2的优化选项会触发这个bug,为了解决这个问题,需要弄清楚两点:
- 编译时加-O2会有哪些优化选项
- jce 的哪些代码会触发这个bug gcc -O2优化开启了很多优化选项,其中有一项就是-fstrict-aliasing,先来看看gcc 对-fstrict-aliasing的解释:Allows the compiler to assume the strictest aliasing rules applicable to the language being compiled. For C (and C ), this activates optimizations based on the type of expressions. In particular, an object of one type is assumed never to reside at the same address as an object of a different type, unless the types are almost the same. For example, an unsigned int can alias an int, but not a void* or a double. A character type may alias any other type.
大概意思是说不同类型(除了相似的类型,比如int 和 unsigned int)的指针不会指向同一个内存地址,如果违反这个规则,将会出现未知情况,举个例子:
代码语言:txt复制[huanghaibin33@DevTJ-todo ~/test]$ cat test_aliasing.cpp
#include <iostream>
int main()
{
int i = 0x12345678;
short *p = (short *) &i;
short tmp;
tmp = *p;
*p = *(p 1);
*(p 1) = tmp;
printf("i=%xn", i);
return 0;
}
[huanghaibin33@DevTJ-todo ~/test]$ g test_aliasing.cpp -o test_aliasing
[huanghaibin33@DevTJ-todo ~/test]$ ./test_aliasing
i=56781234
[huanghaibin33@DevTJ-todo ~/test]$ g -O2 test_aliasing.cpp -o test_aliasing
[huanghaibin33@DevTJ-todo ~/test]$ ./test_aliasing
i=12345678
[huanghaibin33@DevTJ-todo ~/test]$ g -O2 -fno-strict-aliasing test_aliasing.cpp -o test_aliasing
[huanghaibin33@DevTJ-todo ~/test]$ ./test_aliasing
i=56781234
这段代码的目的是交换一个int类型的前两个字节和后两个字节,正常编译和加了-O2, -fno-strict-aliasing 选项,程序可以正常运行,但是加了-O2而不加-fno-strict-aliasing 时, 结果并不是我们预期想要的。原因是加了-O2选项,默认打开了-strict-aliasing,程序中的short *p = (short *) &i, 破坏了aliasing 规则,编译器不会认为short 型指针p 指向 整形&i 的地址,因此对p的操作不会影响到i 的结果。
至此问题比较清晰了,接下来看看jce 哪块代码违反了aliasing规则:
代码语言:txt复制inline Int64 jce_htonll(Int64 x)
{
jce::bswap_helper h;
h.i64 = x;
Int32 tmp = htonl(h.i32[1]);
h.i32[1] = htonl(h.i32[0]);
h.i32[0] = tmp;
return h.i64;
}
inline Double jce_ntohd(Double x)
{
Int64 __t__ = jce_htonll((*((Int64 *)&x)));
return *((Double *) &__t__);
}
上述有两处代码违反了aliasing规则,编译出来的程序运行结果将不可知。wup已经在新版本wup-linux-c -1.0.8.1.tgz 修复了这个bug,看看修复的代码:
代码语言:txt复制inline Double jce_ntohd(Double x)
{
union helper {
Double d;
Int64 i64;
};
helper.d = x;
helper.i64 = jce_htonll( helper.i64 );
return helper.d;
}
三、解决方法
- 从代码层面上优化,比如可以通过union数据结构巧妙的进行转换,可以看下面代码
- 在开启-O2 和-O3 的情况下,可以加上-fno-strict-aliasing,允许不同指针指向同一个内存地址。(这在已有代码违反aliasing规则比较多的情况下是一个快速解决方法)
- 不开启-O2、-O3优化
[huanghaibin33@DevTJ-todo ~/test]$ cat test_aliasing_v2.cpp
#include <iostream>
int main()
{
union helper {
int i;
short s;
};
int i = 0x12345678;
helper h;
h.i = i;
short tmp = h.s;
h.s = *(&(h.s) 1);
*(&(h.s) 1) = tmp;
i = h.i;
printf("i=%xn", i);
return 0;
}
[huanghaibin33@DevTJ-todo ~/test]$ g test_aliasing_v2.cpp -o test_aliasing_v2
[huanghaibin33@DevTJ-todo ~/test]$ ./test_aliasing_v2
i=56781234
[huanghaibin33@DevTJ-todo ~/test]$ g -O2 test_aliasing_v2.cpp -o test_aliasing_v2
[huanghaibin33@DevTJ-todo ~/test]$ ./test_aliasing_v2
i=56781234
[huanghaibin33@DevTJ-todo ~/test]$ g -O2 -fno-strict-aliasing test_aliasing_v2.cpp -o test_aliasing_v2
[huanghaibin33@DevTJ-todo ~/test]$ ./test_aliasing_v2
i=56781234
四、结语
在存在强制类型转换的情况下,采用-O1和采用-O2或-O3产生的运行结果是不同的。在项目中应尽量避免不同类型的指针转换,使用编译优化选项时要多加重视编译告警。
参考资料
http://km.oa.com/group/578/articles/show/150732?kmref=search&from_page=1&no=1
https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule