【问题标题】:Why does clang produces wrong results for my c code compiled with -O1 but not with -O0?为什么clang会为我用-O1而不是-O0编译的c代码产生错误的结果?
【发布时间】:2017-12-21 23:56:27
【问题描述】:

对于input 0xffffffff,以下c 代码在没有优化的情况下工作正常,但在使用-O1 编译时会产生错误结果。其他编译选项是 -g -m32 -Wall。该代码在 macOS 10.13.2 中使用 clang-900.0.39.2 进行了测试。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc < 2) return 1;

    char *endp;
    int x = (int)strtoll(argv[1], &endp, 0);

    int mask1 = 0x55555555;
    int mask2 = 0x33333333;
    int count = (x & mask1) + ((x >> 1) & mask1);

    int v1 = count >> 2;
    printf("v1 = %#010x\n", v1);

    int v2 = v1 & mask2;
    printf("v2 = %#010x\n", v2);

    return 0;
}

输入:0xffffffff

-O0 的输出:(预期)

v1 = 0xeaaaaaaa

v2 = 0x22222222

-O1 的输出:(错误)

v1 = 0x2aaaaaaa

v2 = 0x02222222

下面是“int v1 = count >> 2;”行的反汇编指令使用 -O0 和 -O1。

使用 -O0:

sarl $0x2, %esi

使用-O1:

shrl $0x2, %esi

以下是“int v2 = v1 & mask2;”行的反汇编指令使用 -O0 和 -O1。

使用 -O0:

andl -0x24(%ebp), %esi //-0x24(%ebp) 存储 0x33333333

使用-O1:

andl $0x13333333, %esi //优化为什么会从0x33333333变成0x13333333?

此外,如果 x 在本地设置为 0xffffffff 而不是从参数中获取其值,则即使使用 -O1,代码也会按预期工作。

P.S:代码是基于我在 CS:APP 课程@CMU 中对数据实验室的解决方案的实验性代码。实验室要求学生实现一个函数,该函数计算 int 变量的 1 位的个数,不使用除 int 以外的任何类型

【问题讨论】:

  • 0xffffffff is > MAX_INT 在您的情况下,int 的溢出是未定义的行为。
  • @Stargateur 你确定吗?这不应该是-1吗?
  • 也就是说,出于多种原因,我建议您使用unsigned int,位运算符只能与unsigned integerprintf("v1 = %#010x\n", v1); 一起使用=> %x 期望unsigned int 所以它是未定义的发送int 的行为。
  • 但我认为问题在于右移负符号类型是实现定义的(显然 clang 和 gcc 处理方式不同)。将位操作限制为无符号类型可能最安全:stackoverflow.com/questions/4009885/…
  • @Stargateur:将过大的整数值转换为有符号整数类型不是未定义的行为。它是实现定义的行为。有符号溢出在计算算术运算符期间发生时会导致未定义的行为(这是您的链接所谈论的内容)。 转换到有符号整数类型期间的溢出是实现定义的 (port70.net/~nsz/c/c11/n1570.html#6.3.1.3p3)。只有浮点值在转换为整数类型时会导致 UB。

标签: c clang


【解决方案1】:

正如几位评论者所指出的,右移有符号值定义不明确

我把x的声明和初始化改成

unsigned int x = (unsigned int)strtoll(argv[1], &endp, 0);

并在 -O0 和 -O1 下得到一致的结果。 (但在进行更改之前,我能够在 MacOS 下的 clang 下重现您的结果。)

【讨论】:

  • 我可以确认将 x 的类型更改为无符号消除了不一致。但是我还是不明白为什么优化将mask2改为0x13333333。
  • @ZackZhu 我也不太明白,尽管它可能只是在左边缘移入 0 或移入 1 之间的区别。但是,对我来说,这甚至不是一个有趣的问题。尽管我们在这里讨论的是实现定义的行为,而不是未定义的行为,但 this other answer 的大部分内容都适用。
【解决方案2】:

正如您所发现的,您提出 实现定义的行为 尝试将 0xffffffff (4294967295) 存储在 int x 中(其中 INT_MAX7fffffff 或 @ 987654328@)。 C11 Standard §6.3.1.3 (draft n1570) - Signed and unsigned integers 每当使用strtoll(或strtoull)(1-l 的两个版本都可以)并尝试将值存储为int 时,您必须在制作之前对照INT_MAX 检查结果有演员表的作业。 (或者如果使用精确宽度类型,则针对INT32_MAX,或UINT32_MAX 用于无符号)

此外,在这种涉及位操作的情况下,您可以使用stdint.h 中提供的确切宽度类型和@987654337 中提供的相关格式说明符 来消除不确定性并确保可移植性@。在这里,不需要使用签名的int。将所有值处理为unsigned(或uint32_t)会更有意义。

例如,以下为输入提供了一个默认值,以避免在您的代码在没有参数的情况下执行时调用 Undefined Behavior(您也可以简单地测试argc),替换使用strtollstrtoul,在分配处理错误之前验证输入是否适合关联变量,然后使用明确的确切类型,例如

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

int main (int argc, char *argv[]) {

    uint64_t tmp = argc > 1 ? strtoul (argv[1], NULL, 0) : 0xffffffff;

    if (tmp > UINT32_MAX) {
        fprintf (stderr, "input exceeds UINT32_MAX.\n");
        return 1;
    }

    uint32_t x = (uint32_t)tmp,
        mask1 = 0x55555555,
        mask2 = 0x33333333,
        count = (x & mask1) + ((x >> 1) & mask1),
        v1 = count >> 2,
        v2 = v1 & mask2;

    printf("v1 = 0x%" PRIx32 "\n", v1);

    printf("v2 = 0x%" PRIx32 "\n", v2);

    return 0;
}

使用/输出示例

$ ./bin/masktst
v1 = 0x2aaaaaaa
v2 = 0x22222222

编译

$ gcc -Wall -Wextra -pedantic -std=gnu11 -Ofast -o bin/masktst masktst.c

查看一下,如果您还有其他问题,请告诉我。

【讨论】:

  • 将过大的整数值转换为有符号整数类型不是未定义的行为。它是实现定义的行为
  • 好点,正是“已定义”措辞的细微之处让我大吃一惊。 (附件 J.2 或 J.3)实际的标准参考(附件外)是 6.3.1.3 Signed and unsigned integers 无论是未定义还是实现已定义——我尽量避免像瘟疫一样...
【解决方案3】:

此声明:

int x = (int)strtoll(argv[1], &endp, 0);

导致有符号溢出,这是未定义的行为。

(在我的系统上,结果是:-1431655766

结果值往往会从那里走下坡路:

变量:v1 接收:-357913942

变量:v2 接收:572662306

%x 格式说明符仅适用于无符号变量

【讨论】:

  • 这是超出范围的转换,不是溢出。有关超出范围的转换(不是 UB)的行为,请参见 6.3.1.3/3。
  • 或者,如果 OP 实际包含 stdlib.h ——照原样,对 strtoll 的调用是 C99 中调用未声明函数的约束违规(并且 strtoll 不存在于C89)
猜你喜欢
  • 2012-01-03
  • 1970-01-01
  • 2017-01-11
  • 1970-01-01
  • 1970-01-01
  • 2015-05-10
  • 2023-03-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多