未对齐的内存访问数组

问题描述

在C程序中,具有一个数组,该数组旨在用作缓冲区FooBuffer[],用于存储如下结构的数据成员的内容

struct Foo {
    uint64_t data;
};

有人告诉我这行可能会导致未对齐的访问:

uint8_t FooBuffer[10] = {0U};

我有一些知识,未对齐访问取决于处理器的对齐偏移量,通常,它会占用更多的读/写周期。在什么情况下会导致未对齐的内存访问,我该如何防止?

编辑: 类型为struct Foo 的变量将存储在缓冲区中。特别是,其成员 data 将被分成八个字节,这些字节将存储在数组 FooBuffer 中。请参阅附带的代码以及一些选项。

#include <stdio.h>
#include <string.h>

typedef unsigned long uint64;
typedef unsigned char uint8;

struct Foo
{
    uint64 data;
};

int main()
{
    struct Foo foo1 = {0x0123456789001122};
    uint8 FooBuffer[10] = {0U};
    
    FooBuffer[0] = (uint8)(foo1.data);
    FooBuffer[1] = (uint8)(foo1.data >> 8);
    FooBuffer[2] = (uint8)(foo1.data >> 16);
    FooBuffer[3] = (uint8)(foo1.data >> 24);
    FooBuffer[4] = (uint8)(foo1.data >> 32);
    FooBuffer[5] = (uint8)(foo1.data >> 40);
    FooBuffer[6] = (uint8)(foo1.data >> 48);
    FooBuffer[7] = (uint8)(foo1.data >> 56);
    
    struct Foo foo2 = {0x9876543210112233};
    uint8 FooBuffer2[10] = {0U};
    
    memcpy(FooBuffer2,&foo2,sizeof(foo2));

    return 0;
}

但是,由于一个私有软件执行该操作,因此不清楚如何完成此过程。在“转换”之后可能导致未对齐的内存访问的情况是什么?

解决方法

定义诸如struct Foo { uint64_t data; }之类的结构或诸如uint8_t FooBuffer[10];之类的数组并以常规方式使用它们不会导致未对齐的访问。 (为什么您为FooBuffer使用10?此示例仅需要8个字节?)

新手有时会尝试导致未对齐访问的方法,是尝试将字节数组重新解释为数据结构。例如,考虑:

// Get raw bytes from network or somewhere.
uint8_t FooBuffer[10];
CallRoutineToReadBytes(FooBuffer,...);

// Reinterpret bytes as original type.
struct Foo x = * (struct Foo *) FooBuffer; // Never do this!

这里的问题是struct Foo有一些对齐要求,而FooBuffer没有。因此FooBuffer可以位于任何地址,但是对struct Foo *的强制转换会尝试将其强制为struct Foo的地址。如果对齐方式不正确,则行为不会由C标准定义。即使系统允许它并且程序“运行”,它也可能在未正确对齐的地址上访问struct Foo并遭受性能问题。

为避免这种情况,重新解释字节的正确方法是将其复制到新对象中:

struct Foo x;
memcpy(&x,FooBuffer,sizeof x);

通常,编译器会认识到这里发生的事情,特别是如果struct Foo不大时,可以高效地实现memcpy,例如,将其作为两个加载四字节指令或一个加载-八字节指令。

您可以采取以下措施来帮助编译器通过使用FooBuffer关键字声明_Alignas来使其对齐:

uint8_t _Alignas(Struct Foo) FooBuffer[10];

请注意,如果您需要从缓冲区中间获取字节,例如从包含先前协议字节和其他数据的网络消息中获取字节,这可能无济于事。而且,即使它确实提供了所需的对齐方式,也不要使用上面显示的* (struct Foo *) FooBuffer。它有很多问题,而不仅仅是对齐问题,其中之一是C标准不能保证像这样重新解释数据的行为。 (在C中支持此操作的一种方法是通过联合,但是memcpy是一个很好的解决方案。)

在您显示的代码中,使用位移将字节从foo1.data复制到FooBuffer。这也不会引起对齐问题;像这样操作数据的表达式就可以了。但是有两个问题。一种是名义上逐个操作单个字节。这在C语言中是完全合法的,但速度可能很慢。编译器可能会对其进行优化,并且可能会依赖于内置平台或内置函数来提供帮助。

另一个问题是,它根据字节的位置值将其按顺序放置:低位置值字节首先放入缓冲区。相反,memcpy方法按字节存储在内存中的顺序复制字节。您要使用哪种方法取决于您要解决的问题。要将数据存储在一个系统上并稍后在同一系统上读回,可以使用memcpy方法。要使用相同的字节顺序在两个系统之间发送数据,可以使用memcpy方法。但是,如果要将数据从Internet上的一个系统发送到另一个系统,并且两个系统在内存中不使用相同的字节顺序,则需要同意在网络程序包中使用的顺序。在这种情况下,通常使用按位置排列字节数的方法。同样,您的平台可能具有内置插件或库例程来协助完成此任务。例如,htonlntohl例程是BSD例程,它们采用普通的32位无符号整数,并以按网络顺序排列的字节形式返回它,反之亦然。