Что такое стек процесса

Что такое стек процесса

Primary tabs

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

Эта часть памяти называется стеком, так как она организована по принципу стека — последний записанный в него элемент будет забыт первым.
На русский язык слово "стек" можно перевести как "стопка". Для "визуализации" понятия "стек" можно представить себе стопку тяжелых, неподъемных книг. Читать корешки у книг можно в любом порядке, но новую книгу можно положить только сверху, и взять книгу из стопки можно так же только верхнюю.

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

Фрейм стека — это блок данных, описывающих информацию о ходе работы одной подпрограммы, выполняющейся в данный момент. В него входят:

  • 1. параметры подпрограммы
  • 2. адрес возврата из подпрограммы
  • 3. сохраненные регистры процессора на момент вызова подпрограммы (такое замечание: один из сохраненных регистров на архитектуре x86 — это ebp, который, по соглашению, используется в качестве указателя на текущий фрейм стека с небольшим смещением. Иными словами, в фрейме вызванной подпрограммы записывается адрес фрейма вызывающей подпрограммы, благодаря этому многие отладчики могут восстановить всю цепочку вызовов. Однако, это соглашение может и нарушаться, особенно в коде на ассемблере или в коде, скомпилированном с оптимизацией)
  • 4. фрейм обработки исключений, на платформе Windows состоит из указателя на предыдущий фрейм обработки исключений и на функцию-обработчик исключений. В зависимости от реализации, число и местоположение этих фреймов может меняться, в том числе и во времени
  • 5. локальные переменные

Размер фрейма стека может динамически меняться в ходе работы программы до тех пор пока он в стеке остается верхним. Кроме того, в некоторых языках программирования есть специальные функции, которые не создают своих фреймов, а меняют родительский — так, на С/С++ этим "грешат" setjmp/longjmp и alloca

Вообще, фрейм стека создается самой подпрограммой в начале ее работы (это называется "пролог" подпрограммы) и уничтожается в конце (как называется — не знаю, "эпилог"?). Но, например, на ассемблере программист сам определяет поведение подпрограммы, да и на языках высокого уровня есть возможность отказаться от пролога (например, __declspec(naked) в Visual C/C++), так что создавать полный фрейм стека или нет — определяется решением программиста.

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

Так, соглашение о вызове cdecl, применяемое для функций языков C и C++, означает, что параметры в стек кладутся в обратном порядке, а извлекаются вызывающей подпрограммой. Это позволяет использовать подпрограммы с переменным числом аргументов, поскольку в таком случае первый параметр, отвечающий за количество аргументов, оказывается ближе всего к вершине стека, а, значит, расстояние между ним и вершиной стека постоянное. Это позволяет вызываемое подпрограмме его найти.

Как я уже сказал в самом начале, стеки существуют не у процессов, а у потоков. Это связано с тем, что для разных потоков цепочка вызовов подпрограмм может быть совершенно разной. Это не является неудобством, скорее это даже хорошо, поскольку позволяет для реализации переключения потоков просто подменять текущий указатель вершины стека, подменяя таким образом весь стек вызовов. Остальные величины (например, регистры процессора) явно сохранять и подменять не надо — достаточно сохранить их в стеке, а потом восстановить. Таким образом, если отбросить мишуру из маркеров безопасности, блокировок и прочего, то на принципиальном уровне поток — это всего лишь сохраненный указатель на вершину стека (в процессорах x86 — регистр esp или регистровая пара ss:esp)

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

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

добавка
Текст прог вообще нигде не хранится
После компиляции и линковки уже нет текста, а есть только машинные коды (если не включены опции компилятора закачивающие текст внутрь проги для последующего дебага)
переменные — тут по разному. могут в память основного процесса пихаться а могут динамически появляться-исчезать
зависит от программёра, языка и ОС.

Читайте также:  Силиконовые шланги для полива

Стек (stack) — область памяти, в которой переменные хранятся методом LIFO (Last In First Out).
Как правило, стек процесса используется функциями для быстрого размещения локальных переменных в памяти. Например, функция

void my_function(const int N)
<
float my_array[N];
>

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

Надо отличать стек от свободной памяти (heap). Чтобы в C разместить данные в свободной памяти, используется malloc:

void my_function(const int N)
< // Плохая функция!
// Функции должны размещать данные на стеке во избежание утечек памяти и для ускорения работы!
float* my_array=(float*)malloc(sizeof(float)*N);
free(my_array);
>

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

Свободная память позволяет разместить большой объём информации, но обладает двумя недостатками:

1) Свободная память не использует LIFO, поэтому данные в свободной памяти могут быть фрагментированы. Из-за этого размещение данных в свободной памяти, а также их чтение, занимают больше времени, чем размещение и чтение из стека.

2) Стек функции освобождается от данных автоматически при завершении функции (опять-таки, благодаря LIFO). А чтобы убрать данные из свободной памяти, нужно вызывать функцию free(). Если забыть это сделать, использованная область свободной памяти останется зарезервированной. Это может приводить к "утечкам памяти" — ошибкам, при которых программа занимает по мере работы всё больше и больше памяти без какой-либо пользы.

Свободную память надо использовать для переменных, имеющих долгое время жизни. Для короткоживущих (и малых по объёму) данных надо использовать стек.


Управление памятью – центральный аспект в работе операционных систем. Он оказывает основополагающее влияние на сферу программирования и системного администрирования. В нескольких последующих постах я коснусь вопросов, связанных с работой памяти. Упор будет сделан на практические аспекты, однако и детали внутреннего устройства игнорировать не будем. Рассматриваемые концепции являются достаточно общими, но проиллюстрированы в основном на примере Linux и Windows, выполняющихся на x86-32 компьютере. Первый пост описывает организацию памяти пользовательских процессов.

Каждый процесс в многозадачной ОС выполняется в собственной “песочнице”. Эта песочница представляет собой виртуальное адресное пространство, которое в 32-битном защищенном режиме всегда имеет размер равный 4 гигабайтам. Соответствие между виртуальным пространством и физической памятью описывается с помощью таблицы страниц (page table). Ядро создает и заполняет таблицы, а процессор обращается к ним при необходимости осуществить трансляцию адреса. Каждый процесс работает со своим набором таблиц. Есть один важный момент — концепция виртуальной адресации распространяется на все выполняемое ПО, включая и само ядро. По этой причине для него резервируется часть виртуального адресного пространства (т.н. kernel space).

Это конечно не значит, что ядро занимает все это пространство, просто данный диапазон адресов может быть использован для мэппирования любой части физического адресного пространства по выбору ядра. Страницы памяти, соответствующие kernel space, помечены в таблицах страниц как доступные исключительно для привилегированного кода (кольцо 2 или более привилегированное). При попытке обращения к этим страницам из user mode кода генерируется page fault. В случае с Linux, kernel space всегда присутствует в памяти процесса, и разные процессы мэппируют kernel space в одну и ту же область физической памяти. Таким образом, код и данные ядра всегда доступны при необходимости обработать прерывание или системный вызов. В противоположность, оперативная память, замэппированная в user mode space, меняется при каждом переключении контекста.

Синим цветом на рисунке отмечены области виртуального адресного пространства, которым в соответствие поставлены участки физической памяти; белым цветом — еще не использованные области. Как видно, Firefox использовал большую часть своего виртуального адресного пространства. Все мы знаем о легендарной прожорливости этой программы в отношении оперативной памяти. Синие полосы на рисунке — это сегменты памяти программы, такие как куча (heap), стек и так далее. Обратите внимание, что в данном случае под сегментами мы подразумеваем просто непрерывные адресные диапазоны. Это не те сегменты, о которых мы говорим при описании сегментации в Intel процессорах. Так или иначе, вот стандартная схема организации памяти процесса в Linux:

Давным давно, когда компьютерная техника находилась в совсем еще младенческом возрасте, начальные виртуальные адреса сегментов были совершенно одинаковыми почти для всех процессов, выполняемых машиной. Из-за этого значительно упрощалось удаленное эксплуатирование уязвимостей. Эксплойту часто необходимо обращаться к памяти по абсолютным адресам, например по некоторому адресу в стеке, по адресу библиотечной функции, и тому подобное. Хакер, рассчитывающий осуществить удаленную атаку, должен выбирать адреса для обращения в слепую в расчете на то, что размещение сегментов программы в памяти на разных машинах будет идентичным. И когда оно действительно идентичное, случается, что людей хакают. По этой причине, приобрел популярность механизм рандомизации расположения сегментов в адресном пространстве процесса. Linux рандомизирует расположение стека, сегмента для memory mapping, и кучи – их стартовый адрес вычисляется путем добавления смещения. К сожалению, 32-битное пространство не очень-то большое, и эффективность рандомизации в известной степени нивелируется.

Читайте также:  Как пересадить цветок мужское счастье видео

В верхней части user mode space расположен стековый сегмент. Большинство языков программирования используют его для хранения локальных переменных и аргументов, переданных в функцию. Вызов функции или метода приводит к помещению в стек т.н. стекового фрейма. Когда функция возвращает управление, стековый фрейм уничтожается. Стек устроен достаточно просто — данные обрабатываются в соответствии с принципом «последним пришёл — первым обслужен» (LIFO). По этой причине, для отслеживания содержания стека не нужно сложных управляющих структур – достаточно всего лишь указателя на верхушку стека. Добавление данных в стек и их удаление – быстрая и четко определенная операция. Более того, многократное использование одних и тех же областей стекового сегмента приводит к тому, что они, как правило, находятся в кеше процессора, что еще более ускоряет доступ. Каждый тред в рамках процесса работает с собственным стеком.

Возможна ситуация, когда пространство, отведенное под стековый сегмент, не может вместить в себя добавляемые данные. В результате, будет сгенерирован page fault, который в Linux обрабатывается функцией expand_stack(). Она, в свою очередь, вызовет другую функцию — acct_stack_growth(), которая отвечает за проверку возможности увеличить стековый сегмент. Если размер стекового сегмента меньше значения константы RLIMIT_STACK (обычно 8 МБ), то он наращивается, и программа продолжает выполняться как ни в чем не бывало. Это стандартный механизм, посредством которого размер стекового сегмента увеличивается в соответствии с потребностями. Однако, если достигнут максимально разрещённый размер стекового сегмента, то происходит переполнение стека (stack overflow), и программе посылается сигнал Segmentation Fault. Стековый сегмент может увеличиваться при необходимости, но никогда не уменьшается, даже если сама стековая структура, содержащаяся в нем, становиться меньше. Подобно федеральному бюджету, стековый сегмент может только расти.

Динамическое наращивание стека – единственная ситуация, когда обращение к «немэппированной» области памяти, может быть расценено как валидная операция. Любое другое обращение приводит к генерации page fault, за которым следует Segmentation Fault. Некоторые используемые области помечены как read-only, и обращение к ним также приводит к Segmentation Fault.

Под стеком располагается сегмент для memory mapping. Ядро использует этот сегмент для мэппирования (отображания в память) содержимого файлов. Любое приложение может воспользоваться данным функционалом посредством системного вызовома mmap() (ссылка на описание реализации вызова mmap) или CreateFileMapping() / MapViewOfFile() в Windows. Отображение файлов в память – удобный и высокопроизводительный метод файлового ввода / вывода, и он используется, например, для загрузки динамических библиотек. Существует возможность осуществить анонимное отображение в память (anonymous memory mapping), в результате чего получим область, в которую не отображен никакой файл, и которая вместо этого используется для размещения разного рода данных, с которыми работает программа. Если в Linux запросить выделение большого блока памяти с помощью malloc(), то вместо того, чтобы выделить память в куче, стандартная библиотека C задействует механизм анонимного отображения. Слово «большой», в данном случае, означает величину в байтах большую, чем значение константы MMAP_THRESHOLD. По умолчанию, это величина равна 128 кБ, и может контролироваться через вызов mallopt().

Кстати о куче. Она идет следующей в нашем описании адресного пространства процесса. Подобно стеку, куча используется для выделения памяти во время выполнения программы. В отличие от стека, память, выделенная в куче, сохранится после того, как функция, вызвавшая выделение этой памяти, завершится. Большинство языков предоставляют средства управления памятью в куче. Таким образом, ядро и среда выполнения языка совместно осуществляют динамическое выделение дополнительной памяти. В языке C, интерфейсом для работы с кучей является семейство функций malloc(), в то время как в языках с поддержкой garbage collection, вроде C#, основной интерфейс – это оператор new.

Читайте также:  Утепление металлических дверей пеноплексом

Если текущий размер кучи позволяет выделить запрошенный объем памяти, то выделение может быть осуществлено средствами одной лишь среды выполнения, без привлечения ядра. В противном случае, функция malloc() задействует системный вызов brk() для необходимого увеличения кучи (ссылка на описание реализации вызова brk). Управление памятью в куче – нетривиальная задача, для решения которой используются сложные алгоритмы. Данные алгоритмы стремятся достичь высокой скорости и эффективности в условиях непредсказуемых и хаотичных пэттернов выделения памяти в наших программах. Время, затрачиваемое на каждый запрос по выделению памяти в куче, может разительно отличаться. Для решения данной проблемы, системы реального времени используют специализированные аллокаторы памяти. Куча также подвержена фрагментированию, что, к примеру, изображено на рисунке:

Наконец, мы добрались до сегментов, расположенных в нижней части адресного пространства процесса: BSS, сегмент данных (data segment) и сегмент кода (text segment). BSS и data сегмент хранят данные, соответствующий static переменным в исходном коде на C. Разница в том, что в BSS хранятся данные, соответствующие неинициализированным переменным, чьи значения явно не указаны в исходном коде (в действительности, там хранятся объекты, при создании которых в декларации переменной либо явно указано нулевое значение, либо значение изначально не указано, и в линкуемых файлах нет таких же common символов, с ненулевым значением. – прим. перевод.). Для сегмента BSS используется анонимное отображение в память, т.е. никакой файл в этот сегмент не мэппируется. Если в исходном файле на C использовать int cntActiveUsers, то место под соответствующий объект будет выделено в BSS.

В отличии от BSS, data cегмент хранит объекты, которым в исходном коде соответствуют декларации static переменных, инициализированных ненулевым значением. Этот сегмент памяти не является анонимным — в него мэппируется часть образа программы. Таким образом, если мы используем static int cntWorkerBees = 10, то место под соответствующий объект будет выделено в data сегменте, и оно будет хранить значение 10. Хотя в data сегмент отображается файл, это т.н. «приватный мэппинг» (private memory mapping). Это значит, что изменения данных в этом сегменте не повлияют на содержание соответствующего файла. Так и должно быть, иначе присвоения значений глобальным переменным привели бы к изменению содержания файла, хранящегося на диске. В данном случае это совсем не нужно!

С указателями все немножко посложнее. В примере из наших диаграмм, содержимое объекта, соответствующего переменной gonzo – это 4-байтовый адрес – размещается в data сегменте. А вот строка, на которую ссылается указатель, не попадет в data сегмент. Строка будет находиться в сегменте кода, который доступен только на чтение и хранит весь Ваш код и такие мелочи, как, например, строковые литералы (в действительности, строка хранится в секции .rodata, которая вместе с другими секциями, содержащими исполняемый код, рассматривается как сегмент, который загружается в память с правами на выполнение кода / чтения данных – прим. перевод.). В сегмент кода также мэппируется часть исполняемого файла. Если Ваша программа попытается осуществить запись в text сегмент, то заработает Segmentation Fault. Это позволяет бороться с «бажными» указателями, хотя самый лучший способ борьбы с ними – это вообще не использовать C. Ниже приведена диаграмма, изображающая сегменты и переменные из наших примеров:

Мы можем посмотреть, как используются области памяти процесса, прочитав содержимое файла /proc/pid_of_process/maps. Обратите внимание, что содержимое самого сегмента может состоять из различных областей. Например, каждой мэппируемой в memory mapping сегмент динамической библиотеке отводится своя область, и в ней можно выделить области для BSS и data сегментов библиотеки. В следующем посте поясним, что конкретно подразумевается под словом “область”. Учтите, что иногда люди говорят “data сегмент”, подразумевая под этим data + BSS + heap.

Можно использовать утилиты nm и objdump для просмотра содержимого бинарных исполняемых образов: символов, их адресов, сегментов и т.д. Наконец, то, что описано в этом посте – это так называемая “гибкая” организация памяти процесса (flexible memory layout), которая вот уже несколько лет используется в Linux по умолчанию. Данная схема предполагает, что у нас определено значение константы RLIMIT_STACK. Когда это не так, Linux использует т.н. классическую организации, которая изображена на рисунке:

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

Ссылка на основную публикацию
Adblock detector