#include <stdio.h> #include <stdlib.h> typedef unsigned short UINT16; typedef unsigned int UINT32; struct EndPoint{ UINT16 tcpPort_; UINT16 udpPort_; //UINT32 ipAddress_; }; inline UINT32 EndPointToUInt32(struct EndPoint* ep){ return *(const UINT32*)(ep); //buug here } struct EndPoint endpoint = {0x8080, 0x1080}; int main() { //下句在inline+乱序优化时出错 endpoint.udpPort_ = 0; UINT32 tmp2 = EndPointToUInt32(&endpoint); //UINT32 tmp2 = *(const UINT32*)(&endpoint); //用这一句替换上一句同样出错 srand(tmp2); // for break the optimize printf("%08x %08x should be same as 00008080 ", tmp2, EndPointToUInt32(&endpoint)); }
运行结果如下:
gcc buggy.c
./a.out
00008080 00008080 should be same as 00008080
gcc -O2 buggy.c
./a.out
10808080 00008080 should be same as 00008080
可以看到打开优化之后EndPointToUInt32这个函数的第一次执行就不正常了。
分析
---------
粗略的分析一下目标码
gcc直接编译的结果 | 替换掉函数调用后的结果 | gcc -o2编译的结果 |
movw $0,endpoint+2 | movw $0,endpoint+2 | movl endpoint,%ebx |
pushl $endpoint | movl endpoint,%eax | subl $28,%esp |
call EndPiontToUInt32 | movl %eax,-4(%ebp) | pushl %ebx |
addl $4,%esp | subl $12,%esp | movw $0,endpoint+2 |
movl %esx,-4(%ebp) | pushl -4(%ebp) | call srand |
subl $12,%esp | call srand | addl $12,%esp |
pushl -4(%ebp) | addl $16,%esp | pushl endpoint |
call srand | subl $4,%esp | pushl %ebx |
addl %16,%esp | pushl $endpoint | pushl $.LC0 |
subl $4,%esp | call EndPointToUInt32 | call printf |
pushl $endpoint | addl $4,%esp | |
call EndPointToUInt32 | pushl %eax | |
addl $4,%esp | pushl -4(%ebp) | |
pushl %eax | pushl $.LC0 | |
pushl -4(%ebp) | call printf | |
pushl $.LC0 | ||
call printf | ||
左边的是优化之前的代码,然后movw置endpoint的一半为0,然后取出endpoint的地址调用EndPointToUInt32,并把结果放到tmp2也就是-4(%ebp)中。
中间的代码是将函数inline化以后的结果,注意到现在直接把endpoint的内容通过%eax传给了tmp2也就是-4(%ebp)
右边的代码经过了-o2优化,首先做了一次inline操作,取消了对EndPointToUInt32的调用,也就是直接把endpoint的内容作为EndPointToUInt32的返回值来处理。其次,取消了tmp2变量,用%ebx来替代。至此都没有问题。
问题在于将movw $0,endpoint+2一句优化到了movl endpoint, %ebx的后面。这里做了一个错误的乱序优化。这是因为首先gcc没有能够正确的判断出*(const UINT32*)(&endpoint)实际上和endpoint.udpPort_是相关的,从而优化出错。本来这也是可以容忍的,毕竟写法太变态。但是gcc又在处理inline时过于冒进,没有按照真正的函数调用那样在函数调用处设置一个边界,阻止函数调用前后的代码混杂,而是像一个宏展开一样简单的处理了,最后导致了和预想不一致的结果。
结论
---------
gcc除少数版本外,在-o2乱序优化时都不够完善,不能正确判断代码的影响范围,从而做出错误的乱序。所以请不要引入一些编译器难以判断影响范围的语句,尤其是胡乱cast。典型的如上面程序中的*(const UINT32*)(ep);
gcc的乱序优化对inline函数是像宏展开一样处理的,这可能导致将函数和函数附近的代码乱序,需要小心,常用的FC3/FC5上的gcc都有此问题。