假设我有以下的C代码:
int32_t foo(int32_t x) {
return x + 1;
}
当 x == INT_MAX
时,这是未定义的行为。现在假设我使用内联汇编执行加法:
int32_t foo(int32_t x) {
asm("incl %0" : "+g"(x));
return x;
}
问题:当x == INT_MAX
时,内联汇编版本是否仍会调用未定义的行为?还是未定义的行为仅适用于C代码?
假设我有以下的C代码:
int32_t foo(int32_t x) {
return x + 1;
}
当 x == INT_MAX
时,这是未定义的行为。现在假设我使用内联汇编执行加法:
int32_t foo(int32_t x) {
asm("incl %0" : "+g"(x));
return x;
}
问题:当x == INT_MAX
时,内联汇编版本是否仍会调用未定义的行为?还是未定义的行为仅适用于C代码?
No, 这个没有未定义行为。C语言规则不适用于汇编指令本身。至于inline-asm语法包装的指令,那是一个良好定义的语言扩展,在支持它的实现上有定义的行为。
更一般化的版本可参考Does undefined behavior apply to asm code?, 那里的回答聚焦于C端,并引用了C和C++标准中记录的关于实现定义扩展语言的内容。
同时也可以看看此comp.lang.c线程,其中讨论了是否可以说在“一般情况下”具有UB,因为并非所有实现都具有该扩展。
顺便说一句,如果您只想在GNU C中得到定义的有符号带环绕行为,则使用-fwrapv
编译即可,无需使用内联汇编。(或者使用__attribute__
仅为需要它的函数启用该选项。)wrapv
与-fno-strict-overflow
并不完全相同,后者仅禁用基于假设程序没有任何未定义行为的优化;例如,在编译时常量计算中的溢出,只有使用-fwrapv
才是安全的。
内联汇编行为是实现定义的,而GNU C 内联汇编被定义为编译器的黑盒。输入进去,输出出来,编译器不知道如何处理。 它只知道您使用out / in / clobber约束告诉它的内容。
您的使用内联汇编的foo
与以下代码行为相同:
int32_t foo(int32_t x) {
uint32_t u = x;
return ++u;
}
在x86上,因为x86是一个二进制补码机器,所以整数环绕(integer wraparound)是有明确定义的。(除了性能:汇编版本会打败常量传播,并且也不给编译器优化 x - inc(x)
为 -1 的能力等等。 请勿使用内联汇编(https://gcc.gnu.org/wiki/DontUseInlineAsm),除非通过调整C来激发编译器生成最佳汇编代码的唯一方法。)
它不会引发异常。设置OF标志对任何事情都没有影响,因为x86(i386和amd64)的GNU C内联汇编具有隐式的"cc"
破坏,因此编译器将假定每个内联汇编语句后 EFLAGS 中的条件代码保留垃圾值。GCC6引入了一种新的语法,用于生成标志结果,可以在asm中省略SETCC和TEST,这可以为希望返回标志条件的asm块节省代码。
一些体系结构在整数溢出时会引发异常(陷阱),但x86并不是其中之一(除非除法商不适合目标寄存器)。在MIPS上,如果想要带符号整数能够环绕而不会陷入异常,则可以使用ADDIU代替ADDI(因为它也是一个二进制补码ISA,因此有符号环绕与无符号环绕在二进制中相同)。
BSF和 BSR(查找第一个设置的位(向前或向后))如果输入为零,则将其目标寄存器保留为未定义内容。(TZCNT和LZCNT没有这个问题)。 英特尔最近的x86 CPU确实定义了这种行为,即保留目标不变,但x86手册并不保证。请参见此答案中关于TZCNT的更多讨论,例如TZCNT / LZCNT / POPCNT在Intel CPU中对输出具有虚假依赖性的含义。
其他一些指令在某些/所有情况下会使某些标志未定义。(尤其是AF / PF)。例如,IMUL将ZF、PF和AF保留为未定义。
假设任何一个CPU都有一致的行为,但重点是其他CPU可能会表现不同,即使它们仍然是x86。如果您是Microsoft,Intel将设计他们未来的CPU以不破坏您现有的代码。如果您的代码受到广泛依赖,最好只依赖于手册中记录的行为,而不仅仅是您的CPU碰巧做什么。请参见Andy Glew在此处的回答和评论。Andy是英特尔P6微架构的其中一名架构师。
这些示例与C中的UB不是相同的东西。我们更像是在谈论一个未指定的值,而不是鼻涕恶魔的可能性。 (或者更可信的是修改其他寄存器或跳转到某个地方)。
对于真正的未定义行为,您可能需要查看特权指令,或至少是多线程代码。自修改代码在x86上也有潜在的UB问题:CPU不能保证“注意”将要执行的地址的存储,直到跳转指令之后才能发现。这是上面链接的问题(答案是:x86的实际实现超越了x86 ISA手册所要求的范围,以支持依赖于它的代码,并且因为始终进行监视比在跳转时刷新更有利于高性能。)
汇编语言中的未定义行为非常罕见,尤其是如果您不计算特定值未指定但“损坏”的范围是可预测且有限的情况。
cc
clobber(说实话,我忘记加了 :-p);如果有人感兴趣,可以在 GCC源代码 的 ix86_md_asm_adjust
函数末尾找到它。 - Andrew Sun"cc"
破坏的问题:请参见https://dev59.com/j3zaa4cB1Zd3GeqPLRRw。此外,去年我在x86-64.org的邮件列表档案中查找时发现了一个线程,他们正在决定amd64 inline-asm是否会有相同的隐式"cc"
破坏,并且他们决定是的,因为大多数汇编语句都需要它,错过优化的成本是微不足道的。而且,如果没有它,会有很大的缺陷。这是在gcc6标志输出出现15年之前的事情。:P - Peter Cordesasm("incl %0" : "+g"(x));
是绝对的未定义行为。C语言没有将asm
指定为某个标识符。由于遗漏,它是未定义的行为。标识符指的是所有符合规范的编译器以某种实现方式处理的代码。符合规范的编译器不需要处理asm
。 - chux - Reinstate Monicaasm
是一个关键字,并且有符号溢出(使用C运算符)是UB(没有-fwrapv
),就像在ISO C11中一样。如果OP使用了__asm__
,你会更开心吗?GNU C编译器即使使用-std=c11
(而不是-std=gnu11
)也会接受它。 - Peter Cordesasm
在C
中是未定义行为。当然,在GNU C或其他方言中可能没问题。__asm__
也没有在C规范中指定。只因为一种编译器很好地指定了代码,并不意味着它对所有符合C标准的编译器都可以。 - chux - Reinstate Monica
asm
关键字是编译器的扩展 (§J.5.10)。因此,使用内联汇编所做的任何事情都是实现定义的。 - user3386109for (int i...)
,所以除了 2 的补码之外还有其他原因。 - Peter Cordeslong long x
,x+1>y
可以改为x>=y
,因为在溢出情况下,表达式x+1
将被允许像持有一个大于 long long 的最大值一样的值一样行事。我看到的基于溢出是 UB 的所有有用优化仍然可用于该模型,但是... - supercat