为什么clang会为使用-O1编译的c代码产生错误的结果,但不能使用-O0?

时间:2017-12-21 23:56:28

标签: c clang

对于输入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

以下是该行&#34; int v1 = count&gt;&gt;的反汇编指令2;&#34;与-O0和-O1。

使用-O0:

  

sarl $ 0x2,%esi

使用-O1:

  

shrl $ 0x2,%esi

以下是该行的反汇编指令&#34; int v2 = v1&amp; MASK2;&#34;与-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 以外的任何类型。

3 个答案:

答案 0 :(得分:2)

正如一些评论者指出的那样,右移符号值定义不明确

我将x的声明和初始化更改为

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

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

答案 1 :(得分:2)

正如您所发现的,在尝试将0xffffffff4294967295)存储在int xINT_MAX时,您会引发实施定义的行为 }是7fffffff2147483647)。 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中提供的确切宽度类型和相关的格式说明符来消除不确定性并确保可移植性。 inttypes.h。在这里,不需要使用签名的int。将所有值处理为unsigned(或uint32_t)。

会更有意义

例如,以下提供输入的默认值,以避免在没有参数的情况下执行代码时调用的未定义行为(您也可以简单地测试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

仔细看看,如果您有其他问题,请告诉我。

答案 2 :(得分:-2)

这句话:

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

导致签名溢出,这是未定义的行为。

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

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

变量:v1收到:-357913942

变量:v2收到:572662306

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