int32_t foo(int32_t x) { return x + 1; }
当x == INT_MAX时,这是未定义的行为.现在说我用inline assembly代替了
int32_t foo(int32_t x) { asm("incl %0" : "+g"(x)); return x; }
解决方法
有关这个问题的更通用版本,请参见Does undefined behavior apply to asm code?(与此相关的x86程序集和GNU C inline asm语言扩展).这里的答案主要集中在C方面,C和C标准中引用了标准对实现定义的语言扩展有多少要求.
另请参阅this comp.lang.c thread,了解是否有意义,说它有一般UB是有意义的,因为并不是所有的实现都有这样的扩展.
BTW,如果您只想在GNU C中使用定义的2的补全行为签名回卷,请编译为-fwrapv
.不要使用inline asm. (或者使用__attribute__为只需要它的函数启用该选项.)wrapv与-fno-strict-overflow
不完全相同,它只是基于假设程序没有UB来禁用优化;例如,在编译时常数计算中溢出只能使用-fwrapv安全.
内联asm行为是实现定义的,而GNU C inline asm is defined作为编译器的黑盒子.输入进入,输出出来,编译器不知道如何.所有它知道的是你告诉它使用out / in / clobber约束.
您使用内联asm的foo行为相同
int32_t foo(int32_t x) { uint32_t u = x; return ++u; }
在x86上,因为x86是一个2的补码机器,所以整数包绕是明确定义的. (除了性能:asm版本打败了常量传播,并且还使编译器无法将x-inc(x)优化为-1等.@L_502_5@除非没有办法通过调整来哄骗编译器生成最优的asm )
它不引起例外.设置OF标志对任何事情都没有影响,因为x86(i386和amd64)的GNU C inline asm有一个隐式的“cc”clobber,所以编译器会假设在每个inline-asm语句之后,EFLAGS中的条件代码保持垃圾. gcc6为asm引入了一个新的语法来产生标志结果(可以在你的asm中保存一个SETCC,并且由编译器为要返回标志条件的asm块生成一个TEST).
一些体系结构在整数溢出中引发异常(陷阱),但x86不是其中之一(except when a division quotient doesn’t fit in the destination register).在MIPS上,如果您希望它们能够在没有陷阱的情况下进行换行,则可以使用ADDIU instead of ADDI的带符号整数. (因为它也是一个2的补码ISA,所以签名的环绕与二进制一样是无符号的环绕.)
未定义(或至少与实现相关)x86 asm中的行为:
BSF和BSR(找到第一个设置位正向或反向)如果输入为零,则将其目标寄存器与未定义的内容保持一致. (TZCNT和LZCNT没有这个问题).英特尔最近的x86 cpu确实定义了行为,即将目的地未经修改,但x86手册不能保证.有关这些含义的更多讨论,请参阅this answer中关于TZCNT的部分. TZCNT / LZCNT / POPCNT对Intel cpu的输出有错误的依赖关系.
其他几个指令在某些/所有情况下都会留下一些标志. (特别是AF / PF). IMUL例如留下ZF,PF和AF未定义.
可能任何给定的cpu具有一致的行为,但重要的是,即使它们仍然是x86,其他cpu可能会有所不同.如果您是微软,英特尔将会设计未来的cpu,以免破坏您现有的代码.如果您的代码是广泛依赖的,那么最好只坚持使用手册中记录的行为,而不仅仅是您的cpu发生的事情.见Andy Glew’s answer and comments here. Andy是英特尔P6微架构的架构师之一.
这些例子与C中的UB不一样.它们更像C将被称为“实现定义”,因为我们只是谈论一个未指定的值,而不是nasal demons的可能性(或者更合理的修改其他寄存器,或跳到某处).
对于真正未定义的行为,您可能需要查看特权指令,或至少是多线程代码.自修改代码在x86上也是潜在的UB:不能保证cpu“注意”存储到将要执行的地址,直到跳转指令为止.这是the question linked above的主题(答案是:x86的真正实现超越了x86 ISA手册需要的,支持依赖于它的代码,并且因为窥探一直比高性能比冲洗更好跳跃.)
汇编语言中未定义的行为是非常罕见的,特别是如果您不计算特定值未指定的情况,但“损害”的范围是可预测和有限的.