Стек растет вверх или вниз?
У меня есть этот кусок кода в 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 вещи:
- Стек растет или падает? (Похоже, как для меня в этом случае)
- Что происходит между
a[2]
а такжеq
адреса памяти? Почему там большая разница в памяти? (20 байт).
Примечание: это не домашнее задание. Мне интересно, как работает стек. Спасибо за любую помощь.
15 ответов
Поведение стека (увеличение или уменьшение) зависит от двоичного интерфейса приложения (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', которые сначала выделяют стековую память. Но, как правило, компилятор выполняет выделение стековой памяти в соответствии с порядком объявления переменных).
Но в случае массивов распределение имеет только один указатель, и память, которая должна быть выделена, будет фактически указана одним указателем. Память должна быть смежной для массива. Таким образом, хотя стек растет вниз, для массивов стек растет.
Это на самом деле два вопроса. Один из них - о том, как растет стек, когда одна функция вызывает другую (когда выделяется новый кадр), а другой - о том, как переменные располагаются в кадре конкретной функции.
Ни один из них не указан в стандарте C, но ответы немного отличаются:
- Каким образом увеличивается стек при выделении нового кадра - если функция f() вызывает функцию g(),
f
указатель кадра будет больше или меньшеg
указатель кадра? Это может происходить в любом случае - это зависит от конкретного компилятора и архитектуры (см. "Соглашение о вызовах"), но оно всегда согласованно в рамках конкретной платформы (с несколькими странными исключениями, см. Комментарии). Вниз чаще встречается; это относится к x86, PowerPC, MIPS, SPARC, EE и Cell SPU. - Как локальные переменные функции расположены внутри стекового фрейма? Это не определено и совершенно непредсказуемо; компилятор может свободно размещать свои локальные переменные, однако ему нравится получать наиболее эффективный результат.
Направление роста стеков зависит от архитектуры. Тем не менее, я понимаю, что только очень немногие аппаратные архитектуры имеют стеки, которые растут.
Направление роста стека не зависит от макета отдельного объекта. Таким образом, в то время как стек может уменьшаться, массивы не будут (т.е. &array[n] всегда будет <& array [n + 1]);
В стандарте нет ничего, что предписывало бы, как все организовано в стеке вообще. Фактически, вы могли бы создать соответствующий компилятор, который вообще не сохранял бы элементы массива в смежных элементах в стеке, при условии, что у него есть ум, чтобы все еще правильно выполнять арифметику элементов массива (так, чтобы он знал, например, что 1 был 1K от [0] и может подстроиться под это).
Причина, по которой вы можете получать разные результаты, заключается в том, что, хотя стек может увеличиваться для добавления к нему "объектов", массив представляет собой один "объект", и он может иметь элементы восходящего массива в обратном порядке. Но полагаться на такое поведение небезопасно, поскольку направление может меняться, а переменные можно менять местами по разным причинам, включая, но не ограничиваясь:
- оптимизация.
- выравнивание.
- Прихоти человека часть управления стека компилятора.
Смотрите здесь для моего превосходного трактата о направлении стека:-)
В ответ на ваши конкретные вопросы:
- Стек растет или падает?
Это не имеет значения вообще (с точки зрения стандарта), но, как вы спросили, он может увеличиваться или уменьшаться в памяти, в зависимости от реализации. - Что происходит между [2] и q адресами памяти? Почему там большая разница в памяти? (20 байт)?
Это не имеет значения вообще (с точки зрения стандарта). Смотрите выше по возможным причинам.
На x86 "выделение" памяти стекового фрейма состоит просто из вычитания необходимого количества байтов из указателя стека (я считаю, что другие архитектуры похожи). В этом смысле, я предполагаю, что стек растет "вниз", поскольку адреса становятся все меньше по мере того, как вы обращаетесь к стеку глубже (но я всегда предполагаю, что память начинается с 0 в верхнем левом углу и получает большие адреса по мере движения направо и завернуть, так что в моем воображении стек растет...). Порядок объявляемых переменных может не иметь никакого отношения к их адресам - я полагаю, что стандарт позволяет компилятору переупорядочивать их, если это не вызывает побочных эффектов (кто-то, пожалуйста, исправьте меня, если я ошибаюсь), Они просто застряли где-то в этом промежутке в используемых адресах, созданных, когда он вычитает количество байтов из указателя стека.
Разрыв вокруг массива может быть своего рода дополнением, но для меня это загадочно.
Возрастает, и это происходит из-за порядка байтов с прямым порядком байтов, когда речь идет о наборе данных в памяти.
Один из способов, которым вы можете посмотреть на это, - это то, что стек ДОЛЖЕН расти вверх, если вы посмотрите на память с 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;
}
Я просто хочу упомянуть, что для совсем новичков. Слова «вверх» или «вниз» также могут звучать запутанно. Это зависит от того, как вы рисуете макет памяти. Обычно мы рисуем меньший адрес внизу, а больший адрес вверху, как показано ниже:
fff0
ffe0
ffd0
...
0010
0000
Тогда увеличение вниз означает, что адрес становится меньше.
Прежде всего, это 8 байт неиспользуемого пространства в памяти (его не 12, запомните, стек растет вниз, поэтому пространство, которое не выделено, составляет от 604 до 597). и почему?. Потому что каждый тип данных занимает место в памяти, начиная с адреса, кратного его размеру. В нашем случае массив из 3 целых занимает 12 байт пространства памяти, а 604 не делится на 12. Таким образом, он оставляет пустые места, пока не встретит адрес памяти, который делится на 12, то есть 596.
Таким образом, пространство памяти, выделенное массиву, составляет от 596 до 584. Но поскольку распределение массива продолжается, поэтому первый элемент массива начинается с адреса 584, а не с 596.
Компилятор может свободно размещать локальные (авто) переменные в любом месте локального фрейма стека, из которого вы не можете точно определить направление роста стека. Вы можете определить направление роста стека, сравнивая адреса вложенных фреймов стека, т.е. сравнивая адрес локальной переменной внутри фрейма стека функции по сравнению с ее вызываемым:
#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;
}
Это зависит от вашей операционной системы и вашего компилятора.
Стек действительно растет вниз. Таким образом, f(g(h())), стек, выделенный для h, будет начинаться с более низкого адреса, чем g, а g будет ниже, чем f. Но переменные в стеке должны соответствовать спецификации C,
http://c0x.coding-guidelines.com/6.5.8.html
1206 Если указанные объекты являются членами одного и того же агрегатного объекта, указатели на элементы структуры, объявленные позже, сравниваются больше, чем указатели на элементы, объявленные ранее в структуре, а указатели на элементы массива с большими значениями нижнего индекса сравниваются больше, чем указатели, на элементы того же самого массив с нижними значениями индекса.
& a [0] <& a [1], всегда должно быть истинным, независимо от того, как распределено "a"
Похоже, мой стек расширяется в сторону адресов с меньшими номерами.
Он может отличаться на другом компьютере или даже на моем собственном компьютере, если я использую другой вызов компилятора.... или компилятор 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 -pedantic stack.c
$./a.out уровень 4: х на 0x7fff7781190c уровень 3: х в 0x7fff778118ec уровень 2: х в 0x7fff778118cc уровень 1: х на 0x7fff778118ac уровень 0: х на 0x7fff7781188c
Стек растет (на x86). Однако стек загружается в один блок при загрузке функции, и у вас нет гарантии, в каком порядке элементы будут в стеке.
В этом случае он выделил пространство для двух целых и массива с тремя целыми числами в стеке. Он также выделил дополнительные 12 байтов после массива, так что это выглядит так:
[12 байт]
отступ (?) [12 байт]
с [4 байта]
q [4 байта]
По какой-то причине ваш компилятор решил, что ему нужно выделить 32 байта для этой функции и, возможно, больше. Это непрозрачно для вас как для программиста на C, вы не знаете почему.
Если вы хотите знать, почему, скомпилируйте код на ассемблер, я считаю, что это -S на gcc и /S на компиляторе C MS. Если вы посмотрите инструкции по открытию этой функции, то увидите, что старый указатель стека сохраняется, а затем из него вычитается 32 (или что-то еще!). Оттуда вы можете увидеть, как код обращается к этому 32-байтовому блоку памяти, и выяснить, что делает ваш компилятор. В конце функции вы видите восстановленный указатель стека.
Я не думаю, что это так детерминировано. Массив кажется "растущим", потому что эта память должна выделяться непрерывно. Однако, поскольку q и s вообще не связаны друг с другом, компилятор просто помещает каждый из них в произвольную свободную область памяти в стеке, возможно, те, которые лучше всего соответствуют целочисленному размеру.
Что произошло между a[2] и q, так это то, что пространство вокруг местоположения q было недостаточно большим (т. Е. Не больше 12 байт), чтобы выделить массив из 3 целых чисел.