【问题标题】:Is it safe to access a struct using pointer pointed to unaligned memory address in C/C++?使用指向 C/C++ 中未对齐内存地址的指针访问结构是否安全?
【发布时间】:2021-12-30 11:34:30
【问题描述】:
struct CustomData
{
    char flag;
    int count;
    double value;
};

CustomData custom_data{};

char *buf = new char[sizeof(CustomData) + 3];
memcpy(buf + 3, &custom_data, sizeof(CustomData));

CustomData* ptr = (CustomData *)(buf + 3);

ptr->count = 10;
ptr->value = 20.0;

我有一个由一些标头字节和一个结构组成的内存缓冲区。该结构是此内存缓冲区的 memcpy。我必须修改这个嵌入结构中的一些字段,如上所示。

我的问题是:

  1. 这是 x86-64 上的安全访问吗?其他平台呢?

  2. memcpy 到本地结构,修改并复制回来肯定可以,但看起来很浪费。有没有办法检查 ptr 是否正确对齐以便安全访问结构?

【问题讨论】:

  • 请选择一种语言
  • 由于违反了严格的别名规则,此代码未根据 C(假设将 new 替换为 malloc)标准进行定义
  • @tstanisl: Per C 2018 6.5 6,动态分配内存并使用memcpy填充数据或使用字符类型复制创建一个具有原始数据有效类型的对象。
  • @EricPostpischil,对,我对设置对象类型的new 产生了偏见。
  • @463035818_is_not_a_number 你的意思是它在 C 和 C++ 之间是不同的?我什至不知道有什么区别。

标签: c++ c struct memcpy memory-alignment


【解决方案1】:

C 和 C++ 标准都没有定义在没有左值类型所需对齐的情况下访问内存的行为,甚至在指针值不具有目标类型所需的对齐时也没有定义将指针转换为另一种指针类型的行为。问题不在于您的目标架构(x86-64 或其他)是否支持未对齐访问,而是您的 C 或 C++ 实现,尤其是编译器是否支持它。

查看您的示例代码,我怀疑它是将数据从网络或文件或其他来源复制到缓冲区的代理,然后尝试将部分数据解释为所需的类型,可能是在检查了接收的数据以确定主题数据的类型和/或位置。如果是这样,你应该描述一下原来的情况,因为你显示的代理代码是不够的。

从网络或文件中以字节形式读取原始数据时,最好将数据直接读取到所需的结构中。理想情况下,将指定通信协议以根据需要对齐数据,以便一旦将数据接收到对齐的缓冲区中就不需要进一步的操作。在 C 中,联合可能有助于处理将一种类型的数据别名为另一种类型的问题。

直接进入结构方法失败,memcpy 可能会被使用,它可能没有你想象的那么低效,因为memcpy 方法导致代码具有明确定义的行为,然后编译器通常可以,给代码的相关部分足够的可见性,优化它生成的程序集,这样就不会出现实际的memcpy

如果做不到这一点,一些编译器对 C 和 C++ 标准进行了各种扩展,允许您在没有对齐要求的情况下定义结构类型并使用任意类型访问内存(支持将别名字符数组作为其他类型)。

至少在 C 中,动态分配的内存可以使用字符类型填充数据,然后按照有关“有效类型”的规则作为另一种类型访问。但是,必须遵循某些关于别名的规则,这不会允许访问不正确对齐的数据(不使用上述扩展)。

【讨论】:

  • 我从另一个模块读取数据(使用共享文件),它为每条记录提供了一个缓冲区,该缓冲区以一些字段开头,然后遵循一个结构。在我的阶段,我必须更改结构中的一些字段,并将其写入其他字段。我知道有一些序列化协议,例如potobuf,但我认为我们不能根据我们目前的情况(大量工作)切换到它们。
【解决方案2】:

任何指针双关都是危险的,因为它可能违反严格的别名规则。

您需要使用memcpy。大多数优化编译器都非常了解memcpy,并且通常不会发出对memcpy 的实际调用:

typedef struct CustomData
{
    char flag;
    int count;
    double value;
}CustomData;


CustomData *foo(int val, double d)
{
    char *buf = new char[sizeof(CustomData) + 3];

    CustomData cd;

    cd.count = val;
    cd.value = d;

    memcpy(buf + 3, &cd, sizeof(cd));
    return (CustomData *)buf;
}
foo(int, double):
        push    rbp
        mov     ebp, edi
        mov     edi, 19
        push    rbx
        sal     rbp, 32
        movq    rbx, xmm0
        sub     rsp, 8
        call    operator new[](unsigned long)
        mov     QWORD PTR [rax+3], rbp
        mov     QWORD PTR [rax+11], rbx
        add     rsp, 8
        pop     rbx
        pop     rbp
        ret

【讨论】:

  • 我在 VS 2019 上试过,它似乎是依赖于编译器的行为。在 VS 2019 上,memcpy 转换为 movupsmovsd SSE 指令。
  • @guanboshen 如何优化当然取决于编译器和平台
  • 这也取决于我们编写的代码。我今天早上试过了,最后我认为为所有前导字段和结构本身定义一个适当的包装结构是安全的。
猜你喜欢
  • 2020-02-07
  • 2020-05-09
  • 2017-01-04
  • 1970-01-01
  • 2014-04-29
  • 2021-10-16
  • 1970-01-01
  • 1970-01-01
  • 2012-07-16
相关资源
最近更新 更多