Почему заполнение в Си действительно для переменных / структур, размещенных в стеке? - PullRequest
3 голосов
/ 09 мая 2019

Я читаю о заполнении структуры в C здесь: http://www.catb.org/esr/structure-packing/.
Я не понимаю, почему заполнение, определенное во время компиляции для переменных / структур , выделенных в стеке , действует семантически во всехслучаев.Позвольте мне привести пример.Скажем, у нас есть этот игрушечный код для компиляции:

int main() {
    int a;
    a = 1;
}

На X86-64 gcc -S -O0 a.c генерирует эту сборку (ненужные символы удалены):

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $1, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

В этом случае почему мызнаете, что значение %rbp и, следовательно, %rbp-4 выровнено по 4, что подходит для хранения / загрузки int?

Давайте попробуем тот же пример со структурами.

struct st{
    char a;
    int b;
}

Изчитая, я предполагаю, что дополненная версия структуры выглядит примерно так:

struct st{
    char a;      // 1 byte
    char pad[3]; // 3 bytes
    int b;       // 4 bytes
}

Итак, второй пример игрушки

int main() {
    struct st s;
    s.a = 1;
    s.b = 2;
}

генерирует

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movb    $1, -8(%rbp)
    movl    $2, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

И мы наблюдаем, чтоэто действительно так.Но опять же, какова гарантия того, что само значение rbp в произвольном кадре стека будет правильно выровнено?Разве значение rbp не доступно только во время выполнения?Как компилятор может выровнять членов структуры, если ничего не известно о выравнивании начального адреса структуры во время компиляции?

Ответы [ 2 ]

6 голосов
/ 09 мая 2019

Как указывает @P__J__ (в уже удаленном ответе) - то, как компилятор C генерирует код, является подробностью реализации.Поскольку вы пометили это как вопрос ABI, ваш реальный вопрос: «Когда GCC ориентируется на Linux, как можно допустить, что RSP имеет какое-либо конкретное минимальное выравнивание?».В Linux используется 64-разрядный интерфейс ABI - AMD64 (x86-64) System V ABI .Минимальное выравнивание стека за до CALL с ABI-совместимой функцией 1,2 (включая main) гарантированно равно из 16 байтов (это может быть 32 байта или 64 байта в зависимости от типов, передаваемых в функцию).ABI сообщает:

3.2.2 Кадр стека

В дополнение к регистрам каждая функция имеет кадр в стеке времени выполнения.Этот стек растет вниз от высоких адресов.На рисунке 3.3 показана организация стека. Конец области входного аргумента должен быть выровнен по 16 (32 или 64, если в стеке передается __m256 или __m512) граница байта .Другими словами, значение (% rsp + 8) всегда кратно 16 (32 или 64) , когда управление передается в точку входа в функцию. Указатель стека,%rsp, всегда указывает на конец последнего выделенного кадра стека.

Вы можете спросить, почему упоминание RSP + 8 кратно 16 (а не RSP+ 0 ).Это связано с тем, что концепция CALL в функции подразумевает, что 8-байтовый адрес возврата будет помещен в стек самой инструкцией CALL .Независимо от того, вызывается ли функция или к ней переходят (т. Е. хвостовой вызов ), генератор кода всегда предполагает, что непосредственно перед выполнением первой инструкции в функции стек всегда смещается на 8. Имеется автоматическая гарантияхотя этот стек будет выровнен по 8-байтовой границе.Если вы вычтете 8 из RSP , вы гарантированно снова выровняетесь на 16 байт.

Следует отметить, что приведенный ниже код гарантирует, что после PUSHQ стек будет выровнен по16-байтовая граница, поскольку инструкция PUSH уменьшает RSP на 8 и снова выравнивает стек по 16-байтовой границе:

main:
                             # <------ Stack pointer (RSP) misaligned by 8 bytes
    pushq   %rbp
                             # <------ Stack pointer (RSP) aligned to 16 byte boundary
    movq    %rsp, %rbp
    movb    $1, -8(%rbp)
    movl    $2, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

Для 64-битного кода вывод одинИз всего этого можно сделать вывод, что хотя фактическое значение указателя стека известно во время выполнения, ABI позволяет сделать вывод, что значение при входе в функцию имеет определенное выравнивание, и система генерации кода компилятора может использовать это дляего преимущество при размещении struct в стеке.


Когда выравнивания стека функции недостаточно для выравнивания переменной?

Логический вопрос - если выравнивание стекачто может быть гарантировано при входе в функцию, недостаточно для выравнивания структуры или типа данных, помещенных в стек, что делает компилятор GCCилер делать?Рассмотрим следующую версию вашей программы:

struct st{
    char a;      // 1 byte
    char pad[3]; // 3 bytes
    int b;       // 4 bytes
};

int main() {
    struct st s __attribute__(( aligned(32)));
    s.a = 1;
    s.b = 2;
}

Мы сказали GCC, что переменная s должна быть выровнена на 32 байта.Функция, которая может гарантировать выравнивание стека 16 байтов, не гарантирует выравнивание 32 байтов (выравнивание 32 байтов гарантирует выравнивание 16 байтов, поскольку 32 делится на 16).Компилятор GCC должен будет сгенерировать пролог функции, чтобы s мог быть правильно выровнен.Вы можете посмотреть на неоптимизированный вывод Godbolt для этой программы , чтобы увидеть, как GCC достигает этого:

main:
        pushq   %rbp
        movq    %rsp, %rbp
        andq    $-32, %rsp    # ANDing RSP with -32 (0xFFFFFFFFFFFFFFE0) 
                              # rounds RSP down to next 32 byte boundary
                              # by zeroing the lower 5 bits of RSP.
        movb    $1, -32(%rsp) 
        movl    $2, -28(%rsp)
        movl    $0, %eax
        leave
        ret

Сноски

  • 1 AMD64 System V ABI также используется 64-разрядными ОС Solaris, MacOS и BSD, а также Linux
  • 2 64-разрядное соглашение о вызовах Microsoft Windows(ABI) гарантирует, что до вызова функции стек выровнен на 16 байтов (8 байтов смещены непосредственно перед первой инструкцией выполняемой функции).
4 голосов
/ 09 мая 2019

В этом случае, почему мы знаем, что значение% rbp и, следовательно,% rbp-4 выровнено 4, чтобы подходить для хранения / загрузки int?

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

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

Если мы не принимаем вызов из отдельного домена безопасности (например, ядро ​​получает системный вызов из пространства пользователя), мы просто доверяем вызывающей стороне. Как функция strcmp знает, что ее аргументы указывают на допустимые строки с нулевым символом в конце? Это доверяет звонящему. То же самое.

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

Как компилятор может выровнять членов структуры, если ничего не известно о выравнивании начального адреса структуры во время компиляции?

Члены struct получают смещения в предположении, что базовый адрес времени выполнения объекта будет соответствующим образом выровнен даже для самого строго выровненного члена структуры. Вот почему первый член структуры просто помещается с нулевым смещением, независимо от его типа.

Время выполнения должно гарантировать, что любой адрес, выделенный для произвольного объекта, имеет строжайшее выравнивание любого стандартного типа, alignof(maxalign_t). Например, если самое строгое выравнивание в системе составляет 16 байтов (как в x86-64 System V), тогда malloc должен выдавать указатели на 16-байтовые выровненные адреса. Тогда любой тип структуры может быть помещен в полученную память.

Если вы напишите свой собственный предположительно универсальный распределитель, который раздает 4-байтовые совмещенные указатели в системе, где выравнивание может быть столь же строгим, как 16, то это неправильно.


(Обратите внимание, что типы __m256 и __m512 не учитываются для maxalign_t: malloc по-прежнему обеспечивает только 16-байтовое выравнивание в x86-64 System V и недостаточно для избыточного выровненные типы, такие как __m256 или пользовательские struct foo { alignas(32) int32_t a[8]; };. Используйте aligned_alloc() для выровненных типов.)

Также обратите внимание, что формулировка в стандарте ISO C гласит, что память, возвращаемая malloc, должна использоваться для любого типа. 4-байтовое распределение в любом случае не может содержать 16-байтовый тип, поэтому небольшие распределения могут быть выровнены менее чем на 16 байтов.

...