问题描述
这更是一种好奇。但是我想知道,此代码在裸机环境中实现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,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","two","three",6)
printf("%s\n",buffer);
return 0;
}
可打印
Hello world
one
two
three
据我了解,它不会违反严格的别名规则,因为指向struct
的指针始终等于指向其第一个成员的指针,在这种情况下,第一个成员是{{1} }。参见6.7.2.1p15:
指向经过适当转换的结构对象的指针指向其初始成员(或者,如果该成员是位字段,则指向它所驻留的单元),反之亦然。
由于它的char
是_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)); }
编辑#2
如果您担心假设的外来编译器会添加任何填充,则添加#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","two")
printf("%s\n","three")
printf("%s\n",buffer);
return 0;
}
将使宏非常安全:
_Static_assert()
:
MY_MEMcpy()
#define MY_MEMcpy(DST,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()
编辑#3
讨论代码的合法性
如果将任何内存位置都转换为/*
**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)); }
是合法的,那么我们可以将非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
编辑#4
这是对Nate Eldredge的answer的答复–似乎在启用优化的情况下,编译器可能会做出错误的假设。通过在复制数据到 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;
之前添加一个简单的*((char *) DST) = 0
来明确告诉编译器有关别名的信息就足够了。在这里,新版本的宏也将与启用的优化一起工作:
DST
:
MY_MEMcpy()
#define MY_MEMcpy(DST,"You have a very stupid compiler"); *((char *) DST) = 0; \
*((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }
:
MY_LIteraL_MEMcpy():
解决方法
原始
据我所知,它不会违反严格的别名规则,因为指向结构的指针始终等于指向其第一个成员的指针,…
这是不正确的。可以将指向结构的指针转换为指向其第一成员的指针的规则,反之亦然,这仅是关于指针转换的规则。严格的别名规则是关于访问对象的规则,无论如何获取指向它的指针。
无论您的DST
或SRC
是什么类型,都可能不是struct tmp
。然后,就好像访问struct tmp
一样访问其内存,这违反了C 2018 6.5 7中的别名规则,该规则规定只能通过兼容类型,字符类型或某些其他特殊情况访问对象。>
如果DST
或SRC
是char
的数组,则其中一种特殊情况可能会救您。允许的访问类型之一是“在其成员中包括上述类型之一的聚合或联合类型……”由于struct tmp
在其成员中包括char
的数组,因此这似乎可以满足要求。但是,为了成为兼容的类型,两个数组-struct tmp
中的一个数组和SRC
或DST
中的一个数组必须大小相同。
C别名规则的目的是允许编译器得出结论,对两种不同类型的事物的访问将访问不同的内存。例如,如果函数传递了一个float *
和一个int *
,并且将元素从一个元素复制到另一个元素,则别名规则允许编译器得出结论,即它们指向的数组不重叠,并且因此,它可以一次复制多个元素来优化循环。出于相同的原因,当您使用struct tmp
类型的分配时,编译器中的优化程序可能会得出结论,该分配不会影响对DST
的附近操作(以不同的方式执行),并且结果优化可能会破坏您的程序。
另一个问题是,无论是否需要对齐,都允许C实现在数组的末尾包括填充。因此,仅凭C标准就无法确定struct tmp { char mem[SIZE]; }
的大小为SIZE
个字节。
“编辑#3”
“编辑#3”认为可以使用char *
写入任何对象。这是正确的,尽管它错误地将其表述为“将任何内存位置强制转换为char *
是合法的。”如前所述,允许进行各种指针转换并部分或完全定义的事实与定义是否通过特定的左值类型访问对象。 C 2018 6.3.2.3 7表示只要对齐正确,任何指向对象类型的指针都可以转换为任何其他指向该对象类型的指针,但是6.5 7表示该对象只能访问具有某些类型的左值表达式。很明显,可以转换指针这一事实并不意味着可以使用新类型访问指向的对象。 (为方便起见,“可以”或“可能不”是指动作的行为是否由C标准定义。从字面上看,程序“可以”实质上尝试任何事情,但是,在此讨论中,我们只是对C标准的定义感兴趣。)
“编辑#3”进一步指出,“将以上所有字节共同映射到单个char数组也是合法的,因为我们正在映射相邻字节。”此时,“编辑#3”显示了定义指向字节数组的指针的代码。 C标准中没有进行此推断的依据; C标准中没有规则说内存中对象的字节(可以通过字符左值单独访问)可以被视为字符数组。此外,指针转换的规则并没有说将指向char
的指针转换为指向char
数组的指针会导致指针值实际上指向指向{{1 }} 将会。 6.3.2.3 7仅表示再次将指针转换回原来的值将等于原始指针。
“编辑#3”进一步指出,因为我们可以将字节“映射”为char
的数组,所以我们可以“投射”(再次错误,问题是用于访问的左值,而不是有效性指针转换的结果)指向数组的指针指向包含数组类型的结构的指针。这是不正确的,因为6.5 7中的规则没有说,如果您可以使用X类型访问某些东西,并且Y类型与X兼容或者是针对X列出的其他情况之一,那么您可以使用类型6.5。6.5 7是根据对象的类型而不是您可以合法访问的其他类型来表示的。
换句话说,6.5 7不是可传递的。如果在某些规则中可以使用类型Y访问类型X的对象O,并且在某些情况下可以使用类型Z访问类型Y的对象,则并不意味着该类型的对象可以使用Z类型访问X。
此外,将6.5 7表示为绝对禁止。它说对象应通过某些左值表达式仅访问其存储值。如果违反了该规则,则行为是不确定的,并且是结论性的。
动态分配
在聊天中发表的评论中,OP争论道:“如果我可以访问一系列相邻的字节,就像它们是char类型,那么我可以像访问char []一样集体访问所有字节。如果不是这种情况,我们就不能做char * newmem = malloc(1000); newmem [whatever] ='a';,因为我们会非法将char *强制转换为char []。”
此参数忽略了某些条件。 C标准规定了对象有效类型的规则。对于动态分配的对象(没有声明的类型;它们是通过分配内存并通过指针向它们分配值来创建的),可以通过使用字符类型以外的任何左值为其分配值来更改其有效类型(6.5 6) 。这意味着通过左值分配给char
是一个包含DST
数组的结构,这将由C标准定义(如果还定义了右操作数的值)。但是,仍然存在两个问题:
- 未定义读取
char
,因为它(通常)不是包含SRC
数组的结构,因此请通过左值读取它,该值是包含{{1 }}未由6.5 7定义(除非实际上是这样的结构或6.5 7特别允许的类型之一)。 - 一旦赋值给
char
赋予其新的有效类型,则将其读取为6.5 7允许的类型以外的任何其他类型都具有未定义的行为。
我还没有足够的语言律师来就严格的别名对代码是否正确提出意见。但是,似乎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。 / p>
与您的看法相反,编译器显然假设宏中的struct tmp *
不能别名int *
,因此(*x)++
可以在MY_MEMCPY()
分配周围重新排序。碰巧的是,它在复制之前将*x
加载到寄存器中,加2,然后在复制之后将其存储回去。因此,复制到a[0]
中的值将被其旧值加2覆盖,而不是增加。
现在,也许您认为GCC错了,我想您可以通过提交错误报告来与GCC开发人员联系,尽管我怀疑他们会坚持他们的解释并拒绝“修复”该错误。但是至少从务实的角度来看,如果您知道广泛使用的编译器不会按照您希望的方式编译它,那么采用结构复制实现似乎是不明智的。