c - 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 *
每个单个字节映射到不同的变量: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 的回答的回复——似乎启用优化后,编译器可能会做出错误的假设。在将数据复制*((char *) DST) = 0
到DST
. 这里的新版本的宏也将在启用优化的情况下工作:
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)); }
解决方案
就严格的别名而言,我还不足以成为一名语言律师,无法就代码是否正确发表意见。但是,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 实现似乎是不明智的。
推荐阅读
- azure - Azure - 为存储容器中的每个新 blob 触发 Databricks 笔记本
- elasticsearch - Elasticsearch - 如何过滤嵌套对象列表
- c - C中字符常量的值
- java - 拆分url字符串时如何使用LinkedHashMap创建键值对?
- r - 向上添加列计数,直到另一列中的值发生变化
- javascript - 想要一次只定位一个 div,悬停的 div 而不是 ReactJS 中的另一个兄弟 div?
- android - 有没有办法知道地图是否加载成功?(如果网络不好)
- image - GH 页面不显示图片
- python - 在带有 Jupyter Notebook 的 Visual Studio Code 中使用 rpy2 不起作用
- python - 我的 OpenCV 双线性调整大小的实现不正确