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