Почему мы все еще наращиваем стек назад?

46

При компиляции кода C и просмотре сборки, все это имеет стек, растущий в обратном направлении, вот так:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- означает ли это, что базовый указатель или указатель стека фактически перемещаются вниз по адресам памяти, а не идут вверх? Почему это?

Я изменил $5, -4(%rbp)на $5, +4(%rbp), скомпилировал и запустил код, и ошибок не было. Так почему же мы все еще должны идти назад в стеке памяти?

Алекс
источник
2
Обратите внимание, что -4(%rbp)базовый указатель вообще не перемещается, и это +4(%rbp)не может сработать.
Маргарет Блум
14
« почему мы все еще должны идти в обратном направлении » - что, по вашему мнению, было бы преимуществом для продвижения вперед? В конечном счете, это не имеет значения, вам просто нужно выбрать один.
Берги
31
"Почему мы наращиваем стек назад?" - потому что, если мы этого не сделаем, кто-то еще спросит, почему mallocрастет куча назад
slebetman
2
@MargaretBloom: Судя по всему, на платформе OP код запуска CRT не заботится о том, mainчто его RBP будет перегружен. Это конечно возможно. (И да, запись 4(%rbp)будет наступать на сохраненное значение RBP). На самом деле, это правило никогда не выполняется mov %rsp, %rbp, поэтому доступ к памяти зависит от RBP вызывающего , если это именно то, что на самом деле проверял OP !!! Если это было фактически скопировано из вывода компилятора, некоторые инструкции были опущены!
Питер Кордес
1
Мне кажется, что «назад» или «вперед» (или «вниз» и «вверх») зависит от вашей точки зрения. Если вы представляете память как столбец с низкими адресами сверху, то увеличение стека путем уменьшения указателя стека будет аналогично физическому стеку.
Джеймсдлин

Ответы:

86

Означает ли это, что базовый указатель или указатель стека фактически перемещаются вниз по адресам памяти, а не идут вверх? Почему это?

Да, pushинструкции уменьшают указатель стека и записывают в стек, в то время как popвыполняют обратное чтение из стека и увеличивают указатель стека.

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

Если стек и куча растут в одном и том же направлении, то есть два пробела, и стек действительно не может врасти в промежуток кучи (наоборот, это также проблематично).

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

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


Вы должны были бы изменить инструкции по обработке стеки CPU для того , чтобы изменить направление стека, либо отказаться от использования выделенного толкая и выскакивают инструкции (например push, pop, call, retи других).

Обратите внимание, что архитектура набора команд MIPS не имеет выделенного символа push& pop, поэтому практично увеличивать стек в любом направлении - вам все еще может потребоваться размещение памяти с одним зазором для однопоточного процесса, но можно увеличить стек вверх и кучу вниз. Однако, если вы это сделаете, для некоторого кода C varargs может потребоваться корректировка в передаче исходных или скрытых параметров.

(На самом деле, поскольку в MIPS нет выделенной обработки стека, мы можем использовать предварительное или последующее увеличение или предварительное или последующее уменьшение для добавления в стек до тех пор, пока мы используем точную обратную операцию для выталкивания из стека, а также при условии, что операционная система соответствует выбранной модели использования стека. Действительно, в некоторых встроенных системах и некоторых образовательных системах стек MIPS растет.)

Эрик Эйдт
источник
32
Это не просто pushи popна большинстве архитектур, но и гораздо более важное прерывание обработку, call, retи все остальное испекло-во взаимодействии со стеком.
дедупликатор
3
ARM может иметь все четыре вкуса стека.
Маргарет Блум
14
Что бы это ни стоило, я не думаю, что «направление роста является произвольным» в том смысле, что любой выбор одинаково хорош. У растущего свойства есть свойство, которое переполняет конец буфера, забивая более ранние кадры стека, включая сохраненные адреса возврата. Рост имеет свойство переполнять конец буфера, оставляя только хранилище в том же или более позднем (если буфер не последнем, могут быть более позднем) кадре вызова и, возможно, даже только неиспользуемом пространстве (все при условии защиты страница за стеком). Так что с точки зрения безопасности взросление кажется крайне предпочтительным
Р.
6
@R ..: взросление не устраняет эксплойты переполнения буфера, потому что уязвимые функции обычно не являются конечными функциями: они вызывают другие функции, размещая адрес возврата над буфером. Конечные функции, которые получают указатель от своего вызывающего, могут стать перезаписывающими свой собственный адрес возврата. Например, если функция выделяет буфер в стеке и передает его gets()или делает strcpy()его без встроенного, то при возврате в этих библиотечных функциях будет использоваться перезаписанный адрес возврата. В настоящее время с растущими вниз стеками, это когда их вызывающий возвращается.
Питер Кордес
5
@PeterCordes: На самом деле, мой комментарий отметил, что стековые фреймы того же уровня или более поздние, чем переполненный буфер, все еще потенциально могут перерабатываться, но это намного меньше. В случае, когда функция clobbering является листовой функцией, вызываемой непосредственно функцией, чей буфер она (например strcpy), в дуге, где адрес возврата хранится в регистре, если только он не должен быть разлит, нет доступа к clobber возврата адрес.
Р.
8

В вашей конкретной системе стек начинается с высокого адреса памяти и «растет» вниз до низкого адреса памяти. (симметричный случай от низкого до высокого также существует)

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

надир
источник
1

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

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

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

Таким образом, хотя существовали веские исторические причины для выбора стеков, чтобы они росли вниз (и некоторые из них сохраняются, если вы программируете на языке ассемблера и не удосуживаетесь установить правильный кадр стека), они стали менее заметными.


источник
2
«В настоящее время во многих системах существует отдельный регистр для стековых фреймов», вы отстали от времени. Более богатые форматы отладочной информации в настоящее время в значительной степени устранили необходимость в указателях кадров.
Питер Грин