Подтвердить что ты не робот

Что означает выравнивание стека?

Я был высокоуровневым кодером, а архитектуры для меня довольно новы, поэтому я решил прочитать учебник по сборке здесь:

http://en.wikibooks.org/wiki/X86_Assembly/Print_Version

Вдалеке к руководству, инструкции о том, как преобразовать Hello World! программа

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}

в эквивалентный код сборки и было создано следующее:

        .text
LC0:
        .ascii "Hello, world!\12\0"
.globl _main
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        andl    $-16, %esp
        movl    $0, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $LC0, (%esp)
        call    _printf
        movl    $0, %eax
        leave
        ret

Для одной из строк

andl    $-16, %esp

объяснение было:

Этот код "и" s ESP с 0xFFFFFFF0, выравнивание стека со следующей самая низкая 16-байтовая граница. изучение исходного кода Mingw показывает, что это может быть для SIMD инструкции, появляющиеся в "_main" рутины, которые работают только на выровненных адреса. Поскольку наша процедура не содержат инструкции SIMD, эта строка не требуется.

Я не понимаю этого. Может ли кто-нибудь дать мне объяснение, что означает выравнивание стека со следующей 16-байтной границей и почему это требуется? И как достигается andl?

4b9b3361

Ответ 1

Предположим, что стек выглядит следующим образом: _main (адрес указателя стека - пример):

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230

Нажмите %ebp и вычтите 8 из %esp, чтобы зарезервировать некоторое пространство для локальных переменных:

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230
|      %ebp       |
+-----------------+  <--- 0xbfff122c
:    reserved     :
:     space       :
+-----------------+  <--- 0xbfff1224

Теперь команда andl обнуляет низкие 4 бита %esp, что может уменьшить ее; в этом конкретном примере это приводит к резервированию дополнительных 4 байтов:

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230
|      %ebp       |
+-----------------+  <--- 0xbfff122c
:    reserved     :
:     space       :
+ - - - - - - - - +  <--- 0xbfff1224
:   extra space   :
+-----------------+  <--- 0xbfff1220

Дело в том, что есть некоторые инструкции "SIMD" (Single Instruction, Multiple Data) (также известные в x86-land как "SSE" для "Streaming SIMD Extensions" ), которые могут выполнять параллельные операции над несколькими словами в памяти, но требуют, чтобы эти несколько слов были блоком, начинающимся с адреса, который кратен 16 байтам.

В общем случае компилятор не может предположить, что конкретные смещения из %esp приведут к соответствующему адресу (поскольку состояние %esp при входе в функцию зависит от вызывающего кода). Но, преднамеренно выровняв указатель стека таким образом, компилятор знает, что добавление любого кратного 16 байтов в указатель стека приведет к выровненному 16-байтовому адресу, который безопасен для использования с этими инструкциями SIMD.

Ответ 2

Это не похоже на специфику стека, но выравнивание в целом. Возможно, подумайте о терминах integer multiple.

Если у вас есть элементы в памяти, размер которых равен байту, единицам 1, то можно просто сказать, что все они выровнены. Вещи размером два байта, затем целые числа 2 будут выровнены, 0, 2, 4, 6, 8 и т.д. И нецелые кратные, 1, 3, 5, 7 не будут выровнены. Элементы размером 4 байта, целые кратные 0, 4, 8, 12 и т.д. Выровнены, 1,2,3,5,6,7 и т.д. Нет. То же самое касается 8, 0,8,16,24 и 16 16,32,48,64 и т.д.

Это означает, что вы можете посмотреть базовый адрес для элемента и определить, выровнено ли оно.

size in bytes, address in the form of 
1, xxxxxxx
2, xxxxxx0
4, xxxxx00
8, xxxx000
16,xxx0000
32,xx00000
64,x000000
and so on

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

Скажем, например, у вас есть два 8 байтовых элемента в стеке, 16 общих байтов, и вы действительно хотите, чтобы они были выровнены (на 8 байтовых границах). При входе функция вычитала бы 16 из указателя стека, как обычно, чтобы освободить место для этих двух элементов. Но для их согласования потребуется больше кода. Если бы мы хотели, чтобы эти два 8-байтовых элемента были выровнены по 8-байтным границам, а указатель стека после вычитания 16 был 0xFF82, ну, нижние 3 бита не равны 0, поэтому он не выровнен. Нижние три бита - 0b010. В общем смысле мы хотим вычесть 2 из 0xFF82, чтобы получить 0xFF80. Как мы определяем, что это будет 2, мы будем использовать 0b111 (0x7) и вычитаем эту сумму. Это означает, что операции alu и a и вычесть. Но мы можем взять ярлык, если мы и с теми, которые дополняют значение 0x7 (~ 0x7 = 0xFFFF... FFF8), получаем 0xFF80 с использованием одной операции alu (пока у компилятора и процессора есть один способ opcode для этого, если нет, это может стоить вам больше, чем & и вычесть).

Похоже, это ваша программа. Anding с -16 - это то же самое, что и с 0xFFFF.... FFF0, в результате получается адрес, выровненный по 16-байтовой границе.

Итак, чтобы обернуть это, если у вас есть что-то вроде типичного указателя стека, который работает по пути вниз по памяти с более высоких адресов на более низкие адреса, то вы хотите

 
sp = sp & (~(n-1))

где n - количество байтов, которые нужно выровнять (должны быть полномочия, но это нормально, большинство выравниваний обычно включает в себя полномочия двух). Если вы сказали, что сделали malloc (адреса увеличены с низкого до высокого) и хотите выровнять адрес чего-то (помните, чтобы malloc больше, чем вам нужно, по крайней мере, размер выравнивания), затем

if(ptr&(~(n-)) { ptr = (ptr+n)&(~(n-1)); }

Или, если вы хотите просто взять if, там и каждый раз выполнять добавление и маску.

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

Ответ 3

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

То есть, если вам требуется, например, 64-битное выравнивание для указателя, то вы можете концептуально разделить всю адресуемую память на 64-битные куски, начиная с нуля. Адрес будет "выровнен", если он точно впишется в один из этих фрагментов и не будет выровнен, если он примет участие в одном фрагменте и части другого.

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

Ответ 4

Представьте себе этот "рисунок"

addresses
 xxx0123456789abcdef01234567 ...
    [------][------][------] ...
registers

Значения в адресах, кратных 8 "слайду", легко в (64-разрядные) регистры

addresses
         56789abc ...
    [------][------][------] ...
registers

Конечно, регистрируется "прогулка" с шагом 8 байт.

Теперь, если вы хотите поместить значение в адрес xxx5 в регистр намного сложнее: -)


Изменить andl -16

-16 - 11111111111111111111111111110000 в двоичном формате

когда вы "и" что-либо с -16, вы получаете значение с последними 4 битами, установленными в 0... или многоточием из 16.

Ответ 5

Он должен быть только с четными адресами, а не с нечетными, потому что есть дефицит производительности, доступ к ним.

Ответ 6

Когда процессор загружает данные из памяти в регистр, ему необходимо получить доступ по базовому адресу и размеру. Например, он будет извлекать 4 байта из адреса 10100100. Обратите внимание, что в конце этого примера есть два нуля. Это потому, что четыре байта хранятся так, что ведущие биты 101001 являются значительными. (Процессор действительно обращается к ним через "не волнует", извлекая 101001XX.)

Итак, чтобы выровнять что-то в памяти, нужно переставить данные (обычно через прописку), чтобы нужный адрес элемента имел достаточное количество нулевых байтов. Продолжая приведенный выше пример, мы не можем извлечь 4 байта из 10100101, поскольку последние два бита не равны нулю; что приведет к ошибке шины. Таким образом, мы должны поднять адрес до 10101000 (и тратить три адреса в этом процессе).

Компилятор делает это для вас автоматически и представлен в коде сборки.

Обратите внимание, что это проявляется как оптимизация в C/С++:

struct first {
    char letter1;
    int number;
    char letter2;
};

struct second {
    int number;
    char letter1;
    char letter2;
};

int main ()
{
    cout << "Size of first: " << sizeof(first) << endl;
    cout << "Size of second: " << sizeof(second) << endl;
    return 0;
}

Выходной сигнал

Size of first: 12
Size of second: 8

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