Может ли компилятор оптимизировать использование стековой памяти путем изменения порядка локальных переменных?


Рассмотрим следующую программу:

#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 7

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 может быть быстрее, если оно хранится выровненным по словам. Таким образом, если поместить chars рядом друг с другом, то один из них, по крайней мере, не будет выровнен по словам и замедлит чтение.

Не будет ли более эффективным использование памяти, если компилятор изменит порядок переменных

Невозможно сказать, не говоря о конкретном процессоре, конкретной операционной системе и конкретном компиляторе. В общем, компилятор работает оптимально. Для того чтобы оптимизировать код осмысленным образом, необходимы глубокие знания о конкретной системе.

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

Позволяет ли компилятор выполнять эту оптимизацию (по стандарту C и т. д.)?

Да, стандарт C даже не требует выделения переменных. Компилятор совершенно свободен в обращении с этим любым способом, который он хочет, и ему не нужно документировать, как или почему. Он может распределять переменные в любом месте, он может полностью оптимизировать их, или распределять их внутри регистров процессора, или в стеке, или в маленькой деревянной коробке под вашим столом.

Как правило, в нормальных системах, где скорость имеет значение, чтение слово мудрый быстрее, чем чтение символ мудрый. Потеря памяти по сравнению с увеличением скорости игнорируется. Но в случае системы, где важна память, например, в различных кросс-компиляторах, которые генерируют исполняемый файл (в очень общем смысле) для конкретной целевой платформы, картина может быть совершенно иной. Компилятор может упаковать их вместе, даже проверить их время жизни и использования, в зависимости от того, что уменьшит разрядность и т. д. Так в основном это сильно зависит от необходимости. Но в целом каждый компилятор дает вам гибкость, если вы хотите "pack" их плотно. Вы можете посмотреть в руководстве для этого

Компиляторы с защитой от переполнения буфера для стека (/GS для компилятора Microsoft) могут изменять порядок переменных в качестве функции безопасности. Например, если локальными переменными являются некоторый массив символов постоянного размера (буфер) и указатель функции, злоумышленник, который может переполнить буфер, может также перезаписать указатель функции. Таким образом, локальные переменные переупорядочиваются таким образом, что буфер находится рядом с канарейкой. Таким образом, злоумышленник не может (напрямую) скомпрометировать указатель функции и переполнение буфера (надеюсь) обнаружено уничтоженной канарейкой.

Предупреждение: такие функции не препятствуют компромиссу, они просто поднимают барьеры для атакующего, но опытный атакующий обычно находит обходной путь.