C++ LLDB 기초 완벽 가이드 | macOS·브레이크포인트
이 글의 핵심
macOS에서 printf 디버깅의 한계를 넘어서. LLDB 브레이크포인트, 워치포인트, backtrace, frame variable, step/next/continue 완전 예제, GDB와 비교, 흔한 에러, 모범 사례, 프로덕션 패턴까지 실전 코드로 다룹니다.
들어가며: macOS에서 printf 디버깅의 한계
”cout 100개를 찍어도 버그를 못 찾겠어요”
macOS에서 세그폴트가 발생하는 버그를 찾고 있었습니다. std::cout을 수십 개 추가했지만 출력이 버퍼링되어 정확한 크래시 위치를 알 수 없었습니다. LLDB(Low Level Debugger)는 macOS·iOS의 기본 디버거로, “어느 줄에서 멈췄는지, 그때 변수 값과 호출 스택이 어떤지”를 멈춘 상태에서 직접 볼 수 있게 해 줍니다. 브레이크포인트(실행을 멈출 지점)·워치포인트(변수 변경 감지)·백트레이스(호출 스택)·frame variable(변수 출력)·step/next/continue(단계별 실행)만 익혀도 printf 디버깅보다 훨씬 빠르게 버그 위치를 좁힐 수 있습니다.
요구 환경: LLDB(macOS: Xcode 명령줄 도구 xcode-select --install). Linux에서도 apt install lldb로 설치 가능. 디버그 정보가 있어야 하므로 빌드 시 -g 옵션 사용(clang++ -g, CMake에서는 Debug 구성).
이 글을 읽으면:
- LLDB의 핵심 명령어(breakpoint, watchpoint, backtrace, frame variable, step/next/continue)를 완전히 익힐 수 있습니다.
- 문제 시나리오별로 LLDB를 활용하는 방법을 알 수 있습니다.
- GDB와 LLDB의 명령어 차이를 파악할 수 있습니다.
- 흔한 에러와 해결법을 적용할 수 있습니다.
- 프로덕션 환경에서의 디버깅 패턴을 적용할 수 있습니다.
목차
- 문제 시나리오
- 디버그 빌드와 LLDB 시작
- 브레이크포인트 완전 예제
- 워치포인트 완전 예제
- 백트레이스(backtrace) 완전 예제
- frame variable과 변수 검사 완전 예제
- step/next/continue 완전 예제
- LLDB vs GDB 명령어 비교
- 통합 실습: 버그 찾기 end-to-end
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
1. 문제 시나리오
시나리오 1: 세그폴트—배열 범위 초과
상황: 10만 개 원소를 처리하는 루프에서 가끔 크래시가 납니다. cout으로 i를 찍어봤지만 출력이 버퍼링되어 정확한 i 값을 알 수 없습니다.
// buggy_array.cpp
#include <iostream>
void processData(int* arr, int size) {
for (int i = 0; i <= size; ++i) { // ❌ <= 버그 (i==size일 때 범위 초과)
arr[i] = i * 2;
}
}
int main() {
int arr[100];
processData(arr, 100); // 크래시!
std::cout << "done\n";
return 0;
}
LLDB로 해결: run → 크래시 → bt → frame variable i → i=100 발견 → i <= size가 i < size여야 함을 확인.
시나리오 2: 널 포인터 역참조
상황: 외부 라이브러리에서 받은 포인터가 가끔 null인데, 어디서 null이 들어오는지 모릅니다.
// null_ptr.cpp
struct Node { int value; Node* next; };
int sumList(Node* head) {
int sum = 0;
while (head != nullptr) {
sum += head->value; // head가 null이면 크래시
head = head->next;
}
return sum;
}
LLDB로 해결: breakpoint set -n sumList → run → frame variable head → null이면 호출자 확인.
시나리오 3: 무한 루프
상황: 프로그램이 멈춘 것처럼 보입니다. i++를 누락했을 가능성이 있습니다.
// infinite_loop.cpp
void processData() {
int i = 0;
while (i < 100) {
process(i);
// i++ 누락!
}
}
LLDB로 해결: Ctrl+C로 중단 → bt → frame variable i → i가 변하지 않음 확인.
시나리오 4: 메모리 오염—어디선가 값이 덮어씌워짐
상황: arr[5]가 0이어야 하는데 어딘가에서 999로 바뀝니다. 어디서 바뀌는지 모릅니다.
// memory_corruption.cpp
int arr[10] = {0};
someFunction(); // 이 함수 내부 어딘가에서 arr[5] 오염
LLDB로 해결: watchpoint set variable arr[5] → continue → 값이 변경되는 시점에서 멈춤.
시나리오 5: 1000번 중 500번째만 버그
상황: 루프가 1000번 돌 때 500번째에서만 크래시합니다. 매번 next로 500번 진행하는 것은 비효율적입니다.
// conditional_bug.cpp
for (int i = 0; i < 1000; ++i) {
process(i); // i=500일 때만 버그
}
LLDB로 해결: breakpoint set -f main.cpp -l 10 -c 'i == 500' → run → 500번째에서만 멈춤.
문제 시나리오 요약 다이어그램
flowchart TB
subgraph problems["문제 유형"]
P1[세그폴트]
P2[널 포인터]
P3[무한 루프]
P4[메모리 오염]
P5[조건부 버그]
end
subgraph lldb_tools["LLDB 도구"]
L1[backtrace]
L2[frame variable]
L3[watchpoint]
L4[조건부 breakpoint]
end
P1 --> L1
P1 --> L2
P2 --> L2
P3 --> L1
P3 --> L2
P4 --> L3
P5 --> L4
2. 디버그 빌드와 LLDB 시작
디버그 빌드
-g 옵션을 주면 실행 파일에 디버그 심볼(소스 줄 번호, 변수 이름)이 들어가서, LLDB에서 “어느 줄에서 멈췄는지”, “변수 이름으로 값을 보는 것”이 가능해집니다. **-O0**로 최적화를 끄면 변수가 최적화로 사라지거나 코드 순서가 바뀌는 일이 줄어듭니다.
# 디버그 정보 포함 (-g), 최적화 끄기 (-O0)
clang++ -g -O0 main.cpp -o myapp
# CMake 사용 시
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
LLDB 시작
# 프로그램 로드
lldb ./myapp
# LLDB 프롬프트에서 실행
(lldb) run
# 인자와 함께 실행
(lldb) run arg1 arg2
# 환경 변수 설정 후 실행
(lldb) settings set target.env-vars LD_LIBRARY_PATH=/path/to/libs
(lldb) run
# 종료
(lldb) quit
LLDB 워크플로우
sequenceDiagram
participant Dev as 개발자
participant LLDB as LLDB
participant App as 대상 프로그램
Dev->>LLDB: lldb ./myapp
LLDB->>App: 로드 (디버그 심볼)
Dev->>LLDB: breakpoint set -n main
Dev->>LLDB: run
LLDB->>App: 실행 시작
App->>LLDB: main() 도달 → 중단
LLDB->>Dev: 프롬프트 반환
Dev->>LLDB: next / step / frame variable
LLDB->>Dev: 결과 출력
Dev->>LLDB: continue
LLDB->>App: 다음 브레이크포인트까지 실행
3. 브레이크포인트 완전 예제
브레이크포인트란?
브레이크포인트는 프로그램 실행이 특정 위치에 도달했을 때 자동으로 멈추게 하는 지점입니다. printf를 여러 개 넣는 대신, 한 번 설정하면 해당 줄에 도달할 때마다 멈춥니다.
기본 브레이크포인트 설정
# 함수에 브레이크포인트
(lldb) breakpoint set --name main
(lldb) b main
(lldb) breakpoint set --name processData
(lldb) b processData
# 파일:라인에 브레이크포인트
(lldb) breakpoint set --file main.cpp --line 15
(lldb) b main.cpp:15
# 정규식으로 여러 함수에 브레이크포인트
(lldb) breakpoint set --name-re 'process.*'
조건부 브레이크포인트
# i가 50일 때만 멈춤
(lldb) breakpoint set --file main.cpp --line 20 --condition 'i == 50'
# ptr이 null일 때만 멈춤
(lldb) breakpoint set --name processData --condition 'ptr == nullptr'
# size가 0보다 클 때만 멈춤
(lldb) breakpoint set --file main.cpp --line 10 --condition 'size > 0'
브레이크포인트 관리
# 브레이크포인트 목록
(lldb) breakpoint list
(lldb) br list
# 출력 예시:
# 1: name = 'main', locations = 1
# resolved = 1, hit count = 0
# 2: file = 'main.cpp', line = 15, locations = 1
# resolved = 1, hit count = 0
# 브레이크포인트 삭제
(lldb) breakpoint delete 1
(lldb) br del 1
# 모든 브레이크포인트 삭제
(lldb) breakpoint delete --all
# 브레이크포인트 비활성화/활성화
(lldb) breakpoint disable 2
(lldb) breakpoint enable 2
# 위치로 삭제
(lldb) breakpoint delete --file main.cpp --line 15
브레이크포인트 동작 원리
flowchart LR
subgraph normal["일반 실행"]
N1[명령 실행] --> N2[다음 명령]
N2 --> N1
end
subgraph bp["브레이크포인트 도달 시"]
B1[명령 실행] --> B2{브레이크포인트?}
B2 -->|예| B3[실행 중단]
B3 --> B4[개발자 제어]
B2 -->|아니오| B1
end
실습: 브레이크포인트로 배열 버그 찾기
// breakpoint_demo.cpp - clang++ -g -O0 -o bp_demo breakpoint_demo.cpp
#include <iostream>
void fillArray(int* arr, int size) {
for (int i = 0; i <= size; ++i) { // 버그: <=
arr[i] = i;
}
}
int main() {
int arr[5];
fillArray(arr, 5);
std::cout << "done\n";
return 0;
}
# 1. 빌드
$ clang++ -g -O0 -o bp_demo breakpoint_demo.cpp
# 2. LLDB 실행
$ lldb ./bp_demo
# 3. fillArray 함수에 브레이크포인트
(lldb) breakpoint set --name fillArray
(lldb) b fillArray
# 4. 실행
(lldb) run
# 5. 브레이크포인트에서 멈춤. next로 한 줄씩 진행
(lldb) next
(lldb) next
# i가 5일 때 arr[5] 접근 → 다음 next에서 세그폴트
# 6. 또는 조건부 브레이크로 i==5에서만 멈춤
(lldb) run
(lldb) run
(lldb) breakpoint set --name fillArray --condition 'i == 5'
(lldb) continue
4. 워치포인트 완전 예제
워치포인트란?
워치포인트는 특정 변수나 메모리 주소의 값이 변경될 때 실행을 멈추게 하는 기능입니다. “어디서 이 변수가 바뀌는지” 모를 때 유용합니다.
기본 워치포인트
# 변수 변경 시 멈춤
(lldb) watchpoint set variable variable_name
(lldb) watchpoint set expression -- variable_name
# 포인터가 가리키는 값 변경 시 멈춤
(lldb) watchpoint set expression -- *ptr
# 배열 요소 변경 시
(lldb) watchpoint set variable arr[5]
워치포인트 종류
# watch: 쓰기 시 멈춤 (기본)
(lldb) watchpoint set variable x
# 읽기 시 멈춤
(lldb) watchpoint set variable x -w read
# 읽기 또는 쓰기 시 멈춤
(lldb) watchpoint set variable x -w read_write
실습: 메모리 오염 추적
// watchpoint_demo.cpp - clang++ -g -O0 -o wp_demo watchpoint_demo.cpp
#include <iostream>
void corruptData(int* arr) {
arr[5] = 999; // 여기서 arr[5] 변경
}
int main() {
int arr[10] = {0};
std::cout << "arr[5] before: " << arr[5] << "\n";
corruptData(arr); // arr[5]가 여기서 바뀜
std::cout << "arr[5] after: " << arr[5] << "\n";
return 0;
}
# 1. main에 브레이크포인트 후 실행
$ lldb ./wp_demo
(lldb) breakpoint set --name main
(lldb) run
# 2. arr[5]에 워치포인트 설정 (main에서 arr이 유효한 상태에서)
(lldb) watchpoint set variable arr[5]
# 3. continue로 진행
(lldb) continue
# 4. arr[5]가 변경되면 멈춤
Watchpoint 2 hit:
old value: 0
new value: 999
corruptData (arr=0x7fff...) at watchpoint_demo.cpp:6
6 arr[5] = 999;
# 5. backtrace로 호출 경로 확인
(lldb) bt
* frame #0: ... corruptData(...) at watchpoint_demo.cpp:6
frame #1: ... main at watchpoint_demo.cpp:14
워치포인트 제한
- 하드웨어 워치포인트: CPU가 지원하면 개수 제한 있음(보통 4개). 매우 빠름.
- 소프트웨어 워치포인트: 개수 제한 없지만 매 단계마다 확인하여 느림.
- 지역 변수는 스코프를 벗어나면 워치포인트가 자동 해제될 수 있음.
5. 백트레이스(backtrace) 완전 예제
백트레이스란?
백트레이스(또는 스택 트레이스)는 “현재 실행 위치에 도달하기까지 어떤 함수들이 호출되었는지” 호출 스택을 보여줍니다. 크래시 시 어디서 문제가 발생했는지, 누가 그 함수를 호출했는지 파악하는 데 필수입니다.
기본 백트레이스
# 호출 스택 보기
(lldb) backtrace
(lldb) bt
# 출력 예시:
# * frame #0: ... buggyFunction(...) at main.cpp:5
# frame #1: ... main at main.cpp:12
상세 백트레이스 (모든 프레임의 지역 변수)
(lldb) thread backtrace --extended
(lldb) bt all
# 또는 각 프레임에서 frame variable
(lldb) bt
(lldb) frame select 0
(lldb) frame variable
(lldb) frame select 1
(lldb) frame variable
프레임 이동
# 2번 프레임으로 이동
(lldb) frame select 2
(lldb) f 2
# 현재 프레임 정보
(lldb) frame info
# 위/아래 프레임으로 이동
(lldb) up
(lldb) down
실습: 세그폴트에서 백트레이스 활용
// backtrace_demo.cpp - clang++ -g -O0 -o bt_demo backtrace_demo.cpp
#include <iostream>
void level3(int* p) {
*p = 42; // p가 null이면 크래시
}
void level2(int* p) {
level3(p);
}
void level1(int* p) {
level2(p);
}
int main() {
int* p = nullptr;
level1(p); // 크래시!
return 0;
}
$ clang++ -g -O0 -o bt_demo backtrace_demo.cpp
$ lldb ./bt_demo
(lldb) run
Process ... stopped with signal SIGSEGV
* frame #0: ... level3 (p=0x0) at backtrace_demo.cpp:5
5 *p = 42;
# 백트레이스로 호출 경로 확인
(lldb) bt
* frame #0: ... level3 (p=0x0) at backtrace_demo.cpp:5
frame #1: ... level2 (p=0x0) at backtrace_demo.cpp:9
frame #2: ... level1 (p=0x0) at backtrace_demo.cpp:13
frame #3: ... main at backtrace_demo.cpp:18
# main 프레임으로 이동해 p 확인
(lldb) frame select 3
(lldb) frame variable p
(int *) p = 0x0000000000000000
# level3에서 p 확인
(lldb) frame select 0
(lldb) frame variable p
(int *) p = 0x0000000000000000
스택 프레임 구조
flowchart TB
subgraph stack["호출 스택 (아래→위)"]
F0["main() - 프레임 3"]
F1["level1() - 프레임 2"]
F2["level2() - 프레임 1"]
F3["level3() - 프레임 0 (현재)"]
end
F0 --> F1
F1 --> F2
F2 --> F3
subgraph vars["프레임 0 지역 변수"]
V1["p = 0x0"]
end
6. frame variable과 변수 검사 완전 예제
frame variable 기본 사용법
LLDB에서는 frame variable(또는 fr v)이 GDB의 print보다 더 직관적입니다. 현재 프레임의 모든 변수를 한 번에 보여주거나, 특정 변수만 지정할 수 있습니다.
# 현재 프레임의 모든 지역 변수
(lldb) frame variable
(lldb) fr v
# 특정 변수만
(lldb) frame variable x
(lldb) fr v x
# 포인터 역참조
(lldb) frame variable *ptr
(lldb) expr *ptr
expression (expr) — 표현식 평가
# C++ 표현식 평가 (print와 유사)
(lldb) expression x
(lldb) expr x
(lldb) p x
# 포인터 역참조
(lldb) expr *ptr
# 구조체/클래스 멤버
(lldb) expr person.name
(lldb) expr obj->value
# 배열
(lldb) expr arr[0]
(lldb) expr arr[0]@10 # 10개 원소 (LLDB)
다양한 출력 형식
# 16진수
(lldb) expr -f x -- ptr
# 10진수
(lldb) expr -f d -- x
# 문자
(lldb) expr -f c -- c
# 부동소수점
(lldb) expr -f f -- f
memory read (메모리 검사)
# x/[개수][형식][크기] 주소
# 형식: x(16진), d(10진), s(문자열)
# 크기: b(1바이트), h(2바이트), w(4바이트), g(8바이트)
# 16진수로 10개 워드
(lldb) memory read -c 10 -f x ptr
# 10진수로 10개
(lldb) memory read -c 10 -f d ptr
# 문자열로
(lldb) memory read -c 10 -f s ptr
실습: frame variable으로 버그 원인 확인
// print_demo.cpp - clang++ -g -O0 -o print_demo print_demo.cpp
#include <iostream>
struct Point { int x, y; };
void process(Point* p) {
if (p == nullptr) return;
p->x *= 2;
p->y *= 2;
}
int main() {
Point pt = {10, 20};
process(&pt);
std::cout << pt.x << ", " << pt.y << "\n";
return 0;
}
$ lldb ./print_demo
(lldb) breakpoint set --name process
(lldb) run
# process 내부에서 p 검사
(lldb) frame variable p
(Point *) p = 0x00007ff7bfeff2a0
(lldb) expr *p
(Point) $0 = (x = 10, y = 20)
(lldb) frame variable p->x
(int) p->x = 10
(lldb) next
(lldb) frame variable p->x
(int) p->x = 20
7. step/next/continue 완전 예제
step vs next vs continue
| 명령 | 동작 |
|---|---|
| next (n) | 다음 소스 줄로 이동. 함수 호출이 있으면 함수 안으로 들어가지 않고 한 번에 실행 |
| step (s) | 다음 소스 줄로 이동. 함수 호출이 있으면 함수 안으로 들어감 |
| continue (c) | 다음 브레이크포인트까지 계속 실행 |
| finish | 현재 함수가 반환될 때까지 실행 |
단계별 실행 흐름
flowchart TD
A[현재 위치] --> B{next vs step?}
B -->|next| C[다음 소스 줄로]
B -->|step| D{함수 호출?}
D -->|예| E[함수 내부로 진입]
D -->|아니오| C
C --> F[다음 명령 대기]
E --> F
F --> G{finish?}
G -->|예| H[현재 함수 반환까지 실행]
G -->|아니오| A
H --> F
실습: step vs next
// stepping_demo.cpp - clang++ -g -O0 -o step_demo stepping_demo.cpp
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
int x = 10;
int y = 20;
int z = add(x, y); // 이 줄에서 next vs step 차이
std::cout << z << "\n";
return 0;
}
$ lldb ./step_demo
(lldb) breakpoint set --name main
(lldb) run
# next: add() 안으로 들어가지 않음
(lldb) next
(lldb) next
(lldb) next # add(x, y) 호출 후 다음 줄(std::cout)로
(lldb) frame variable z
(int) z = 30
# step: add() 안으로 들어감
(lldb) run
(lldb) next
(lldb) next
(lldb) step # add() 함수 내부로 진입
add (a=10, b=20) at stepping_demo.cpp:4
4 return a + b;
(lldb) frame variable a
(int) a = 10
(lldb) frame variable b
(int) b = 20
(lldb) finish # add() 반환까지 실행
(lldb) frame variable z
(int) z = 30
N번 반복·continue
(lldb) next -c 5 # next 5번
(lldb) step -c 3 # step 3번
(lldb) continue # 다음 브레이크포인트까지
8. LLDB vs GDB 명령어 비교
핵심 명령어 대응표
| 기능 | GDB | LLDB |
|---|---|---|
| 실행 | run | run |
| 브레이크포인트 설정 | break main | breakpoint set -n main 또는 b main |
| 파일:라인 | break main.cpp:15 | b main.cpp:15 또는 breakpoint set -f main.cpp -l 15 |
| 조건부 브레이크 | break main.cpp:20 if i == 50 | breakpoint set -f main.cpp -l 20 -c 'i == 50' |
| 브레이크포인트 목록 | info breakpoints | breakpoint list |
| 브레이크포인트 삭제 | delete 1 | breakpoint delete 1 |
| 워치포인트 | watch variable_name | watchpoint set variable variable_name |
| 다음 줄 | next | next |
| 함수 진입 | step | step |
| 함수 끝까지 | finish | finish |
| 계속 실행 | continue | continue |
| 변수 출력 | print x | frame variable x 또는 expr x |
| 모든 지역 변수 | info locals | frame variable |
| 함수 인자 | info args | frame variable (인자 포함) |
| 스택 추적 | backtrace | backtrace 또는 bt |
| 프레임 이동 | frame 2 | frame select 2 또는 f 2 |
| 소스 보기 | list | list |
| 종료 | quit | quit |
플랫폼별 선택 가이드
flowchart TD
A[디버깅 환경] --> B{플랫폼?}
B -->|macOS| C[LLDB 권장]
B -->|Linux| D[GDB 권장]
B -->|iOS/embedded| C
C --> E[Xcode CLI: xcode-select --install]
D --> F[apt install gdb]
- macOS: LLDB가 기본. Xcode 명령줄 도구에 포함.
- Linux: GDB가 표준. LLDB도
apt install lldb로 사용 가능. - iOS/embedded: LLDB 전용.
9. 통합 실습: 버그 찾기 end-to-end
전체 예제 코드
// full_demo.cpp - clang++ -g -O0 -o full_demo full_demo.cpp
#include <iostream>
#include <vector>
void processVector(std::vector<int>& vec) {
for (size_t i = 0; i <= vec.size(); ++i) { // ❌ <= 버그
vec[i] *= 2;
}
}
int main() {
std::vector<int> vec = {1, 2, 3};
processVector(vec);
std::cout << "done\n";
return 0;
}
단계별 LLDB 디버깅
# 1. 빌드
$ clang++ -g -O0 -o full_demo full_demo.cpp
# 2. LLDB 시작
$ lldb ./full_demo
# 3. processVector에 브레이크포인트
(lldb) breakpoint set --name processVector
# 4. 실행
(lldb) run
# 5. next로 루프 진행 (또는 조건부 break)
(lldb) next
(lldb) next
# ... i가 3일 때 vec[3] 접근 → 세그폴트
# 6. 크래시 후
(lldb) bt
#0 processVector (vec=...) at full_demo.cpp:5
#1 main () at full_demo.cpp:12
(lldb) frame variable i
(size_t) i = 3
(lldb) expr vec.size()
(size_t) $0 = 3
# 7. 원인: i <= vec.size() → i가 3일 때 vec[3] 접근 (범위 초과)
# 수정: i < vec.size()
LLDB 명령어 체크리스트
- [ ] breakpoint set -n [함수] — 브레이크포인트 설정
- [ ] run — 실행
- [ ] next / step — 단계별 실행
- [ ] continue — 다음 브레이크포인트까지
- [ ] backtrace — 호출 스택
- [ ] frame variable — 변수 값
- [ ] watchpoint set variable — 변경 감지
- [ ] frame select N — 프레임 이동
- [ ] expr — 표현식 평가
10. 자주 발생하는 에러와 해결법
에러 1: “No symbol table” / “No debug symbols”
원인: -g 옵션 없이 빌드함.
해결법:
# 재빌드 시 -g 추가
clang++ -g -O0 main.cpp -o myapp
# 기존 바이너리에 심볼 있는지 확인
file myapp
# "not stripped" 또는 "with debug_info" 확인
에러 2: “Cannot access memory at address 0x0”
원인: Null 포인터 역참조.
해결법:
(lldb) bt
(lldb) frame select 0
(lldb) frame variable
(lldb) expr ptr # 0x0인지 확인
에러 3: “optimized out” (변수 값이 표시되지 않음)
원인: -O2, -O3 등 최적화로 변수가 제거되거나 레지스터에만 있음.
해결법:
# -O0으로 재빌드
clang++ -g -O0 main.cpp -o myapp
에러 4: LLDB가 “run” 후 즉시 종료
원인: 프로그램이 정상 종료되거나, 자식 프로세스에서 크래시.
해결법:
# 자식 프로세스 추적
(lldb) settings set target.process.follow-fork-mode child
# fork 전에 브레이크포인트
(lldb) breakpoint set --name fork
(lldb) run
(lldb) continue
에러 5: 워치포인트 “Cannot create watchpoint”
원인: 상수나 레지스터만 있는 값은 워치 불가.
해결법:
# 메모리에 있는 변수에만 워치 설정
# 지역 변수는 해당 스코프에서 설정
(lldb) breakpoint set --name main
(lldb) run
(lldb) watchpoint set variable arr[5]
에러 6: “variable not found” / “no variable named ‘x’”
원인: 최적화로 변수 제거, 또는 스코프 밖.
해결법:
# frame variable로 현재 프레임 변수 확인
(lldb) frame variable
# expression으로 주소 직접 접근
(lldb) expr *(int*)0x7fff...
에러 7: “Program received signal SIGSEGV” — 원인 불명
해결법:
(lldb) thread backtrace --extended
(lldb) frame select 0
(lldb) frame variable
# 크래시 직전 상태로 run 후 break로 그 함수에 멈추고 next로 한 줄씩 진행
에러 8: macOS에서 “Unable to find process” (코드 서명)
원인: macOS 보안 정책으로 인한 디버거 제한.
해결법:
# 개발자 도구 권한 부여
# 시스템 설정 → 개인 정보 보호 및 보안 → 개발자 도구 → 터미널 허용
# 또는 codesign으로 자체 서명
codesign -s - ./myapp
에러 요약 표
| 에러 | 원인 | 해결 |
|---|---|---|
| No symbol table | -g 없음 | clang++ -g -O0 |
| Cannot access 0x0 | Null 포인터 | bt, frame variable ptr |
| optimized out | -O2 이상 | -O0 재빌드 |
| run 후 즉시 종료 | fork 등 | follow-fork-mode child |
| 워치 불가 | 상수/레지스터 | 메모리 변수에만 |
| variable not found | 최적화/스코프 | frame variable |
| Unable to find process | macOS 코드 서명 | codesign 또는 개발자 도구 권한 |
11. 모범 사례
핵심 원칙
- 디버그 빌드:
clang++ -g -O0 -Wall main.cpp -o myapp - 조건부 브레이크:
breakpoint set -f main.cpp -l 10 -c 'i == 500'— 1000번 루프에서 500번째만 확인 - thread backtrace —extended: 크래시 시 모든 프레임의 지역 변수 확인
- .lldbinit:
settings set target.process.stop-on-sharedlibrary-events false등 - 로그 저장:
log enable lldb command→ 디버깅 →log disable lldb command
디버깅 체크리스트
- [ ] -g -O0로 빌드했는가?
- [ ] backtrace로 크래시 위치 확인
- [ ] frame select N으로 해당 프레임 이동
- [ ] frame variable으로 변수 확인
- [ ] 조건부 브레이크로 특정 케이스만 추적
- [ ] 워치포인트로 메모리 변경 추적
12. 프로덕션 패턴
Core Dump 분석 (Linux)
Linux에서 LLDB로 core dump를 분석할 수 있습니다.
# core dump 활성화 (Linux)
ulimit -c unlimited
echo /tmp/core.%e.%p | sudo tee /proc/sys/kernel/core_pattern
# 크래시 후
$ lldb ./myapp -c /tmp/core.myapp.12345
(lldb) bt
(lldb) thread backtrace --extended
디버그 심볼 분리
Release 빌드와 디버그 심볼을 분리해 배포합니다.
# Release 빌드 + 별도 .dSYM (macOS)
# clang++ -g -O2 ... → myapp + myapp.dSYM
# dSYM은 자동 생성됨. strip으로 바이너리에서 제거
strip ./myapp
# myapp.dSYM은 별도 보관
# 분석 시
lldb -o "target create -d ./myapp.dSYM ./myapp" -o "target modules add -c core.12345"
원격 디버깅 (lldb-server)
# 대상: lldb-server platform --listen *:1234 --server
# 개발: process connect connect://192.168.1.100:1234
프로덕션 디버깅 시 주의사항
flowchart TD
A[프로덕션 크래시] --> B{디버그 빌드 있음?}
B -->|예| C[core dump 수집]
B -->|아니오| D[재현 환경 구축]
C --> E[lldb-server 또는 로컬 분석]
D --> E
E --> F[backtrace 분석]
F --> G[원인 파악]
G --> H[수정 및 배포]
주의사항:
- 프로덕션 바이너리와 core dump의 빌드가 정확히 일치해야 함
-O2빌드에서는 변수/줄 번호가 어긋날 수 있음 → RelWithDebInfo 권장- lldb-server는 프로세스를 중단하므로 트래픽 적은 시간에 사용
LLDB 명령어 치트시트
| 기능 | 명령 |
|---|---|
| 실행 | run, run arg1 arg2, process kill |
| 브레이크포인트 | breakpoint set -n main, breakpoint list, breakpoint delete 1 |
| 워치포인트 | watchpoint set variable var |
| 실행 제어 | next(n), step(s), finish, continue(c) |
| 검사 | frame variable(fr v), expr, backtrace(bt), frame select N |
| 기타 | list(l), quit |
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ GDB 기초 완벽 가이드 | 브레이크포인트·워치포인트
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
- C++ Segmentation fault | core dump
- C++ Sanitizers | ASan·TSan으로 메모리 버그·data race 자동 탐지
이 글에서 다루는 키워드 (관련 검색어)
C++ LLDB, LLDB 기초, macOS 디버깅, 브레이크포인트, 워치포인트, 백트레이스, frame variable, step next, 디버깅, 세그폴트, core dump, GDB LLDB 비교 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 도구 | 용도 |
|---|---|
| breakpoint set | 특정 위치에서 실행 중단 |
| watchpoint set | 변수 변경 시 중단 |
| backtrace | 호출 스택 확인 |
| frame variable | 변수 값 출력 |
| step/next | 단계별 실행 |
| continue | 다음 브레이크포인트까지 |
핵심 원칙:
- printf 대신 LLDB
- 브레이크포인트 + 조건부 브레이크
- backtrace로 호출 경로 파악
- frame variable으로 변수 검증
- 워치포인트로 메모리 오염 추적
- 프로덕션은 core dump + dSYM 분리
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. macOS에서 세그폴트·크래시 원인 파악, 무한 루프 디버깅, 특정 조건에서만 발생하는 버그 추적, 메모리 오염 추적 등 printf로 찾기 어려운 버그를 LLDB 기본 명령어로 빠르게 해결할 때 사용합니다.
Q. GDB와 LLDB의 차이는?
A. LLDB는 macOS·iOS의 기본 디버거로, GDB와 명령어가 유사합니다. Linux에서는 GDB, macOS에서는 LLDB를 주로 사용합니다. 이 글의 LLDB vs GDB 비교표를 참고하세요.
Q. 프로덕션에서 LLDB를 붙여도 되나요?
A. lldb-server로 붙이면 프로세스가 중단되므로, 가능하면 core dump를 수집해 오프라인에서 분석하는 것이 좋습니다. 트래픽이 적은 시간에만 lldb-server 사용을 권장합니다.
한 줄 요약: LLDB 브레이크포인트·워치포인트·백트레이스·frame variable·step/next로 macOS에서 버그를 빠르게 찾을 수 있습니다. 다음으로 GDB 기초(#4-1) 또는 GDB/LLDB 디버거 가이드(#16-1)를 읽어보면 좋습니다.
다음 글: [C++ 실전 가이드 #16-1] GDB/LLDB 디버거 완벽 가이드
이전 글: [C++ 실전 가이드 #4-1] GDB 기초 완벽 가이드
관련 글
- C++ GDB 기초 완벽 가이드 | 브레이크포인트·워치포인트
- CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)
- C++ 디버깅 기초 완벽 가이드 | GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기
- VS Code C++ 설정 | IntelliSense·빌드·디버깅
- C++ 전처리기 완벽 가이드 | #define·#ifdef