Что значит выровнять стек?
Я был кодером высокого уровня, и архитектуры довольно новые для меня, поэтому я решил прочитать учебник по сборке здесь:
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!"
.globl _main
_main:
pushl %ebp
movl %esp, %ebp
subl , %esp
andl $-16, %esp
movl , %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
call __alloca
call ___main
movl $LC0, (%esp)
call _printf
movl , %eax
leave
ret
для одного из линии,
andl $-16, %esp
объяснение:
этот код " и " S 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 "(одна инструкция, несколько данных) (также известные в x86-land как" SSE "для" Streaming SIMD Extensions"), которые могут выполнять параллельные операции с несколькими словами в памяти, но требуют, чтобы эти несколько слов были блоком, начинающимся с адреса, который кратен 16 байтам.
в общем случае компилятор не может предположить, что определенные смещения от %esp
приведет к подходящему адресу (потому что состояние %esp
при входе в функция зависит от вызывающего кода). Но, преднамеренно выравнивая указатель стека таким образом, компилятор знает, что добавление любого кратного 16 байтам указателю стека приведет к 16-байтовому выровненному адресу, который безопасен для использования с этими инструкциями SIMD.
это не звучит как специфичный стек, но выравнивание в целом. Возможно, подумайте о термине integer multiple.
Если у вас есть элементы в памяти, которые имеют размер байта, единицы 1, то давайте просто скажем, что они все выровнены. Вещи, которые имеют размер два байта, а затем целые числа, умноженные на 2, будут выровнены, 0, 2, 4, 6, 8, etc. И нецелочисленные кратные, 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 будет путем anding с 0b111 (0x7) и вычитания этой суммы. Это означает, что ALU операции и и вычитание. Но мы можем взять ярлык, если мы и с теми, которые дополняют значение 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 бит.
представьте себе этот "чертеж"
addresses xxx0123456789abcdef01234567 ... [------][------][------] ... registers
значения по адресам, кратным 8 "слайд" легко в (64-разрядные) регистры
addresses 56789abc ... [------][------][------] ... registers
конечно регистрирует "прогулку" в шагах 8 байт
теперь, если вы хотите поместить значение по адресу xxx5 в регистр, намного сложнее : -)
редактировать andl -16
-16 это 11111111111111111111111111110000 в двоичном
когда вы "и" что-нибудь с -16 вас значение последние 4 бита равны 0 ... или multitple 16 лет.
Это должно быть только по четным адресам, а не по нечетным, потому что есть дефицит производительности, обращающийся к ним.
когда процессор загружает данные из памяти в регистр, он должен получить доступ к базовым адресом и размером. Например, он будет получать 4 байта от адреса 10100100. Обратите внимание, что в конце этого примера есть два нуля. Это потому, что четыре байта хранятся так, что 101001 ведущие биты являются значимыми. (Процессор действительно обращается к ним через "все равно", получая 101001XX.)
так выровнять что-то в памяти значит переставить данные (обычно через padding), так что адрес нужного элемента будет иметь достаточно нулевых байтов. Продолжая приведенный выше пример, мы не можем получить 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
будет выровнен должным образом, и поэтому компилятору не нужно увеличивать базовый адрес с помощью заполнения. Вот почему размер второго меньше.