txt[0] = 0x4142; 是对 char 对象的赋值,因此右侧在被评估后被隐式转换为 (char)。
等效的 NASM 是 mov byte [rsp-4], 'BA'。用 NASM 组装它会给你和你的 C 编译器一样的警告:
foo.asm:1: warning: byte data exceeds bounds [-w+number-overflow]
此外,现代 C 不是高级汇编程序。 C 有类型,NASM 没有(操作数大小仅基于每条指令)。不要期望 C 像 NASM 一样工作。
C 被定义为“抽象机器”,编译器的工作是为目标 CPU 生成 asm,它产生相同的可观察结果 就好像 C 直接在 C 抽象机器上运行。除非您使用volatile,否则实际存储到内存中不算作可观察到的副作用。这就是 C 编译器可以将变量保存在寄存器中的原因。
更重要的是,在为 x86 编译时,根据 ISO C 标准未定义的行为可能仍然是未定义的。例如,x86 asm 对有符号溢出有明确定义的行为:它环绕。但在 C 中,这是未定义的行为,因此编译器可以利用这一点为for (int i=0 ; i<=len ;i++) arr[i] *= 2; 生成更高效的代码,而不必担心i<=len 可能始终为真,从而产生无限循环。请参阅 What Every C Programmer Should Know About Undefined Behavior。
除char* 或unsigned char*(或__m128i* 和其他英特尔SSE/AVX 内在类型,因为它们也被定义为may_alias 类型)之外的指针强制类型转换违反了严格-别名规则。 txt 是一个字符数组,但我认为通过uint16_t* 写入它然后通过txt[0] 和txt[1] 读取它仍然是一个严格的别名违规。
某些编译器可能会定义 *(uint16_t*)txt = 0x4142 的行为,或者在某些情况下发生以生成您期望的代码,但您不应该指望它始终有效且安全,其他代码也会读取并写txt[]。
允许编译器(即 C 实现,使用 ISO 标准的术语)定义 C 标准未定义的行为。但是为了追求更高的性能,他们选择留下很多未定义的东西。 这就是为什么为 x86 编译 C不类似于直接用 asm 编写。
许多人认为现代 C 编译器对程序员怀有敌意,寻找“错误编译”代码的借口。请参阅gcc, strict-aliasing, and horror stories 上此答案的第二半部分以及 cmets。 (该答案中的示例使用正确的memcpy 是安全的;问题是a custom implementation of memcpy that copied using long*。)
Here's a real-life example of a misaligned pointer leading to a fault on x86(因为 gcc 的自动矢量化策略假设某些整数会达到 16 字节对齐边界。即它取决于 uint16_t* 是否对齐。) p>
显然,如果您希望您的 C 具有可移植性(包括到非 x86),则必须使用明确定义的方式来进行类型双关。在 ISO C99 及更高版本中,写入一个联合成员并读取另一个是明确定义的。 (在 GNU C++ 和 GNU C89 中)。
在 ISO C++ 中,唯一明确定义的类型双关方法是使用 memcpy 或其他 char* 访问来复制对象表示。
现代编译器知道如何针对较小的编译时常量大小优化 memcpy。
#include <string.h>
#include <stdint.h>
void set2bytes_safe(char *p) {
uint16_t src = 0x4142;
memcpy(p, &src, sizeof(src));
}
void set2bytes_alias(char *p) {
*(uint16_t*)p = 0x4142;
}
两个函数 compile to the same code 与 gcc、clang 和 ICC 用于 x86-64 System V ABI:
# clang++6.0 -O3 -march=sandybridge
set2bytes_safe(char*):
mov word ptr [rdi], 16706
ret
Sandybridge 系列没有针对 16 位 mov 立即数的 LCP 解码停顿,仅针对带有 ALU 指令的 16 位立即数。这是对 Nehalem 的改进(参见 Agner Fog's microarch guide),但显然 gcc8.1 -march=sandybridge 并不知道,因为它仍然喜欢:
# gcc and ICC
mov eax, 16706
mov WORD PTR [rdi], ax
ret
将数组定义为单个变量。
...并使用((uint8_t*) &txt)[0]访问元素
是的,这很好,假设uint8_t 是unsigned char,因为char* 可以给任何东西起别名。
几乎所有支持 uint8_t 的实现都是这种情况,但理论上可以在不支持的地方构建一个,char 是 16 位或 32 位类型,uint8_t 是用包含单词的更昂贵的读取/修改/写入。