Search

uClibc heap malloc 분석

Category
study
임베디드
Column
2024/02/11 15:10
Tags

1. infomation

uClibc 0.9.33.2 기준으로 설명. 32bit 기준.
malloc, free에 초점하여 소스코드 분석.
중요한 부분만 상세설명하며 매크로 함수 역시 중요한 부분만 상세 설명. 나머지는 간략 표시.
가독성을 위해 설명에 필요없는 라인(주석 포함)은 삭제.
libc/stdlib/malloc-standard/malloc.c, malloc.h

2. heap arena

struct malloc_state { /* The maximum chunk size to be eligible for fastbin */ size_t max_fast; /* low 2 bits used as flags */ // 하위 2bits는 flags로 설정 // 첫 하위 1bit[FASTCHUNKS_BIT] -> any free 청크가 있는지 체크. 있다면 1로 세팅 // ㄴ오로지 malloc_consolidate시 에만 0으로 설정됨 // 그 다음 1bit[ANYCHUNKS_BIT] -> fastbin에 free청크가 존재한다면 1로 세팅. /* Fastbins */ mfastbinptr fastbins[NFASTBINS]; // NFASTBINS==10, fastbins[10] /* 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]; // NBINS==96, bins[192] /* Bitmap of bins. Trailing zero map handles cases of largest binned size */ unsigned int binmap[BINMAPSIZE+1]; ...
C
복사
libc/stdlib/malloc-standard/malloc.h
glibc의 heap 구조를 알고 있다는 전제로 설명.
malloc_state 구조는 다음과 같다. glibc와 유사하며 fastbin은 10개의 빈을 가진다.
fastbin의 최대 크기는 80이다
fastbin 다음에 top 청크의 주소와 last_remainder 청크가 온다.
그다음 unsorted bin, small bin, large bin 을 관리하는 bins[192]가 온다.

3. malloc -fastbin 검색

void* malloc(size_t bytes) { mstate av; size_t nb; /* normalized request size */ unsigned int idx; /* associated bin index */ mbinptr bin; /* associated bin */ mfastbinptr* fb; /* associated fastbin */ mchunkptr victim; /* inspected/selected chunk */ size_t size; /* its size */ int victim_index; /* its bin index */ mchunkptr remainder; /* remainder from a split */ unsigned long remainder_size; /* its size */ unsigned int block; /* bit map traverser */ unsigned int bit; /* bit map traverser */ unsigned int map; /* current word of binmap */ mchunkptr fwd; /* misc temp for linking */ mchunkptr bck; /* misc temp for linking */ void * sysmem; void * retval; av = get_malloc_state(); // main_arena 객체 가져오기. checked_request2size(bytes, nb); // 매크로 함수. 아래 주석 참조 /* #define checked_request2size(req, sz) \ if (REQUEST_OUT_OF_RANGE(req)) { \ errno = ENOMEM;// 12 \ return 0; \ } //요청 크기가 REQUEST_OUT_OF_RANGE 보다 크면 안됨. //최대 요청 크기는 0x0xFFFFFFDF (sz) = request2size(req); // 요청크기 + 메타데이터(prev_size,size) 8bytes를 포함할 수 있는 크기 반환. // ex) 요청크기 0x20 -> 0x28 반환. 상세 구조는 request2size 매크로 확인. */ /* av->max_fast의 하위 1bit가 1이면 현재 free된 청크가 없다는 의미. 아직 free된 청크가 없는 경우, top 청크에서 split 하여 할당. */ if (!have_anychunks(av)) { if (av->max_fast == 0) /* initialization check */ __malloc_consolidate(av); goto use_top; }
C
복사
malloc.c
uClibc-0.9.33.2 를 사용하는 httpd main 함수의 시작 부분에서 av 값을 확인한 메모리 구조이다.
av→max_fast의 크기는 0x48로 설정되어 있다.
초기상태이므로 max_fast(0x48)의 하위 두 bits는 0으로 설정되어 있다
max_fast 다음 부터 4*10이 fastbin[0~10] 이다. (Top 전 까지)
초기 max_fast는 0x48로 설정되어 있으며, 두 bits에 따른 표현 범위는 다음과 같다
FASTCHUNKS_BIT : 0 ANYCHUNKS_BIT : 0 ⇒ 0x48
FASTCHUNKS_BIT : 0 ANYCHUNKS_BIT : 1 ⇒ 0x49
FASTCHUNKS_BIT : 1 ANYCHUNKS_BIT : 1 ⇒ 0x4b
따라서 0x48 사이즈 기준 실제 사용하는 fastbin은 0~7이다.
(각 사이즈별 크긴는 아래서 설명)
초기 bins 들은 아직 free된 청크가 없으므로 fd,bk가 동일한 값을 가지고 있다.
void* malloc(size_t bytes) { ... /* If the size qualifies as a fastbin, first check corresponding bin. #define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) - 2) */ if ((unsigned long)(nb) <= (unsigned long)(av->max_fast)) { fb = &(av->fastbins[(fastbin_index(nb))]); if ( (victim = *fb) != 0) { *fb = victim->fd; // fastbin에 들어있는 청크를 재할당 해주고 fd에 들어있는 // 다음 free 청크를 앞으로 땡겨야하기 때문에 fb에 해당 청크 주소 저장 check_remalloced_chunk(victim, nb); //재할당 하려는 청크 사이즈 유효성 검증 retval = chunk2mem(victim); // 청크헤더+8 주소 반환 goto DONE; } }
C
복사
nb(요청사이즈+헤더(8))가 fastbin 사이즈에 만족하면(≤ av→max_fast)
fastbin_index() 매크로 함수를 이용하여 fasbin 인덱스를 찾는다.
각 인덱스 별 fastbin 크기는 다음과 같다.(실제 사용 인덱스 0~7, fastbin은 단일 연결 리스트)
fastbin[0] : 0x10 ~ 0x17
fastbin[1] : 0x18 ~ 0x1f
fastbin[2] : 0x20 ~ 0x27
fastbin[3] : 0x28 ~ 0x2f
fastbin[4] : 0x30 ~ 0x37
fastbin[5] : 0x38 ~ 0x3f
fastbin[6] : 0x40 ~ 0x47
fastbin[7] : 0x48 ~ 0x4f

4. malloc - smallbin 검색

/* If a small request, check regular bin. Since these "smallbins" hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) MIN_LARGE_SIZE = 256 #define in_smallbin_range(sz) ((unsigned long)(sz) < (unsigned long)MIN_LARGE_SIZE) #define smallbin_index(sz) (((unsigned)(sz)) >> 3) /* addressing -- note that bin_at(0) does not exist */ #define bin_at(m, i) ((mbinptr)((char*)&((m)->bins[(i)<<1]) - ((sizeof(size_t))<<1))) #define first(b) ((b)->fd) #define last(b) ((b)->bk) */ if (in_smallbin_range(nb)) { // 최대 smallbin 크기는 255 idx = smallbin_index(nb); bin = bin_at(av,idx); // 인덱스에 맞는 bin 주소 가져오기 if ( (victim = last(bin)) != bin) { // 재할당 하려는 bin에 연결된 다른 free 청크들이 있다면 연결 해제 bck = victim->bk; set_inuse_bit_at_offset(victim, nb); bin->bk = bck; bck->fd = bin; // A -> B -> C 구조에서 B를 재할당 하려면 // A -> C 로 free list 재구성 // B를 재할당 하기 때문에 C 청크의 inuse bit 설정 check_malloced_chunk(victim, nb); retval = chunk2mem(victim); //청크헤더+8 주소 반환 goto DONE; } } else { idx = __malloc_largebin_index(nb); if (have_fastchunks(av)) __malloc_consolidate(av); }
C
복사
요청 크기가 fastbin 크기가 아니거나, 현재 fastbin에 가용할 청크가 없다면 해당 루틴으로 내려온다.
따라서 bin[2] ~ bin[16] 크기는 fastbin[0]~fastbin[7] 크기와 동일하다.
smallbin 최대 크기는 255로 이보다 작은 값이면 smallbin_index를 계산한다.
fastbin 을 제외한 unsortedbin, smallbin, largebin은 이중연결 리스트로 bin의 각 인덱스 당 2개 씩 가진다.(fd,bk)
청크 최소 사이즈는 0x10이기 때문에 idx는 0x10>>3 : 2부터 시작한다.
각 인덱스 별 최소 smallbin 크기는 다음과 같다.(idx 0은 사용X)
idx 2 ⇒ bin[2-3] : 0x10 // unsorted bin
idx 3 ⇒ bin[4-5] : 0x18
idx 4 ⇒ bin[6-7] : 0x20
idx 5 ⇒ bin[8-9] : 0x28
idx 6 ⇒ bin[10-11] : 0x30
idx 7 ⇒ bin[12-13] : 0x38
idx 8 ⇒ bin[14-15] : 0x40
idx 9 ⇒ bin[16-17] : 0x48
idx 10 ⇒ bin[18-19] : 0x50
idx 11 ⇒ bin[20-21] : 0x58
idx 31 ⇒ bin[30-31] : 0xf8
/* If a small request, check regular bin. Since these "smallbins" hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) MIN_LARGE_SIZE = 256 #define in_smallbin_range(sz) ((unsigned long)(sz) < (unsigned long)MIN_LARGE_SIZE) #define smallbin_index(sz) (((unsigned)(sz)) >> 3) #define bin_at(m, i) ((mbinptr)((char*)&((m)->bins[(i)<<1]) - ((sizeof(size_t))<<1))) #define first(b) ((b)->fd) #define last(b) ((b)->bk) # */ if (in_smallbin_range(nb)) { // 최대 smallbin 크기는 255 idx = smallbin_index(nb); bin = bin_at(av,idx); // 인덱스에 맞는 bin 주소 가져오기 ... } } else { // 요청 사이즈가 255보다 크다면 largebin에서 검색 // __malloc_largebin_index -> bin[32] 부터 각 bin당 64bytes 크기 idx = __malloc_largebin_index(nb); if (have_fastchunks(av)) __malloc_consolidate(av); }
C
복사
255보다 큰 값 일 경우 large bin의 index를 가져온다.
largebin 인덱스는 idx 32 부터 시작한다.
idx 32 => bin[64-65] : 0x100 ~ 0x13f idx 33 => bin[66-67] : 0x140 ~ 0x17f
C
복사
largebin index 계산 후 fastbin에 청크가 존재할 경우 malloc_consolidate를 호출하여 fastbin 청크들의 병합을 수행한다.
병합된 fastbin 청크들은 unsorted bin에 위치한다.

5. malloc - unsortedbin 검색

/* Process recently freed or remaindered chunks, taking one only if it is exact fit, or, if this a small request, the chunk is remainder from the most recent non-exact fit. Place other traversed chunks in bins. Note that this step is the only place in any routine where chunks are placed in bins. #define unsorted_chunks(M) (bin_at(M, 1)) // -> bin[0] #define bin_at(m, i) ((mbinptr)((char*)&((m)->bins[(i)<<1]) - ((sizeof(size_t))<<1))) */ while ( (victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) { // unsortedbin에 가용 가능한 청크가 존재하는 경우. 소진 될 때 까지 반복 bck = victim->bk; // unsorted bin의 맨 뒤 청크 size = chunksize(victim); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range(nb) && // 요청 크기가 smallbin 크기고 bck == unsorted_chunks(av) && // unsorted bin에 한 개의 청크만 있고 victim == av->last_remainder && // 그 청크가 last_remainder라면 (unsigned long)(size) > (unsigned long)(nb + MINSIZE)) { // unsorted bin에 있는 청크 크기가 요청크기+MINSIZE보다 크다면 // last_remainder 청크를 split 수행 /* split and reattach remainder */ remainder_size = size - nb; // split 후 남은 크기 remainder = chunk_at_offset(victim, nb); // split 한 청크 unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder; // split 하고 남은 청크를 새로운 unsorted bin으로 등록 av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks(av); // split 하고 남은 청크는 또한 last_remainder 청크가 된다. set_head(victim, nb | PREV_INUSE); set_head(remainder, remainder_size | PREV_INUSE); set_foot(remainder, remainder_size); check_malloced_chunk(victim, nb); // split하여 만든 청크 재할당 retval = chunk2mem(victim); goto DONE; } /* remove from unsorted list */ // 요청크기가 smallbin 크기가 아니거나 // unsorted bin에 여러개의 청크가 있거나 // victim이 last_remainder가 아닐경우 // unsorted bin 탐색에 알맞는 청크 검색이 실패했다는 의미 // unsorted bin에서 제거한다 unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av); /* Take now instead of binning if exact fit */ // 제거한 거를 일단 처리를 해야하니까 // victim의 크기가 요청 크기와 딱 맞다면 해당 청크 반환 if (size == nb) { set_inuse_bit_at_offset(victim, size); check_malloced_chunk(victim, nb); retval = chunk2mem(victim); goto DONE; } /* place chunk in bin */ // 여기까지 왔다면 unsorted bin의 탐색 실패를 의미. 각자의 bin으로 집어넣기 수행 if (in_smallbin_range(size)) { // smallbin일 경우 smallbin에 넣기 victim_index = smallbin_index(size); bck = bin_at(av, victim_index); fwd = bck->fd; } else {//largebin일 경우 largebin에 넣기 victim_index = __malloc_largebin_index(size); bck = bin_at(av, victim_index); fwd = bck->fd; if (fwd != bck) { /* if smaller than smallest, place first */ //largebin 크기 정렬 수행 if ((unsigned long)(size) < (unsigned long)(bck->bk->size)) { fwd = bck; bck = bck->bk; } else if ((unsigned long)(size) >= (unsigned long)(FIRST_SORTED_BIN_SIZE)) { /* maintain large bins in sorted order */ size |= PREV_INUSE; /* Or with inuse bit to speed comparisons */ while ((unsigned long)(size) < (unsigned long)(fwd->size)) fwd = fwd->fd; bck = fwd->bk; } } } mark_bin(av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim; }
C
복사
만약
fastbin에 가용 가능한 청크가 없거나
smallbin에 가용가능한 청크가 없거나
largebin 요청 크기거나
할 경우 해당 로직으로 온다.
unsortedbin에 가용가능한 청크를 검색하고 존재한다면 분기문 안으로 들어온다.

6. malloc -Largebin 검색

/* If a large request, scan through the chunks of current bin to find one that fits. (This will be the smallest that fits unless FIRST_SORTED_BIN_SIZE has been changed from default.) This is the only step where an unbounded number of chunks might be scanned without doing anything useful with them. However the lists tend to be short. */ if (!in_smallbin_range(nb)) { //smallbin 크기가 아니면 largebin에서 검색 bin = bin_at(av, idx); for (victim = last(bin); victim != bin; victim = victim->bk) { size = chunksize(victim); if ((unsigned long)(size) >= (unsigned long)(nb)) { remainder_size = size - nb; unlink(victim, bck, fwd); /* Exhaust */ if (remainder_size < MINSIZE) { set_inuse_bit_at_offset(victim, size); check_malloced_chunk(victim, nb); retval = chunk2mem(victim); goto DONE; } /* Split */ //split하여 남은 청크를 unsortedbin에 삽입 else { remainder = chunk_at_offset(victim, nb); unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder; remainder->bk = remainder->fd = unsorted_chunks(av); set_head(victim, nb | PREV_INUSE); set_head(remainder, remainder_size | PREV_INUSE); set_foot(remainder, remainder_size); check_malloced_chunk(victim, nb); retval = chunk2mem(victim); // 세팅 끝난 청크 반환 goto DONE; } } } }
C
복사
Largebin에서 검색하여 할당 가능한 청크를 검색한다.
요청 크기를 처리할 수 있는 청크가 존재할 경우 split하여 가용가능한 남은 청크 크기가 존재하면 unsortedbin에 넣어두고
split한 청크를 반환한다.

7. malloc -binmap 검색

/* Search for a chunk by scanning bins, starting with next largest bin. This search is strictly by best-fit; i.e., the smallest (with ties going to approximately the least recently used) chunk that fits is selected. The bitmap avoids needing to check that most blocks are nonempty. */ ++idx; bin = bin_at(av,idx); block = idx2block(idx); map = av->binmap[block]; bit = idx2bit(idx); for (;;) { /* Skip rest of block if there are no more set bits in this block. */ if (bit > map || bit == 0) { do { if (++block >= BINMAPSIZE) /* out of bins */ goto use_top; } while ( (map = av->binmap[block]) == 0); bin = bin_at(av, (block << BINMAPSHIFT)); bit = 1; } /* Advance to bin with set bit. There must be one. */ while ((bit & map) == 0) { bin = next_bin(bin); bit <<= 1; assert(bit != 0); } /* Inspect the bin. It is likely to be non-empty */ victim = last(bin); /* If a false alarm (empty bin), clear the bit. */ if (victim == bin) { av->binmap[block] = map &= ~bit; /* Write through */ bin = next_bin(bin); bit <<= 1; } else { size = chunksize(victim); /* We know the first chunk in this bin is big enough to use. */ assert((unsigned long)(size) >= (unsigned long)(nb)); remainder_size = size - nb; /* unlink */ bck = victim->bk; bin->bk = bck; bck->fd = bin; /* Exhaust */ if (remainder_size < MINSIZE) { set_inuse_bit_at_offset(victim, size); check_malloced_chunk(victim, nb); retval = chunk2mem(victim); goto DONE; } /* Split */ else { remainder = chunk_at_offset(victim, nb); unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder; remainder->bk = remainder->fd = unsorted_chunks(av); /* advertise as last remainder */ if (in_smallbin_range(nb)) av->last_remainder = remainder; set_head(victim, nb | PREV_INUSE); set_head(remainder, remainder_size | PREV_INUSE); set_foot(remainder, remainder_size); check_malloced_chunk(victim, nb); retval = chunk2mem(victim); goto DONE; } } }
C
복사
fastbin, smallbin,largebin,unsortedbin들에서 가용가능한 청크가 없다면
binmap을 참조하여 할당 가능한 청크를 검색한다.

7. malloc -Top 검색

use_top: /* If large enough, split off the chunk bordering the end of memory (held in av->top). Note that this is in accord with the best-fit search rule. In effect, av->top is treated as larger (and thus less well fitting) than any other available chunk since it can be extended to be as large as necessary (up to system limitations). We require that av->top always exists (i.e., has size >= MINSIZE) after initialization, so if it would otherwise be exhuasted by current request, it is replenished. (The main reason for ensuring it exists is that we may need MINSIZE space to put in fenceposts in sysmalloc.) */ victim = av->top; size = chunksize(victim); if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset(victim, nb); av->top = remainder; set_head(victim, nb | PREV_INUSE); set_head(remainder, remainder_size | PREV_INUSE); check_malloced_chunk(victim, nb); retval = chunk2mem(victim); goto DONE; } /* If no space in top, relay to handle system-dependent cases */ sysmem = __malloc_alloc(nb, av); retval = sysmem; DONE: __MALLOC_UNLOCK; return retval;
C
복사
그래도 못찾았다면 Top 청크에서 검색
Top 청크에서도 마땅한 청크가 없다면 메모리 추가 요청 후 할당