본문 바로가기

CS/CS Book

[컴퓨터 시스템 딥다이브] ch03 C 디버깅 도구

💡이 글은 컴퓨터 시스템 딥다이브(수잰 J. 매슈스, 한빛미디어 2024)를 읽고 적은 글입니다.

GDB, Valgrind, DDD 등 디버깅 도구들에 대해 소개 및 활용방법에 대해 알려준다.

아무래도 컴퓨터 시스템이 주제이다 보니 이후에 나오는 내용들에 대해 실습을 할 수 있고 그럴 때 디버깅을 해야 할 일이 생기기 쉬울 테니 알려주는 것 같다.

C++ 홍정모님의 자료구조 강의를 듣고 실습 중이었는데 책에 소개된 디버깅 방법들을 활용해 보았다.

Source Code

LinkedNode.cpp

#include <iostream>

using namespace std;

struct Node
{
	int item = 0;	  // <- 큰 데이터도 가능
	Node* next = nullptr;

	friend ostream& operator<<(ostream& os, const Node& n)
	{
		cout << n.item << " " << flush;
		return os;
	}
};

void RecurPrint(Node* node)
{
	// TODO:
	if (node)
	{
		std::cout << node->item << std::endl;
		RecurPrint(node->next);
	}
}

void IterPrint(Node* node)
{
	// TODO:
	std::cout << "# IterPrint" << std::endl;
	Node *cur = node;

	while (cur)
	{
		std::cout << cur->item << std::endl;
		cur = cur->next;
	}
}

int main()
{
	// ListArray와 비교

	Node* first = nullptr;
	first = new Node;
	first->item = 1;
	first->next = nullptr;

	Node* second = nullptr;
	second = new Node;
	second->item = 2;
	second->next = nullptr;

	Node* third = nullptr;
	third = new Node;
	third->item = 3;
	third->next = nullptr;

	Node* fourth = nullptr;
	fourth = new Node;
	fourth->item = 4;
	fourth->next = nullptr;

	Node* fifth = nullptr;
	fifth = new Node;
	fifth->item = 5;
	fifth->next = nullptr;

	// 계속 추가 가능

	cout << *first << endl;
	cout << *second << endl;
	cout << *third << endl;
	cout << *fourth << endl;
	cout << *fifth << endl;
	cout << endl;

	// cur = first;
	// // 연결 관계 만들어 주기
	// first->next = second;
	// TODO:
	first->next = second;
	second->next = third;
	third->next = fourth;
	fourth->next = fifth;
	// 마지막

	cout << *(first) << endl;
	cout << *(first->next) << endl;
	cout << *(first->next->next) << endl;
	cout << *(first->next->next->next) << endl;
	cout << *(first->next->next->next->next) << endl;
	// cout << *(first->next->next->next->next->next) << endl; // 오류

	cout << endl;

	// 임시 변수 사용
	{
		Node* current = first;

		cout << "# 임시 변수 사용" << endl;
	// TODO:
		while (current)
		{
			cout << *current << endl;
			current = current->next;
		}
		cout << endl;
	}

	// 재귀 호출 이용
	RecurPrint(first);
	cout << endl;

	// 반복문 이용
	IterPrint(first);
	cout << endl;

	// TODO: 데이터 삭제
	// Node *cur = first;
	Node *cur = first;
	Node *next;
	while (cur)
	{
		next = cur->next;
		delete cur;
		cur = next;
	}

	return 0;
}

Makefile

NAME = LinkedNode
CXX = g++
CXXFLAGS = #-Wall -Wextra -Werror
RM = rm -f

SRCS = LinkedNode.cpp

OBJS = $(SRCS:.cpp=.o)

%.o : %.cpp
	$(CXX) $(CXXFLAGS) -c $^ -o $@

$(NAME) : $(OBJS)
	$(CXX) $(CXXFLAGS) -o $@ $^

all : $(NAME)

clean :
	$(RM) $(OBJS)

fclean : clean
	$(RM) $(NAME)

re :
	make fclean
	make all

.PHONY: all clean fclean re

 

 

GDB

command 순서

  1. g++ -g LinkedNode.cpp
    1. C++ 파일이라서 명령어를 g++로 대체하였다. (C의 경우 gcc -g [file_name.c])
  2. gdb a.out

 

gdb 내부에서 쓰는 명령어

break [function name]

  1. 여기서는 break main으로 하였다

run

n (next의 의미)

  1. 다음 줄로 진행된다.

p (print의 의미)

  1. 현재까지 진행된 코드들 중에서 변수나 함수의 값을 들여다볼 수 있다.

 

display

  1. 중단점에 도달할 때마다 동일한 프로그램 변수 집합을 자동으로 출력하도록 GDB에 요청한다.

 

break [source code line number]

  1. break 100
  2. 특정 라인으로 디버깅 중단점을 설정할 수 있다.

cont (continue의 의미)

  1. 설정한 중단점으로 넘어갈 수 있다.

where

  1. 스택의 현재 상태 출력
  2. 현재 디버깅 중인 위치가 소스코드 상에서 어디인지 알려준다.

list

중단점 인근의 소스코드들을 볼 수 있다.

frame

  1. 스택의 모든 프레임 콘텍스트로 이동한다.
  2. 각 스택 프레임 컨텍스트 내에서 사용자는 해당 프레임의 지역 변수와 매개변수를 검사할 수 있다.
  3. 스택 프레임에 쌓인 것이 main함수 밖에 없어서 frame 0만 동작하는 듯하다. main안에서 다른 함수를 호출하여 들어간 상태라면 frame1이 가능할 것으로 보인다.

4. RecurPrint() 재귀로 동작하는 함수에 중단점을 두었다. 호출한 깊이만큼 스택 프레임에 쌓인다. 여기서는 frame4까지도 스택 프레임이 찍히는 것을 확인할 수 있다.

 

 

Valgrind

valgrind --leak-check=full --log-file=memcheck.txt -v --error-limit=no --show-reachable=yes ./LinkedNode

==566== **LEAK SUMMARY**:
==566==    definitely lost: 16 bytes in 1 blocks
==566==    indirectly lost: 64 bytes in 4 blocks
==566==      possibly lost: 0 bytes in 0 blocks
==566==    still reachable: 0 bytes in 0 blocks
==566==         suppressed: 0 bytes in 0 blocks
==566==
==566== Use --track-origins=yes to see where uninitialised values come from
==566== **ERROR SUMMARY: 2 errors from 2 contexts** (suppressed: 0 from 0)
==566==
==566== 1 errors in context 1 of 2:
==566== Conditional jump or move depends on uninitialised value(s)
==566==    at 0x10971F: main (in /mnt/d/project/DataStructure/CPP/HongLab/ch06LinkedList/LinkedNode/LinkedNode)
==566==
==566== **ERROR SUMMARY: 2 errors from 2 contexts** (suppressed: 0 from 0)

 

 

사실 주로 Summary만 볼 때가 많다 그러다 문제가 있으면 위로 타고 올라가서 확인하고 있다

이 명령어의 장점은 --log-file=memcheck.txt 옵션을 통해 valgrind로 확인된 사항들이 별도 파일로 만들 수 있어서 내가 만든 출력문과 섞이지 않는 점이 좋다.

 

내가 만든 프로그램에서의 Error와 Leak 원인

 

cur변수에 시작을 부분을 담고 있지 않았던 것 문제였다.

125~130줄 까지는 제대로 초기화가 되지 않는 변수들을 이용하였기에 에러가 날 수밖에 없었다.

cur 노드에 first를 추가함으로써 동적할당한 노드들이 빠짐없이 메모리 할당 해제 되도록 하였다.

	**Node *cur = first;**
	Node *next;
	while (cur)
	{
		next = cur->next;
		delete cur;
		cur = next;
	}