Чому ми все ще вирощуємо стек назад?


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
Мені здається, що "назад" або "вперед" (або "вниз" і "вгору") залежить від вашої точки зору. Якщо ви діаграмували пам'ять як стовпець з низькими адресами зверху, то нарощування стека шляхом зменшення покажчика стека було б аналогічно фізичному стеку.
jamesdlin

Відповіді:


86

Чи означає це, що базовий вказівник або покажчик стека насправді рухаються вниз по пам'яті, а не вгору? Чому так?

Так, pushінструкції зменшують покажчик стека і записують у стек, тоді як popроблять зворотний, читають із стека та збільшують покажчик стека.

Це дещо історично, оскільки для машин з обмеженою пам’яттю стек був розміщений високо і зростав вниз, тоді як купа була розміщена низько і виросла вгору. Існує лише один проміжок "вільної пам'яті" - між купою та стеком, і цей проміжок розділяється, або кожен може перерости в проміжок за потреби індивідуально. Таким чином, програма втрачає пам'ять лише тоді, коли стек і купа стикаються, не залишаючи вільної пам'яті. 

Якщо і стек, і купа ростуть в одному напрямку, тоді є дві прогалини, і стек не може реально перерости в проміжок купи (навпаки, також проблематично).

Спочатку процесори не мали спеціальних інструкцій з обробки стеків. Однак, оскільки підтримка стека була додана до апаратного забезпечення, вона взяла цю схему зростання вниз, і процесори досі слідують цій схемі і сьогодні.

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


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

Зауважте, що архітектура набору інструкцій MIPS не має виділених значень push& pop, тому практично вирощувати стек в будь-якому напрямку - ви все одно можете мати розклад пам’яті з одним розривом для одного потокового процесу, але може збільшити стек вгору та купу вниз. Якщо ви це зробили, однак, деякий код varargs C може потребувати коригування в джерелі або при проходженні параметра під кришкою.

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


32
Це не просто pushі popна більшості архітектур, а й набагато важливіше переривання обробку, call, retі все інше спекли-у взаємодії зі стеком.
Дедуплікатор

3
ARM може мати усі чотири стеки ароматів.
Маргарет Блум

14
Я не думаю, що "напрямок зростання довільний" в тому сенсі, що обидва варіанти є однаково хорошими. Зростаючий має властивість, що переповнює кінець буфера клобірує попередні кадри стека, включаючи збережені зворотні адреси. Зростаючий має властивість, що переповнення кінця буферних клоберів зберігається лише в тому самому або пізнішому (якщо буфер не є останнім, можуть бути пізніші) кадр виклику, і, можливо, навіть лише невикористаний простір (усі припускають, що охороняється сторінки після стека). Отже, з точки зору безпеки, дорослішання здається дуже бажаним
R ..

6
@ R ..: дорослішання не усуває подвиги перевищення буфера, оскільки вразливі функції зазвичай не є функціями листків: вони викликають інші функції, розміщуючи зворотну адресу над буфером. Функції Leaf, які отримують вказівник від свого абонента, можуть стати вподобаними для запису власної зворотної адреси. Наприклад, якщо функція виділяє буфер на стек і передає його gets(), або робить strcpy()цей не вбудований, тоді для повернення в цих функціях бібліотеки буде використана перезаписана повернення адреси. Наразі з низхідними зростаючими стеками, саме коли їхній абонент повертається.
Пітер Кордес

5
@PeterCordes: Дійсно мій коментар зазначав, що однорівневі або новіші кадри стека, ніж переповнений буфер, все ще можуть бути клоберирующими, але це набагато менше. У випадку, коли функція клобування - це функція аркуша, безпосередньо викликана функцією, буфером якої вона є (наприклад strcpy), на арці, де зворотна адреса зберігається в реєстрі, якщо її не потрібно розлити, немає доступу до повернення клоббера адреса.
Р ..

8

У вашій конкретній системі стек починається з високої адреси пам’яті і «зростає» вниз до низьких адрес пам'яті. (симетричний випадок від низького до високого також існує)

А оскільки ви змінилися з -4 і +4, і він пробіг, це не означає, що це правильно. Макет пам’яті запущеної програми складніший і залежить від багатьох інших факторів, які можуть сприяти тому, що ви не враз потрапили в аварію над цією надзвичайно простою програмою.


1

Вказівник стека вказує на межу між виділеною та нерозподіленою пам'яттю стека. Зростання його вниз означає, що він вказує на початок першої структури у виділеному просторі стеку, а інші виділені елементи слід за більшими адресами. Якщо вказівники вказують на початок виділених структур, це набагато частіше, ніж навпаки.

Зараз у багатьох системах існує окремий реєстр стеків кадрів, який можна дещо надійно розкрутити, щоб з'ясувати ланцюг викликів, з локальною змінною пам’яттю змінних. Шлях цей стек регістр кадрів встановлюється на деяких архітектурах означає , що він закінчує наведення за зберігання локальної змінної , на відміну від покажчика стека перед ним. Тож використання цього регістра кадру стека вимагає негативної індексації.

Зауважте, що кадри стека та їх індексація є побічним аспектом компільованих комп'ютерних мов, тому саме генератор коду компілятора повинен мати справу з "неприродністю", а не поганим програмістом мови складання.

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


2
"Зараз у багатьох системах сьогодні існує окремий реєстр стеків кадрів", які ви відстаєте від часу. Більш багаті формати інформації про налагодження значною мірою усунули потребу в покажчиках кадрів на сьогодні.
Пітер Зелений
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.