리눅스 환경에서 C 언어를 개발할 때 필수 도구 중 하나는 바로 gdb(GNU Debugger)이다.
gdb는 메모리 내부를 들여다보고 프로그램 로직 오류의 원인을 파악하는 데 필요하다.
gdb의 설치 방법과 기초 사용법을 알아보자.
1. gdb란?
gdb(GNU Debugger)는 GNU 소프트웨어 시스템을 위한 기본 디버거이다.
작성한 소스 코드가 실행되는 동안 내부적으로 어떤 일이 벌어지는지 추적하거나, 프로그램이 비정상적으로 종료되었을 때의 상태를 분석하는 용도로 사용된다.
gdb의 기능
- 특정 행(Line)이나 조건에서 프로그램 실행을 중단시킨다.
- 프로그램이 멈췄을 때, 현재 변수의 값이나 메모리 상태를 확인한다.
- 코드의 흐름을 한 단계씩(Step-by-step) 따라가며 로직 오류를 찾는다.
VS Code 디버그 툴과의 차이점
VS Code와 같은 IDE에서 제공하는 디버깅 도구는 시각적으로 편리하지만, 결국 이 역시도 내부적으로 gdb나 lldb 같은 디버거를 GUI로 래핑하여 사용하는 방식이다.
- VS Code: GUI 기반으로 가독성이 좋고 사용이 쉽지만, 리소스를 많이 소모하며 세밀한 메모리 덤프나 복잡한 명령어 조합을 사용하기에는 한계가 있다.
- gdb: CLI(명령줄 인터페이스) 기반으로 동작하며 가볍다. GUI가 없는 서버 환경에서도 즉시 사용 가능하며, 기계어 수준의 레지스터 분석 및 세밀한 메모리 조작이 가능하다.
🛡️보안 분야에서의 활용
화이트해커나 보안 전문가에게 gdb는 필수 프로그램이다.
소스 코드가 없는 바이너리 파일을 분석하거나, 스택 메모리의 구조를 파악하여 보안 취약점을 찾는 리버스 엔지니어링 과정에 gdb가 활용된다.
2. gdb 설치 및 환경 구성
리눅스 환경에서 gdb를 설치하고 컴파일 환경을 구성하는 방법은 다음과 같다. (Ubuntu 기준)
패키지 설치
최신 리눅스 환경은 대부분 64비트이지만, 실습을 할 때는 32비트 환경이 메모리 구조를 이해하기에 더 직관적인 경우가 많다.
이를 위해 32비트 컴파일 관련 패키지를 먼저 설치한다.
먼저 gdb --version 명령으로 gdb가 기존에 설치되어 있는지를 확인한 후, 없다면 다음과 같이 설치한다.
# gdb 설치
$ sudo apt update
$ sudo apt install gdb
# 32bit 컴파일을 위한 멀티 라이브러리 설치
$ sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
3. gdb를 이용한 C 프로그램 디버깅 실습
예제 : 정수형 변수와 메모리 주소 확인 (datatype.c)
먼저 분석할 간단한 C 코드를 작성한다.
// datatype.c
#include <stdio.h>
int main()
{
int number1; // 변수 선언
int number2;
int number3; // 메모리에 12Byte(4Byte * 3)가 할당된다.
number1 = 1;
number2 = 2;
number3 = 3;
printf("%d %d %d\n", number1, number2, number3);
return 0;
}
컴파일 옵션
gdb로 분석하기 위해서는 컴파일 시 반드시 -g 옵션을 추가해야 한다.
이 옵션이 있어야 실행 파일 내부에 디버깅 심볼(소스 코드 정보 등)이 포함된다.
- -m32: 32비트로 컴파일
- -g: gdb 분석용 디버깅 정보 포함
- -o: 출력파일명 지정
$ gcc -m32 -g -o datatype datatype.c
gdb 실행 및 주요 명령어
실행 파일을 gdb로 연다.
-q 옵션은 시작 시 저작권 관련 메시지를 생략해줘서 더 보기 편하게 해준다.
$ gdb -q datatype2
gdb 실행 시 불필요한 네트워크 딜레이를 없애기 위해 다음 명령어로 debuginfod 기능을 꺼준다.
set debuginfod enabled off
Tip: 매번 치기 귀찮다면 ~/.gdbinit 파일에 set debuginfod enabled off를 추가해두면 실행 시 자동으로 적용된다.
💡debuginfod 기능:
원래 디버깅을 하려면 프로그램의 상세 정보(심볼)가 담긴 파일이 내 컴퓨터에 있어야 하는데, 이게 없을 때 debuginfod가 켜져 있으면 외부 서버(Ubuntu, RedHat 등)에 접속해서 필요한 파일을 알아서 다운로드 해준다.
- 장점: 내가 일일이 파일을 찾아서 설치할 필요가 없음.
- 단점: 서버가 느리면 파일을 받느라 네트워크 지연 발생.
| 명령어 | 축약형 | 설명 |
| list | l | 소스 코드를 출력한다. |
| break | b | 중단점(Break Point)을 설정한다. (예: b main) |
| run | r | 프로그램을 실행한다. |
| next | n | 다음 줄을 실행한다. (함수 내부로 들어가지 않음) |
| p | 변수의 값이나 주소를 출력한다. (예: p number1, p &number1) | |
| x | x | 특정 메모리 주소의 내용을 직접 조사한다. -> "이 주소에 가서 데이터를 읽어와라" |
| continue | c | 다음 중단점까지 계속 실행한다. |
| quit | q | gdb를 종료한다. |
(gdb) b main //main 함수에 bp 걸기 (행 번호로 줄 수도 있음)
Breakpoint 1, main () at datatype.c:11
11 number1 = 1;
(gdb) r //실행
(gdb) p number1 //변수에 들어있는 값 출력
$1 = 0
(gdb) p number2
$2 = 0
(gdb) p number3
$3 = 0
(gdb) n //모든 변수에 값이 대입된 후까지 연속으로 진행
12 number2 = 2;
(gdb) p number1
$4 = 1
(gdb) n
13 number3 = 3;
(gdb) n
15 printf("%d %d %d\n", number1, number2, number3);
(gdb) p number2
$5 = 2
(gdb) p number3
$6 = 3
(gdb) n
1 2 3
17 return 0;
메모리 주소 및 값 분석 (x 명령어)
변수가 메모리에 어떻게 저장되는지 확인하는 것이 gdb 분석의 핵심이다.
메모리 분석에는 x/xw 명령어를 주로 사용한다.
//변수의 주소 확인 (메모리 주소는 실행할 때 마다 달라짐)
(gdb) p &number1
$7 = (int *) 0xffffcf14
(gdb) p &number2
$8 = (int *) 0xffffcf18
(gdb) p &number3
$9 = (int *) 0xffffcf1c
//해당 주소에 들어있는 값을 16진수(x) 워드 단위(w, 4byte)로 출력
(gdb) x/xw &number1
0xffffcf14: 0x00000001
(gdb) x/xw &number2
0xffffcf18: 0x00000002
(gdb) x/xw &number3
0xffffcf1c: 0x00000003
(gdb) c
(gdb) q
x/xw 옵션 뜻:
- x: (eXamine) 메모리 조사 명령어
- /: 구분자
- x: 16진수(heXadecimal)로 출력
- w: 워드(Word, 4바이트) 단위로 출력
4. 16진수의 이해
컴퓨터는 데이터를 2진수로 저장하지만, 4바이트를 2진수로 표현하면 자릿수가 32비트로 너무 길어서 사람이 읽기 불편하므로, gdb를 포함한 대부분의 디버깅 도구는 이를 4자리씩 묶어서 16진수(Hexadecimal)로 표현한다.
- 1바이트(8비트)의 구조: 상위 4비트(니블, Nibble)와 하위 4비트로 쪼갠다.
- 4비트의 범위: 2^4이므로 0부터 15까지 표현 가능하다. 이는 16진수 한 자리(0~F)와 정확히 일치한다.
- 32비트(4바이트) ÷ 4비트 = 8글자: 따라서 32자리 2진수는 16진수 8자리가 된다.
🟢비트와 16진수
16진수: 0 0 0 0 0 0 0 1
| | | | | | | | | | | | | | | |
2진수: 0000 0000 0000 0000 0000 0000 0000 0001 (총 32비트)
\_________/ \_________/ \_________/ \_________/
바이트: 1 Byte 1 Byte 1 Byte 1 Byte (총 4바이트)
🟢윈도우 계산기로 검증해보기
윈도우 계산기를 이용하면 눈으로 검증해볼 수 있다.
계산기를 열어서 '프로그래머' 모드로 변경하고, 가운데 단위를 DWORD(Double Word, 32비트)로 맞춘다.
💡참고로 GDB에서는 w가 32비트를 뜻하지만 Microsoft는 과거 CPU가 한 번에 16비트를 처리하던 시절부터 계속 word를 16비트로 고정해 두고 있기 때문에 32비트가 나오자 "Double Word"라는 뜻의 "DWORD"를 만들었다고 한다. 그래서 계산기에서는 word가 dword로 표현된다.
숫자판 위에 있는 '비트 표시'(점 모양 아이콘)을 누르면, 0번부터 31번까지 총 32개의 자리가 활성화된 것을 볼 수 있다.

여기서 값을 입력해보자.
- HEX 모드에서 1을 입력해 본다. 오른쪽 끝(0번 비트)에 한 자리만 1로 바뀐다.

- HEX 모드에서 F를 입력해 본다. 오른쪽 끝 4개의 비트(0000번~0003번)가 모두 1로 바뀐다.

- HEX 모드에서 F를 8번 입력해 본다. 0번부터 31번까지 32개의 비트가 모두 1로 바뀌는 것을 확인할 수 있다.

'Journey to Security > C언어' 카테고리의 다른 글
| C 주소 전달 메커니즘: scanf 원형과 작동 원리, 메모리 구조 (0) | 2026.02.14 |
|---|---|
| GDB에서 x 명령어로 메모리 분석하기 (0) | 2026.02.14 |
| C언어 char 배열의 메모리 할당 및 문자열 초기화 메커니즘 (feat. VS Code 디버거) (0) | 2026.02.11 |
| VSCode로 C 코드 디버깅하기 (0) | 2026.02.10 |
| C언어 개발 환경 비교: Visual Studio vs VSCode (0) | 2026.02.10 |