【问题标题】:memcpy() for a bare-metal environmentmemcpy() 用于裸机环境
【发布时间】:2020-10-22 03:30:14
【问题描述】:

这更像是一种好奇心。但我想知道,这段代码在裸机环境中实现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

据我了解,它不会违反严格的别名规则,因为指向 struct 的指针始终等于指向其第一个成员的指针,在这种情况下,第一个成员是 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

如果您担心假设的外星编译器添加任何可能的填充,添加 _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

关于代码合法性的讨论

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

    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 的answer 的回复——似乎启用优化后,编译器可能会做出错误的假设。在将数据复制到DST 之前,通过添加一个简单的*((char *) DST) = 0 来明确告诉编译器我们的别名就足够了。这里是新版本的宏,也可以在启用优化的情况下工作:

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)); }

【问题讨论】:

标签: c memcpy bare-metal


【解决方案1】:

就严格的别名而言,我还不足以成为一名语言律师,无法就代码是否正确发表意见。但是,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。但是,在x86-64 上使用gcc -O2,它会输出12。Try it on godbolt

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

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

【讨论】:

  • 很好,内特!见§ EDIT #4
  • @madmurphy:好的,因为char * 可以给任何东西起别名,所以你的*((char *) DST) = 0; 充当了“屏障”。但是一个认为struct tmp * 不能为int * 别名的编译器可能仍然会以我的例子为例,并在*((char *) DST) = 0; 和您的struct tmp 分配之间重新排序第二个(*x)++
  • 但是为什么要这样呢?在这种情况下,性能增益会是多少?如果编译器非常愚蠢,我们仍然可以使用 volatile 关键字来完全按照我们的意愿冻结所有内容——如果我们使用 volatile,我们甚至可以删除 *((char *) DST) = 0;
  • @madmurphy:例如,也许您之后对*x 进行了复杂的操作序列,编译器认为将它们与复制指令交错(对于超标量执行)会更有效struct tmp。但从我的角度来看,“编译器承诺按照我的意愿编译我的代码”和“编译器没有明显的理由不按照我的意愿编译我的代码”之间存在很大的鸿沟。我会在第二方面感到非常不舒服;谁知道如果你稍微改变一下周围的代码,或者编译器升级会发生什么?
  • 使用宏可能还有一些风险。我还没有尝试过,但是如果我们将宏转换为一个内联函数,该函数接受两个void * 参数和一个size_t,将会很有趣。我认为有许多可能的解决方法可以使代码非常安全。如果编译器也弄乱了内联函数,那么带有__attribute__((noinline)) 的普通函数肯定可以一劳永逸地解决所有问题。
【解决方案2】:

原创

据我了解,它不会违反严格的别名规则,因为指向结构的指针总是等于指向其第一个成员的指针,...

这是不正确的。指向结构的指针可以转换为指向其第一个成员的指针(反之亦然)的规则仅仅是关于指针转换的规则。严格的别名规则是关于访问对象的规则,不管指向它的指针是如何获得的。

无论您的DSTSRC 是什么类型,它都可能不是struct tmp。然后像 struct tmp 一样访问它的内存违反了 C 2018 6.5 7 中的别名规则,即只能通过兼容类型、字符类型或某些其他特殊情况访问对象。

如果DSTSRCchar 的数组,其中一种特殊情况可能会为您提供帮助。允许访问的类型之一是“在其成员中包含上述类型之一的聚合或联合类型……”由于struct tmp 在其成员中包含char 的数组,这似乎满足了要求。但是,为了成为兼容的类型,这两个数组(struct tmp 中的一个和SRCDST 中的一个)必须具有相同的大小。

C 别名规则的目的是允许编译器得出结论,对两种不同类型的事物的访问访问不同的内存。例如,如果一个函数被传递了一个float * 和一个int *,并且它将元素从一个复制到另一个,则别名规则允许编译器断定它们指向的数组不重叠,因此它可以优化通过一次复制多个元素来循环。出于同样的原因,当您使用 struct tmp 类型的赋值时,编译器中的优化器可能会得出结论,该赋值不会影响以不同方式执行的 DST 上的附近操作,并且由此产生的优化可能会破坏您的程序。

另一个问题是允许 C 实现在数组末尾包含填充,而不管对齐是否需要它。因此,仅基于 C 标准,您无法确定 struct tmp { char mem[SIZE]; } 的大小是 SIZE 字节。

“编辑#3”

“EDIT #3”认为可以使用char * 编写任何对象。这是正确的,尽管它错误地表述为“将任何内存位置转换为char * 是合法的。”如前所述,允许并部分或完全定义各种指针转换的事实与是否或定义了不通过特定的左值类型访问对象。 C 2018 6.3.2.3 7 表示只要对齐正确,任何指向对象类型的指针都可以转换为指向对象类型的任何其他指针,但 6.5 7 表示对象应通过以下方式访问具有某些类型的左值表达式。很明显,可以转换指针这一事实并不意味着可以使用新类型访问指向的对象。 (为方便起见,“可能”或“可能不”指的是动作的行为是否由 C 标准定义。从字面上看,程序“可能”基本上尝试任何事情,但对于本次讨论,我们只是对 C 标准定义的内容感兴趣。)

“Edit #3”进一步指出“将上面的所有字节共同映射到单个 char 数组也是合法的,因为我们正在映射相邻的字节。”此时,“Edit #3”提供了定义指向字节数组的指针的代码。 C 标准中没有这种推论的依据; C 标准中没有规定可以将内存中对象的字节(可以通过字符左值单独访问)视为字符数组。此外,指针转换的规则并没有说将指向char 的指针转换为指向char 数组的指针会导致指针值实际指向char 数组所在的内存。 6.3.2.3 7 只是说再次将指针转换回来会产生一个等于原始指针的值。

“Edit #3”进一步指出,因为我们可以将字节“映射”为char 的数组,所以我们可以“强制转换”(再次错误,问题在于用于访问的左值,而不是指针转换)将指向数组的指针转换为指向包含数组类型的结构的指针。这是不正确的,因为 6.5 7 中的规则并没有说,如果您可以使用类型 X 访问某物,并且类型 Y 与 X 兼容或者是针对 X 列出的其他情况之一,那么您可以使用类型 Y。6.5 7 表示对象的类型,而不是您可以合法访问它的其他类型。

换句话说,6.5 7 是不及物的。如果在它的规则中有某种情况可以用类型 Y 访问类型 X 的对象 O,并且有某种情况可以使用类型 Z 访问类型 Y 的对象,这并不意味着类型对象X 可以通过类型 Z 访问。

此外,6.5 7 被表述为绝对禁止。它表示对象的存储值应由某些左值表达式访问。如果违反了该规则,则行为是未定义的,这是决定性的。

动态分配

在移至聊天的评论中,OP 争辩说:“如果我可以访问一系列相邻的字节,就好像它们是 char 类型一样,我可以将它们全部访问,就好像它们是 char [] 类型一样。如果不是这样,我们就不能做 char * newmem = malloc(1000); newmem[whatever] = 'a';,因为我们会非法地将 char * 转换为 char []。”

此参数省略了某些条件。 C 标准规定了对象的有效类型的规则。对于动态分配的对象(没有声明的类型;它们是通过分配内存并通过指针为它们分配值来创建的),您可以通过使用字符类型以外的任何左值为其分配值来更改其有效类型 (6.5 6) .这意味着通过左值分配给DST,该左值是一个包含char 数组的结构,将由C 标准定义(如果还定义了右操作数的值)。但是,仍然存在两个问题:

  • 未定义读取SRC,因为它(通常)不是包含char 数组的结构,因此未定义通过左值读取它,该左值是包含char 数组的结构,由 6.5 7 (除非它实际上是这种结构或 6.5 7 明确允许的类型之一)。
  • 一旦 DST 被赋值赋予其新的有效类型,则将其读取为除 6.5 7 允许的类型之外的任何类型都具有未定义的行为。

【讨论】:

  • 我在问题中添加了关于代码合法性的讨论。
猜你喜欢
  • 1970-01-01
  • 2013-07-04
  • 2012-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-16
  • 2021-12-04
  • 2017-10-26
相关资源
最近更新 更多