Может ли компилятор оптимизировать использование стековой памяти путем изменения порядка локальных переменных?
Рассмотрим следующую программу:
#include <stdio.h>
void some_func(char*, int*, char*);
void stack_alignment(void) {
char a = '-';
int i = 1337;
char b = '+';
some_func(&a, &i, &b); // to prevent the compiler from removing the local variables
printf("%c|%i|%c", a, i, b);
}
Он генерирует следующую сборку (комментарии добавлены мной, я полный новичок в сборке):
$ vim stack-alignment.c
$ gcc -c -S -O3 stack-alignment.c
$ cat stack-alignment.s
.file "stack-alignment.c"
.section .rdata,"dr"
LC0:
.ascii "%c|%i|%c"
.text
.p2align 2,,3
.globl _stack_alignment
.def _stack_alignment; .scl 2; .type 32; .endef
_stack_alignment:
LFB7:
.cfi_startproc
subl $44, %esp
.cfi_def_cfa_offset 48
movb $45, 26(%esp) // local variable 'a'
movl $1337, 28(%esp) // local variable 'i'
movb $43, 27(%esp) // local variable 'b'
leal 27(%esp), %eax
movl %eax, 8(%esp)
leal 28(%esp), %eax
movl %eax, 4(%esp)
leal 26(%esp), %eax
movl %eax, (%esp)
call _some_func
movsbl 27(%esp), %eax
movl %eax, 12(%esp)
movl 28(%esp), %eax
movl %eax, 8(%esp)
movsbl 26(%esp), %eax
movl %eax, 4(%esp)
movl $LC0, (%esp)
call _printf
addl $44, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
LFE7:
.def _some_func; .scl 2; .type 32; .endef
.def _printf; .scl 2; .type 32; .endef
Как вы можете видеть, есть 3 локальные переменные (a
, i
и b
) с размерами 1 байт, 4 байта и 1 байт. Включая заполнение, это будет 12 байт (при условии, что компилятор выравнивает до 4 байт).
Не будет ли более эффективным использование памяти, если компилятор изменит порядок переменных на (a
, b
, i
)? Тогда потребуется всего 8 байт.
Вот "графическое" представление:
3 bytes unused 3 bytes unused
vvvvvvvvvvv vvvvvvvvvvv
+---+---+---+---+---+---+---+---+---+---+---+---+
| a | | | | i | b | | | |
+---+---+---+---+---+---+---+---+---+---+---+---+
|
v
+---+---+---+---+---+---+---+---+
| a | b | | | i |
+---+---+---+---+---+---+---+---+
^^^^^^^
2 bytes unused
Разрешается ли компилятору делать эту оптимизацию (по стандарту C и т. д.)?
- если нет (как я думаю, вывод сборки показывает), то почему?
- если да, то почему это не происходит выше?
5 ответов:
Разрешается ли компилятору делать эту оптимизацию (по стандарту C и т. д.)?
Да.
Это действительно произошло.Если да, то почему это не происходит выше?
Внимательно прочитайте выходные данные ассемблера.
movb $45, 26(%esp) // local variable 'a' movl $1337, 28(%esp) // local variable 'i' movb $43, 27(%esp) // local variable 'b'
Переменная
a
имеет смещение 26. Переменнаяb
находится на смещении 27. Переменнаяi
находится на смещении 28.Используя изображения, которые вы сделали макет теперь:
+---+---+---+---+---+---+---+---+ | | | a | b | i | +---+---+---+---+---+---+---+---+ ^^^^^^^ 2 bytes unused
Компилятор может компоновать локальные переменные по своему усмотрению. Ему даже не нужно использовать стек.
Он может хранить локальные переменные в порядке, не связанном с порядком объявления в стеке, если он использует стек.
Позволяет ли компилятор выполнять эту оптимизацию (по стандарту C и т. д.)?
- если да, то почему это не происходит выше?
Ну, это вообще оптимизация?
Это не совсем ясно. Он использует пару байт меньше, но это редко имеет значение. Но на некоторых архитектурах чтение
char
может быть быстрее, если оно хранится выровненным по словам. Таким образом, если поместитьchar
s рядом друг с другом, то один из них, по крайней мере, не будет выровнен по словам и замедлит чтение.
Не будет ли более эффективным использование памяти, если компилятор изменит порядок переменных
Невозможно сказать, не говоря о конкретном процессоре, конкретной операционной системе и конкретном компиляторе. В общем, компилятор работает оптимально. Для того чтобы оптимизировать код осмысленным образом, необходимы глубокие знания о конкретной системе.
В вашем случае компилятор, скорее всего, настроен на оптимизацию скорости в этом случае. Похоже, что компилятор уже решил это выравнивание адресов для каждой переменной дает наиболее эффективный код. В некоторых системах это не только быстрее, но и обязательно распределять по четным адресам, потому что некоторые процессоры могут обрабатывать только выровненный доступ.
Позволяет ли компилятор выполнять эту оптимизацию (по стандарту C и т. д.)?
Да, стандарт C даже не требует выделения переменных. Компилятор совершенно свободен в обращении с этим любым способом, который он хочет, и ему не нужно документировать, как или почему. Он может распределять переменные в любом месте, он может полностью оптимизировать их, или распределять их внутри регистров процессора, или в стеке, или в маленькой деревянной коробке под вашим столом.
Как правило, в нормальных системах, где скорость имеет значение, чтение слово мудрый быстрее, чем чтение символ мудрый. Потеря памяти по сравнению с увеличением скорости игнорируется. Но в случае системы, где важна память, например, в различных кросс-компиляторах, которые генерируют исполняемый файл (в очень общем смысле) для конкретной целевой платформы, картина может быть совершенно иной. Компилятор может упаковать их вместе, даже проверить их время жизни и использования, в зависимости от того, что уменьшит разрядность и т. д. Так в основном это сильно зависит от необходимости. Но в целом каждый компилятор дает вам гибкость, если вы хотите
"pack"
их плотно. Вы можете посмотреть в руководстве для этого
Компиляторы с защитой от переполнения буфера для стека (
/GS
для компилятора Microsoft) могут изменять порядок переменных в качестве функции безопасности. Например, если локальными переменными являются некоторый массив символов постоянного размера (буфер) и указатель функции, злоумышленник, который может переполнить буфер, может также перезаписать указатель функции. Таким образом, локальные переменные переупорядочиваются таким образом, что буфер находится рядом с канарейкой. Таким образом, злоумышленник не может (напрямую) скомпрометировать указатель функции и переполнение буфера (надеюсь) обнаружено уничтоженной канарейкой.Предупреждение: такие функции не препятствуют компромиссу, они просто поднимают барьеры для атакующего, но опытный атакующий обычно находит обходной путь.