Linux ARM上的未对齐内存访问功能
我正在从事一个从内存读取数据的项目。其中一些数据是整数,在未对齐的地址访问它们时出现问题。我的想法是使用memcpy,即Linux ARM上的未对齐内存访问功能,linux,gcc,arm,undefined-behavior,memory-alignment,Linux,Gcc,Arm,Undefined Behavior,Memory Alignment,我正在从事一个从内存读取数据的项目。其中一些数据是整数,在未对齐的地址访问它们时出现问题。我的想法是使用memcpy,即 uint32_t readU32(const void* ptr) { uint32_t n; memcpy(&n, ptr, sizeof(n)); return n; } 我从项目源代码中找到的解决方案类似于以下代码: uint32_t readU32(const uint32_t* ptr) { union {
uint32_t readU32(const void* ptr)
{
uint32_t n;
memcpy(&n, ptr, sizeof(n));
return n;
}
我从项目源代码中找到的解决方案类似于以下代码:
uint32_t readU32(const uint32_t* ptr)
{
union {
uint32_t n;
char data[4];
} tmp;
const char* cp=(const char*)ptr;
tmp.data[0] = *cp++;
tmp.data[1] = *cp++;
tmp.data[2] = *cp++;
tmp.data[3] = *cp;
return tmp.n;
}
所以我的问题是:
char*
来处理内存,因此必须进行从char*
到uint32\u t*
的转换。如果uint32\u t*
没有正确对齐,那么这不是未定义行为的结果吗?如果是,那么函数就没有意义了,因为您可以编写*(uint32\u t*)
来获取内存。此外,我想我在某个地方读到,编译器可能期望int*
正确对齐,任何未对齐的int*
也意味着未定义的行为,因此为该函数生成的代码可能会生成一些快捷方式,因为它可能期望函数参数正确对齐
volatile
,因为内存内容可能会改变(它是驱动程序中的数据缓冲区(没有寄存器))。也许这就是为什么它不使用memcpy的原因,因为它不能处理易失性数据。但是,在哪一个世界上这样做才有意义呢?如果基础数据可以随时更改,则所有赌注都将取消。数据甚至可能在这些字节复制操作之间发生变化。因此,您必须使用某种互斥来同步对该数据的访问。但如果有这样的同步,为什么需要volatile呢memcpy
uint32_t readU32(const uint32_t* ptr)
{
union {
uint32_t n;
char data[4];
} tmp;
const char* cp=(const char*)ptr;
tmp.data[0] = *cp++;
tmp.data[1] = *cp++;
tmp.data[2] = *cp++;
tmp.data[3] = *cp;
return tmp.n;
}
将指针作为uint32\u t*
传递。如果它实际上不是一个uint32\u t
,那就是UB。参数可能应该是常量void*
在转换本身中使用常量char*
不是未定义的行为。根据第6.3.2.3节的第7段(重点):
指向对象类型的指针可以转换为指向对象类型的指针
不同的对象类型。如果生成的指针不正确
与引用的类型对齐,行为未定义。否则,当再次转换回时,应比较结果 等于原始指针当指向对象的指针 转换为指向字符类型的指针,结果指向 对象的最低寻址字节。连续增量 结果,直到对象的大小,都会产生指向剩余对象的指针 对象的字节数。
使用
volatile
直接在特定硬件上访问内存/寄存器的正确方法将没有规范的/可接受的/最佳的解决方案。这方面的任何解决方案都是针对您的系统的,超出了标准C的范围。允许实现在标准没有定义行为的情况下定义行为,并且一些实现可能指定所有指针类型具有相同的表示形式,并且可以在彼此之间自由转换,而不考虑对齐方式,只要实际用于访问对象的指针适当对齐
不幸的是,由于一些迟钝的编译器强制使用“memcpy”作为
即使已知指针已对齐,也会出现锯齿问题的安全阀,
编译器能够有效地处理需要生成的代码的唯一方法
与类型无关的对对齐存储的访问是假定需要对齐的类型的任何指针都将始终与该类型相匹配。因此,你本能地认为使用
uint32\u t*
是危险的。可能需要进行编译时检查,以确保函数传递的是void*或uint32_t*,而不是uint16_t*或double*,但如果不允许编译器“优化”,就无法以这种方式声明函数该函数通过将字节访问合并到32位加载中来实现,如果指针未对齐,该加载将失败。联合确保对其中包含的所有类型进行充分对齐memcpy
是目前常用的方法,但是它不适用于易失性访问,有时您必须向优化器提供最适合的版本。至于volatile
,它本身可以充当同步原语,例如在写入volatile I/O寄存器后读回volatile结果,但通常使用其他机制,如编译器屏障。此外,volatile
限定符强制按字节连续访问,这对于I/O可能很重要。对于第二个版本,不是结果指针,而是源指针没有正确对齐,但是调用函数的行为是UB。您自己的(第一个)版本看起来不错,特别是当编译器执行memcpy
内联时。这里需要特别记住的是,I/O寄存器不是正常的内存缓冲区,并且往往具有副作用和限制,因此需要小心地强制特定的所需访问模式。例如,读取状态寄存器可能会确认事件,仅支持32位访问,不小心的读-修改-写周期来翻转位可能会导致与硬件和软件的竞争。因此,请注意仔细阅读数据表
uint32_t readU32(const uint32_t* ptr)
{
union {
uint32_t n;
char data[4];
} tmp;
const char* cp=(const char*)ptr;
tmp.data[0] = *cp++;
tmp.data[1] = *cp++;
tmp.data[2] = *cp++;
tmp.data[3] = *cp;
return tmp.n;
}