是否可以通过溢出C中的第一个元素来写入数组的第二个元素?

时间:2018-05-17 11:35:42

标签: c++ c assembly nasm

在低级语言中,有可能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]元素。

4 个答案:

答案 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;

Live Example