gcc 编译参数 -fno-strict-aliasing

2018-07-17 19:23:35 浏览数 (1)

一、问题引入

  最近在项目中遇到一个问题,当使用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,为了解决这个问题,需要弄清楚两点:

  1. 编译时加-O2会有哪些优化选项
  2. 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;
}

三、解决方法

  1. 从代码层面上优化,比如可以通过union数据结构巧妙的进行转换,可以看下面代码
  2. 在开启-O2 和-O3 的情况下,可以加上-fno-strict-aliasing,允许不同指针指向同一个内存地址。(这在已有代码违反aliasing规则比较多的情况下是一个快速解决方法)
  3. 不开启-O2、-O3优化
代码语言:c 复制
[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

gcc

0 人点赞