Почему компилятор генерирует такой код при инициализации энергозависимого массива?

У меня есть следующая программа, которая включает бит проверки выравнивания (AC) в регистре флагов процессора x86, чтобы улавливать неприглаженные обращения к памяти. Затем программа объявляет две изменчивые переменные:

#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 <[email protected]>
   0x080484aa <+109>:   leave
   0x080484ab <+110>:   ret

Однако в main+60 он делает что-то странное: он перемещает массив из 6 байтов в другую часть стека: данные перемещаются по одному 4-байтовому слову за раз в регистры. Но байты начинаются со смещения 0x16, которые не выровнены, поэтому при попытке выполнить mov программа будет аварийно завершена.

Итак, у меня есть два вопроса:

  • Почему компилятор испускает код для копирования массива в другую часть стека? Я предположил, что volatile будет пропускать каждую оптимизацию и всегда выполнять обращения к памяти. Может быть, волатильные вары всегда должны быть доступны как целые слова, и поэтому компилятор всегда будет использовать временные регистры для чтения/записи целых слов?

  • Почему компилятор не помещает массив char в выровненный адрес, если позже он намеревается выполнить эти вызовы mov? Я понимаю, что x86, как правило, безопасен с неравномерным доступом, а на современных процессорах он даже не нести штраф за производительность; однако во всех других случаях я вижу, что компилятор пытается избежать генерации несвязанных доступов, поскольку они считаются AFAIK, неопределенным поведением в C. Я предполагаю, что, поскольку позже он обеспечивает правильно выровненный указатель для скопированного массива в стеке, просто не нужно выравнивать данные, используемые только для инициализации, таким образом, который невидим для программы C?

Если мои гипотезы выше правильны, это означает, что я не могу ожидать, что компилятор x86 всегда будет генерировать согласованные обращения, даже если скомпилированный код никогда не пытается выполнить неприсоединившиеся обращения сам по себе, и поэтому установка флага AC не является практическим способом обнаруживать части кода, в которых выполняются неравномерные обращения.

РЕДАКТИРОВАТЬ: после дальнейших исследований я могу ответить на большинство из них сам. В попытке добиться прогресса я добавил параметр в Redis, чтобы установить флаг AC и в противном случае работать нормально. Я обнаружил, что этот подход нецелесообразен: процесс немедленно сбой внутри libc: __mempcpy_sse2 () at ../sysdeps/x86_64/memcpy.S:83. Я предполагаю, что весь пакет программного обеспечения x86 просто не заботится о несоосности, поскольку эта архитектура очень хорошо обрабатывается. Таким образом, работать с установленным флагом AC нецелесообразно.

Таким образом, ответ на вопрос 2 выше заключается в том, что, как и весь пакет программного обеспечения, компилятор может делать все, что ему нравится, и перемещать вещи в стеке, не заботясь о выравнивании, если поведение правильное из перспектива программы C.

Остается только ответить на вопрос, почему с volatile, это копия, сделанная в другой части стека? Лучше всего предположить, что компилятор пытается получить доступ к целым словам в переменных, объявленных volatile даже во время инициализации (представьте, был ли этот адрес сопоставлен с портом ввода-вывода), но я не уверен.

Ответ 1

Компилятор заполняет массив в рабочей области хранения, по одному байту за раз, что не является атомарным. Затем он перемещает весь массив в конечное место отдыха с помощью команды atomic MOVZ (атомарность неявно, когда целевой адрес с естественным выравниванием).

Запись должна быть атомарной, потому что компилятор должен предполагать (из-за ключевого слова volatile), к которому массив может быть доступен в любой момент кем-либо еще.

Ответ 2

Вы компилируете без оптимизации, поэтому компилятор генерирует прямолинейный код, не беспокоясь о том, насколько он неэффективен. Поэтому он сначала создает инициализатор { 1, 2, 3, 4, 5, 6 } в временном пространстве в стеке, а затем копирует его в пространство, выделенное для foo.