为什么/如何在此签名溢出测试中gcc编译未定义的行为,以便它可以在x86上运行但不能在ARM64上运行? [英] Why/how does gcc compile the undefined behaviour in this signed-overflow test so it works on x86 but not ARM64?

查看:286
本文介绍了为什么/如何在此签名溢出测试中gcc编译未定义的行为,以便它可以在x86上运行但不能在ARM64上运行?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在研究CSAPP时,在断言测试过程中遇到一个奇怪的问题时得到了一个奇怪的结果.

I was self-studying CSAPP and got a strange result when I ran into a strange issue during the run of a assertion test.

我不确定从什么开始这个问题,所以让我先获取代码(文件名在注释中可见):

I'm not sure what to start this question with, so let me get the code first (file name visible in comments):

// File: 2.30.c
// Author: iBug

int tadd_ok(int x, int y) {
    if ((x ^ y) >> 31)
        return 1;  // A positive number and a negative integer always add without problem
    if (x < 0)
        return (x + y) < y;
    if (x > 0)
        return (x + y) > y;
    // x == 0
    return 1;
}

// File: 2.30-test.c
// Author: iBug

#include <assert.h>

int tadd_ok(int x, int y);

int main() {
    assert(sizeof(int) == 4);

    assert(tadd_ok(0x7FFFFFFF, 0x80000000) == 1);
    assert(tadd_ok(0x7FFFFFFF, 0x7FFFFFFF) == 0);
    assert(tadd_ok(0x80000000, 0x80000000) == 0);
    return 0;
}

和命令:

gcc -o test -O0 -g3 -Wall -std=c11 2.30.c 2.30-test.c
./test

(附带说明:命令行中没有任何-O选项,但是由于它默认为级别0,因此显式添加-O0不会有太大变化.)

(Side note: There wasn't any -O option present in the command line, but as it defaults to level 0, explicitly adding -O0 shouldn't change much.)

以上两个命令在我的Ubuntu VM(amd64,GCC 7.3.0)上运行得很好,但是其中一个断言在我的 Android手机(AArch64或armv8-a,GCC 8.2.0)上失败了.

The above two commands ran very well on my Ubuntu VM (amd64, GCC 7.3.0), but one of the assertions failed on my Android phone (AArch64 or armv8-a, GCC 8.2.0).

2.30-test.c:13: main: assertion "tadd_ok(0x7FFFFFFF, 0x7FFFFFFF) == 0" failed

请注意,第一个断言已通过,因此确保int在平台上为4个字节.

Note that the first assertion passed, so int is guaranteed to be 4 bytes on the platforms.

因此,我在手机上启动了gdb,试图获得一些见解:

So I fired up gdb on my phone trying to get some insights:

(gdb) l 2.30.c:1
1       // File: 2.30.c
2       // Author: iBug
3
4       int tadd_ok(int x, int y) {
5           if ((x ^ y) >> 31)
6               return 1;  // A positive number and a negative integer always add without problem
7           if (x < 0)
8               return (x + y) < y;
9           if (x > 0)
10              return (x + y) > y;
(gdb) b 2.30.c:10
Breakpoint 1 at 0x728: file 2.30.c, line 10.
(gdb) r
Starting program: /data/data/com.termux/files/home/CSAPP-2019/ch2/test
warning: Unable to determine the number of hardware watchpoints available.
warning: Unable to determine the number of hardware breakpoints available.

Breakpoint 1, tadd_ok (x=2147483647, y=2147483647)
    at 2.30.c:10
10              return (x + y) > y;
(gdb) p x
$1 = 2147483647
(gdb) p y
$2 = 2147483647
(gdb) p (x + y) > y
$3 = 0
(gdb) c
Continuing.
2.30-test.c:13: main: assertion "tadd_ok(0x7FFFFFFF, 0x7FFFFFFF) == 0" failed

Program received signal SIGABRT, Aborted.
0x0000007fb7ca5928 in abort ()
   from /system/lib64/libc.so
(gdb) d 1
(gdb) p tadd_ok(0x7FFFFFFF, 0x7FFFFFFF)
$4 = 1
(gdb)

正如您在GDB输出中所看到的,结果非常不一致,因为已到达2.30.c:10上的return语句,并且返回值应该为0,但是该函数仍返回1,从而使断言失败

As you see in the GDB output, the result is very inconsistent, as the return statement on 2.30.c:10 was reached, and the return value should have been 0, but the function still returns 1, making the assertion fail.

请提供一个想法,我在这里出错了.

Kindly provide an idea what I'm getting wrong here.

请尊重我介绍的内容.只是说不涉及平台,特别是GDB输出的UB,将无济于事.

Please respect what I have presented. Just saying it's UB without relating the platforms, especially GDB output, is not going to be any helpful.

推荐答案

签名溢出是ISO C中的未定义行为.您不能可靠地导致它,然后然后检查它是否发生.

Signed overflow is Undefined Behaviour in ISO C. You can't reliably cause it and then check if it happened.

在表达式(x + y) > y;中,允许编译器假定x+y没有溢出(因为它将是UB).因此,它会优化到检查x > 0.(是的,即使在-O0,gcc也会这样做).

In the expression (x + y) > y;, the compiler is allowed to assume that x+y doesn't overflow (because that would be UB). Therefore, it optimizes down to checking x > 0. (Yes, really, gcc does this even at -O0).

此优化是gcc8中的新增功能.在x86和AArch64上相同;您必须在AArch64和x86上使用了不同的GCC版本. (甚至在-O3,gcc7.x和更早的版本(故意?))都错过了此优化.clang7.0也没有做到这一点.他们实际上进行了32位加法和比较.他们也错过了将tadd_ok优化为return 1add并检查溢出标志(在ARM上为V,在x86上为OF)Clang优化的asm是>>31,OR和一个XOR操作的有趣组合,但实际上-fwrapv更改了该asm,因此它可能没有进行完整的溢出检查.)

This optimization is new in gcc8. It's the same on x86 and AArch64; you must have used different GCC versions on AArch64 and x86. (Even at -O3, gcc7.x and earlier (intentionally?) miss this optimization. clang7.0 doesn't do it either. They actually do a 32-bit add and compare. They also miss optimizing tadd_ok to return 1, or to add and checking the overflow flag (V on ARM, OF on x86). Clang's optimized asm is an interesting mix of >>31, OR and one XOR operation, but -fwrapv actually changes that asm so it's probably not doing a full overflow check.)

您可以说gcc8破坏了"您的代码,但实际上它已经被合法/可移植的ISO C破坏了.gcc8刚刚揭示了这一事实.

You could say that gcc8 "breaks" your code, but really it was already broken as far as being legal / portable ISO C. gcc8 just revealed that fact.

为了更清楚地看到它,让我们仅将该表达式隔离为一个函数. gcc -O0无论如何都将分别编译每个语句,因此该信息仅在x<0不会影响tadd_ok函数中该语句的-O0代码源时才运行.

To see it more clearly, lets isolate just that expression into one function. gcc -O0 compiles each statement separately anyway, so the information that this only runs when x<0 doesn't affect the -O0 code-gen for this statement in your tadd_ok function.

// compiles to add and checking the carry flag, or equivalent
int unsigned_overflow_test(unsigned x, unsigned y) {
    return (x+y) >= y;    // unsigned overflow is well-defined as wrapping.
}

// doesn't work because of UB.
int signed_overflow_expression(int x, int y) {
    return (x+y) > y;
}

GCC -ftree-dump-original-optimized甚至通过以下优化操作(从Godbolt链接)将其GIMPLE转换回类似C的代码:

GCC -ftree-dump-original or -optimized will even turn its GIMPLE back into C-like code with this optimization done (from the Godbolt link):

;; Function signed_overflow_expression (null)
;; enabled by -tree-original

{
  return x > 0;
}

不幸的是,即使使用-Wall -Wextra -Wpedantic,也没有关于比较的警告.这不是琐事真实的;仍然取决于x.

Unfortunately, even with -Wall -Wextra -Wpedantic, there's no warning about a the comparison. It's not trivially true; it still depends on x.

毫无疑问,优化后的asm是cmp w0, 0/cset w0, gt/ret. 0xff的AND是多余的. cset是以下内容的别名csinc ,同时使用零寄存器作为两个源.因此它将产生0/1.对于其他寄存器,csinc的一般情况是有条件选择和任意2个寄存器的递增.

The optimized asm is unsurprisingly cmp w0, 0 / cset w0, gt / ret. The AND with 0xff is redundant. cset is an alias of csinc, using the zero-register as both sources. So it will produce 0 / 1. With other registers, the general case of csinc is a conditional select and increment of any 2 registers.

无论如何,cset等同于x86 setcc的AArch64,用于将标志条件转换为寄存器中的bool.

Anyway, cset is AArch64's equivalent of x86 setcc, for turning a flag condition into a bool in a register.

如果您希望代码按书面形式工作,则需要-fwrapv 进行编译,以使其在-fwrapv使GCC实现的C变体中具有明确定义的行为.默认值为-fstrict-overflow,类似于ISO C标准.

If you want your code to work as written, you'd need to compile with -fwrapv to make it well-defined behaviour in the variant of C that -fwrapv makes GCC implement. The default is -fstrict-overflow, like the ISO C standard.

如果要在现代C语言中检查带符号的溢出,则需要编写检测而实际上没有引起溢出的检查. 编译器作者和(某些)开发人员之间的争论点.他们认为,围绕未定义行为的语言规则并不是要作为在为目标机器编译在asm中有意义的目标机器时无偿破坏"代码的借口.但是,即使针对x86和ARM之类的目标体系结构(其中带符号的整数都没有填充(因此可以很好地包装))并且不会在溢出时陷入陷阱,现代的编译器大多仅实现ISO C(具有一些扩展和额外定义的行为).

If you want to check for signed overflow in modern C, you need to write checks that detect overflow without actually causing it. This is harder, annoying, and a point of contention between compiler writers and (some) developers. They argue that the language rules around undefined behaviour weren't meant to be used as an excuse to "gratuitously break" code when compiling for target machines where it would make sense in asm. But modern compilers mostly only implement ISO C (with some extensions and extra defined behaviour), even when compiling for target architectures like x86 and ARM where signed integers have no padding (and thus wrap just fine), and don't trap on overflow.

因此您可以在那场战争中说开枪",将gcc8.x更改为实际上是破坏"这样的不安全代码. :P

So you could say "shots fired" in that war, with the change in gcc8.x to actually "breaking" unsafe code like this. :P

请参见在C/C ++中检测签名溢出 如何在没有不确定行为的情况下在C中检查带符号整数溢出?

由于有符号和无符号加法是2的补码中相同的二进制运算,因此您可以仅将其强制转换为unsigned进行添加,然后将其强制转换为有符号比较.那将使您的函数版本在常规"实现中安全:2的补码,并且unsignedint之间的强制转换只是对相同位的重新解释.

Since signed and unsigned addition are the same binary operation in 2's complement, you could maybe just cast to unsigned for the add, and cast back for a signed compare. That would make a version of your function that's safe on "normal" implementations: 2's complement, and casting between unsigned and int is just a reinterpret of the same bits.

这不能有UB,只是不能给人的补码或符号/幅值C实现提供正确的答案.

This can't have UB, it just won't give the right answer on one's complement or sign/magnitude C implementations.

return  (int)((unsigned)x + (unsigned)y) > y;

这会编译(对于AArch64,使用gcc8.2 -O3)

This compiles (with gcc8.2 -O3 for AArch64) to

    add     w0, w0, w1            // x+y
    cmp     w0, w1                // x+y  cmp  y
    cset    w0, gt
    ret


如果您将int sum = x+y作为与return sum < y分开的C语句编写,则禁用优化功能的gcc将看不到该UB.但是,作为同一表达式的一部分,即使<默认值为-O0的c47>可以看到它.


If you had written int sum = x+y as a separate C statement from return sum < y, this UB wouldn't be visible to gcc with optimization disabled. But as part of the same expression, even gcc with the default -O0 can see it.

对编译时可见的UB有很多坏处.在这种情况下,只有特定范围的输入会产生UB,因此编译器假定它不会发生.如果在执行路径上看到无条件的UB,则优化的编译器可以假定该路径永远不会发生. (在没有分支的函数中,它可能会假设该函数从未被调用过,并将其编译为一条非法指令.)请参见

Compile-time-visible UB is all kinds of bad. In this case, only certain ranges of inputs would produce UB, so the compiler assumes it doesn't happen. If unconditional UB is seen on a path of execution, an optimizing compiler can assume that path never happens. (In a function with no branching, it could assume the function is never called, and compile it to a single illegal instruction.) See Does the C++ standard allow for an uninitialized bool to crash a program? for more about compile-time-visible UB.

(-O0并不意味着没有优化",它意味着没有额外的优化,除了在任何目标平台上通过gcc的内部表示形式转换为asm的方式已经必须进行的优化之外. Starynkevitch解释说 在GCC中禁用所有优化选项)

(-O0 doesn't mean "no optimization", it means no extra optimization besides what's already necessary to transform through gcc's internal representations on the way to asm for whatever target platform. @Basile Starynkevitch explains in Disable all optimization options in GCC)

在禁用优化的情况下,其他一些编译器可能会更多地绞尽脑汁",并做一些更接近将C音译为asm的事情,但是gcc并不是 那样.例如,gcc仍将乘除逆用于整数除以-O0的常数. (为什么GCC使用乘法)所有其他3种主要的x86编译器(clang/ICC/MSVC)都使用div.

Some other compilers may "turn their brains off" even more with optimization disabled, and do something closer to transliterating C into asm, but gcc is not like that. For example, gcc still uses a multiplicative inverse for integer division by a constant at -O0. (Why does GCC use multiplication by a strange number in implementing integer division?) All 3 other major x86 compilers (clang/ICC/MSVC) use div.

这篇关于为什么/如何在此签名溢出测试中gcc编译未定义的行为,以便它可以在x86上运行但不能在ARM64上运行?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆