x86内联汇编中整数溢出是否未定义?

12

假设我有以下的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代码?


它应该设置溢出标志以导致未定义的行为。如果是uint,则不会设置标志。 - Turtle
@AndrewSun 加法操作从来不会出现未定义的情况:机器会自动计算。问题在于,当发生溢出时,如果将结果赋值给有符号变量,则会出现问题。 - Weather Vane
2
asm 关键字是编译器的扩展 (§J.5.10)。因此,使用内联汇编所做的任何事情都是实现定义的。 - user3386109
@HansPassant:我认为 C 将其设置为 UB,是因为他们不想在 2 的补码上进行标准化。如果检测符号 wraparound 有用,那么检测无符号 wraparound 也同样有用,但在 C 中它是明确定义的。您是否认为仅仅是 abort-on-overflow 是导致其成为UB的主要原因,或者您认为这是 OP 想要实现的内容?无论如何,这篇LLVM博客指出了一些现代优化编译器对于符号溢出 UB 的优势,例如 for (int i...),所以除了 2 的补码之外还有其他原因。 - Peter Cordes
2
@PeterCordes:允许编译器将溢出结果作为不确定地持有算术正确值、"wrapped" 值或任何其他会包装到相同值的数学整数,是有用的。因此,对于给定的 long long xx+1>y 可以改为 x>=y,因为在溢出情况下,表达式 x+1 将被允许像持有一个大于 long long 的最大值一样的值一样行事。我看到的基于溢出是 UB 的所有有用优化仍然可用于该模型,但是... - supercat
显示剩余7条评论
2个回答

17

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,因此有符号环绕与无符号环绕在二进制中相同)。


x86汇编中未定义(或至少与实现相关)的行为:

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手册所要求的范围,以支持依赖于它的代码,并且因为始终进行监视比在跳转时刷新更有利于高性能。)

汇编语言中的未定义行为非常罕见,尤其是如果您不计算特定值未指定但“损坏”的范围是可预测且有限的情况。


1
讲解得非常清楚,谢谢!实际上我不知道隐式的 cc clobber(说实话,我忘记加了 :-p);如果有人感兴趣,可以在 GCC源代码ix86_md_asm_adjust 函数末尾找到它。 - Andrew Sun
1
@AndrewSun:关于"cc"破坏的问题:请参见https://dev59.com/j3zaa4cB1Zd3GeqPLRRw。此外,去年我在x86-64.org的邮件列表档案中查找时发现了一个线程,他们正在决定amd64 inline-asm是否会有相同的隐式"cc"破坏,并且他们决定是的,因为大多数汇编语句都需要它,错过优化的成本是微不足道的。而且,如果没有它,会有很大的缺陷。这是在gcc6标志输出出现15年之前的事情。:P - Peter Cordes
1
将寄存器加载为未指定值与未定义行为之间存在巨大差异。一些处理器(例如DEC Alpha)具有更受限制的不可预测行为类别,这比未定义行为更受限制,因为前者仍将受到诸如内存权限之类的限制,而后者可能不会。请注意,任何可能调用未定义行为的Alpha指令都将陷阱,除非从监管模式执行;用户模式指令最多产生未指定行为。 - supercat
@supercat:嗯,是的,这是一个很好的观点。在C语言中,无法通过不使用该结果来恢复UB。更新以更正最后一部分。CPU设计者可以在特权指令中留下系统范围的UB潜在问题,并将其留给内核来避免问题,而不会使阻止不受信任的用户空间导致机器崩溃变得不可能。 - Peter Cordes
1
@PeterCordes:由于一些平台可能无法高效支持某些类型的系统编程,但仍然可以有用地处理许多其他类型的C程序,因此标准的作者们并没有要求实现提供使系统编程变得实用所需的一切,但是他们认为那些能够实际提供这些功能(例如能够安全地对任意指针执行关系比较的能力)的实现会这样做。如果一个内核代码片段应该对被故意篡改的参数具有鲁棒性,... - supercat
1
在编译器基础上省略某些检查,因为某些事情“不可能”发生,这将是极其无益的。在某个层面上,安全代码需要能够看到事物的实际情况,而不仅仅是编译器认为它们应该是的样子。 - supercat

3
根据C标准,任何内联汇编都是未定义行为。您正在使用略微不同的语言"C with x86 32 bit inline assembler"。您生成了一个有效的汇编语句。该行为可能由Intel的参考手册定义。在那里,将INT_MAX加1的整数加法的行为得到良好定义。它被定义为不干扰C程序的执行方式。尝试通过空指针读取值的内联汇编在汇编级别上也可以良好定义,但它的行为会干扰程序的执行(也就是导致崩溃)。

@PeterCordes asm("incl %0" : "+g"(x)); 是绝对的未定义行为。C语言没有将asm指定为某个标识符。由于遗漏,它是未定义的行为。标识符指的是所有符合规范的编译器以某种实现方式处理的代码。符合规范的编译器不需要处理asm - chux - Reinstate Monica
1
@chux:我们正在谈论C语言的GNU方言,其中asm是一个关键字,并且有符号溢出(使用C运算符)是UB(没有-fwrapv),就像在ISO C11中一样。如果OP使用了__asm__,你会更开心吗?GNU C编译器即使使用-std=c11(而不是-std=gnu11)也会接受它。 - Peter Cordes
@PeterCordes 这不是关于我的快乐的问题。"C规则不适用于asm指令"是误导性的。C定义了C语言,而不是GNU C语言方言。asmC中是未定义行为。当然,在GNU C或其他方言中可能没问题。__asm__也没有在C规范中指定。只因为一种编译器很好地指定了代码,并不意味着它对所有符合C标准的编译器都可以。 - chux - Reinstate Monica
1
@chux:我的观点是,严格的C11要求编译器将“asm”保留为关键字,但与双下划线名称有关的任何内容都是实现定义的。这是一个单独的观点,与你所提出的观点无关。我想我知道你的意思,但这似乎是吹毛求疵。你有没有任何建议,可以在我的答案或gnasher的答案中使用简洁的措辞,避免任何你所考虑的错误陈述? - Peter Cordes
2
@PeterCordes:C89包括一个“常见扩展”列表,并且并未说明这些扩展,尽管很常见,但并不符合标准。结合扩展必须被记录的要求,我认为人们可以合理地推断出意图是符合标准的实现可以以指定的方式扩展语言,前提是他们记录了自己这样做的过程。这不是标准现在的解释方式,但与1990年代初期的常见做法一致。 - supercat
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接