8

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

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

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 位读取和movzwl16 位读取),那么为什么一个未定义的行为是另一个未定义的行为呢?是否memcpy有一些特殊的属性可以保证某些东西?

我不想在memcpy这里使用,因为我不能依赖编译器在优化它方面做得足够好。

4

2 回答 2

15

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

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

指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐 68) 对于引用的类型,则行为未定义。

IE

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

这种转换的存在允许编译器假定它ba与 4 字节边界对齐(在u32需要对齐的平台上,许多编译器将在 x86 上这样做),之后它可以生成假定对齐的代码。

即使在 x86 平台上,也有一些指令会严重失败: 看似无辜的代码可以编译成机器代码,这将导致运行时中止。UBSan 应该在代码中捕捉到这一点,否则这些代码在运行时看起来很正常并且行为“如预期”,但如果使用另一组选项或不同的优化级别编译则会失败。

编译器可以生成正确的代码memcpy- 并且经常会生成,但这只是因为编译器会知道未对齐的访问将在目标平台上工作并执行得足够好。

最后:

我不想在memcpy这里使用,因为我不能依赖编译器在优化它方面做得足够好。

你在这里说的是:“我希望我的代码只有在由垃圾或生成慢代码的两个十年前的编译器编译时才能可靠地工作绝对不是在使用可以优化它以快速运行的编译器编译时。”

于 2017-12-03T19:26:52.277 回答
2

您的对象的原始类型最好是...u32的数组,u32否则,您可以通过使用memcpy. 这不太可能成为现代系统的重大瓶颈。我不会担心的。

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

有时在硬件上执行优化以基于此对齐总线(从 CPU 到各种外围设备、内存等的一系列线路),其中之一是假设各种类型的地址仅以例如,它们的尺寸。在这样的平台上未对齐的访问可能会导致陷阱(segfault)。

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

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


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

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

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

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

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

"%"PRIu16那里可能就足够了。

于 2017-12-03T15:50:32.783 回答