리눅스 환경에서는 GCC를 사용하여 C/C++ 프로그램을 컴파일할 수 있으며, GCC는 내부적으로 glibc 라이브러리를 활용합니다. glibc가 제공하는 backtrace() 함수를 사용하면 프로그램에 오류가 발생한 시점의 호출 스택(call stack)을 추적할 수 있습니다. 이 함수는 execinfo.h 헤더 파일에 정의되어 있습니다.
아래 예제에서는 세그먼테이션 폴트(Segmentation Fault)가 발생했을 때 스택 추적 정보를 자동으로 출력하는 방법을 살펴보겠습니다.
예제 코드
#include <iostream>
#include <execinfo.h>
#include <signal.h>
#include <cstdlib>
#include <unistd.h>
using namespace std;
void error_handler(int sig) {
void *array[10];
size_t size;
size = backtrace(array, 10); // 모든 엔트리에 대한 void 포인터를 가져옴
cout << "Error: signal " << sig << ":\n"; // 오류 시그널 번호 출력
backtrace_symbols_fd(array, size, STDERR_FILENO);
exit(1);
}
void invalid_index() {
int *ptr = (int*) - 1;
cout << *ptr << endl; // 세그먼테이션 오류 발생 지점
}
void func1() {
invalid_index();
}
void func2() {
func1();
}
int main(int argc, char **argv) {
signal(SIGSEGV, error_handler); // SIGSEGV 발생 시 호출될 핸들러 등록
func2(); // 오류를 유발하는 함수들을 연쇄적으로 호출
}
실행 결과
Error: signal 11: ./a.out(+0x825)[0x5579a31d7825] /lib/x86_64-linux-gnu/libc.so.6(+0x3ef20)[0x7f7689009f20] ./a.out(+0x880)[0x5579a31d7880] ./a.out(+0x8a1)[0x5579a31d78a1] ./a.out(+0x8ad)[0x5579a31d78ad] ./a.out(+0x8d5)[0x5579a31d78d5] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7)[0x7f7688fecb97] ./a.out(+0x71a)[0x5579a31d771a]
동작 원리
signal(SIGSEGV, error_handler) 구문은 SIGSEGV(세그먼테이션 폴트) 시그널이 발생했을 때 실행될 사용자 정의 핸들러를 등록합니다. 위 예제에서는 잘못된 메모리 주소((int*)-1)에 접근하는 순간 운영체제가 SIGSEGV 시그널을 프로세스에 전달하고, 이에 따라 등록된 error_handler() 함수가 자동으로 호출됩니다.
핸들러 내부에서는 backtrace() 함수가 현재 호출 스택의 반환 주소 목록을 최대 10개까지 가져오고, 이어서 backtrace_symbols_fd() 함수가 해당 주소들을 사람이 읽을 수 있는 심볼 형태로 변환하여 표준 에러 스트림(stderr)으로 출력합니다. 출력 결과에는 오류가 발생하기까지 거친 함수 호출 경로가 순서대로 표시되므로, 어느 함수에서 문제가 시작되었는지 추적할 수 있습니다.
디버깅 팁
기본 출력에는 함수 이름 대신 메모리 오프셋만 표시되는 경우가 많습니다. 이때 컴파일 단계에서 -g 옵션(디버깅 정보 포함)과 -rdynamic 옵션(전체 심볼 테이블 export)을 함께 사용하면 스택 추적 결과에 실제 함수 이름이 함께 출력되어 디버깅이 훨씬 쉬워집니다.
g++ -g -rdynamic main.cpp -o a.out