【问题标题】:Using std::memmove to work around strict aliasing?使用 std::memmove 解决严格的别名?
【发布时间】:2021-07-04 09:51:55
【问题描述】:

可以使用std::memmove() 将内存“移动”到同一位置,以便能够使用不同类型对其进行别名处理吗?

例如:

#include <cstring>
#include <cstdint>
#include <iomanip>
#include <iostream>

struct Parts { std::uint16_t v[2u]; };

static_assert(sizeof(Parts) == sizeof(std::uint32_t), "");
static_assert(alignof(Parts) <= alignof(std::uint32_t), "");

int main() {
    std::uint32_t u = 0xdeadbeef;

    Parts * p = reinterpret_cast<Parts *>(std::memmove(&u, &u, sizeof(u)));

    std::cout << std::hex << u << " ~> "
              << p->v[0] << ", " << p->v[1] << std::endl;
}
$ g++-10.2.0 -Wall -Wextra test.cpp -o test -O2 -ggdb -fsanitize=address,undefined -std=c++20 && ./test
deadbeef ~> beef, dead

这是一种安全的方法吗?有什么注意事项?这里可以用static_cast代替reinterpret_cast吗?

【问题讨论】:

  • 您仍然没有合适的Parts 对象。通过内存表示创建普通对象的可移植方法是先使用Parts p;,然后使用memcpy&amp;pmemmove 在这里无关紧要。
  • 我很确定这是未定义的行为,不管有没有memmove。代码访问生命周期从未开始的Parts 对象。我看不出memmove 是如何改变这一点的。
  • @IgorTandetnik 但是struct Parts 不是implicit-lifetime typecreated by memmove
  • 代码示例中没有struct Parts 对象。有一个std::uint32_t。有一个struct Parts*,它指向一个不是struct Parts 的对象。
  • FYI C++20 引入了std::bit_cast 作为一种安全、便捷的方式来执行此操作。 cppreference 页面有一个示例实现,如果您的编译器尚未提供它(由于 GCC 11 FWIW),您可以使用它。

标签: c++ undefined-behavior strict-aliasing memmove


【解决方案1】:

如果有人想知道在没有-fno-strict-aliasing 的情况下,clang 和 gcc 优化器将可靠地处理哪些构造,而不是假设标准定义的所有内容都会得到有意义的处理,那么 clang 和 gcc 有时会忽略不会影响存储区域中的位模式的操作或操作序列对活动/有效类型所做的更改。

举个例子:

#include <limits.h>
#include <string.h>

#if LONG_MAX == LLONG_MAX
typedef long long longish;
#elif LONG_MAX == INT_MAX
typedef int longish;
#endif

__attribute((noinline))
long test(long *p, int index, int index2, int index3)
{
    if (sizeof (long) != sizeof (longish))
        return -1;

    p[index] = 1;
    ((longish*)p)[index2] = 2;

    longish temp2 = ((longish*)p)[index3];
    p[index3] = 5; // This should modify p[index3] and set its active/effective type
    p[index3] = temp2; // Shouldn't (but seems to) reset effective type to longish

    long temp3;
    memmove(&temp3, p+index3, sizeof (long));
    memmove(p+index3, &temp3, sizeof (long));
    return p[index];
}
#include <stdio.h>
int main(void)
{
    long arr[1] = {0};
    long temp = test(arr, 0, 0, 0);
    printf("%ld should equal %ld\n", temp, arr[0]);
}

虽然 gcc 恰好在 32 位 ARM 上正确处理此代码(即使使用标志 -mcpu=cortex-m3 来避免调用 memmove),但 clang 在两个平台上都无法正确处理它。有趣的是,虽然 clang 没有尝试重新加载 p[index],但 gcc 确实会在两个平台上重新加载它,但 x64 上 test 的代码是:

test(long*, int, int, int):
        movsx   rsi, esi
        movsx   rdx, edx
        lea     rax, [rdi+rsi*8]
        mov     QWORD PTR [rax], 1
        mov     rax, QWORD PTR [rax]
        mov     QWORD PTR [rdi+rdx*8], 2
        ret

此代码将值 1 写入 p[index1],然后读取 p[index1],将 2 存储到 p[index2],并返回刚刚从 p[index1] 读取的值。

memmove 可能会在正确处理标准规定的所有极端情况的所有实现上清除活动/有效类型,但在 clang 和 gcc 的 -fno-strict-aliasing 方言上没有必要,在 @ 987654328@ 这些编译器处理的方言。

【讨论】:

    猜你喜欢
    • 2014-02-27
    • 2018-12-14
    • 1970-01-01
    • 2021-05-23
    • 2018-03-04
    • 2023-02-02
    • 2012-05-20
    • 2015-10-15
    • 1970-01-01
    相关资源
    最近更新 更多