指示Clang进行ARM兼容性的非对齐访问

3
我正在通过将文件映射到内存并通过C结构定义访问它来解析文件格式。该文件格式使用紧凑的结构,因此我无法保证字段将对齐到字边界。
解析工作正常,但不幸的是,在某些情况下优化器可能会造成破坏。特别是在编译armv7时,有些加载指令需要字对齐,而其他加载指令则不需要。考虑以下代码片段:
#define PACKED __attribute__((packed))

typedef struct PACKED _Box_mvhd {
    union {
        struct {
            int32_t creation_time;
            int32_t modification_time;
            int32_t time_scale;
            int32_t duration;
            ...
        } v0;
    } data;
} Box_mvhd;

Container mvhd = find_single_box(&moov, 'mvhd');
if (mvhd.boxStart) {
    Box_mvhd *mvhdBox = mvhd.mvhd;
    if (0 == mvhdBox.box.version) {
        uint32_t ts = ntohl(mvhdBox->data.v0.time_scale);
        uint32_t dur = ntohl(mvhdBox->data.v0.duration);
        ...
    }
}

-O0(调试)模式下,最内层的块会被编译为以下的汇编代码,其可以正常工作:
ldr r1, [r0, #24]
ldr r2, [r0, #20]

-O2编译优化级别下,编译器会意识到这些字段是相邻的,并生成以下汇编代码:
ldrdeq  r2, r3, [r0, #20]

很不幸,由于规范和实践,LDRD总是会生成对齐错误。因此,我需要一种有效地通知编译器这个问题的方式。理想情况下,这可以通过在结构上使用属性来完成。也有可能这是编译器或ARM后端的一个bug,但我愿意相信这不是。

我正在使用Xcode 4.2 (clang 3.0)编译,目标是面向iPhone的armv7。


4
放弃你的紧凑结构体并编写一个适当的解析器。实际上,我很困惑你的代码在哪里出了故障,因为该结构体已经正确对齐,不应需要填充。指针mvhdBoxr0)本身是否未对齐?如果是这样,你应该将数据memcpy到适当对齐的缓冲区中,从那里访问它,并删除填充属性。 - R.. GitHub STOP HELPING ICE
clang对alignof(Box_mvhd)返回什么?(如果clang不原生支持alignof(),您可以使用类似于#define alignof(t) offsetof(struct { char c; t x; }, x)的方法) - Michael Burr
1
@R..:他的代码出现故障是因为他将未对齐的指针转换为 Box_mvhd *,然后访问字段。 - Stephen Canon
抱歉如果不够清晰,但是mvhdBox本身是错位的,因为它可以出现在文件的任意偏移量处。 - mjr
1
那么不要直接在原地访问它。首先,将其从原始的unsigned char[]文件缓冲区中使用memcpy复制到一个真正的(因此正确对齐)Box_mvhd类型的对象中。 - R.. GitHub STOP HELPING ICE
2个回答

4

问题不在于结构体的字段没有所需的对齐,而是您将任意指针强制转换为指向您的结构体的指针,并且您要转换的指针没有结构体所需的对齐方式。严格来说,这是未定义的行为。

相反,使用memcpy函数从源缓冲区复制数据到您的结构体中。memcpy速度快,保证能处理任何对齐方式。


谢谢,这确实有效,并且是我们已经采取的解决方法。如果能够普遍避免复制,那将是很好的,因为文件中的一些框可以非常大(尽管在这种情况下不是)。 - mjr
如果盒子是可变大小的,您可能不需要复制整个盒子,只需复制固定大小的标题即可。如果剩余数据是字节数据,则可以轻松地在原地访问它。如果它是较大的类型,则可以为其编写一些宏来读取它,或者如果情况不太糟糕,可以继续使用您正在进行的复制。 - R.. GitHub STOP HELPING ICE
如果您需要在大的任意对齐缓冲区上进行一些处理,请考虑使用NEON通过内部函数-- NEON load/store可以处理任意对齐。 - Stephen Canon
如果您是从文件/管道/套接字等读取数据,那么它取决于您如何获取数据 - 您可能能够将读取数据的缓冲区对齐,以便它已经适当地对齐到您的结构体。@mjr - nos

0

实际上,指向紧凑结构体的不对齐指针是完全可以的。但是在编译时需要确保 -mcpu-march 参数设置正确。一些 ARM CPU 支持不对齐的 ldrd,而其他的则不支持。

是的,不对齐访问是非标准和编译器扩展,但它是完全安全的,除非您不小心传递了一个指向紧凑结构体成员的指针给另一个函数。

在这里,问题似乎是联合体内部的结构没有声明为紧凑型。您可以使用 alignof 运算符验证结构成员的对齐方式。它类似于 sizeof 运算符。如果 alignof 运算符告诉您,结构及其成员的对齐方式为 1,而 gcc 仍然使用 ldrd,那么您可能面临着编译器错误或命令行选项错误的情况。

请注意,-O0 的代码可能一开始就是错误的!(除非您的目标 ARM 架构支持不对齐的 ldr)。使用 packed 属性将结构成员的对齐方式重置为 1,这意味着 clang 应该发出八个 ldrb 指令而不是两个 ldr
是的,正如其他人已经告诉您的那样,在没有任何编译器扩展和不会出现难以发现的错误(例如将不对齐的指针传递给函数)的情况下,从普通 C 中访问不对齐的数据的唯一安全且可移植的方法是使用 memcpy

更新:使用-march=armv7,clang 3.3在访问data.v0.duration时生成了大量的ldrb指令。GCC更加聪明,使用了ldr,因为armv7支持不对齐的ldr。我根本不需要改变结构体的声明!您可以尝试将所有结构体成员添加打包属性。我相信,除非您的选项有误,否则您显然正在经历一个llvm错误。 - Sven

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接