Память процесса и сегменты
Память процесса и сегменты
Зачем это новичку
Когда вы пишете 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 обычно хранится только размер, а не содержимое — экономия места. При запуске ОС выделяет нулевой блок нужной длины.
| Область | Когда используется | Жизненный цикл |
|---|---|---|
| data | int g = 1; | весь процесс |
| BSS | static 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]на стеке.
См. также: Системное программирование на С, Идиомы и обработка ошибок.