为什么编译器在初始化volatile数组时会生成这样的代码?

ant*_*rez 11 c assembly compilation

我有以下程序启用x86处理器标志寄存器中的对齐检查(AC)位,以捕获未对齐的内存访问.然后程序声明两个volatile变量:

#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;
}
Run Code Online (Sandbox Code Playgroud)

如果我编译它,最初生成的代码会做一些显而易见的事情,比如设置堆栈并通过将值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
Run Code Online (Sandbox Code Playgroud)

然而,main+60它做了一些奇怪的事情:它将6个字节的数组移动到堆栈的另一部分:数据在寄存器中一次移动一个4字节字.但是字节从偏移量0x16开始,没有对齐,因此程序在尝试执行时会崩溃mov.

所以我有两个问题:

  1. 为什么编译器发出代码将数组复制到堆栈的另一部分?我假设volatile会跳过每个优化并始终执行内存访问.也许挥发性变量需要始终作为整个单词访问,因此编译器总是使用临时寄存器来读/写整个单词?

  2. 为什么编译器如果稍后打算进行这些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端口),但我不确定.

Joh*_* Wu 1

编译器将数组填充到工作存储区域中,一次一个字节,这不是原子的。然后,它使用原子MOVZ 指令将整个数组移动到其最终静止位置(当目标地址自然对齐时,原子性是隐式的)。

写入必须是原子的,因为编译器必须假设(由于关键字volatile)该数组可以由任何其他人随时访问。