x86上的错误对齐指针

时间:2009-02-14 00:01:06

标签: c pointers alignment casting

有人提供的示例是否会因为未对齐而将指针从一种类型转换为另一种类型失败?

在对this answer的评论中,他们表示做了类似

的事情
char * foo = ...;
int bar = *(int *)foo;
如果启用了对齐检查,

即使在x86上也可能导致错误。

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

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


答案中的代码片段都没有在我的系统上失败 - 我将尝试使用不同的编译器版本,稍后在不同的PC上尝试。

顺便说一句,我自己的测试代码看起来像这样(现在也使用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上提到失败......

8 个答案:

答案 0 :(得分:21)

这种情况并不常见,因为未对齐的访问会导致x86出现问题(除了让内存访问需要更长的时间)。以下是我听过的一些内容:

  1. 您可能不会将此视为x86问题,但SSE操作会从对齐中受益。对齐数据可用作存储器源操作数以保存指令。在Nehalem之前,movups之类的未对齐加载指令比微架构慢movaps,但在Nehalem及更高版本(以及AMD Bulldozer-family)上,未对齐的16字节加载/存储与未对齐8的效率相同 - 字节加载/存储;如果数据恰好在运行时对齐或者没有跨越缓存行边界,则单个uop并且没有任何惩罚,否则高效的硬件支持缓存行分割。 4k分裂非常昂贵(约100个周期),直到Skylake(低至~10个周期,如高速缓存线分割)。有关详细信息,请参阅https://agner.org/optimize/中的x86 tag 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. 我记得(但没有参考 - 所以我不确定这个)类似的问题与未对齐的访问跨越页面边界,也涉及页面错误。我会看看我是否可以为此挖掘参考资料。

  5. 我在研究这个问题时学到了一些新东西(我想知道几个地方提到的“$ps |= (1<<18)”GDB命令)。我没有意识到x86 CPU(从486开始)可以在执行错位访问时导致异常。

    来自Jeffery Richter的“Windows编程应用程序,第4版”:

      

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

    这对我来说是新闻。

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

答案 1 :(得分:8)

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

为了它的价值,我确实在eflags(AC位 - 对齐检查)中设置了那个标志,当我被带走时优化我正在进行的项目。事实证明,窗口是完全未对齐的访问 - 很多,以至于我无法在我们的代码中找到任何未对齐的内存访问,我被库和Windows代码中的这么多未对齐的访问轰炸,我没有时间继续。

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

HTH

答案 2 :(得分:3)

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

int bar = *(int *)(foo + 1);

答案 3 :(得分:3)

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

答案 4 :(得分:2)

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

编译器将foo放在一个字边界上,然后当你递增它时,它是一个字+ 1,这对于一个int指针是无效的。

答案 5 :(得分:2)

#include <stdio.h>

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

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

在gdb中设置set $ps |= (1<<18)之后,这给了我SIGBUS,当地址对齐不正确时(显示其他原因),这显然会被抛出。

编辑:提升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));

答案 6 :(得分:1)

要享受例外情况,请使用SetErrorMode

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

有关详细信息,请参阅Windows Data Alignment on IPF, x86, and x64

答案 7 :(得分:0)

自动向量化时,

gcc假定uint16_t*对齐2字节边界。如果违反此假设,则可能会出现段错误:  Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?

因此即使针对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(或自动向量化之类),但在SPARC或MIPS64r6之前的MIPS上,它将编译为未对齐加载所需的任何指令序列。 memcpy的这种使用将完全优化那些支持未对齐负载的目标。

即您的编译器知道目标ISA是否支持未对齐的负载,并会发出看起来合适的asm。