19

有人可以提供一个示例,由于未对齐而将指针从一种类型转换为另一种类型失败吗?

在对这个答案的评论中,bothie 说做类似的事情

char * foo = ...;
int bar = *(int *)foo;

如果启用对齐检查,即使在 x86 上也可能导致错误。

通过在 GDB 中设置对齐检查标志后,我尝试产生错误条件set $ps |= (1<<18),但没有发生任何事情。

工作(即不工作;))示例是什么样的?


答案中的所有代码片段都不会在我的系统上失败 - 我稍后会在不同的编译器版本和不同的电脑上尝试它。

顺便说一句,我自己的测试代码看起来像这样(现在也使用 asm 来设置AC标志和未对齐的读写):

#include <assert.h>

int main(void)
{
    #ifndef NOASM
    __asm__(
        "pushf\n"
        "orl $(1<<18),(%esp)\n"
        "popf\n"
    );
    #endif

    volatile unsigned char foo[] = { 1, 2, 3, 4, 5, 6 };
    volatile unsigned int bar = 0;

    bar = *(int *)(foo + 1);
    assert(bar == 0x05040302);

    bar = *(int *)(foo + 2);
    assert(bar == 0x06050403);

    *(int *)(foo + 1) = 0xf1f2f3f4;
    assert(foo[1] == 0xf4 && foo[2] == 0xf3 && foo[3] == 0xf2 &&
        foo[4] == 0xf1);

    return 0;
}

断言通过没有问题,即使生成的代码肯定包含未对齐的访问mov -0x17(%ebp), %edxmovl $0xf1f2f3f4,-0x17(%ebp).


那么设置是否会AC触发SIGBUS?我无法让它在 Windows XP 下的英特尔双核笔记本电脑上运行,而我测试的任何 GCC 版本(MinGW-3.4.5、MinGW-4.3.0、Cygwin-3.4.4)都没有,而 codelogic 和 Jonathan Leffler提到x86上的失败......

4

8 回答 8

21

在 x86 上未对齐的访问会导致问题的情况并不常见(除了内存访问需要更长的时间)。以下是我听说过的一些:

  1. 您可能不会将此视为 x86 问题,但 SSE 操作受益于对齐。对齐的数据可以用作内存源操作数来保存指令。像在 Nehalem 之前的微架构上的未对齐加载指令movupsmovaps在 Nehalem 之前的微架构上要慢,但在 Nehalem 及更高版本(以及 AMD Bulldozer 系列)上,未对齐的 16 字节加载/存储与未对齐的 8 字节加载/存储一样有效;如果数据恰好在运行时对齐或没有跨越缓存线边界,则单个 uop 并且根本没有任何惩罚,否则对缓存线拆分提供有效的硬件支持。在 Skylake 之前,4k 拆分非常昂贵(约 100 个周期)(像缓存行拆分一样低至约 10 个周期)。有关更多信息,请参阅https://agner.org/optimize/和x86 标签 wiki中的性能链接。

  2. 如果联锁操作(如lock add [mem], eax)没有充分对齐,则它们会非常慢,尤其是当它们跨越缓存线边界时,它们不能只在 CPU 内核中使用缓存锁。在较旧的(有缺陷的)SMP 系统上,它们实际上可能不是原子的(请参阅https://blogs.msdn.com/oldnewthing/archive/2004/08/30/222631.aspx)。

  3. Raymond Chen 讨论的另一种可能性是在处理具有硬件存储内存的设备时(诚然是一种奇怪的情况) - https://blogs.msdn.com/oldnewthing/archive/2004/08/27/221486.aspx

  4. 我记得(但没有参考 - 所以我不确定这个)与跨越页面边界的未对齐访问的类似问题也涉及页面错误。我会看看我是否可以为此挖掘参考。

在研究这个问题时,我学到了一些新东西(我想知道$ps |= (1<<18)在几个地方提到的“” GDB 命令)。我没有意识到 x86 CPU(似乎从 486 开始)能够在执行未对齐的访问时引发异常。

来自 Jeffery Richter 的“Programming Applications for Windows, 4th Ed”:

让我们仔细看看 x86 CPU 是如何处理数据对齐的。x86 CPU 在其 EFLAGS 寄存器中包含一个特殊的位标志,称为 AC(对齐检查)标志。默认情况下,当 CPU 首次通电时,此标志设置为零。当此标志为零时,CPU 会自动执行任何操作以成功访问未对齐的数据值。但是,如果该标志设置为 1,则只要尝试访问未对齐的数据,CPU 就会发出 INT 17H 中断。x86 版本的 Windows 2000 和 Windows 98 永远不会改变这个 CPU 标志位。因此,当应用程序在 x86 处理器上运行时,您永远不会在应用程序中看到数据未对齐异常。

这对我来说是个新闻。

当然,未对齐访问的最大问题是,当您最终为非 x86/x64 处理器编译代码时,您最终不得不追踪并修复一大堆东西,因为几乎所有其他 32 位或更大处理器对对齐问题很敏感。

于 2009-02-14T03:12:29.263 回答
8

如果您阅读 Core I7 架构(特别是他们的优化文献),英特尔实际上已经在其中放置了大量硬件,以使未对齐的内存访问几乎免费。据我所知,只有跨越缓存线边界的错位才会有任何额外的成本——即使如此,它也是最小的。据我所知(虽然已经有一段时间了),AMD 在访问不对齐(循环访问)方面也几乎没有问题。

对于它的价值,当我在优化我正在处理的项目时,我确实在 eflags(AC 位 - 对齐检查)中设置了该标志。事实证明,windows 充满了未对齐的访问 - 如此之多,以至于我无法在我们的代码中找到任何未对齐的内存访问,我被库和 windows 代码中如此多的未对齐访问轰炸了,我没有时间去继续。

也许我们可以了解到,当 CPU 使事情变得免费或成本非常低时,程序员会变得自满并做一些额外开销的事情。也许英特尔的工程师做了一些调查,发现典型的 x86 桌面软件每秒执行数百万次未对齐的访问,因此他们在 CoreI7 中放置了令人难以置信的快速未对齐访问硬件。

高温高压

于 2011-09-22T15:29:33.400 回答
3

char *foo 可能与 int 边界对齐。试试这个:

int bar = *(int *)(foo + 1);
于 2009-02-14T00:06:44.750 回答
3

EFLAGS.AC 实际生效还有一个未提及的附加条件。必须设置 CR0.AM 以防止 INT 17h 在 486 之前没有此异常处理程序的旧操作系统上跳闸。不幸的是,Windows 默认不设置它,您需要编写内核模式驱动程序来设置它。

于 2009-12-18T17:03:40.880 回答
2
char *foo = "....";
foo++;
int *bar = (int *)foo;

编译器会将 foo 放在字边界上,然后当您将其递增时,它位于字 + 1,这对于 int 指针无效。

于 2009-02-14T00:05:53.580 回答
2
#include <stdio.h>

int main(int argc, char **argv)
{
  char c[] = "a";

  printf("%d\n", *(int*)(c));
}

这在 gdb 中设置后给了我一个SIGBUSset $ps |= (1<<18),这显然是在地址对齐不正确时抛出的(以及其他原因)。

编辑:提高 SIGBUS 相当容易:

int main(int argc, char **argv)
{
    /* EDIT: enable AC check */
    asm("pushf; "
        "orl $(1<<18), (%esp); "
        "popf;");

    char c[] = "1234567";
    char d[] = "12345678";
    return 0;
}

在 gdb 中查看 main 的反汇编:

Dump of assembler code for function main:
....
0x08048406 <main+34>:   mov    0x8048510,%eax
0x0804840b <main+39>:   mov    0x8048514,%edx
0x08048411 <main+45>:   mov    %eax,-0x10(%ebp)
0x08048414 <main+48>:   mov    %edx,-0xc(%ebp)
0x08048417 <main+51>:   movl   $0x34333231,-0x19(%ebp)   <== BAM! SIGBUS
0x0804841e <main+58>:   movl   $0x38373635,-0x15(%ebp)
0x08048425 <main+65>:   movb   $0x0,-0x11(%ebp)

无论如何,Christoph 你的测试程序在 Linux 下失败了,因为它应该提高 SIGBUS。这可能是Windows的事情?


您可以使用以下代码段在代码中启用对齐检查位:

/* enable AC check */
asm("pushf; "
    "orl $(1<<18), (%esp); "
    "popf;");

此外,确保确实设置了标志:

unsigned int flags;
asm("pushf; "
    "movl (%%esp), %0; "
    "popf; " : "=r"(flags));
fprintf(stderr, "%d\n", flags & (1<<18));
于 2009-02-14T00:15:04.497 回答
2

要享受例外,请SetErrorMode致电SEM_NOALIGNMENTFAULTEXCEPT

int main(int argc, char* argv[])
{
   SetErrorMode(GetErrorMode() | SEM_NOALIGNMENTFAULTEXCEPT);
   ...
}

有关详细信息,请参阅IPF、x86 和 x64 上的 Windows 数据对齐

于 2013-03-05T22:04:27.383 回答
1

自动矢量化时的 gcc 假定它uint16_t*与 2 字节边界对齐。如果你违反这个假设,你会得到一个段错误: 为什么在 AMD64 上对 mmap 内存的未对齐访问有时会出现段错误?

因此,即使针对 x86,尊重 C 对齐规则也很重要。


使用它来有效地表达 C 中的未对齐负载:

static inline
uint32_t load32(char *p)     // char*  is allowed to alias anything
    uint32_t tmp;
    memcpy(&tmp, p, sizeof(tmp));
    return tmp;
}

在 x86 上,它将编译为mov您期望的单个(或自动矢量化或其他),但在 MIPS64r6 之前的 SPARC 或 MIPS 上,或者它将编译为未对齐加载所需的任何指令序列。这种使用memcpy将完全优化支持未对齐负载的目标。

即您的编译器知道目标 ISA 是否支持未对齐的加载,并且会发出它认为合适的 asm。

于 2018-07-26T02:32:12.477 回答