[Linux] GDB를 이용해 C 디버깅하기 (Debugging 예제)

반응형

 

  1. Debugging이란?
  2. Linux와 C 프로그래밍에서의 디버깅 예시
  3. 코어 덤프(Core Dump) / 메모리 덤프(Memory Dump) / 시스템 덤프(System Dump)
  4. GDB를 이용한 디버깅 예시

 


 

 

1. Debugging 이란?

"버그"라고 불리는 오류를 찾아내고 수정하는 과정을 말한다. 모든 소프트웨어 개발 과정에서 필수적인 단계로, 프로그램의 정확성, 안정성 그리고 성능을 보장하기 위해 수행된다. 리눅스와 C 프로그래밍에서 디버깅의 툴의 예는 아래와 같다.

 

 

 

2. Linux와 C 프로그래밍에서의 디버깅 예시

 

1. GDB 사용: GNU 디버거(GDB)는 C, C+, Rust, Fortran 등의 언어로 작성된 프로그램을 위한 디버깅 도구이다. GDB는 프로그램 개발과 테스트 단계에서 주로 사용하며, 프로그램을 단계별로 실행하고, 변수의 값 검사와 함수 호출을 추적하여 프로그램을 중단시킬 수 있는 조건을 설정할 수 있다.

 

ex) 프로그램이 특정 함수에서 예상치 못한 결과를 반환하는 경우, GDB를 사용하여 해당 함수에 중단점(breakpoint)을 설정하고, 함수가 호출될 때 변수 값과 실행 흐름을 검사하여 문제 원인을 파악할 수 있다.

 

 

2. Valgrind 사용: 프로그램 개발과 테스트 단계에서 메모리 관련 오류를 찾아내는 데 유용한 도구이다. C 프로그램에서 메모리 누수(Memory Leaks), 사용 후 해제되지 않은 메모리 접근, 초기화되지 않은 메모리 읽기 등의 문제를 식별할 수 있다.

 

ex) 프로그램이 예상치 못하게 종료되거나 이상 행동을 보이는 경우, Valgrind를 사용하여 메모리 할당과 해제 과정을 분석하고, 문제가 되는 부분을 찾아낼 수 있다.

 

 

3. 코어 덤프(Core Dump), 메모리 덤프(Memory Dump), 시스템 덤프(System Dump)

코어 덤프(Core Dump): 코어 덤프는 프로그램이 비정상적으로 종료될 때, 비정상 종료의 원인을 찾기 위해 사용된다. 그 시점에서의 프로그램의 메모리 이미지를 파일로 저장함으로써 프로그램 오류 진단과 디버깅을 수행한다. 많은 운영 체제 프로그램들이 오류가 발생하면 자동으로 코어 덤프를 실행시키는데, 이를 "코어를 덤프 한다"라고 한다. 생성된 파일은 GDB 같은 디버거를 사용하여 분석할 수 있다.

 

메모리 덤프(Memory Dump): 특정 시점에서의 프로그램이나 시스템 메모리의 상태를 포착한다. 메모리 덤프는 코어 덤프와 유사한 개념이지만, 일반적으로 시스템 전체 또는 특정 프로세스에 초점을 맞춘 더 넓은 범위를 가진다.

시스템 덤프(System Dump): 시스템 전체의 상태, 즉 모든 실행 중인 프로세스와 커널 상태를 포착한다. system crash나 오류 발생 시 유용하며, 시스템 전체의 문제 해결에 사용된다.

 

 

GDB를 이용한 디버깅 예시

 

[gdb 설치]

sudo apt-get install update
sudo apt-get install gdb

 

[gdb 실행]

gdb test1
(gdb) help
(gdb) run
 

gdb를 끝내려면 quit 명령어를 사용한다.

 

[gdb 디버깅 테스트]

(gdb) handle SIGSEGV stop print // 세그멘테이션 오류 시그널(SIGSEGV)을 받으면 오류 메시지를 출력하도록 설정
(gdb) run // 프로그램 실행
(gdb) backtrace // 오류가 발생한 지점에 도달 시 호출 스택을 검사
(gdb) list // 오류가 발생한 소스 코드의 정확한 지점을 확인
 

 

[test1.c 코드 작성]

sudo vi test1.c

 

[test1.c 컴파일]

gcc -g -o test1 test1.c
 
[test1 실행]
./test1

[정상적인 출력 결과]

array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}

 

[실제 출력 화면]

다른 요소들은 정상적으로 정렬되었으나, '(null):0'이라는 예상하지 못한 값이 출력되었다. 이는 문자열 포인터가 null을 가리킬 때 발생한다. 즉, array[0].data가 'NULL'을 가리키고 있다는 것을 의미한다. for 루프 내 코드를 보면 배열 접근이 배열의 범위를 넘어서고 있는 걸 확인할 수 있다. sort 함수 내부에서 j < n으로 설정되어 있지만, if 문 내에서 a[j+1].key에 접근하고 있다. 이는 j가 n-1 일 때 a[n]에 접근하려고 하고, 이는 배열의 범위를 넘어서기 때문에 정의되지 않은 행동(undefined behavior)에 해당한다. 버블 정렬의 각 반복은 마지막으로 정렬된 요소 이전까지만 실행되어야 하는데, 여기서는 그러한 제한 없이 배열의 마지막 원소를 초과하여 접근하려고 하기 때문에 발생한 것이다. 그럼 GDB를 이용해 해당 프로그램을 디버깅해보자.

 

[GDB를 이용한 디버깅 예시]

음.. 책에서 보고 세그먼테이션 폴트가 발생하도록 코드를 수정했는데 GDB로 프로그램 실행 시 Segmentation Fault가 발생하지 않는다. 운영체제에서 자체적으로 메모리 보호와 관련된 뭔가가 동작해서 예외가 발생하지 않은 건지 이유를 모르겠다. 리눅스 배포판이나 커널 버전에 따라 다를 수 있다고 하니 오늘은 여기까지만 해야겠다.ㅎ

반응형