Стек растет вверх или вниз?


89

У меня есть этот фрагмент кода в 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 - посмотрите разборку и убедитесь сами.
Том Лейс

Согласен, ассемблерный код читайте. Если вы задаете такие вопросы, пора научиться читать.
Пер Йоханссон,

Версия сборки, на которую проще ответить: stackoverflow.com/questions/664744/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Ответы:


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, в третьем блоке, означает ли u «сохраненный регистр calleR, используемый в CALLER», потому что когда f1 вызывает f2, мы должны сохранить адрес f1 (который является обратным адресом для регистров f2) и f1 (calleR), а не регистров f2 (вызываемого). Правильно?
CSawy

44

На самом деле это два вопроса. Один касается того, как растет стек, когда одна функция вызывает другую (когда выделяется новый фрейм), а другой касается того, как переменные размещаются в кадре конкретной функции.

Ни то, ни другое не указано в стандарте C, но ответы немного отличаются:

  • В каком направлении увеличивается стек при выделении нового кадра - если функция f () вызывает функцию g (), будет ли fуказатель кадра больше или меньше gуказателя кадра? Это может происходить любым путем - это зависит от конкретного компилятора и архитектуры (см. «Соглашение о вызовах»), но всегда согласовано в рамках данной платформы (за некоторыми причудливыми исключениями, см. Комментарии). Вниз встречается чаще; это относится к x86, PowerPC, MIPS, SPARC, EE и Cell SPU.
  • Как локальные переменные функции располагаются внутри ее кадра стека? Это не указано и совершенно непредсказуемо; компилятор может упорядочивать свои локальные переменные, однако ему нравится получать наиболее эффективный результат.

7
«он всегда согласован в рамках данной платформы» - не гарантируется. Я видел платформу без виртуальной памяти, где стек расширялся динамически. Новые блоки стека фактически были искажены, что означало, что вы на некоторое время спускались «вниз» на один блок стека, а затем внезапно «вбок» переходили к другому блоку. «Вбок» может означать больший или меньший адрес, полностью в зависимости от удачи розыгрыша.
Стив Джессоп

2
Для дополнительной информации к пункту 2 - компилятор может решить, что переменная никогда не должна находиться в памяти (сохраняя ее в регистре в течение всего срока службы переменной), и / или если время жизни двух или более переменных не требуется t перекрытия, компилятор может решить использовать одну и ту же память для более чем одной переменной.
Майкл Берр

2
Я думаю, что S / 390 (IBM zSeries) имеет ABI, в котором кадры вызовов связаны, а не растут в стеке.
ephemient

2
Правильно на S / 390. Звонок - «БАЛР», ветка и ссылка на регистр. Возвращаемое значение помещается в регистр, а не помещается в стек. Функция возврата - это переход к содержимому этого регистра. По мере того, как стек становится глубже, в куче выделяется место, и они связываются вместе. Здесь эквивалент MVS «/ bin / true» получил свое название: «IEFBR14». Первая версия имела единственную инструкцию: «BR 14», которая переходила к содержимому регистра 14, содержащего адрес возврата.
janm

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

13

Направление роста стеков зависит от архитектуры. Тем не менее, насколько я понимаю, только очень немногие аппаратные архитектуры имеют растущие стеки.

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


4

В стандарте нет ничего, что определяло бы, как вещи вообще организованы в стеке. Фактически, вы могли бы создать соответствующий компилятор, который вообще не хранил бы элементы массива в смежных элементах в стеке, при условии, что у него был ум, чтобы по-прежнему правильно выполнять арифметические операции с элементами массива (чтобы он знал, например, что 1 было 1K от [0] и можно было бы отрегулировать для этого).

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

  • оптимизация.
  • выравнивание.
  • капризы человека - часть компилятора, управляющая стеком.

Смотрите здесь мой отличный трактат о направлении стека :-)

В ответ на ваши конкретные вопросы:

  1. Стек растет или опускается?
    Это вообще не имеет значения (с точки зрения стандарта), но, поскольку вы спросили, он может увеличиваться или уменьшаться в памяти, в зависимости от реализации.
  2. Что происходит между адресами памяти a [2] и q? Почему там большая разница памяти? (20 байт)?
    Совсем не важно (с точки зрения стандарта). См. Возможные причины выше.

Я видел, как вы связали, что большинство архитектур ЦП используют метод «роста вниз», знаете ли вы, есть ли в этом какие-то преимущества?
Байян Хуанг

Понятия не имею, правда. Это возможно , что кто - то мысль код идет вверх от 0 , так стек должен идти вниз от Highmem, таким образом , чтобы свести к минимуму возможность пересекаться. Но некоторые процессоры специально запускают код в ненулевых местах, так что это может быть не так. Как и в большинстве случаев, возможно, это было сделано таким образом просто потому, что это был первый способ, которым кто-то подумал сделать это :-)
paxdiablo

@lzprgmr: Есть некоторые небольшие преимущества в том, что определенные виды распределения кучи выполняются в возрастающем порядке, и исторически сложилось, что стек и куча располагаются на противоположных концах общего адресного пространства. При условии, что комбинированное использование статического + кучи + стека не превышает доступной памяти, не нужно беспокоиться о том, сколько именно памяти стека использует программа.
supercat 07

3

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

Зазор вокруг массива может быть чем-то вроде прокладки, но для меня это загадочно.


1
Фактически, я знаю, что компилятор может их переупорядочить, потому что также можно вообще не выделять их. Он может просто помещать их в регистры и вообще не использовать какое-либо пространство стека.
rmeador

Он не может поместить их в регистры, если вы укажете их адреса.
florin

хороший момент, не подумал об этом. но этого по-прежнему достаточно в качестве доказательства того, что компилятор может их переупорядочивать, поскольку мы знаем, что он может это делать по крайней мере в некоторых случаях :)
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
Я почти уверен, что это поведение undefined для вычитания указателей на разные объекты стека - указатели, которые не являются частью одного и того же объекта, не сопоставимы. Очевидно, что он не выйдет из строя на любой «нормальной» архитектуре.
Стив Джессоп

@SteveJessop Есть ли способ исправить это, чтобы программно определить направление стека?
xxks-kkk

@ xxks-kkk: в принципе нет, потому что реализация C не требует наличия «направления стека». Например, стандарт не нарушает соглашение о вызовах, в котором блок стека выделяется заранее, а затем используется некоторая псевдослучайная процедура выделения внутренней памяти, чтобы прыгать внутри него. На практике это действительно работает, как описывает Матья.
Стив Джессоп

0

Я не думаю, что это так детерминировано. Кажется, что массив a "растет", потому что эта память должна выделяться непрерывно. Однако, поскольку q и s вообще не связаны друг с другом, компилятор просто вставляет каждый из них в произвольную свободную ячейку памяти в стеке, возможно, ту, которая лучше всего подходит к целочисленному размеру.

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


Если да, то почему q, s, a не имеют случайной памяти? (Пример: Адрес q: 2293612 Адрес s: 2293608 Адрес a: 2293604)

Я вижу «разрыв» между s и a

Поскольку s и a не были размещены вместе - единственными указателями, которые должны быть смежными, являются указатели в массиве. Остальную память можно выделить где угодно.
javanix 05

0

Кажется, что мой стек расширяется в сторону адресов с меньшими номерами.

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

$ 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 -pedantic stack.c
$ ./a.out
уровень 4: x находится на 0x7fff7781190c
уровень 3: x находится на 0x7fff778118ec
уровень 2: x находится на 0x7fff778118cc
уровень 1: x находится на 0x7fff778118ac
уровень 0: x находится на 0x7fff7781188c

0

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

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

a [12 байтов]
заполнение (?) [12 байтов]
s [4 байта]
q [4 байта]

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

Если вы хотите знать, почему, скомпилируйте код на язык ассемблера, я считаю, что это -S в gcc и / S в компиляторе C. Если вы посмотрите на инструкции по открытию этой функции, вы увидите, что старый указатель стека сохраняется, а затем из него вычитается 32 (или что-то еще!). Отсюда вы можете увидеть, как код обращается к этому 32-байтовому блоку памяти, и выяснить, что делает ваш компилятор. В конце функции вы можете увидеть восстанавливаемый указатель стека.


0

Это зависит от вашей операционной системы и вашего компилятора.


Не знаю, почему мой ответ был отклонен. Это действительно зависит от вашей ОС и компилятора. В некоторых системах стек растет вниз, а в других - вверх. А в некоторых системах нет реального стека кадров с опусканием вниз, а скорее моделируется с зарезервированной областью памяти или набором регистров.
Дэвид Р. Триббл

3
Возможно, потому, что утверждения, состоящие из одного предложения, не являются хорошими ответами.
Lightness Races на орбите

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

растет вниз, и это происходит из-за стандарта порядка байтов с обратным порядком байтов, когда речь идет о наборе данных в памяти.

Один из способов, которым вы могли бы это увидеть, - это то, что стек ДЕЙСТВИТЕЛЬНО растет вверх, если вы посмотрите на память от 0 сверху и max снизу.

Причина увеличения стека вниз заключается в возможности разыменования с точки зрения стека или базового указателя.

Помните, что разыменование любого типа увеличивается от наименьшего к наибольшему адресу. Поскольку стек растет вниз (от самого высокого до самого низкого адреса), это позволяет вам рассматривать стек как динамическую память.

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


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.