malloc и free: как это работает внутри¶
Когда вы вызываете malloc(100), происходит больше, чем просто «выделить 100 байт». glibc поддерживает сложную
структуру — heap — чтобы сотни тысяч маленьких аллокаций не превращались в сотни тысяч syscall'ов.
Реализация в glibc называется ptmalloc2, написанная Wolfram Gloger на основе dlmalloc Дуга Ли.
Heap и syscalls¶
Heap — регион виртуальной памяти, начинающийся сразу за BSS-сегментом. glibc расширяет его двумя способами:
brk(2)— сдвигает «program break», верхнюю границу heap. Используется для обычных аллокаций.mmap(MAP_ANONYMOUS)— выделяет отдельный анонимный регион. Используется для блоков ≥ 128 KB; приfreeвозвращается черезmunmap.
brk нельзя двигать бесконечно: heap ограничен RLIMIT_DATA (ulimit -d) и виртуальным адресным пространством. Когда sbrk для main arena возвращает (void*)-1, glibc не падает сразу — sysmalloc() делает fallback на mmap(MAP_ANONYMOUS). NULL из malloc получается только если и mmap тоже не смог выделить память. На 64-битных системах до этого доходит редко (128 TB виртуального адресного пространства на процесс), но на 32-битных (3 GB user space) это была реальная проблема.
Chunk¶
Каждый блок памяти — занятый или свободный — имеет header. У занятого chunk'а структура такая:
┌────────────────────────────────┐
chunk ──▶ │ prev_size (8 байт) │ размер предыдущего chunk,
│ │ если он свободен; иначе — данные
├────────────────────────────────┤
│ size | P | M | N (8 байт) │ размер + флаги в младших битах
├────────────────────────────────┤
ptr ────▶ │ данные пользователя │ ← malloc возвращает указатель сюда
│ ... │
└────────────────────────────────┘
Флаги в поле size (размер всегда кратен 16, поэтому три младших бита свободны):
| Флаг | Бит | Значение |
|---|---|---|
PREV_INUSE (P) |
0 | предыдущий chunk занят |
IS_MMAPPED (M) |
1 | chunk выделен через mmap, не из heap |
NON_MAIN_ARENA (N) |
2 | chunk принадлежит не main arena |
Минимальный размер chunk на x86-64 — 32 байта.
У свободного chunk'а данные пользователя не нужны, поэтому то же пространство используется для указателей бина:
┌────────────────────────────────┐
chunk ──▶ │ prev_size (8 байт) │
├────────────────────────────────┤
│ size | flags (8 байт) │
├────────────────────────────────┤
│ fd (8 байт) │ forward ptr — следующий chunk в bin
├────────────────────────────────┤
│ bk (8 байт) │ back ptr — предыдущий chunk в bin
├────────────────────────────────┤
│ fd_nextsize (8 байт) │ только у largebins:
│ bk_nextsize (8 байт) │ прыжок к следующему размеру
└────────────────────────────────┘
Bins¶
Когда вы вызываете free, glibc не отдаёт память сразу обратно в ОС — chunk попадает в один из bins. При следующем
malloc подходящего размера glibc достаёт chunk оттуда. Это на порядки быстрее, чем каждый раз делать syscall.
| Bin | Диапазон | Структура | Особенность |
|---|---|---|---|
| tcache | 16–1032 байт | per-thread LIFO | без блокировок, без coalescing |
| fastbins | 16–112 байт | LIFO | без immediate coalescing |
| smallbins | 32–1008 байт | FIFO, doubly-linked | coalescing при free |
| largebins | ≥ 1024 байт | sorted, doubly-linked | best-fit поиск |
| unsorted bin | любой | FIFO буфер | временное хранилище |
Tcache¶
Tcache появился в glibc 2.26 и сейчас — самый быстрый путь. Каждый поток имеет свою tcache_perthread_struct с 64
односвязными списками (LIFO) для размеров от 16 до 1032 байт с шагом 16:
tcache_perthread_struct (per-thread)
┌────────────┬────────────┬─────────────┬─────┐
│ counts[64] │ │ │ ... │ ← кол-во chunk'ов в каждом списке
├────────────┴────────────┴─────────────┴─────┤
│ entries[0] entries[1] entries[2] ... │ ← head-указатели
└──────┬──────────┬───────────────────────────┘
│ │
▼ ▼
[chunk] [chunk] ──▶ [chunk] ──▶ NULL
size=32 size=48
Каждый список вмещает до 7 chunks. Если список полон — chunk идёт в fastbin или дальше.
Tcache не делает coalescing и не проверяет целостность указателей — отсюда его уязвимость к атакам типа tcache poisoning.
Fastbins¶
Fastbins — односвязные LIFO-списки для небольших chunks (16–112 байт, 10 bins с шагом 16). Верхняя граница определяется константой DEFAULT_MXFAST = 128 байт (chunk size) на x86-64, что соответствует 112 байтам полезной нагрузки. Каждый bin хранит только chunks одного размера:
fastbin[0] (size=32)
┌──────┐
│ head │──▶ [chunk] ──▶ [chunk] ──▶ [chunk] ──▶ NULL
└──────┘ fd fd fd
Fastbins намеренно не делают immediate coalescing — это их главное свойство. Если вы выделяете и освобождаете много
маленьких объектов одного размера, fastbin позволяет переиспользовать их мгновенно. Объединение со свободными соседями
происходит позже, при вызове malloc_consolidate().
Smallbins и largebins¶
Smallbins (32–1008 байт) — кольцевой двусвязный список. Каждый chunk хранит два указателя: fd (forward, на следующий) и bk (backward, на предыдущий). Голова списка — запись bins[] в malloc_state.
smallbin[n] — кольцевой двусвязный список, FIFO
malloc_state самый старый самый новый
┌──────────┐ fd ┌──────────┐ fd ┌──────────┐ fd
│ bins[n] │ ────────▶│ chunk A │ ────────▶│ chunk B │ ────────▶ bins[n]
│ (head) │ ◀────────│ │ ◀────────│ │ ◀────────
└──────────┘ bk └──────────┘ bk └──────────┘ bk
▲ │
│ └── malloc() заберёт chunk A первым (FIFO)
└── free() вставляет новый chunk сюда (через bk от head)
Все chunks в одном smallbin — одного размера. malloc всегда берёт самый старый chunk (FIFO), что снижает фрагментацию.
Largebins устроены так же, но chunks разного размера и отсортированы по убыванию. Дополнительные указатели fd_nextsize/bk_nextsize формируют второй список только из уникальных размеров — чтобы при best-fit поиске не перебирать все chunks, а прыгать сразу к нужному размеру.
Unsorted bin¶
Любой освобождённый chunk (кроме tcache/fastbin) попадает сначала в unsorted bin. При следующем malloc glibc
перебирает unsorted bin: chunk подходящего размера возвращается сразу, остальные раскладываются по smallbins/largebins.
Top chunk¶
Top chunk — большой свободный кусок в самом верху heap, всегда выровненный по странице. Если ни один bin не подошёл —
malloc отрезает от него кусок нужного размера. Если top chunk кончился — brk или mmap расширяет heap.
heap (main arena, растёт вправо через brk)
низкие адреса program break
│ │
▼ ▼
┌─────────────┬──────────────┬─────────────┬──────────────────────────┐
│ chunk A │ chunk B │ chunk C │ top chunk │
│ (busy) │ (free) │ (busy) │ │
│ 32 байт │ 128 байт │ 64 байта │ всё свободное │
│ P=1 │ P=1 │ P=0 (!) │ P=1 │
└─────────────┴──────────────┴─────────────┴──────────────────────────┘
│
└──▶ в unsorted bin (или smallbin/fastbin)
Arenas¶
Если бы все потоки шарили один heap, каждый malloc требовал бы захвата глобального mutex'а. Под высокой нагрузкой это
узкое место убивает масштабируемость.
Решение — arenas: независимые heap-структуры, каждая со своим mutex'ом. Структура malloc_state описывает одну
arena:
malloc_state (arena)
┌──────────────────────────────────┐
│ mutex │ ← блокировка arena
│ flags │
│ top ──▶ top chunk │
│ fastbinsY[10] ──▶ fastbin lists │
│ bins[128] ──▶ small/largebins│
│ next ──▶ следующая arena│
└──────────────────────────────────┘
Когда поток вызывает malloc:
- Пробует захватить mutex своей текущей arena
- Если arena занята — перебирает linked list arena, ищет свободную
- Если все заняты — создаёт новую arena через
mmap(до2 × num_cpuarena)
Thread 1 ──▶ Arena 1 [mutex: free] ✓ захватывает
Thread 2 ──▶ Arena 1 [mutex: busy] → ищет дальше
Arena 2 [mutex: free] ✓ захватывает
Thread 3 ──▶ Arena 2 [mutex: busy] → создаёт Arena 3
Main arena (одна) расширяется через brk — у неё доступ к program break. Все остальные arenas выделяют память через mmap кусками по 64 MB (HEAP_MAX_SIZE). В virtual memory это выглядит так:
Virtual memory process (высокие адреса наверху)
┌───────────────────────────────────┐ 0xffff...
│ kernel space │
├───────────────────────────────────┤
│ stack │ ↓ растёт вниз
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ │
│ ┌─────────────────────────────┐ │
│ │ Arena 3 (mmap, 64 MB) │ │ ← дополнительная arena
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Arena 2 (mmap, 64 MB) │ │ ← дополнительная arena
│ └─────────────────────────────┘ │
│ │
│ libc.so, libstdc++.so, ... │ ← динамические библиотеки
│ │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ ← program break (brk)
│ Main Arena heap │ ↑ растёт вверх через brk
│ [chunk][chunk] ... [top chunk] │
├───────────────────────────────────┤
│ BSS / data / text │
└───────────────────────────────────┘ 0x0000...
Tcache не входит в arena — это per-thread структура, живёт на heap'е основного потока, без mutex'а.
Как работает malloc¶
malloc(size):
1. size > MMAP_THRESHOLD (≈128 KB)
→ mmap(MAP_ANONYMOUS) напрямую, минуя arenas, IS_MMAPPED = 1
2. Ищем в tcache своего потока (без блокировки)
→ нашли: возвращаем, готово
3. Захватываем mutex arena
4. size <= 112 байт: ищем в fastbins (exact fit)
→ нашли: возвращаем
5. size <= 1008 байт: ищем в smallbins (exact fit)
→ нашли: возвращаем
6. Перебираем unsorted bin:
- exact fit → возвращаем сразу
- не подходит → раскладываем chunk в small/largebin
7. Ищем в largebins (best-fit, closest size ≥ запрошенного)
→ нашли: возможно split, возвращаем
8. Берём кусок от top chunk (split)
→ top chunk кончился: brk/mmap расширяем heap
Как работает free¶
free(ptr):
1. ptr == NULL → ничего (гарантия стандарта C)
2. chunk.IS_MMAPPED == 1 → munmap, готово
3. Подходит под tcache (≤ 1032 байт, список не полон)
→ кладём в tcache, готово
4. Подходит под fastbin (≤ 112 байт)
→ кладём в fastbin без coalescing, готово
5. Иначе — coalescing:
a. PREV_INUSE == 0: предыдущий chunk свободен
→ backward coalescing: объединяем с prev chunk
b. Следующий chunk свободен (смотрим его PREV_INUSE)
→ forward coalescing: объединяем с next chunk
c. Следующий chunk == top chunk
→ объединяем с top, heap «сжимается»
d. Кладём результат в unsorted bin
Coalescing¶
Coalescing — объединение смежных свободных chunks, чтобы уменьшить фрагментацию heap'а.
Пример: forward coalescing
До free(B):
┌──────────┬──────────┬──────────────┐
│ chunk A │ chunk B │ chunk C │
│ (busy) │ (busy) │ (free) │
│ P=1 │ P=1 │ P=1 (B=busy)│
└──────────┴──────────┴──────────────┘
После free(B) — C свободен (видно по header'у C), объединяем:
┌──────────┬───────────────────────────┐
│ chunk A │ chunk B+C │
│ (busy) │ (free) │
│ P=1 │ P=1 │
└──────────┴───────────────────────────┘
Пример: backward + forward coalescing
До free(B):
┌──────────┬──────────┬──────────────┐
│ chunk A │ chunk B │ chunk C │
│ (free) │ (busy) │ (free) │
│ │ P=0 (!) │ P=1 (B=busy)│
└──────────┴──────────┴──────────────┘
После free(B) — A свободен (P=0 в header'е B), C свободен, объединяем:
┌──────────────────────────────────────┐
│ chunk A+B+C │
│ (free) │
└──────────────────────────────────────┘
glibc определяет, свободен ли предыдущий chunk, по флагу PREV_INUSE в header'е текущего chunk'а. Адрес предыдущего
chunk'а — (char*)chunk - prev_size.
Fastbins не делают immediate coalescing — ради скорости. Объединение происходит при malloc_consolidate(), который
вызывается, например, при запросе размера в диапазоне largebin.
Фрагментация¶
После многих аллокаций и free разного размера суммарно свободной памяти может хватать, но непрерывного блока нужного размера нет. glibc не гарантирует устранение фрагментации.
Фрагментированный heap:
┌──────┬────────┬──────┬────────┬──────┬────────┐
│ busy │ free │ busy │ free │ busy │ free │
│ 64 │ 128 │ 64 │ 64 │ 64 │ 128 │
└──────┴────────┴──────┴────────┴──────┴────────┘
Свободно суммарно: 320 байт, но malloc(256) не сработает — нет непрерывного блока
Что делать:
- Object pool — выделять блоки одного размера, никакой внешней фрагментации.
- Bump-аллокатор — для данных с одним временем жизни, освобождается целиком.
- Альтернативные аллокаторы: jemalloc, tcmalloc (Google), mimalloc (Microsoft) — меньше фрагментируются в многопоточных сценариях.
Отладка heap¶
# Valgrind: утечки, use-after-free, double-free
valgrind --leak-check=full ./prog
# AddressSanitizer: быстрее Valgrind, ловит те же классы ошибок
gcc -fsanitize=address -g prog.c -o prog && ./prog
# Статистика glibc heap прямо из кода
#include <malloc.h>
malloc_stats(); // выводит в stderr
malloc_info(0, stdout); // детальный XML-формат
Связанные темы¶
- Виртуальная память — как
brk/mmapрасширяют heap, page fault при первом обращении - mmap и маппинг файлов —
mmap(MAP_ANONYMOUS)для крупных аллокаций - Устройство памяти и аллокация — место heap в адресном пространстве
Источники¶
- malloc internals — sourceware.org
- Heap exploitation part 1 — azeria-labs
- Heap exploitation part 2 — azeria-labs
man 3 malloc,man 2 brk,man 2 mmap