Linux ARM上的未对齐内存访问功能

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 {

我正在从事一个从内存读取数据的项目。其中一些数据是整数,在未对齐的地址访问它们时出现问题。我的想法是使用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 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;
}
所以我的问题是:

  • 第二个版本不是未定义的行为吗?C标准在6.2.3.2指针中指出,第7点:
  • 指向对象或不完整类型的指针可能会转换为指向其他类型的指针 对象或不完整类型。如果结果指针未正确对齐(57),则 指向类型时,行为未定义

    由于调用代码在某些时候使用了
    char*
    来处理内存,因此必须进行从
    char*
    uint32\u t*
    的转换。如果
    uint32\u t*
    没有正确对齐,那么这不是未定义行为的结果吗?如果是,那么函数就没有意义了,因为您可以编写
    *(uint32\u t*)
    来获取内存。此外,我想我在某个地方读到,编译器可能期望
    int*
    正确对齐,任何未对齐的
    int*
    也意味着未定义的行为,因此为该函数生成的代码可能会生成一些快捷方式,因为它可能期望函数参数正确对齐

  • 原始代码在参数和所有变量上都有
    volatile
    ,因为内存内容可能会改变(它是驱动程序中的数据缓冲区(没有寄存器))。也许这就是为什么它不使用memcpy的原因,因为它不能处理易失性数据。但是,在哪一个世界上这样做才有意义呢?如果基础数据可以随时更改,则所有赌注都将取消。数据甚至可能在这些字节复制操作之间发生变化。因此,您必须使用某种互斥来同步对该数据的访问。但如果有这样的同步,为什么需要volatile呢

  • 对于这个内存访问问题,是否有一个规范的/可接受的/更好的解决方案?经过一些搜索,我得出结论,您需要一个互斥体,不需要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;
    }