下面的程序允许x86处理器标志寄存器中的对齐检查(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 = 0xaa;
return 0;
}如果我编译它,最初生成的代码会做一些显而易见的事情,比如设置堆栈和通过将值1、2、3、4、5、6移动到堆栈上来创建字符数组:
/tmp ➤ gcc test3.c -m32
/tmp ➤ gdb ./a.out
(gdb) disassemble main
0x0804843d <+0>: push %ebp
0x0804843e <+1>: mov %esp,%ebp
0x08048440 <+3>: and $0xfffffff0,%esp
0x08048443 <+6>: sub $0x20,%esp
0x08048446 <+9>: mov %gs:0x14,%eax
0x0804844c <+15>: mov %eax,0x1c(%esp)
0x08048450 <+19>: xor %eax,%eax
0x08048452 <+21>: pushf
0x08048453 <+22>: orl $0x40000,(%esp)
0x0804845a <+29>: popf
0x0804845b <+30>: movb $0x1,0x16(%esp)
0x08048460 <+35>: movb $0x2,0x17(%esp)
0x08048465 <+40>: movb $0x3,0x18(%esp)
0x0804846a <+45>: movb $0x4,0x19(%esp)
0x0804846f <+50>: movb $0x5,0x1a(%esp)
0x08048474 <+55>: movb $0x6,0x1b(%esp)
0x08048479 <+60>: mov 0x16(%esp),%eax
0x0804847d <+64>: mov %eax,0x10(%esp)
0x08048481 <+68>: movzwl 0x1a(%esp),%eax
0x08048486 <+73>: mov %ax,0x14(%esp)
0x0804848b <+78>: movl $0xaa,0xc(%esp)
0x08048493 <+86>: mov $0x0,%eax
0x08048498 <+91>: mov 0x1c(%esp),%edx
0x0804849c <+95>: xor %gs:0x14,%edx
0x080484a3 <+102>: je 0x80484aa <main+109>
0x080484a5 <+104>: call 0x8048310 <__stack_chk_fail@plt>
0x080484aa <+109>: leave
0x080484ab <+110>: ret然而,在main+60中,它做了一些奇怪的事情:它将6个字节的数组移动到堆栈的另一部分:数据一次在寄存器中移动一个4字节的单词。但是字节开始于偏移量0x16,这是不对齐的,所以程序在尝试执行mov时会崩溃。
所以我有两个问题:
volatile会跳过每一个优化,并且总是执行内存访问。可能需要始终作为整个单词访问易失性vars,所以编译器总是使用临时寄存器来读取/写入整个单词?mov调用,它为什么不将char数组放在对齐的地址上呢?我知道x86通常对非对齐访问是安全的,而在现代处理器上它甚至不会带来性能上的损失;然而,在所有其他情况下,我看到编译器试图避免生成未对齐的访问,因为AFAIK是C中的一种未指定的行为。我的猜测是,由于后来它为堆栈上复制的数组提供了一个正确的对齐指针,所以它只是不关心只用于初始化的数据的对齐方式,因为C程序是看不见的?如果上述假设是正确的,这意味着我不能期望x86编译器总是生成对齐访问,即使编译后的代码从未尝试执行非对齐访问,因此设置AC标志并不是检测执行未对齐访问的部分代码的实用方法。
编辑:经过进一步的研究,我可以自己回答大部分问题。为了取得进展,我在Redis中添加了一个选项,以设置AC标志并以其他方式正常运行。我发现这种方法是不可行的:进程立即崩溃在libc:__mempcpy_sse2 () at ../sysdeps/x86_64/memcpy.S:83中。我假设整个x86软件栈根本不关心不对齐,因为这个体系结构很好地处理了它。因此,在设置AC标志时运行是不实际的。
所以上面问题2的答案是,像软件栈的其他部分一样,编译器可以随意地做它想做的事情,并且可以在堆栈上重新定位,而不关心对齐问题,只要行为从C程序的角度来看是正确的。
唯一要回答的问题是,为什么使用volatile是在堆栈的另一个部分中复制的?我的最佳猜测是编译器试图访问声明为volatile的变量中的全部单词,即使在初始化过程中也是如此(假设这个地址被映射到I/O端口),但我不确定。
发布于 2017-02-21 02:08:00
编译器在一个工作存储区域中填充数组,每次一个字节,这不是原子的。然后,它使用原子性 MOVZ指令将整个数组移动到其最后的休息位置(当目标地址为自然排列时,原子性是隐式的)。
写入必须是原子的,因为编译器必须假设(由于volatile关键字)数组可以由任何人在任何时候访问。
发布于 2017-02-18 23:03:19
编译不需要优化,所以编译器可以直接生成代码,而不用担心它的效率有多低。因此,它首先在堆栈上的临时空间中创建初始化器{ 1, 2, 3, 4, 5, 6 },然后将其复制到为foo分配的空间中。
https://stackoverflow.com/questions/42317329
复制相似问题