Что значит выровнять стек?
Я был программистом высокого уровня, и архитектуры довольно новы для меня, поэтому я решил прочитать руководство по сборке здесь:
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 с 0xFFFFFFF0, выравнивающий стек со следующей наименьшей 16-байтовой границей. Изучение исходного кода Mingw показывает, что это может быть для инструкций SIMD, появляющихся в подпрограмме "_main", которые работают только на выровненных адресах. Поскольку наша процедура не содержит SIMD-инструкций, эта строка не нужна.
Я не понимаю этот момент. Может ли кто-нибудь дать мне объяснение того, что означает выравнивание стека по следующей 16-байтовой границе и почему это требуется? И как andl
достижения этого?
6 ответов
Предположим, что стек выглядит так при входе в _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" для "Потоковых расширений SIMD"), которые могут выполнять параллельные операции над несколькими словами в памяти, но требуется, чтобы эти несколько слов были блоком, начинающимся с адреса, кратного 16 байтам.
В общем, компилятор не может предположить, что конкретные смещения от %esp
приведет к подходящему адресу (потому что состояние %esp
от входа в функцию зависит код вызова). Но, преднамеренно выравнивая указатель стека таким образом, компилятор знает, что добавление любого кратного 16 байтов к указателю стека приведет к 16-байтовому выровненному адресу, который безопасен для использования с этими инструкциями SIMD.
Это звучит не так, чтобы быть специфичным для стека, но выравнивание в целом. Возможно, придумайте термин целое число, кратное.
Если в памяти есть элементы размером в байт, единицей 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 и так далее.
Это означает, что вы можете посмотреть на базовый адрес элемента и определить, выровнен ли он.
размер в байтах, адрес в виде 1, ххххххх 2, хххххх0 4, ххххх00 8, хххх000 16,xxx0000 32,xx00000 64,x000000 и так далее
В случае, когда компилятор смешивает данные с инструкциями в сегменте.text, достаточно просто выровнять данные по мере необходимости (ну, это зависит от архитектуры). Но стек - это вещь времени выполнения, компилятор обычно не может определить, где будет находиться стек во время выполнения. Поэтому во время выполнения, если у вас есть локальные переменные, которые необходимо выровнять, вам нужно, чтобы код корректировал стек программно.
Например, у вас есть два 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, поэтому даже если вы можете это сделать, вы должны стремиться оставаться выровненными, как если бы другая архитектура. Возможно, именно это и делал этот код.
Это связано с выравниванием байтов. Определенные архитектуры требуют, чтобы адреса, используемые для определенного набора операций, были приведены в соответствие с конкретными границами битов.
То есть, если вы хотите, например, 64-битное выравнивание для указателя, вы можете концептуально разделить всю адресуемую память на 64-битные порции, начиная с нуля. Адрес был бы "выровнен", если он точно вписывался в один из этих кусков, и не выровнялся, если он занимал часть одного куска и часть другого.
Важной особенностью байтового выравнивания (при условии, что число является степенью 2) является то, что младшие значащие биты X адреса всегда равны нулю. Это позволяет процессору представлять больше адресов с меньшим количеством битов, просто не используя младшие биты X.
Представь себе этот "рисунок"
адреса xxx0123456789abcdef01234567 ... [------][------][------] ... регистры
Значения по адресам, кратным 8, легко "вставляются" в (64-битные) регистры
адреса 56789abc ... [------][------][------] ... регистры
Конечно регистры "гуляют" с шагом 8 байтов
Теперь, если вы хотите поместить значение по адресу xxx5 в регистр, это намного сложнее:-)
Редактировать andl -16
-16 это двоичный код 11111111111111111111111111110000
когда вы "и" что-нибудь с -16, вы получите значение с последними 4 битами, установленными на 0... или кратное 16.
Когда процессор загружает данные из памяти в регистр, ему требуется доступ по базовому адресу и размеру. Например, он получит 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
будет правильно выровнен, и поэтому компилятору не придется увеличивать базовый адрес с помощью отступов. Вот почему размер второго меньше.
Он должен быть только по четным адресам, а не по нечетным, поскольку доступ к ним имеет дефицит производительности.