我在移植一些代码时遇到了一些意想不到的行为。我把它归结为这个例子:
#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中)?
答案 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
。移位分别产生0x0000ADDE
和0x00ADDE00
。按位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;
}
所以结果是合理的。
请注意,v2
和v3
是整体促销的结果。
答案 3 :(得分:0)
让我们看看你的逻辑:
return (*(uint16_t*)value << 8 | *(uint16_t*)value >> 8);
*(uint16_t*)value
是0xADDE
,因为您的系统是小端的。 (根据我将在下面提到的一些警告)。
0xADDE << 8
为0xADDE00
,假设您有32位(或更大)int
。请记住,左移相当于乘以2的幂。
0xADDE >> 8
是0xAD
。
0xADDE00 | 0xAD
是0xADDEAD
,这是你观察到的。
如果您期望0xDEAD
,那么您完全是错误的方式。相反,以下代码将起作用(并且与endian无关):
return (value[0] << 8) | value[1];
虽然我个人偏好,因为我们正在做算术,但是把它写成value[0] * 0x100u + value[1]
。
*(uint16_t *)value
还有其他问题。首先,如果您的系统对整数有一个对齐限制,它将导致未定义的行为。其次,它违反了严格的别名规则:类型uint8_t
的对象可能无法通过类型uint16_t
的左值读取,再次导致未定义的行为。
如果要移植使用这样的别名类型的代码,我建议在编译器中禁用基于类型的别名优化,直到您完全理解这些问题为止。在gcc中,标志为-fno-strict-aliasing
。