内联 x86 程序集中是否未定义整数溢出?
Is integer overflow undefined in inline x86 assembly?
假设我有以下 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 代码?
没有,这个没有UB。 C 规则不适用于 asm 指令本身。就包装指令的内联 asm 语法而言,这是一种定义明确的语言扩展,在支持它的实现上定义了行为。
请参阅 了解此问题的更通用版本(与关于 x86 汇编和 GNU C 内联 asm 语言扩展的问题相比)。那里的答案集中在 C 方面,引用了 C 和 C++ 标准,这些标准记录了标准对语言的实现定义扩展的描述是多么少。
另请参阅 this comp.lang.c thread,了解关于说它“一般”具有 UB 是否有意义的争论,因为并非所有实现都具有该扩展名。
顺便说一句,如果您只想在 GNU C 中使用已定义的 2 的补码行为进行带符号环绕,请使用 -fwrapv
. Don't use inline asm. (Or use an __attribute__
to enable that option for just the function that needs it.) wrapv
is not quite the same thing as -fno-strict-overflow
进行编译,这只会禁用基于假设程序没有任何 UB 的优化;例如,编译时常量计算中的溢出仅在 -fwrapv
.
时是安全的
内联 asm 行为是实现定义的,GNU C inline asm is defined 作为编译器的黑盒。输入进去,输出出来,编译器不知道怎么做。它所知道的就是您使用 out/in/clobber 约束告诉它的内容。
您使用内联汇编的 foo
的行为与
相同
int32_t foo(int32_t x) {
uint32_t u = x;
return ++u;
}
在 x86 上,因为 x86 是一个 2 的补码机器,所以整数环绕是明确定义的。 (性能除外:asm 版本击败了常量传播,并且还使编译器无法将 x - inc(x)
优化为 -1,等等 https://gcc.gnu.org/wiki/DontUseInlineAsm 除非没有办法诱使编译器生成最佳asm 通过调整 C.)
它不会引发异常。设置 OF 标志对任何事情都没有影响,因为 x86(i386 和 amd64)的 GNU C 内联 asm 有一个隐式的 "cc"
破坏,因此编译器将假定 EFLAGS 中的条件代码在每个内联 asm 之后保存垃圾陈述。 gcc6 为 asm 引入了一种新语法来产生标志结果(它可以在你的 asm 中保存一个 SETCC 和编译器为想要 return 标志条件的 asm 块生成的 TEST)。
有些架构确实会在整数溢出时引发异常(陷阱),但 x86 不是其中之一(except when a division quotient doesn't fit in the destination register). On MIPS, you'd use ADDIU instead of ADDI 在有符号整数上,如果您希望它们能够在不捕获的情况下换行。(因为它也是一个2 的补码 ISA,因此有符号环绕在二进制中与无符号环绕相同。)
x86 asm 中的未定义(或至少依赖于实现)行为:
BSF and BSR (find first set bit forward or reverse) leave their destination register with undefined contents if the input was zero. (TZCNT and LZCNT don't have that problem). Intel's recent x86 CPUs do define the behaviour, which is to leave the destination unmodified, but the x86 manuals don't guarantee that. See the section on TZCNT in 以获得更多关于其影响的讨论,例如TZCNT/LZCNT/POPCNT 对 Intel CPUs.
中的输出有错误的依赖
其他一些指令在 some/all 情况下留下一些未定义的标志。 (特别是 AF/PF)。 IMUL 例如,未定义 ZF、PF 和 AF。
大概任何给定的 CPU 都具有一致的行为,但关键是其他 CPU 的行为可能会有所不同,即使它们仍然是 x86。如果您是 Microsoft,Intel 将设计他们未来的 CPUs 以不破坏您现有的代码。如果你的代码被广泛依赖,你最好坚持只依赖手册中记录的行为,而不仅仅是你的 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 手册的要求,以支持依赖于它的代码,并且因为始终窥探更适合高性能比跳跃时冲洗。)
汇编语言中的未定义行为非常罕见,特别是如果您不计算未指定特定值但“损坏”范围是可预测和有限的情况。
好吧,C 标准没有定义内联汇编器的作用,因此根据 C 标准,任何内联汇编器都是未定义的行为。
您使用的语言略有不同"C with x86 32 bit inline assembler"。您生成了有效的汇编程序语句。该行为大概由 Intel 的参考手册定义。并且在 INT_MAX 上加 1 的整数加法的行为是明确定义的。它的定义方式不会干扰您的 C 程序的执行。
试图通过空指针读取值的内联汇编程序也将在汇编程序级别上得到很好的定义,但它的行为会干扰程序的执行(a.k.a。使其崩溃)。
假设我有以下 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 代码?
没有,这个没有UB。 C 规则不适用于 asm 指令本身。就包装指令的内联 asm 语法而言,这是一种定义明确的语言扩展,在支持它的实现上定义了行为。
请参阅
另请参阅 this comp.lang.c thread,了解关于说它“一般”具有 UB 是否有意义的争论,因为并非所有实现都具有该扩展名。
顺便说一句,如果您只想在 GNU C 中使用已定义的 2 的补码行为进行带符号环绕,请使用 -fwrapv
. Don't use inline asm. (Or use an __attribute__
to enable that option for just the function that needs it.) wrapv
is not quite the same thing as -fno-strict-overflow
进行编译,这只会禁用基于假设程序没有任何 UB 的优化;例如,编译时常量计算中的溢出仅在 -fwrapv
.
内联 asm 行为是实现定义的,GNU C inline asm is defined 作为编译器的黑盒。输入进去,输出出来,编译器不知道怎么做。它所知道的就是您使用 out/in/clobber 约束告诉它的内容。
您使用内联汇编的 foo
的行为与
int32_t foo(int32_t x) {
uint32_t u = x;
return ++u;
}
在 x86 上,因为 x86 是一个 2 的补码机器,所以整数环绕是明确定义的。 (性能除外:asm 版本击败了常量传播,并且还使编译器无法将 x - inc(x)
优化为 -1,等等 https://gcc.gnu.org/wiki/DontUseInlineAsm 除非没有办法诱使编译器生成最佳asm 通过调整 C.)
它不会引发异常。设置 OF 标志对任何事情都没有影响,因为 x86(i386 和 amd64)的 GNU C 内联 asm 有一个隐式的 "cc"
破坏,因此编译器将假定 EFLAGS 中的条件代码在每个内联 asm 之后保存垃圾陈述。 gcc6 为 asm 引入了一种新语法来产生标志结果(它可以在你的 asm 中保存一个 SETCC 和编译器为想要 return 标志条件的 asm 块生成的 TEST)。
有些架构确实会在整数溢出时引发异常(陷阱),但 x86 不是其中之一(except when a division quotient doesn't fit in the destination register). On MIPS, you'd use ADDIU instead of ADDI 在有符号整数上,如果您希望它们能够在不捕获的情况下换行。(因为它也是一个2 的补码 ISA,因此有符号环绕在二进制中与无符号环绕相同。)
x86 asm 中的未定义(或至少依赖于实现)行为:
BSF and BSR (find first set bit forward or reverse) leave their destination register with undefined contents if the input was zero. (TZCNT and LZCNT don't have that problem). Intel's recent x86 CPUs do define the behaviour, which is to leave the destination unmodified, but the x86 manuals don't guarantee that. See the section on TZCNT in
其他一些指令在 some/all 情况下留下一些未定义的标志。 (特别是 AF/PF)。 IMUL 例如,未定义 ZF、PF 和 AF。
大概任何给定的 CPU 都具有一致的行为,但关键是其他 CPU 的行为可能会有所不同,即使它们仍然是 x86。如果您是 Microsoft,Intel 将设计他们未来的 CPUs 以不破坏您现有的代码。如果你的代码被广泛依赖,你最好坚持只依赖手册中记录的行为,而不仅仅是你的 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 手册的要求,以支持依赖于它的代码,并且因为始终窥探更适合高性能比跳跃时冲洗。)
汇编语言中的未定义行为非常罕见,特别是如果您不计算未指定特定值但“损坏”范围是可预测和有限的情况。
好吧,C 标准没有定义内联汇编器的作用,因此根据 C 标准,任何内联汇编器都是未定义的行为。
您使用的语言略有不同"C with x86 32 bit inline assembler"。您生成了有效的汇编程序语句。该行为大概由 Intel 的参考手册定义。并且在 INT_MAX 上加 1 的整数加法的行为是明确定义的。它的定义方式不会干扰您的 C 程序的执行。
试图通过空指针读取值的内联汇编程序也将在汇编程序级别上得到很好的定义,但它的行为会干扰程序的执行(a.k.a。使其崩溃)。