2017-02-18 28 views
11

У меня есть следующая программа, которая включает бит проверки выравнивания (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.

Так что я имею два вопроса:

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

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

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

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

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

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

+0

Вы должны скомпилировать с 'gcc -m32 -fverbose-asm -Wall -O -S test3.c' и посмотреть в сгенерированный' test3.s'. Вы можете использовать инструкцию 'volatile asm' –

+0

Basile Я тоже пытался это сделать, но сгенерированный код в основном идентичен, включая смещенный доступ. – antirez

+2

«Я не могу ожидать, что компилятор x86 всегда будет генерировать согласованные обращения, даже если скомпилированный код на самом деле никогда не пытается выполнить неприсоединившиеся обращения». Это правда. Скорее всего, вы обнаружите, что 'memcpy' всегда будет генерировать неприсоединенные обращения при передаче источника и адресата, которые не выровнены одинаково. Он выровнят источник и опустит место назначения, но все равно использует 'rep mov' с операндом' dword' . –

ответ

1

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

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

4

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

+1

Даже с -O2 создается тот же код. И наоборот, даже без оптимизации, но при удалении 'volatile' генерируется обычный код. Я думаю, что есть что-то в стандарте C о доступе изменчивых переменных, которые диктуют этот шаблон доступа. Я буду исследовать больше, но так как переключение оптимизации не изменяет эту часть сгенерированного ассемблера, мое предположение заключается в том, что ваш ответ здесь не применим. – antirez