32位平台gcc -O1优化下unsigned int第31位检测错误如何解决
问题描述
在32位处理器上使用gcc编译C代码,-O0优化等级下运行正常,但开启-O1(含-Ofast)优化时输出不符合预期。
问题代码
void foo() { volatile unsigned int *reg = (volatile unsigned int *)0x1000; unsigned int reg_value; unsigned int busy; do { reg_value = *reg; busy = (reg_value & 0x80000000U); } while (busy == 0); }
不同优化等级编译结果
-O1优化生成的汇编
1030cea6 <foo>: 1030cea6: a1 00 10 00 00 mov 0x1000,%eax 1030ceab: 85 c0 test %eax,%eax 1030cead: 79 f7 jns 1030cea6 <foo> 1030ceaf: c3 ret
原有疑问:
test %eax,%eax会检测全部32位,而非仅检测第31位,不符合代码逻辑预期。
-O0优化生成的汇编(符合预期结果)
10312b6d: 55 push %ebp 10312b6e: 89 e5 mov %esp,%ebp 10312b70: 83 ec 10 sub $0x10,%esp 10312b73: c7 45 fc 00 10 00 00 movl $0x1000,-0x4(%ebp) 10312b7a: 8b 45 fc mov -0x4(%ebp),%eax 10312b7d: 8b 00 mov (%eax),%eax 10312b7f: 89 45 f8 mov %eax,-0x8(%ebp) 10312b82: 8b 45 f8 mov -0x8(%ebp),%eax 10312b85: 25 00 00 00 80 and $0x80000000,%eax 10312b8a: 89 45 f4 mov %eax,-0xc(%ebp) 10312b8d: 83 7d f4 00 cmpl $0x0,-0xc(%ebp) 10312b91: 74 e7 je 10312b7a <foo+0xd> 10312b93: 90 nop 10312b94: c9 leave 10312b95: c3 ret
说明:
and $0x80000000,%eax将检测范围限制为仅第31位,和代码写法直接对应。
对照组测试结果
将代码修改为检测第30位而非第31位,也就是将busy = (reg_value & 0x80000000U);改为busy = (reg_value & 0x40000000U);,-O1优化下会生成和代码写法直接对应的输出:
1030cea6: a1 00 10 00 00 mov 0x1000,%eax 1030ceab: a9 00 00 00 40 test $0x40000000,%eax 1030ceb0: 74 f4 je 1030cea6 <foo> 1030ceb2: c3 ret
逻辑误解说明
首先纠正原有判断的误差:你认为test %eax,%eax会检测全部32位是不准确的。test %eax,%eax执行后会按结果设置多个标志位,而后续的jns指令只判断符号标志位SF,SF的值正好等于32位寄存器的最高位(第31位)的值,所以jns 1030cea6等价于“第31位为0时跳转”,这段汇编的逻辑和你的C代码逻辑实际上是完全等价的,运行结果一致。
如果你的场景需要强制生成显式检测0x80000000位的汇编(比如要兼容非x86架构、或者要匹配硬件调试的指令特征),可以使用以下两种方案修改:
方案1:修改代码逻辑,避免优化歧义
直接在循环条件里做比特位检测,强制编译器识别你要检测的是具体比特位:
void foo() { volatile unsigned int *reg = (volatile unsigned int *)0x1000; do { // 写法1:直接按位与判断,不赋值给中间变量 } while ((*reg & 0x80000000U) == 0); }
或者使用移位写法完全规避符号相关的优化歧义:
void foo() { volatile unsigned int *reg = (volatile unsigned int *)0x1000; do { // 写法2:右移31位后判断最低位是否为1 } while (((*reg) >> 31) == 0); }
以上两种写法都会让gcc在-O1优化下生成test $0x80000000,%eax+je的汇编指令,和O0优化的执行逻辑完全对应。
方案2:添加编译选项禁用相关优化
如果不想修改业务代码,可以给当前编译单元添加-fno-strict-overflow编译选项,关闭gcc基于有符号数溢出未定义行为的优化,也可以避免这个优化歧义。
内容的提问来源于stack exchange,提问作者James

