加载未对齐的地址和 UBsan 发现

Load of misaligned address and UBsan finding

这个问题不是关于未对齐数据访问的定义,而是为什么 memcpy 沉默了 UBsan 的发现,而类型转换却没有,尽管生成了相同的汇编代码。

我有一些示例代码来解析发送字节数组的协议,该数组被分成六个字节的组。

void f(u8 *ba) {
    // I know this array's length is a multiple of 6
    u8 *p = ba;
    u32 a = *(u32 *)p;
    printf("a = %d\n", a);
    p += 4;
    u16 b = *(u16 *)p;
    printf("b = %d\n", b);

    p += 2;
    a = *(u32 *)p;
    printf("a = %d\n", a);
    p += 4;
    b = *(u16 *)p;
    printf("b = %d\n", b);
}

将我的指针递增 6 并进行另一次 32 位读取后,UBSan 报告有关加载未对齐的错误。我使用 memcpy 而不是类型双关来抑制此错误,但我不太了解原因。要清楚,这是没有 UBSan 错误的相同例程,

void f(u8 *ba) {
    // I know this array's length is a multiple of 6 (
    u8 *p = ba;
    u32 a;
    memcpy(&a, p, 4);
    printf("a = %d\n", a);
    p += 4;
    memcpy(&b, p, 2);
    printf("b = %d\n", b);

    p += 2;
    memcpy(&a, p, 4);
    printf("a = %d\n", a);
    p += 4;
    memcpy(&b, p, 2);
    printf("b = %d\n", b);
}

两个例程都编译为相同的汇编代码(使用 movl 进行 32 位读取,使用 movzwl 进行 16 位读取),那么为什么一个是未定义行为而另一个不是? memcpy 是否有一些特殊属性可以保证某些东西?

我不想在这里使用 memcpy,因为我不能指望编译器对其进行足够好的优化。

您的对象的原始类型最好是 u32,一个 u32 的数组...否则,您可以使用 memcpy 明智地处理它。这不太可能成为现代系统的重大瓶颈;我不会担心那个。

在某些平台上,整数不可能存在于每个可能的地址。考虑您系统的最大地址,我们可以假设 0xFFFFFFFFFFFFFFFF。这里不可能存在一个四字节的整数吧?

有时会基于此在硬件上执行优化以对齐总线(从 CPU 到各种外围设备、内存等的一系列电线),其中之一是假设例如,各种类型的地址仅以其大小的倍数出现。此类平台上的未对齐访问可能会导致陷阱(段错误)。

因此,UBSan 正确地警告您这个不可移植且难以调试的问题。

这个问题不仅会导致某些系统无法完全工作,而且您会发现允许您访问不对齐的系统需要通过总线进行第二次提取以检索整数的第二部分,无论如何。


此代码中还有一些其他问题。

printf("a = %d\n", a);

如果你想打印一个 int,你应该使用 %d。但是,你的论点是u32。不要像这样不匹配你的论点;这也是未定义的行为。我不确定 u32 是如何为您定义的,但我猜最接近标准的功能可能是 uint32_t(来自 <stdint.h>)。您应该在任何要打印 uint32_t 的地方使用 "%"PRIu32 作为格式字符串。 PRIu32(来自 <inttypes.h>)符号提供了一个实现定义的字符序列,这些字符将被实现 printf 函数识别。

请注意,此问题在其他地方重复出现,您使用的是 u16 类型:

printf("b = %d\n", b);

"%"PRIu16 可能就足够了。

UB sanitizer 用于检测代码不严格符合,实际上取决于无法保证的未定义行为。

实际上,C 标准指出,只要您将指针转换为地址未适当对齐的类型,行为就未定义C11 (draft, n1570) 6.3.2.3p7:

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned 68) for the referenced type, the behavior is undefined.

u8 *p = ba;
u32 *a = (u32 *)p; // undefined behaviour if misaligned. No dereference required

this 转换的存在允许编译器假定 ba 对齐到 4 字节边界(在需要 u32 的平台上因此对齐,许多编译器将在 x86 上执行此操作),之后它可以生成假定对齐的代码。

即使在 x86 平台上,也有一些指令会失败: 可以编译成机器代码,在 运行 时会导致中止。 UBSan 应该 catch 在代码中 otherwise 看起来理智并且当你 运行 它时表现 "as expected",但是如果使用另一组选项或不同的优化级别编译则失败。

编译器可以为 memcpy 生成 正确的 代码 - 通常会 ,但这只是因为编译器将知道未对齐的访问在目标平台上可以正常工作并表现良好。

最后:

I don't want to use memcpy here because I can't rely on compilers doing a good enough job optimising it.

你在这里说的是:"I want my code to work reliably only whenever compiled by garbage or two-decades-old compilers that generate slow code. Definitely not when compiled with the ones that could optimize it to run fast."