x86上的错误对齐指针

有人提供一个示例是否会因为错位而将指针从一种类型转换为另一种类型失败?

在对这个答案的评论中,两者都表示做了类似的事情

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

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

我在GDB中通过set $ps |= (1<<18)设置了alignment-check标志后试图产生错误条件,但没有任何反应。

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


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

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

 #include  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上提到了失败……

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

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

  1. 您可能不会将此视为x86问题,但SSE操作可从对齐中受益。 对齐数据可用作存储器源操作数以保存指令。 在Nehalem之前,诸如movups类的未对齐加载指令比微架构上的movups速度慢,但在Nehalem及更高版本(以及AMD Bulldozer-family)上,未对齐的16字节加载/存储与未对齐的8字节加载/存储一样有效; 如果数据恰好在运行时对齐或者没有跨越缓存行边界,则单个uop并且没有任何惩罚,否则高效的硬件支持缓存行分割。 4k分裂非常昂贵(约100个周期)直到Skylake(低至~10个周期,如高速缓存线分割)。 有关详细信息,请参阅x86标记wiki中的https://agner.org/optimize/和性能链接。

  2. 互锁操作(如lock add [mem], eax )如果没有充分对齐则非常慢,特别是如果它们跨越高速缓存行边界,那么它们不能只在CPU内核中使用高速缓存锁定。 在较旧的(有缺陷的)SMP系统上,它们实际上可能无法成为primefaces(参见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开始)能够在执行未对齐的访问时导致exception。

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

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

这对我来说是新闻。

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

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

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

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

HTH

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

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

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

 #include  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 : mov 0x8048510,%eax 0x0804840b : mov 0x8048514,%edx 0x08048411 : mov %eax,-0x10(%ebp) 0x08048414 : mov %edx,-0xc(%ebp) 0x08048417 : movl $0x34333231,-0x19(%ebp) <== BAM! SIGBUS 0x0804841e : movl $0x38373635,-0x15(%ebp) 0x08048425 : 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)); 

要享受exception,请使用SEM_NOALIGNMENTFAULTEXCEPT调用SetErrorMode

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

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

自动向量化时的gcc假定uint16_t*与2字节边界对齐。 如果你违反了这个假设,你可以得到一个段错误: 为什么对mmap的内存的未对齐访问有时会在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 (或自动向量化或其他),但在MIPS64r6之前的SPARC或MIPS或其将编译为未对齐加载所需的任何指令序列的任何内容。 memcpy这种使用将完全取决于支持未对齐加载的目标。

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