UBSan:未对齐地址的加载

时间:2017-12-03 15:31:51

标签: c clang undefined-behavior memcpy ubsan

**编辑:问题不是关于未对齐数据访问的定义,而是为什么memcpy会使ubsanitizers静音,而类型转换则不会,尽管生成相同的汇编代码**

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

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);
}

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

我不想在这里使用memcpy,因为我不能依赖编译器做足够好的优化工作。

2 个答案:

答案 0 :(得分:6)

UB清理程序用于信号代码不严格符合,实际上取决于未保证的未定义行为。

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

  

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

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

强制转换的存在允许编译器假设ba已与4字节边界对齐(在需要u32对齐的平台上,许多编译器将在x86上执行哪些操作,之后它可以生成假定对齐的代码。

即使在x86平台上,也有令人失望的指令。甚至可以将innocent-looking code编译成只会导致运行时中止的机器代码。 UBSan应该在代码中捕获这个否则看起来很健全,并按照预期行事#34;当你运行它时,如果使用另一组选项或不同的优化级别进行编译则会失败。

编译器可以为memcpy生成完全相同的代码 - 而通常会,但这只是因为编译器会知道未对齐的访问可以正常工作并且在目标平台。

最后:

  

我不想在这里使用memcpy,因为我不能依赖编译器做好优化工作。

你在这里说的是:"我希望我的代码能够可靠地工作 每当由垃圾或二十年前编译生成慢速代码的编译器编译。绝对不是在编译时可以优化它以便快速运行。"

答案 1 :(得分:2)

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

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

有时在硬件上执行优化以使总线(从CPU引出的一系列电线到各种外围设备,内存和什么不是)基于此,其中一个是假设仅发生各种类型的地址例如,它们的大小的倍数。在这样的平台上错位访问可能会导致陷阱(段错误)。

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

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

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

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

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

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

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

"%"PRIu16可能就足够了。