GDB 디버깅

3 minute read

GDB 디버깅

Introduction

  • GDB(GNU Debugger)는 GNU 소프트웨어 시스템을 위한 기본 디버거이다.
  • 다양한 유닉스 기반의 시스템에서 동작하는 이식성 있는 디버거로 C, C++, Fortran 등 여러 프로그래밍 언어를 지원한다.
  • 프로그램을 줄 단위로 실행하거나 특정 지점에서 멈추도록 할 수 있다.
  • 프로그램 수행 중간에 각각의 변수에 어떤 값이 할당되어 있는지 확인할 수 있다.
  • 원하는 값을 변수에 할당 후 결과를 확인할 수 있다.
  • 컴파일 시 -g 옵션을 주어 디버깅 정보를 추가해야한다.

기능

  • 정지점(Break Point) 설정
    • 지정한 지점에서 프로그램 실행을 일시적으로 중단할 수 있다.
    • 정지점에서 프로그램을 중단한 뒤 특정 값을 표시하고 확인할 수 있다.
    • 조건을 주어 정지점을 설정할 수도 있다.
  • 하드웨어 감시점
    • 일부 프로세서에서는 하드웨어를 사용하여 특정 메모리 값이 변하는지 감시할 수 있다.
  • 프로그램 값과 속성 표시
    • 프로그램을 실행하면서 gdb 명령으로 현재 변수 값을 표시할 수 있다.
  • 프로그램 단계별 수행
    • 실행 프로그램의 각 행을 한 번에 하나씩 실행할 수 있다.
  • 스택 프레임
    • 프로그램에서 함수를 호출할 때마다 호출 관련 정보가 스택 프레임(stack frame) 또는 프레임(frame)이라는 자료 블록에 저장된다.
    • 함수 호출 하나당 프레임은 하나이다.
    • 프레임에는 함수에 전달된 매개변수, 함수의 지역변수 및 실행주소 등이 담겨있다.
    • 모든 스택 프레임은 콜 스택(call stack)이라는 메모리 영역에 할당된다.
    • frame, info frame, backtrace 등의 명령이 있다.

GDB 명령

[출처: 패스트캠퍼스]

코어 덤프(core dump)

  • 컴퓨터 프로그램이 특정 시점에 작업 중이던 메모리 상태를 기록한 것으로, 보통 프로그램이 비정상적으로 종료되었을 때 만들어진다.
  • 프로그램 카운터, 스택 포인터 등 CPU 레지스터나 메모리 관리 정보, 프로세서 및 운영 체제 플래그 및 정보 등이 포함된다.
  • 프로그램 오류 진단과 디버깅에 사용된다.
  • 사용자 응용 프로그램이 문제가 생겼을 경우 코어(core) 파일을 생성할 수 있으려면, 시스템에서 제한하는 코어 파일 크기가 0보다 커야 한다.
  • ulimit -a 명령으로 코어 파일 크기를 확인할 수 있고, ulimit -c 명령으로 코어 파일 크기를 변경할 수 있다.

  • 코어 덤프의 네이밍 형식은 다음과 같다.
    • %e - executable filename
    • %p - PID of dumped process
    • %h - hostname (same as nodename returned by unname(2))
    • %t - time of dump (seconds since 0:00h, 1, Jan 1970)

  • gdb_test1 : 실행 파일 이름
  • 1566 : 비정상적으로 종료된 프로세스의 PID
  • localhost.localdomain : hostname
  • 1693444800 : time

  • gdb [소스파일] 명령으로 오류 정보를 바로 출력하면서 확인할 수 있다.
  • run 을 이용하여 실행

  • gdb [코어 덤프 파일] 명령으로 생성된 코어 덤프 파일 내용을 위와 같이 출력할 수 있다.

backtrace

  • 코어 덤프 파일을 남길 수 없는 환경에서 segfault 발생, SIGKILL 시그널 수신 등 문제점이 발생한 상황이거나 시스템 진단 등의 목적으로 의도적으로 backtrace를 남겨 활용할 수 있다.
  • addr2line 명령어로 기계어 명령 주소(16진수 형태)를 명령이 시작된 파일의 행에 매핑할 수 있다.
#include <execinfo.h>

int backtrace(void** buffer, int size);
  • 포인터 목록으로 현재 스레드에 대한 backtrace 정보를 가져와 버퍼에 넣는다.
  • size는 버퍼 크기 이상이어야 한다.
  • 반환값은 확보된 버퍼의 실제 항목 수이다.
  • 버퍼 내의 포인터들은 실제로 스택을 검사하여 얻은 반환 주소이며, 스택 프레임 당 하나의 반환주소이다.
char **backtrace_symbols (void *const *buffer, int size);
  • backtrace_symbols 함수는 backtrace 함수에서 얻은 정보를 문자열 배열로 변환한다.
  • buffer는 backtrace 함수를 통해 얻은 주소 배열에 대한 포인터 여야하며 size는 해당 배열의 항목 수이다.
  • 반환값은 배열 버퍼와 마찬가지로 크기 항목이 있는 문자열 배열에 대한 포인터이다.
  • 각 문자열에서 함수 이름, 함수에 대한 오프셋, 실제 리턴 주소(16진)가 포함된다..
  • 함수 이름을 프로그램에서 사용할 수 있도록 링커에게 추가 플래그를 전달해야 한다. (-rdynamic)
  • backtrace_symbols의 반환값은 malloc 함수를 통해 얻은 포인터이며 해당 포인터를 해제하는 것은 호출자의 책임이다.
  • 개별 문자열이 아닌 반환 값만 해제해야 한다.
  • 문자열에 충분한 메모리를 확보할 수 없는 경우 리턴값은 NULL 이다.
void backtrace_symbols_fd (void *const *buffer, int size, int fd);
  • backtrace_symbols_fd 함수는 backtrace_symbols 함수와 동일한 변환을 수행한다.
  • 호출자에게 문자열을 반환하는 대신 문자열을 파일 디스크립터 fd에 한 줄에 하나씩 쓴다.
  • malloc 기능을 사용하지 않으므로 해당 기능이 실패할 수 있는 상황에서 사용할 수 있다.

예제

#include <execinfo.h>
#include <stdio.h>
 include <stdlib.h>
/* Obtain a backtrace and print it to stdout. */
void print_trace (void)
{
    void *array[10];
    size_t size;
    char **strings;
    size_t i;
    size = backtrace (array, 10);
    strings = backtrace_symbols (array, size);
    printf ("Obtained %d stack frames.\n", size);
    for (i = 0; i < size; i++)
    printf ("%s\n", strings[i]);
    free (strings);
}
    /* A dummy function to make the backtrace more interesting. */
void dummy_function (void)
{
    print_trace ();
}
int main (void)
{
    dummy_function ();
    return 0;
}
  • 코드 실행 모습

  • 링커에게 backtrace를 사용한다는 플래그 -rdynamic 전달
  • 5개의 스택 프레임 생성
    • 함수당 한개 이므로 main, print_trace, dummy_function 함수에 대해 총 3개
    • 기타 프로그램 실행에 필요한 정보로 2개 생성
  • addr2line 명령어로 해당 주소가 소스코드에서 어디에 위치하는지 행단위로 파악할 수 있음

Reference.

  • 패스트캠퍼스 리눅스 올인원 패키지 강좌

Categories:

Updated:

Leave a comment