[W04] double free error

silver ·2026년 9월 12일

크래프톤 정글

목록 보기
14/22

연결 리스트의 frontBackSplitLinkedList() 문제를 풀면서 하나의 연결 리스트를 front와 back으로 나누는 함수를 구현했다.

예를 들어,
1 -> 2 -> 3 -> 4 -> 5 를

Front: 1 -> 2 -> 3 ,
Back : 4 -> 5
로 나누는 문제였다.

리스트를 나누는 것 자체는 잘 됐고 출력도 예상대로 나왔다.

그런데 프로그램이 끝나는 순간 갑자기 이런 에러가 발생했다.

free(): double free detected in tcache 2
Aborted (core dumped)

처음에는 리스트를 나누는 로직이 잘못된 줄 알았는데, 확인해보니 문제는 출력이 아니라 메모리를 해제하는 과정에 있었다.원인을 따라가 보니 같은 메모리를 두 번 해제하면서 발생한 Double Free 에러였다.

Double Free란?

Double Free는 말 그대로 같은 메모리 주소를 두 번 이상 free()하는 것이다.

예를 들어,

int *p = malloc(sizeof(int));

free(p);
free(p);

첫 번째 free(p)에서 이미 해당 메모리는 해제됐다.

그런데 같은 주소를 다시 free()하면 메모리 관리 구조가 꼬일 수 있다.

단순히 프로그램이 종료되는 정도로 끝날 수도 있지만, 경우에 따라 힙 메모리가 손상되거나 보안 취약점으로 이어질 수도 있다.

같은 포인터를 두 번 free해야만 발생하는 건 아니다

처음에는 Double Free라고 해서 이런 경우만 생각했다.

free(p);
free(p);

그런데 서로 다른 포인터가 같은 메모리를 가리키고 있는 경우에도 Double Free가 발생할 수 있다.

예를 들어,

int *p = malloc(sizeof(int));
int *q = p;

라고 하면 p와 q는 서로 다른 포인터 변수지만 같은 메모리를 가리킨다.

p ──┐
    ↓
  memory
    ↑
q ──┘

이 상태에서

free(p);
free(q);

를 하면 결국 같은 메모리를 두 번 해제하게 된다.

이 내용을 보고 나니, 내가 풀던 연결 리스트 문제에서 왜 같은 에러가 났는지도 조금씩 보이기 시작했다.

내 코드에서는 뭐가 문제였을까?

frontBackSplitLinkedList()를 구현하면서 이런 코드를 작성했다.

resultFrontList->head = ll->head;

이 코드를 다시 살펴보면, 리스트 내용을 새로 복사하는 것이 아니라
resultFrontList->head에 ll->head가 가지고 있던 노드 주소를 그대로 대입하고 있다.

따라서 실제 구조는

ll->head ────────────────┐
                      	↓
                      	1 -> 2 -> 3
                      	↑
resultFrontList->head ──┘

처럼 된다.

즉 ll과 resultFrontList가 서로 다른 리스트 변수이기는 하지만, 내부적으로는 같은 노드들을 가리키고 있는 상태다.

그래서 리스트를 나눈 뒤에도 원본 ll이 그대로 그 노드들을 가리키고 있으면, 나중에 각 리스트를 해제하는 과정에서 문제가 생길 수 있다.

예를 들어,

removeAllItems(&ll);
removeAllItems(&resultFrontList);
removeAllItems(&resultBackList);

처럼 모두 해제한다고 하면,

먼저 ll을 해제하면서 1, 2, 3 노드의 메모리가 이미 해제된다.

그런데 resultFrontList도 같은 1, 2, 3 노드를 가리키고 있으니까 다시 같은 메모리를 해제하려고 한다.

그 결과,

free(): double free detected

에러가 발생한 것이다.

해결방법은?

리스트를 front와 back으로 나눈 뒤, 원본 리스트가 더 이상 기존 노드들을 가리키지 않도록 했다.

ll->head = NULL;

그러면 구조가 이렇게 된다.

ll:
head -> NULL


resultFrontList:
1 -> 2 -> 3


resultBackList:
4 -> 5

이제 실제 노드들은 resultFrontList와 resultBackList가 나눠서 가지고 있고, 원본 ll은 더 이상 같은 노드를 가리키지 않는다.

그래서 각 노드를 한 번씩만 해제할 수 있게 된다.

이번 문제에서 알게 된 점

resultFrontList->head = ll->head;

라고 작성하면 새로운 노드가 만들어지는 것이 아니라, 두 포인터가 같은 노드를 가리키게 된다.

포인터 A ──┐
          ↓
         Node
          ↑
포인터 B ──┘

즉 포인터 변수가 두 개 있어도 실제 메모리는 하나일 수 있다.

그래서 메모리를 해제할 때는 단순히 포인터의 개수만 보는 게 아니라, 각 포인터가 실제로 어떤 메모리를 가리키고 있는지를 확인해야 한다.

Double Free를 피하려면

이번 문제를 통해 메모리를 직접 관리하는 C에서는 다음 내용들을 신경 써야 한다는 걸 알게 됐다.

  • malloc()으로 할당한 메모리는 한 번만 free()하기
  • 여러 포인터가 같은 메모리를 가리키고 있는지 확인하기
  • 어떤 자료구조가 해당 메모리를 관리할지 명확히 하기
  • 필요하면 free() 후 포인터를 NULL로 바꾸기

예를 들어,

free(p);
p = NULL;

처럼 처리해두면 같은 포인터를 실수로 다시 사용하는 걸 줄일 수 있다.

정리하면,

포인터가 여러 개여도 실제 메모리는 하나일 수 있다.

그리고 연결 리스트처럼 노드의 주소를 직접 연결해서 사용하는 경우에는 next 연결만 맞추는 데서 끝나는 게 아니라, 지금 이 노드를 어떤 포인터들이 가리키고 있는지, 마지막에 누가 free()해야 하는지도 같이 봐야 한다.

0개의 댓글