Bins & Memory Allocations

Wesprzyj HackTricks

Podstawowe Informacje

Aby poprawić efektywność przechowywania kawałków, każdy kawałek nie jest przechowywany tylko w jednym związku, ale istnieje kilka rodzajów. Są to zbiory, a istnieje 5 rodzajów zbiorów: 62 małych zbiorów, 63 dużych zbiorów, 1 nieuporządkowany zbiór, 10 szybkich zbiorów i 64 tcache bins na wątek.

Początkowy adres do każdego nieuporządkowanego, małego i dużego zbioru znajduje się w tym samym tablicy. Indeks 0 jest nieużywany, 1 to zbiór nieuporządkowany, zbiory 2-64 to małe zbiory, a zbiory 65-127 to duże zbiory.

Zbiory Tcache (Bufor na Wątek)

Mimo że wątki starają się mieć własny sterta (patrz Areny i Podsterty), istnieje możliwość, że proces z wieloma wątkami (np. serwer internetowy) będzie dzielił stertę z innymi wątkami. W takim przypadku głównym rozwiązaniem jest użycie blokad, które mogą znacząco spowolnić wątki.

Dlatego tcache jest podobny do szybkiego zbioru na wątek w taki sposób, że jest to jednokierunkowa lista, która nie łączy kawałków. Każdy wątek ma 64 jednokierunkowe tcache bins. Każdy zbiór może zawierać maksymalnie 7 kawałków tego samego rozmiaru o rozmiarze 24 do 1032B na systemach 64-bitowych i 12 do 516B na systemach 32-bitowych.

Gdy wątek zwalnia kawałek, jeśli nie jest zbyt duży, aby być przydzielony do tcache, a odpowiedni zbiór tcache nie jest pełny (już 7 kawałków), zostanie tam przydzielony. Jeśli nie może trafić do tcache, będzie musiał poczekać na blokadę sterty, aby móc wykonać operację zwolnienia globalnie.

Gdy kawałek jest przydzielany, jeśli istnieje wolny kawałek potrzebnego rozmiaru w Tcache, zostanie on użyty, jeśli nie, będzie musiał poczekać na blokadę sterty, aby znaleźć go w globalnych zbiorach lub utworzyć nowy. Istnieje także optymalizacja, w tym przypadku, podczas posiadania blokady sterty, wątek wypełni swoje Tcache kawałkami sterty (7) o żądanym rozmiarze, więc w przypadku potrzeby więcej, znajdzie je w Tcache.

Przykład dodania kawałka do tcache

```c #include #include

int main(void) { char *chunk; chunk = malloc(24); printf("Address of the chunk: %p\n", (void *)chunk); gets(chunk); free(chunk); return 0; }

Skompiluj to i debuguj z punktem przerwania w opcode ret z funkcji main. Następnie za pomocą gef możesz zobaczyć używany tcache bin:
```bash
gef➤  heap bins
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
Tcachebins[idx=0, size=0x20, count=1] ←  Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

Struktury i funkcje Tcache

W poniższym kodzie można zobaczyć maksymalne pojemniki i kawałki na indeks, strukturę tcache_entry stworzoną w celu uniknięcia podwójnych zwolnień oraz tcache_perthread_struct, strukturę, którą każdy wątek używa do przechowywania adresów do każdego indeksu pojemnika.

// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c

/* We want 64 entries.  This is an arbitrary limit, which tunables can reduce.  */
# define TCACHE_MAX_BINS		64
# define MAX_TCACHE_SIZE	tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables.  */
# define tidx2usize(idx)	(((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

/* When "x" is from chunksize().  */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size.  */
# define usize2tidx(x) csize2tidx (request2size (x))

/* With rounding and alignment, the bins are...
idx 0   bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1   bytes 25..40 or 13..20
idx 2   bytes 41..56 or 21..28
etc.  */

/* This is another arbitrary limit, which tunables can change.  Each
tcache bin will hold at most this number of chunks.  */
# define TCACHE_FILL_COUNT 7

/* Maximum chunks in tcache bins for tunables.  This value must fit the range
of tcache->counts[] entries, else they may overflow.  */
# define MAX_TCACHE_COUNT UINT16_MAX

[...]

typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees.  */
uintptr_t key;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct").  Keeping
overall size low is mildly important.  Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons.  */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

Funkcja __tcache_init to funkcja, która tworzy i alokuje miejsce dla obiektu tcache_perthread_struct

kod tcache_init

```c // From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L3241C1-L3274C2

static void tcache_init(void) { mstate ar_ptr; void *victim = 0; const size_t bytes = sizeof (tcache_perthread_struct);

if (tcache_shutting_down) return;

arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); if (!victim && ar_ptr != NULL) { ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); }

if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex);

/* In a low memory situation, we may not be able to allocate memory

  • in which case, we just keep trying later. However, we typically do this very early, so either there is sufficient memory, or there isn't enough memory to do non-trivial allocations anyway. */ if (victim) { tcache = (tcache_perthread_struct *) victim; memset (tcache, 0, sizeof (tcache_perthread_struct)); }

}

</details>

#### Indeksy Tcache

Tcache ma kilka kubełków w zależności od rozmiaru, a początkowe wskaźniki do **pierwszego fragmentu każdego indeksu oraz ilość fragmentów na indeks znajdują się wewnątrz fragmentu**. Oznacza to, że zlokalizowanie fragmentu z tą informacją (zazwyczaj pierwszego) pozwala znaleźć wszystkie początkowe punkty tcache i ilość fragmentów Tcache.

### Szybkie kubełki

Szybkie kubełki są zaprojektowane, aby **przyspieszyć alokację pamięci dla małych fragmentów**, trzymając niedawno zwolnione fragmenty w strukturze szybkiego dostępu. Te kubełki korzystają z podejścia Last-In, First-Out (LIFO), co oznacza, że **najbardziej niedawno zwolniony fragment jest pierwszy**, który będzie ponownie używany, gdy pojawi się nowe żądanie alokacji. To zachowanie jest korzystne dla szybkości, ponieważ szybciej jest wstawiać i usuwać z góry stosu (LIFO) w porównaniu do kolejki (FIFO).

Dodatkowo, **szybkie kubełki używają list jednokierunkowych**, a nie dwukierunkowych, co dodatkowo poprawia szybkość. Ponieważ fragmenty w szybkich kubełkach nie są łączone z sąsiadującymi, nie ma potrzeby skomplikowanej struktury, która pozwala na usuwanie z środka. Lista jednokierunkowa jest prostsza i szybsza dla tych operacji.

W zasadzie, to co się tutaj dzieje, to że nagłówek (wskaźnik do pierwszego fragmentu do sprawdzenia) zawsze wskazuje na najnowszy zwolniony fragment tego rozmiaru. Więc:

* Gdy alokowany jest nowy fragment tego rozmiaru, nagłówek wskazuje na wolny fragment do użycia. Ponieważ ten wolny fragment wskazuje na następny do użycia, ten adres jest przechowywany w nagłówku, aby następna alokacja wiedziała, gdzie znaleźć dostępny fragment
* Gdy fragment jest zwalniany, wolny fragment zapisze adres do aktualnie dostępnego fragmentu, a adres tego nowo zwolnionego fragmentu zostanie umieszczony w nagłówku

Maksymalny rozmiar listy jednokierunkowej to `0x80`, a są one zorganizowane tak, że fragment o rozmiarze `0x20` będzie w indeksie `0`, fragment o rozmiarze `0x30` będzie w indeksie `1`...

<div data-gb-custom-block data-tag="hint" data-style='danger'>

Fragmenty w szybkich kubełkach nie są ustawione jako dostępne, więc są trzymane jako fragmenty szybkich kubełków przez jakiś czas zamiast móc łączyć się z innymi wolnymi fragmentami otaczającymi je.

</div>

```c
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711

/*
Fastbins

An array of lists holding recently freed small chunks.  Fastbins
are not doubly linked.  It is faster to single-link them, and
since chunks are never removed from the middles of these lists,
double linking is not necessary. Also, unlike regular bins, they
are not even processed in FIFO order (they use faster LIFO) since
ordering doesn't much matter in the transient contexts in which
fastbins are normally used.

Chunks in fastbins keep their inuse bit set, so they cannot
be consolidated with other free chunks. malloc_consolidate
releases all chunks in fastbins and consolidates them with
other free chunks.
*/

typedef struct malloc_chunk *mfastbinptr;
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])

/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)


/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE     (80 * SIZE_SZ / 4)

#define NFASTBINS  (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)

Nieposortowany blok

Nieposortowany blok to pamięć podręczna używana przez menedżera sterty do przyspieszenia alokacji pamięci. Oto jak to działa: Gdy program zwalnia kawałek pamięci, i jeśli ten kawałek nie może być zaalokowany w tcache lub fast bin i nie koliduje z kawałkiem najwyższej sterty, menedżer sterty nie umieszcza go od razu w określonym małym lub dużym pojemniku. Zamiast tego najpierw próbuje połączyć go z sąsiednimi wolnymi kawałkami, aby utworzyć większy blok wolnej pamięci. Następnie umieszcza ten nowy kawałek w ogólnym pojemniku o nazwie "nieposortowany blok".

Gdy program prosi o pamięć, menedżer sterty sprawdza nieposortowany blok, aby zobaczyć, czy jest tam kawałek wystarczającej wielkości. Jeśli znajdzie taki, od razu go używa. Jeśli nie znajdzie odpowiedniego kawałka w nieposortowanym bloku, przenosi wszystkie kawałki z tej listy do odpowiadających im pojemników, małych lub dużych, w zależności od ich wielkości.

Należy zauważyć, że jeśli większy kawałek jest podzielony na 2 połowy i reszta jest większa niż MINSIZE, zostanie on ponownie umieszczony w nieposortowanym bloku.

Tak więc nieposortowany blok jest sposobem na przyspieszenie alokacji pamięci poprzez szybkie ponowne wykorzystanie niedawno zwolnionej pamięci i zmniejszenie potrzeby czasochłonnych wyszukiwań i łączeń.

Należy zauważyć, że nawet jeśli kawałki należą do różnych kategorii, jeśli dostępny kawałek koliduje z innym dostępnym kawałkiem (nawet jeśli początkowo należą do różnych pojemników), zostaną one połączone.

Małe pojemniki

Małe pojemniki są szybsze niż duże pojemniki, ale wolniejsze niż szybkie pojemniki.

Każdy z 62 pojemników będzie zawierał kawałki tej samej wielkości: 16, 24, ... (z maksymalną wielkością 504 bajtów w 32 bitach i 1024 w 64 bitach). Pomaga to w szybkości znajdowania pojemnika, w którym powinno być zaalokowane miejsce oraz w wstawianiu i usuwaniu wpisów na tych listach.

Tak jest obliczana wielkość małego pojemnika zgodnie z indeksem pojemnika:

  • Najmniejsza wielkość: 2*4*indeks (np. indeks 5 -> 40)

  • Największa wielkość: 2*8*indeks (np. indeks 5 -> 80)

// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
#define NSMALLBINS         64
#define SMALLBIN_WIDTH    MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > CHUNK_HDR_SZ)
#define MIN_LARGE_SIZE    ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

#define in_smallbin_range(sz)  \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)

Funkcja do wyboru między małymi a dużymi kubełkami:

#define bin_index(sz) \
((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))

<szczegóły>

Dodaj przykład małego kawałka```c #include #include

int main(void) { char *chunks[10]; int i;

// Loop to allocate memory 8 times for (i = 0; i < 9; i++) { chunks[i] = malloc(0x100); if (chunks[i] == NULL) { // Check if malloc failed fprintf(stderr, "Memory allocation failed at iteration %d\n", i); return 1; } printf("Address of chunk %d: %p\n", i, (void *)chunks[i]); }

// Loop to free the allocated memory for (i = 0; i < 8; i++) { free(chunks[i]); }

chunks[9] = malloc(0x110);

return 0; }

Zauważ, jak alokujemy i zwalniamy 9 bloków tego samego rozmiaru, aby **wypełnić tcache**, a ósmy jest przechowywany w nieuporządkowanym bloku, ponieważ jest **za duży dla fastbin**, a dziewiąty nie jest zwolniony, więc dziewiąty i ósmy **nie są scalane z głównym blokiem**. Następnie alokujemy większy blok o rozmiarze 0x110, co powoduje, że **blok w nieuporządkowanym bloku przechodzi do małego bloku**.

Skompiluj to i debuguj z punktem przerwania w operacji `ret` z funkcji `main`. Następnie za pomocą `gef` możesz zobaczyć, że blok tcache jest pełny, a jeden blok znajduje się w małym bloku:
```bash
gef➤  heap bins
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
Tcachebins[idx=15, size=0x110, count=7] ←  Chunk(addr=0xaaaaaaac1d10, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  Chunk(addr=0xaaaaaaac1c00, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  Chunk(addr=0xaaaaaaac1af0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  Chunk(addr=0xaaaaaaac19e0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  Chunk(addr=0xaaaaaaac18d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  Chunk(addr=0xaaaaaaac17c0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  Chunk(addr=0xaaaaaaac12a0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
──────────────────────────────────────────────────────────────────────── Small Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
[+] small_bins[16]: fw=0xaaaaaaac1e10, bk=0xaaaaaaac1e10
→   Chunk(addr=0xaaaaaaac1e20, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in 1 small non-empty bins.

Duże pojemniki

W przeciwieństwie do małych pojemników, które zarządzają kawałkami o stałych rozmiarach, każdy duży pojemnik obsługuje zakres rozmiarów kawałków. Jest to bardziej elastyczne, pozwalając systemowi pomieścić różne rozmiary bez konieczności posiadania osobnego pojemnika dla każdego rozmiaru.

W alokatorze pamięci duże pojemniki zaczynają się tam, gdzie kończą się małe pojemniki. Zakresy dla dużych pojemników rosną stopniowo, co oznacza, że pierwszy pojemnik może obejmować kawałki od 512 do 576 bajtów, podczas gdy następny obejmuje 576 do 640 bajtów. Ten wzorzec się kontynuuje, a największy pojemnik zawiera wszystkie kawałki powyżej 1 MB.

Duże pojemniki są wolniejsze w obsłudze w porównaniu z małymi pojemnikami, ponieważ muszą sortować i przeszukiwać listę różnych rozmiarów kawałków, aby znaleźć najlepsze dopasowanie dla alokacji. Gdy kawałek jest wstawiany do dużego pojemnika, musi być posortowany, a podczas alokacji pamięci system musi znaleźć odpowiedni kawałek. Dodatkowa praca sprawia, że są wolniejsze, ale ponieważ duże alokacje są mniej powszechne niż małe, jest to akceptowalny kompromis.

Istnieją:

  • 32 pojemniki o zakresie 64B (kolidują z małymi pojemnikami)

  • 16 pojemników o zakresie 512B (kolidują z małymi pojemnikami)

  • 8 pojemników o zakresie 4096B (częściowo kolidują z małymi pojemnikami)

  • 4 pojemniki o zakresie 32768B

  • 2 pojemniki o zakresie 262144B

  • 1 pojemnik dla pozostałych rozmiarów

Kod rozmiarów dużych pojemników

```c // From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711

#define largebin_index_32(sz) (((((unsigned long) (sz)) >> 6) <= 38) ? 56 + (((unsigned long) (sz)) >> 6) : ((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) : ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) : ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) : ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) : 126)

#define largebin_index_32_big(sz) (((((unsigned long) (sz)) >> 6) <= 45) ? 49 + (((unsigned long) (sz)) >> 6) : ((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) : ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) : ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) : ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) : 126)

// XXX It remains to be seen whether it is good to keep the widths of // XXX the buckets the same or whether it should be scaled by a factor // XXX of two as well. #define largebin_index_64(sz) (((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) : ((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) : ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) : ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) : ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) : 126)

#define largebin_index(sz) (SIZE_SZ == 8 ? largebin_index_64 (sz) : MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz) : largebin_index_32 (sz))

</details>

<details>

<summary>Dodaj przykład dużej porcji</summary>
```c
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
char *chunks[2];

chunks[0] = malloc(0x1500);
chunks[1] = malloc(0x1500);
free(chunks[0]);
chunks[0] = malloc(0x2000);

return 0;
}

2 duże alokacje są wykonywane, następnie jedna jest zwalniana (umieszczając ją w nieuporządkowanym pojemniku) i dokonywana jest większa alokacja (przenosząc zwolnioną do pojemnika nieuporządkowanego do pojemnika dużego).

Skompiluj to i debuguj z punktem przerwania w operacji ret z funkcji main. Następnie za pomocą gef możesz zobaczyć, że pojemnik tcache jest pełny, a jeden fragment jest w pojemniku dużym:

gef➤  heap bin
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
All tcachebins are empty
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
──────────────────────────────────────────────────────────────────────── Small Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
──────────────────────────────────────────────────────────────────────── Large Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
[+] large_bins[100]: fw=0xaaaaaaac1290, bk=0xaaaaaaac1290
   Chunk(addr=0xaaaaaaac12a0, size=0x1510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in 1 large non-empty bins.

Górny Kawałek

// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711

/*
Top

The top-most available chunk (i.e., the one bordering the end of
available memory) is treated specially. It is never included in
any bin, is used only if no other chunk is available, and is
released back to the system if it is very large (see
M_TRIM_THRESHOLD).  Because top initially
points to its own bin with initial zero size, thus forcing
extension on the first malloc request, we avoid having any special
code in malloc to check whether it even exists yet. But we still
need to do so when getting memory from system, so we make
initial_top treat the bin as a legal but unusable chunk during the
interval between initialization and the first call to
sysmalloc. (This is somewhat delicate, since it relies on
the 2 preceding words to be zero during this interval as well.)
*/

/* Conveniently, the unsorted bin can be used as dummy top on first call */
#define initial_top(M)              (unsorted_chunks (M))

Podstawowo, to kawałek zawiera całą obecnie dostępną stertę. Gdy wykonywane jest malloc, jeśli nie ma dostępnego wolnego kawałka do użycia, ten kawałek wierzchni będzie zmniejszał swoją wielkość, zapewniając niezbędną przestrzeń. Wskaźnik do kawałka wierzchniego jest przechowywany w strukturze malloc_state.

Co więcej, na początku możliwe jest użycie kawałka niesortowanego jako kawałka wierzchniego.

Zobacz przykład kawałka wierzchniego

```c #include #include

int main(void) { char *chunk; chunk = malloc(24); printf("Address of the chunk: %p\n", (void *)chunk); gets(chunk); return 0; }

Po skompilowaniu i debugowaniu z punktem przerwania w operacji `ret` w `main`, zauważyłem, że funkcja malloc zwróciła adres `0xaaaaaaac12a0`, a oto kawałki:
```bash
gef➤  heap chunks
Chunk(addr=0xaaaaaaac1010, size=0x290, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac1010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac12a0     41 41 41 41 41 41 41 00 00 00 00 00 00 00 00 00    AAAAAAA.........]
Chunk(addr=0xaaaaaaac12c0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac12c0     41 64 64 72 65 73 73 20 6f 66 20 74 68 65 20 63    Address of the c]
Chunk(addr=0xaaaaaaac16d0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac16d0     41 41 41 41 41 41 41 0a 00 00 00 00 00 00 00 00    AAAAAAA.........]
Chunk(addr=0xaaaaaaac1ae0, size=0x20530, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  top chunk

Gdzie można zobaczyć, że najwyższy kawałek znajduje się pod adresem 0xaaaaaaac1ae0. To żadna niespodzianka, ponieważ ostatni przydzielony kawałek był pod adresem 0xaaaaaaac12a0 o wielkości 0x410 i 0xaaaaaaac12a0 + 0x410 = 0xaaaaaaac1ae0. Można również zobaczyć długość najwyższego kawałka na jego nagłówku kawałka:

gef➤  x/8wx 0xaaaaaaac1ae0 - 16
0xaaaaaaac1ad0:	0x00000000	0x00000000	0x00020531	0x00000000
0xaaaaaaac1ae0:	0x00000000	0x00000000	0x00000000	0x00000000

Ostatni Resztek

Kiedy używane jest malloc i kawałek jest podzielony (na przykład z nieuporządkowanego bloku lub z kawałka górnego), kawałek stworzony z reszty podzielonego kawałka nazywany jest Ostatnim Resztkiem, a jego wskaźnik jest przechowywany w strukturze malloc_state.

Przepływ Alokacji

Sprawdź:

malloc & sysmalloc

Przepływ Zwolnienia

Sprawdź:

free

Sprawdzenia Bezpieczeństwa Funkcji Stosowanych w Stosie

Sprawdź sprawdzenia bezpieczeństwa wykonywane przez często używane funkcje w stosie w:

Heap Functions Security Checks

Referencje

Wesprzyj HackTricks

Last updated