Что значит выровнять стек? - PullRequest
45 голосов
/ 14 ноября 2010

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

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

объяснение было следующим:

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

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

Ответы [ 6 ]

56 голосов
/ 14 ноября 2010

Предположим, что стек выглядит следующим образом при входе в _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.

16 голосов
/ 14 ноября 2010

Звучит не для конкретного стека, а для выравнивания в целом.Возможно, придумайте термин целое число, кратное.

Если в памяти есть элементы размером в байт, единицы 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

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

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

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

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

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

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

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

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

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

7 голосов
/ 14 ноября 2010

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

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

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

5 голосов
/ 14 ноября 2010

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

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

Значения по адресам, кратным 8, легко вставляются в (64-разрядные) регистры

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

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

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


Редактировать andl -16

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

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

4 голосов
/ 14 ноября 2010

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

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

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

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

3 голосов
/ 14 ноября 2010

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

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