Росте стек вгору чи вниз?


88

У мене є цей шматок коду на c:

int q = 10;
int s = 5;
int a[3];

printf("Address of a: %d\n",    (int)a);
printf("Address of a[1]: %d\n", (int)&a[1]);
printf("Address of a[2]: %d\n", (int)&a[2]);
printf("Address of q: %d\n",    (int)&q);
printf("Address of s: %d\n",    (int)&s);

Вихід:

Address of a: 2293584
Address of a[1]: 2293588
Address of a[2]: 2293592
Address of q: 2293612
Address of s: 2293608

Отже, я бачу, що з aдо a[2], адреси пам’яті збільшуються на 4 байти кожна. Але відq до s, адреси пам'яті зменшуються на 4 байти.

Цікаво 2 речі:

  1. Стік росте вгору чи вниз? (У цьому випадку мені це здається обом)
  2. Що відбувається між адресами a[2]та qадресами пам'яті? Чому там велика різниця в пам’яті? (20 байт).

Примітка: Це не питання домашнього завдання. Мені цікаво, як працює стек. Дякую за будь-яку допомогу.


Порядок довільний. Розрив, ймовірно, полягає у збереженні проміжного результату, такого як & q або & s - подивіться на розбирання та переконайтесь самі.
Tom Leys

Я згоден, прочитайте код збірки. Якщо ви задаєте такі запитання, пора навчитися їх читати.
Per Johansson

Простіший для відповіді збірки версії: stackoverflow.com/questions/664744 / ...
Чіро Сантіллі郝海东冠状病六四事件法轮功

Відповіді:


74

Поведінка стека (зростання або зростання) залежить від бінарного інтерфейсу програми (ABI) і від того, як організований стек викликів (він же запис активації).

Протягом свого життя програма зобов'язана спілкуватися з іншими програмами, такими як ОС. ABI визначає, як програма може взаємодіяти з іншою програмою.

Стек для різних архітектур може зростати в будь-який спосіб, але для архітектури він буде послідовним. Перевірте це посилання на вікі. Але зростання стека визначається ABI цієї архітектури.

Наприклад, якщо ви берете MIPS ABI, стек дзвінків визначається, як показано нижче.

Давайте розглянемо, що функція 'fn1' викликає 'fn2'. Тепер кадр стека, як видно з 'fn2', є таким:

direction of     |                                 |
  growth of      +---------------------------------+ 
   stack         | Parameters passed by fn1(caller)|
from higher addr.|                                 |
to lower addr.   | Direction of growth is opposite |
      |          |   to direction of stack growth  |
      |          +---------------------------------+ <-- SP on entry to fn2
      |          | Return address from fn2(callee) | 
      V          +---------------------------------+ 
                 | Callee saved registers being    | 
                 |   used in the callee function   | 
                 +---------------------------------+
                 | Local variables of fn2          |
                 |(Direction of growth of frame is |
                 | same as direction of growth of  |
                 |            stack)               |
                 +---------------------------------+ 
                 | Arguments to functions called   |
                 | by fn2                          |
                 +---------------------------------+ <- Current SP after stack 
                                                        frame is allocated

Тепер ви можете бачити, як стек росте вниз. Отже, якщо змінні призначені локальному фрейму функції, адреси змінної насправді зростають вниз. Компілятор може приймати рішення про порядок змінних для розподілу пам'яті. (У вашому випадку це може бути або 'q' або 's', що спочатку виділяється пам'яттю стека. Але, як правило, компілятор розподіляє пам'ять стека відповідно до порядку оголошення змінних).

Але у випадку масивів виділення має лише один покажчик, і пам'ять, яку потрібно виділити, буде фактично вказана одним покажчиком. Пам'ять повинна бути суміжною для масиву. Отже, хоча стек росте вниз, для масивів стек росте.


5
Крім того, якщо ви хочете перевірити, росте стек вгору чи вниз. Оголосіть локальну змінну в основній функції. Надрукуйте адресу змінної. Виклик іншої функції з main. Оголосіть у функції локальну змінну. Роздрукуйте його адресу. На основі надрукованих адрес можна сказати, що стек росте вгору або вниз.
Ганеш Гопаласубраманіан

дякую Ганешу, у мене невелике запитання: на малюнку u draw, у третьому блоці, ви означали u "збережений реєстр calleR, який використовується у CALLER", тому що коли f1 викликає f2, ми повинні зберігати адресу f1 (яка є зворотним адресом для регістрів f2) та f1 (calleR), а не регістрів f2 (callee). Правда?
CSawy

44

Це насправді два запитання. Один - про те, яким чином стек зростає, коли одна функція викликає іншу (коли виділяється новий кадр), а інший - про те, як розміщені змінні у кадрі конкретної функції.

Ні те, ні інше не визначено стандартом С, але відповіді трохи інші:

  • Яким чином стек зростає при виділенні нового кадру - якщо функція f () викликає функцію g (), чи буде fпокажчик кадру більшим або меншим, ніж gпокажчик кадру? Це може піти в будь-який бік - це залежить від конкретного компілятора та архітектури (шукайте "конвенцію викликів"), але воно завжди узгоджується в межах певної платформи (за кількома химерними винятками, див. Коментарі). Донизу частіше; це стосується x86, PowerPC, MIPS, SPARC, EE та Cell SPU.
  • Як локальні змінні функції розміщені всередині її фрейму стека? Це невстановлене і абсолютно непередбачуване; компілятор може вільно впорядковувати свої локальні змінні, проте він любить отримувати максимально ефективний результат.

7
"він завжди узгоджується в межах певної платформи" - не гарантується. Я бачив платформу без віртуальної пам'яті, де стек динамічно розширювався. Нові блоки стеків були фактично несправними, це означає, що ви на деякий час опускаєтеся по одному блоку стека, а потім раптом "вбік" переходите до іншого блоку. "Набік" може означати більшу чи меншу адресу, повністю до удачі розіграшу.
Стів Джессоп,

2
Для додаткової деталізації до пункту 2 - компілятор може вирішити, що змінна ніколи не повинна бути в пам'яті (зберігаючи її в реєстрі на весь час дії змінної), та / або якщо термін служби двох або більше змінних не t перекриваючись, компілятор може прийняти рішення використовувати одну і ту ж пам’ять для більш ніж однієї змінної.
Michael Burr

2
Я думаю, що S / 390 (IBM zSeries) має ABI, де кадри викликів пов'язані, а не ростуть у стеку.
ефемієнт

2
Правильно на S / 390. Дзвінок - це "BALR", реєстр філій та посилань. Повернене значення поміщається в регістр, а не надсилається у стек. Функція повернення є гілкою до вмісту цього реєстру. У міру того, як стек стає глибшим, у купі виділяється простір, і вони зв’язуються між собою. Ось де еквівалент MVS "/ bin / true" отримав свою назву: "IEFBR14". Перша версія мала єдину інструкцію: "BR 14", яка розгалужувалася до вмісту реєстру 14, який містив адресу повернення.
janm

1
А деякі компілятори на процесорах PIC виконують цілий аналіз програм та виділяють фіксовані місця для автоматичних змінних кожної функції; фактичний стек крихітний і не доступний із програмного забезпечення; це лише для зворотних адрес.
janm

13

Напрямок зростання стеків є специфічним для архітектури. Тим не менш, я розумію, що лише дуже мало апаратних архітектур мають стеки, які ростуть.

Напрямок зростання стека не залежить від розміщення окремого об’єкта. Отже, хоча стек може зростати, масиви не будуть (тобто & array [n] завжди буде <& array [n + 1]);


4

У стандарті немає нічого, що б передбачало взагалі організацію речей у стеку. Насправді, ви можете створити відповідний компілятор, який взагалі не зберігав би елементи масиву в суміжних елементах у стеці, за умови, що він мав розумність робити арифметику елементів масиву належним чином (щоб він знав, наприклад, що 1 був 1K від [0] і може підлаштуватися під це).

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

  • оптимізація.
  • вирівнювання.
  • примхи людини, частина компілятора, що керує стеком.

Дивіться тут мій чудовий трактат про напрямок стека :-)

Відповідаючи на ваші конкретні запитання:

  1. Стік росте вгору чи вниз?
    Це взагалі не має значення (з точки зору стандарту), але, з вашого запитання, воно може вирости або зменшуватися в пам'яті, залежно від реалізації.
  2. Що відбувається між адресами пам'яті [2] та q? Чому там велика різниця в пам’яті? (20 байт)?
    Це взагалі не має значення (з точки зору стандарту). Див. Вище щодо можливих причин.

Я бачив, як ви посилаєтесь на те, що більшість процесорних архітектур приймають спосіб "зростати", чи знаєте ви, чи є якась перевага від цього?
Baiyan Huang

Не знаю, насправді. Це можливо , що хтось - то думка код йде вгору від 0 , так стек повинен йти вниз від Highmem, таким чином , щоб звести до мінімуму можливість перетинатися. Але деякі процесори спеціально починають запускати код у ненульових місцях, тому це може бути не так. Як і в більшості речей, можливо, це було зроблено так просто тому, що це був перший спосіб, як хтось думав це робити :-)
paxdiablo

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

3

У x86 "виділення" пам'яті кадру стека складається просто з віднімання необхідної кількості байтів з покажчика стека (я вважаю, що інші архітектури схожі). У цьому сенсі я думаю, що стек зростає "вниз", оскільки адреси поступово зменшуються, коли ви заходите глибше в стек (але я завжди передбачаю, що пам'ять починається з 0 у верхньому лівому куті та отримує більші адреси під час переміщення праворуч і загорнути вниз, так що в моєму розумовому образі стек росте ...). Порядок оголошень змінних може не мати жодного відношення до їх адрес - я вважаю, що стандарт дозволяє компілятору змінювати їх порядок, якщо це не викликає побічних ефектів (хтось, будь ласка, виправте мене, якщо я помиляюся) . Вони'

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


1
Насправді, я знаю, що компілятор може їх перевпорядковувати, оскільки також можна взагалі не розподіляти їх. Він може просто помістити їх у регістри і не використовувати жодного простору стека.
rmeador

Він не може внести їх до реєстрів, якщо ви посилаєтесь на їх адреси.
флорин

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

1

Перш за все, його 8 байт невикористаного місця в пам'яті (його не 12, пам’ятайте, стек зростає вниз, отже, простір, який не виділяється, становить від 604 до 597). і чому?. Оскільки кожен тип даних займає місце в пам'яті, починаючи з адреси, що ділиться на його розмір. У нашому випадку масив із 3 цілих чисел займає 12 байт простору пам'яті, а 604 не ділиться на 12. Отже, він залишає порожні місця, поки не зустріне адресу пам'яті, яка ділиться на 12, це 596.

Отже, простір пам'яті, виділений для масиву, становить від 596 до 584. Але оскільки розподіл масиву триває, то перший елемент масиву починається з адреси 584, а не з 596.


1

Компілятор може вільно розподіляти локальні (автоматичні) змінні в будь-якому місці локального кадру стека, ви не можете надійно зробити висновок щодо напряму зростання стека суто з цього. Ви можете зробити висновок щодо напряму зростання стека, порівнюючи адреси вкладених кадрів стека, тобто порівнюючи адресу локальної змінної всередині кадру стека функції порівняно з її викликом:

#include <stdio.h>
int f(int *x)
{
  int a;
  return x == NULL ? f(&a) : &a - x;
}

int main(void)
{
  printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up");
  return 0;
}

5
Я майже впевнений, що невизначеною поведінкою є віднімання покажчиків на різні об’єкти стека - покажчики, які не є частиною одного об’єкта, не можна порівняти. Очевидно, що це не призведе до збою на жодній "звичайній" архітектурі.
Steve Jessop

@SteveJessop Чи можна якось це виправити, щоб програмно отримати напрямок стека?
xxks-kkk

@ xxks-kkk: в принципі ні, оскільки реалізація C не вимагає наявності "напряму стека". Наприклад, це не буде порушувати стандарт, якщо існує умова викликів, в якій блок стека виділяється вперед, а потім використовується деяка псевдовипадкова процедура розподілу внутрішньої пам'яті, щоб стрибати всередині нього. На практиці це насправді працює так, як описує Матя.
Steve Jessop

0

Я не думаю, що це так детерміновано. Масив, здається, "росте", тому що ця пам'ять повинна виділятися суміжно. Однак, оскільки q та s взагалі не пов’язані між собою, компілятор просто вставляє кожен із них у довільне вільне місце пам’яті в стеці, ймовірно, те, що найкраще відповідає цілому числу.

Що сталося між [2] і q, це те, що простір навколо розташування q був недостатньо великим (тобто не був більшим за 12 байт), щоб виділити 3 цілочисельний масив.


якщо так, то чому q, s, a не мають умовної пам’яті? (Наприклад: Адреса q: 2293612 Адреса s: 2293608 Адреса a: 2293604)

Я бачу "розрив" між s та a

Оскільки s та a не були виділені разом - єдиними вказівниками, які повинні бути суміжними, є ті, що є в масиві. Іншу пам’ять можна виділити де завгодно.
javanix

0

Здається, мій стек поширюється на адреси з меншими номерами.

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

$ cat stack.c
#include <stdio.h>

int stack(int x) {
  printf("level %d: x is at %p\n", x, (void*)&x);
  if (x == 0) return 0;
  return stack(x - 1);
}

int main(void) {
  stack(4);
  return 0;
}
$ / usr / bin / gcc -Wall -Wextra -std = c89 -педантичний стек.c
$ ./a.out
рівень 4: x знаходиться на рівні 0x7fff7781190c
рівень 3: x знаходиться на рівні 0x7fff778118ec
рівень 2: x знаходиться на рівні 0x7fff778118cc
рівень 1: x знаходиться на рівні 0x7fff778118ac
рівень 0: x знаходиться на рівні 0x7fff7781188c

0

Стек росте вниз (на x86). Однак стек виділяється в один блок, коли функція завантажується, і ви не маєте гарантії, в якому порядку будуть елементи в стеку.

У цьому випадку він виділив простір для двох ints та масиву з трьома int у стеку. Він також виділив додаткові 12 байт після масиву, так що це виглядає так:


відступ [12 байт] (?) [12 байт]
с [4 байти]
q [4 bytes]

З будь-якої причини ваш компілятор вирішив, що йому потрібно виділити 32 байти для цієї функції, а можливо і більше. Це непрозоро для вас як програміста на С, ви не знаєте, чому.

Якщо ви хочете знати чому, скомпілюйте код на мові асемблера, я вважаю, що це -S на gcc та / S на компіляторі C MS. Якщо ви подивитеся на вказівки щодо відкриття цієї функції, ви побачите, як зберігається старий вказівник стека, а потім від нього віднімається 32 (або щось інше!). Звідти ви можете побачити, як код отримує доступ до цього 32-байтового блоку пам'яті, і зрозуміти, що робить ваш компілятор. В кінці функції ви можете побачити відновлення покажчика стека.


0

Це залежить від вашої операційної системи та компілятора.


Не знаю, чому моя відповідь була проголосована проти. Це насправді залежить від вашої ОС та компілятора. У деяких системах стек росте вниз, а в інших - вгору. У деяких системах немає реального стека кадру, а навпаки, він імітується із зарезервованою областю пам'яті або набором регістрів.
David R Tribble,

3
Можливо, тому, що твердження з одним реченням не є хорошими відповідями.
Гонки легкості на орбіті

0

Стек справді зростає. Отже f (g (h ())), стек, виділений для h, почнеться з нижчої адреси, тоді g та g будуть нижчими, ніж f. Але змінні всередині стеку повинні відповідати специфікації C,

http://c0x.coding-guidelines.com/6.5.8.html

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

& a [0] <& a [1], завжди має бути істинним, незалежно від того, як розподілено 'a'


На більшості машин стек росте вниз - за винятком тих, де він росте вгору.
Джонатан Леффлер

0

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

Одним із способів, на який ви могли б подивитися, є те, що стек РОСТЕ зростає вгору, якщо дивитись на пам’ять зверху і максимум знизу.

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

Пам'ятайте, що переспрямування будь-якого типу збільшується від найнижчої до найвищої адреси. Оскільки стек зростає вниз (від найвищої до найнижчої адреси), це дозволяє вам обробляти стек як динамічну пам’ять.

Це одна з причин, чому так багато мов програмування та сценаріїв використовують віртуальну машину на основі стеку, а не регістр.


The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.Дуже приємні міркування
user3405291

0

Це залежить від архітектури. Щоб перевірити власну систему, використовуйте цей код від GeeksForGeeks :

// C program to check whether stack grows 
// downward or upward. 
#include<stdio.h> 

void fun(int *main_local_addr) 
{ 
    int fun_local; 
    if (main_local_addr < &fun_local) 
        printf("Stack grows upward\n"); 
    else
        printf("Stack grows downward\n"); 
} 

int main() 
{ 
    // fun's local variable 
    int main_local; 

    fun(&main_local); 
    return 0; 
} 
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.