Linux программирование в примерах
Шрифт:
Глава 3
Управление памятью на уровне пользователя
Без памяти для хранения данных программа не может выполнить никакую работу (Или, скорее, невозможно выполнить никакую полезную работу.) Реальные программы не могут позволить себе полагаться на буферы и массивы структур данных фиксированного размера. Они должны быть способны обрабатывать вводимые данные различных размеров, от незначительных до больших. Это, в свою очередь, ведет к использованию динамически выделяемой памяти — памяти, выделяемой в ходе исполнения, а не при компиляции. Вот как вводится в действие принцип GNU «никаких произвольных ограничений».
Поскольку динамически выделяемая память является основным строительным блоком для реальных программ, мы рассмотрим этот вопрос в
3.1. Адресное пространство Linux/Unix
В качестве рабочего определения мы приняли, что процесс является запушенной программой. Это означает, что операционная система загрузила исполняемый файл для этой программы в память, сделала доступными аргументы командной строки и переменные окружения и запустила ее. Процесс имеет пять выделенных для него концептуально различных областей памяти:
Код
Часто называемая сегментом текста область, в которой находятся исполняемые инструкции. Linux и Unix организуют вещи таким образом, что несколько запушенных экземпляров одной программы по возможности разделяют свой код; в любое время в памяти находится лишь одна копия инструкций одной и той же программы (Это прозрачно для работающих программ.) Часть исполняемого файла, содержащая сегмент текста, называется секцией текста.
Инициализированные данные
Статически выделенные и глобальные данные, которые инициализированы ненулевыми значениями, находятся в сегменте данных. У каждого процесса с одной и той же запущенной программой свой собственный сегмент данных. Часть исполняемого файла, содержащая сегмент данных, является секцией данных.
Инициализированные нулями данные [38]
Глобальные и статически выделенные данные, которые по умолчанию инициализированы нулями, хранятся в области процесса, который называют областью BSS [39] . У каждого процесса, в котором запущена одна и та же программа, своя область BSS. При запуске данные BSS помещаются в сегмент данных. В исполняемом файле они хранятся в секции BSS.
38
Существует также другое название для этой области данных — Неинициализированные данные — Примеч. науч. ред.
39
BSS означает 'Block Started by Symbol', мнемоника из ассемблера IBM 7094 — Примеч. автора.
Формат исполняемого файла Linux/Unix таков, что пространство исполняемого файла на диске занимают лишь переменные, инициализированные ненулевыми значениями. Поэтому большой массив, объявленный как '
Куча (heap)
Куча является местом, откуда выделяется динамическая память (получаемая с помощью функции
Хотя память можно вернуть обратно системе и сократить адресное пространство процесса, этого почти никогда не происходит. (Мы различаем освобождение больше не использующейся динамической памяти и сокращение адресного пространства; подробнее это обсуждается далее в этой главе.)
Для кучи характерен «рост вверх». Это означает, что последовательные элементы, добавляемые к куче, добавляются по адресам, численно превосходящим предыдущие. Куча обычно начинается сразу после области BSS сегмента данных.
Стек
Сегмент стека — это область, в которой выделяются локальные переменные. Локальными являются все переменные, объявленные внутри левой открывающей фигурной скобки тела функции (или другой левой фигурной скобки) и не имеющие ключевого слова
В большинстве архитектур параметры функций также помещаются в стек наряду с «невидимой» учетной информацией, генерируемой компилятором, такой, как возвращаемое функцией значение и адрес возврата для перехода из функции к месту, откуда произошел вызов. (В некоторых архитектурах для этого используются регистры.) Именно использование стека для параметров функций и возвращаемых ими значений делает удобным написание рекурсивных функций (тех, которые вызывают сами себя) Переменные, хранящиеся в стеке, «исчезают», когда функция, их содержащая, возвращается, пространство стека используется повторно для последующих вызовов функций. В большинстве современных архитектур стек «растет вниз», это означает, что элементы, находящиеся глубже в цепи вызова, находятся по численно меньшим адресам. В работающей программе области инициализированных данных, BSS и кучи обычно размещаются в единой протяженной области: сегменте данных. Сегменты стека и кода отделены от сегмента данных и друг от друга. Это показано на рис. 3.1.
Рис. 3.1. Адресное пространство Linux/Unix
Хотя перекрывание стека и кучи теоретически возможно, операционная система предотвращает этот случай, и любая программа, пытающаяся это сделать, напрашивается на неприятности. Это особенно верно для современных систем, в которых адресные пространства большие и интервал между верхушкой стека и концом кучи значителен. Различные области памяти могут иметь различную установленную на память аппаратную защиту. Например, сегмент текста может быть помечен «только для исполнения», тогда как у сегментов данных и стека разрешение на исполнение может отсутствовать. Такая практика может предотвратить различные виды атак на безопасность. Подробности, конечно, специфичны для оборудования и операционной системы, и они могут со временем меняться. Стоит заметить, что стандартные как С, так и C++ позволяют размещать элементы с атрибутом
Таблица 3.1. Сегменты исполняемой программы и их размещение
Память программы | Сегмент адресного пространства | Секция исполняемого файла |
---|---|---|
Код | Text | Text |
Инициализированные данные | Data | Data |
BSS | Data | BSS |
Куча | Data | |
Стек | Stack |
Программа