디버깅 & 트러블슈팅

커널 문제를 재현부터 원인 확정까지 좁혀가는 디버깅 체계를 설명합니다. 로그/트레이스 계측(`printk`, dynamic_debug, tracepoint), 런타임 관측(perf/ftrace/bpftrace), 메모리·동시성 결함 탐지(KASAN/KMSAN/KCSAN/UBSAN/lockdep), 패닉 덤프 수집(kdump/pstore), `crash` 도구 후분석, softlockup·hardlockup·RCU stall 대응 절차를 실제 트러블슈팅 순서대로 상세히 다룹니다.

문서 구조 재정렬: 이 문서는 디버깅 전략과 선택 기준 중심으로 유지합니다. 도구별 심화는 ftrace/Tracepoints, 크래시 분석, 개발 도구 문서를 우선 참고하세요.
전제 조건: 커널 개발 주의사항빌드 시스템 문서를 먼저 읽으세요. 디버깅은 증상보다 재현 조건 고정이 먼저이므로, 커널 빌드 옵션(CONFIG_DEBUG_*)과 흔한 실수 패턴을 먼저 파악하는 것이 핵심입니다.
일상 비유: 이 주제는 현장 사고 원인 조사와 비슷합니다. 증거를 순서 없이 모으면 결론이 흔들리듯이, 커널 디버깅도 관측 지점을 단계적으로 고정해야 빠르게 수렴합니다.

핵심 요약

  • 재현 우선 — 증상보다 재현 조건 고정이 먼저입니다.
  • 도구 선택 — 로그, 트레이스, 코어덤프 용도를 분리합니다.
  • 증거 보존 — 원인 추적 전 관측 데이터를 먼저 확보합니다.
  • 오버헤드 관리 — 추적 범위를 최소화해 왜곡을 줄입니다.
  • 사후 검증 — 수정 후 동일 조건에서 재발 여부를 확인합니다.

단계별 이해

  1. 재현 시나리오 정의
    입력 조건과 타이밍을 고정합니다.
  2. 관측 포인트 배치
    핵심 함수/이벤트만 선별 추적합니다.
  3. 원인 축소
    가설을 하나씩 배제하며 범위를 줄입니다.
  4. 수정 검증
    회귀 테스트와 운영 지표를 함께 확인합니다.
관련 표준: DWARF Debugging Format, GDB Remote Protocol — 디버거가 사용하는 표준 프로토콜 및 형식입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

커널 디버깅 철학

커널 디버깅은 유저 공간 프로그래밍과 근본적으로 다릅니다. 유저 공간에서는 gdb를 실행하고 브레이크포인트를 설정하고 변수 값을 확인하는 것이 자연스럽지만, 커널에서는 한 번의 잘못된 메모리 접근이 전체 시스템을 패닉 상태로 몰아넣을 수 있습니다. 따라서 커널 개발자들은 "printk is your best friend"라는 격언을 중시합니다.

커널 디버깅이 유저 공간 디버깅과 다른 핵심적인 차이점은 다음과 같습니다.

ℹ️

안전한 디버깅 원칙: 프로덕션 시스템에서 직접 디버깅하지 마십시오. 가능하면 QEMU/KVM 가상머신에서 재현하고, CONFIG_DEBUG_INFOCONFIG_DEBUG_KERNEL을 활성화한 커널을 사용하십시오. 디버그 심볼이 포함된 vmlinux 파일을 항상 보관하십시오.

커널 디버깅의 일반적인 접근 순서는 다음과 같습니다. 먼저 printk로 문제 범위를 좁히고, ftrace나 perf로 성능 병목이나 실행 흐름을 추적하고, 필요시 KGDB로 라이브 디버깅을 수행합니다. 크래시가 발생하면 kdump로 코어 덤프를 수집하고 crash 유틸리티로 사후 분석합니다. 새니타이저(KASAN, KCSAN 등)는 개발 단계에서 항상 활성화하여 잠재적 버그를 조기에 발견하는 것이 좋습니다.

디버깅 도구 선택 가이드

문제 유형과 환경에 따라 적절한 디버깅 도구를 선택하는 것이 효율적인 문제 해결의 핵심입니다. 아래 의사결정 트리는 증상별로 최적의 도구를 찾도록 안내합니다.

커널 디버깅 도구 선택 의사결정 트리 문제 증상은? 시스템 크래시/패닉? YES kdump + crash 코어 덤프 수집 사후 메모리 분석 NO 성능 저하/병목? YES perf + ftrace CPU 프로파일링 함수 호출 추적 타이밍 분석 NO 메모리 오염/UAF? YES KASAN/KMSAN 메모리 오염 탐지 UAF/경계 초과 초기화 누락 NO 경합 조건/데드락? YES lockdep + KCSAN 락 순서 검증 데이터 경합 탐지 동시성 버그 NO printk + KGDB 로직 추적 변수 상태 확인 단계별 디버깅 개발 환경: 모든 새니타이저 활성화 권장 | 프로덕션: printk/ftrace/kdump 우선
그림: 증상별 커널 디버깅 도구 선택 의사결정 트리

다중 도구 워크플로

복잡한 문제는 여러 도구를 조합하여 접근하는 것이 효과적입니다. 아래 다이어그램은 일반적인 디버깅 워크플로에서 도구들이 어떻게 연계되는지 보여줍니다.

커널 디버깅 다중 도구 워크플로 1단계: 초기 증상 확인 도구: dmesg, /proc 시스템 로그 분석 리소스 상태 확인 문제 범위 식별 2단계: 범위 축소 도구: printk, ftrace 핵심 함수 추적 실행 경로 확인 원인 후보 발견 3단계: 정밀 분석 도구: KGDB, perf 변수 상태 검사 타이밍 분석 병렬 실행: 자동 탐지 도구 KASAN/KMSAN (메모리) | KCSAN (경합) | lockdep (락) | UBSAN (정의되지 않은 동작) 개발 환경에서 상시 활성화 권장 크래시 발생 시 kdump 수집 /var/crash/*.vmcore crash 분석 bt, log, ps, dis 명령 성능 문제 시 perf record CPU/메모리 프로파일링 perf report + ftrace 병목 지점 특정 로직 버그 시 printk 코드 삽입 변수 값 추적 KGDB 단계 실행 브레이크포인트 설정
그림: 커널 디버깅 다중 도구 워크플로 - 단계별 도구 조합 전략
ℹ️

도구 선택 우선순위:

  1. 개발 환경: 모든 새니타이저(KASAN/KCSAN/lockdep/UBSAN) 활성화 → 문제 조기 탐지
  2. 재현 가능한 버그: printk → ftrace → KGDB 순서로 범위 축소
  3. 간헐적 크래시: kdump 설정 → 덤프 수집 대기 → crash 분석
  4. 성능 저하: perf top 실시간 확인 → perf record/report 상세 분석 → ftrace 함수 타이밍
  5. 프로덕션 환경: 오버헤드 최소화 — printk/ftrace만 사용, 새니타이저 비활성화

디버깅 출력 해석 가이드

디버깅 도구의 출력을 정확히 해석하는 능력은 문제 해결 속도를 크게 향상시킵니다. 아래 표는 주요 도구별로 출력에서 주목해야 할 핵심 정보를 정리합니다.

도구 핵심 출력 항목 해석 포인트 예시
dmesg 타임스탬프, 로그레벨, 호출 스택 • 시간 순서로 이벤트 재구성
• WARNING/BUG 키워드 검색
• RIP(명령어 포인터) 주소 확인
[12345.678] BUG: at mm/slab.c:1234
→ slab.c 1234줄에서 발생
ftrace 함수 호출 시퀀스, 타이밍 • 예상 외 함수 호출 경로
• 함수 실행 시간(duration)
• 인터럽트/태스크 전환 지점
func() { 1234.567 us }
→ 1.2ms 소요 (병목 의심)
perf 샘플링 비율, 호출 그래프 • 상위 5% 함수에 집중
• Children vs Self 컬럼 비교
• 캐시 미스율(LLC-load-misses)
45.23% schedule()
→ CPU 시간의 45% 소모
KASAN 오염 유형, 접근 크기, 스택 트레이스 • UAF vs heap-buffer-overflow 구분
• Allocated/Freed by 위치
• Shadow byte 값 분석
use-after-free in kfree()
→ 해제 후 접근, kfree 호출 위치 확인
lockdep 락 순서, 체인, IRQ 상태 • ABBA 데드락 패턴
• hardirq-safe vs hardirq-unsafe
• 락 획득 순서(Chain)
lock A → lock B
lock B → lock A
→ 순환 의존성 (데드락 위험)
crash 백트레이스(bt), 레지스터, 메모리 • RIP/PC 값으로 크래시 지점 특정
• RAX/RDI 레지스터 (함수 인자)
• struct 멤버 값 확인(p 명령)
RIP: 0xffffffff81234567
→ dis 0xffffffff81234567로 디스어셈블
KCSAN 경합 주소, 접근 유형, 스택 • read-write vs write-write
• 두 CPU의 동시 접근 경로
• 보호되지 않은 공유 변수
data-race write in foo()
data-race read in bar()
→ 동시 읽기/쓰기 충돌
UBSAN 정의되지 않은 동작 유형 • 정수 오버플로(overflow)
• NULL 포인터 산술
• 시프트 범위 초과
signed integer overflow
→ INT_MAX + 1 발생
💡

출력 해석 체크리스트:

  1. 타임스탬프: 이벤트 순서와 간격 확인 (타이밍 관련 버그)
  2. 프로세스/CPU 정보: 멀티코어 경합 여부 파악
  3. 호출 스택: 상위 5개 함수로 컨텍스트 파악
  4. 메모리 주소: 커널/유저 공간 구분 (0xffff... = 커널)
  5. 오류 코드: errno 값 확인 (EINVAL=-22, ENOMEM=-12 등)
  6. 레지스터 값: NULL 포인터(0x0), 오염된 값(0xdeadbeef) 체크

자주 보는 패턴과 의미

패턴 의미 조치
BUG: unable to handle kernel NULL pointer NULL 포인터 역참조 RIP 주소로 코드 위치 확인 → 포인터 NULL 검사 누락
WARNING: CPU: 2 PID: 1234 at ... WARN_ON() 트리거 조건식 확인 → 예외 처리 추가 또는 가정 수정
INFO: rcu_sched detected stalls RCU 스톨 (20초 이상 선점 불가) 긴 루프/락 점유 찾기 → cond_resched() 추가
soft lockup - CPU#3 stuck for 22s! 소프트락업 (인터럽트 응답 지연) 스택 트레이스로 무한 루프/락 대기 지점 특정
Out of memory: Killed process 5678 OOM Killer 발동 /proc/sys/vm/oom_* 확인 → 메모리 누수 추적
KASAN: use-after-free in ... 해제된 메모리 접근 Allocated/Freed by 위치 → 수명 관리 수정
possible circular locking dependency lockdep 데드락 경고 Chain 출력 → 락 획득 순서 통일
perf: interrupt took too long 인터럽트 핸들러 과부하 IRQ별 /proc/interrupts 확인 → 워크큐 이동 검토
⚠️

오해하기 쉬운 출력:

  • Call Trace: — 스택 트레이스는 아래에서 위로 읽습니다 (최하단 = 가장 최근 호출)
  • RIP: 0010:[<ffffffff81234567>] — 0010은 세그먼트, 대괄호 안이 실제 주소
  • RSP: 0018:ffff888012345678 — 스택 포인터, 값이 아닌 위치
  • Code: 48 89 c7 e8 ... <c3> 90 ... — <> 안이 크래시 명령어 (여기선 ret)
  • perf의 Children — 하위 함수 포함 시간, Self — 순수 실행 시간
  • ftrace의 + — 인터럽트 비활성화, d — 선점 비활성화 플래그

printk 심화

printk()는 커널에서 가장 기본적이면서도 가장 강력한 디버깅 도구입니다. 유저 공간의 printf()와 유사하지만, 어떤 컨텍스트에서든 호출 가능하고(인터럽트, NMI 포함), 로그 레벨 시스템을 내장하고 있으며, 링 버퍼에 메시지를 저장합니다.

로그 레벨

printk는 8단계의 로그 레벨을 지원하며, 숫자가 낮을수록 심각도가 높습니다.

레벨매크로용도
EMERGKERN_EMERG0시스템 사용 불가
ALERTKERN_ALERT1즉각 조치 필요
CRITKERN_CRIT2치명적 조건
ERRKERN_ERR3오류 조건
WARNINGKERN_WARNING4경고 조건
NOTICEKERN_NOTICE5정상이지만 주목할 사항
INFOKERN_INFO6정보성 메시지
DEBUGKERN_DEBUG7디버그 메시지

pr_* 매크로

현대 커널 코드에서는 직접 printk()를 호출하는 대신 래퍼 매크로를 사용합니다. pr_fmt()을 정의하면 모든 메시지에 공통 접두사를 붙일 수 있습니다.

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/printk.h>

static int __init mymod_init(void)
{
    pr_info("모듈 로드됨, 버전 %s\\n", VERSION);
    pr_warn("실험적 기능 활성화됨\\n");
    pr_err("디바이스 초기화 실패: %d\\n", err);
    pr_debug("디버그: 레지스터 값 = 0x%08x\\n", val);
    return 0;
}

dev_* 디바이스 매크로

디바이스 드라이버에서는 dev_info(), dev_err() 등을 사용합니다. 이 매크로들은 struct device *를 인자로 받아 자동으로 디바이스 이름을 로그 메시지에 포함시킵니다.

#include <linux/device.h>

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;

    dev_info(dev, "프로브 시작, IRQ=%d\\n", irq);
    dev_err(dev, "리소스 할당 실패\\n");
    dev_dbg(dev, "레지스터 오프셋: 0x%lx\\n", offset);
    dev_warn_once(dev, "이 경고는 한 번만 출력됩니다\\n");

    return 0;
}

Dynamic Debug

Dynamic Debug는 pr_debug()dev_dbg()의 출력을 런타임에 활성화/비활성화할 수 있는 기능입니다. CONFIG_DYNAMIC_DEBUG 옵션이 필요합니다.

1. 제어 파일 확인 debugfs 마운트 control 존재 확인 2. 범위 선택 file / module / func 필요 범위만 최소화 3. 플래그 적용 +p 출력 활성화 +f/+l 메타 정보 4. 확인/정리 grep '=p'로 점검 -p로 비활성화 Dynamic Debug 실전 절차
# 1) 제어 파일 확인
mount -t debugfs debugfs /sys/kernel/debug  # 미마운트 시
test -f /sys/kernel/debug/dynamic_debug/control

# 2) 필요한 범위만 활성화 (과도한 로그 방지)
echo 'file drivers/net/ethernet/intel/e1000e/*.c +p' > /sys/kernel/debug/dynamic_debug/control
echo 'func my_probe +pfl' > /sys/kernel/debug/dynamic_debug/control
echo 'module e1000e +p' > /sys/kernel/debug/dynamic_debug/control

# 3) 현재 활성화 상태 확인
grep '=p' /sys/kernel/debug/dynamic_debug/control

# 4) 디버깅 종료 후 정리
echo 'module e1000e -p' > /sys/kernel/debug/dynamic_debug/control

# 5) 부팅 시 활성화 (커널 커맨드라인)
dyndbg="file drivers/usb/* +p"

printk 성능 고려사항

⚠️

핵심 위험: 고빈도 경로에서 printk를 남발하면 CPU 사용량 자체보다 콘솔 출력 지연이 병목이 됩니다. 특히 시리얼 콘솔 환경에서는 초당 수백 줄만 출력돼도 스케줄링 지터와 타임아웃을 유발할 수 있습니다.

고빈도 경로에서 printk 다량 호출 링 버퍼는 빠르게 쌓이지만 콘솔 출력은 느림 지연 누적 지터/타임아웃 대응 제한/전환 printk 성능 병목 경로
/* trace_printk: ftrace 링 버퍼에 기록 (콘솔 출력 없음, 매우 빠름) */
trace_printk("hot path: val=%d\\n", val);

/* printk_ratelimited: 일정 시간 내 중복 메시지 억제 */
printk_ratelimited(KERN_WARNING "잦은 인터럽트 감지\\n");

/* pr_info_once: 최초 한 번만 출력 */
pr_info_once("드라이버 초기화 경로 진입\\n");

/* dump_stack: 현재 콜 스택 출력 */
dump_stack();

printk 내부 아키텍처

printk의 내부 동작을 이해하면 커널 로깅 문제를 효과적으로 진단할 수 있습니다. printk는 크게 링 버퍼(Ring Buffer)콘솔 드라이버(Console Driver)의 두 단계로 동작합니다.

printk() 호출 vprintk_store() 메시지 포맷팅 타임스탬프 부착 seq 번호 할당 log_buf (Ring Buffer) 기본 크기: 2^CONFIG_LOG_BUF_SHIFT struct printk_ringbuffer lockless read/write /dev/kmsg dmesg syslog() console_unlock() 등록된 콘솔 드라이버에 출력 시리얼 콘솔 VGA/fbcon netconsole

링 버퍼의 핵심 구조체와 동작 방식을 살펴보겠습니다.

/* kernel/printk/printk_ringbuffer.h */
struct printk_ringbuffer {
    struct prb_desc_ring  desc_ring;    /* 디스크립터 링 */
    struct prb_data_ring  text_data_ring; /* 텍스트 데이터 링 */
    atomic_long_t        fail;         /* 실패 카운터 */
};

/* 각 로그 레코드의 메타데이터 */
struct printk_info {
    u64  seq;           /* 시퀀스 번호 (단조 증가) */
    u64  ts_nsec;       /* 타임스탬프 (나노초) */
    u16  text_len;      /* 텍스트 길이 */
    u8   facility;      /* syslog facility */
    u8   flags;         /* LOG_NEWLINE, LOG_CONT 등 */
    u8   level;         /* 로그 레벨 (0-7) */
    u32  caller_id;     /* 호출자 식별 (thread/CPU) */
    struct dev_printk_info dev_info; /* 디바이스 정보 */
};

링 버퍼는 lock-free 알고리즘을 사용하여 NMI 컨텍스트를 포함한 모든 실행 컨텍스트에서 안전하게 동작합니다. 커널 6.x에서는 기존의 logbuf_lock을 완전히 제거하고 prb(printk_ringbuffer) 기반의 lockless 구조로 전환되었습니다.

# 링 버퍼 크기 확인 (바이트 단위)
dmesg | head -1   # 첫 메시지의 타임스탬프 확인

# 커널 빌드 시 링 버퍼 크기 설정
CONFIG_LOG_BUF_SHIFT=17    # 2^17 = 128KB (기본값)
CONFIG_LOG_CPU_MAX_BUF_SHIFT=12  # CPU당 추가 버퍼

# 부팅 시 크기 변경 (커널 커맨드라인)
log_buf_len=4M    # 4MB로 확장 (대량 디버그 메시지 수집 시)

# 현재 링 버퍼 크기 확인
wc -c /dev/kmsg    # 주의: blocking read이므로 Ctrl+C 필요
dmesg --buffer-size  # util-linux 최신 버전

콘솔 로그 레벨 제어

커널은 4개의 로그 레벨 파라미터를 통해 콘솔 출력을 제어합니다. /proc/sys/kernel/printk에서 확인하고 변경할 수 있습니다.

# 현재 설정 확인 (4개의 값)
cat /proc/sys/kernel/printk
# 출력 예: 4    4    1    7
#         │    │    │    └─ default_console_loglevel (부팅 시 기본값)
#         │    │    └────── minimum_console_loglevel (최소 콘솔 레벨)
#         │    └─────────── default_message_loglevel (레벨 미지정 시 기본값)
#         └──────────────── console_loglevel (현재 콘솔 레벨)

# console_loglevel: 이 값보다 낮은(=심각한) 레벨만 콘솔에 출력
# 값 8: 모든 메시지 출력 (DEBUG 포함)
# 값 1: EMERG만 출력

# 런타임에 콘솔 레벨 변경
echo 8 > /proc/sys/kernel/printk       # 모든 메시지 출력
echo 1 > /proc/sys/kernel/printk       # EMERG만 출력

# sysctl로도 변경 가능
sysctl -w kernel.printk="7 4 1 7"

# dmesg 명령으로도 조정 가능
dmesg -n 8    # console_loglevel을 8로 설정
dmesg -n 1    # console_loglevel을 1로 설정

커널 부팅 파라미터로도 로그 레벨을 제어할 수 있습니다.

커널 파라미터설명예시
loglevel=Nconsole_loglevel 설정 (0-7)loglevel=7
quietconsole_loglevel을 4(WARNING)로 설정quiet
debugconsole_loglevel을 10으로 설정 (모든 메시지)debug
ignore_loglevel콘솔 레벨 무시, 모든 메시지 출력ignore_loglevel
log_buf_len=N링 버퍼 크기 지정log_buf_len=4M
printk.devkmsg=on|off|ratelimit/dev/kmsg 유저 공간 쓰기 제어printk.devkmsg=on
printk.time=1타임스탬프 출력 강제printk.time=1

커널 전용 포맷 지정자

Linux 커널은 printf()의 표준 포맷 지정자 외에 커널 데이터 구조를 위한 전용 포맷 확장(%p 계열)을 제공합니다. 이들은 lib/vsprintf.c에 구현되어 있으며, 올바른 포맷을 사용하면 로그 가독성이 크게 향상됩니다.

포맷설명출력 예시
%p해시된 포인터 (KASLR 보안)0000000012345678
%px실제 포인터 값 (디버깅 전용)ffff888012345678
%pKkptr_restrict 정책 적용 포인터권한에 따라 해시/실제값
%pS심볼 이름 + 오프셋my_func+0x1c/0x40 [mymod]
%ps심볼 이름만 (오프셋 없음)my_func
%pSR심볼 이름 + 오프셋 (릴로케이션 보정)my_func+0x1c/0x40
%pB백트레이스 심볼 (tail-call 보정)my_func+0x1c/0x40
%pI4IPv4 주소192.168.1.1
%pI6IPv6 주소2001:0db8::0001
%pI6cIPv6 주소 (축약형)2001:db8::1
%pISpcIP 주소 + 포트 (sockaddr)192.168.1.1:8080
%pMMAC 주소 (콜론 구분)00:11:22:33:44:55
%pMRMAC 주소 (역순)55:44:33:22:11:00
%paphys_addr_t (물리 주소)0x00000001ffffffff
%prstruct resource 범위[mem 0x10000-0x1ffff]
%pRstruct resource (플래그 포함)[mem 0x10000-0x1ffff flags 0x200]
%pOFDevice Tree 노드 전체 경로/soc/serial@12340000
%pOFfpDT 노드 이름 + phandleserial@12340000
%pUUUID (소문자)01234567-89ab-cdef-...
%pUBUUID (대문자)01234567-89AB-CDEF-...
%pdstruct dentry 이름filename.txt
%pDstruct file 경로/path/to/file
%pVva_format 구조체(가변)
%*phhex dump (공백 구분)00 01 02 03
%*phChex dump (콜론 구분)00:01:02:03
%*phNhex dump (구분자 없음)00010203
⚠️

보안 주의: 커널 4.15부터 일반 %p는 해시된 값을 출력합니다(KASLR 보호). 실제 포인터 값이 필요하면 %px를 사용하되, 보안 민감한 코드에서는 %pK를 사용하여 /proc/sys/kernel/kptr_restrict 정책을 따르십시오. %px는 디버깅 전용이며 프로덕션 코드에 남겨두지 마십시오.

/* 커널 전용 포맷 지정자 실전 예제 */
#include <linux/printk.h>
#include <linux/inet.h>

/* 함수 심볼: 백트레이스, 콜스택 분석 */
pr_info("caller: %pS\\n", __builtin_return_address(0));
/* 출력: caller: do_init_module+0x4c/0x220 */

/* 네트워크 주소: 바이트 오더 변환 자동 처리 */
__be32 addr = htonl(0xC0A80101);  /* 192.168.1.1 */
pr_info("IP: %pI4\\n", &addr);
/* 출력: IP: 192.168.1.1 */

/* MAC 주소 */
u8 mac[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
pr_info("MAC: %pM\\n", mac);
/* 출력: MAC: 00:11:22:33:44:55 */

/* 물리 주소 */
phys_addr_t phys = 0x80000000ULL;
pr_info("phys: %pa\\n", &phys);
/* 출력: phys: 0x0000000080000000 */

/* 리소스 범위 */
struct resource *res = pdev->resource;
pr_info("resource: %pR\\n", res);
/* 출력: resource: [mem 0x10000000-0x1000ffff flags 0x200] */

/* Device Tree 노드 */
struct device_node *np = pdev->dev.of_node;
pr_info("DT node: %pOF\\n", np);
/* 출력: DT node: /soc/serial@12340000 */

/* 바이너리 hex dump (최대 64바이트) */
u8 buf[] = {0xDE, 0xAD, 0xBE, 0xEF};
pr_info("data: %*ph\\n", (int)sizeof(buf), buf);
/* 출력: data: DE AD BE EF */

/* dentry/file 이름 */
pr_info("file: %pD\\n", filp);
/* 출력: file: /proc/self/maps */

pr_cont()와 연속 메시지

여러 printk 호출로 한 줄의 로그 메시지를 구성해야 할 때 pr_cont()를 사용합니다. 일반적인 예로 루프에서 배열 원소를 출력하거나, 조건에 따라 메시지를 점진적으로 구성하는 경우가 있습니다.

/* pr_cont() 사용법: 줄바꿈 없이 이전 메시지에 이어서 출력 */
int regs[8];
int i;

pr_info("register dump:");
for (i = 0; i < 8; i++)
    pr_cont(" R%d=0x%08x", i, regs[i]);
pr_cont("\\n");
/* 출력: register dump: R0=0x00000001 R1=0x00000002 ... R7=0x00000008 */

/* 조건부 연속 메시지 */
pr_info("features:");
if (has_feature_a) pr_cont(" FEATURE_A");
if (has_feature_b) pr_cont(" FEATURE_B");
if (has_feature_c) pr_cont(" FEATURE_C");
pr_cont("\\n");
💡

pr_cont 주의사항: SMP 환경에서 여러 CPU가 동시에 pr_cont()를 사용하면 메시지가 뒤섞일 수 있습니다. 로그 무결성이 중요한 경우 snprintf()로 버퍼에 먼저 구성한 후 한 번에 출력하는 것이 안전합니다. 또한 pr_cont()KERN_CONT 레벨을 사용하므로 별도의 로그 레벨이 적용되지 않습니다.

/dev/kmsg와 structured logging

/dev/kmsg는 커널 링 버퍼에 대한 유저 공간 인터페이스입니다. 기존의 /proc/kmsg(syslog 인터페이스)와 달리, /dev/kmsg는 구조화된 메시지를 제공하고, 다중 reader를 지원하며, 유저 공간에서 커널 로그에 메시지를 삽입할 수도 있습니다.

# /dev/kmsg 직접 읽기 (구조화된 형식)
cat /dev/kmsg
# 출력 형식: facility,seq,timestamp,-;message
# 예: 6,1234,56789012,-;eth0: link up 1000Mbps Full Duplex
#     │  │      │       │
#     │  │      │       └─ 플래그 (- = 없음, c = CONT)
#     │  │      └───────── 타임스탬프 (마이크로초)
#     │  └──────────────── 시퀀스 번호
#     └─────────────────── priority (facility * 8 + level)

# 유저 공간에서 커널 로그에 메시지 삽입
echo "<6>myapp: 사용자 정의 메시지" > /dev/kmsg
echo "<3>myapp: 오류 메시지" > /dev/kmsg

# systemd-journald도 /dev/kmsg를 통해 커널 메시지를 수집
# journalctl은 journal과 kmsg를 통합하여 표시

# /dev/kmsg 유저 공간 쓰기 제어
cat /proc/sys/kernel/printk_devkmsg
# on: 무제한 허용 (기본)
# off: 차단
# ratelimit: 속도 제한 적용
ℹ️

/proc/kmsg vs /dev/kmsg: /proc/kmsg는 레거시 인터페이스로 읽으면 메시지가 소비(consume)됩니다. 즉 한 reader가 읽으면 다른 reader는 볼 수 없습니다. 반면 /dev/kmsg는 각 reader가 독립적인 읽기 위치를 가지므로 여러 프로세스가 동시에 같은 메시지를 읽을 수 있습니다. 현대 시스템에서는 /dev/kmsg 사용을 권장합니다.

earlyprintk와 earlycon

커널 부팅 초기에는 아직 정규 콘솔 드라이버가 초기화되기 전이므로, 일반 printk 메시지가 콘솔에 출력되지 않습니다. 이 시점의 메시지를 보려면 earlyprintk 또는 earlycon을 사용합니다.

기능earlyprintkearlycon
도입 시기커널 2.6커널 3.x+
구현아키텍처 의존적통합 프레임워크
Device Tree미지원지원 (stdout-path)
권장레거시현재 표준
CONFIGCONFIG_EARLY_PRINTKCONFIG_SERIAL_EARLYCON
# earlycon 설정 (커널 커맨드라인)

# UART 시리얼 (x86, 명시적 I/O 포트)
earlycon=uart8250,io,0x3f8,115200n8

# UART 시리얼 (ARM, MMIO 주소)
earlycon=pl011,mmio32,0x9000000

# Device Tree의 stdout-path에서 자동 감지
earlycon

# earlyprintk (레거시, x86)
earlyprintk=serial,ttyS0,115200
earlyprintk=vga     # VGA 텍스트 콘솔에 직접 출력

# 필수 CONFIG 옵션
CONFIG_SERIAL_EARLYCON=y
CONFIG_SERIAL_8250=y           # x86 UART
CONFIG_SERIAL_8250_CONSOLE=y
/* earlycon 드라이버 등록 예제 (커널 소스) */
/* drivers/tty/serial/8250/8250_early.c */

static void early_serial8250_write(
    struct console *con,
    const char *s, unsigned n)
{
    struct earlycon_device *dev = con->data;

    uart_console_write(&dev->port, s, n,
                       serial_putc);
}

static int __init early_serial8250_setup(
    struct earlycon_device *device,
    const char *options)
{
    /* 시리얼 포트 초기화 */
    device->con->write = early_serial8250_write;
    return 0;
}

OF_EARLYCON_DECLARE(uart8250, "ns16550a",
                    early_serial8250_setup);

netconsole

netconsole은 네트워크를 통해 커널 로그 메시지를 원격 시스템으로 전송하는 기능입니다. 시리얼 콘솔이 없는 서버 환경이나 대규모 클러스터에서 유용합니다. UDP 기반이므로 오버헤드가 낮고 커널 네트워킹 스택의 최소 부분만 사용합니다.

# ============ 송신 측 (디버깅 대상 시스템) ============

# 방법 1: 커널 커맨드라인 (부팅 시 설정)
# netconsole=[src-port]@[src-ip]/[dev],[tgt-port]@[tgt-ip]/[tgt-macaddr]
netconsole=6665@192.168.1.10/eth0,6666@192.168.1.20/00:11:22:33:44:55

# 방법 2: 모듈로 런타임 로드
modprobe netconsole \
  netconsole=6665@192.168.1.10/eth0,6666@192.168.1.20/00:11:22:33:44:55

# 방법 3: configfs를 통한 동적 설정 (유연한 방식)
modprobe netconsole
mkdir /sys/kernel/config/netconsole/target1
cd /sys/kernel/config/netconsole/target1
echo 6665 > local_port
echo 192.168.1.10 > local_ip
echo eth0 > dev_name
echo 6666 > remote_port
echo 192.168.1.20 > remote_ip
echo 00:11:22:33:44:55 > remote_mac
echo 1 > enabled   # 활성화

# ============ 수신 측 (로그 수집 시스템) ============

# netcat으로 간단히 수신
nc -u -l -p 6666

# socat으로 파일에 저장
socat UDP-LISTEN:6666,fork - | tee /var/log/netconsole.log

# rsyslog로 수신 (프로덕션 환경)
# /etc/rsyslog.conf:
# module(load="imudp")
# input(type="imudp" port="6666")
💡

netconsole 확장 기능: 커널 6.4+에서는 CONFIG_NETCONSOLE_DYNAMIC이 기본 활성화되어 configfs를 통한 동적 설정이 가능합니다. 또한 release prepend 옵션(echo 1 > release)을 사용하면 메시지 앞에 커널 릴리즈 문자열이 추가되어 여러 시스템의 로그를 구분할 수 있습니다.

printk와 NMI 안전성

NMI(Non-Maskable Interrupt) 컨텍스트에서의 printk는 특별한 처리가 필요합니다. NMI는 일반 인터럽트도 비활성화할 수 없으므로, NMI 핸들러 내에서 락을 획득하면 데드락이 발생할 수 있습니다.

/* NMI-safe printk 동작 방식 (kernel/printk/printk_safe.c) */

/*
 * NMI 컨텍스트에서 printk 호출 시:
 * 1. printk_nmi_enter()가 per-CPU 플래그 설정
 * 2. vprintk()가 NMI 모드 감지
 * 3. 메시지를 per-CPU NMI 버퍼에 임시 저장
 * 4. NMI 종료 후 printk_nmi_flush()가 메인 링 버퍼로 전송
 *
 * 커널 6.x에서는 lockless 링 버퍼 덕분에
 * NMI에서도 직접 메인 링 버퍼에 기록 가능
 */

/* NMI 핸들러에서의 printk 사용 예 */
static int nmi_handler(unsigned int cmd,
                       struct pt_regs *regs)
{
    /* NMI 컨텍스트에서도 printk 호출 가능 */
    pr_emerg("NMI watchdog: CPU#%d stuck!\\n",
             smp_processor_id());

    /* 콜 스택 출력도 NMI-safe */
    show_regs(regs);

    /* 성능 민감 경로에서는 trace_printk 사용 */
    trace_printk("NMI perf event on CPU %d\\n",
                 smp_processor_id());

    return NMI_HANDLED;
}

dmesg와 journalctl 활용

유저 공간에서 커널 로그를 확인하고 분석하는 방법입니다.

# ============ dmesg 기본 사용법 ============

# 전체 커널 로그 출력
dmesg

# 사람이 읽기 쉬운 타임스탬프
dmesg -T      # 또는 dmesg --ctime

# 상대 타임스탬프 (부팅 후 초)
dmesg -e      # reltime (최근 메시지 강조)

# 컬러 출력
dmesg --color=always

# 로그 레벨 필터링
dmesg -l err           # ERR만 출력
dmesg -l warn,err      # WARNING + ERR
dmesg -l emerg,alert,crit,err  # 심각한 메시지만

# facility 필터링
dmesg -f kern          # 커널 메시지만
dmesg -f daemon        # 데몬 메시지만

# 실시간 모니터링 (tail -f와 유사)
dmesg -w               # --follow
dmesg -wT              # 타임스탬프 + 실시간

# 특정 문자열 검색
dmesg | grep -i "error\|fail\|panic"
dmesg -T | grep "usb"

# 링 버퍼 클리어
dmesg -C               # sudo 필요

# 읽고 클리어 (로그 수집 스크립트용)
dmesg -c > /var/log/dmesg_snapshot.log

# ============ journalctl (systemd 환경) ============

# 커널 메시지만 표시
journalctl -k              # 또는 journalctl --dmesg

# 현재 부팅의 커널 메시지만
journalctl -k -b 0

# 이전 부팅의 커널 메시지 (크래시 분석 시 유용)
journalctl -k -b -1        # 직전 부팅
journalctl -k -b -2        # 2번 전 부팅

# 우선순위 필터링
journalctl -k -p err       # ERR 이상만
journalctl -k -p warning   # WARNING 이상만

# 시간 범위 필터링
journalctl -k --since "2024-01-01 10:00:00" --until "2024-01-01 11:00:00"
journalctl -k --since "1 hour ago"

# JSON 출력 (스크립트 연동)
journalctl -k -o json-pretty | head -50

# 실시간 모니터링
journalctl -kf             # -k --follow

# 특정 디바이스/드라이버 관련 메시지
journalctl -k --grep="e1000e\|ixgbe"
💡

크래시 분석 팁: 시스템 크래시 후 이전 부팅 로그를 확인하려면 journalctl -k -b -1을 사용하십시오. 이를 위해 /var/log/journal/ 디렉토리가 존재해야 합니다(mkdir -p /var/log/journal). 기본적으로 systemd-journald는 영속 저장소가 없으면 이전 부팅 로그를 보존하지 않습니다. /etc/systemd/journald.conf에서 Storage=persistent로 설정하십시오.

syslog 파이프라인 심화

현대 Linux 배포판에서 로그 경로는 단순하지 않습니다. 커널 로그는 printk 링 버퍼에 기록된 뒤 /dev/kmsg를 통해 systemd-journald가 수집하고, 필요하면 rsyslog 또는 syslog-ng가 다시 받아 파일/원격 서버로 전달합니다. 즉 커널 - journald - rsyslog - 저장소/전송 순서로 병목과 유실 지점을 분리해서 봐야 합니다.

커널 printk 링 버퍼 /dev/kmsg 커널 메시지 인터페이스 systemd-journald 1차 수집/메타데이터 부여 rsyslog 필터/큐/전송 /var/log/journal 영속 binary journal /run/systemd/journal/syslog ForwardToSyslog=yes 최종 저장/전송 /var/log/messages, /var/log/kern.log 원격 syslog 서버 (TCP/RELP/TLS) 분기 지점
⚠️

중요: ForwardToSyslog=no이면 journald에만 기록되고 rsyslog 규칙은 동작하지 않습니다. 반대로 rsyslog에서 imjournalimuxsock를 동시에 잘못 구성하면 중복 수집이 발생할 수 있습니다.

facility/severity와 필터 전략

syslog 우선순위는 facility.severity 조합으로 표현됩니다. 커널 문제 분석에서는 kern.*와 심각도 상위(warning 이상)를 분리 저장하는 구성이 실전에서 가장 자주 사용됩니다.

Severity 숫자 의미 journalctl 예시
emerg0시스템 사용 불가journalctl -p 0
alert1즉시 조치 필요journalctl -p 1
crit2치명적 오류journalctl -p 2
err3오류journalctl -p 3
warning4경고journalctl -p 4
notice5중요 이벤트journalctl -p 5
info6일반 정보journalctl -p 6
debug7디버그 상세journalctl -p 7
# 커널 facility만 추출 (rsyslog selector 문법)
kern.*                         /var/log/kern.log

# 커널 warning 이상만 별도 파일
kern.warning;kern.err;kern.crit;kern.alert;kern.emerg  /var/log/kern.warn

# userspace daemon 중 오류 이상만
daemon.err                     /var/log/daemon.err

# journald 기준으로도 동일한 필터 가능
journalctl -k -p warning..emerg
journalctl -t sshd -p err..emerg

journald + rsyslog 실전 구성

systemd 환경에서는 journald가 1차 수집기입니다. 운영 환경에서는 journald를 영속 저장으로 두고, rsyslog는 필터링/원격 전송에 집중시키는 구성이 일반적입니다.

# /etc/systemd/journald.conf (핵심 항목 예시)
[Journal]
Storage=persistent
SystemMaxUse=2G
RuntimeMaxUse=512M
RateLimitIntervalSec=30s
RateLimitBurst=20000
ForwardToSyslog=yes

# 적용
systemctl restart systemd-journald

# /etc/rsyslog.d/10-kernel.conf
module(load="imuxsock")     # journald가 전달한 syslog 소켓 수신
module(load="imjournal" StateFile="imjournal.state")  # 중복 구성 주의

template(name="PreciseFmt" type="string"
         string="%timegenerated:::date-rfc3339% %HOSTNAME% %syslogtag%%msg%\\n")

# 커널 로그 분리 저장
if ($syslogfacility-text == "kern") then {
  action(type="omfile" file="/var/log/kern.log" template="PreciseFmt")
  stop
}

# 기본 시스템 로그
*.*;auth,authpriv.none          /var/log/messages

# 설정 검사 및 재시작
rsyslogd -N1
systemctl restart rsyslog
# 원격 전달 (TCP + TLS 기본형)
# /etc/rsyslog.d/30-forward.conf
global(
  DefaultNetstreamDriver="gtls"
  DefaultNetstreamDriverCAFile="/etc/ssl/certs/ca-certificates.crt"
)

action(
  type="omfwd"
  target="10.10.10.20"
  port="6514"
  protocol="tcp"
  StreamDriver="gtls"
  StreamDriverMode="1"
  StreamDriverAuthMode="x509/name"
  StreamDriverPermittedPeers="log-collector.example.com"
  action.resumeRetryCount="-1"
  queue.type="LinkedList"
  queue.filename="fwdq"
  queue.maxdiskspace="2g"
  queue.saveonshutdown="on"
)

고부하 환경 로그 유실 대응

로그 유실은 보통 세 지점에서 발생합니다. 커널 printk 레이트 리밋, journald 레이트 리밋, rsyslog 큐 포화입니다. 병목 위치를 먼저 확인한 뒤 해당 계층만 조정해야 부작용을 줄일 수 있습니다.

# 1) 커널 printk 레이트 리밋 확인
cat /proc/sys/kernel/printk_ratelimit
cat /proc/sys/kernel/printk_ratelimit_burst

# 임시 완화 (런타임)
sysctl -w kernel.printk_ratelimit=5
sysctl -w kernel.printk_ratelimit_burst=200

# 2) journald 드롭 여부 확인
journalctl -u systemd-journald --since "10 min ago" | grep -i "rate limit\|dropped"

# 3) rsyslog 큐 상태/에러 확인
journalctl -u rsyslog --since "10 min ago"
grep -i "action.*suspended\|queue" /var/log/syslog /var/log/messages

# 4) 폭주 소스 식별
journalctl --since "5 min ago" -o short-unix | awk '{print $5}' | sort | uniq -c | sort -nr | head
💡

운영 권장: 커널 개발/검증 환경에서는 debug 레벨을 넓게 열고, 프로덕션에서는 기본 저장을 info 이상으로 제한한 뒤 문제 구간에서만 일시적으로 확장하는 방식이 안정적입니다. 항상 원격 전송 큐의 디스크 버퍼를 활성화해 네트워크 장애 시 로그를 보존하십시오.

printk 디버깅 실전 패턴

커널 개발에서 자주 사용되는 printk 기반 디버깅 패턴을 정리합니다.

/* 패턴 1: 진입/퇴장 추적 (함수 흐름 파악) */
#define pr_fmt(fmt) KBUILD_MODNAME ": %s: " fmt, __func__

static int my_probe(struct platform_device *pdev)
{
    pr_debug("enter\\n");
    /* ... */
    pr_debug("exit, ret=%d\\n", ret);
    return ret;
}

/* 패턴 2: 조건부 디버그 메시지 (모듈 파라미터 연동) */
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug verbosity (0=off, 1=basic, 2=verbose)");

#define DBG(level, fmt, ...) \
    do { if (debug_level >= (level)) \
        pr_info(fmt, ##__VA_ARGS__); } while (0)

/* 사용: echo 2 > /sys/module/mymod/parameters/debug_level */
DBG(1, "기본 디버그: irq=%d\\n", irq);
DBG(2, "상세 디버그: reg[0x%x]=0x%x\\n", reg, val);

/* 패턴 3: hex dump (바이너리 데이터 디버깅) */
#include <linux/printk.h>

/* print_hex_dump: 대량 바이너리 데이터 출력 */
print_hex_dump(KERN_DEBUG, "RX: ", DUMP_PREFIX_OFFSET,
               16, 1, buf, len, true);
/* 출력 예:
 * RX: 00000000: 45 00 00 3c 1c 46 40 00 40 06 b1 e6 ac 10 0a 63  E..<.F@.@......c
 * RX: 00000010: ac 10 0a 0c 00 14 00 50 18 28 56 1f 00 00 00 00  .......P.(V.....
 */

/* print_hex_dump_bytes: 간단 버전 (KERN_DEBUG, DUMP_PREFIX_OFFSET) */
print_hex_dump_bytes("data: ", DUMP_PREFIX_OFFSET, buf, len);

/* 동적 디버그와 연동 */
print_hex_dump_debug("PKT: ", DUMP_PREFIX_OFFSET,
                     16, 1, skb->data, skb->len, true);

/* 패턴 4: WARN/BUG 매크로 (assertion 유사) */
/* WARN_ON: 조건이 참이면 경고 + 스택 트레이스 출력 */
WARN_ON(irqs_disabled());
WARN_ON_ONCE(ptr == NULL);

/* WARN: 커스텀 메시지 포함 */
WARN(count > MAX_COUNT,
     "count (%d) exceeds max (%d)\\n", count, MAX_COUNT);

/* BUG_ON: 조건이 참이면 커널 패닉 (프로덕션 사용 주의) */
BUG_ON(list_empty(&head));  /* 가급적 사용 자제, WARN_ON 권장 */

/* 패턴 5: 상태 변경 추적 */
static const char * const state_names[] = {
    [STATE_IDLE]    = "IDLE",
    [STATE_RUNNING] = "RUNNING",
    [STATE_STOPPED] = "STOPPED",
};

static void set_state(struct my_dev *dev,
                      enum my_state new_state)
{
    pr_debug("state: %s -> %s\\n",
             state_names[dev->state],
             state_names[new_state]);
    dev->state = new_state;
}
⚠️

printk 디버깅 제거 체크리스트: 디버깅 완료 후 제출하기 전에 반드시 임시 printk를 제거하십시오. git diff로 추가된 pr_info/pr_debug를 검토하고, checkpatch.pl에서 "Unnecessary pr_debug" 경고를 확인하십시오. 영구적으로 유용한 로그는 pr_debug()로 남겨두면 Dynamic Debug를 통해 필요 시 활성화할 수 있습니다.

KGDB/KDB

KGDB는 커널을 위한 원격 GDB 디버거입니다. 시리얼 포트나 네트워크를 통해 호스트 머신의 GDB에서 타겟 커널을 직접 디버깅할 수 있습니다. KDB는 커널 내장 텍스트 모드 디버거로, 시리얼 콘솔에서 직접 명령을 입력할 수 있습니다.

커널 CONFIG 옵션

# KGDB 필수 옵션
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y              # KDB 프론트엔드
CONFIG_DEBUG_INFO=y             # 디버그 심볼 포함
CONFIG_DEBUG_INFO_DWARF5=y      # DWARF5 포맷 (권장)
CONFIG_FRAME_POINTER=y          # 정확한 백트레이스
CONFIG_MAGIC_SYSRQ=y            # SysRq 키로 KGDB 진입

# 최적화 관련 (디버깅 시 권장)
CONFIG_GDB_SCRIPTS=y            # GDB 파이썬 도우미 스크립트
# CONFIG_RANDOMIZE_BASE is not set  # KASLR 비활성화 (디버깅 편의)

시리얼 콘솔 설정

# 타겟 커널 부팅 파라미터
console=ttyS0,115200 kgdboc=ttyS0,115200 kgdbwait

# kgdbwait: 부팅 초기에 GDB 연결을 기다림
# 런타임에 KGDB 활성화
echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc

GDB 연결 방법

# 호스트에서 GDB 실행
gdb vmlinux

# 시리얼 포트로 연결
(gdb) target remote /dev/ttyS0

# QEMU 사용 시 (TCP 연결)
# QEMU 실행: qemu-system-x86_64 -s -S -kernel bzImage ...
(gdb) target remote :1234

# 커널 GDB 도우미 스크립트 로드
(gdb) add-auto-load-safe-path /path/to/linux/scripts/gdb/
(gdb) source /path/to/linux/scripts/gdb/vmlinux-gdb.py

# 유용한 커널 GDB 명령
(gdb) lx-dmesg                    # dmesg 출력
(gdb) lx-lsmod                    # 로드된 모듈 목록
(gdb) lx-ps                       # 프로세스 목록
(gdb) lx-cmdline                  # 커널 커맨드라인
(gdb) p (*(struct task_struct *)0xffff...)  # 구조체 확인

브레이크포인트와 스텝 실행

# 함수에 브레이크포인트 설정
(gdb) break do_sys_open
(gdb) break kernel/fork.c:2150

# 하드웨어 워치포인트 (변수 변경 감시)
(gdb) watch *(int *)0xffffffff81a00000

# 조건부 브레이크포인트
(gdb) break schedule if current->pid == 1234

# 실행 제어
(gdb) continue                     # 계속 실행
(gdb) next                         # 다음 줄 (함수 호출 건너뜀)
(gdb) step                         # 다음 줄 (함수 안으로 진입)
(gdb) finish                       # 현재 함수 리턴까지 실행
(gdb) bt                           # 백트레이스

KDB 명령어

KDB는 시리얼 콘솔에서 직접 사용하는 텍스트 기반 디버거입니다. SysRq+g를 눌러 진입합니다.

# KDB 진입 (시리얼 콘솔에서)
# SysRq+g 또는:
echo g > /proc/sysrq-trigger

# KDB 주요 명령어
[0]kdb> help                       # 사용 가능한 명령 목록
[0]kdb> bt                         # 현재 CPU 백트레이스
[0]kdb> btc                        # 모든 CPU 백트레이스
[0]kdb> btp <pid>                  # 특정 프로세스 백트레이스
[0]kdb> ps                         # 프로세스 목록
[0]kdb> lsmod                      # 모듈 목록
[0]kdb> md <addr>                  # 메모리 덤프
[0]kdb> mds <addr>                 # 메모리 덤프 (심볼 해석)
[0]kdb> go                         # 실행 재개
[0]kdb> kgdb                       # KGDB 모드로 전환
💡

QEMU + KGDB: QEMU의 -s 옵션(gdbserver on :1234)과 -S 옵션(시작 시 정지)을 사용하면 별도 하드웨어 없이 편리하게 커널을 디버깅할 수 있습니다. 커널의 scripts/gdb/vmlinux-gdb.py 스크립트는 커널 자료구조를 읽기 쉽게 표시해줍니다.

perf

perf는 Linux 커널의 성능 분석 도구입니다. 하드웨어 성능 카운터(PMU), 소프트웨어 이벤트, 트레이스 포인트를 활용하여 CPU 사이클, 캐시 미스, 분기 예측 실패, 함수별 실행 시간 등을 측정합니다.

하드웨어 카운터 활용

# 사용 가능한 하드웨어 이벤트 확인
perf list hw
# cpu-cycles, instructions, cache-references, cache-misses,
# branch-instructions, branch-misses, bus-cycles ...

# 소프트웨어 이벤트 확인
perf list sw
# cpu-clock, task-clock, page-faults, context-switches,
# cpu-migrations, minor-faults, major-faults ...

perf stat

프로그램 실행 중 하드웨어/소프트웨어 이벤트의 카운트를 수집합니다.

# 기본 통계
perf stat ./my_program
#  Performance counter stats for './my_program':
#       1,234.56 msec  task-clock
#          2,345       context-switches
#    123,456,789       cycles
#     89,012,345       instructions        # 0.72 insn per cycle
#      1,234,567       cache-misses         # 5.12% of cache refs

# 특정 이벤트만 측정
perf stat -e cache-misses,cache-references,L1-dcache-load-misses \
    ./my_program

# 커널 전체 모니터링 (5초간)
perf stat -a -e cycles,instructions,cache-misses sleep 5

# 반복 측정 (통계적 신뢰도)
perf stat -r 10 ./my_program

perf record / perf report

# 프로파일 기록 (-g: 콜 그래프 수집)
perf record -g ./my_program

# 커널 전체 프로파일 (10초간)
perf record -a -g -- sleep 10

# 특정 이벤트로 기록
perf record -e cache-misses -g -p $(pidof nginx) -- sleep 30

# 결과 보기 (인터랙티브 TUI)
perf report

# 텍스트 출력
perf report --stdio --sort comm,dso,symbol

# 콜 그래프 포함 출력
perf report -g graph,0.5,caller --stdio

Flame Graph 생성

Flame Graph는 perf 프로파일 데이터를 시각적으로 표현하여 성능 병목 지점을 직관적으로 파악할 수 있게 합니다.

# 1. perf 데이터 수집
perf record -F 99 -a -g -- sleep 30

# 2. 스택 트레이스 추출
perf script > out.perf

# 3. FlameGraph 도구로 변환
git clone https://github.com/brendangregg/FlameGraph.git
./FlameGraph/stackcollapse-perf.pl out.perf > out.folded
./FlameGraph/flamegraph.pl out.folded > flamegraph.svg

# 커널만 포함된 flame graph
perf script | ./FlameGraph/stackcollapse-perf.pl --kernel > kernel.folded
./FlameGraph/flamegraph.pl kernel.folded > kernel_flamegraph.svg

perf probe (동적 트레이스 포인트)

# 커널 함수에 동적 프로브 추가
perf probe --add 'do_sys_open filename:string'

# 리턴 프로브 추가
perf probe --add 'do_sys_open%return ret=$retval'

# 프로브를 이용한 기록
perf record -e probe:do_sys_open -a -- sleep 5
perf report

# 특정 소스 라인에 프로브 추가
perf probe -s /path/to/kernel/source --add 'tcp_sendmsg:15 sk size'

# 등록된 프로브 목록
perf probe --list

# 프로브 삭제
perf probe --del 'do_sys_open'
💡

perf + 커널 디버그 정보: perf probe의 소스 라인 기반 프로브를 사용하려면 CONFIG_DEBUG_INFO가 활성화된 커널이 필요합니다. 배포판 커널의 경우 linux-image-*-dbg(Debian) 또는 kernel-debuginfo(RHEL) 패키지를 설치하십시오.

💡

perf annotate, perf c2c, perf diff, perf bench, 콜 그래프 수집 방법 비교(fp/DWARF/LBR), Intel PEBS/AMD IBS 등 고급 기능은 성능 최적화 — perf 종합 가이드를 참고하세요.

커널 새니타이저

커널 새니타이저는 컴파일 시 계측(instrumentation) 코드를 삽입하여 런타임에 다양한 종류의 메모리 버그, 데이터 레이스, 정의되지 않은 동작을 탐지합니다. 개발 및 테스트 단계에서 우선적으로 활성화하는 것이 권장되는 도구들입니다.

KASAN (Kernel Address Sanitizer)

KASAN은 out-of-bounds 접근, use-after-free, double-free 등 메모리 접근 오류를 컴파일 시 계측 코드를 삽입하여 런타임에 탐지합니다. Google에서 개발하여 커널 4.0부터 통합되었으며, 커널 메모리 버그 탐지에서 가장 널리 사용되는 새니타이저입니다.

동작 원리: 섀도 메모리

KASAN Generic 모드는 섀도 메모리(shadow memory) 기법을 사용합니다. 실제 메모리 8바이트마다 1바이트의 섀도 메모리를 매핑하여, 해당 메모리 영역의 접근 가능 여부를 기록합니다. 컴파일러가 모든 메모리 접근(load/store) 앞에 섀도 메모리를 확인하는 코드를 삽입합니다.

/* 섀도 메모리 인코딩 (1 shadow byte = 8 real bytes) */
/*  0x00: 8바이트 모두 접근 가능 (유효 영역) */
/*  0x01~0x07: 처음 N바이트만 접근 가능 (부분 유효) */
/*  0xFC: KASAN 내부 redzone */
/*  0xFB: 해제된 slab 객체 (freed object) */
/*  0xFE: 해제된 slab 영역 (freed region) */
/*  0xF1: 스택 좌측 redzone */
/*  0xF2: 스택 중간 redzone */
/*  0xF3: 스택 우측 redzone */
/*  0xF8: 전역 변수 redzone */

/* KASAN이 삽입하는 검증 의사 코드 */
shadow_val = *(shadow_addr(addr));
if (shadow_val != 0) {
    if (shadow_val < 0 || (addr & 7) + size > shadow_val)
        kasan_report(addr, size, is_write);
}

세 가지 모드 비교

KASAN은 세 가지 모드를 지원하며, 각각 성능과 탐지 범위가 다릅니다.

모드CONFIG 옵션아키텍처원리오버헤드탐지 범위
GenericKASAN_GENERIC모든 아키텍처섀도 메모리 + 컴파일러 계측메모리 ~2배, CPU ~2배slab/page/stack/global OOB, UAF
SW_TAGSKASAN_SW_TAGSARM64 전용포인터 상위 비트에 태그 저장메모리 ~1.5배, CPU ~1.5배slab/page OOB, UAF (확률적)
HW_TAGSKASAN_HW_TAGSARM64 MTE하드웨어 MTE(Memory Tagging Extension)거의 없음slab/page OOB, UAF (확률적)
💡

모드 선택 가이드: x86_64에서는 Generic 모드만 사용 가능합니다. ARM64 개발 환경에서는 SW_TAGS가 Generic보다 빠르면서 대부분의 버그를 탐지합니다. 프로덕션에 가까운 테스트에서는 HW_TAGS(MTE 지원 하드웨어 필요)가 최소 오버헤드로 적합합니다.

커널 설정

# Generic 모드 (x86_64, ARM64 등 모든 아키텍처)
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y
CONFIG_KASAN_INLINE=y          # 인라인 계측 (빠르지만 바이너리 크기 증가)
# CONFIG_KASAN_OUTLINE=y       # 아웃라인 계측 (바이너리 작지만 느림)
CONFIG_KASAN_STACK=y           # 스택 변수 OOB 탐지 (추가 오버헤드)

# SW_TAGS 모드 (ARM64 전용)
# CONFIG_KASAN=y
# CONFIG_KASAN_SW_TAGS=y

# HW_TAGS 모드 (ARM64 MTE 하드웨어 필요)
# CONFIG_KASAN=y
# CONFIG_KASAN_HW_TAGS=y

# 공통 유용한 옵션
CONFIG_KASAN_VMALLOC=y         # vmalloc 영역도 검사
CONFIG_STACKTRACE=y            # 콜스택 추적 (리포트 품질 향상)

부트 파라미터

# kasan.fault: 오류 발생 시 동작 제어
kasan.fault=report             # 리포트만 출력 (기본값, Generic/SW_TAGS)
kasan.fault=panic              # 리포트 출력 후 커널 패닉

# kasan.mode: HW_TAGS 모드에서 동작 수준 제어
kasan.mode=sync                # 동기 모드 — 즉시 탐지 (기본값)
kasan.mode=async               # 비동기 모드 — 성능 우선, 지연 탐지
kasan.mode=asymm               # 읽기=비동기, 쓰기=동기 (절충)

# kasan.stacktrace: 할당/해제 콜스택 저장 여부
kasan.stacktrace=on            # 콜스택 저장 (기본값)
kasan.stacktrace=off           # 메모리 절약을 위해 비활성화

탐지 가능한 버그 유형

/* 1. Slab out-of-bounds (OOB) */
char *buf = kmalloc(64, GFP_KERNEL);
buf[64] = 'x';    /* BUG: slab-out-of-bounds — 64바이트 할당에 인덱스 64 접근 */

/* 2. Use-after-free (UAF) */
char *p = kmalloc(32, GFP_KERNEL);
kfree(p);
*p = 'a';           /* BUG: slab-use-after-free */

/* 3. Double-free */
char *q = kmalloc(16, GFP_KERNEL);
kfree(q);
kfree(q);           /* BUG: double-free or invalid-free */

/* 4. Stack out-of-bounds (CONFIG_KASAN_STACK=y 필요) */
void stack_oob(void) {
    char arr[16];
    arr[16] = 'x';   /* BUG: stack-out-of-bounds */
}

/* 5. Global out-of-bounds */
static char global_buf[8];
void global_oob(void) {
    global_buf[8] = 'x';  /* BUG: global-out-of-bounds */
}

/* 6. kmalloc 크기 불일치 (krealloc 없이 다른 크기 해제 등) */
void *p2 = kmalloc(128, GFP_KERNEL);
kfree(p2 + 64);    /* BUG: invalid-free (객체 시작이 아닌 주소 해제) */

KASAN 리포트 읽기

KASAN이 메모리 오류를 감지하면 다음과 같은 상세 리포트를 출력합니다. 각 섹션의 의미를 이해하는 것이 디버깅에 핵심적입니다.

# KASAN 리포트 예시 (use-after-free)
==================================================================
BUG: KASAN: slab-use-after-free in my_function+0x4c/0x80 [my_module]
#     ↑ 버그 유형              ↑ 발생 위치 (함수+오프셋/크기)
Read of size 4 at addr ffff8881234abcd0 by task test_prog/1234
# ↑ 접근 종류/크기  ↑ 접근 주소                ↑ 프로세스/PID

CPU: 2 PID: 1234 Comm: test_prog Not tainted 6.1.0-debug #1
Call Trace:
 dump_stack_lvl+0x34/0x48
 print_report+0x171/0x4b6
 kasan_report+0xc8/0x100
 my_function+0x4c/0x80 [my_module]       # ← 실제 버그 발생 지점
 ...

# 이 객체가 언제 할당되었는지 — alloc 콜스택
Allocated by task 1234:
 kasan_save_stack+0x1e/0x40
 kmalloc+0x9a/0x130
 my_alloc_function+0x28/0x60 [my_module]
 ...

# 이 객체가 언제 해제되었는지 — free 콜스택
Freed by task 1234:
 kasan_save_stack+0x1e/0x40
 kfree+0x78/0x120
 my_free_function+0x1c/0x40 [my_module]
 ...

# 메모리 레이아웃 — 화살표(>)가 문제 주소를 가리킴
The buggy address belongs to the object at ffff8881234abc80
 which belongs to the cache kmalloc-128 of size 128
The buggy address is located 80 bytes inside of
 freed 128-byte region [ffff8881234abc80, ffff8881234abd00)

# 섀도 바이트 덤프 — fb=freed, 00=valid, fc=redzone
Memory state around the buggy address:
 ffff8881234abc00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 ffff8881234abc80: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
                         ^
 ffff8881234abd00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
==================================================================

KASAN 테스트 및 어노테이션

/* kasan_test 모듈로 KASAN 동작 확인 */
/* CONFIG_KASAN_KUNIT_TEST=m 설정 후: */
/* # modprobe kasan_test */
/* KUnit 프레임워크로 자동 테스트 실행 */

/* --- KASAN 어노테이션 --- */

/* 특정 함수에서 KASAN 비활성화 (거짓 양성 회피) */
__no_sanitize_address
void special_memory_access(void *addr)
{
    /* 의도적으로 유효하지 않은 메모리에 접근하는 코드 */
}

/* 커스텀 메모리 할당자에서 KASAN에 유효 영역 알리기 */
#include <linux/kasan.h>

void my_pool_alloc(void *pool, size_t size)
{
    void *ptr = pool_get_object(pool);
    kasan_unpoison_range(ptr, size);     /* 이 영역을 접근 가능으로 표시 */
    return ptr;
}

void my_pool_free(void *pool, void *ptr, size_t size)
{
    kasan_poison_range(ptr, size, KASAN_SLAB_FREE);  /* 접근 불가로 표시 */
    pool_put_object(pool, ptr);
}

KMSAN (Kernel Memory Sanitizer)

KMSAN은 초기화되지 않은 메모리 사용을 탐지합니다. 할당된 메모리가 값이 쓰여지기 전에 읽히는 것을 감지합니다. 커널 6.1부터 통합되었으며, Clang 컴파일러가 필수입니다(GCC 미지원).

# 커널 설정 (KASAN과 동시 사용 불가)
CONFIG_KMSAN=y
# KMSAN은 x86_64 아키텍처에서만 지원
# 반드시 Clang으로 빌드해야 합니다:
# make CC=clang -j$(nproc)
/* KMSAN이 탐지하는 패턴 예시 */
struct my_data {
    int a;
    int b;
    int c;
};

void buggy_function(void)
{
    struct my_data *p = kmalloc(sizeof(*p), GFP_KERNEL);
    p->a = 1;
    p->b = 2;
    /* p->c는 초기화되지 않음! */

    if (p->c > 0)  /* KMSAN 경고: 초기화되지 않은 값 사용 */
        do_something();
}

/* 유저 공간으로 초기화되지 않은 데이터 유출 — 보안 취약점! */
struct info {
    u32 type;
    u32 flags;
    /* 4바이트 패딩 — 초기화되지 않음! */
    u64 value;
};

long ioctl_handler(unsigned int cmd, unsigned long arg)
{
    struct info i;
    i.type = 1;
    i.flags = 0;
    i.value = 42;
    /* 패딩 바이트가 초기화되지 않은 채 유저 공간으로 복사! */
    copy_to_user((void __user *)arg, &i, sizeof(i));
    /* 해결: memset(&i, 0, sizeof(i)); 로 초기화 */
}

KCSAN (Kernel Concurrency Sanitizer)

KCSAN은 데이터 레이스(data race)를 탐지합니다. C11 메모리 모델에 따르면, 적절한 동기화 없이 여러 스레드에서 동일 메모리 위치에 접근(하나 이상이 쓰기)하면 정의되지 않은 동작(UB)입니다. KCSAN은 이러한 데이터 레이스를 런타임에 감지합니다. 커널 5.8부터 통합되었습니다.

동작 원리: 워치포인트 기반 탐지

KCSAN은 섀도 메모리가 아닌 워치포인트(watchpoint) 기반으로 동작합니다. 메모리 접근 시 해당 주소에 워치포인트를 설정하고, 짧은 지연(delay window) 동안 다른 CPU에서 동일 주소에 비동기적으로 접근하면 데이터 레이스로 보고합니다. 확률적 탐지이므로 테스트를 반복 실행할수록 더 많은 레이스를 발견합니다.

/* KCSAN 워치포인트 동작 의사 코드 */
/* CPU 0: write(addr)가 발생하면 */
set_watchpoint(addr, size, WRITE);
delay();                          /* 잠시 대기하며 다른 CPU 접근을 관찰 */
if (watchpoint_triggered())       /* 다른 CPU가 같은 주소에 접근함 */
    kcsan_report_race();           /* 데이터 레이스 보고! */
remove_watchpoint(addr);

커널 설정

# 기본 설정
CONFIG_KCSAN=y
CONFIG_KCSAN_STRICT=y              # 엄격 모드: marked-but-racy 접근도 보고
CONFIG_KCSAN_WEAK_MEMORY=y         # 약한 메모리 모델 시뮬레이션
CONFIG_KCSAN_REPORT_ONCE_IN_MS=0   # 모든 레이스 보고 (0=제한 없음)
CONFIG_KCSAN_REPORT_VALUE_CHANGE_ONLY=y  # 값이 실제로 변한 경우만 보고

# 선택적 옵션
CONFIG_KCSAN_VERBOSE=y             # 상세 디버그 메시지
CONFIG_KCSAN_INTERRUPT_WATCHER=y   # 인터럽트 컨텍스트 레이스도 탐지

리포트 읽기

# KCSAN 리포트 예시
==================================================================
BUG: KCSAN: data-race in update_counter / read_counter
#                        ↑ 쓰기 함수      ↑ 읽기 함수

write to 0xffffffff81a23450 of 4 bytes by task 567 on cpu 3:
# ↑ 쓰기 접근 — 주소, 크기, 태스크, CPU
 update_counter+0x28/0x40
 worker_thread+0x1a0/0x3c0

read to 0xffffffff81a23450 of 4 bytes by task 890 on cpu 7:
# ↑ 읽기 접근 — 같은 주소에 다른 CPU에서
 read_counter+0x14/0x30
 show_counter+0x20/0x50

value changed: 0x0000002a -> 0x0000002b
# ↑ 워치포인트 기간 동안 값이 실제로 변경됨을 확인

Reported by Kernel Concurrency Sanitizer on:
CPU: 3 PID: 567 Comm: kworker/3:1
CPU: 7 PID: 890 Comm: cat
==================================================================

데이터 레이스 수정 방법

/* ===== 레이스가 보고된 원래 코드 ===== */
static int counter;

void update_counter(void) {
    counter++;    /* 쓰기: 보호 없음 → 데이터 레이스! */
}

int read_counter(void) {
    return counter;  /* 읽기: 보호 없음 → 데이터 레이스! */
}

/* ===== 해결 1: atomic 사용 (권장) ===== */
static atomic_t counter = ATOMIC_INIT(0);

void update_counter(void) {
    atomic_inc(&counter);
}

int read_counter(void) {
    return atomic_read(&counter);
}

/* ===== 해결 2: READ_ONCE/WRITE_ONCE (의도적 레이스 표시) ===== */
/* 값의 정확성이 중요하지 않은 통계 카운터 등에 적합 */
static int stats_counter;

void update_stats(void) {
    WRITE_ONCE(stats_counter, READ_ONCE(stats_counter) + 1);
}

int read_stats(void) {
    return READ_ONCE(stats_counter);
}

/* ===== 해결 3: data_race() 어노테이션 (거짓 양성 억제) ===== */
/* 레이스가 무해함을 확인한 경우에만 사용 */
int read_stats_racy(void) {
    return data_race(stats_counter);  /* KCSAN 보고 억제 */
}

KCSAN 어노테이션

/* --- 파일/디렉터리 수준에서 KCSAN 비활성화 --- */
/* Makefile에서 특정 파일 제외: */
# KCSAN_SANITIZE_myfile.o := n
# KCSAN_SANITIZE := n      ← 디렉터리 전체 제외

/* --- 함수 수준에서 KCSAN 비활성화 --- */
__no_kcsan
void intentionally_racy_func(void)
{
    /* 이 함수 내 모든 접근은 KCSAN이 무시 */
}

/* --- 특정 접근만 마킹 --- */
/* data_race(): 특정 표현식의 레이스를 무해한 것으로 표시 */
int val = data_race(*ptr);

/* READ_ONCE()/WRITE_ONCE(): 컴파일러 최적화 방지 + KCSAN 마킹 */
int val = READ_ONCE(*ptr);     /* 정렬된 접근은 tear-free 보장 */
WRITE_ONCE(*ptr, new_val);

/* smp_load_acquire()/smp_store_release(): 순서 보장 + 마킹 */
int val = smp_load_acquire(ptr);
smp_store_release(ptr, new_val);
ℹ️

KCSAN vs lockdep: lockdep은 잠금 순서 위반(데드락 가능성)을 탐지하고, KCSAN은 잠금 없이 공유 데이터에 접근하는 데이터 레이스를 탐지합니다. 두 도구는 보완적이므로 함께 사용하는 것이 좋습니다.

UBSAN (Undefined Behavior Sanitizer)

UBSAN은 C 표준에서 정의되지 않은 동작(Undefined Behavior, UB)을 런타임에 탐지합니다. 정수 오버플로, 배열 범위 초과, 정렬 위반, 0으로 나누기 등을 감지합니다. 오버헤드가 상대적으로 낮아(CPU ~5~10%) 프로덕션에 가까운 환경에서도 활성화할 수 있습니다.

커널 설정

# 기본 설정
CONFIG_UBSAN=y
CONFIG_UBSAN_BOUNDS=y          # 배열 경계 검사
CONFIG_UBSAN_BOUNDS_STRICT=y   # flexible array 멤버도 검사
CONFIG_UBSAN_SHIFT=y           # 시프트 연산 검사
CONFIG_UBSAN_DIV_ZERO=y        # 0으로 나누기 검사
CONFIG_UBSAN_UNREACHABLE=y     # 도달 불가 코드 검사
CONFIG_UBSAN_SIGNED_WRAP=y     # 부호 있는 정수 오버플로
CONFIG_UBSAN_BOOL=y            # bool 타입에 0/1 외 값 로드 검사
CONFIG_UBSAN_ENUM=y            # enum 타입에 범위 외 값 로드 검사
CONFIG_UBSAN_ALIGNMENT=y       # 정렬 위반 검사 (주의: 노이즈 많음)

# 오류 동작 제어
CONFIG_UBSAN_TRAP=n            # y로 설정 시 UB 발생 즉시 트랩 (프로덕션용)

탐지 가능한 UB 유형과 코드 예시

/* 1. 부호 있는 정수 오버플로 (signed integer overflow) */
int add_values(int a, int b)
{
    return a + b;  /* a=INT_MAX, b=1이면 UB! */
    /* UBSAN: signed-integer-overflow:
       2147483647 + 1 cannot be represented in type 'int' */
}

/* 2. 시프트 연산 오류 (shift-out-of-bounds) */
u32 bad_shift(int n)
{
    return 1U << n;  /* n=32이면 UB! (32비트 타입을 32비트 시프트) */
    /* UBSAN: shift-out-of-bounds:
       1 shifted by 32 places exceeds 32-bit type 'unsigned int' */
    /* 해결: n >= 32 검사 추가 또는 1ULL << n 사용 */
}

void negative_shift(void)
{
    int x = 1 << -1;  /* UB: 음수 시프트 */
    /* UBSAN: shift-out-of-bounds:
       shift exponent -1 is negative */
}

/* 3. 배열 범위 초과 (array-index-out-of-bounds) */
struct fixed_array {
    int data[4];
};

void oob_access(struct fixed_array *arr)
{
    arr->data[4] = 0;  /* UB: 인덱스 4는 크기 4 배열의 범위 밖 */
    /* UBSAN: array-index-out-of-bounds:
       index 4 is out of range for type 'int[4]' */
}

/* 4. 0으로 나누기 (division-by-zero) */
int divide(int a, int b)
{
    return a / b;  /* b=0이면 UB! */
    /* UBSAN: division-by-zero:
       division by zero */
}

/* 5. 정렬 위반 (misaligned-access) */
void misaligned(void)
{
    char buf[16];
    int *p = (int *)(buf + 1);  /* 1바이트 오프셋 = 정렬되지 않은 포인터 */
    *p = 42;               /* UB: int는 4바이트 정렬 필요 */
    /* UBSAN: misaligned-access:
       member access within misaligned address */
}

/* 6. 도달 불가 코드 (unreachable) */
int get_type(int code)
{
    switch (code) {
    case 0: return 1;
    case 1: return 2;
    }
    /* code=2이면 여기에 도달 — __builtin_unreachable() 사용 시 UB */
    __builtin_unreachable();
    /* UBSAN: reached unreachable code */
}

UBSAN 리포트 읽기

# 실제 UBSAN 커널 리포트 예시
==================================================================
UBSAN: shift-out-of-bounds in drivers/net/mydriver.c:245:12
shift exponent 32 is too large for 32-bit type 'unsigned int'
CPU: 1 PID: 3456 Comm: systemd-networkd Not tainted 6.8.0 #1
Call Trace:
 dump_stack_lvl+0x34/0x48
 ubsan_epilogue+0x5/0x40
 __ubsan_handle_shift_out_of_bounds+0x10f/0x170
 my_driver_set_reg+0x84/0xc0 [mydriver]     # ← 버그 발생 지점
 my_driver_init+0x120/0x200 [mydriver]
 do_one_initcall+0x80/0x2a0
 ...
==================================================================

UBSAN 어노테이션

# Makefile에서 특정 파일/디렉터리 UBSAN 비활성화
UBSAN_SANITIZE_myfile.o := n          # 특정 파일 제외
UBSAN_SANITIZE := n                   # 디렉터리 전체 제외
/* 함수 수준에서 UBSAN 비활성화 (GCC/Clang 속성) */
__attribute__((no_sanitize("undefined")))
void intentional_overflow(void)
{
    /* 의도적으로 정의되지 않은 동작을 사용하는 코드 */
}

/* 커널 매크로: 의도적 부호 있는 래핑 */
/* check_add_overflow(a, b, &result) — 오버플로 시 true 반환 */
int a = INT_MAX, b = 1, result;
if (check_add_overflow(a, b, &result)) {
    pr_err("overflow detected\\n");
    return -EOVERFLOW;
}

새니타이저 조합 가이드

새니타이저탐지 대상CPU 오버헤드메모리 오버헤드호환성
KASAN (Generic)OOB, UAF, double-free~1.5~3배~2~3배 (섀도 메모리)KMSAN과 동시 사용 불가
KMSAN초기화되지 않은 메모리 사용~3배~3배KASAN과 동시 사용 불가, Clang 필수
KCSAN데이터 레이스~2배미미KASAN/KMSAN/UBSAN과 동시 사용 가능
UBSAN정의되지 않은 동작~5~10%미미모든 새니타이저와 동시 사용 가능
⚠️

새니타이저 오버헤드: KASAN은 메모리 사용량을 약 2~3배, CPU 오버헤드를 1.5~3배 증가시킵니다. KMSAN과 KCSAN도 유사한 수준입니다. 프로덕션 커널에서는 비활성화하고, CI/CD 파이프라인이나 테스트 환경에서만 사용하십시오. KASAN과 KMSAN은 동시에 활성화할 수 없습니다.

💡

권장 테스트 전략: CI/CD에서 두 개의 커널 빌드를 사용하면 대부분의 버그를 커버할 수 있습니다. (1) KASAN + KCSAN + UBSAN 빌드, (2) KMSAN + UBSAN 빌드. syzbot(Google의 퍼저)도 이 조합으로 수천 개의 커널 버그를 발견했습니다.

lockdep

lockdep(Lock Dependency Validator)은 커널의 잠금 순서(lock ordering)를 추적하여 잠재적 데드락을 탐지하는 도구입니다. 실제 데드락이 발생하기 전에 잘못된 잠금 순서를 감지하므로, 재현하기 어려운 데드락 버그를 조기에 발견할 수 있습니다.

데드락 탐지 원리

lockdep은 모든 잠금 획득/해제를 추적하여 잠금 의존성 그래프(lock dependency graph)를 구축합니다. 이 그래프에 사이클(cycle)이 발견되면 잠재적 데드락이 존재하는 것입니다. 예를 들어 CPU A가 lock1 -> lock2 순서로 획득하고, CPU B가 lock2 -> lock1 순서로 획득하면 ABBA 데드락이 발생할 수 있습니다.

CONFIG 옵션

CONFIG_PROVE_LOCKING=y          # lockdep 핵심 옵션
CONFIG_LOCK_STAT=y              # 잠금 통계 (/proc/lock_stat)
CONFIG_DEBUG_LOCK_ALLOC=y       # 잠금 할당 디버깅
CONFIG_DEBUG_LOCKDEP=y          # lockdep 자체 디버깅
CONFIG_LOCKDEP=y                # PROVE_LOCKING이 자동 선택

lock ordering 검증

/* lockdep이 탐지하는 ABBA 데드락 패턴 */
static DEFINE_MUTEX(lock_a);
static DEFINE_MUTEX(lock_b);

/* CPU 0 */
void func1(void)
{
    mutex_lock(&lock_a);    /* lock_a 획득 */
    mutex_lock(&lock_b);    /* lock_b 획득 → 순서: a → b */
    /* ... */
    mutex_unlock(&lock_b);
    mutex_unlock(&lock_a);
}

/* CPU 1 */
void func2(void)
{
    mutex_lock(&lock_b);    /* lock_b 획득 */
    mutex_lock(&lock_a);    /* lock_a 획득 → 순서: b → a (경고!) */
    /* ... */
    mutex_unlock(&lock_a);
    mutex_unlock(&lock_b);
}

lockdep 경고 메시지 해석

# 전형적인 lockdep 경고 출력
=====================================================
WARNING: possible circular locking dependency detected
6.1.0-debug #1 Not tainted
------------------------------------------------------
test_prog/1234 is trying to acquire lock:
ffff8881234abcd0 (&lock_a){+.+.}-{3:3}, at: func2+0x18/0x40

but task is already holding lock:
ffff8881234abce0 (&lock_b){+.+.}-{3:3}, at: func2+0x0c/0x40

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

-> #1 (&lock_b){+.+.}-{3:3}:
       mutex_lock+0x2c/0x40
       func1+0x24/0x50
       ...

-> #0 (&lock_a){+.+.}-{3:3}:
       mutex_lock+0x2c/0x40
       func2+0x18/0x40
       ...

other info that might help us debug this:
 Possible unsafe locking scenario:
       CPU0                    CPU1
       ----                    ----
  lock(&lock_a);
                               lock(&lock_b);
                               lock(&lock_a);   # 데드락!
  lock(&lock_b);               # 데드락!

lockstat (/proc/lock_stat)

CONFIG_LOCK_STAT=y 옵션을 활성화하면 /proc/lock_stat을 통해 각 잠금의 경합(contention) 통계를 확인할 수 있습니다.

# lockstat 활성화
echo 1 > /proc/sys/kernel/lock_stat

# 통계 확인
cat /proc/lock_stat | head -30
# lock_stat version 0.4
# --------------------------------------------------
#                  class name    con-bounces    contentions   ...
#                  -----------    -----------    -----------
#            &rq->__lock:       12345          6789   ...
#         &mm->mmap_lock:        5678          3456   ...

# 통계 초기화
echo 0 > /proc/lock_stat
ℹ️

lockdep 제한: lockdep은 잠금 클래스 단위로 추적합니다. 동일한 코드 라인에서 생성된 잠금은 같은 클래스로 간주됩니다. 배열 등에서 여러 인스턴스의 잠금이 다른 순서로 획득되어야 하는 경우 lockdep_set_class()mutex_lock_nested()를 사용하여 서브클래스를 지정하십시오.

Lock 상태 어노테이션 상세 해석

lockdep 경고 메시지에 나타나는 {+.+.}-{3:3} 형태의 어노테이션은 해당 lock의 IRQ 컨텍스트 사용 이력을 나타냅니다. 이를 정확히 읽을 수 있어야 lockdep 경고의 근본 원인을 파악할 수 있습니다.

# 어노테이션 형식
(&lock_name){XXXX}-{Y:Z}

# {XXXX} — 4자리 IRQ 컨텍스트 상태 (각 위치별)
#  위치 1: hardirq 컨텍스트에서 이 lock을 획득한 적 있는가?
#  위치 2: hardirq 활성화 상태에서 이 lock을 획득한 적 있는가?
#  위치 3: softirq 컨텍스트에서 이 lock을 획득한 적 있는가?
#  위치 4: softirq 활성화 상태에서 이 lock을 획득한 적 있는가?
#
#  각 위치의 값:
#    '+' = 해당 컨텍스트에서 획득됨 (ever used in)
#    '-' = 해당 컨텍스트에서 획득됨 + read lock
#    '.' = 해당 컨텍스트에서 사용된 적 없음
#    '?' = read lock으로만 사용됨 (아직 판정 불가)

# {Y:Z} — lock 타입 정보
#  Y = lock 타입 (0:spin, 1:rwlock, 2:mutex, 3:rwsem, 4:percpu-rwsem)
#  Z = 동일 (보통 Y와 같음)

# 예시 해석
(&my_lock){+.+.}-{2:2}
#  + hardirq 컨텍스트에서 사용됨
#  . hardirq 활성화 상태에서 사용 안 됨
#  + softirq 컨텍스트에서 사용됨
#  . softirq 활성화 상태에서 사용 안 됨
#  {2:2} = mutex

(&rq->__lock){-.-.}-{2:2}
#  - hardirq에서 read lock 사용
#  . hardirq 활성 시 미사용
#  - softirq에서 read lock 사용
#  . softirq 활성 시 미사용

IRQ-safe/unsafe Lock 혼용 경고

가장 흔한 lockdep 경고 중 하나는 같은 lock을 IRQ 컨텍스트와 프로세스 컨텍스트에서 안전하지 않게 사용하는 경우입니다:

# IRQ-safe/unsafe 혼용 경고 메시지
========================================================
WARNING: inconsistent lock state
6.1.0-debug #1 Not tainted
--------------------------------------------------------
inconsistent {HARDIRQ-ON-W} -> {IN-HARDIRQ-W} usage.
irq_handler/0 [HC1[1]:SC0[0]:HE0:SE1] takes:
ffff888123456780 (&my_lock){+.+.}-{2:2}, at: my_irq_func+0x20/0x80

{HARDIRQ-ON-W} state was registered at:
  mutex_lock+0x2c/0x40
  my_process_func+0x18/0x60
  ...

# 해석:
# 1. "inconsistent {HARDIRQ-ON-W} -> {IN-HARDIRQ-W}"
#    이전에 hardirq 활성화(ON) 상태에서 write(W) 획득했는데,
#    지금 hardirq 안(IN)에서 write 획득 시도
#    → hardirq가 프로세스 컨텍스트를 인터럽트하면 데드락!
#
# 2. [HC1[1]:SC0[0]:HE0:SE1] — CPU 상태 인코딩:
#    HC1 = Hardirq Count 1 (hardirq 내부)
#    SC0 = Softirq Count 0 (softirq 아님)
#    HE0 = Hardirq Enable 0 (hardirq 비활성화)
#    SE1 = Softirq Enable 1 (softirq 활성화)
#
# 해결: 프로세스 컨텍스트에서 spin_lock_irqsave() 사용
/* 문제 코드 */
static DEFINE_SPINLOCK(my_lock);

void my_process_func(void)
{
    spin_lock(&my_lock);     /* hardirq 활성화 상태에서 획득 */
    /* ... */
    spin_unlock(&my_lock);
}

irqreturn_t my_irq_handler(int irq, void *dev)
{
    spin_lock(&my_lock);     /* hardirq 안에서 같은 lock! → 데드락 */
    /* ... */
    spin_unlock(&my_lock);
    return IRQ_HANDLED;
}

/* 수정 코드 */
void my_process_func(void)
{
    unsigned long flags;
    spin_lock_irqsave(&my_lock, flags);   /* IRQ 비활성화 후 획득 */
    /* ... */
    spin_unlock_irqrestore(&my_lock, flags);
}

의존성 체인 읽기

lockdep 의존성 체인 — 3개 락 순환 탐지 순환 의존성 (Deadlock!) Task 1 func_ab() Task 2 func_bc() Task 3 func_ca() lock_a &lock_a lock_b &lock_b lock_c &lock_c 보유 보유 보유 대기 대기 순환! Deadlock 탐지 해결: 전역 락 순서 정의 전역 순서: lock_a → lock_b → lock_c 모든 태스크가 항상 이 순서로만 획득 절대 역순(lock_c → lock_b → lock_a) 금지 올바른 의존성 체인 (비순환) lock_a #1 (먼저) lock_b #2 (다음) lock_c #3 (마지막) lockdep 역방향 체인 출력 → #2 (&lock_c): func_bc — lock_b→lock_c → #1 (&lock_b): func_ab — lock_a→lock_b → #0 (&lock_a): func_ca — lock_c→lock_a ← 순환! 역순으로 읽어야 실제 획득 순서가 됨 중첩 락 해결 기법 • 전역 순서: 모든 팀이 동일한 획득 순서 준수 • mutex_lock_nested(): 같은 타입 중첩 시 서브클래스 • lockdep_set_class(): 명시적 클래스 분리 • /proc/lockdep_stats: 의존성 체인 통계 확인 • echo d > /proc/sysrq-trigger: held locks 덤프

lockdep_set_class와 nested locking

/* 같은 타입의 lock을 여러 인스턴스에서 중첩 획득하는 경우 */
/* lockdep은 같은 코드 라인의 lock을 같은 클래스로 봄 → 오탐 */

struct my_node {
    struct mutex lock;
    struct my_node *parent;
};

/* 부모 → 자식 순서로 lock 획득 (정당한 중첩) */
mutex_lock(&parent->lock);
mutex_lock_nested(&child->lock, SINGLE_DEPTH_NESTING);
/* SINGLE_DEPTH_NESTING = 서브클래스 1 */
/* lockdep이 parent.lock과 child.lock을 다른 클래스로 인식 */

/* 또는 명시적 클래스 지정 */
static struct lock_class_key parent_key, child_key;

lockdep_set_class(&parent->lock, &parent_key);
lockdep_set_class(&child->lock, &child_key);

/* inode lock의 실제 커널 예시 */
/* fs/namei.c: 디렉토리 rename 시 두 inode를 동시 lock */
inode_lock_nested(inode1, I_MUTEX_PARENT);
inode_lock_nested(inode2, I_MUTEX_CHILD);

lockdep 실전 디버깅 명령

# lockdep 통계 확인
cat /proc/lockdep_stats
# lock classes:        1523
# direct dependencies: 8234
# indirect dependencies: 25678
# max locking depth:   15
# → "lock classes" 수가 MAX_LOCKDEP_KEYS(8192)에 근접하면
#   lockdep이 비활성화됨 ("BUG: MAX_LOCKDEP_KEYS too low!")

# 모든 lock 의존성 관계 덤프
cat /proc/lockdep

# lock chain 확인
cat /proc/lockdep_chains

# 현재 held locks (특정 태스크)
cat /proc/PID/status | grep -i lock
# 또는 SysRq로 모든 태스크의 held locks 덤프
echo d > /proc/sysrq-trigger

# lockdep 경고 발생 후 비활성화 방지 (디버깅 시)
# lockdep은 첫 경고 후 성능을 위해 자동 비활성화됨
# 여러 문제를 한 번에 찾으려면 재부팅 필요

lockdep 내부 아키텍처

lockdep의 핵심 자료구조를 이해하면 경고 메시지 해석과 성능 튜닝에 크게 도움이 됩니다. lockdep은 크게 lock class, lock chain, held lock 세 가지 축으로 잠금 의존성을 관리합니다.

/* kernel/locking/lockdep_internals.h — 핵심 자료구조 */

/*
 * lock_class: 잠금 "클래스"를 나타내는 구조체
 * 같은 코드 위치(lock_class_key)에서 초기화된 모든 잠금 인스턴스는
 * 동일한 lock_class를 공유함
 */
struct lock_class {
    struct hlist_node   hash_entry;      /* 해시 테이블 엔트리 */
    struct list_head    lock_entry;      /* 전역 lock class 리스트 */

    struct list_head    locks_after;     /* 이 lock 이후에 획득된 lock들 */
    struct list_head    locks_before;    /* 이 lock 이전에 획득된 lock들 */

    const struct lock_class_key *key;   /* 잠금 식별자 (코드 위치 기반) */
    unsigned int        subclass;        /* nesting subclass (0~7) */

    unsigned long       usage_mask;      /* IRQ 컨텍스트 사용 비트마스크 */
    const char          *name;           /* lock 이름 (디버깅용) */
    short               wait_type_inner; /* LD_WAIT_* (wait context 타입) */
    short               wait_type_outer; /* 외부 wait context 타입 */
};

/*
 * held_lock: 현재 태스크가 보유한 잠금 정보 (per-task 스택)
 * current->held_locks[] 배열로 관리 (최대 MAX_LOCK_DEPTH=48)
 */
struct held_lock {
    u64                 prev_chain_key;   /* 이전까지의 chain hash */
    unsigned long       acquire_ip;       /* lock_acquire() 호출 주소 */
    struct lockdep_map  *instance;        /* 실제 잠금 인스턴스 */
    unsigned int        class_idx:13;     /* lock_class 인덱스 */
    unsigned int        irq_context:2;    /* 0=normal, 1=softirq, 2=hardirq */
    unsigned int        trylock:1;        /* trylock으로 획득? */
    unsigned int        read:2;            /* 0=write, 1=read, 2=recursive read */
    unsigned int        hardirqs_off:1;   /* IRQ 비활성화 상태? */
};

/*
 * lock_chain: 잠금 획득 순서 체인
 * held_locks 순서의 해시로 식별 → 같은 순서의 잠금 패턴을 빠르게 검증
 */
struct lock_chain {
    u64            chain_key;    /* 체인 전체의 해시 키 */
    int            depth;        /* 체인 깊이 (보유 lock 수) */
    int            base;         /* chain_hlocks[] 시작 인덱스 */
};

/* lock_list: lock class 간 의존성 엣지 (방향 그래프) */
struct lock_list {
    struct list_head    entry;    /* locks_after 또는 locks_before 리스트 */
    struct lock_class   *class;   /* 대상 lock class */
    struct lock_trace   trace;    /* 의존성이 기록된 스택 트레이스 */
    u16                 distance; /* BFS 탐색 거리 */
};
ℹ️

의존성 그래프 검증 알고리즘: lockdep은 새로운 잠금 의존성(A → B)이 추가될 때마다, B의 locks_after 리스트에서 시작하는 BFS(너비 우선 탐색)로 A에 도달할 수 있는지 검사합니다. 도달 가능하면 순환(cycle)이 존재하므로 데드락 경고를 출력합니다. 이 검증은 check_noncircular() 함수에서 수행되며, 이미 검증된 chain은 lock_chain 해시로 캐싱하여 중복 검증을 회피합니다.

lockdep_map과 lock_acquire/release API

모든 커널 잠금 프리미티브(spinlock, mutex, rwsem 등)는 내부에 lockdep_map 구조체를 포함하며, 잠금 획득/해제 시 lockdep 프레임워크에 이벤트를 전달합니다. 커스텀 동기화 메커니즘을 구현할 때 이 API를 직접 사용하여 lockdep 검증을 받을 수 있습니다.

/* include/linux/lockdep_types.h */
struct lockdep_map {
    struct lock_class_key  *key;       /* lock class 식별 키 */
    struct lock_class      *class_cache[2]; /* subclass 0,1 캐시 */
    const char             *name;      /* 사람이 읽을 수 있는 이름 */
    short                   wait_type_inner;
    short                   wait_type_outer;
};

/* 표준 잠금 내부의 lockdep_map */
struct mutex {
    atomic_long_t   owner;
    struct list_head wait_list;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;    /* lockdep 추적용 */
#endif
};

/* 커스텀 잠금에 lockdep 통합하기 */
struct my_custom_lock {
    atomic_t       state;
    struct lockdep_map dep_map;  /* lockdep 지원 추가 */
};

static struct lock_class_key my_lock_key;

void my_lock_init(struct my_custom_lock *lock)
{
    atomic_set(&lock->state, 0);
    lockdep_init_map(&lock->dep_map, "my_custom_lock",
                     &my_lock_key, 0);  /* subclass=0 */
}

void my_lock_acquire(struct my_custom_lock *lock)
{
    /* lockdep에 잠금 획득 시도를 통보 (검증 수행) */
    lock_acquire(&lock->dep_map,
        0,              /* subclass */
        0,              /* trylock (0=일반, 1=trylock) */
        0,              /* read (0=write, 1=read, 2=recursive read) */
        1,              /* check (1=의존성 검증, 0=스킵) */
        NULL,           /* nest_lock (중첩 잠금 컨텍스트) */
        _THIS_IP_);    /* 호출 위치 (instruction pointer) */

    /* 실제 잠금 획득 로직 */
    while (atomic_cmpxchg(&lock->state, 0, 1) != 0)
        cpu_relax();
}

void my_lock_release(struct my_custom_lock *lock)
{
    atomic_set(&lock->state, 0);

    /* lockdep에 잠금 해제를 통보 */
    lock_release(&lock->dep_map, _THIS_IP_);
}
💡

lockdep_assert API 전체 목록: 런타임 검증에 활용할 수 있는 assert 매크로들입니다. 디버그 빌드에서만 동작하며, 프로덕션 빌드에서는 빈 매크로로 컴파일됩니다.

/* lockdep_assert 매크로 — 잠금 상태 런타임 검증 */

/* 기본 보유 검증 */
lockdep_assert_held(&lock);              /* lock 보유 확인 (read 또는 write) */
lockdep_assert_held_write(&lock);        /* write lock 보유 확인 */
lockdep_assert_held_read(&lock);         /* read lock 보유 확인 */
lockdep_assert_not_held(&lock);          /* lock 미보유 확인 */
lockdep_assert_held_once(&lock);         /* 한 번만 보유 확인 (재귀 금지) */

/* IRQ 컨텍스트 검증 */
lockdep_assert_irqs_enabled();            /* IRQ 활성화 상태 확인 */
lockdep_assert_irqs_disabled();           /* IRQ 비활성화 상태 확인 */
lockdep_assert_in_irq();                  /* hardirq 컨텍스트 확인 */

/* preemption 검증 */
lockdep_assert_preemption_enabled();      /* 선점 가능 상태 확인 */
lockdep_assert_preemption_disabled();     /* 선점 불가 상태 확인 */

/* 실전 활용 예시 — VFS에서의 inode lock 검증 */
void ext4_truncate(struct inode *inode)
{
    /* 이 함수 진입 전에 반드시 inode write lock을 보유해야 함 */
    lockdep_assert_held_write(&inode->i_rwsem);

    /* ... truncate 로직 ... */
}

/* 네트워크 스택에서의 소켓 lock 검증 */
void tcp_send_fin(struct sock *sk)
{
    lockdep_assert_held(&sk->sk_lock.slock);
    /* ... FIN 패킷 전송 ... */
}

Wait type 검증 (LD_WAIT_*)

커널 5.x 이후 lockdep은 wait type 기반 검증을 도입하여, 잠금 획득 시 현재 컨텍스트에서 허용되는 대기 유형을 검사합니다. 이전에는 "spin lock 안에서 mutex를 잡으면 안 된다"는 규칙을 코드 리뷰에 의존했지만, 이제 lockdep이 자동으로 탐지합니다.

/* include/linux/lockdep_types.h — Wait type 상수 */
enum lockdep_wait_type {
    LD_WAIT_INV,      /* 잘못된 컨텍스트 (절대 사용 불가) */
    LD_WAIT_FREE,     /* lock-free 컨텍스트 (NMI, 어떤 lock도 불가) */
    LD_WAIT_SPIN,     /* spin lock 컨텍스트 (spin만 가능, sleep 불가) */
    LD_WAIT_CONFIG,   /* PREEMPT_RT 구성 의존 (spin or sleep) */
    LD_WAIT_SLEEP,    /* sleep 가능 컨텍스트 (mutex, rwsem 등) */
    LD_WAIT_MAX,
};

/*
 * Wait type 계층 (내부 → 외부):
 *   LD_WAIT_FREE < LD_WAIT_SPIN < LD_WAIT_CONFIG < LD_WAIT_SLEEP
 *
 * 규칙: wait_type_inner >= 현재 컨텍스트의 wait_type이어야 함
 *   - hardirq 컨텍스트: LD_WAIT_SPIN만 허용
 *   - spin_lock 보유 중: LD_WAIT_SPIN까지만 허용 (mutex 불가)
 *   - preemptible 컨텍스트: 모든 wait type 허용
 */

/* 각 잠금 프리미티브의 wait type 설정 */
/* raw_spinlock_t:  inner=LD_WAIT_SPIN,  outer=LD_WAIT_INV   */
/* spinlock_t:      inner=LD_WAIT_CONFIG, outer=LD_WAIT_INV  */
/* mutex:           inner=LD_WAIT_SLEEP,  outer=LD_WAIT_INV  */
/* rwsem:           inner=LD_WAIT_SLEEP,  outer=LD_WAIT_INV  */
/* percpu_rwsem:    inner=LD_WAIT_SLEEP,  outer=LD_WAIT_INV  */
# Wait type 위반 경고 메시지 예시
=============================================
WARNING: possible incorrect hardirq context
6.8.0-debug #1 Not tainted
---------------------------------------------
swapper/0/0 is trying to acquire lock:
ffff888123456780 (&my_mutex){+.+.}-{3:3}, at: my_irq_handler+0x28/0x80

which is a SLEEP lock, but held in HARDIRQ context!

other info that might help us debug this:
 context-{2:2}   # 현재 컨텍스트: LD_WAIT_SPIN (hardirq)
 lock-{3:3}      # 요청된 lock: LD_WAIT_SLEEP (mutex)
                  # → SLEEP > SPIN이므로 위반!

# 해석:
# hardirq 컨텍스트(LD_WAIT_SPIN)에서 mutex(LD_WAIT_SLEEP) 획득 시도
# mutex는 contention 시 sleep하므로 hardirq에서 사용 불가
# 해결: spin_lock으로 변경하거나, 작업을 workqueue로 지연

Cross-lock 타입 의존성 검증

lockdep은 서로 다른 타입의 잠금(spinlock, mutex, rwsem, rwlock 등) 간 의존성도 추적합니다. 특히 rwlock과 rwsem의 reader/writer 구분은 데드락 탐지의 정밀도에 중요한 역할을 합니다.

/* rwlock의 reader/writer 구분과 데드락 패턴 */

/*
 * rwlock에서의 ABBA 데드락은 reader-writer 조합에 따라 다름:
 *
 * Case 1: WW-WW (데드락)
 *   CPU0: write_lock(A) → write_lock(B)
 *   CPU1: write_lock(B) → write_lock(A)
 *   → 전형적 ABBA, lockdep 탐지
 *
 * Case 2: WR-RW (데드락)
 *   CPU0: write_lock(A) → read_lock(B)
 *   CPU1: read_lock(B)  → write_lock(A) [B의 reader 때문에 writer 대기]
 *   → lockdep 탐지 (단, 타이밍 의존적)
 *
 * Case 3: RR-WW (데드락 가능!)
 *   CPU0: read_lock(A)  → read_lock(B)
 *   CPU1: write_lock(B) → write_lock(A)
 *   → CPU0이 B reader 보유, CPU1이 B writer 대기
 *   → CPU1이 A writer 시도하지만 CPU0의 A reader 때문에 대기
 *   → lockdep 탐지
 *
 * Case 4: RR-RR (데드락 아님)
 *   CPU0: read_lock(A)  → read_lock(B)
 *   CPU1: read_lock(B)  → read_lock(A)
 *   → reader끼리는 공유 가능, 데드락 없음
 *   → lockdep이 경고하지 않음
 */

/* rwsem의 reader-writer 데드락 실제 사례 */
static DECLARE_RWSEM(rwsem_a);
static DECLARE_RWSEM(rwsem_b);

/* 잘못된 코드: RW-WR 역전 */
void thread1(void) {
    down_read(&rwsem_a);    /* A reader 획득 */
    down_write(&rwsem_b);   /* B writer 획득 */
    up_write(&rwsem_b);
    up_read(&rwsem_a);
}

void thread2(void) {
    down_write(&rwsem_b);   /* B writer 획득 */
    down_read(&rwsem_a);    /* A reader 시도 — 데드락 가능! */
    /*
     * thread1이 A reader를 보유하고 B writer를 대기,
     * thread2가 B writer를 보유하고 A reader를 시도
     * → 만약 thread3이 A writer를 대기 중이면
     *   A reader(thread2) 허용 여부는 rwsem 구현에 따라 다름
     *   (writer starvation 방지를 위해 reader 차단 가능)
     */
    up_read(&rwsem_a);
    up_write(&rwsem_b);
}
# lockdep의 reader/writer 구분 경고 메시지
========================================================
WARNING: possible circular locking dependency detected
--------------------------------------------------------
thread2/5678 is trying to acquire lock:
ffff888012345678 (&rwsem_a){++++}-{3:3}, at: thread2+0x20/0x60

 but task is already holding lock:
ffff888087654321 (&rwsem_b){++++}-{3:3}, at: thread2+0x10/0x60

  Possible unsafe locking scenario:

        CPU0                    CPU1
        ----                    ----
   lock(&rwsem_a);          # R (read)
                               lock(&rwsem_b);      # W (write)
                               lock(&rwsem_a);      # R
   lock(&rwsem_b);          # W

# 경고 메시지의 R/W 표시를 주의 깊게 확인!
# R-R 교차는 안전할 수 있지만, R-W 또는 W-W 교차는 데드락

lockdep 한계와 오버플로우 복구

lockdep은 고정 크기 자원을 사용하며, 자원이 소진되면 자동으로 비활성화됩니다. 이 한계를 이해하고 대비하는 것이 중요합니다.

/* kernel/locking/lockdep_internals.h — 주요 한계 상수 */

/* lock class 관련 */
#define MAX_LOCKDEP_KEYS       8192   /* 최대 lock class 수 */
#define MAX_LOCKDEP_KEYS_BITS  13     /* 2^13 = 8192 */

/* 의존성 그래프 관련 */
#define MAX_LOCKDEP_ENTRIES    32768  /* 최대 의존성 엣지 수 */
#define MAX_LOCKDEP_CHAINS     65536  /* 최대 lock chain 수 */
#define MAX_LOCKDEP_CHAIN_HLOCKS 131072 /* chain 내 held_lock 총합 */

/* per-task 관련 */
#define MAX_LOCK_DEPTH         48     /* 태스크당 최대 동시 보유 lock 수 */

/* stack trace 관련 */
#define MAX_LOCKDEP_STACK_TRACE_ENTRIES 524288
# lockdep 오버플로우 메시지 예시
BUG: MAX_LOCKDEP_KEYS too low!
turning off the locking correctness validator.

# 또는
BUG: MAX_LOCKDEP_ENTRIES too low!

# 또는
BUG: MAX_LOCKDEP_CHAINS too low!

# 또는 per-task 깊이 초과
BUG: MAX_LOCK_DEPTH too low!
turning off the locking correctness validator.
48 locks held by process_name/1234:
 #0: ffff888... (&lock_1){....}-{2:2}, at: func1+0x10
 #1: ffff888... (&lock_2){....}-{2:2}, at: func2+0x20
 ...
 #47: ffff888... (&lock_48){....}-{2:2}, at: func48+0x10

# lockdep 현재 자원 사용량 확인
cat /proc/lockdep_stats

# 출력 예시:
 lock-classes:                         1523 [max: 8192]
 direct dependencies:                  8234 [max: 32768]
 indirect dependencies:               25678
 all direct dependencies:             16468
 dependency chains:                   12345 [max: 65536]
 dependency chain hlocks:             45678 [max: 131072]
 in-hardirq chains:                     234
 in-softirq chains:                     567
 in-process chains:                   11544
 stack-trace entries:                 89012 [max: 524288]

# 핵심 확인 포인트:
# 1. lock-classes가 MAX_LOCKDEP_KEYS의 80%를 넘으면 위험
# 2. dependency chains이 MAX_LOCKDEP_CHAINS에 근접하면 위험
# 3. stack-trace entries 소진도 비활성화 원인
/* lockdep 비활성화/재활성화 메커니즘 */

/*
 * debug_locks: lockdep 활성화 상태를 제어하는 전역 변수
 * - 1 = 활성화 (정상)
 * - 0 = 비활성화 (오버플로우 또는 첫 경고 후)
 *
 * lockdep은 다음 상황에서 자동 비활성화:
 * 1. 첫 번째 경고 출력 후 (성능을 위해)
 * 2. 자원(lock class, chain 등) 오버플로우
 * 3. lockdep 내부 BUG 탐지
 *
 * 비활성화 후 재활성화하려면 재부팅이 필요함
 * (런타임에 debug_locks를 1로 설정해도 내부 상태가 불완전)
 */

/* debug_locks_off() — lockdep 비활성화 함수 */
int debug_locks_off(void)
{
    int ret;

    ret = xchg(&debug_locks, 0);
    if (ret) {
        if (!debug_locks_silent)
            console_verbose();
    }
    return ret;
}

/* 오버플로우 방지 전략:
 *
 * 1. 커널 모듈 최소화: 불필요한 모듈 언로드로 lock class 확보
 * 2. lockdep_set_class(): 동적 생성 객체의 lock class를 명시적으로
 *    지정하여 class 수 절감
 * 3. 커널 빌드 시 불필요한 드라이버 제외
 * 4. MAX_LOCKDEP_KEYS 등을 커널 소스에서 직접 증가 (비권장:
 *    메모리 사용량 증가, BFS 검색 시간 증가)
 */
⚠️

lockdep 비활성화 감지: dmesg | grep "turning off the locking correctness validator"로 lockdep 비활성화 여부를 확인하세요. 비활성화된 상태에서는 어떤 잠금 순서 위반도 탐지되지 않습니다. CI/CD 파이프라인에서 이 메시지를 경고로 처리하는 것을 권장합니다.

lockdep과 커널 모듈

커널 모듈의 로드/언로드는 lockdep의 lock class 관리에 영향을 미칩니다. 모듈이 언로드되면 해당 모듈의 코드 섹션에 위치한 lock_class_key가 무효화되므로, lockdep은 이를 정리해야 합니다.

/* 모듈 언로드 시 lockdep 정리 과정 */

/*
 * kernel/locking/lockdep.c — lockdep_free_key_range()
 *
 * 모듈 언로드 시 호출 경로:
 *   delete_module() → free_module()
 *     → lockdep_free_key_range(mod->core_layout.base,
 *                               mod->core_layout.size)
 *
 * 해당 모듈의 메모리 범위에 속하는 모든 lock_class_key를
 * 해시 테이블에서 제거하고 관련 의존성 엣지를 정리
 */
void lockdep_free_key_range(void *start, unsigned long size)
{
    /* 1. lock class 해시 테이블에서 해당 범위의 키 제거 */
    /* 2. 관련 lock_list(의존성 엣지) 제거 */
    /* 3. lock_chain 캐시에서 관련 체인 제거 */
    /* → lock class 슬롯이 재사용 가능해짐 */
}

/* 모듈에서 동적으로 lock class를 등록하는 올바른 방법 */

/* 방법 1: 정적 key (모듈 내 전역 변수) — 가장 일반적 */
static struct lock_class_key my_driver_lock_key;

static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev = devm_kzalloc(...);
    mutex_init(&dev->lock);
    lockdep_set_class(&dev->lock, &my_driver_lock_key);
    /* 모든 my_device 인스턴스의 lock이 같은 class 공유
     * → lock class 수 절약 */
    return 0;
}

/* 방법 2: DEFINE_MUTEX()와 같은 정적 초기화 매크로 사용 */
/* → 매크로 내부에서 lock_class_key가 자동 생성됨 */
static DEFINE_MUTEX(my_global_lock);

/* 주의: 모듈 내에서 alloc + init 패턴으로 lock을 대량 생성하면
 * 각 init 호출 위치마다 별도의 lock_class_key가 생성되어
 * MAX_LOCKDEP_KEYS 소진 위험이 있음
 * → lockdep_set_class()로 공유하는 것이 바람직 */

lockdep procfs 인터페이스 상세

lockdep은 여러 /proc 파일을 통해 내부 상태를 노출합니다. 이를 활용하면 잠금 의존성 관계를 전체적으로 파악하고 병목을 식별할 수 있습니다.

# /proc/lockdep — 모든 lock class와 의존성 관계 덤프
cat /proc/lockdep

# 출력 형식:
# all lock classes:
# &rq->__lock                   [hardirq-safe]
#   -> &mm->mmap_lock            [hardirq-unsafe]
#   -> &inode->i_rwsem           [hardirq-unsafe]
#
# &mm->mmap_lock                 [hardirq-unsafe]
#   -> &inode->i_data.i_pages.xa_lock [hardirq-safe]
#   -> &sb->s_type->i_mutex_key  [hardirq-unsafe]

# 해석 방법:
# - 들여쓰기 없는 줄: lock class 이름 + [IRQ 안전성]
# - 들여쓰기된 "-> ..." 줄: 해당 class가 보유된 상태에서
#   획득한 적 있는 다른 lock class (직접 의존성)
# - [hardirq-safe]: IRQ 비활성화 상태에서 획득되는 lock
# - [hardirq-unsafe]: IRQ 활성화 상태에서 획득되는 lock

# /proc/lockdep_chains — 관찰된 모든 lock chain 나열
cat /proc/lockdep_chains

# 출력 형식:
# irq_context: 0
# [ffff...] &mm->mmap_lock -> &inode->i_rwsem -> &sb->s_type->i_mutex_key
#
# irq_context: 2
# [ffff...] &rq->__lock

# 해석 방법:
# - irq_context: 0=프로세스, 1=softirq, 2=hardirq
# - A -> B -> C: A를 보유한 상태에서 B를 획득하고,
#   B를 보유한 상태에서 C를 획득한 패턴
# - 같은 chain이 여러 번 등장하지 않음 (해시로 중복 제거)

# /proc/lockdep_stats — lockdep 자원 사용 현황
cat /proc/lockdep_stats

# 핵심 항목:
#  lock-classes:     1523 [max: 8192]    ← lock class 수/한계
#  direct dependencies:  8234 [max: 32768]  ← 직접 의존성 엣지
#  dependency chains: 12345 [max: 65536] ← 관찰된 chain 수
#  max locking depth:    15              ← 최대 동시 보유 lock 수
#  combined max deps:   25678            ← 직간접 의존성 합계
#  hardirq-safe:        234              ← IRQ-safe lock class 수
#  hardirq-unsafe:     1289              ← IRQ-unsafe lock class 수

# /proc/lock_stat — 잠금 경합 통계 (CONFIG_LOCK_STAT=y 필요)
cat /proc/lock_stat | head -30

# 주요 컬럼:
#   class name         — lock class 이름
#   con-bounces        — 캐시 라인 바운스 횟수 (lock contention)
#   contentions        — 경합 발생 횟수
#   waittime-min/max   — 대기 시간 범위 (ns)
#   acquisitions       — 총 획득 횟수
#   holdtime-min/max   — 보유 시간 범위 (ns)
#
# 성능 병목 식별:
# 1. contentions가 높으면 경합이 심한 lock
# 2. waittime-max가 크면 특정 상황에서 오래 대기
# 3. holdtime-max가 크면 critical section이 긴 lock
# → per-CPU lock, 세분화(fine-grained lock), RCU 전환 고려

lockdep 경고 수정 체계적 워크플로우

lockdep 경고를 체계적으로 분석하고 수정하는 절차입니다. 경고를 단순히 무시하거나 lockdep 어노테이션으로 숨기는 것은 잠재적 데드락을 방치하는 것이므로, 근본 원인을 파악하여 수정해야 합니다.

# Step 1: 경고 메시지 수집 및 분류
dmesg | grep -A 50 "WARNING:.*locking"

# 경고 유형별 분류:
#
# (A) "possible circular locking dependency detected"
#     → ABBA 데드락 (잠금 순서 위반)
#     → Step 2A로 이동
#
# (B) "inconsistent lock state"
#     → IRQ-safe/unsafe 혼용
#     → Step 2B로 이동
#
# (C) "possible recursive locking detected"
#     → 같은 lock class의 재귀 획득
#     → Step 2C로 이동
#
# (D) "possible incorrect hardirq context"
#     → wait type 위반 (sleep lock in atomic)
#     → Step 2D로 이동
# Step 2A: ABBA 데드락 분석

# 1. "Possible unsafe locking scenario" 섹션에서 데드락 시나리오 확인
#    → 어떤 lock이 어떤 순서로 획득되는지 파악

# 2. "the existing dependency chain" 섹션에서 의존성 체인 추적
#    → 역순(#N → #0)으로 읽어 순환 경로 파악
#    → 각 "#N" 항목의 함수명과 소스 파일 확인

# 3. 해당 소스 코드에서 잠금 획득 순서 확인
#    → addr2line으로 정확한 소스 위치 파악
addr2line -e vmlinux func1+0x24

# 4. 수정 방안:
#    a) 잠금 획득 순서를 일관되게 통일
#    b) lock의 범위를 축소하여 중첩 획득 회피
#    c) trylock + retry 패턴으로 순서 역전 회피
#    d) 구조를 변경하여 하나의 lock으로 통합
/* Step 2A 수정 예시: trylock + retry로 ABBA 회피 */

/* 수정 전: func2에서 b→a 순서로 획득 (func1의 a→b와 역전) */
void func2_fixed(void)
{
retry:
    mutex_lock(&lock_b);
    if (!mutex_trylock(&lock_a)) {
        /* lock_a를 즉시 획득할 수 없음 → 역순 방지 */
        mutex_unlock(&lock_b);
        mutex_lock(&lock_a);    /* 올바른 순서: a 먼저 */
        mutex_lock(&lock_b);    /* 그 다음 b */
    }
    /* ... critical section ... */
    mutex_unlock(&lock_a);
    mutex_unlock(&lock_b);
}

/* Step 2B: IRQ-safe/unsafe 혼용 수정 */
/* 경고의 {HARDIRQ-ON-W} → {IN-HARDIRQ-W} 확인 후 */
/* 프로세스 컨텍스트에서의 획득을 spin_lock_irqsave()로 변경 */

/* Step 2C: 재귀 잠금 수정 */
/* 진짜 재귀인 경우: mutex_lock_nested()로 subclass 지정 */
/* 오탐인 경우: lockdep_set_class()로 다른 class 할당 */

/* Step 2D: Wait type 위반 수정 */
/* hardirq에서 mutex → spinlock으로 변경 */
/* 또는 작업을 workqueue/tasklet으로 지연 */
⚠️

lockdep 경고를 숨기면 안 되는 이유: lockdep_set_novalidate_class()lock_acquire(..., check=0, ...)으로 lockdep 검증을 비활성화하면 경고는 사라지지만 데드락 위험은 그대로 남습니다. 이 방법은 lockdep 자체의 한계(예: lock class 부족)로 인한 오탐에만 극히 제한적으로 사용해야 합니다. 항상 먼저 코드를 수정하여 근본 원인을 제거하세요.

locktorture: 잠금 스트레스 테스트

locktorture는 커널 내장 모듈로, 다양한 잠금 프리미티브에 대해 고강도 스트레스 테스트를 수행합니다. lockdep과 함께 사용하면 실제 워크로드에서 발생하기 어려운 경합 상황을 재현하고 잠금 구현의 정확성을 검증할 수 있습니다.

# locktorture 커널 설정
CONFIG_LOCK_TORTURE_TEST=m     # 모듈로 빌드 (또는 =y)
CONFIG_PROVE_LOCKING=y         # lockdep 활성화 (병행 검증)

# 모듈 로드 (기본: spin_lock 테스트)
modprobe locktorture

# 파라미터 지정하여 다양한 lock 타입 테스트
modprobe locktorture torture_type=mutex_lock nwriters_stress=8

# 지원되는 torture_type:
#   spin_lock          — spinlock 기본
#   spin_lock_irq      — spinlock + IRQ 비활성화
#   rw_lock            — rwlock write
#   rw_lock_irq        — rwlock + IRQ
#   mutex_lock         — mutex 기본
#   rtmutex_lock       — RT mutex
#   rwsem_lock         — rwsem write
#   percpu_rwsem_lock  — per-CPU rwsem

# 주요 파라미터:
#   nwriters_stress=N   — writer 스트레스 스레드 수 (기본: 온라인 CPU 수)
#   nreaders_stress=N   — reader 스트레스 스레드 수 (rw 계열만)
#   stat_interval=S     — 통계 출력 간격 (초)
#   stutter=S           — 테스트 일시정지/재개 간격 (초)
#   shuffle_interval=S  — CPU 이동 간격 (초)
#   onoff_interval=S    — CPU 온/오프 간격 (hotplug 테스트)
#   shutdown_secs=S     — 지정 시간 후 자동 종료

# 실행 결과 확인
dmesg | grep "torture"
# lock_torture_stats: Writes: Total: 12345678 Max/Min: 1567890/1234567
# lock_torture_stats: Reads:  Total: 98765432 Max/Min: 12456789/12345678

# 모듈 언로드로 테스트 종료
rmmod locktorture

# CI/CD 통합 예시 (자동 종료 + 결과 확인)
modprobe locktorture torture_type=mutex_lock \
    nwriters_stress=16 shutdown_secs=300 stat_interval=60

# 5분 후 자동 종료, 매 60초마다 통계 출력
# dmesg에서 "FAILURE" 또는 lockdep 경고 확인
dmesg | grep -E "FAILURE|WARNING.*locking|BUG"
💡

lockdep + locktorture 조합 전략: (1) CONFIG_PROVE_LOCKING=yCONFIG_LOCK_TORTURE_TEST=m을 함께 활성화하세요. (2) 각 잠금 타입별로 locktorture를 실행하여 잠금 구현의 정확성과 lockdep 검증을 동시에 수행하세요. (3) onoff_interval을 설정하면 CPU hotplug와의 상호작용도 테스트됩니다. (4) PREEMPT_RT 커널에서 실행하면 spinlock의 sleeping lock 변환이 올바른지 검증할 수 있습니다. (5) rcutorture, locktorture를 병행 실행하면 RCU와 잠금의 상호작용도 테스트됩니다.

kdump/crash

kdump는 커널 패닉 발생 시 시스템 메모리 덤프(vmcore)를 캡처하는 메커니즘입니다. kexec를 사용하여 미리 로드된 캡처 커널로 전환한 뒤, 크래시된 커널의 메모리를 디스크에 저장합니다. 저장된 vmcore는 crash 유틸리티로 사후 분석합니다.

kexec 시스템 콜 내부 구조

kexec는 부트로더를 거치지 않고 커널에서 직접 새 커널을 로드·실행하는 메커니즘입니다. 두 가지 시스템 콜이 존재합니다.

항목kexec_load (283)kexec_file_load (320)
도입Linux 2.6.13 (2005)Linux 3.17 (2014)
인터페이스커널/initrd를 raw segment로 전달fd(파일 디스크립터)로 전달
서명 검증불가 (바이너리 blob 전달)커널 내부에서 PE/bzImage 서명 검증 가능
Secure BootLOCK_DOWN_KERNEL_FORCE_INTEGRITY에서 차단서명 유효 시 허용
purgatory유저 공간에서 제공커널 내장 purgatory 사용
kexec-tools 옵션kexec -l / kexec -pkexec -l -s / kexec -p -s
/* kexec_load: segments 배열을 직접 전달 */
long kexec_load(
    unsigned long entry,           /* 새 커널 진입점 물리 주소 */
    unsigned long nr_segments,     /* segment 수 (최대 16) */
    struct kexec_segment *segments, /* 커널, initrd, cmdline 등 */
    unsigned long flags             /* KEXEC_ON_CRASH 등 */
);

/* kexec_file_load: 파일 기반 인터페이스 */
long kexec_file_load(
    int kernel_fd,                  /* 커널 이미지 fd */
    int initrd_fd,                  /* initrd fd (-1이면 없음) */
    unsigned long cmdline_len,
    const char *cmdline,
    unsigned long flags
);

/* flags 비트 */
#define KEXEC_ON_CRASH   0x00000001  /* 패닉 시 실행 (kdump용) */
#define KEXEC_PRESERVE_CONTEXT 0x00000002  /* 하이버네이션용 */
#define KEXEC_FILE_UNLOAD  0x00000008  /* 로드 해제 */
#define KEXEC_FILE_ON_CRASH 0x00000002  /* file_load 전용 crash 플래그 */
#define KEXEC_FILE_NO_INITRAMFS 0x00000004

Purgatory: 커널 전환 중간 단계

kexec 실행 시 현재 커널에서 새 커널로 바로 점프하지 않습니다. purgatory라는 중간 코드가 먼저 실행되어 메모리 무결성을 검증한 뒤 새 커널로 제어를 넘깁니다.

/* arch/x86/purgatory/purgatory.c - 핵심 흐름 */
void purgatory(void)
{
    /* 1. 로드된 세그먼트의 SHA-256 체크섬 검증 */
    if (verify_sha256_digest()) {
        /* 검증 실패: 무한 루프 (시스템 정지) */
        for(;;);
    }

    /* 2. 아키텍처별 초기화 (x86: GDT/IDT 설정, 페이징 비활성화 등) */
    setup_arch();

    /* 3. 새 커널 진입점으로 점프 */
    /*    x86_64: entry64.S에서 real mode → protected → long mode */
}

/* purgatory 위치: arch/x86/purgatory/
 *   entry64.S   - 진입점 (어셈블리)
 *   purgatory.c - SHA-256 검증
 *   setup-x86_64.S - CPU 상태 초기화
 *   sha256.c    - 해시 구현
 *
 * kexec_file_load는 커널 내장 purgatory 사용 (CONFIG_ARCH_HAS_KEXEC_PURGATORY)
 * kexec_load는 kexec-tools가 purgatory 바이너리를 유저 공간에서 전달
 */

kexec/kdump 동작 흐름

Phase 1: 부팅 시 준비 커널 부팅 crashkernel=256M 파싱 메모리 예약 memblock에서 256MB 분리 kdump 서비스 시작 kexec -p vmlinuz (캡처 커널 로드) 대기 정상 운영 물리 메모리 레이아웃 커널 코드/데이터 (0~16MB) crashkernel 예약 영역 캡처 커널 + initrd (256MB) 일반 메모리 (유저 공간 + 커널 할당 + 페이지 캐시) 이 영역이 vmcore로 캡처됨 Phase 2: 크래시 → 캡처 패닉 발생! panic() / Oops __crash_kexec() 다른 CPU 정지(NMI) machine_kexec() CPU 리셋/재설정 purgatory SHA-256 검증 캡처 커널 부팅 시작 Phase 3: 캡처 커널에서 vmcore 저장 캡처 커널 부팅 예약 영역에서 실행 /proc/vmcore ELF 형식으로 노출 makedumpfile 필터링 + 압축 vmcore 저장 로컬/NFS/SSH 시스템 재부팅 정상 커널 복귀 Phase 4: 사후 분석 crash 유틸리티 vmcore + vmlinux bt / ps / log 크래시 원인 분석 drgn / gdb 심층 메모리 분석 __crash_kexec() 내부 실행 순서 1. crash_setup_regs() 현재 CPU 레지스터 저장 (pt_regs) 2. crash_kexec_prepare_cpus() NMI로 다른 CPU 정지 각 CPU의 레지스터 저장 3. machine_crash_shutdown() IOAPIC/LAPIC 비활성화 HPET 타이머 정지 IOMMU 비활성화 4. machine_kexec() identity mapping 설정 purgatory 진입점 점프
ℹ️

kexec_file_load 권장: 최신 시스템에서는 kexec -s 옵션(kexec_file_load 시스템 콜)을 사용하는 것이 좋습니다. Secure Boot 환경에서 서명 검증을 커널 내부에서 수행하므로 호환성이 높고, 커널 내장 purgatory를 사용하여 유저 공간 purgatory 호환성 문제를 회피합니다. RHEL 8+, Ubuntu 20.04+에서는 기본적으로 kexec_file_load를 시도합니다.

kexec/kdump 커널 CONFIG 상세

CONFIG 옵션용도비고
CONFIG_KEXECkexec_load 시스템 콜 활성화기본 kexec 지원
CONFIG_KEXEC_FILEkexec_file_load 시스템 콜 활성화Secure Boot 호환, 서명 검증
CONFIG_CRASH_DUMP캡처 커널이 vmcore를 /proc/vmcore로 노출캡처 커널 빌드 시 필수
CONFIG_PROC_VMCORE/proc/vmcore 파일시스템 지원CRASH_DUMP와 함께 사용
CONFIG_KEXEC_SIGkexec_file_load에서 커널 서명 검증Secure Boot 필수
CONFIG_KEXEC_SIG_FORCE서명 없는 커널 kexec 차단보안 강화
CONFIG_KEXEC_BZIMAGE_VERIFY_SIGbzImage PE 서명 검증x86 전용
CONFIG_CRASH_HOTPLUGCPU/메모리 핫플러그 시 kdump 자동 업데이트6.5+ (RHEL 9.3+)
CONFIG_PROC_VMCORE_DEVICE_DUMP디바이스별 크래시 데이터 vmcore에 포함NIC, GPU 등 FW 덤프
CONFIG_ARCH_HAS_KEXEC_PURGATORY커널 내장 purgatory 사용x86_64, arm64
# 현재 커널의 kexec 관련 설정 확인
grep -E 'KEXEC|CRASH_DUMP|PROC_VMCORE' /boot/config-$(uname -r)
# 또는 실행 중인 커널에서
zcat /proc/config.gz | grep -E 'KEXEC|CRASH_DUMP|PROC_VMCORE'

# kexec 시스템 콜 지원 확인
cat /sys/kernel/kexec_loaded       # 일반 kexec 커널 로드 여부
cat /sys/kernel/kexec_crash_loaded # 크래시 캡처 커널 로드 여부
cat /sys/kernel/kexec_crash_size   # crashkernel 예약 크기 (바이트)

kdump 설정 (kexec)

# 1. 커널 설정
CONFIG_KEXEC=y
CONFIG_CRASH_DUMP=y
CONFIG_PROC_VMCORE=y

# 2. 커널 부팅 파라미터 (crashkernel 메모리 예약)
# GRUB: /etc/default/grub
GRUB_CMDLINE_LINUX="crashkernel=256M"
# 또는 자동 크기 조정: crashkernel=auto

# 3. kdump 서비스 설치 및 활성화
# Debian/Ubuntu:
apt install kdump-tools kexec-tools
systemctl enable kdump-tools

# RHEL/CentOS:
yum install kexec-tools
systemctl enable kdump

# 4. 크래시 커널 수동 로드
kexec -p /boot/vmlinuz-$(uname -r) \
    --initrd=/boot/initrd.img-$(uname -r) \
    --append="root=/dev/sda1 irqpoll maxcpus=1 reset_devices"

vmcore 캡처

# 패닉 트리거 (테스트용)
echo c > /proc/sysrq-trigger

# kdump 후 vmcore 저장 위치
# Debian: /var/crash/
# RHEL:   /var/crash/<timestamp>/
ls -la /var/crash/

# makedumpfile로 vmcore 압축/필터링
makedumpfile -l -d 31 /var/crash/vmcore /var/crash/vmcore.compressed
# -l: lzo 압축
# -d 31: 제로 페이지, 캐시, 유저 데이터, 프리 페이지 제외

# makedumpfile 덤프 레벨
# -d  1: 제로 페이지 제외
# -d  2: 캐시 페이지 제외
# -d  4: 캐시 프라이빗 제외
# -d  8: 유저 페이지 제외
# -d 16: 프리 페이지 제외
# -d 31: 위 모두 제외 (커널 데이터만 보존)

crash 유틸리티 사용법

# crash 실행
crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/vmcore

# 또는 직접 빌드한 vmlinux 사용
crash vmlinux /var/crash/vmcore

crash 주요 명령어

# bt (backtrace): 크래시 시점 콜 스택
crash> bt
PID: 1234  TASK: ffff8881234abcd0  CPU: 2  COMMAND: "test_prog"
 #0 [ffff88812aaaf000] machine_kexec at ffffffff81060c3a
 #1 [ffff88812aaaf048] __crash_kexec at ffffffff811556cc
 #2 [ffff88812aaaf108] panic at ffffffff81bb72e1
 #3 [ffff88812aaaf188] my_buggy_function at ffffffffa0001234 [my_module]
 #4 [ffff88812aaaf1c0] process_one_work at ffffffff810a8b2c
 ...

# 특정 PID의 백트레이스
crash> bt 1234

# 모든 CPU의 백트레이스
crash> bt -a

# ps: 프로세스 목록
crash> ps
   PID    PPID  CPU  TASK             ST  %MEM  COMM
      0       0   0  ffffffff81c11480  RU   0.0  swapper/0
      1       0   2  ffff888100123400  SL   0.1  systemd
   1234       1   2  ffff8881234abcd0  RU   0.0  test_prog
   ...

# log: 커널 로그 (dmesg)
crash> log | tail -50

# struct: 구조체 내용 확인
crash> struct task_struct.comm,pid ffff8881234abcd0
  comm = "test_prog"
  pid = 1234

# files: 프로세스의 열린 파일
crash> files 1234

# vm: 프로세스 가상 메모리 맵
crash> vm 1234

# kmem: 슬랩 정보
crash> kmem -s

# mod: 로드된 모듈
crash> mod

# rd (read): 메모리 읽기
crash> rd -64 0xffffffff81a00000 8

# dis (disassemble): 디스어셈블
crash> dis my_buggy_function
💡

vmlinux 보관: crash 분석에는 크래시 커널과 정확히 같은 빌드의 vmlinux 파일이 필요합니다. 커널을 빌드할 때마다 vmlinux를 보관하는 습관을 들이십시오. CONFIG_DEBUG_INFO=y가 설정되어야 심볼 정보가 포함됩니다.

crash 고급 분석 기법

# foreach: 모든 태스크에 대해 명령 반복 실행
crash> foreach bt              # 모든 프로세스 백트레이스
crash> foreach RU bt           # RUNNING 상태 프로세스만
crash> foreach UN bt           # UNINTERRUPTIBLE (D상태) 프로세스만
crash> foreach bt -l           # 소스 라인 포함 백트레이스
crash> foreach files           # 모든 프로세스의 열린 파일
crash> foreach task task_struct.comm,policy,prio
# 모든 프로세스의 이름, 스케줄링 정책, 우선순위

# foreach 필터링 옵션
crash> foreach k bt            # 커널 스레드만
crash> foreach u bt            # 유저 프로세스만
crash> foreach g bt            # 스레드 그룹 리더만

# search: 메모리에서 특정 패턴 검색
crash> search -k deadbeef      # 커널 메모리에서 0xdeadbeef 검색
crash> search -u cafebabe      # 유저 메모리에서 검색
crash> search -k -s ffff888100000000 -e ffff888200000000 12345678
# 특정 주소 범위에서 검색 (-s: start, -e: end)

# search로 slab 오염(poison) 패턴 찾기
crash> search -k 6b6b6b6b     # SLAB_POISON: 해제된 객체
crash> search -k 5a5a5a5a     # SLAB_RED_ZONE: 레드존 마커
crash> search -k a5a5a5a5     # POISON_INUSE: 사용 중 초기화

# sym: 심볼/주소 변환
crash> sym ffffffff81060c3a    # 주소 → 심볼 + 오프셋
crash> sym machine_kexec      # 심볼 → 주소
crash> sym -l machine_kexec   # 소스 라인 포함

# whatis: 타입 정보 조회
crash> whatis task_struct      # 구조체 정의 출력
crash> whatis -o task_struct   # 오프셋 포함 출력

# struct 고급 사용법
crash> struct task_struct -o   # 전체 멤버와 오프셋 출력
crash> struct task_struct.mm ffff8881234abcd0
crash> struct mm_struct.pgd <mm 주소>
# 체인 따라가기: task → mm → pgd

# list: 연결 리스트 순회
crash> list task_struct.tasks -s task_struct.comm,pid -H init_task
# init_task부터 tasks 리스트 순회하며 comm, pid 출력

crash> list module.list -s module.name -H modules
# 로드된 모든 모듈 이름 출력

# tree: RB트리/xarray 순회
crash> tree -t rbtree -s vm_area_struct.vm_start,vm_end \
       vm_area_struct.vm_rb -r <mm->mm_rb>
# 프로세스의 VMA RB트리 순회

# net: 네트워크 정보
crash> net                     # 네트워크 디바이스 목록
crash> net -s                  # 소켓 목록

# dev: 블록 디바이스 정보
crash> dev                     # 블록 디바이스
crash> dev -d                  # 디스크 I/O 통계

# timer: 활성 타이머 목록
crash> timer                   # 커널 타이머 목록

# irq: 인터럽트 통계
crash> irq -s                  # IRQ별 발생 횟수
crash> irq -a                  # IRQ affinity

# runq: Run queue 분석
crash> runq                    # 각 CPU의 런큐
crash> runq -t                 # 타임스탬프 포함

# waitq: 대기 큐 분석
crash> waitq <wait_queue_head 주소>
# 특정 waitqueue에서 대기 중인 태스크

crash에서 GDB 명령 활용

crash는 내부적으로 GDB를 임베드하고 있어 GDB 명령을 직접 사용할 수 있습니다. gdb 접두사로 GDB 명령에 접근합니다.

# GDB 명령 직접 사용
crash> gdb info registers      # 크래시 시점 레지스터
crash> gdb p jiffies           # 전역 변수 출력
crash> gdb p/x &init_task      # 변수 주소 (16진수)
crash> gdb x/20i ffffffff81060c3a  # 20개 명령어 디스어셈블
crash> gdb x/10gx 0xffff888100000000  # 메모리 8바이트 단위 출력

# GDB 매크로로 복잡한 데이터 구조 분석
crash> gdb p ((struct task_struct *)0xffff8881234abcd0)->se.vruntime
crash> gdb p ((struct super_block *)0xffff888105000000)->s_type->name

# extend: crash 확장 모듈 로드
crash> extend /usr/lib64/crash/extensions/dminfo.so
# device-mapper 정보 분석 확장

crash> extend /usr/lib64/crash/extensions/snap.so
# 스냅샷 관련 확장

# 사용 가능한 확장 목록
ls /usr/lib64/crash/extensions/

# alias: 자주 쓰는 명령 단축키
crash> alias dtask foreach UN bt
crash> dtask   # D상태 프로세스 백트레이스

# set: 디폴트 컨텍스트 변경
crash> set 1234                # PID 1234를 현재 컨텍스트로
crash> bt                      # PID 1234의 백트레이스
crash> struct task_struct      # PID 1234의 task_struct

# wr (writemem): 메모리 수정 (라이브 커널 디버깅 시)
# crash -w /dev/mem 로 쓰기 모드 실행 시 사용 가능
crash> wr <address> <value>

# 출력 리다이렉트
crash> bt > /tmp/backtrace.txt
crash> foreach bt >> /tmp/all_bt.txt

# repeat: 명령 반복 (라이브 디버깅 시)
crash> repeat -1 ps | grep RU   # 매 초 RUNNING 프로세스 확인

crash 분석 실전 시나리오

### 시나리오 1: NULL 포인터 역참조 분석 ###
crash> bt
# RIP이 모듈 함수 + 오프셋을 가리킴
# → dis로 해당 위치 디스어셈블
crash> dis -l my_func+0x28
# 소스 라인: ptr->member (ptr이 NULL)
# → 레지스터 확인 (RAX=0 이면 NULL deref)

### 시나리오 2: 슬랩 메모리 누수 추적 ###
crash> kmem -s
# 비정상적으로 큰 슬랩 캐시 식별
CACHE            OBJSIZE  ALLOCATED     TOTAL  SLABS  SSIZE  NAME
ffff888100abc000     256     985234    985300   3942    16k  my_cache
# → 누수 객체의 할당 출처 추적
crash> kmem -S my_cache | head -20

### 시나리오 3: D상태(TASK_UNINTERRUPTIBLE) 프로세스 분석 ###
crash> ps | grep UN
# D상태 프로세스 PID 확인
crash> bt <PID>
# 콜스택에서 어떤 락/IO에서 대기 중인지 확인
crash> waitq <waitqueue 주소>
# 해당 waitqueue에서 대기 중인 다른 프로세스 확인

### 시나리오 4: 스핀락 데드락 분석 ###
crash> bt -a
# 모든 CPU의 콜스택 확인 → 같은 락에서 스핀 중인 CPU 식별
crash> struct spinlock_t <lock 주소>
# 락 소유자 CPU/태스크 확인
crash> bt -c <CPU 번호>
# 락 소유자의 콜스택 → 왜 해제하지 않는지 분석

drgn - 프로그래머블 커널 디버거

drgn은 Python 기반의 프로그래머블 커널 디버거로, crash 유틸리티의 현대적 대안입니다. Meta(Facebook)에서 개발했으며, Python의 강력한 표현력으로 복잡한 커널 데이터 구조를 프로그래밍 방식으로 분석할 수 있습니다.

# drgn 설치
pip3 install drgn
# 또는 배포판 패키지
apt install python3-drgn  # Debian/Ubuntu
dnf install drgn          # Fedora

# vmcore 분석
drgn -c /var/crash/vmcore

# 라이브 커널 분석
sudo drgn -k

# 특정 vmlinux 지정
drgn -c /var/crash/vmcore -s vmlinux
# drgn 기본 사용법
import drgn

# 현재 태스크 정보
task = prog["init_task"]
print(task.comm)        # 프로세스 이름
print(task.pid)         # PID
print(task.mm.pgd)      # 페이지 테이블 주소

# 모든 프로세스 순회
from drgn.helpers.linux.pid import for_each_task
for task in for_each_task(prog):
    if task.state.value_() & 0x02:  # TASK_UNINTERRUPTIBLE
        print(f"D-state: {task.comm.string_().decode()} (PID {task.pid})")

# 모듈 목록
from drgn.helpers.linux.module import for_each_module
for mod in for_each_module(prog):
    print(mod.name.string_().decode())

# 슬랩 캐시 분석 (메모리 누수 추적)
from drgn.helpers.linux.slab import for_each_slab_cache, slab_cache_for_each_allocated_object
for cache in for_each_slab_cache(prog):
    name = cache.name.string_().decode()
    if "kmalloc-256" in name:
        count = sum(1 for _ in slab_cache_for_each_allocated_object(cache))
        print(f"{name}: {count} objects")

# 네트워크 소켓 분석
from drgn.helpers.linux.net import for_each_net
from drgn.helpers.linux.tcp import for_each_tcp_socket
for net in for_each_net(prog):
    for sk in for_each_tcp_socket(net):
        print(f"state={sk.__sk_common.skc_state.value_()}")

# 스택 트레이스 출력
import drgn
from drgn.helpers.linux.pid import find_task
task = find_task(prog, 1234)
print(prog.stack_trace(task))
💡

crash vs drgn 선택 기준:

  • crash: 대화형 분석, 빠른 상태 확인, 기존 확장 모듈 활용, Red Hat 지원 (sosreport 연계)
  • drgn: 프로그래밍 기반 복잡 분석, 대규모 데이터 집계, 커스텀 스크립트 자동화, 현대적 API
  • 두 도구 모두 vmcore와 라이브 커널을 지원합니다. 복잡한 메모리 누수 추적이나 통계 수집에는 drgn이, 빠른 크래시 분석에는 crash가 적합합니다.

커널 패닉 분석

커널 Oops는 심각한 오류 발생 시 출력되는 진단 메시지입니다. Oops가 반드시 패닉으로 이어지는 것은 아니지만(프로세스만 종료될 수 있음), 커널 상태가 손상될 수 있으므로 반드시 분석해야 합니다. panic_on_oops=1 설정을 통해 Oops 발생 시 즉시 패닉으로 전환하여 kdump를 유발할 수 있습니다.

Oops 메시지 구조 해석

# 전형적인 커널 Oops 메시지
BUG: unable to handle page fault for address: 0000000000001234
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
PGD 0 P4D 0
Oops: 0000 [#1] PREEMPT SMP NOPTI
CPU: 3 PID: 5678 Comm: test_prog Tainted: G        W   OE  6.1.0 #1
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009)
RIP: 0010:my_buggy_function+0x28/0x60 [my_module]
Code: 48 89 e5 41 54 53 48 8b 1f 48 85 db 74 1e 48 8b 43 08 ...
RSP: 0018:ffffc9000123fe00 EFLAGS: 00010286
RAX: 0000000000001234 RBX: ffff888101234500 RCX: 0000000000000000
RDX: 0000000000000001 RSI: ffff888101234600 RDI: ffff888101234500
RBP: ffffc9000123fe20 R08: 0000000000000000 R09: 0000000000000001
R10: 0000000000000000 R11: 0000000000000000 R12: ffff888101234700
R13: 0000000000000000 R14: ffff888101234800 R15: 0000000000000000
FS:  00007f1234567890(0000) GS:ffff88813fd80000(0000)
Call Trace:
 <TASK>
 caller_function+0x3c/0x80 [my_module]
 process_one_work+0x1e8/0x3c0
 worker_thread+0x50/0x3b0
 kthread+0xe9/0x110
 ret_from_fork+0x22/0x30
 </TASK>

스택 트레이스 읽는 방법

Oops 메시지의 핵심 정보를 해석하는 방법입니다.

addr2line, faddr2line 활용

# addr2line: 주소 → 소스 파일:라인 변환
addr2line -e vmlinux ffffffff81234567
# kernel/sched/core.c:3456

# 모듈의 경우 (상대 오프셋 사용)
addr2line -e drivers/net/my_module.ko 0x28

# faddr2line: 함수명+오프셋 → 소스 위치 (커널 스크립트)
./scripts/faddr2line vmlinux my_buggy_function+0x28
# my_buggy_function+0x28/0x60:
# my_source.c:42

# 모듈에 대해 faddr2line 사용
./scripts/faddr2line drivers/net/my_module.ko my_func+0x1c

# objdump으로 디스어셈블하여 확인
objdump -dS --start-address=0x0000 --stop-address=0x0060 \
    drivers/net/my_module.ko | less

# decode_stacktrace.sh: Oops 전체를 소스 위치로 변환
./scripts/decode_stacktrace.sh vmlinux /path/to/modules < oops.txt

BUG/WARN 매크로

/* BUG(): 무조건 Oops 발생 (프로세스 종료) */
if (impossible_condition)
    BUG();

/* BUG_ON(): 조건이 참이면 Oops */
BUG_ON(ptr == NULL);

/* WARN(): 경고 메시지 + 스택 트레이스 (실행은 계속) */
WARN(size > MAX_SIZE, "size %zu exceeds max %zu\\n", size, MAX_SIZE);

/* WARN_ON(): 조건이 참이면 경고 */
WARN_ON(irqs_disabled());

/* WARN_ON_ONCE(): 최초 한 번만 경고 */
WARN_ON_ONCE(in_interrupt());

/* WARN_ONCE(): 메시지 포함, 한 번만 */
WARN_ONCE(ret < 0, "unexpected return: %d\\n", ret);

/* BUILD_BUG_ON(): 컴파일 타임 검사 */
BUILD_BUG_ON(sizeof(struct my_data) != 64);
☢️

주의: BUG()는 커널 상태를 손상시킬 수 있으므로 꼭 필요한 경우에만 사용하십시오. 복구 가능한 오류에는 에러 코드를 반환하고, 이상 상태 알림에는 WARN_ON_ONCE()를 사용하는 것이 좋습니다. Linus Torvalds는 불필요한 BUG() 사용을 강하게 비판합니다.

bpftrace

bpftrace는 eBPF 기반의 고수준 동적 트레이싱 도구입니다. awk와 유사한 간결한 문법으로 커널과 유저 공간의 이벤트를 트레이싱할 수 있습니다. DTrace/SystemTap의 Linux 대안으로, 프로덕션 환경에서도 낮은 오버헤드로 사용할 수 있습니다. 프로브 종류(kprobe·kretprobe·tracepoint·uprobe·usdt 등)와 원라이너 예제·히스토그램 집계에 대한 상세 내용은 ftrace — bpftrace 심화를 참고하세요.

성능 모니터링 스크립트

#!/usr/bin/env bpftrace
/* runqlat.bt - 스케줄러 큐 대기 시간 측정 */

tracepoint:sched:sched_wakeup,
tracepoint:sched:sched_wakeup_new
{
    @qtime[args->pid] = nsecs;
}

tracepoint:sched:sched_switch
{
    $ns = @qtime[args->next_pid];
    if ($ns) {
        @usecs = hist((nsecs - $ns) / 1000);
    }
    delete(@qtime[args->next_pid]);
}

interval:s:5
{
    time();
    print(@usecs);
    clear(@usecs);
}

END
{
    clear(@qtime);
}
ℹ️

bpftrace 요구사항: 커널 4.9 이상, CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y 필요. BTF(BPF Type Format) 지원(CONFIG_DEBUG_INFO_BTF=y)이 있으면 헤더 파일 없이도 커널 구조체에 접근할 수 있어 훨씬 편리합니다.

/proc /sys 활용

커널은 /proc/sys 가상 파일시스템을 통해 방대한 런타임 정보를 제공합니다. 별도의 도구 설치 없이 시스템 상태를 진단할 수 있는 강력한 인터페이스입니다.

/proc/slabinfo

슬랩 할당자의 캐시별 사용 현황을 확인합니다. 메모리 누수 조사에 유용합니다.

# 슬랩 캐시 통계 확인
cat /proc/slabinfo | head -5
# slabinfo - version: 2.1
# # name     <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
# kmalloc-256    12345    13000    256    16    1
# task_struct      789     1024   6720     4    8

# slabtop: 실시간 모니터링 (top과 유사)
slabtop -s c  # 캐시 크기순 정렬

# 특정 캐시 사용량 변화 관찰 (메모리 누수 탐지)
watch -d 'grep kmalloc-256 /proc/slabinfo'

/proc/vmstat

# 가상 메모리 통계
cat /proc/vmstat | grep -E "pgfault|pgmajfault|oom_kill|pswp"
# pgfault 1234567        # 전체 페이지 폴트 수
# pgmajfault 123         # 메이저 페이지 폴트 (디스크 I/O 발생)
# pswpin 456             # 스왑 인 페이지 수
# pswpout 789            # 스왑 아웃 페이지 수
# oom_kill 0             # OOM killer 실행 횟수

# 실시간 변화 모니터링
watch -d 'cat /proc/vmstat | grep -E "pgfault|pgmajfault|oom"'

/proc/buddyinfo

# 버디 시스템 프리 페이지 분포 (메모리 단편화 확인)
cat /proc/buddyinfo
# Node 0, zone   DMA32    456  234  123   67   34   12    5    2    1    0    0
# Node 0, zone  Normal  12345 5678 2345  890  456  234  123   56   23    8    2
#                       (4K) (8K) (16K)(32K)(64K)(128K)(256K)(512K)(1M)(2M)(4M)

# 큰 order(오른쪽)의 값이 0이면 메모리 단편화가 심한 상태
# /proc/pagetypeinfo로 더 상세한 정보 확인 가능

/sys/kernel/debug/ 주요 항목

# debugfs 마운트 (보통 자동 마운트됨)
mount -t debugfs debugfs /sys/kernel/debug

# 주요 디버그 인터페이스
ls /sys/kernel/debug/
# tracing/          # ftrace 인터페이스
# dynamic_debug/    # 동적 디버그 제어
# kprobes/          # kprobe 등록 정보
# sleep_time        # 슬립 관련 정보
# gpio              # GPIO 상태
# regmap/           # 레지스터 맵
# block/            # 블록 디바이스 정보
# dma_buf/          # DMA 버퍼 정보
# ieee80211/        # WiFi 디버그 정보

# 등록된 kprobe 확인
cat /sys/kernel/debug/kprobes/list

# 인터럽트 통계
cat /proc/interrupts

# softirq 통계
cat /proc/softirqs

# 타이머 목록
cat /proc/timer_list | head -40
💡

/proc/lock_stat: CONFIG_LOCK_STAT=y가 활성화된 커널에서 사용 가능합니다. 각 잠금의 경합 횟수, 대기 시간, 보유 시간 등을 보여줍니다. 성능 병목이 잠금 경합인지 확인할 때 유용합니다. echo 0 > /proc/lock_stat으로 카운터를 초기화한 뒤 측정하면 정확합니다.

실전 팁

재현 기법

간헐적인 커널 버그를 재현하는 것은 디버깅에서 가장 어려운 부분입니다. 다음 도구와 기법을 활용하면 재현 확률을 높일 수 있습니다.

# stress-ng: 다양한 리소스에 부하 발생
# CPU 부하
stress-ng --cpu 16 --timeout 60s

# 메모리 부하 (mmap, brk, malloc 혼합)
stress-ng --vm 4 --vm-bytes 2G --vm-method all --timeout 60s

# I/O 부하
stress-ng --io 4 --hdd 2 --timeout 60s

# 레이스 컨디션 유도: 작은 sleep 삽입
# 의심 지점에 임시로 mdelay() 또는 udelay() 삽입

# 메모리 압박 상황 유도
echo 1 > /proc/sys/vm/drop_caches     # 페이지 캐시 해제
stress-ng --vm 8 --vm-bytes 90% --timeout 120s

# fault injection으로 메모리 할당 실패 유도
echo 1 > /sys/kernel/debug/failslab/probability
echo 10 > /sys/kernel/debug/failslab/interval
echo 100 > /sys/kernel/debug/failslab/times

git bisect 활용

특정 커밋에서 버그가 도입되었는지 찾을 때 git bisect는 매우 효과적입니다. 이진 탐색으로 수천 개 커밋 중에서도 빠르게 범인 커밋을 찾을 수 있습니다.

# git bisect 시작
cd /path/to/linux
git bisect start
git bisect bad                 # 현재(버그 있는) 커밋
git bisect good v6.0           # 버그 없는 커밋

# 커널 빌드 & 테스트 후 결과 입력
make -j$(nproc)
# ... 테스트 ...
git bisect good                # 또는 git bisect bad

# 자동 bisect (스크립트 사용)
git bisect run ./test_script.sh
# test_script.sh: 종료 코드 0=good, 1-124=bad, 125=skip

# bisect 결과 확인
git bisect log

# bisect 종료
git bisect reset

시리얼 콘솔 설정

커널 패닉 시 그래픽 콘솔은 정보를 놓치기 쉽습니다. 시리얼 콘솔은 패닉 메시지를 빠짐없이 캡처할 수 있습니다.

# GRUB에서 시리얼 콘솔 설정 (/etc/default/grub)
GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200n8"
GRUB_TERMINAL="serial console"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"

# systemd에서 시리얼 getty 활성화
systemctl enable serial-getty@ttyS0.service

# 호스트에서 시리얼 로그 캡처
screen /dev/ttyUSB0 115200
# 또는 minicom
minicom -D /dev/ttyUSB0 -b 115200 -C serial_log.txt

# QEMU에서 시리얼 콘솔
qemu-system-x86_64 -kernel bzImage -append "console=ttyS0" \
    -serial stdio -nographic

로그 분석 기법

# dmesg 분석 (에러/경고만 필터링)
dmesg --level=err,warn --time-format iso

# 부팅 이후 특정 시간대 로그
dmesg --since="2024-01-15 10:00:00"

# journalctl로 커널 로그 필터링
journalctl -k -p err                   # 에러 이상만
journalctl -k --since="1 hour ago"     # 최근 1시간
journalctl -k -o verbose               # 상세 출력

# 이전 부팅의 커널 로그 (persistent journal 필요)
journalctl -k -b -1                    # 직전 부팅
journalctl --list-boots                # 부팅 기록 목록

# Oops/BUG/WARNING 패턴 검색
dmesg | grep -E "BUG|WARNING|Oops|Call Trace|RIP:"

QEMU 디버깅 환경

QEMU는 커널 개발과 디버깅에 최적의 환경을 제공합니다. 빠른 부팅, 스냅샷, GDB 연결, 다양한 디바이스 에뮬레이션을 활용할 수 있습니다.

# 최소 루트 파일시스템 생성 (busybox)
mkdir -p rootfs/{bin,sbin,etc,proc,sys,dev,tmp}
cp busybox rootfs/bin/
cd rootfs/bin && ln -s busybox sh && cd ../..

# init 스크립트
cat > rootfs/init <<'INITEOF'
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t debugfs debugfs /sys/kernel/debug
echo "Boot OK"
exec /bin/sh
INITEOF
chmod +x rootfs/init

# initramfs 생성
cd rootfs && find . | cpio -o -H newc | gzip > ../initramfs.gz && cd ..

# QEMU로 커널 부팅 (GDB 디버깅 가능)
qemu-system-x86_64 \
    -kernel arch/x86/boot/bzImage \
    -initrd initramfs.gz \
    -append "console=ttyS0 nokaslr" \
    -nographic \
    -s -S \           # -s: gdbserver :1234, -S: 시작 시 정지
    -m 1G \
    -smp 4 \
    -enable-kvm

# 다른 터미널에서 GDB 연결
gdb vmlinux
(gdb) target remote :1234
(gdb) hbreak start_kernel      # 하드웨어 브레이크포인트
(gdb) continue
💡

virtme-ng: 현재 호스트의 파일시스템을 그대로 QEMU 게스트에서 사용할 수 있는 도구입니다. virtme-ng --kdir /path/to/linux 한 줄로 커널을 빌드하고 부팅할 수 있어 빠른 개발-테스트 사이클에 매우 유용합니다.

커널 디버깅과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.