x86 上未对齐的指针
Posted
技术标签:
【中文标题】x86 上未对齐的指针【英文标题】:Mis-aligned pointers on x86 【发布时间】:2010-10-07 14:00:44 【问题描述】:有人可以提供一个示例,由于未对齐而将指针从一种类型转换为另一种类型失败吗?
在this answer 的 cmets 中,bothie 表示正在做类似的事情
char * foo = ...;
int bar = *(int *)foo;
如果启用对齐检查,即使在 x86 上也可能会导致错误。
我尝试在 GDB 中通过set $ps |= (1<<18)
设置对齐检查标志后产生错误条件,但什么也没发生。
工作(即不工作;))示例是什么样的?
答案中的所有代码 sn-ps 在我的系统上都没有失败 - 我稍后会在不同的编译器版本和不同的电脑上尝试它。
顺便说一句,我自己的测试代码是这样的(现在也使用 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), %edx
和 movl $0xf1f2f3f4,-0x17(%ebp)
,但断言顺利通过。
那么设置AC
是否会触发SIGBUS
?我无法在 Windows XP 下的英特尔双核笔记本电脑上使用我测试的任何 GCC 版本(MinGW-3.4.5、MinGW-4.3.0、Cygwin-3.4.4),而 codelogic 和 Jonathan Leffler提到 x86 上的失败...
【问题讨论】:
该代码将崩溃并烧毁 x86 系统(SPARC、PPC,可能是 IA64,除非将其设置为容错)。 不是编译器或操作系统,而是 x86 - 它非常容忍这个错误。查看迈克尔·伯尔的回答 @Mark:那么其他人是否认为会出现错误? Christoph,您检查过反汇编并尝试逐步完成吗? @codelogic:是的,我有 - 未对齐的访问肯定存在:mov -0x17(%ebp), %edx
【参考方案1】:
在 x86 上未对齐的访问会导致问题的情况并不常见(除了内存访问需要更长的时间)。以下是我听说过的一些:
您可能不会将此视为 x86 问题,但 SSE 操作受益于对齐。对齐的数据可以用作内存源操作数来保存指令。在 Nehalem 之前的微架构上,像 movups
这样的未对齐加载指令比 movaps
慢,但在 Nehalem 及更高版本(以及 AMD Bulldozer 系列)上,未对齐的 16 字节加载/存储与未对齐的 8 字节加载/存储效率差不多商店;如果数据恰好在运行时对齐或没有跨越缓存线边界,则单个 uop 并且根本没有任何惩罚,否则对缓存线拆分的有效硬件支持。在 Skylake 之前,4k 拆分非常昂贵(约 100 个周期)(像缓存行拆分一样低至约 10 个周期)。有关详细信息,请参阅 https://agner.org/optimize/ 和 x86 tag wiki 中的性能链接。
如果没有充分对齐,则联锁操作(如 lock add [mem], eax
)会非常缓慢,尤其是当它们跨越缓存行边界时,它们不能只使用缓存-锁定在 CPU 内核内部。在较旧的(有缺陷的)SMP 系统上,它们可能实际上不是原子的(参见 https://blogs.msdn.com/oldnewthing/archive/2004/08/30/222631.aspx)。
Raymond Chen 讨论的另一种可能性是在处理具有硬件存储内存的设备时(诚然是一种奇怪的情况)-https://blogs.msdn.com/oldnewthing/archive/2004/08/27/221486.aspx
我记得(但没有参考 - 所以我不确定这个)与跨页边界的未对齐访问的类似问题也涉及页面错误。我会看看我是否可以为此挖掘参考。
在研究这个问题时我学到了一些新东西(我想知道在几个地方提到的“$ps |= (1<<18)
”GDB 命令)。我没有意识到 x86 CPU(似乎从 486 开始)能够在执行未对齐访问时引发异常。
来自 Jeffery Richter 的“Programming Applications for Windows,第 4 版”:
让我们仔细看看 x86 CPU 如何处理数据对齐。 x86 CPU 在其 EFLAGS 寄存器中包含一个特殊的位标志,称为 AC(对齐检查)标志。默认情况下,当 CPU 首次通电时,此标志设置为零。当此标志为零时,CPU 会自动执行任何操作以成功访问未对齐的数据值。但是,如果该标志设置为 1,则只要尝试访问未对齐的数据,CPU 就会发出 INT 17H 中断。 Windows 2000 和 Windows 98 的 x86 版本从不更改此 CPU 标志位。因此,当应用程序在 x86 处理器上运行时,您永远不会看到应用程序发生数据未对齐异常。
这对我来说是个新闻。
当然,未对齐访问的最大问题是,当您最终为非 x86/x64 处理器编译代码时,您最终不得不追踪并修复一大堆东西,因为几乎所有其他 32-位或更大的处理器对对齐问题很敏感。
【讨论】:
“SSE 操作必须处理对齐的数据”不一定是真的。在最近的 Intel CPU(我认为是 Penryn 和更新版本)上,“对齐”和“未对齐”的 SSE 操作实际上做同样的事情,如果访问未对齐,则速度会慢一些。 是的,在 Penryn 之前的 CPU 上,未对齐的 SSE 读取会降低性能。据说在 Penryn 上,虽然(我没有一个要基准测试),CPU 将只执行两次对齐读取并使用 Penryn“超级随机播放引擎”将它们重新组合成请求的未对齐读取,所以它们不是这样慢。 我只是对它进行了测试——Penryn 未对齐 SSE 操作的延迟仍然是对齐读取的大约三倍,尽管它们的吞吐量比以前的内核要好。这与您描述的行为一致(以及 VMX 处理它的方式,例如 load load shuffle)。 只是好奇 - 不知道 Penryn 之前的热门歌曲是什么?胡思乱想对我来说很好,因为我只是好奇。 对“联锁操作必须对对齐的数据进行操作以确保它们在多处理器系统上是原子的”的小修正。联锁操作将适用于 X86 上的未对齐数据,它们只是碰巧有 MUCH 慢但您的代码不应该崩溃的边缘情况。 FWIW,您也不必对某些 PowerPC 上的联锁操作使用完全对齐(例如,Microsoft 制造的某个游戏系统将处理仅 32 位对齐的 64 位联锁就好了)。【参考方案2】:如果您阅读 Core I7 架构(特别是他们的优化文献),英特尔实际上已经在其中放置了大量硬件,以使未对齐的内存访问几乎免费。据我所知,只有跨越缓存线边界的错位才会有任何额外的成本——即使如此,它也是最小的。据我所知(虽然已经有一段时间了),AMD 在访问不对齐(周期方面)方面也几乎没有问题。
对于它的价值,我确实在 eflags(AC 位 - 对齐检查)中设置了该标志,当时我正忙于优化我正在处理的项目。事实证明,windows 充满了未对齐的访问 - 如此之多,以至于我无法在我们的代码中找到任何未对齐的内存访问,我被库和 windows 代码中如此多的未对齐访问轰炸了,我没有时间去继续。
也许我们可以了解到,当 CPU 使事情变得免费或成本非常低时,程序员会变得自满并做一些额外开销的事情。也许英特尔的工程师做了一些调查,发现典型的 x86 桌面软件每秒执行数百万次未对齐访问,因此他们在 CoreI7 中放置了速度极快的未对齐访问硬件。
HTH
【讨论】:
【参考方案3】:EFLAGS.AC 实际生效还有一个未提及的附加条件。必须设置 CR0.AM 以防止 INT 17h 在 486 之前没有此异常处理程序的旧操作系统上跳闸。可惜windows默认没有设置,需要自己写内核驱动来设置。
【讨论】:
我认为不需要内核模式驱动程序。也许我读错了回复。详情请见Windows Data Alignment on IPF, x86, and x64。【参考方案4】:char *foo 可能与 int 边界对齐。 试试这个:
int bar = *(int *)(foo + 1);
【讨论】:
这正是我所做的——你检查了吗?发生了什么不应该发生的事情?【参考方案5】:char *foo = "....";
foo++;
int *bar = (int *)foo;
编译器会将 foo 放在字边界上,然后当您将其递增时,它位于字+1,这对于 int 指针无效。
【讨论】:
看来Core2Duo在64位模式下不会产生这个错误。 在 x86 上,只要您没有设置标志以对未对齐的数据提供异常,访问未对齐的数据是完全有效的。它只是可能更慢。【参考方案6】:#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 的问题?
您可以使用此 sn-p 在代码中启用对齐检查位:
/* 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));
【讨论】:
我无法使用 gcc (GCC) 3.4.5(mingw-vista special r3)重现此问题,稍后我将尝试使用其他版本... 这是使用 gcc 4.3.2 在 Intel Core 2 Duo 上运行内核 2.6.27 编译的。 此外,如果不设置您提到的标志,它不会抛出 SIGBUS。 它可能失败的原因是它试图从 2 字节 char[] 中取消引用一个 int,尽管就像我说的那样,当未设置标志时它不会发生。 也许这是一个奇怪的 windows 问题 - gcc3.4.5 和 gcc4.3.0 都不会为我生成失败的代码......【参考方案7】:要享受例外,请致电SetErrorMode
和SEM_NOALIGNMENTFAULTEXCEPT
:
int main(int argc, char* argv[])
SetErrorMode(GetErrorMode() | SEM_NOALIGNMENTFAULTEXCEPT);
...
详情请见Windows Data Alignment on IPF, x86, and x64。
【讨论】:
【参考方案8】:自动矢量化时的 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
(或自动矢量化或其他),但在 MIPS64r6 之前的 SPARC 或 MIPS 上,或者它将编译为未对齐加载所需的任何指令序列。 memcpy
的这种使用将完全优化支持未对齐负载的目标。
即您的编译器知道目标 ISA 是否支持未对齐的加载,并且会发出它认为合适的 asm。
【讨论】:
here's a case 您甚至不需要矢量化来使未对齐的指针失败。很有意思。我不知道这个问题是否存在规范问题(人们在为 x86 编写 C/C++ 时认为未对齐的问题是可以的,因为那里大多允许未对齐),所以我把它留在这里。 @BeeOnRope:谢谢,用这些博客链接更新了Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?。这是我在出现时总是链接的规范,包括在这个答案中。以上是关于x86 上未对齐的指针的主要内容,如果未能解决你的问题,请参考以下文章
分配粒度和内存页面大小(x86处理器平台的分配粒度是64K,内存页是4K,所以section都是0x1000对齐,硬盘扇区大小是512字节,所以PE文件默认文件对齐是0x200)