首页 > 解决方案 > memcpy() 用于裸机环境

问题描述

这比其他任何事情都更令人好奇。但我想知道,这段代码在memcpy()裸机环境中实现的合法性如何?

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

然后我们可以使用

#include <stdio.h>

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

int main () {

    char buffer[100] = "Hello world";

    printf("%s\n", buffer);

    MY_MEMCPY(buffer, "one", 4)
    printf("%s\n", buffer);

    MY_MEMCPY(buffer, "two", 4)
    printf("%s\n", buffer);

    MY_MEMCPY(buffer, "three", 6)
    printf("%s\n", buffer);

    return 0;

}

哪个打印

Hello world
one
two
three

据我了解,它不会违反严格的别名规则,因为指向 a 的指针struct始终等于指向其第一个成员的指针,在这种情况下,第一个成员是 a char。见6.7.2.1p15

一个指向结构对象的指针,经过适当的转换,指向它的初始成员(或者如果该成员是位域,则指向它所在的单元),反之亦然。

它也不会有对齐问题,因为它_Alignof()1.

进一步阅读:

编辑#1

仅对于文字字符串,我们可以创建不需要任何长度作为参数传递的宏的另一个版本。当然,目的地仍然需要有足够的内存来保存新字符串。

这是修改后的版本:

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

我们可以使用

#include <stdio.h>

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

int main () {

    char buffer[100] = "Hello world";

    printf("%s\n", buffer);

    MY_LITERAL_MEMCPY(buffer, "one")
    printf("%s\n", buffer);

    MY_LITERAL_MEMCPY(buffer, "two")
    printf("%s\n", buffer);

    MY_LITERAL_MEMCPY(buffer, "three")
    printf("%s\n", buffer);

    return 0;

}

编辑#2

如果您担心假设的外星编译器添加的任何可能的填充,添加 a_Static_assert()将使宏非常安全:

MY_MEMCPY()

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; _Static_assert(sizeof(struct tmp) \
    == SIZE, "You have a very stupid compiler"); \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

MY_LITERAL_MEMCPY()

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; _Static_assert(sizeof(struct tmp) \
    == sizeof(SRC), "You have a very stupid compiler"); \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

编辑#3

关于代码合法性的讨论

如果将任何内存位置强制转换为 a 是合法的,那么我们可以将非类型的char *每个单个字节映射到不同的变量:charchar *

    some_non_char_type test;

    char * one = (char *) &test;
    char * two = (char *) &test + 1;
    char * three = (char *) &test + 2;

    ...

    char * last = (char *) &test + sizeof(test) - 1;

如果上面的代码是合法的,那么将上面的所有字节共同映射到一个char数组也是合法的,因为我们正在映射相邻的字节:

    char (* all_of_them)[sizeof(some_non_char_type)] = (char (*)[sizeof(some_non_char_type)]) &test;

在这种情况下,我们将以(*all_of_them)[0](*all_of_them)[1](*all_of_them)[2]等方式访问它们。

如果将相邻字节的集合映射到char数组是合法的,那么将这样的数组转换为单成员聚合类型是合法的,前提是编译器不向后者添加填充:

    struct tmp {
        char mem[sizeof(some_non_char_type)];
    };

    _Static_assert(sizeof(struct tmp) == sizeof(some_non_char_type),
        "You have a very stupid compiler");

    struct tmp * wrap = (struct tmp *) &test;

编辑#4

这是对 Nate Eldredge 的回答的回复——似乎启用优化后,编译器可能会做出错误的假设。在将数据复制*((char *) DST) = 0DST. 这里的新版本的宏也将在启用优化的情况下工作:

MY_MEMCPY()

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; _Static_assert(sizeof(struct tmp) \
    == SIZE, "You have a very stupid compiler"); *((char *) DST) = 0; \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

MY_LITERAL_MEMCPY():

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; _Static_assert(sizeof(struct tmp) \
    == sizeof(SRC), "You have a very stupid compiler"); *((char *) DST) = 0; \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

标签: cmemcpybare-metal

解决方案


就严格的别名而言,我还不足以成为一名语言律师,无法就代码是否正确发表意见。但是,GCC 似乎认为不是。

我想出了以下反例:

#include <string.h>
#include <stdio.h>

#define SIZE 50
#define STRUCT_COPY 

#ifdef STRUCT_COPY
// your proposed macro
#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; _Static_assert(sizeof(struct tmp) \
    == SIZE, "You have a very stupid compiler"); \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }
#else
#define MY_MEMCPY memcpy
#endif


void foo(int *x, void *a, void *b) {
    (*x)++;
    MY_MEMCPY(a, b, SIZE * sizeof(int));
    (*x)++;
}

int a[SIZE], b[SIZE];

int main(void) {
    a[0] = 10;
    b[0] = 20;
    foo(&a[0], a, b);
    printf("a[0] = %d", a[0]);
}

如果MY_MEMCPY()真的等于memcpy()那么程序应该输出 21。但是,gcc -O2在 x86-64 上,它会输出 12。 在 godbolt 上试试

与您的观点相反,编译器显然假设struct tmp *在宏中不能 alias int *,因此(*x)++可以围绕MY_MEMCPY()赋值重新排序。碰巧的是,它在复制之前加载*x到寄存器中,加 2,然后在复制之后将其存储回来。因此,复制到的值a[0]被其旧值加 2 覆盖,而不是递增。

现在,也许您认为 GCC 在这里是错误的,我想您可以通过提交错误报告与 GCC 开发人员一起解决这个问题,尽管我怀疑他们会坚持他们的解释并拒绝“修复”它。但至少从实用的角度来看,如果您知道广泛使用的编译器不会按照您想要的方式编译它,那么采用您的 struct copy 实现似乎是不明智的。


推荐阅读