C

时间:2015-07-08 04:19:19

标签: c

我在移植一些代码时遇到了一些意想不到的行为。我把它归结为这个例子:

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

uint32_t swap_16_p(uint8_t *value)
{
    return (*(uint16_t*)value << 8 | *(uint16_t*)value >> 8);
}

int main()
{
    uint8_t start[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xBE, 0xEF };
    printf("0x%08x\n", swap_16_p(start));
    return 0;
}

在像x86-64这样的Little Endian系统上,我希望这会打印0x0000dead,而是打印0x00addead。查看汇编输出使问题更加清晰:

uint32_t swap_16_p(uint8_t *value)
{
  400506:       55                      push   %rbp
  400507:       48 89 e5                mov    %rsp,%rbp
  40050a:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
    return (*(uint16_t*)value << 8 | *(uint16_t*)value >> 8);
  40050e:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  400512:       0f b7 00                movzwl (%rax),%eax
  400515:       0f b7 c0                movzwl %ax,%eax
  400518:       c1 e0 08                shl    $0x8,%eax
  40051b:       89 c2                   mov    %eax,%edx
  40051d:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  400521:       0f b7 00                movzwl (%rax),%eax
  400524:       66 c1 e8 08             shr    $0x8,%ax
  400528:       0f b7 c0                movzwl %ax,%eax
  40052b:       09 d0                   or     %edx,%eax
}
  40052d:       5d                      pop    %rbp
  40052e:       c3                      retq   

通过使用eax作为执行计算的临时区域,额外字节通过shl $0x8,%eax移过16位边界。我不希望计算被视为32位值,直到返回之前(因为它需要将它提升为uint32_t);将值存储在临时uint32_t中并打印时,会看到类似的行为。

我是否反对(或不正确地解释)C规范,或者这是一个编译器错误(似乎不太可能,因为这发生在clang和GCC中)?

4 个答案:

答案 0 :(得分:2)

整数提升在&#34;读取端&#34;完成,因此在评估表达式时。这意味着在读取大小小于int的整数值之后。 unsigned立即转换:

  

如果可以使用int或unsigned int,则可以在表达式中使用以下内容:

     

- 具有整数类型的对象或表达式,其整数转换等级小于或等于int和unsigned int的等级。

     

- _Bool,int,signed int或unsigned int。

类型的位字段      

如果int可以表示原始类型的所有值,则该值将转换为int;否则,它将转换为unsigned int。这些被称为整数促销。 48)

     

48)仅应用整数提升:作为通常算术转换的一部分,某些参数表达式,一元+, - 和〜运算符的操作数,以及移位运算符的两个操作数,如指定的那样各自的分条款。

ISO / IEC 9899:TC3 6.3.1.1-2

因此

*(uint16_t*)value

立即转换为int,然后转移。

答案 1 :(得分:2)

在小端系统上,您正在读取包含值unit16_t的{​​{1}}内存位置。在执行轮班之前,该值将提升为0xADDE类型,在您的平台上可能为32位宽,生成int。移位分别产生0x0000ADDE0x00ADDE00。按位OR生成0x000000AD

一切都如预期。

C语言不会在小于0x00ADDEAD(或int)的类型中执行任何算术运算。在执行操作之前,任何较小的类型始终会提升为unsigned int(或int)。这就是你的轮班所发生的事情。您的班次为unsigned int班次。 C没有“更窄”的转变。 C没有“更窄”的加法和乘法。 C没有“更窄”任何

如果你想要一个“更窄”的移位(或任何其他操作),你必须通过精心手动截断中间结果来模拟它,以便强制它们变成更小的类型

int

他们会不断回到(uint16_t) (*(uint16_t*) value << 8) | (uint16_t) (*(uint16_t*) value >> 8); ,你必须不断地将他们击回int

答案 2 :(得分:1)

这就是编译器的作用:

uint32_t swap_16_p(uint8_t *value)
{
    uint16_t v1 = *(uint16_t*)value;  // -> 0x0000ADDE
    int v2 = v1 << 8;                 // -> 0x00ADDE00
    int v3 = v1 >> 8;                 // -> 0x000000AD
    uint32_t v4 = v2 | v3;            // -> 0x00ADDEAD
    return v4;
}

所以结果是合理的。

请注意,v2v3整体促销的结果。

答案 3 :(得分:0)

让我们看看你的逻辑:

return (*(uint16_t*)value << 8 | *(uint16_t*)value >> 8);

*(uint16_t*)value0xADDE,因为您的系统是小端的。 (根据我将在下面提到的一些警告)。

0xADDE << 80xADDE00,假设您有32位(或更大)int。请记住,左移相当于乘以2的幂。

0xADDE >> 80xAD

0xADDE00 | 0xAD0xADDEAD,这是你观察到的。

如果您期望0xDEAD,那么您完全是错误的方式。相反,以下代码将起作用(并且与endian无关):

return (value[0] << 8) | value[1];

虽然我个人偏好,因为我们正在做算术,但是把它写成value[0] * 0x100u + value[1]

*(uint16_t *)value还有其他问题。首先,如果您的系统对整数有一个对齐限制,它将导致未定义的行为。其次,它违反了严格的别名规则:类型uint8_t的对象可能无法通过类型uint16_t的左值读取,再次导致未定义的行为。

如果要移植使用这样的别名类型的代码,我建议在编译器中禁用基于类型的别名优化,直到您完全理解这些问题为止。在gcc中,标志为-fno-strict-aliasing