Структура загруженных в память программ
Программирование на языках, которые позволяют взаимодействовать с памятью на более низком уровне, как например в C и C++, иногда доставляет немало проблем, с которыми вы раньше не сталкивались, например: segfaults (ошибки сегментации). Такие ошибки очень раздражают, и могут стать причиной множества проблем; часто они свидетельствуют о том, что вы используете память, которую не следует использовать.
Одна из самых распространённых проблем — это попытка получить доступ к памяти, которая уже освобождена. Эту память вы могли освободить сами ― функцией free, или её автоматически освободила программа (например, из стека).
Поняв некоторые аспекты, связанные с памятью, вы станете действительно лучше и умнее в программировании!
Как разделена память
Память разделена на несколько сегментов, два из наиболее важных (для этой статьи) — это стек и heap. Стек — это упорядоченная область внедрения, а heap полностью произвольная ― вы резервируете память там, где это возможно.
Стек память обладает набором средств и операций для собственной работы (это место, где сохраняется информация регистров процессора). Здесь хранится информация, касающаяся работы программы, например о вызванных функциях и созданных переменных и т.д. Этой памятью управляет программа, а не разработчик.
Сегмент heap часто используется для резервирования большого объёма памяти. Этот резерв существует столько, сколько потребуется разработчику. Другими словами, контроль памяти в сегменте heap предоставлен разработчику. Разрабатывая сложные программы, часто приходится резервировать большие части памяти. Именно для этого и предназначен сегмент heap. Мы называем это ― Динамическая память.
Каждый раз, когда вы используете malloc, чтобы разместить что-либо в памяти – вы обращаетесь к сегменту heap. Любой другой вызов, например int i;, относится к стек памяти. Очень важно это понимать, чтобы с лёгкостью находить ошибки, в том числе ошибки сегментации.
Понимание стека
Ваша программа постоянно резервирует стек память, вы об этом даже не задумываетесь. Каждая функция и локальная переменная, которую вы вызываете, попадает в стек. Большинство из того, что можно сделать со стеком ― вам следует избегать, например переполнение буфера или некорректный доступ к памяти и т.д.
Как это работает изнутри?
Стек имеет структуру данных LIFO (Last-In-First-Out) ― «последним пришёл –первым ушёл». Представьте аккуратную стопку книг. В этой стопке вы можете взять первой, ту книгу, которую положили последней. Такая структура позволяет программе управлять своими операциями и пространствами, двумя простыми командами: push и pop . Команда push добавляет значение сверху стека, а pop наоборот ― изымает значение.
Для отслеживания текущего места в памяти существует специальный регистр процессора ― Stack Pointer (указатель стека). Например, переменные или обратный адрес из функции при сохранении попадают в стек и stack pointer перемещается вверх. Завершение функции означает, что всё изымается из стека, начиная с текущего положения stack pointer, до сохранённого обратного адреса из функции. Всё просто!
Чтобы проверить, всё ли вы поняли, давайте используем следующий пример (попробуйте найти ошибку самостоятельно ☺️):
Если запустить программу, она выдаст ошибку сегментации. Почему так происходит? Кажется, что всё на своих местах! За исключением… стека.
Когда мы вызываем функцию createArray, стек сохраняет обратный адрес, создаёт arr в памяти стека и возвращает arr (массив — это просто указатель на область памяти с этой информацией). Но, так как мы не использовали malloc, arr остаётся в стек памяти. После того как мы возвращаем указатель, так как мы не контролируем операции стека, программа изымает информацию из стека и использует её для своих нужд. Когда мы пытаемся заполнить массив, после того как вернули его из функции, мы повреждаем память и получаем ошибку сегментации.
Понимание кучи (heap)
В отличии от стека, в куче сохраняется что-либо, независимо от функций и пространств, в течение необходимого времени. Для использования этой памяти, в языке C есть библиотека stdlib с функциями malloc и free.
Malloc (memory allocation) запрашивает у системы необходимый объем памяти и возвращает указатель на начальный адрес. Free сообщает системе, что запрошенная память больше не нужна и может быть использована для других задач. Выглядит действительно просто ― до тех пор, пока вы избегаете ошибок.
Поскольку система не может перезаписать то, что запросил разработчик, мы должны сами управлять этим процессом с помощью этих двух функций. Это создаёт опасность возникновения утечек памяти.
Утечка произойдёт, если память не освободить вовремя, до завершения выполнения программы или если указатели на расположение этой памяти были потеряны. Программа будет использовать больше памяти чем должна. Чтобы этого не случалось, мы должны освобождать выделенную в куче память, когда она нам не нужна.
На изображении видно, как в «bad» версии кода, мы не освобождаем память. Это приводит к растрачиванию 20 * 4 байт (размер int 64-бит) = 80 байт. Может показаться, что это незначительно, но, если это большая программа, речь может идти о гигабайтах!
Чтобы программа эффективно использовала память — важно контролировать память кучи. В тоже время, делать это нужно с осторожностью. Если память была освобождена, то попытка получения доступа к ней или её использование может привести к ошибке сегментации.
Бонус: Struct и куча
Одна из распространённых ошибок при использовании struct заключается в том, что мы просто освобождаем struct. Всё прекрасно до тех пор, пока мы не выделяем память для указателей, внутри struct. В этом случае нам нужно сначала очистить их, а потом struct.
Как я устраняю утечки памяти
Программируя на C, я часто использую struct, поэтому у меня всегда под рукой две необходимые функции: конструктор и деструктор. Это единственные функции, для которых я использую mallocs и frees в struct. Это упрощает решение проблем с утечкой памяти.
Рустэм Галеев aka Roustem
3. Исполняемые файлы Windows
Как сделать, чтобы программа заработала? Работа приложения начинается с того, что операционная система создает процесс. Это не просто загруженная в память программа пользователя; процесс предполагает создание множества внутренних системных структур для обеспечения работы программы и предоставления ей различных ресурсов, таких как память, процессорное время, доступ к установленному в системе оборудованию и т.д.Важнейшим ресурсом являетcя виртуальная память. Каждый процесс получает в свое распоряжение собственное виртуальное адресное пространство памяти размером 4 Гб. Это значит, что он может обращаться по любому из адресов памяти от 0 до FFFFFFFFh. Но это значит также и то, что различные процессы могут использовать одни и те же адреса, не мешая друг другу. Система работает с памятью в виде блоков фиксированного размера, называемых страницами (обычно по 4 Кб; на современных процессорах могут быть страницы также по 2 Мб) и использует страничную переадресацию для отображения одних и тех же виртуальных адресов различных процессов в разные области физической памяти. Кроме того, при недостатке физической памяти временно неиспользуемые данные могут сохраняться на диске, освобождая физическую память для других виртуальных адресов (это называется подкачкой).
Расширение "exe" осталось в наследство от старых досовских исполняемых (executable) файлов. Используемый в настоящее время формат исполняемых файлов Windows называется "Portable Executable" (PE), поскольку один и тот же формат используется для разных платформ. Более того, он построен на основе шаблонов, являющихся общими и для объектных файлов формата COFF (используемых в том числе в мире Unix), а также построенных на их основе библиотечных файлов и файлов импорта (.lib). Формат PE в системе Win32 является универсальным: его используют не только исполняемые файлы (exe), но и динамические библиотеки (dll) и их особые разновидности -элементы ActiveX (ocx) и системные драйверы (sys и drv).
Как и старый формат exe для DOS, PE-файл состоит из заголовка и собственно образа исполняемой программы. Образ программы, как уже отмечалось, может быть составлен из одной или нескольких секций. Заголовок же можно условно разделить на "старый" и "новый" (см. рис.)
"Новый" заголовок составлен из собственно PE-заголовка и таблицы секций, которая фактически является картой отображения записанных в файле секций образа программы в память. В PE-заголовке выделяют также несколько составных частей, но для нашего рассмотрения они несущественны. Отметим лишь каталог смещений-размеров, который указывает на расположение и размеры специальных служебных таблиц. Для размещения последних могут быть выделены отдельные секции в образе программы, но это не является обязательным; в принципе, для всей программы можно использовать одну единственную секцию, разместив в ней и данные, и код, и все необходимые вспомогательные структуры.
Теперь рассмотрим все подробнее. Поскольку попытка запуска создаваемых нами программ под DOS маловероятна, можно без особых проблем обойтись без программы-заглушки DOS. PE-заголовок в этом случае будет следовать сразу за старым заголовком DOS, а именно - непосредственно после 4-байтного поля со смещением 3Ch, т.е. по смещению 40h (само поле 3Ch будет содержать в данном случае это же значение). Единственное, что нужно еще оставить в старом заголовке - это сигнатуру в виде 2 ASCII-символов 'MZ' в начале файла (байты 4Dh 5Ah). Остальные поля могут содержать нули - загрузчик Windows их не использует.
Поля PE-заголовка приведены в таблице 1. Смещения указаны относительно начала заголовка, а жирным шрифтом выделены те поля, при неверных значениях которых Windows откажется загружать программу. Остальные поля либо содержат необязательные данные (например, указатель на размещение и размер отладочных данных), либо для них предусмотрены значения по умолчанию (как для размеров кучи и стека), либо используются лишь для определенных видов файлов (например, флаги dll или контрольная сумма).
Таблица 1. PE-заголовок
Смещение | Размер, байт | Поле | Типичное значение |
---|---|---|---|
0 | 4 | Сигнатура 'PE' | 50h 45h 00 00 |
4 | 2 | Тип процессора | 14Ch |
6 | 2 | Число секций в образе программы | - |
8 | 4 | Время/дата создания файла | - |
0Ch | 4 | Указатель на таблицу символов | 0 |
10h | 4 | Количество отладочных символов | 0 |
14h | 2 | Размер дополнительного заголовка | E0h |
16h | 2 | Тип файла | 10Fh |
18h | 2 | "Магическое" значение | 10Bh |
1Ah | 1 | Старшая версия компоновщика | - |
1Bh | 1 | Младшая версия компоновщика | - |
1Ch | 4 | Размер кода | - |
20h | 4 | Размер инициализированных данных | - |
24h | 4 | Размер неинициализированных данных | - |
28h | 4 | Смещение точки входа | - |
2Ch | 4 | Смещение секции кода в памяти | - |
30h | 4 | Смещение секции данных в памяти | - |
34h | 4 | Адрес загрузки образа в память | 400000h |
38h | 4 | Выравнивание секций в памяти | 1000h |
3Ch | 4 | Выравнивание в файле | 200h |
40h | 2 | Старшая версия Windows | 4 |
42h | 2 | Младшая версия Windows | 0 |
44h | 2 | Старшая версия образа | - |
46h | 2 | Младшая версия образа | - |
48h | 2 | Старшая версия подсистемы | 4 |
4Ah | 2 | Младшая версия подсистемы | 0 |
4Ch | 4 | Зарезервировано | 0 |
50h | 4 | Размер загруженного файла в памяти | - |
54h | 4 | Размер всех заголовков в файле | - |
58h | 4 | Контрольная сумма | 0 |
5Ch | 2 | Подсистема | 2 или 3 |
5Eh | 2 | Флаги dll | 0 |
60h | 4 | Зарезервированный размер стека | 100000h |
64h | 4 | Выделенный размер стека | 1000h |
68h | 4 | Зарезервированный размер кучи | 100000h |
6Ch | 4 | Выделенный размер кучи | 1000h |
70h | 4 | Устарело | 0 |
74h | 4 | Число элементов в каталоге смещений | 10h |
Далее в PE-заголовке следует каталог размещения вспомогательных таблиц: первые 4 байта для каждого элемента являются смещением начала соответствующих данных относительно базового адреса загрузки, следующие 4 байта - размером этих данных. Хотя число элементов в каталоге указывается в поле PE-заголовка, Windows 9* не допускает значения, меньшего 10h. Структура каталога фиксирована; указатели на соответствующие данные должны следовать в следующем порядке:
- таблица экспорта;
- таблица импорта;
- таблица ресурсов;
- таблица исключений;
- таблица сертификатов;
- таблица настроек;
- отладочные данные;
- специфичные для архитектуры данные;
- глобальный указатель;
- таблица локального хранилища потоков (TLS);
- таблица конфигурирования загрузки;
- таблица связанного импорта;
- таблица импортируемых адресов (IAT);
- дескриптор отложенного импорта;
- зарезервировано;
- зарезервировано.
Это не значит, что все перечисленные данные должны присутствовать. Если те или иные данные отсутствуют, соответствующие поля каталога содержат нули. Мы будем рассматривать эти структуры по мере того, как начнем с ними работать.
Таблица секций следует непосредственно после PE-заголовка (после каталога смещений). Каждый вход таблицы имееет следующий формат (см. табл. 2).
Таблица 2. Строка таблицы секций
Смещение | Размер, байт | Поле |
---|---|---|
0 | 8 | Произвольное имя секции |
8 | 4 | Размер секции в памяти |
0Ch | 4 | Смещение секции в памяти относительно адреса загрузки |
10h | 4 | Размер данных секции в файле |
14h | 4 | Смещение начала данных секции в файле |
18h | 12 | Используется лишь в объектных файлах |
24h | 4 | Флаги секции |
Таблица секций имеет столько входов, сколько секций в образе программы. Расположение секций в файле и в виртуальной памяти созданного процесса может не совпадать. Данные различных секций как в файле, так и в памяти располагаются не вплотную друг к другу - они должны быть соответствующим образом выровнены. Например, если код занимает всего 2 байта, следующая за ним секция (допустим, данных) располагается не по смещению +2 байта, а на границе следующей страницы, т.е. как минимум через 4 Кб, если это образ в памяти, и минимум через 512 байт для образа в файле. Значения для выравнивания в файле и в памяти указаны в PE-заголовке, причем они обязательны.
Секция может содержать т.н. неинициализированные данные. Фактически, это просто резервирование определенных адресов памяти под будущие переменные. Для таких данных место в файле не отводится; память резервируется лишь при загрузке на исполнение. Если вся секция содержит лишь неинициализированные данные, поля размера данных секции в файле и смещения начала данных секции в файле равны нулю. В любом случае, когда размер секции в файле меньше указанного размера секции в памяти, остаток заполняется до нужного размера нулями.
Поле флагов секции - то самое, где задаются атрибуты страниц памяти, отводимых под секцию. Возможно использование до 32 флагов (по одному на каждый бит 4-байтного значения), но часть из них зарезервирована, другая часть используется лишь в объектных файлах. Биты нумеруются от младшего к старшему, начиная от 0 (самый младший бит - 0, самый старший - 31). Наиболее употребительные для исполняемых файлов следующие:
бит 5 - секция кода;
бит 6 - инициализированные данные;
бит 7 - неинициализированные данные;
бит 28 - секция может быть общей (разделяемой - shared);
бит 29 - разрешено исполнение;
бит 30 - разрешено чтение;
бит 31 - разрешена запись.
Например, в секции кода с разрешениями на чтение и исполнение установлены следующие флаги:
Секция с инициализированными данными с разрешениями на чтение и запись:
Та же секция, но с разрешением только для чтения:
Перейдем, наконец, к практике и составим шаблон заголовка PE-файла, имеющего 3 секции с минимальными размерами. Тогда в памяти каждая будет занимать 1000h (1 страница - отвести меньше памяти невозможно), а в файле - 200h байт (1 сектор диска). Такими же будут и значения выравнивания. Первой пусть идет секция кода; назовем ее '.code' (см. рис.) Она будет располагаться по смещению 200h от начала файла, а в памяти - по смещению 1000h от адреса загрузки (первую страницу памяти и первые 200h байтов файла занимает заголовок). Секция кода будет иметь флаги, которые мы вычислили ранее (60000020h)
Следующей будет секция с данными только для чтения; назовем ее '.rdata'. Она будет расположена в файле по смещению 400h, а в памяти - по смещению 2000h. Флаги: 40000040h. За ней - секция данных с разрешениями на чтение и запись: '.data', расположение в файле - 600h, в памяти - 3000h; флаги: C0000040h.
Теперь составим командный файл для отладчика debug. Имеет смысл сначала создать специальную папку "Шаблоны". В ней сохраним этот файл для использования в дальнейшем. Открываем Блокнот и набираем:
Бинарный файл с заголовом будет называться 'Header.bin', его размер - 200h байт. Сначала очищаем "область сборки" - первые 200h байт, затем набираем стандартные сигнатуры. Программы-заглушки у нас не будет - PE-заголовок следует непосредственно за DOS-заголовком; заодно это сэкономит размер заголовка.
А вот дальше пойдут поля PE-заголовка, которые нужно будет настраивать для каждого отдельного exe-файла. Чтобы было удобнее редактировать этот файл в дальнейшем, оставим здесь комментарии - а для этого нам придется изменить способ ввода и перейти в режим ассемблирования.
Режим ассемблирования начинается с команды 'a', за которой следует смещение, по которому нужно вводить данные. В нашем случае, PE-заголовок начинается со смещения 40h от начала файла, поэтому к значениям смещения в таблице 1 нужно добавлять 40h. Близко отстоящие друг от друга поля можно набирать "в один заход"; когда же разрыв большой, можно выйти из режима ассемблирования (оставив для этого пустую строку) и вновь набрать 'a' уже с новым смещением. В "разрыве" при этом останутся нули. Учтите, что комментарии можно оставлять лишь "внутри" режима ассемблирования - вне его отладчик выдаст ошибку.
Имеет смысл также выделить те участки, которые нужно будет в дальнейшем редактировать (как этот случай - число секций может каждый раз быть разным); для этого удобно выделять каким-либо способом строку с комментарием, чтобы она сразу бросалась в глаза. Оставшуюся часть файла для debug приведем, как есть; она не должна вызвать проблем (обратите внимание на пустые строки - их нельзя удалять; и помните про обратный порядок байтов в числах, требующих более 1 байта):
Перед записью созданного "образа заголовка" сдвигаем его на 100h байт, чтобы все записалось правильно. Сохраним этот текст в файле "Header.txt".
Теперь у нас есть шаблон, который можно вставлять в начало exe-файла с 3 секциями, размеры которых не превышают 200h байт каждая. Чтобы протестировать его, нужно собрать "настоящий" exe-файл с его использованием. Для этого немного схитрим: вставим две пустые секции (содержащие лишь нули) в качестве секций данных; а в секции кода используем всего 2 байта: EB FE. Это инструкция, передающая управление на себя (как мы узнаем в дальнейшем). Т.е. наша программа просто зацикливается; но пока нам большего и не надо.
В блокноте создадим еще 2 простых файла. Первый - "s1.txt" (содержит наш "код"):
Второй - "s2.txt" (секция в 200h байт, заполненная нулями):
А теперь в том же Блокноте создаем файл "make.bat":
Первый вызов debug исполняет команды, записанные в файле header.txt (при этом создается файл header.bin). Отчет выводится в файл report.lst; это необходимо для того, чтобы можно было проверить, не были ли допущены ошибки.
Второй вызов debug исполняет команды в файле s1.txt, создавая файл s1.bin с нашей "секцией кода". Перенаправление с двумя знаками >> означает, что отчет записывается не с начала указанного файла (затирая его содержимое), а добавляется в его конец. Третий вызов debug выполняет s2.txt, создавая пустую секцию в файле s2.bin. Наконец, мы объединяем эти секции в единый файл с расширением exe, причем заметьте - файл s2.bin использован дважды (2 пустые секции).
Под динамической загрузкой понимается загрузка подпрограммы в память при первом обращении к ней из пользовательской программы. Это весьма полезный принцип, если требуется сэкономить память , поскольку никакой "лишний" код в этом случае в память не загружается. При статической линковке объем исполняемого кода может оказаться очень большим, именно за счет того, что к файлу бинарного кода добавлен полностью код всех используемых библиотек. При динамической загрузке никакой специальной поддержки от ОС не требуется на этапе разработки программы.
С динамической загрузкой вызываемых подпрограмм тесно связан другой родственный механизм – динамическая линковка: линковка во время исполнения программы. Разумеется, это не означает, что во время выполнения область кода программы расширяется, и к ней добавляется код динамически линкуемой подпрограммы. Используется иная схема. В коде программы размещается заглушка для исполнения (execution stub) – небольшой фрагмент кода, выполняющий системный вызов модуля ОС, размещающего в памяти код динамически линкуемой библиотечной подпрограммы. При первом вызове заглушка заменяет себя на код обращения по адресу динамически размещенной в памяти подпрограммы. Операционная система при вызове динамически линкуемого модуля должна проверить, размещен ли его код в адресном пространстве процесса . Очевидно, что динамическая линковка наиболее целесообразна для библиотек. Файл бинарного кода динамически линкуемой библиотеки имеет в системе UNIX расширение имени .so ( аббревиатура термина shared object ), в системе Windows – расширение имени .dll (аббревиатура от dynamically linked library ).
Возникает, однако, вовсе не философский вопрос: каково должно быть оптимальное соотношение статической и динамической линковки в системе? Следует ли ограничиваться только статической или только динамической загрузкой и линковкой? На наш взгляд, следует соблюдать "золотую середину". В операционных системах прошлых лет в этом отношении принимались подчас самые экзотические решения. В ОС "Эльбрус", например, разработчики пошли по чересчур радикальному, на наш взгляд, пути – вообще исключили статическую линковку и все независимые программы загружали только динамически (с помощью механизма ПРОГР, который в "Эльбрусе" назывался открытием программы,или динамическим знакомством ). К чему это привело на практике, хорошо помнят мои коллеги из СПбГУ – разработчики математических пакетов прикладных программ , которые мы с ними в 1980-х гг. переносили с ЕС ЭВМ на "Эльбрус". Они быстро освоили новую конструкцию ПРОГР и обращения ко всем независимо компилируемым модулям оформили именно таким образом. В результате очень сильно замедлилось суммарное время выполнения программы. Это и понятно: реализация каждой математической функции как динамически загружаемой программы – слишком "дорогая" операция, требующая вмешательства ОС, по крайней мере, при первом обращении к каждой такой программе, по сравнению с обычным обращением, например, к функции sin как к подпрограмме (процедуре), элементу статически линкуемой библиотеки, обычной машинной командой вызова процедуры.
Оверлейная структура программы
Как мы уже отмечали во вводных лекциях, в ранних ОС, в особенности – для персональных компьютеров, для пользовательского процесса были вынужденно введены очень жесткие ограничения по памяти, - например, в MS DOS – не более 640 килобайт . При таком дефиците основной памяти, если программа оказывается настолько велика, что полностью не помещается в память максимально разрешенного объема, необходимо предпринимать специальные меры при разработке программы, чтобы разбить ее на непересекающиеся группы модулей, такие. что в каждой группе модули логически взаимосвязаны и должны присутствовать в памяти одновременно, модули же разных групп не обязательно должны вместе загружаться в память . Во время исполнения такой программы должен использоваться специальный системный механизм, называемый оверлейная структура ( overlay ,дословно – наложение ), обеспечивающий поочередную загрузку в одну и ту же область памяти то одной, то другой исполняемой группы модулей. Простая программа , которая выполняет эти действия, называется драйвер оверлея ( overlay driver ).Интегрированная среда разработки Турбо Паскаль обеспечивала специальные опции компилятора, которые позволяли явно указывать модули, входящие в каждый оверлей.
В этой статье общё описывается структура приложения, которую обычно можно встретить на практике. Стандарт языка си не определяет структуры приложения, поэтому неверно говорить, например, о том, что в приложении на си автоматические перемные располагаются на стеке, а статические в bss сегменте и т.п. Однако, реальные примеры помогают глубже понять поведение компилятора и программы.
- Сегмент данных (Data, bss-сегмент и куча)
- Стек
- Сегмент кода
Data cостоит из статических и глобальных переменных, которые явно инициализируются значениями. Этот сегмент может быть далее разбит на ro-data (read only data) – сегмент данных только для чтения, и rw-data (read write data) – сегмент данных для чтения и записи. Например, глобальные переменные
Будут храниться в rw-области. Для выражения типа
указатель будет храниться в rw-области, а строкой литерал "hello world" в ro-области.
оба будут расположены в сегменте данных.
BSS-сегмент (block started by symbol) содержит неинициализированные глобальные переменные, или статические переменные без явной инициализации. Этот сегмент начинается непосредственно за data-сегментом. Обычно загрузчик программ инициализирует bss область при загрузке приложения нулями. Дело в том, что в data области переменные инициализированы – то есть затирают своими значениями выделенную область памяти. Так как переменные в bss области не инициализированы явно, то они теоретически могли бы иметь значение, которое ранее хранилось в этой области, а это уязвимость, которая предоставляет доступ до, возможно, приватных данных. Поэтому загрузчик вынужден обнулять все значения. За счёт этого и неинициализированные глобальные переменные, и статические переменные по умолчанию равны нулю.
Куча – начинается за BSS сегментом и начиная оттуда растёт, соответственно с увеличением адреса. Этот участок используется для выделения на нём памяти с использованием функции malloc (и прочих) и для очисти с помощью функции free.
Стек вызовов обычно растёт "навстречу" куче, то есть с увеличением стека адрес вершины стека уменьшается. Набор значений, которые кладутся на стек одной функцией, называются фреймом. После выхода из функции стек освобождается от фрейма. Один фрейм хранит как минимум одно значение - адрес возврата. Каждый раз, когда мы создаём локальные переменные, они располагаются на стеке. Как только мы выходим из функции, стек очищается и переменные исчезают. Вызов функций и передача параметров также происходит через стек.
Сегмент кода, или текстовый сегмент, или просто текст, содержит исполняемые инструкции. У него фиксированный размер и обычно он используется только для чтения, если же в него можно писать, то архитектура поддерживает самомодификацию. Сегмент кода располагается после начала стека, поэтому в случае роста он [стек] не перекрывает сегмент кода.
Читайте также: