icon안동민 개발노트

디버거 사용법


디버거의 기본 개념

 디버깅은 프로그램의 오류를 찾아 수정하는 과정으로, 효율적인 소프트웨어 개발에 필수적인 기술입니다.

 이 절에서는 C++ 프로그램 디버깅을 위한 다양한 도구와 기법을 자세히 살펴보고, 실습을 통해 이해를 깊이 있게 할 것입니다.

 디버거는 프로그램의 실행을 제어하고 내부 상태를 검사할 수 있게 해주는 도구입니다. 주요 기능은 다음과 같습니다.

  1. 브레이크포인트 설정
  2. 단계별 실행 (Step Over, Step Into, Step Out)
  3. 변수 값 검사
  4. 콜 스택 확인
  5. 메모리 내용 확인

GDB (GNU Debugger) 심화

 GDB는 강력한 명령줄 디버거로, 다양한 고급 기능을 제공합니다.

 조건부 브레이크포인트

 특정 조건이 만족될 때만 실행을 중단하도록 설정할 수 있습니다.

(gdb) break 10 if i == 5

 이 명령은 10번 줄에 브레이크포인트를 설정하지만, 변수 i가 5일 때만 실행을 중단합니다.

 워치포인트

 특정 변수의 값이 변경될 때 실행을 중단하도록 설정할 수 있습니다.

(gdb) watch x

 이 명령은 변수 x의 값이 변경될 때마다 실행을 중단합니다.

 역방향 디버깅

 GDB는 프로그램의 실행을 거꾸로 되돌릴 수 있는 기능을 제공합니다.

(gdb) record
(gdb) reverse-next
(gdb) reverse-step

 이 기능을 사용하면 버그의 원인을 추적하기 쉬워집니다.

 Python 스크립팅

 GDB는 Python 스크립트를 통해 확장할 수 있습니다. 예를 들어, 사용자 정의 명령을 만들 수 있습니다.

import gdb
 
class PrintStackCommand(gdb.Command):
    def __init__(self):
        super(PrintStackCommand, self).__init__("pstack", gdb.COMMAND_USER)
 
    def invoke(self, arg, from_tty):
        gdb.execute("bt")
 
PrintStackCommand()

 이 스크립트를 GDB에 로드하면 pstack 명령을 사용할 수 있게 됩니다.

LLDB 사용법

 LLDB는 LLVM 프로젝트의 일부로, macOS에서 기본 디버거로 사용됩니다.

 GDB와 유사한 기능을 제공하지만 몇 가지 차이점이 있습니다.

 LLDB 기본 명령어

  • b 또는 breakpoint set : 브레이크포인트 설정
  • r 또는 run : 프로그램 실행
  • n 또는 next : 다음 줄로 이동 (함수 호출 시 함수 내부로 들어가지 않음)
  • s 또는 step : 다음 줄로 이동 (함수 호출 시 함수 내부로 들어감)
  • p 또는 print : 변수 값 출력
  • bt 또는 thread backtrace : 콜 스택 표시
  • c 또는 continue : 다음 브레이크포인트까지 실행
  • q 또는 quit : LLDB 종료

 LLDB 고급 기능

 LLDB는 강력한 명령 구문 분석 기능을 제공합니다.

 예를 들어,

(lldb) breakpoint set --file foo.c --line 12 --condition '(int)strcmp(name, "error") == 0'

 이 명령은 foo.c 파일의 12번 줄에 조건부 브레이크포인트를 설정합니다.

Visual Studio Code에서의 디버깅

 Visual Studio Code는 다양한 언어와 플랫폼을 지원하는 강력한 에디터입니다.

 C++ 디버깅을 위해 다음과 같은 기능을 제공합니다.

 launch.json 설정

 디버깅 구성을 위해 launch.json 파일을 사용합니다.

예시
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "g++ build and debug active file",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}/${fileBasenameNoExtension}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "g++ build active file",
            "miDebuggerPath": "/usr/bin/gdb"
        }
    ]
}

 VS Code 디버깅 기능

  • 브레이크포인트 설정 : 라인 번호 왼쪽을 클릭
  • 디버깅 시작 : F5
  • Step Over : F10
  • Step Into : F11
  • Step Out : Shift+F11
  • 변수 검사 : 디버그 뷰의 변수 섹션 또는 마우스 오버

메모리 오류 디버깅 도구

 Valgrind 심화

 Valgrind의 Memcheck 도구는 다양한 메모리 오류를 탐지할 수 있습니다.

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose ./myprogram

 이 명령은 모든 종류의 메모리 누수를 체크하고, 초기화되지 않은 값의 사용을 추적합니다.

 AddressSanitizer 심화

 AddressSanitizer는 다양한 메모리 오류를 탐지할 수 있습니다.

g++ -fsanitize=address -fno-omit-frame-pointer -g myprogram.cpp -o myprogram

 이 명령으로 컴파일하면 힙 버퍼 오버플로우, 스택 버퍼 오버플로우, 전역 버퍼 오버플로우, use-after-free, use-after-return, 초기화되지 않은 메모리 읽기 등을 탐지할 수 있습니다.

고급 디버깅 기법

 코어 덤프 분석

 프로그램이 비정상 종료될 때 생성되는 코어 덤프를 분석하여 문제의 원인을 찾을 수 있습니다.

gdb ./myprogram core

 원격 디버깅

 GDB를 사용하여 원격 시스템에서 실행 중인 프로그램을 디버깅할 수 있습니다.

  1. 원격 시스템에서
gdbserver :1234 ./myprogram
  1. 로컬 시스템에서
(gdb) target remote hostname:1234

 멀티스레드 프로그램 디버깅

 GDB에서 멀티스레드 프로그램을 디버깅할 때 유용한 명령어들

  • info threads : 모든 스레드 정보 표시
  • thread apply all bt : 모든 스레드의 백트레이스 표시
  • set scheduler-locking on : 현재 스레드만 실행되도록 설정

실습 : 복잡한 버그 찾기

 다음 코드에는 여러 가지 버그가 숨어 있습니다.

 디버거를 사용하여 이 버그들을 찾아내고 수정해보세요.

#include <iostream>
#include <vector>
#include <algorithm>
#include <thread>
#include <mutex>
 
std::mutex mtx;
 
void process_data(std::vector<int>& data, int start, int end) {
    for (int i = start; i < end; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        if (i % 2 == 0) {
            data[i] *= 2;
        } else {
            data[i] += 1;
        }
    }
}
 
int main() {
    std::vector<int> data(1000);
    for (int i = 0; i < 1000; ++i) {
        data[i] = i;
    }
 
    std::thread t1(process_data, std::ref(data), 0, 500);
    std::thread t2(process_data, std::ref(data), 500, 1000);
 
    t1.join();
    t2.join();
 
    int sum = 0;
    for (int i = 0; i <= 1000; ++i) {  // 버그 1: 배열 범위 초과
        sum += data[i];
    }
 
    std::cout << "Sum: " << sum << std::endl;
 
    auto it = std::find(data.begin(), data.end(), 42);
    // 버그 2: 데이터가 변경되어 42를 찾을 수 없음
 
    if (it != data.end()) {
        std::cout << "Found 42 at index: " << std::distance(data.begin(), it) << std::endl;
    } else {
        std::cout << "42 not found" << std::endl;
    }
 
    return 0;
}

연습 문제

  1. GDB를 사용하여 세그멘테이션 폴트가 발생하는 프로그램을 디버깅하세요. 오류의 정확한 위치와 원인을 찾아내세요.
  2. 데이터 레이스 조건이 있는 멀티스레드 프로그램을 작성하고, 이를 Thread Sanitizer를 사용하여 디버깅하세요.
  3. 메모리 누수가 있는 프로그램을 작성하고, Valgrind를 사용하여 누수를 찾아내고 수정하세요.
  4. 재귀 함수에서 발생하는 스택 오버플로우를 디버거를 사용하여 분석하세요. 콜 스택을 검사하고 문제의 원인을 설명하세요.

 참고자료

  • "Debugging with GDB : The GNU Source-Level Debugger" by Richard M. Stallman, Roland Pesch, Stan Shebs
  • "LLDB Debugging Guide" - Apple Developer Documentation
  • "Advanced C and C++ Compiling" by Milan Stevanovic
  • "Windows Debugging : Practical Foundations" by Dmitry Vostokov
  • "The Art of Debugging with GDB, DDD, and Eclipse" by Norman Matloff and Peter Jay Salzman
  • "Intel 64 and IA-32 Architectures Software Developer's Manual"
  • "Valgrind 3.x Documentation"
  • "AddressSanitizer for x86/Linux : Installation and Usage"
  • "Visual Studio Code C++ Documentation"
  • "Effective Debugging : 66 Specific Ways to Debug Software and Systems" by Diomidis Spinellis