O heap é basicamente o lugar onde um programa poderá armazenar dados quando solicita dados chamando funções como **`malloc`**, `calloc`... Além disso, quando essa memória não é mais necessária, ela é disponibilizada chamando a função **`free`**.
Como mostrado, é logo após onde o binário está sendo carregado na memória (ver a seção `[heap]`):
Quando alguns dados são solicitados para serem armazenados no heap, um espaço do heap é alocado para isso. Esse espaço pertencerá a um bin e apenas os dados solicitados + o espaço dos cabeçalhos do bin + o deslocamento do tamanho mínimo do bin serão reservados para o chunk. O objetivo é reservar a menor quantidade de memória possível sem complicar a localização de onde cada chunk está. Para isso, as informações de chunk de metadados são usadas para saber onde cada chunk usado/livre está.
Existem diferentes maneiras de reservar o espaço, dependendo principalmente do bin utilizado, mas uma metodologia geral é a seguinte:
* O programa começa solicitando uma certa quantidade de memória.
* Se na lista de chunks houver alguém disponível grande o suficiente para atender à solicitação, ele será usado.
* Isso pode até significar que parte do chunk disponível será usada para essa solicitação e o restante será adicionado à lista de chunks.
* Se não houver nenhum chunk disponível na lista, mas ainda houver espaço na memória heap alocada, o gerenciador de heap cria um novo chunk.
* Se não houver espaço de heap suficiente para alocar o novo chunk, o gerenciador de heap solicita ao kernel que expanda a memória alocada para o heap e, em seguida, usa essa memória para gerar o novo chunk.
* Se tudo falhar, `malloc` retorna nulo.
Observe que se a **memória solicitada ultrapassar um limite**, **`mmap`** será usado para mapear a memória solicitada.
## Arenas
Em aplicações **multithreaded**, o gerenciador de heap deve prevenir **condições de corrida** que poderiam levar a falhas. Inicialmente, isso era feito usando um **mutex global** para garantir que apenas um thread pudesse acessar o heap por vez, mas isso causou **problemas de desempenho** devido ao gargalo induzido pelo mutex.
Para resolver isso, o alocador de heap ptmalloc2 introduziu "arenas", onde **cada arena** atua como um **heap separado** com suas **próprias** estruturas de **dados** e **mutex**, permitindo que múltiplos threads realizem operações de heap sem interferir uns nos outros, desde que usem arenas diferentes.
A arena "principal" padrão lida com operações de heap para aplicações de thread único. Quando **novos threads** são adicionados, o gerenciador de heap os atribui **arenas secundárias** para reduzir a contenção. Ele tenta primeiro anexar cada novo thread a uma arena não utilizada, criando novas se necessário, até um limite de 2 vezes o número de núcleos de CPU para sistemas de 32 bits e 8 vezes para sistemas de 64 bits. Uma vez que o limite é alcançado, **threads devem compartilhar arenas**, levando a uma potencial contenção.
Diferente da arena principal, que se expande usando a chamada de sistema `brk`, as arenas secundárias criam "subheaps" usando `mmap` e `mprotect` para simular o comportamento do heap, permitindo flexibilidade na gestão de memória para operações multithreaded.
### Subheaps
Subheaps servem como reservas de memória para arenas secundárias em aplicações multithreaded, permitindo que elas cresçam e gerenciem suas próprias regiões de heap separadamente do heap principal. Aqui está como os subheaps diferem do heap inicial e como operam:
1.**Heap Inicial vs. Subheaps**:
* O heap inicial está localizado diretamente após o binário do programa na memória, e se expande usando a chamada de sistema `sbrk`.
* Subheaps, usados por arenas secundárias, são criados através de `mmap`, uma chamada de sistema que mapeia uma região de memória especificada.
* Quando o gerenciador de heap cria um subheap, ele reserva um grande bloco de memória através de `mmap`. Essa reserva não aloca memória imediatamente; ela simplesmente designa uma região que outros processos ou alocações do sistema não devem usar.
* Por padrão, o tamanho reservado para um subheap é de 1 MB para processos de 32 bits e 64 MB para processos de 64 bits.
3.**Expansão Gradual com `mprotect`**:
* A região de memória reservada é inicialmente marcada como `PROT_NONE`, indicando que o kernel não precisa alocar memória física para esse espaço ainda.
* Para "crescer" o subheap, o gerenciador de heap usa `mprotect` para mudar as permissões de página de `PROT_NONE` para `PROT_READ | PROT_WRITE`, solicitando ao kernel que aloque memória física para os endereços previamente reservados. Essa abordagem passo a passo permite que o subheap se expanda conforme necessário.
Esta struct aloca informações relevantes do heap. Além disso, a memória heap pode não ser contínua após mais alocações, esta struct também armazenará essa informação.
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/arena.c#L837
typedef struct _heap_info
{
mstate ar_ptr; /* Arena for this heap. */
struct _heap_info *prev; /* Previous heap. */
size_t size; /* Current size in bytes. */
size_t mprotect_size; /* Size in bytes that has been mprotected
PROT_READ|PROT_WRITE. */
size_t pagesize; /* Page size used when allocating the arena. */
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-3 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;
```
### malloc\_state
**Cada heap** (arena principal ou outras arenas de threads) tem uma **estrutura `malloc_state`.**\
É importante notar que a **estrutura `malloc_state` da arena principal** é uma **variável global na libc** (portanto localizada no espaço de memória da libc).\
* O `mchunkptr bins[NBINS * 2 - 2];` contém **ponteiros** para os **primeiros e últimos chunks** dos **bins** pequenos, grandes e não ordenados (o -2 é porque o índice 0 não é usado)
* Portanto, o **primeiro chunk** desses bins terá um **ponteiro reverso para esta estrutura** e o **último chunk** desses bins terá um **ponteiro para frente** para esta estrutura. O que basicamente significa que se você puder **vazar esses endereços na arena principal**, você terá um ponteiro para a estrutura na **libc**.
* As structs `struct malloc_state *next;` e `struct malloc_state *next_free;` são listas encadeadas de arenas
* O chunk `top` é o último "chunk", que é basicamente **todo o espaço restante do heap**. Uma vez que o chunk top está "vazio", o heap está completamente utilizado e precisa solicitar mais espaço.
* O chunk `last reminder` vem de casos onde um chunk de tamanho exato não está disponível e, portanto, um chunk maior é dividido, uma parte do ponteiro restante é colocada aqui.
```c
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1812
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
```
### malloc\_chunk
Esta estrutura representa um determinado bloco de memória. Os vários campos têm significados diferentes para blocos alocados e não alocados.
Os metadados geralmente são 0x08B, indicando o tamanho atual do chunk, usando os últimos 3 bits para indicar:
*`A`: Se 1, vem de um subheap; se 0, está na arena principal
*`M`: Se 1, este chunk é parte de um espaço alocado com mmap e não parte de um heap
*`P`: Se 1, o chunk anterior está em uso
Então, o espaço para os dados do usuário, e finalmente 0x08B para indicar o tamanho do chunk anterior quando o chunk está disponível (ou para armazenar dados do usuário quando está alocado).
Além disso, quando disponível, os dados do usuário também são usados para conter alguns dados:
* **`fd`**: Ponteiro para o próximo chunk
* **`bk`**: Ponteiro para o chunk anterior
* **`fd_nextsize`**: Ponteiro para o primeiro chunk na lista que é menor que ele mesmo
* **`bk_nextsize`:** Ponteiro para o primeiro chunk na lista que é maior que ele mesmo
Quando malloc é usado, um ponteiro para o conteúdo que pode ser escrito é retornado (logo após os cabeçalhos), no entanto, ao gerenciar chunks, é necessário um ponteiro para o início dos cabeçalhos (metadados).\
Note que para calcular o espaço total necessário, `SIZE_SZ` é adicionado apenas 1 vez porque o campo `prev_size` pode ser usado para armazenar dados, portanto, apenas o cabeçalho inicial é necessário.
É possível ver que a string panda foi armazenada em `0xaaaaaaac12a0` (que foi o endereço dado como resposta pelo malloc dentro de `x0`). Verificando 0x10 bytes antes, é possível ver que o `0x0` representa que o **chunk anterior não está em uso** (comprimento 0) e que o comprimento deste chunk é `0x21`.
Os espaços extras reservados (0x21-0x10=0x11) vêm dos **cabeçalhos adicionados** (0x10) e 0x1 não significa que foi reservado 0x21B, mas os últimos 3 bits do comprimento do cabeçalho atual têm alguns significados especiais. Como o comprimento é sempre alinhado a 16 bytes (em máquinas de 64 bits), esses bits na verdade nunca serão usados pelo número de comprimento.
```
0x1: Previous in Use - Specifies that the chunk before it in memory is in use
0x2: Is MMAPPED - Specifies that the chunk was obtained with mmap()
0x4: Non Main Arena - Specifies that the chunk was obtained from outside of the main arena
```
### Exemplo de Multithreading
<details>
<summary>Multithread</summary>
```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void* threadFuncMalloc(void* arg) {
printf("Hello from thread 1\n");
char* addr = (char*) malloc(1000);
printf("After malloc and before free in thread 1\n");
free(addr);
printf("After free in thread 1\n");
}
void* threadFuncNoMalloc(void* arg) {
printf("Hello from thread 2\n");
}
int main() {
pthread_t t1;
void* s;
int ret;
char* addr;
printf("Before creating thread 1\n");
getchar();
ret = pthread_create(&t1, NULL, threadFuncMalloc, NULL);
getchar();
printf("Before creating thread 2\n");
ret = pthread_create(&t1, NULL, threadFuncNoMalloc, NULL);
printf("Before exit\n");
getchar();
return 0;
}
```
</details>
Depurando o exemplo anterior, é possível ver como no início há apenas 1 arena: