[Glibc분석]__libc_malloc (2.38)

chk_pass·2024년 2월 20일
0

size_t형의 bytes(동적할당 크기)를 인자로 받아 void형 포인터 victim(할당된 힙 영역의 주소)를 반환한다.

부분을 나눠 자세하게 살펴보자.

PART 1

  mstate ar_ptr;
  void *victim;

  _Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2,
                  "PTRDIFF_MAX is not more than half of SIZE_MAX");

  if (!__malloc_initialized)
    ptmalloc_init ();

우선 mstate형 ar_ptr변수와 void형 포인터 victim이 선언된다.

mstate는 malloc_state구조체에 대한 포인터형이다.

typedef struct malloc_state *mstate;

그리고 victime은 malloc이 종료된 다음 반환될 변수라는 점을 기억하자.

그리고 그 다음 _Static_assert함수는 컴파일 타임에 조건을 검사하는 함수이므로 일단 넘어가자,

다음으로는 malloc이 초기화되었는지를 나타내는 __malloc_initialized가 flase이면 ptmalloc_init ()를 실행해 초기화를 진행한다.

/* Already initialized? */
static bool __malloc_initialized = false;

//https://elixir.bootlin.com/glibc/latest/source/malloc/arena.c#L261
static void
ptmalloc_init (void)
{
  if (__malloc_initialized)
    return;

  __malloc_initialized = true;

#if USE_TCACHE
  tcache_key_initialize ();
#endif
.....(중략)

우선 malloc_initialized의 초기값은 flase이며 ptmalloc_init() 내부 루틴에 의해 이 함수가 한번이라도 실행되면 malloc_initialized를 true로 만들기 때문에 무조건 초기화(=ptmalloc_init의 실행)는 맨 처음 한 번 이루어질 것이다.

PART 2

#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes = checked_request2size (bytes);
  if (tbytes == 0)
    {
      __set_errno (ENOMEM);
      return NULL;
    }
  size_t tc_idx = csize2tidx (tbytes);

  MAYBE_INIT_TCACHE ();

  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx < mp_.tcache_bins
      && tcache != NULL
      && tcache->counts[tc_idx] > 0)
    {
      victim = tcache_get (tc_idx);
      return tag_new_usable (victim);
    }
  DIAG_POP_NEEDS_COMMENT;
#endif

#if USE_TCACHE~#endif에 해당하는 위 부분은 tcache가 도입되며 추가된 부분이라고 한다.

즉, tcache에 관한 부분이다.

우선 size_t형의 tbytes라는 변수에 checked_request2size (bytes)의 반환 값을 넣어준다.

checked_request2size는 입력 받은 bytes가 PTRDIFF_MAX 보다 크다면 바로 0을, MINSIZE 보다 작으면 MINSIZE 를, 그 외에는 제대로 된 size의 값을 반환한다. 따라서 만약 요청된 bytes가 정해진 값보다 크다면 아래의 if문이 실행되어 에러 넘버가 설정되고 널 값이 반환되면서 malloc이 종료될 것이다. 그 외의 경우에는 tbytes에 적당한 size값이 대입 된 채로 계속 진행될 것이다.

다음으로는 tc_idx라는 변수에 csize2tidx (tbytes)의 반환값(=tbytes에 해당하는 tcache인덱스 값)을 넣는다.

다음으로는 MAYBE_INIT_TCACHE ()를 수행하여 tcache에 대한 초기화를 수행한다 (tcache_perthread_struct 구조체를 동적할당, 구조체 포인터 변수 tcache에 해당 주소 저장)

이는 tcache의 값이 NULL일 때만 실행되므로 가장 첫 번째의 malloc수행에만 초기화가 진행될 것임.

  • MAYBE_INIT_TCACHE 상세 <tcache관련 구조체, 변수 정의>
    typedef struct tcache_entry
    {
      struct tcache_entry *next;
      /* This field exists to detect double frees.  */
      uintptr_t key;
    } tcache_entry;
    
    //tcache를 관리하는 구조체
    typedef struct tcache_perthread_struct
    {
      uint16_t counts[TCACHE_MAX_BINS];
      tcache_entry *entries[TCACHE_MAX_BINS];
    } tcache_perthread_struct;
    
    static __thread tcache_perthread_struct *tcache = NULL;
    <함수 내부 코드>
    //https://elixir.bootlin.com/glibc/latest/source/malloc/malloc.c#L3264
    
    # define MAYBE_INIT_TCACHE() \
      if (__glibc_unlikely (tcache == NULL)) \
        tcache_init();
    
    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));
        }
    
    }

그리고 아래의 세 가지 조건을 만족하면 tcache_get함수를 실행시켜 해당 idx에 해당하는 tcache 청크를 재 할당한다.

  1. 아까 tcidx에 저장한 인덱스의 값이 mp.tcache_bins, 즉 TCACHE_MAX_BINS 의 값보다 작다면(tcache 내에서 유효한 인덱스라면)
  2. tache가 널이 아니라면
  3. tcache->counts[tc_idx]가 0보다 크다면 (해당 idx에 해당하는 freed tcache 청크가 tcache bin에 이미 존재한다면)

또한, 청크를 재할당하기 위한 매커니즘은 다음과 같다.

victim = tcache_get (tc_idx); ⇒ 해당 idx에 해당하는 tcache list의 청크 주소를 반환함 + tcache_perthread_struct의 해당 idx의 count를 1 줄이고 반환한(재할당할)청크의 key값을 널로 바꿈.

  • 함수 내부 상세
    tcache_get (size_t tc_idx)
    {
      return tcache_get_n (tc_idx, & tcache->entries[tc_idx]);
    }
    
    tcache_get_n (size_t tc_idx, tcache_entry **ep)
    {
      tcache_entry *e;
      if (ep == &(tcache->entries[tc_idx]))
        e = *ep;
      else
        e = REVEAL_PTR (*ep);
    
      if (__glibc_unlikely (!aligned_OK (e)))
        malloc_printerr ("malloc(): unaligned tcache chunk detected");
    
      if (ep == &(tcache->entries[tc_idx]))  
          *ep = REVEAL_PTR (e->next);
      else
        *ep = PROTECT_PTR (ep, REVEAL_PTR (e->next));
    
      --(tcache->counts[tc_idx]);
      e->key = 0;
      return (void *) e;
    }
    
    tag_new_usable (void *ptr)
    {
      if (__glibc_unlikely (mtag_enabled) && ptr)
        {
          mchunkptr cp = mem2chunk(ptr);
          ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp));
        }
      return ptr;
    }

만약 여기서 tcache의 재할당이 이루어진다면, __libc_malloc은 여기서 victim을 return하며 아예 종료되어버린다.

만약 이루어지지 않는다면 함수는 계속 진행된다.

PART 3

if (SINGLE_THREAD_P)
    {
      victim = tag_new_usable (_int_malloc (&main_arena, bytes));
      assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
	      &main_arena == arena_for_chunk (mem2chunk (victim)));
      return victim;
    }

tcache의 재할당이 이루어지지 않았을 때 실행되는 부분들이다.

해당 if문 내부는 단일 스레드일 때 실행된다. (SINGLE_THREAD_P)

그리고 main_arena의 주소와 bytes를 인자로 _int_malloc을 호출하고 반환 값을 victim에 넣는다.(실질적인 동적 할당이 일어나는 부분)

다음으로 다음 조건들을 확인하고(정상적으로 할당이 되었는지 확인) victim을 반환하고, 함수를 종료한다.

  1. victim이 널이 아님
  2. check for mmap()'ed chunk
  3. 청크가 main_arena의 청크인지 확인

단일 스레드인 경우 여기서 __libc_malloc이 종료된다.

PART 4

 arena_get (ar_ptr, bytes);

  victim = _int_malloc (ar_ptr, bytes);
  /* Retry with another arena only if we were able to find a usable arena
     before.  */
  if (!victim && ar_ptr != NULL)
    {
      LIBC_PROBE (memory_malloc_retry, 1, bytes);
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);
    }

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

  victim = tag_new_usable (victim);

  assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
          ar_ptr == arena_for_chunk (mem2chunk (victim)));
  return victim;

여기서부터는 단일 스레드 상황이 아닐 때 실행되는 부분일 것이다.

우선 알맞은 arena의 주소를 malloc_state포인터인 ar_ptr에 저장하고,

_int_malloc으로 ar_ptr기반의 동적할당을 해 victim에 할당된 주소를 저장한다.

다음으로 victim과 ar_ptr이 널이 아닐 때(즉, 제대로 된 동적할당이 이루어졌을 때)

usable arena가 존재하는 경우 해당 arena에 대해 malloc을 retry한다.

이후 assert로 동적할당이 제대로 이루어졌는지 확인하고

victim을 malloc의 반환값으로 반환하고 함수를 종료한다.

정리

전반적으로 초기화 ⇒ tcache확인 ⇒ 싱글 스레드 할당 ⇒ 멀티스레드 할당 의 순서로 이루어진다.

만약 tcache에 재사용할 bin이 존재하지 않는 이상, 실질적으로 동적할당이 일어나는 부분은 _int_malloc인 듯 하다. 이 함수를 살펴볼 필요가 있다.

0개의 댓글