linking과정은 https://k0n9.tistory.com/entry/heap%EC%9D%98-%EA%B0%9C%EB%85%90-4 에 나오는 그림을 참고하면 좋다.
__libc_free
아래는 free함수 호출시 실행되는 코드이다.
//__libc_free 2.23
void
__libc_free (void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
void (*hook) (void *, const void *)
= atomic_forced_read (__free_hook);
if (__builtin_expect (hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS (0));
return;
}
if (mem == 0) /* free(0) has no effect */
return;
p = mem2chunk (mem);
if (chunk_is_mmapped (p)) /* release mmapped memory. */
{
/* see if the dynamic brk/mmap threshold needs adjusting */
if (!mp_.no_dyn_threshold
&& p->size > mp_.mmap_threshold
&& p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
{
mp_.mmap_threshold = chunksize (p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk (p);
return;
}
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}
libc_hidden_def (__libc_free)
하나하나 살펴보자.
atomic_forced_read로 __free_hook 를 읽어 hook포인터에 저장한다.
__free_hook 포인터(hook)가 지정되어 있을 시 hook 함수가 실행된다.
free(0)과 같이 메모리가 0인데 해제하려고 하면 아무일도 일어나지 않게 하는 코드이다.
mem2chunk(mem)는 mem이 포함된 청크의 포인터를 반환한다.
chunk_is_mmapped는 p(mem이 포함된 chunk의 포인터)가 mmap으로 할당된 메모리인지 여부를 확인한다.
그리고 이후의 과정을 거친 후 munmap으로 해제를 한다.
arena_for_chunk는 해당 청크(p)가 속한 아레나의 포인터를 반환한다.
_int_free는 청크를 해제하고 필요한 경우 다른 청크와 병합하거나 청크를 재사용한다.
free의 실질적인 함수인 __int_free를 보자.
_int_free
//_int_free 2.23
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */
const char *errstr = NULL;
int locked = 0;
size = chunksize (p);
/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
{
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked)
(void) mutex_unlock (&av->mutex);
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
{
errstr = "free(): invalid size";
goto errout;
}
check_inuse_chunk(av, p);
/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{
/* We might not have a lock at this point and concurrent modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock
|| ({ assert (locked == 0);
mutex_lock(&av->mutex);
locked = 1;
chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
|| chunksize (chunk_at_offset (p, size)) >= av->system_mem;
}))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
if (! have_lock)
{
(void)mutex_unlock(&av->mutex);
locked = 0;
}
}
free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);
set_fastchunks(av);
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
mchunkptr old = *fb, old2;
unsigned int old_idx = ~0u;
do
{
/* Check that the top of the bin is not the record we are going to add
(i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
deallocated. See use of OLD_IDX below for the actual check. */
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
{
errstr = "invalid fastbin entry (free)";
goto errout;
}
}
/*
Consolidate other non-mmapped chunks as they arrive.
*/
else if (!chunk_is_mmapped(p)) {
if (! have_lock) {
(void)mutex_lock(&av->mutex);
locked = 1;
}
nextchunk = chunk_at_offset(p, size);
/* Lightweight tests: check whether the block is already the
top block. */
if (__glibc_unlikely (p == av->top))
{
errstr = "double free or corruption (top)";
goto errout;
}
/* Or whether the next chunk is beyond the boundaries of the arena. */
if (__builtin_expect (contiguous (av)
&& (char *) nextchunk
>= ((char *) av->top + chunksize(av->top)), 0))
{
errstr = "double free or corruption (out)";
goto errout;
}
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely (!prev_inuse(nextchunk)))
{
errstr = "double free or corruption (!prev)";
goto errout;
}
nextsize = chunksize(nextchunk);
if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
{
errstr = "free(): invalid next size (normal)";
goto errout;
}
free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "free(): corrupted unsorted chunks";
goto errout;
}
p->fd = fwd;
p->bk = bck;
if (!in_smallbin_range(size))
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
bck->fd = p;
fwd->bk = p;
set_head(p, size | PREV_INUSE);
set_foot(p, size);
check_free_chunk(av, p);
}
/*
If the chunk borders the current high end of memory,
consolidate into top
*/
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}
/*
If freeing a large space, consolidate possibly-surrounding
chunks. Then, if the total unused topmost memory exceeds trim
threshold, ask malloc_trim to reduce top.
Unless max_fast is 0, we don't know if there are fastbins
bordering top, so we cannot tell for sure whether threshold
has been reached unless fastbins are consolidated. But we
don't want to consolidate on each free. As a compromise,
consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
is reached.
*/
if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
if (have_fastchunks(av))
malloc_consolidate(av);
if (av == &main_arena) {
#ifndef MORECORE_CANNOT_TRIM
if ((unsigned long)(chunksize(av->top)) >=
(unsigned long)(mp_.trim_threshold))
systrim(mp_.top_pad, av);
#endif
} else {
/* Always try heap_trim(), even if the top chunk is not
large, because the corresponding heap might go away. */
heap_info *heap = heap_for_ptr(top(av));
assert(heap->ar_ptr == av);
heap_trim(heap, mp_.top_pad);
}
}
if (! have_lock) {
assert (locked);
(void)mutex_unlock(&av->mutex);
}
}
/*
If the chunk was allocated via mmap, release via munmap().
*/
else {
munmap_chunk (p);
}
}
하나하나 분석해보자
해제하려는 메모리 청크의 주소가 유효한지 검사하는 코드이다.
p > -size 이거나 misaligned_chunk함수로 해당 메모리가 정렬이 되어있는지 판단한다.
예외 처리 후에는 mutex_unlock 후 에러 메시지를 출력후 리턴된다.
size가 청크의 최소 크기인 MINSIZE보다 작거나 size가 정렬되어있는지 판단한다.
에러출력후 errout레이블로 이동한다.
다음 chunk의 prev_inuse flag를 확인한다. 즉 현재 chunk가 사용중인지 확인한다.
사용중이 아니라면(이미 free되었다면) double free or corruption 예외를 발생시키고 함수를 종료한다.
즉 중복 해제의 오류를 방지한다.
get_max_fast함수는 fastbin chunk의 최대 크기를 반환하는 함수이다. 그래서 if 조건문으로 fastbin chunk의 최대크기보다 작거나 같은지 확인한다.
만약 TRIM_FASTBINS 매크로가 정의되어있으면 if조건문에 이하의 추가 조건이 생긴다.
chunk가 top chunk와 인접해있는지 확인한다.
만약 인접해있으면 chunk를 fastbin에 추가하지 않는다.
우선 chunk_at_offset (p, size)->size <= 2 * SIZE_SZ 조건은 요청받은 p에서 size만큼 떨어진 곳의 chunk의 크기가 최소크기보다 작은 경우를 검사하는 것이다.
chunksize(chunk_at_offset(p, size)) >= av->system_mem 조건은 요청받은 p에서 size만큼 떨어진 곳의 chunk의 크기가 시스템 메모리의 최대 크기보다 큰 경우를 검사한다.
만약 둘중 하나라도 해당되면 아래의 코드를 실행한다.
만약 have_lock이 참이고,
assert로 locked == 0인지 검사하고 mutex_lock으로 lock을 한 후 locked를 1 로 set한 후 chunk의 다음 size에 대한 검사를 한다.
만약 참일경우 에러를 출력하고 have_lock이 0이면 unlock을 해준다.
해당 chunk를 free한다.
fastchunk_bit를 설정한다. 이는 fastbin을 사용할 수 있다는 것을 나타낸다.
idx에 해당하는 fatbin list의 시작 주소를 fb에 저장한다.
fb에는 bin의 주소 포인터를 저장한다.
old 에는 bin의 주소값이 들어간다.
이전에 해제한 chunk인 old가 현재 해제하려는 chunk인 p 가 같은 경우 double free 오류를 발생시킨다.
have_lock이 설정되어있고, 이전에 해제한 chunk가 있다면
old_idx에 old 크기에 맞는 fast bin index를 저장한다.
현재 chunk의 fd와 old2에 old를 저장한다.
have_lock 이 설정되어있고, old가 NULL이 아니고 old_idx와 idx가 다르면 오류출력 후 errout 한다.
fastbin 연결 리스트에 있는 chunk와 크기가 같은 chunk가 여러개 있을 때, 잘못된 chunk를 fastbin에서 삭제하거나, 중복해서 추가하려고 할 때 발생한다.
해당 청크가 is_mmapped로 설정되어 있지 않으면
그리고 have_lock이 설정되지 않았으면 mutex_lock 걸어주고 locked를 1로 set 한다.
해제할 청크가 top chunk이면 에러 출력후 errout 한다.
해제하려는 chunk의 크기만큼 heap이 존재하는지 확인한다. 만약 없다면 heap의 뒷부분이 메모리 오염으로 간주하여 에러를 출력하고 errout 한다.
해제하려는 다음 chunk의 prev_inuse가 설정되어있지 않다면 에러출력하고 errout 한다.
해제하려는 chunk의 다음 chunk의 크기를 nextsize에 저장하고 크기를 비교한다. 맞지 않다면 에러 출력후 errout한다.
해제할 chunk를 free한다.
해제하려는 chunk의 이전 chunk가 사용중이지 않을 경우 현재 chunk와 이전 chunk를 병합한다.
해제한 chunk의 다음 chunk가 사용중이 아니라면 다음 chunk와 현재 chunk를 병합한다.
아니라면 다음 chunk의 prev_inuse bit를 0으로 set 한다.
해제한 chunk를 unsorted bin에 추가한다.
우선 bck에는 unsorted bin의 header를 넣고 bck의 fd를 fwd에 저장한다.
그리고 fwd->bk 가 bck와 같은지 검사하여 bin이 유효한지 확인한다.
만약 다르다면 오류출력후 errout
p의 fd와 bk를 설정하여 리스트에 추가한다,
만약 chunk의 크기가 smallbin range를 벗어나면 p->fd_nextsize와 p->bk_nextsize를 NULL로 설정한다.
마지막으로 set_head와 set_foot로 chunk의 헤더와 푸터를 설정하고 check_free_chunk함수로 chunk가 유효한지 확인한다.
해제하려는 chunk의 다음 chunk가 top chunk인 경우 topchunk와 병합한다.
헤제하려는 chunk의 size가 FASTBIN 임계값보다 크고
fastbin 이 존재하면 malloc_consolidat함수로 fsatbin을 병합한다.
만약 현재 arena가 main_arena이고 MORECORE_CANNOT_TRIM이 정의되어있지 않다면 topchunk의 크기가 trim 임계값보다 큰기 확인하고 크다면 systrim()함수를 호출하여 힙 공간을 OS에 반납한다.(즉 너무 많이 남는 공간은 반환한다.)
만약 현재 arena가 main_arena가 아니면 heap_for_ptr 함수를 이용하여 현재 arena에 해당하는 heap_info 구조체를 찾은 후 heap_trim 함수를 호출하여 힙 공간을 OS에 반납한다.
have_lock이 false이면 assert검사를 하여 locked가 1인지 검사하고 아니라면 mutex_unlock을 해준다,
마지막으로 munmap_chunk를 호출한다.
<틀린 부분이 있다면 비난과 욕설을 해주세요>
'포너블 > Heap' 카테고리의 다른 글
free 소스코드 분석(libc 2.27) (0) | 2023.08.28 |
---|---|
malloc 소스코드 분석(libc 2.27) (0) | 2023.08.23 |
malloc 소스코드 분석(libc 2.23) (0) | 2023.08.02 |
heap의 개념-(4) (0) | 2023.07.23 |
heap의 개념-(3) (0) | 2023.07.10 |