Выделение памяти на стеке c

Обновлено: 06.07.2024

В этой статье будет объяснено несколько методов использования памяти стека и кучи в C++.

Разница между стеком и памятью кучи в C++

Когда мы хотим обсудить концепции памяти, лучше всего думать в терминах систем, в которых выполняются наиболее распространенные пользовательские программы. Большинство пользовательских программ выполняется в среде операционной системы, которая управляет аппаратными ресурсами за нас и решает различные задачи, которые были бы слишком сложными или неэффективными для выполнения пользовательской программой. Одна из таких задач - напрямую управлять аппаратной памятью. Таким образом, почти все операционные системы предоставляют специальные структуры и функции для взаимодействия с аппаратной памятью. Две общие концепции в структурах памяти, предоставляемых операционной системой, - это стек и куча.

Стек - это область памяти, зарезервированная для каждой запущенной программы в системе, и она работает по принципу LIFO. А именно, когда программа начинает выполнение main функции, последняя получает свой стековый фрейм (подмножество стековой памяти), в котором автоматически сохраняются локальные переменные и адреса возврата вызова функций. Как только main вызывает другую функцию, новый кадр стека создается после предыдущего непрерывно. Самый новый стековый фрейм будет хранить локальные объекты для соответствующей функции, и когда он вернет эту память, эта память не будет занята.

Обратите внимание, что размер стека по умолчанию в большинстве систем фиксирован, но может быть изменен в определенной степени, если у пользователя есть особые потребности. Ограничение размера стековой памяти делает ее подходящей для небольших и в основном временных объектов. Например, размер стека по умолчанию для пользовательской программы в операционной системе Linux составляет 8 МБ. Оно может быть меньше одной фотографии в формате JPEG, которую может потребоваться обработать программе, поэтому пользователь должен использовать это пространство с особой осторожностью. Все переменные, объявленные в следующем фрагменте кода, хранятся в памяти стека. Как правило, каждая локальная переменная размещается в стеке, если у нее нет специальных спецификаторов, таких как static или volatile .

С другой стороны, есть область памяти, называемая - куча (также называемая free store ), где могут храниться большие объекты, а выделения производятся программистом вручную во время выполнения. Эти две функции делают динамическую память кучи по своей природе, поскольку ее размер не нужно определять во время компиляции или в любой момент во время выполнения программы. Программа может вызывать специальные функции и запрашивать выделение памяти из операционной системы. Обратите внимание, что память кучи может показаться бесконечной с точки зрения программы, поскольку она не ограничивается вызовом другой функции распределения для запроса дополнительной памяти. Хотя операционная система управляет памятью для всех запущенных процессов; и он может отклонить новые распределения, когда больше нет доступной физической памяти.

Система памяти в операционной системе довольно сложна и требует понимания различных концепций, связанных с ОС / оборудованием, поэтому в этом разделе мы рассмотрим только больше, чем самый минимум, о памяти кучи и стека. Ручное управление динамической памятью на языке C++ может осуществляться с помощью операторов new / delete или функций malloc / free . Обратите внимание, что эти функции работают аналогичным образом, когда пользователь обычно указывает количество байтов для выделения, и он возвращает адрес, по которому был выделен такой же объем памяти. Следовательно, программист может работать с данной областью памяти по мере необходимости.

В следующем примере кода демонстрируется несколько случаев выделения различных объектов в динамической памяти. Одна из важных функций ручного управления памятью - вернуть выделенную область памяти обратно в операционную систему, когда она больше не нужна. Последняя операция выполняется с помощью вызовов delete / free , соответствующих их аналогам распределения. Если программа не освобождает ненужную память, существует риск того, что операционной системе не хватит памяти, и в результате программа может быть остановлена. Заметьте, однако, что предыдущая проблема, как ожидается, в основном возникнет в долго выполняющихся программах, и они характеризуются как ошибки утечки памяти.

Сегмент кода (или «текстовый сегмент»), где находится скомпилированная программа. Обычно доступен только для чтения.

Сегмент bss (или «неинициализированный сегмент данных»), где хранятся глобальные и статические переменные, инициализированные нулем.

Сегмент данных (или «сегмент инициализированных данных»), где хранятся инициализированные глобальные и статические переменные.

Куча, откуда выделяются динамические переменные.

Стек вызовов, где хранятся параметры функции, локальные переменные и другая информация, связанная с функциями.

Сегмент кучи (или просто «куча») отслеживает память, используемую для динамического выделения. Мы уже немного поговорили о куче на уроке о динамическом выделении памяти в языке С++.

В языке C++ при использовании оператора new динамическая память выделяется из сегмента кучи самой программы:

int * ptr = new int ; // для ptr выделяется 4 байта из кучи int * array = new int [ 10 ] ; // для array выделяется 40 байт из кучи

Адрес выделяемой памяти передается обратно оператором new и затем он может быть сохранен в указателе. О механизме хранения и выделения свободной памяти нам сейчас беспокоиться незачем. Однако стоит знать, что последовательные запросы памяти не всегда приводят к выделению последовательных адресов памяти!

// ptr1 и ptr2 могут не иметь последовательных адресов памяти

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

Куча имеет свои преимущества и недостатки:

Выделение памяти в куче сравнительно медленное.

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

Доступ к динамически выделенной памяти осуществляется только через указатель. Разыменование указателя происходит медленнее, чем доступ к переменной напрямую.

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

Стек вызовов

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

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

Стек как структура данных

Структура данных в программировании — это механизм организации данных для их эффективного использования. Вы уже видели несколько типов структур данных, например, массивы или структуры. Существует множество других структур данных, которые используются в программировании. Некоторые из них реализованы в Стандартной библиотеке C++, и стек как раз является одним из таковых.

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

Посмотреть на поверхность первой тарелки (которая находится на самом верху).

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

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

В компьютерном программировании стек представляет собой контейнер (как структуру данных), который содержит несколько переменных (подобно массиву). Однако, в то время как массив позволяет получить доступ и изменять элементы в любом порядке (так называемый «произвольный доступ»), стек более ограничен.

В стеке вы можете:

Посмотреть на верхний элемент стека (используя функцию top() или peek() ).

Вытянуть верхний элемент стека (используя функцию pop() ).

Добавить новый элемент поверх стека (используя функцию push() ).

Например, рассмотрим короткую последовательность, показывающую, как работает добавление и удаление в стеке:

Stack: empty
Push 1
Stack: 1
Push 2
Stack: 1 2
Push 3
Stack: 1 2 3
Push 4
Stack: 1 2 3 4
Pop
Stack: 1 2 3
Pop
Stack: 1 2
Pop
Stack: 1

Стопка тарелок довольно-таки хорошая аналогия работы стека, но есть лучшая аналогия. Например, рассмотрим несколько почтовых ящиков, которые расположены друг на друге. Каждый почтовый ящик может содержать только один элемент, и все почтовые ящики изначально пустые. Кроме того, каждый почтовый ящик прибивается гвоздем к почтовому ящику снизу, поэтому количество почтовых ящиков не может быть изменено. Если мы не можем изменить количество почтовых ящиков, то как мы получим поведение, подобное стеку?

Сегмент стека вызовов

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

Когда программа встречает вызов функции, то эта функция помещается в стек вызовов. При завершении выполнения функции, она удаляется из стека вызовов. Таким образом, просматривая функции, добавленные в стек, мы можем видеть все функции, которые были вызваны до текущей точки выполнения.

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

Стек вызовов на практике

Давайте рассмотрим детально, как работает стек вызовов. Ниже приведена последовательность шагов, выполняемых при вызове функции:

Программа сталкивается с вызовом функции.

Создается фрейм стека, который помещается в стек. Он состоит из:

адреса инструкции, который находится за вызовом функции (так называемый «обратный адрес»). Так процессор запоминает, куда ему возвращаться после выполнения функции;

памяти для локальных переменных;

сохраненных копий всех регистров, модифицированных функцией, которые необходимо будет восстановить после того, как функция завершит свое выполнение.

Процессор переходит к точке начала выполнения функции.

Инструкции внутри функции начинают выполняться.

После завершения функции, выполняются следующие шаги:

Регистры восстанавливаются из стека вызовов.

Фрейм стека вытягивается из стека. Освобождается память, которая была выделена для всех локальных переменных и аргументов.

Обрабатывается возвращаемое значение.

ЦП возобновляет выполнение кода (исходя из обратного адреса).

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

Знать все детали работы стека вызовов не так уж и важно. Однако понимание того, что функции при вызове добавляются в стек, а при завершении выполнения — удаляются из стека, дает основы, необходимые для понимания рекурсии, а также некоторых других концепций, которые полезны при отладке программ.

Массивы произвольной длинны и динамическое выделение памяти на стеке

В С90 нет возможности создать массив произвольной длины. Размер массива должен быть известен на момент компиляции

В стандарте С99 появилась возможность создавать массивы, размер которых не известен на момент компиляции. Например, можно передать функции размер массива и создать временный массив

Массивы произвольной длины нельзя инициализировать при создании. Например, следующий код не скомпилируется

Сделано это (по-видимому) из соображений безопасности. Если необходимо заполнить массив значениями, то это можно сделать поэлементно, либо, например, с помощью функции memset.

Тем не менее, VSE не поддерживает это нововведение и не собирается в дальнейшем.

Каким образом можно динамически создать массив произвольного размера на стеке? Очевидно, нам нужна такая функция, которая бы позволяла выделять память на стеке. Эта функция называется alloca, или _alloca и определена в библиотеке malloc.h

(Здесь и далее у нестандартных функций идёт префиксом подчёркивание. В GNU версиях компиляторов его нет). Так как память выделяется на стеке, то нет необходимости её подчищать – после выхода из функции стек будет восстановлен и локальные переменные «уничтожены». К тому же, выделение памяти происходит очень быстро (на несколько порядков быстрее, чем выделение на куче) и не приводит к фрагментации.

Но здесь возникает та же проблема, что и с массивами произвольной длины – если выделить слишком много памяти, то произойдёт переполнение стека. Функция _alloca, в отличие от malloc, не возвращает NULL, если не смогла выделить память, а создаёт структурированное исключение переполнения стека. Например, вызовите

Поэтому предлагают такой способ выделения

Не очень-то красиво. Освобождение памяти происходит только после выхода из функции, а не после выхода за пределы видимости. Функция _alloca была в дальнейшем запрещена и заменена на функцию _malloca. Он отличается тем, что сначала пытается выделить место на стеке, а если не смогла этого сделать, то выделяет место на куче, то есть, работает как malloc. Однако если память может быть выделена на куче, то её придётся освобождать после себя. Для этого используется функция _freea. Если память была выделена на стеке, то функция ничего не делает. Если память была выделена на куче, то _freea освобождает её как стандартная free. Это цена за более безопасное использование.

malloca в случае выделения памяти на куче ведёт себя как и malloc. То есть, если память не удалось выделить на куче, то будет возвращён NULL.

ЗАМЕЧАНИЕ: _malloca будет выделять память на стеке только в релизе. В отладочной версии проекта память всегда выделяется на куче.

email

Всё ещё не понятно? – пиши вопросы на ящик

Выделение памяти для массива в стеке
Правильно ли так выделять память? в gss работает в vs нет.тут память получается на стеки.

Выделение памяти, проверка на утечку памяти
Интересуют два вопроса: 1. Правильно ли устроен алгоритм выделения, удаление и запись ячейки.

Во-первых, заталкивать в стек значения можно и просто обращаясь к регистру esp Подразумевается, что компилятор. Размещает локальные переменные, превращая код такой функции

но всё зависит от компилятора и может быть не совсем так

Добавлено через 5 минут
Во-первых, заталкивать в стек значения можно и просто обращаясь к регистру esp

Подразумевается, что компилятор. Размещает локальные переменные, превращая код такой функции
но всё зависит от компилятора и может быть не совсем так Воу воу. Вы меня простите, я в асме не силён, по-этому, если можно, по-порядку.
Во-первых, заталкивать в стек значения можно и просто обращаясь к регистру esp

Как так? Разве не add esp, 4 надо делать? Ведь вычитание (по идее) аналог pop

Могу только предположить, что esp указывает на следующий элемент стека (сразу после 4 байт)

Только мне никак не понять строку:

Что в ней происходит? И dword - это не то же, что и dw в MASM?
Спасибо! Стек - условно, уже выделенная для программы память некоторого объема. Для этой памяти есть указатель - вершина стека. При создании новой переменной на стеке, указатель смещается на sizeof(тип переменной) и память инициализируется должным образом (конструктором). При уничтожении переменной (выход из области видимости) вызывается деструктор объекта и указатель стека смещается обратно на sizeof объекта. Альтернативный вариант подразумевает выделение памяти в куче (оператор new), и освобождение после работы (оператор delete). Хорошо, допустим мне стало ясно, как располагаются переменные в стеке. Допустим мы создали штук 5 таких локальных переменных. Но как происходит обращение к ним? Через смещения относительно esp? Или как? dword ptr[ebp-4]
dword ptr[ebp-8]
как-то так. указывается смещение, относительно esp Ясно, спасибо. Теперь понятно. Просто раньше я не знаю, что мы можем по элементам стека обращаться через указатель. Мне всегда казалось, что эту задачу выполняют только pop и push. Есть превосходно написанная, свободно распространяемая книга Столярова Андрея Викторовича "Программирование на языке ассемблера NASM для ОС Unix". В параграфе 2.6 рассмотрены все интересующие Вас детали.
Вообще аналогию стека как стопки листов (патронов в рожке автомата, стопки тарелок и т.п.) считаю крайне неудачной. Указатель стека - это лишь значение регистра SP/ESP на ячейки памяти, где размещаются данные. Из истории: регистр специального назначения R6 в 16-битном процессоре PDP-11 ныне почившей фирмы DEC выполнял то же самое действие (эх, прекрасный у них был ассемблер вместо уродливого Intel'овского). В процессоре PDP-11 была команда при этом значение указателя стека уменьшается на размер операнда ДО выполнения операции, а извлечение из стека приводит к изменению указателя стека ПОСЛЕ выполнения операции. Поэтому аналогия типа "для извлечения четвёртой тарелки из стопки от вершины стека мы берем третье слово (поскольку счёт идёт от нуля)"

даёт искажённое представление, т.к. в этой аналогии нет возможности взять четвёртую тарелку из воздуха (т.е. адресоваться по отрицательному смещению, а это возможно).
Знание о том, как работает стек, нужно при рассмотрении работы подпрограмм (call, enter, leave), сохранении/восстановлении всех регистров (pusha/popa).

В свободное время неспешно читаю восьмисотстраничную "Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software" авторов Michael Sikorski и Andrew Honig, вышедшую в 2012 году в издательстве No Starch Press, Inc. - про анализ зловредов. Затрагивается в т.ч. передача параметров в функцию соглашениями cdecl, pascal (stdcall), fastcall. Изумительная книга, даже несмотря на то, что написана на английском. Рекомендую к прочтению, хотя бы для общего развития. В Сети легко находится её электронный вариант.

Читайте также: