Где можно найти подробную информацию о работе стека в процессорах x86 - PullRequest
0 голосов
/ 31 октября 2010

Меня интересует расположение исполняемого и динамического выделения памяти с использованием стека и то, как процессор и ядро ​​совместно управляют областью стека, как во время вызовов функций и других сценариев использования выделения памяти на основе стека. Кроме того, как происходит переполнение стека и другие опасности, связанные с этой моделью, существуют ли другие варианты исполнения кода, которые не основаны на стеке и не имеют таких проблем. Видео или анимация будут очень полезны.

1 Ответ

1 голос
/ 31 октября 2010

Обычно (на любом процессоре, а не только на x86) существует одно адресное пространство оперативной памяти, и, как правило, программа находится в нижней области памяти и увеличивается с ростом при запуске. Скажем, ваша программа имеет размер 0x1000 байт и загружается с размером 0x0000, затем вы делаете malloc 0x3000 байт, в этом случае возвращаемый адрес будет 0x1000, и теперь младшие 0x4000 байт активно используются программой. Дополнительные mallocs продолжают расти таким образом. Функция Free () не обязывает приводить к снижению потребления, это зависит от того, как управляется память, а также от программных комбинаций malloc () и free ().

Стек обычно идет сверху вниз. Скажем, 0x10000 - это адрес, с которого начинается указатель стека. Скажем, у вас есть функция, которая имеет три 32-битные переменные типа unsigned, и никакие параметры не передаются, вам нужно три расположения стека для хранения этих переменных (при условии, что оптимизация не уменьшила это требование), поэтому при входе в функцию указатель стека уменьшается на 3 * 4 = 12 байт, поэтому указатель стека изменяется на 0xFFF4, одна из ваших переменных находится по адресу 0xFFF4 + 0, одна - в 0xFFF4 + 4, а третья - в 0xFFF4 + 8. Если эта функция вызывает другую функцию, то указатель стека продолжает перемещаться в ноль в памяти. И по мере того, как вы продолжаете выполнять malloc (), объем используемой памяти программ увеличивается. Если их не проверять, они будут сталкиваться, и код, необходимый для этой проверки, достаточно затратный, поэтому его редко используют. Вот почему локальные переменные хороши для оптимизации и некоторых других вещей, но плохи, потому что потребление стека часто недетерминировано или, по крайней мере, анализ не выполняется средним программистом.

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

Какие у вас альтернативы? Используйте набор инструкций с большим количеством регистров со спецификацией вызова функции, которая использует больше регистров и меньше стека. Используйте меньше аргументов при вызове функций. Используйте меньше локальных переменных. Маллок меньше. Используйте хороший компилятор с хорошим оптимизатором, а также помогите оптимизатору, используя простые для оптимизации привычки при кодировании.

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

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

Другая ловушка, которую часто спрашивают на собеседованиях, - это что-то вроде:

int * myfun ( int a )
{
   int i;

   i=a+7;
   return(&i);
}

Это может принимать разные формы, но нужно понимать, что переменная i временно выделяется в стеке и выделяется только во время выполнения функции, когда функция возвращает указатель стека, освобождая память, выделенную для i, иСледующая вызываемая функция может очень сильно забить эту память.Поэтому возвращать адрес переменной, хранящейся в стеке, - плохая идея.Код, выполняющий что-то подобное, может правильно работать в течение нескольких недель, месяцев и лет, прежде чем его обнаружат.

Теперь это приемлемо даже для процессоров на основе стека (например, zylin zpu).

int myfun ( int a )
{
   int i;

   i=a+7;
   return(i);
}

Отчасти из-за того, что вы мало что можете сделать, кроме использования глобальных переменных (да, в этом конкретном случае не требуется дополнительная переменная i, но предполагается, что ваш код достаточно сложен, и вам нужна эта локальная возвращаемая переменная),вторая причина в том, что в C вызывающий код освобождает свою часть стека.То есть, например, для x86, если вы вызываете функцию с двумя параметрами в стеке, скажем, двумя 4-байтовыми числами, вызывающий код перемещает указатель стека вниз на 8 и помещает эти два параметра в эту память (sp + 0 и sp +4), затем, когда функция возвращает вызывающий код, он распределяет эти две переменные, добавляя 8 к указателю стека.Таким образом, в приведенном выше коде, использующем i и возвращающем i по значению, соглашение о вызовах C для этого процессора знает, где получить возвращаемое значение, и как только это значение будет получено, стековая память, содержащая это значение, больше не нужна.Насколько я понимаю, Паскаль, скажем, Borland Turbo Pascal, например, Calee очистил стек.Таким образом, вызывающая сторона поместит две переменные в стек, а вызываемая функция очистит стек.Неплохая идея с точки зрения управления стеками, поэтому вы можете вкладывать гораздо глубже.у обоих подходов есть свои плюсы и минусы.

...