在低级语言中,有可能mov
一个dword(32位)到第一个数组元素,这将溢出写入第二,第三和第四个元素,或mov
一个单词( 16位)到第一个,它将溢出到第二个元素。
如何在c中实现相同的效果?就像试图例如:
char txt[] = {0, 0};
txt[0] = 0x4142;
它会发出警告[-Woverflow]
且txt[1]
的值不会更改,txt[0]
设置为0x42
。
如何获得与汇编相同的行为:
mov word [txt], 0x4142
上一个汇编指令将第一个元素[txt+0]
设置为0x42
,将第二个元素[txt+1]
设置为0x41
。
这个建议怎么样?
将数组定义为单个变量。
uint16_t txt;
txt = 0x4142;
并访问第一个元素为((uint8_t*) &txt)[0]
的元素,第二个元素为((uint8_t*) &txt)[1]
元素。
答案 0 :(得分:8)
如果你完全确定这不会导致分段错误,你必须,你可以使用memcpy()
uint16_t n = 0x4142;
memcpy((void *)txt, (void *)&n, sizeof(uint16_t));
通过使用void pointers,这是最通用的解决方案,可以推广到此示例之外的所有情况。
答案 1 :(得分:2)
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编译器可以将变量保存在寄存器中的原因。
更重要的是,根据ISO C标准的未定义行为的东西在编译x86 时可能仍未定义。例如,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内在类型之外,通过指针转换进行类型惩罚,因为它们也被定义为{{1 }})违反严格别名规则。 may_alias
是一个char数组,但我认为仍然是一个严格别名的违规行为,通过txt
将其写入,然后通过uint16_t*
和{ {1}}。
有些编译器可能会定义txt[0]
的行为,或发生以生成您在某些情况下所期望的代码,但您不应指望它始终正常工作并且安全其他代码还可以读写txt[1]
。
允许编译器(即C实现,使用ISO标准的术语)定义C标准未定义的行为。但是为了追求更高的性能,他们选择留下许多未定义的东西。 这就是为什么为x86编译C 不类似于直接写入asm 。
许多人认为现代C编译器对程序员有积极的敌意,寻找“错误编译”代码的借口。请参阅gcc, strict-aliasing, and horror stories上此答案的下半部分以及评论。 (答案中的示例是安全的*(uint16_t*)txt = 0x4142
;问题是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字节的对齐边界。即它依赖于txt[]
对齐。)
显然,如果你希望你的C是可移植的(包括非x86),你必须使用明确定义的方式来输入 - pun。在ISO C99及更高版本中,编写一个联合成员并阅读另一个联合成员是明确定义的。 (在GNU C ++和GNU C89中)。
在ISO C ++中,唯一明确定义的输入方式是使用memcpy
或其他uint16_t*
次访问来复制对象表示。
现代编译器知道如何针对小编译时常量大小优化memcpy
。
char*
x86-64 System V ABI的gcc,clang和ICC两个函数compile to the same code:
memcpy
Sandybridge-family没有16位#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;
}
立即的LCP解码停顿,仅适用于具有ALU指令的16位立即数。这是对Nehalem的改进(见Agner Fog's microarch guide),但显然# clang++6.0 -O3 -march=sandybridge
set2bytes_safe(char*):
mov word ptr [rdi], 16706
ret
不知道它,因为它仍然喜欢:
mov
将数组定义为单个变量。
...并使用
访问元素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
是16或32位类型,{{{ 1}}通过更昂贵的包含单词的读/修改/写入来实现。
答案 2 :(得分:1)
一个选项是Trust Your Compiler(tm),只需编写正确的代码。
使用此测试代码:
<DstTzInfo 'America/New_York' EDT-1 day, 20:00:00 DST>
Clang 6.0产生:
#include <iostream>
int main() {
char txt[] = {0, 0};
txt[0] = 0x41;
txt[1] = 0x42;
std::cout << txt;
}
答案 3 :(得分:0)
您正在寻找一个deep copy,您需要使用循环来完成(或者在内部为您执行循环的函数:memcpy
)。< / p>
只需将char
分配给char
,就必须将其截断以适应const char txt[] = { '\x41', '\x42' };
。这应该发出警告,因为结果将是特定于实现的,但通常保留最低有效位。
在任何情况下,如果您知道要分配的数字,您可以使用它们构建:size(txt)
我建议使用初始化列表进行此操作,显然您需要确保初始化列表至少与copy_n(begin({ '\x41', '\x42' }), size(txt), begin(txt));
一样长。例如:
display: table;