Перейти к основному содержимому

Память процесса и сегменты

Разработчику Архитектору

Память процесса и сегменты

Зачем это новичку

Когда вы пишете int x = 5; внутри функции и static int counter; снаружи, компилятор кладёт их в разные места оперативной памяти. От этого зависят срок жизни переменной, скорость доступа и типичные баги — "вернул указатель на локальную переменную", "переполнил стек огромным массивом", "забыл free". Карта памяти связывает синтаксис С с тем, что происходит при запуске собранной программы.

Процесс — это запущенная программа со своим виртуальным адресным пространством (ОС выдаёт иллюзию "у меня свой кусок RAM"). Сегмент — логическая область внутри этого пространства с общим назначением (код, данные, стек).

Программа на С после сборки превращается в исполняемый файл. При запуске операционная система загружает его в виртуальное адресное пространство процесса и раскладывает содержимое по логическим областям. Понимание этой карты помогает объяснить, почему глобальные переменные живут всё время работы процесса, локальные исчезают после выхода из функции, а malloc берёт память из другого места, чем int x внутри main.

Общая схема (упрощённо, сверху вниз по типичному расположению в адресном пространстве):

высокие адреса
┌─────────────────┐
│ стек │ локальные переменные, кадры вызовов
├─────────────────┤
│ ↓ │ растут навстречу друг другу
│ ↑ │
├─────────────────┤
│ куча │ malloc / calloc / realloc
├─────────────────┤
│ BSS (нулевые │ глобальные и static без явной инициализации
│ глобальные) │
├─────────────────┤
│ data (инициализ.│ глобальные и static с начальными значениями
│ глобальные) │
├─────────────────┤
│ text (код) │ машинные инструкции, константы только для чтения
└─────────────────┘
низкие адреса

Разбор:

  • Схема показывает логические области виртуального адресного пространства процесса, а не "физическую линейку RAM".
  • Стек обычно растёт в сторону уменьшения адресов, а куча — в сторону увеличения, поэтому на диаграмме они движутся навстречу.
  • text, data, BSS формируются ещё на этапе линковки, а стек/куча активно изменяются уже во время выполнения.
  • Такая ментальная карта помогает быстро локализовать классы ошибок — переполнение стека, утечка кучи, запись в read-only сегмент.

На практике порядок и границы зависят от ОС и формата исполняемого файла (ELF, PE, Mach-O), но роли областей одинаковы.


Куда попадает переменная — быстрая таблица

Вы написали в кодеОбластьКогда исчезает
int g = 1; вне функцийdataпри завершении процесса
static int n; вне функцийBSS (ноль по умолчанию)при завершении процесса
int x; внутри mainстекпри выходе из main
char buf[100]; в функциистекпри выходе из функции
malloc(100)кучапосле free или утечка до конца процесса
"hello" в printf("hello")часто read-only (как код)всё время процесса

Сегмент кода (text)

Сюда попадает скомпилированный машинный код функций — main, printf из библиотеки (при статической линковке — внутри образа), пользовательские функции. Область обычно только для чтения и исполнения: запись в код из программы приводит к ошибке доступа (защита от случайных и злонамеренных изменений).

Строковые литералы в классическом С часто тоже размещаются в read-only сегменте:

const char *msg = "Hello";

Разбор:

  • msg — указатель на неизменяемую строковую константу "Hello".
  • Литерал обычно размещается в памяти только для чтения, поэтому попытка менять его содержимое может аварийно завершить программу.
  • const в типе (const char *) дополнительно запрещает запись через этот указатель на уровне компилятора.
  • Если нужна изменяемая строка, лучше использовать массив: char msg[] = "Hello";.

Попытка изменить msg[0] — неопределённое поведение. Для изменяемого буфера нужен массив char buf[] = "Hello"; в стеке или куче.


Data и BSS

Data хранит глобальные и статические переменные с явной инициализацией на этапе компиляции:

int counter = 10;
static double rate = 3.14;

Разбор:

  • Обе переменные имеют статическую длительность хранения и живут весь срок жизни процесса.
  • counter инициализирован явно, поэтому попадает в сегмент data.
  • static rate на уровне файла ограничивает видимость этой переменной текущей единицей трансляции (.c-файлом).
  • Начальные значения этих переменных включаются в исполняемый образ и подготавливаются загрузчиком при старте процесса.

Их начальные значения записаны в исполняемый файл; при старте процесса загрузчик копирует их в RAM.

BSS (Block Started by Symbol) — область для глобальных и static, которые не инициализированы в исходнике (компилятор считает их нулём):

int total_requests;
static char buffer[4096];

Разбор:

  • Переменные без явной инициализации автоматически получают нулевое значение по стандарту C.
  • Такие объекты обычно размещаются в сегменте BSS, где в файле хранится размер, а не сами нули.
  • static у buffer делает имя видимым только в текущем модуле, но длительность хранения остаётся "на всё время процесса".
  • Большой буфер в BSS часто предпочтительнее гигантского локального массива на стеке.

В файле на диске для BSS обычно хранится только размер, а не содержимое — экономия места. При запуске ОС выделяет нулевой блок нужной длины.

ОбластьКогда используетсяЖизненный цикл
dataint g = 1;весь процесс
BSSstatic int n;весь процесс
стекint local; в функциипока активен кадр функции
кучаmalloc(...)до free

Стек

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

Локальные переменные живут в кадре текущей функции:

void demo(void) {
int local = 7; /* стек: исчезнет после выхода из demo */
static int persistent; /* data/BSS: живёт весь процесс */
persistent++;
}

Разбор:

  • local создаётся при входе в demo и уничтожается при выходе — это типичный объект стека.
  • static int persistent хранится не в стеке, а в глобальной области процесса (инициализируется нулём один раз).
  • Повторные вызовы demo получают новый local, но persistent сохраняет накопленное значение.
  • Именно поэтому static внутри функции часто используют для счётчиков и кэшей между вызовами.
  • Но static в многопоточной программе требует отдельной синхронизации.

При выходе из функции кадр уничтожается — поэтому нельзя возвращать указатель на локальную переменную:

int *bad(void) {
int x = 42;
return &x; /* после return x не существует — UB */
}

Разбор:

  • x — локальная переменная, её память принадлежит текущему кадру стека функции bad.
  • После return кадр функции уничтожается, и адрес &x становится висячим указателем (dangling pointer).
  • Любая попытка читать/писать по возвращённому адресу — неопределённое поведение.
  • Правильные варианты: вернуть значение по копии (return x;) или выделить память в куче и явно описать ответственность за free.

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

Стек растёт в одну сторону, куча — в другую. Между ними свободное пространство; если они сближаются — процесс получает ошибку выделения памяти.


Куча (heap)

Динамическая память через malloc, calloc, realloc берётся из кучи. Подробнее о выделении — в Основах языка С. Отличия от стека:

  • блок живёт, пока не вызван free;
  • размер задаётся во время выполнения;
  • порядок освобождения не обязан быть обратным порядку выделения (в отличие от вложенных кадров стека).

Менеджер кучи в libc обслуживает запросы процесса; крупные блоки ОС может выдавать через mmap. Утечки и фрагментация кучи — типичные проблемы долгоживущих сервисов на С.

Сравнение malloc и calloc на одном примере:

#include <stdlib.h>

int *a = malloc(5 * sizeof(int)); /* содержимое не определено */
int *b = calloc(5, sizeof(int)); /* все элементы = 0 */

if (a == NULL || b == NULL) {
/* обработка ошибки */
}

free(a);
free(b);

Разбор:

  • malloc выделяет память, но не гарантирует нулевые значения — там может быть "мусор" от прошлых аллокаций.
  • calloc(count, size) сразу обнуляет блок, что удобно для массивов и таблиц по умолчанию.
  • Оба указателя нужно проверять на NULL перед использованием.
  • free вызывают для каждого успешно выделенного блока отдельно.
  • Указатель a/b после free лучше обнулять, если переменная ещё видна в коде.

Разбор вызова malloc:

int *p = (int *)malloc(10 * sizeof(int));

Разбор:

  • malloc выделяет блок в куче, достаточный для 10 элементов int.

  • Возвращаемый адрес сохраняется в p; сама переменная p, если объявлена в функции, живёт на стеке.

  • Приведение (int *) в C обычно не обязательно, но часто встречается в учебных примерах и в C++-совместимом стиле.

  • До использования блока нужно проверить p != NULL, а после завершения работы — вызвать free(p).

  • malloc просит у менеджера кучи непрерывный блок байт;

  • возвращает адрес начала блока (тип void *, часто приводят к int *);

  • память не обнуляется (в отличие от calloc);

  • указатель p сам лежит на стеке (если объявлен в функции), а данные — в куче.

Подробнее о malloc / free — в Основах языка С.


Связь с объектным файлом и линковкой

На этапе компоновки линкер собирает секции .text, .data, .bss из объектных файлов и библиотек в единый образ. Символы вроде main получают фиксированные смещения; неразрешённые внешние ссылки (printf из libc) подставляются при линковке.

Инструменты вроде size (Unix) или аналог в IDE показывают вклад каждой единицы трансляции в размер кода и данных — полезно при оптимизации встраиваемых проектов.


Практическая польза при отладке

  • Segmentation fault при разыменовании NULL или "мусорного" указателя — обращение вне разрешённых страниц.
  • Коррупция кучи — часто проявляется позже, в другой функции; отладчики (Valgrind, AddressSanitizer) отслеживают выход за границы блока.
  • Стек и куча — большие буферы лучше выделять в куче или статически (с осторожностью к потокобезопасности), а не как char huge[1_000_000] на стеке.

См. также: Системное программирование на С, Идиомы и обработка ошибок.