Чтобы лучше понять, что происходит, давайте представим, что у нас есть только очень примитивная операционная система, работающая на 16-битном процессоре, которая может запускать только один процесс за раз. Это означает, что одновременно может работать только одна программа. Кроме того, давайте представим, что все прерывания отключены.
В нашем процессоре есть конструкция, называемая стеком. Стек является логической конструкцией, навязываемой физической памяти. Допустим, наша оперативная память существует в адресах E000 - FFFF. Это означает, что наша работающая программа может использовать эту память любым удобным для нас способом. Давайте представим, что наша операционная система говорит, что E000 - EFFF - это стек, а F000 - FFFF - это куча.
Стек поддерживается аппаратными средствами и машинными инструкциями. Там действительно не так много, что нам нужно сделать, чтобы поддерживать его. Все, что нам (или нашей ОС) нужно сделать, это убедиться, что мы установили правильный адрес для начала стека. Указатель стека - это физический объект, находящийся в аппаратном обеспечении (процессоре) и управляемый инструкциями процессора. В этом случае наш указатель стека будет установлен в EFFF (при условии, что стек увеличивается BACKWARDS, что довольно распространено, -). В скомпилированном языке, таком как C, когда вы вызываете функцию, она помещает любые аргументы, которые вы передали функции в стек. Каждый аргумент имеет определенный размер. int обычно 16 или 32 бита, char обычно 8 бит и т. д. Давайте представим, что в нашей системе int и int * имеют 16 бит. Для каждого аргумента указатель стека DECREMENTED (-) на sizeof (аргумент), и аргумент копируется в стек. Затем любые переменные, которые вы объявили в области видимости, помещаются в стек таким же образом, но их значения не инициализируются.
Давайте еще раз рассмотрим два примера, аналогичных вашим двум.
int hello(int eeep)
{
int i;
int *p;
}
В нашей 16-битной системе происходит следующее:
1) толкнуть ушко на стек. Это означает, что мы уменьшаем указатель стека до EFFD (потому что sizeof (int) равно 2), а затем фактически копируем eeep в адрес EFFE (текущее значение нашего указателя стека, минус 1, потому что указатель нашего стека указывает на первое доступное место после выделения). Иногда существуют инструкции, которые могут выполнить оба действия одним махом (при условии, что вы копируете данные, которые помещаются в регистр. В противном случае вам придется вручную копировать каждый элемент типа данных в его надлежащее место в стеке - иначе это важно! ).
2) создать пространство для i. Это, вероятно, означает просто уменьшение указателя стека на EFFB.
3) создать пространство для р. Это, вероятно, означает просто уменьшение указателя стека до EFF9.
Затем наша программа запускается, помня, где живут наши переменные (eeep начинается в EFFE, i в EFFC и p в EFFA). Важно помнить, что, хотя стек и имеет значение BACKWARDS, переменные по-прежнему работают с FORWARDS (на самом деле это зависит от порядка байтов, но дело в том, что & eeep == EFFE, а не EFFF).
Когда функция закрывается, мы просто увеличиваем (++) указатель стека на 6 (потому что в стек помещены 3 «объекта», а не вида c ++, размера 2.
Теперь, ваш второй сценарий гораздо сложнее объяснить, потому что есть так много способов его реализовать, что практически невозможно объяснить в Интернете.
int hello(int eeep)
{
int *p = malloc(sizeof(int));//C's pseudo-equivalent of new
free(p);//C's pseudo-equivalent of delete
}
eeep и p по-прежнему помещаются и помещаются в стек, как в предыдущем примере. В этом случае, однако, мы инициализируем p результатом вызова функции. Что malloc (или new, но new делает больше в c ++. Он вызывает конструкторы, когда это уместно, и все остальное.) Делает это, он идет в этот черный ящик, называемый HEAP, и получает адрес свободной памяти. Наша операционная система будет управлять кучей для нас, но мы должны дать ей знать, когда мы хотим память и когда мы закончим с ней.
В примере, когда мы вызываем malloc (), ОС вернет блок из 2 байтов (sizeof (int) в нашей системе равно 2), предоставив нам начальный адрес этих байтов. Допустим, первый звонок дал нам адрес F000. Затем ОС отслеживает, какие адреса F000 и F001 используются в настоящее время. Когда мы вызываем free (p), ОС находит блок памяти, на который указывает p, и помечает 2 байта как неиспользуемый (потому что sizeof (звездочка p) равен 2). Если вместо этого мы выделим больше памяти, адрес F002, скорее всего, будет возвращен как начальный блок новой памяти. Обратите внимание, что сама функция malloc () является функцией. Когда p помещается в стек для вызова malloc (), p снова копируется в стек по первому открытому адресу, который имеет достаточно места в стеке, чтобы соответствовать размеру p (возможно, EFFB, потому что мы выдвинули только 2 в стеке это время размера 2, а sizeof (p) равно 2), и указатель стека снова уменьшается до EFF9, а malloc () помещает свои локальные переменные в стек, начиная с этого места. Когда malloc завершает работу, он выталкивает все свои элементы из стека и устанавливает указатель стека на то, что было до вызова. Возвращаемое значение malloc (), пустой звезды, скорее всего, будет помещено в некоторый регистр (обычно аккумулятор во многих системах) для нашего использования.
В реализации оба примера ДЕЙСТВИТЕЛЬНО не так просты. Когда вы выделяете стековую память для нового вызова функции, вы должны убедиться, что вы сохранили свое состояние (сохраните все регистры), чтобы новая функция не стирала значения постоянно. Как правило, это также включает размещение их в стеке. Таким же образом вы обычно сохраняете регистр счетчика программы, чтобы вы могли вернуться в правильное место после возврата подпрограммы. Менеджеры памяти используют свою собственную память, чтобы «запомнить», какая память выдана, а какая нет. Виртуальная память и сегментация памяти усложняют этот процесс, и алгоритмы управления памятью должны постоянно перемещать блоки (и защищать их тоже), чтобы предотвратить фрагментацию памяти (отдельная тема), и это связано с виртуальной памятью также. Второй пример - большая банка червей по сравнению с первым примером. Кроме того, выполнение нескольких процессов делает все это намного более сложным, поскольку каждый процесс имеет свой собственный стек, и доступ к куче может иметь более одного процесса (что означает, что он должен защищать себя). Кроме того, каждая архитектура процессора отличается. Некоторые архитектуры ожидают, что вы установите указатель стека на первый свободный адрес в стеке, другие ожидают, что вы укажете его на первое несвободное место.
Надеюсь, это помогло. пожалуйста, дайте мне знать.
обратите внимание, что все приведенные выше примеры предназначены для вымышленной машины, которая чрезмерно упрощена. На реальном оборудовании это становится немного более волосатым.
edit: звездочки не отображаются. я заменил их словом «звезда»
Для чего стоит, если мы используем (в основном) один и тот же код в примерах, заменяя «hello» на «example1» и «example2» соответственно, мы получим следующий вывод сборки для intel для wndows.
.file "test1.c"
.text
.globl _example1
.def _example1; .scl 2; .type 32; .endef
_example1:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
leave
ret
.globl _example2
.def _example2; .scl 2; .type 32; .endef
_example2:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl $4, (%esp)
call _malloc
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, (%esp)
call _free
leave
ret
.def _free; .scl 3; .type 32; .endef
.def _malloc; .scl 3; .type 32; .endef