Память процесса и сегменты
Память процесса и сегменты
Зачем это новичку
Когда вы пишете int x = 5; внутри функции и static int counter; снаружи, компилятор кладёт их в разные места оперативной памяти. От этого зависят срок жизни переменной, скорость доступа и типичные баги: «вернул указатель на локальную переменную», «переполнил стек огромным массивом», «забыл free». Карта памяти связывает синтаксис С с тем, что происходит при запуске собранной программы.
Процесс — это запущенная программа со своим виртуальным адресным пространством (ОС выдаёт иллюзию «у меня свой кусок RAM»). Сегмент — логическая область внутри этого пространства с общим назначением (код, данные, стек).
Программа на С после сборки превращается в исполняемый файл. При запуске операционная система загружает его в виртуальное адресное пространство процесса и раскладывает содержимое по логическим областям. Понимание этой карты помогает объяснить, почему глобальные переменные живут всё время работы процесса, локальные исчезают после выхода из функции, а malloc берёт память из другого места, чем int x внутри main.
Общая схема (упрощённо, сверху вниз по типичному расположению в адресном пространстве):
высокие адреса
┌─────────────────┐
│ стек │ локальные переменные, кадры вызовов
├─────────────────┤
│ ↓ │ растут навстречу друг другу
│ ↑ │
├─────────────────┤
│ куча │ malloc / calloc / realloc
├─────────────────┤
│ BSS (нулевые │ глобальные и static без явной инициализации
│ глобальные) │
├─────────────────┤
│ data (инициализ.│ глобальные и static с начальными значениями
│ глобальные) │
├─────────────────┤
│ text (код) │ машинные инструкции, константы только для чтения
└─────────────────┘
низкие адреса
На практике порядок и границы зависят от ОС и формата исполняемого файла (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[0] — неопределённое поведение. Для изменяемого буфера нужен массив char buf[] = "Hello"; в стеке или куче.
Data и BSS
Data хранит глобальные и статические переменные с явной инициализацией на этапе компиляции:
int counter = 10;
static double rate = 3.14;
Их начальные значения записаны в исполняемый файл; при старте процесса загрузчик копирует их в RAM.
BSS (Block Started by Symbol) — область для глобальных и static, которые не инициализированы в исходнике (компилятор считает их нулём):
int total_requests;
static char buffer[4096];
В файле на диске для BSS обычно хранится только размер, а не содержимое — экономия места. При запуске ОС выделяет нулевой блок нужной длины.
| Область | Когда используется | Жизненный цикл |
|---|---|---|
| data | int g = 1; | весь процесс |
| BSS | static int n; | весь процесс |
| стек | int local; в функции | пока активен кадр функции |
| куча | malloc(...) | до free |
Стек
Каждый вызов функции создаёт кадр стека: место под локальные переменные, сохранённые регистры, адрес возврата. При выходе из функции кадр уничтожается — поэтому нельзя возвращать указатель на локальную переменную:
int *bad(void) {
int x = 42;
return &x; /* после return x не существует — UB */
}
Глубина стека ограничена (типично от сотен килобайт до нескольких мегабайт, настраивается ОС). Бесконечная рекурсия или огромные локальные массивы вызывают переполнение стека и аварийное завершение.
Стек растёт в одну сторону, куча — в другую. Между ними свободное пространство; если они сближаются — процесс получает ошибку выделения памяти.
Куча (heap)
Динамическая память через malloc, calloc, realloc берётся из кучи. Подробнее о выделении — в Основах языка С. Отличия от стека:
- блок живёт, пока не вызван
free; - размер задаётся во время выполнения;
- порядок освобождения не обязан быть обратным порядку выделения (в отличие от вложенных кадров стека).
Менеджер кучи в libc обслуживает запросы процесса; крупные блоки ОС может выдавать через mmap. Утечки и фрагментация кучи — типичные проблемы долгоживущих сервисов на С.
Разбор вызова malloc:
int *p = (int *)malloc(10 * sizeof(int));
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]на стеке.
См. также: Системное программирование на С, Идиомы и обработка ошибок.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). История языка C - происхождение, ключевые идеи и влияние на развитие операционных систем и компиляторов. Язык С — это процедурный, компилируемый язык программирования, созданный в начале 1970-х годов Деннисом Ритчи в Bell Labs. Программирование на языке С требует понимания не только самого языка, но и всей совокупности программ, задействованных в процессе превращения исходного текста в исполняемый файл. Программа на языке С не выполняется напрямую процессором. Исходный текст проходит несколько этапов обработки, прежде чем превратится в машинный код, который может быть запущен операционной системой. Язык программирования С существует не как набор случайных правил, а как строго определённая спецификация, зафиксированная в международных стандартах. Архитектура программ на C - организация модулей, процесс компиляции и взаимосвязь компонентов системы. Язык программирования С занимает особое место в истории и практике разработки программного обеспечения. Типизация, набор правил определения типа данных значений языка. Язык программирования С предоставляет механизм создания составных типов данных, позволяющих объединять разнородные элементы под единым именем. Этот механизм называется структурой. Как на С организовать функции, владение ресурсами, коды ошибок и очистку без исключений и сборщика мусора. Реализация ассоциативного массива на С — хеш-функция, коллизии, цепочки, открытая адресация и изменение размера. Работа с встраиваемой SQL-библиотекой из программы на С — соединение, запросы, параметры и транзакции.История языка С
Основы языка С
Инструментальная цепочка компиляции С
Преобразование исходного кода в исполняемый файл
Стандарты языка С
Архитектура программ на С
Компиляторы и среды разработки для С
Типы данных в С
Структуры и объединения
Идиомы кода и обработка ошибок
Хеш-таблица на С
Встраиваемая база данных из С