크래시 분석 심화

운영 중 발생하는 커널 장애를 근거 기반으로 분석하는 절차를 정리합니다. panic/oops 로그와 call trace 프레임 해석, `crashkernel`/kdump 설정 및 vmcore 수집, `crash` 도구로 태스크·메모리·락 상태 확인, softlockup/hardlockup/RCU stall 구분, WARN/BUG의 위험도 평가, pstore/ramoops/SysRq를 통한 증거 확보, 재현 시나리오 구성과 패치 검증까지 실무형 체크리스트로 상세히 다룹니다.

전제 조건: 커널 디버깅RCU 문서를 먼저 읽으세요. 크래시 분석은 스택/레지스터/로그를 교차 검증하는 작업이므로, 패닉 직전 이벤트와 동시성 맥락을 함께 읽어야 정확도가 올라갑니다.
일상 비유: 이 주제는 사고 블랙박스 복기와 비슷합니다. 충돌 직전 몇 초의 기록이 핵심 단서가 되듯이, panic 전후 trace와 call trace를 시간축으로 정렬해 보는 습관이 중요합니다.

핵심 요약

  • 증거 우선 — 패닉 직후 로그/콘솔/vmcore를 먼저 확보해야 합니다.
  • 분류 우선 — Oops, softlockup, hardlockup, hung task를 먼저 구분해야 분석 경로가 정해집니다.
  • 재현성 확보 — 동일 워크로드/커널/설정에서 재현 가능한지 확인해야 합니다.
  • 운영 안전성 — 프로덕션에서는 패닉 정책과 자동 재부팅 정책을 분리해 설계해야 합니다.
  • 사후 검증 — 패치 후 동일 시나리오에서 재발 여부를 반드시 확인해야 합니다.

단계별 이해

  1. 증거 수집
    시리얼 콘솔, pstore, vmcore 확보 상태를 먼저 점검합니다.
  2. 1차 분류
    메시지 패턴으로 lockup/oops/panic 유형을 분리합니다.
  3. 심층 분석
    crash, Call Trace, taint 플래그로 원인 가설을 좁힙니다.
  4. 재현/검증
    수정 후 같은 조건에서 재현 불가를 확인하고 운영 설정을 재점검합니다.
예제 읽기 가이드: 이 문서는 개념 설명용 의사코드와 실행 가능한 실습 예제를 함께 제공합니다. 코드 주석의 개념 예시는 내부 동작 이해 목적이며, 실습 예제는 실제 점검/복구 절차 재현 목적입니다.
관련 표준: ELF Core File Format, DWARF Debugging Format — 코어 덤프 및 디버그 정보 표준 형식입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 페이지: 기본 디버깅 도구(printk, KGDB, ftrace, perf, 새니타이저, lockdep, kdump, bpftrace)는 디버깅 & 트러블슈팅 페이지를 참고하세요.

크래시 유형 분류 가이드

커널 크래시는 증상에 따라 여러 유형으로 분류됩니다. 정확한 분류는 올바른 분석 경로와 해결 전략을 결정하는 첫 번째 단계입니다.

커널 크래시 유형 분류 플로차트 크래시 증상 확인 시스템 응답 가능? NO Hardlockup 완전 멈춤 (NMI 무응답) 증거: 콘솔 멈춤 원인: 인터럽트 비활성화 도구: NMI watchdog, SysRq YES Kernel Panic 발생? YES Kernel Panic BUG()/panic() 호출 증거: "Kernel panic" 메시지 원인: 치명적 일관성 위반 도구: kdump, crash, pstore NO Oops 메시지 있음? YES Oops (복구 가능) NULL deref, GPF, Page Fault 증거: "BUG:", "Oops:" 메시지 원인: 프로세스별 오류 도구: Call Trace, KASAN panic_on_oops=0이면 계속 실행 NO 응답 지연 메시지? YES Softlockup 스케줄링 지연 (20초+) 증거: "soft lockup" 메시지 원인: 긴 루프, 선점 불가 도구: ftrace, perf 시스템은 계속 실행됨 NO RCU Stall RCU 대기 시간 초과 Hung Task I/O 대기 120초+ WARN_ON 경고 (계속 실행) 분류 우선순위 1. 콘솔 메시지 확인 → 2. dmesg 패턴 매칭 → 3. pstore/vmcore 확보 → 4. 유형별 도구 선택
그림: 커널 크래시 유형 분류 플로차트 - 증상별 분석 경로
유형 심각도 시스템 영향 주요 키워드 우선 조치
Hardlockup 최고 완전 멈춤 NMI watchdog NMI 스택 확보, 하드웨어 점검
Kernel Panic 매우 높음 즉시 중단 Kernel panic, BUG() kdump 수집, Call Trace 분석
Oops 높음 프로세스 종료 Oops:, BUG:, NULL pointer RIP 위치 특정, KASAN 재현
Softlockup 중간 응답 지연 soft lockup, stuck for ftrace로 함수 추적
Hung Task 중간 프로세스 블록 hung_task, blocked for I/O 스택 확인, 락 대기 분석
RCU Stall 중간 GC 지연 rcu_sched detected stalls 긴 임계 구역 찾기
WARN_ON 낮음 계속 실행 WARNING:, WARN_ON 조건식 검토, 재현 환경 구축

crashkernel 심화 설정

crashkernel 파라미터는 kdump 캡처 커널이 사용할 메모리를 부팅 시 예약합니다. 올바른 크기 설정은 kdump의 안정성과 시스템 메모리 효율의 균형점에 있습니다.

crashkernel 메모리 크기 결정

파라미터 형식예시설명
고정 크기crashkernel=256M항상 256MB 예약. 가장 단순하고 예측 가능
자동crashkernel=auto시스템 메모리에 따라 자동 결정 (배포판 의존)
범위 기반crashkernel=512M-2G:64M,2G-:256MRAM 512M~2G→64MB 예약, 2G 이상→256MB 예약
오프셋 지정crashkernel=256M@16M물리주소 16MB부터 시작 (레거시 시스템)
high/low 분리crashkernel=256M,high crashkernel=72M,low4GB 이상/이하 영역 분리 예약 (UEFI/대용량 서버)
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# 현재 crashkernel 예약 상태 확인
cat /proc/iomem | grep -i crash
#   2c000000-3bffffff : Crash kernel

# 예약된 메모리 크기 확인
dmesg | grep -i crashkernel
# [    0.000000] Reserving 256MB of memory at 704MB for crashkernel

# kdump 서비스 상태 확인
systemctl status kdump       # RHEL/CentOS
systemctl status kdump-tools # Debian/Ubuntu
kdumpctl status              # RHEL 전용

# 크래시 커널 로드 확인
cat /sys/kernel/kexec_crash_loaded
# 1 = 로드됨, 0 = 미로드

# 크래시 커널이 사용할 크기 추정 (테스트 필요)
# 최소 권장: 기본 커널 + initrd + makedumpfile + 여유
# - 커널 이미지: ~30-50MB
# - initrd:     ~50-100MB
# - makedumpfile 동작 메모리: ~64MB
# - 디바이스 드라이버: 가변
# 보수적 권장: 시스템 RAM 기준
#   RAM ≤ 4GB:   128M
#   RAM ≤ 64GB:  256M
#   RAM ≤ 1TB:   512M
#   RAM > 1TB:   1G 이상 (네트워크 덤프 시 더 필요)
⚠️

crashkernel 설정 시 주의사항:

  • crashkernel=auto는 배포판마다 동작이 다릅니다. RHEL은 RAM 기반 테이블을 사용하지만, 일부 커널에서는 지원하지 않을 수 있습니다.
  • UEFI 시스템에서는 crashkernel=,high/,low 조합이 필요할 수 있습니다. low 영역은 최소 72MB(swiotlb)가 필요합니다.
  • 메모리가 부족하면 캡처 커널이 OOM으로 실패합니다. 반드시 테스트(echo c > /proc/sysrq-trigger)로 검증하십시오.
  • NUMA 시스템에서 crashkernel 메모리는 Node 0에 예약됩니다. Node 0의 메모리가 부족하면 예약에 실패할 수 있습니다.

kdump 생명주기 타임라인

kdump는 커널 크래시 발생부터 분석 완료까지 여러 단계를 거칩니다. 각 단계의 동작과 시간 순서를 이해하면 문제 발생 시 신속하게 대응할 수 있습니다.

kdump 생명주기 타임라인 부팅 시 분석 완료 0. 부팅 초기화 crashkernel 메모리 예약 (grub 설정) 256MB @ 0x2c000000 kexec_load() 호출 1. 정상 실행 프로덕션 커널 워크로드 실행 중 kexec_crash_loaded=1 2. 크래시 발생 panic() 호출 Call Trace 출력 레지스터 덤프 panic_timeout 대기 → crash_kexec() 진입 3. Kexec 전환 기존 커널 중지 캡처 커널 부팅 crashkernel 영역 사용 메모리 보존 (oldmem) 4. 덤프 수집 makedumpfile 실행 /proc/vmcore 읽기 압축 & 필터링 → /var/crash/*.vmcore 시간: 수 분 ~ 수십 분 5. 시스템 재부팅 프로덕션 커널 정상 부팅 자동 재부팅 설정 (panic_timeout=0) 6. 사후 분석 단계 (오프라인) crash 유틸리티 분석 $ crash vmlinux vmcore crash> bt (백트레이스) crash> log (커널 로그) crash> ps (프로세스) crash> files (파일) crash> struct (구조체) crash> dis (디스어셈블) crash> kmem (메모리) crash> mod (모듈) crash> foreach bt (전체) 자동화 스크립트 decode_stacktrace.sh Call Trace 자동 변환 faddr2line RIP 주소 → 소스 라인 로그 기반 분석 pstore/ramoops /sys/fs/pstore/dmesg-* 시리얼 콘솔 로그 패닉 전후 메시지 근본 원인 분석 Git blame/log 관련 커밋 조사 재현 환경 구성 KASAN/lockdep 활성화 최종 단계 패치 작성 → 테스트 → 메일링리스트 제출 → 백포트 → 검증 동일 조건에서 재발 방지 확인
그림: kdump 생명주기 타임라인 - 크래시부터 분석 완료까지 전체 흐름
💡

kdump 동작 시간 특성:

  • 단계 0-3 (부팅~kexec): 수 초 이내 (즉각)
  • 단계 4 (덤프 수집): 가장 시간 소모 — 시스템 RAM 크기와 압축 레벨에 비례
    • 4GB RAM: ~2-5분
    • 64GB RAM: ~10-20분
    • 256GB+ RAM: ~30-60분+ (네트워크 덤프 시 더 길어짐)
  • 단계 5 (재부팅): 정상 부팅 시간과 동일
  • 단계 6 (분석): 사람이 수행 — 수 시간 ~ 수 일

프로덕션 고려사항: 덤프 수집 중 서비스 다운타임 발생. makedumpfile -d 31로 불필요 페이지 제외하여 시간 단축 가능.

kdump 설정 심화

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# RHEL/CentOS: /etc/kdump.conf 주요 설정
path /var/crash                    # 덤프 저장 경로
core_collector makedumpfile -l --message-level 7 -d 31
# -l: lzo 압축 (빠름)
# -d 31: 제로/캐시/유저/프리 페이지 제외
# --message-level 7: 진행 상황 출력

# 네트워크 덤프 (NFS)
nfs my-server.example.com:/export/crash
# 네트워크 덤프 (SSH)
ssh user@my-server.example.com
sshkey /root/.ssh/kdump_id_rsa

# kdump 실패 시 동작 설정
default reboot                     # dump_to_rootfs, halt, poweroff, shell
failure_action reboot              # 덤프 실패 시 재부팅

# 특정 디스크에 직접 덤프
raw /dev/sda3
# 또는 ext4/xfs 파티션
ext4 /dev/sda3
path /crash

# 덤프할 메모리 필터링 (makedumpfile -d 플래그)
# Bit 0 (1):  제로 페이지 제외
# Bit 1 (2):  캐시 페이지 제외
# Bit 2 (4):  캐시 프라이빗 제외
# Bit 3 (8):  유저 페이지 제외
# Bit 4 (16): 프리 페이지 제외
# Bit 5 (32): Hugetlb 프라이빗 제외 (커널 6.0+)
# 일반적: -d 31 (커널 데이터만 보존)
# 상세 분석: -d 1 (유저 메모리 포함)

# Debian/Ubuntu: /etc/default/kdump-tools
USE_KDUMP=1
KDUMP_SYSCTL="kernel.panic_on_oops=1"
KDUMP_COREDIR="/var/crash"
KDUMP_CMDLINE_APPEND="irqpoll nr_cpus=1 reset_devices"

kdump 운영 시 고려사항

☢️

kdump 실패 주요 원인과 대처:

  • 캡처 커널 메모리 부족: crashkernel 크기 증가. 특히 많은 드라이버가 로드된 서버
  • 디스크 공간 부족: vmcore는 수 GB~수십 GB. 로테이션 설정 필수
  • 네트워크 드라이버 미동작: 캡처 커널에서 NIC 리셋 실패 → 로컬 덤프로 fallback
  • IOMMU/DMA 초기화 실패: reset_devices 부팅 파라미터 필요
  • 암호화 디스크: 캡처 커널에서 LUKS 복호화 불가 → 비암호화 파티션 사용
  • Secure Boot: 캡처 커널도 서명 필요. 서명 안 된 커널은 kexec 거부
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# kdump 동작 테스트 (주의: 시스템 크래시 발생!)
# 반드시 비프로덕션 환경에서 테스트

# SysRq로 강제 패닉
echo 1 > /proc/sys/kernel/sysrq
echo c > /proc/sysrq-trigger

# 또는 직접 panic() 호출 테스트 모듈
# echo 1 > /sys/kernel/debug/provoke-crash/type  (CONFIG_LKDTM)

# vmcore 생성 후 검증
crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/*/vmcore
crash> sys            # 시스템 정보 확인
crash> bt             # 크래시 백트레이스
crash> log            # 커널 로그

# makedumpfile 검증
makedumpfile --check-params -x vmlinux /var/crash/vmcore

makedumpfile 심화

makedumpfile은 vmcore에서 불필요한 페이지를 제거하고 압축하여 덤프 크기를 대폭 줄입니다. 대용량 서버(수백 GB~수 TB RAM)에서는 사실상 표준 도구로 사용됩니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# makedumpfile 압축 알고리즘 비교
# -l: lzo 압축 (기본 권장, 빠른 압축/해제)
# -c: zlib 압축 (높은 압축률, 느림)
# -p: snappy 압축 (가장 빠름, 낮은 압축률)
# --zstd: zstd 압축 (makedumpfile 1.7.0+, zlib급 압축률 + lzo급 속도)

# 압축 알고리즘별 성능 비교 (256GB RAM 서버 기준 예시)
# 알고리즘   덤프 크기   덤프 시간   해제 시간
# 없음       ~12GB      ~90초      -
# lzo (-l)   ~4GB       ~100초     ~30초
# zlib (-c)  ~3GB       ~180초     ~50초
# snappy(-p) ~5GB       ~95초      ~25초
# zstd       ~3.2GB     ~110초     ~35초

# 덤프 레벨별 필터링 효과
makedumpfile -l -d 31 /proc/vmcore /var/crash/vmcore
# -d 31 (0b11111): 제로+캐시+프라이빗캐시+유저+프리 제외
# 일반적으로 원본의 1~5% 수준으로 축소

makedumpfile -l -d 1 /proc/vmcore /var/crash/vmcore
# -d 1: 제로 페이지만 제외 (유저 메모리 포함 - 상세 분석용)

# 덤프 전 예상 크기 확인 (dry-run)
makedumpfile --dry-run -l -d 31 /proc/vmcore 2>&1 | grep "Total size"
# 실제 IO 없이 필터링 결과 크기만 계산

# vmcoreinfo: makedumpfile이 사용하는 커널 메타데이터
# 크래시 커널의 메모리 레이아웃, 심볼 오프셋 등 포함
cat /sys/kernel/vmcoreinfo
# OSRELEASE=6.1.0
# PAGESIZE=4096
# SYMBOL(init_uts_ns)=ffffffff...
# OFFSET(task_struct.pid)=...

# makedumpfile -F: flattened 형식 (파이프/네트워크 전송용)
makedumpfile -l -d 31 -F /proc/vmcore \
    | ssh user@server "cat > /crash/vmcore.flat"
# 수신 측에서 복원:
makedumpfile -R /crash/vmcore < /crash/vmcore.flat

# split: 대용량 vmcore를 여러 파일로 분할 덤프
makedumpfile -l -d 31 --split-dumpfile=5 /proc/vmcore \
    /var/crash/vmcore.{1,2,3,4,5}
# 멀티코어 병렬 덤프 (makedumpfile 1.7.0+)
makedumpfile -l -d 31 --num-threads=4 /proc/vmcore /var/crash/vmcore

# vmcore에서 특정 메모리 영역 추출
makedumpfile --dump-dmesg /var/crash/vmcore /var/crash/dmesg.txt
# vmcore를 열지 않고 dmesg만 빠르게 추출

kdump 네트워크 덤프 심화

대용량 서버에서는 로컬 디스크 대신 네트워크를 통해 vmcore를 원격 저장하는 것이 일반적입니다. NFS와 SSH 두 가지 방식을 지원합니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
### NFS 덤프 설정 ###
# 서버 측: NFS export 설정
# /etc/exports:
/export/crash  10.0.0.0/8(rw,sync,no_root_squash)
exportfs -ra

# 클라이언트 측: /etc/kdump.conf (RHEL)
nfs 10.0.0.100:/export/crash
path /
core_collector makedumpfile -l -d 31 -F --message-level 7

### SSH 덤프 설정 ###
# 1. kdump 전용 SSH 키 생성 (패스프레이즈 없음)
ssh-keygen -t ed25519 -f /root/.ssh/kdump_id_ed25519 -N ""

# 2. 원격 서버에 공개키 등록
ssh-copy-id -i /root/.ssh/kdump_id_ed25519.pub user@crash-server

# 3. /etc/kdump.conf 설정
ssh user@crash-server
sshkey /root/.ssh/kdump_id_ed25519
path /var/crash/%HOST
# %HOST: 호스트명으로 자동 치환
core_collector makedumpfile -l -d 31 -F --message-level 7
# -F (flattened) 필수: SSH 파이프를 통한 전송

# 4. kdump 서비스가 캡처 커널의 initrd에 SSH 키를 포함하도록 재빌드
kdumpctl rebuild   # RHEL
kdump-config rebuild # Debian/Ubuntu

### 네트워크 덤프 시 주의사항 ###
# 1. 캡처 커널에서 네트워크 드라이버가 동작해야 함
#    - 커스텀 NIC 드라이버는 initrd에 포함 필요
#    - RHEL: /etc/kdump.conf에 extra_modules 지정
extra_modules bonding ixgbe mlx5_core
#    - dracut: dracut_args --add-drivers "ixgbe mlx5_core"

# 2. 본딩/팀 인터페이스: 캡처 커널에서 단일 NIC로 폴백
#    kdump는 첫 번째 슬레이브 인터페이스 사용

# 3. VLAN/브리지: kdump에서 자동 설정 (RHEL 8+)
#    복잡한 네트워크 구성은 kdumpctl showmem으로 검증

# 4. 대역폭: 256GB 서버 → -d 31로 ~5GB → 1Gbps에서 ~40초
#    10Gbps NIC 환경에서는 ~4초

가상 환경에서의 kdump

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
### KVM/QEMU 게스트에서 kdump ###
# 게스트 내부에서 일반적인 kdump 설정과 동일
# 추가 고려사항:

# 1. virtio 드라이버: 캡처 커널에서 필수
#    CONFIG_VIRTIO_BLK, CONFIG_VIRTIO_NET, CONFIG_VIRTIO_PCI
#    initrd에 자동 포함 (dracut --add-drivers virtio_blk)

# 2. crashkernel 메모리: 가상머신은 물리 서버보다 적게 필요
#    게스트 4GB RAM → crashkernel=128M 충분

# 3. pvpanic: 게스트 패닉을 호스트에 알림
#    QEMU: -device pvpanic
#    게스트 커널: CONFIG_PVPANIC
#    libvirt XML:
#    <panic model='pvpanic'/>

### 호스트에서 게스트 메모리 덤프 ###
# virsh dump: 호스트에서 직접 게스트 메모리 덤프
virsh dump <domain> /var/crash/guest-vmcore --memory-only
# --memory-only: ELF 형식 (crash 도구 호환)
# 기본: libvirt 자체 형식 (비호환)

# crash로 게스트 메모리 분석
crash <게스트의 vmlinux> /var/crash/guest-vmcore

# QEMU monitor에서 직접 덤프
# (Ctrl+Alt+2로 monitor 진입)
# dump-guest-memory -E /tmp/guest.elf
# -E: ELF 형식, -z: zlib 압축

### 컨테이너 환경 ###
# 컨테이너는 호스트 커널을 공유하므로 별도의 kdump 불필요
# 호스트에서 kdump 설정 → 모든 컨테이너의 크래시 포착
# Kubernetes: DaemonSet으로 kdump 설정 배포
#   - crashkernel= 부트 파라미터는 노드 수준에서 설정
#   - vmcore에서 컨테이너 식별: cgroup 경로 확인
crash> struct task_struct.cgroups <task 주소>
# → cgroup 경로에서 pod/container ID 추출

kdump 트러블슈팅

kdump 자체가 실패하면 vmcore를 얻을 수 없으므로, 사전에 반드시 테스트하고 문제를 해결해야 합니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
### kdump 실패 원인 진단 ###

# 1. 캡처 커널이 로드되었는지 확인
cat /sys/kernel/kexec_crash_loaded
# 0이면 로드 실패 → systemctl status kdump / journalctl -u kdump

# 2. crashkernel 메모리 예약 확인
dmesg | grep -i "crashkernel\|crash kernel\|reserving"
cat /proc/iomem | grep -i crash
# "Crash kernel" 영역이 없으면 예약 실패
# 원인: crashkernel= 파라미터 누락, 메모리 부족, 커널 미지원

# 3. kexec 로드 실패 디버깅
kexec -p -s --debug /boot/vmlinuz-$(uname -r) \
    --initrd=/boot/initrd.img-$(uname -r) \
    --append="root=/dev/sda1 irqpoll nr_cpus=1 reset_devices"
# --debug: 상세 로그 출력
# Secure Boot 관련 오류: kexec_file_load 사용 (-s 옵션)

# 4. 캡처 커널 initrd 내용 확인
# RHEL/CentOS:
lsinitrd /boot/initramfs-$(uname -r)kdump.img
lsinitrd /boot/initramfs-$(uname -r)kdump.img | grep -i "network\|ssh\|nfs"

# Debian/Ubuntu:
lsinitramfs /var/lib/kdump/initrd.img-$(uname -r)

# 5. 캡처 커널 부팅 로그 확인 (시리얼 콘솔 필수)
# 캡처 커널 커맨드라인에 시리얼 콘솔 추가:
# KDUMP_COMMANDLINE_APPEND="... console=ttyS0,115200"
# 또는 /etc/kdump.conf:
# kdump_commandline_append="console=ttyS0,115200n8"

# 6. 캡처 커널 부팅 시 셸 진입 (디버깅용)
# kdump initrd에 rd.shell 추가:
# RHEL: /etc/sysconfig/kdump
KDUMP_COMMANDLINE_APPEND="irqpoll nr_cpus=1 reset_devices rd.shell"
# 캡처 커널 부팅 실패 시 dracut shell로 진입

# 7. initrd 수동 재빌드
# RHEL:
kdumpctl rebuild
# 또는 직접:
dracut -f /boot/initramfs-$(uname -r)kdump.img $(uname -r) \
    --add kdumpbase --add-drivers "ixgbe mlx5_core"

# Debian/Ubuntu:
kdump-config rebuild
⚠️

kdump 트러블슈팅 핵심 원칙:

  • 시리얼 콘솔은 필수: kdump 실패 시 캡처 커널의 부팅 로그를 확인할 수 있는 유일한 방법입니다. IPMI SOL, iLO, iDRAC의 가상 시리얼 포트를 활용하십시오.
  • 비프로덕션에서 테스트: echo c > /proc/sysrq-trigger로 의도적 패닉을 발생시켜 전체 흐름을 검증하십시오.
  • 커널 업데이트 후 재검증: 커널 업데이트 시 캡처 커널 initrd가 자동 재빌드되지 않는 배포판이 있습니다. kdumpctl rebuild를 수행하십시오.
  • 메모리 부족 시: /sys/kernel/kexec_crash_size를 확인하고 crashkernel 값을 증가시키십시오. 최소 192MB(드라이버 적은 환경)에서 최대 2GB(대용량 서버, 네트워크 덤프)까지 필요할 수 있습니다.

kdump 운영 자동화

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# vmcore 자동 로테이션 (cron)
# /etc/cron.daily/kdump-rotate:
#!/bin/bash
CRASH_DIR="/var/crash"
MAX_DUMPS=5
MAX_AGE_DAYS=30

# 오래된 vmcore 삭제
find "$CRASH_DIR" -name "vmcore*" -mtime +$MAX_AGE_DAYS -delete

# 최대 개수 초과 시 가장 오래된 것부터 삭제
ls -1t "$CRASH_DIR"/*/vmcore* 2>/dev/null | tail -n +$((MAX_DUMPS+1)) | \
    xargs -r rm -f

# kdump 상태 모니터링 스크립트
#!/bin/bash
check_kdump() {
    local loaded=$(cat /sys/kernel/kexec_crash_loaded 2>/dev/null)
    local crash_size=$(cat /sys/kernel/kexec_crash_size 2>/dev/null)

    if [[ "$loaded" != "1" ]]; then
        echo "CRITICAL: kdump 캡처 커널 미로드!"
        return 2
    fi

    if [[ -z "$crash_size" || "$crash_size" -eq 0 ]]; then
        echo "CRITICAL: crashkernel 메모리 미예약!"
        return 2
    fi

    # 디스크 여유 공간 확인
    local avail=$(df -BG /var/crash | awk 'NR==2{print $4}' | tr -d 'G')
    if [[ "$avail" -lt 10 ]]; then
        echo "WARNING: /var/crash 여유 공간 ${avail}GB (10GB 미만)"
        return 1
    fi

    echo "OK: kdump 정상 (crashkernel=$((crash_size/1048576))MB, 여유=${avail}GB)"
    return 0
}

VMCOREINFO 메커니즘

VMCOREINFO는 크래시 커널이 원본 커널의 메모리 레이아웃, 심볼 오프셋, 구조체 크기 등을 파악하기 위해 사용하는 메타데이터입니다. makedumpfile이 vmcore에서 불필요한 페이지를 분류하려면 원본 커널의 내부 구조를 알아야 하며, VMCOREINFO가 이 정보를 제공합니다.

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* VMCOREINFO 생성: kernel/crash_core.c */

/* 커널 부팅 시 vmcoreinfo_data에 메타데이터 기록 */
static char *vmcoreinfo_data;
static size_t vmcoreinfo_size;
static unsigned long vmcoreinfo_note[VMCOREINFO_NOTE_SIZE / sizeof(unsigned long)];

/* 정보 등록 매크로 */
#define VMCOREINFO_SYMBOL(name) \
    vmcoreinfo_append_str("SYMBOL(%s)=%lx\\n", #name, (unsigned long)&name)

#define VMCOREINFO_OFFSET(name, field) \
    vmcoreinfo_append_str("OFFSET(%s.%s)=%lu\\n", #name, #field, \
        (unsigned long)offsetof(struct name, field))

#define VMCOREINFO_SIZE(name) \
    vmcoreinfo_append_str("SIZE(%s)=%lu\\n", #name, \
        (unsigned long)sizeof(struct name))

#define VMCOREINFO_NUMBER(name) \
    vmcoreinfo_append_str("NUMBER(%s)=%ld\\n", #name, (long)name)

#define VMCOREINFO_CONFIG(name) \
    vmcoreinfo_append_str("CONFIG_%s=y\\n", #name)

/* crash_save_vmcoreinfo_init(): 부팅 시 호출 */
void crash_save_vmcoreinfo_init(void)
{
    /* 기본 시스템 정보 */
    VMCOREINFO_OSRELEASE(init_uts_ns.name.release);
    vmcoreinfo_append_str("PAGESIZE=%ld\\n", PAGE_SIZE);

    /* 핵심 심볼 주소 */
    VMCOREINFO_SYMBOL(init_uts_ns);
    VMCOREINFO_SYMBOL(_stext);
    VMCOREINFO_SYMBOL(swapper_pg_dir);
    VMCOREINFO_SYMBOL(mem_map);        /* FLATMEM */
    VMCOREINFO_SYMBOL(mem_section);     /* SPARSEMEM */
    VMCOREINFO_SYMBOL(vmemmap_base);    /* x86_64 */
    VMCOREINFO_SYMBOL(page_offset_base);
    VMCOREINFO_SYMBOL(vmalloc_base);

    /* struct page 레이아웃 (makedumpfile이 페이지 타입을 판별하기 위해 필수) */
    VMCOREINFO_SIZE(page);
    VMCOREINFO_OFFSET(page, flags);
    VMCOREINFO_OFFSET(page, _refcount);
    VMCOREINFO_OFFSET(page, mapping);
    VMCOREINFO_OFFSET(page, lru);
    VMCOREINFO_OFFSET(page, private);

    /* 메모리 모델 (SPARSEMEM_VMEMMAP, FLATMEM 등) */
    VMCOREINFO_NUMBER(MAX_NR_ZONES);
    VMCOREINFO_NUMBER(NR_FREE_PAGES);
    VMCOREINFO_NUMBER(PG_lru);
    VMCOREINFO_NUMBER(PG_private);
    VMCOREINFO_NUMBER(PG_swapcache);
    VMCOREINFO_NUMBER(PG_slab);
    VMCOREINFO_NUMBER(PG_buddy);       /* 프리 페이지 식별 */
    VMCOREINFO_NUMBER(PG_hugetlb);

    /* KASLR 오프셋 (주소 공간 랜덤화) */
    VMCOREINFO_NUMBER(phys_base);
    VMCOREINFO_NUMBER(KERNELOFFSET);

    /* 추가 서브시스템 정보 */
    arch_crash_save_vmcoreinfo();       /* 아키텍처별 추가 정보 */
}
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# VMCOREINFO 내용 확인 (실행 중인 커널)
cat /sys/kernel/vmcoreinfo
# 출력 예시 (16진수 주소:크기 형식의 노트 위치):
# 첫 줄: 물리 주소와 크기 (예: 3e0a9c00 1000)

# makedumpfile로 vmcore 내의 VMCOREINFO 추출
makedumpfile --dump-vmcoreinfo /var/crash/vmcore /tmp/vmcoreinfo.txt

# 출력 예시:
# OSRELEASE=6.1.0-amd64
# PAGESIZE=4096
# SYMBOL(init_uts_ns)=ffffffff82a13580
# SYMBOL(swapper_pg_dir)=ffffffff82c10000
# SYMBOL(mem_section)=ffffffff83412000
# SYMBOL(vmemmap_base)=ffffea0000000000
# OFFSET(page.flags)=0
# OFFSET(page._refcount)=28
# OFFSET(page.mapping)=8
# SIZE(page)=64
# NUMBER(PG_lru)=3
# NUMBER(PG_slab)=7
# NUMBER(PG_buddy)=10
# NUMBER(KERNELOFFSET)=0
# CONFIG_SPARSEMEM_VMEMMAP=y
# CRASHTIME=1707000000

# VMCOREINFO가 makedumpfile에 전달되는 경로:
# 1. 커널 부팅 시 vmcoreinfo_data에 기록
# 2. kexec -p 시 vmcoreinfo를 ELF NOTE로 elfcorehdr에 포함
# 3. 크래시 발생 → 캡처 커널 부팅
# 4. /proc/vmcore의 PT_NOTE 세그먼트에 VMCOREINFO 노출
# 5. makedumpfile이 PT_NOTE에서 VMCOREINFO 파싱
# 6. 심볼/오프셋 정보로 struct page 분석 → 페이지 타입별 필터링
VMCOREINFO 생성 → 전달 → 사용 흐름 1. 커널 부팅 vmcoreinfo_data SYMBOL, OFFSET, SIZE NUMBER, CONFIG 기록 2. kexec -p (캡처 커널 로드) elfcorehdr ELF NOTE에 VMCOREINFO 포함 3. 크래시 발생 crash_save_vmcoreinfo CRASHTIME 타임스탬프 CPU별 레지스터 저장 4. 캡처 커널 /proc/vmcore PT_NOTE 세그먼트에 VMCOREINFO 노출 makedumpfile 파싱 VMCOREINFO에서 심볼/오프셋 추출 → struct page 레이아웃 파악 페이지 타입 분류 page.flags의 PG_lru, PG_slab, PG_buddy 비트로 분류 필터링 적용 (-d 플래그) 유저/캐시/프리/제로 페이지 제외 → 커널 데이터만 vmcore에 저장 VMCOREINFO 주요 항목과 makedumpfile 활용 심볼 주소 (SYMBOL) mem_section → SPARSEMEM 섹션 배열 vmemmap_base → 가상 memmap 시작 page_offset_base → 직접 매핑 시작 _stext → 커널 텍스트 시작 swapper_pg_dir → PGD 주소 → 물리↔가상 주소 변환에 사용 오프셋/크기 (OFFSET/SIZE) page.flags → 페이지 상태 비트 page.mapping → 파일/anon 판별 page._refcount → 사용 여부 SIZE(page) → memmap 순회 단위 → struct page 정확한 파싱에 사용 → 커널 버전간 구조 변경 대응 플래그 번호 (NUMBER) PG_lru → LRU 리스트 페이지 PG_slab → 슬랩 할당 페이지 PG_buddy → 버디 프리 페이지 PG_hugetlb → Huge Page NR_FREE_PAGES → 프리 페이지 수 → -d 플래그 필터링 기준
ℹ️

VMCOREINFO와 KASLR: KASLR(Kernel Address Space Layout Randomization)이 활성화된 시스템에서는 커널 심볼의 실제 주소가 매 부팅마다 달라집니다. VMCOREINFO의 KERNELOFFSET 값이 KASLR 오프셋을 기록하므로, makedumpfile과 crash 유틸리티가 올바른 주소를 계산할 수 있습니다. NUMBER(KERNELOFFSET)=0x2a000000이면 모든 심볼 주소에 이 값이 더해진 것입니다.

vmcore ELF 형식 내부 구조

/proc/vmcore는 ELF64 형식의 가상 파일로, 캡처 커널이 크래시된 커널의 전체 물리 메모리를 ELF 세그먼트로 노출합니다. kexec가 생성한 elfcorehdr가 ELF 헤더와 프로그램 헤더 테이블의 원본이 됩니다.

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* vmcore ELF 구조 개요 */

/* ELF64 Header */
struct elf64_hdr {
    unsigned char e_ident[16];  /* 매직: 0x7f 'E' 'L' 'F' */
    Elf64_Half  e_type;         /* ET_CORE (4) */
    Elf64_Half  e_machine;      /* EM_X86_64 (62) 등 */
    Elf64_Off   e_phoff;        /* 프로그램 헤더 테이블 오프셋 */
    Elf64_Half  e_phnum;        /* 프로그램 헤더 수 (PT_NOTE + PT_LOAD들) */
    /* ... */
};

/* 프로그램 헤더: 두 종류 */
/* 1. PT_NOTE (1개): VMCOREINFO + CPU별 레지스터 */
struct elf64_phdr {
    Elf64_Word  p_type;    /* PT_NOTE (4) */
    Elf64_Off   p_offset;  /* NOTE 데이터 오프셋 */
    Elf64_Xword p_filesz;  /* NOTE 전체 크기 */
    /* ... */
};

/* 2. PT_LOAD (물리 메모리 세그먼트당 1개) */
struct elf64_phdr {
    Elf64_Word  p_type;    /* PT_LOAD (1) */
    Elf64_Off   p_offset;  /* vmcore 파일 내 오프셋 */
    Elf64_Addr  p_vaddr;   /* 가상 주소 (보통 0) */
    Elf64_Addr  p_paddr;   /* 물리 주소 (핵심!) */
    Elf64_Xword p_filesz;  /* 세그먼트 크기 */
    Elf64_Xword p_memsz;   /* 메모리 크기 (= p_filesz) */
    /* ... */
};

/* PT_NOTE 내부 구조 */
/*
 * NOTE 1: VMCOREINFO
 *   name:  "VMCOREINFO"
 *   type:  0 (NT_VMCOREINFO는 커스텀)
 *   desc:  VMCOREINFO 텍스트 데이터
 *
 * NOTE 2~N: CPU별 레지스터 (PRSTATUS)
 *   name:  "CORE"
 *   type:  NT_PRSTATUS (1)
 *   desc:  struct elf_prstatus (레지스터 덤프)
 *          - pr_pid, pr_reg (pt_regs)
 *          크래시 시점 각 CPU의 레지스터 상태
 */
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# vmcore ELF 헤더 분석
readelf -h /var/crash/vmcore
# ELF Header:
#   Type:    CORE (Core file)
#   Machine: Advanced Micro Devices X86-64

# 프로그램 헤더 확인 (PT_NOTE + PT_LOAD 목록)
readelf -l /var/crash/vmcore
# Program Headers:
#   Type   Offset             VirtAddr           PhysAddr
#   NOTE   0x0000000000001000 0x0000000000000000 0x0000000000000000
#   LOAD   0x0000000000040000 0x0000000000000000 0x0000000000001000
#          FileSiz: 0x000000000009f000  MemSiz: 0x000000000009f000
#   LOAD   0x00000000000df000 0x0000000000000000 0x0000000000100000
#          FileSiz: 0x000000003fefffff  MemSiz: 0x000000003fefffff
#   ... (물리 메모리 영역별 PT_LOAD)

# PT_NOTE 내의 VMCOREINFO와 PRSTATUS 확인
readelf -n /var/crash/vmcore
# Notes at offset 0x1000:
#   VMCOREINFO  0x00001234  (VMCOREINFO 메타데이터)
#   CORE        0x00000150  NT_PRSTATUS (CPU 0 레지스터)
#   CORE        0x00000150  NT_PRSTATUS (CPU 1 레지스터)
#   ...

# elfcorehdr: kexec가 예약 메모리에 미리 작성한 ELF 헤더
# 캡처 커널 부팅 파라미터로 전달:
# elfcorehdr=0x3ff00000 (물리 주소)
# 캡처 커널의 /proc/vmcore 드라이버가 이 주소를 읽어 ELF 구조 파악
dmesg | grep elfcorehdr
# [    0.000000] elfcorehdr: 0x3ff00000-0x3ff10000

# /proc/vmcore 구현: fs/proc/vmcore.c
# - read_vmcore(): PT_LOAD 세그먼트의 물리 주소를 ioremap으로 읽기
# - mmap_vmcore(): 대용량 덤프 시 mmap으로 효율적 접근
# - vmcore_init(): elfcorehdr 파싱 → 세그먼트 목록 구성
/proc/vmcore ELF64 구조 ELF64 Header e_type = ET_CORE e_phnum = N+1 Program Header Table phdr[0]: PT_NOTE phdr[1]: PT_LOAD (0~640KB) phdr[2]: PT_LOAD (1MB~...) phdr[N]: PT_LOAD (마지막) PT_NOTE 세그먼트 VMCOREINFO (커널 메타) CPU0 regs CPU1 regs PT_LOAD 세그먼트 (물리 메모리) p_paddr = 물리 시작 주소 (예: 0x100000) p_filesz = 영역 크기 (예: 0x3FF00000 = ~1GB) p_offset = vmcore 파일 내 데이터 위치 캡처 커널이 ioremap/copy_oldmem으로 크래시 메모리 접근 elfcorehdr 생성 과정 kexec -p 실행 캡처 커널 로드 elfcorehdr 생성 /proc/iomem 기반 PT_LOAD crashkernel 영역 저장 예약 메모리에 기록 캡처 커널 부팅 파라미터 elfcorehdr=0x3ff00000 * /proc/iomem의 "System RAM" 영역 → PT_LOAD 세그먼트로 매핑 * crashkernel 예약 영역은 PT_LOAD에서 제외 (캡처 커널 자체 메모리)

kexec 빠른 재부팅

kexec는 kdump(크래시 덤프) 외에도 빠른 재부팅 용도로 널리 사용됩니다. BIOS/UEFI POST 과정과 부트로더를 우회하여 직접 새 커널을 부팅하므로, 대형 서버에서 수 분 걸리는 재부팅 시간을 수 초로 단축할 수 있습니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
### kexec 빠른 재부팅 기본 사용법 ###

# 1. 새 커널 이미지 로드 (일반 kexec, -p가 아님)
KVER=$(uname -r)
VMLINUX=/boot/vmlinuz-$KVER
INITRD=$(ls -1 /boot/initr*${KVER}* | head -1)

kexec -l "$VMLINUX" \
    --initrd="$INITRD" \
    --append="$(cat /proc/cmdline)"
# --append: 현재 부팅 파라미터 재사용
# -l: 일반 로드 (재부팅용), -p: 패닉 로드 (kdump용)

# 2. 즉시 재부팅 (kexec로 전환)
systemctl kexec
# 또는 직접:
kexec -e
# 또는:
reboot   # 일반 reboot는 보통 일반 재부팅 경로입니다 (kexec 보장 아님)

### kexec_file_load (-s 옵션) 사용 ###
kexec -l -s "$VMLINUX" \
    --initrd="$INITRD" \
    --append="$(cat /proc/cmdline)"
# -s: kexec_file_load 사용 (Secure Boot 호환)

### 재부팅 시간 비교 (일반적인 서버 기준) ###
# 일반 재부팅 (reboot):
#   BIOS POST:       30~120초 (서버 하드웨어 초기화)
#   부트로더(GRUB):   5~10초
#   커널 부팅:        10~30초
#   총:              45~160초
#
# kexec 재부팅 (kexec -e):
#   purgatory:       <1초
#   커널 부팅:        10~30초
#   총:              10~31초  ← BIOS POST 완전 건너뜀

### kexec 재부팅 자동화 (커널 업데이트 후) ###
# 새 커널 설치 후 kexec로 빠르게 전환
NEW_KERNEL=$(ls -1t /boot/vmlinuz-* | head -1)
NEW_INITRD=$(ls -1t /boot/initrd.img-* | head -1)

# 현재 커널 파라미터에서 crashkernel 등 유지
CMDLINE=$(cat /proc/cmdline | sed "s|BOOT_IMAGE=[^ ]*||")

kexec -l -s "$NEW_KERNEL" \
    --initrd="$NEW_INITRD" \
    --append="$CMDLINE"

# 확인 후 실행
cat /sys/kernel/kexec_loaded   # 1이면 로드 성공
systemctl kexec                # 서비스 정상 종료 후 kexec
⚠️

kexec 빠른 재부팅 주의사항:

  • 하드웨어 초기화 생략: BIOS POST를 건너뛰므로 하드웨어가 이전 상태를 유지합니다. 일부 디바이스(GPU, RAID 컨트롤러)가 정상 초기화되지 않을 수 있습니다.
  • 파일시스템 정합성: systemctl kexec 대신 kexec -e를 직접 호출하면 파일시스템 동기화 없이 즉시 전환됩니다. 데이터 손실 위험이 있으므로 반드시 systemctl kexec 또는 sync && kexec -e를 사용하십시오.
  • 부트로더 설정 무시: GRUB의 기본 커널 설정이 변경되지 않으므로, 다음 정상 재부팅 시 이전 커널로 돌아갈 수 있습니다.
  • KEXEC_PRESERVE_CONTEXT 플래그: 하이버네이션 복원 전용. 일반 재부팅에서는 사용하지 마십시오.

crashkernel 예약 내부 메커니즘

커널이 crashkernel= 부팅 파라미터를 처리하여 물리 메모리를 예약하는 과정을 상세히 살펴봅니다. 예약 실패 시 디버깅에 필수적인 지식입니다.

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* kernel/crash_core.c - crashkernel 파라미터 파싱 */

/* 부팅 초기(setup_arch → reserve_crashkernel)에서 호출 */
int __init parse_crashkernel(
    char *cmdline,
    unsigned long long system_ram,
    unsigned long long *crash_size,
    unsigned long long *crash_base)
{
    /* 파라미터 파싱 우선순위:
     * 1. crashkernel=X@Y  (고정 크기 + 고정 위치)
     * 2. crashkernel=X    (고정 크기, 커널이 위치 결정)
     * 3. crashkernel=range1:size1,range2:size2,...
     * 4. crashkernel=auto  (배포판 정의 테이블 사용)
     */

    /* "auto" 처리 (커널 내장 테이블) */
    if (strncmp(cmdline, "auto", 4) == 0) {
        /* CONFIG_CRASH_AUTO_STR 또는 아키텍처별 기본값 */
        /* x86_64 기본 (RHEL): "1G-4G:160M,4G-64G:192M,
         *   64G-1T:256M,1T-:512M" */
    }

    /* 범위 기반 파싱: "512M-2G:64M,2G-:256M" */
    /* system_ram이 해당 범위에 속하는 크기 선택 */
    return parse_crashkernel_mem(ck_cmdline, system_ram,
                                 crash_size, crash_base);
}

/* arch/x86/kernel/setup.c - 실제 예약 */
void __init reserve_crashkernel(void)
{
    unsigned long long crash_size, crash_base;

    /* 1. 파라미터 파싱 */
    parse_crashkernel(boot_command_line, memblock_phys_mem_size(),
                      &crash_size, &crash_base);

    /* 2. high/low 분리 처리 */
    if (crash_base == 0) {
        /* 위치 미지정 → 커널이 자동 결정 */

        /* crashkernel=X,high: 4GB 이상 영역에서 할당 시도 */
        crash_base = memblock_phys_alloc_range(
            crash_size, SZ_256M,     /* 256MB 정렬 */
            SZ_4G, MEMBLOCK_ALLOC_ACCESSIBLE);

        if (!crash_base) {
            /* 4GB 이상 실패 → 4GB 미만에서 재시도 */
            crash_base = memblock_phys_alloc_range(
                crash_size, SZ_1M,
                0, SZ_4G);
        }

        /* crashkernel=X,low: 4GB 미만 영역 추가 예약 */
        /* swiotlb(DMA 바운스 버퍼)용 최소 영역 */
        if (high_allocated && low_size) {
            low_base = memblock_phys_alloc_range(
                low_size, SZ_1M, 0, SZ_4G);
        }
    } else {
        /* 위치 지정: crashkernel=X@Y */
        memblock_reserve(crash_base, crash_size);
    }

    /* 3. crashk_res 리소스 등록 (/proc/iomem에 "Crash kernel" 표시) */
    crashk_res.start = crash_base;
    crashk_res.end = crash_base + crash_size - 1;
    insert_resource(&iomem_resource, &crashk_res);

    /* 4. sysfs 노출: /sys/kernel/kexec_crash_size */
    pr_info("Reserving %ldMB of memory at %ldMB for crashkernel\\n",
            (unsigned long)(crash_size >> 20),
            (unsigned long)(crash_base >> 20));
}
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# crashkernel 예약 과정 디버깅

# 부팅 로그에서 예약 성공/실패 확인
dmesg | grep -i "crashkernel\|crash kernel\|Reserving"
# 성공: [    0.000000] Reserving 256MB of memory at 704MB for crashkernel
# 실패: [    0.000000] crashkernel reservation failed - No suitable area found

# memblock 디버깅 (early_param "memblock=debug")
# GRUB: linux ... memblock=debug
dmesg | grep "memblock_reserve.*crash"
# [    0.000000] memblock_reserve: [0x2c000000-0x3bffffff] crashkernel

# /proc/iomem에서 예약 영역 확인
cat /proc/iomem | grep -A1 -i crash
#   2c000000-3bffffff : Crash kernel

# /sys/kernel에서 런타임 확인
cat /sys/kernel/kexec_crash_size   # 예약 크기 (바이트)
cat /sys/kernel/kexec_crash_loaded # 캡처 커널 로드 여부

# crashkernel 예약 실패 시 디버깅 순서:
# 1. dmesg에서 에러 메시지 확인
# 2. /proc/iomem에서 메모리 레이아웃 확인 (4GB 미만 여유 공간)
# 3. NUMA 시스템: Node 0 메모리가 충분한지 확인
#    numactl --hardware | head -10
# 4. 정렬 요구사항: x86_64는 256MB(high) 또는 1MB(low) 정렬
# 5. memblock=debug 부팅 파라미터로 상세 로그 확인
# 6. high/low 분리: crashkernel=256M,high crashkernel=72M,low 시도

early kdump (부팅 초기 크래시 캡처)

일반 kdump는 systemd의 kdump 서비스가 시작된 이후에만 캡처 커널을 로드합니다. 그러나 드라이버 초기화, 파일시스템 마운트 등 부팅 초기 단계에서 크래시가 발생하면 vmcore를 얻을 수 없습니다. early kdump는 initrd/initramfs 단계에서 캡처 커널을 미리 로드하여 이 문제를 해결합니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
### early kdump 동작 원리 ###
# 일반 kdump: 부팅 → systemd → 네트워크 → kdump.service → kexec -p
#   → 이 시점 이전의 크래시는 캡처 불가!
#
# early kdump: 부팅 → initrd 내 dracut 모듈 → kexec -p (매우 초기)
#   → initrd 단계부터 캡처 가능

### RHEL/CentOS에서 early kdump 설정 ###

# 1. /etc/sysconfig/kdump에서 early kdump 활성화
# KDUMP_EARLY=1

# 2. dracut에 early-kdump 모듈 포함하여 initramfs 재빌드
dracut -f --add early-kdump /boot/initramfs-$(uname -r).img $(uname -r)
# 또는 /etc/dracut.conf.d/early-kdump.conf:
# add_dracutmodules+=" early-kdump "

# 3. 커널 파라미터에 rd.earlykdump 추가
# GRUB: linux ... crashkernel=256M rd.earlykdump

# 4. initramfs 재빌드 후 재부팅
# BIOS(RHEL): grub2-mkconfig -o /boot/grub2/grub.cfg
# UEFI(RHEL): grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg
# Debian/Ubuntu: update-grub
reboot

# 5. 확인: 부팅 로그에서 early kdump 활성화 여부
dmesg | grep -i "early.*kdump\|earlykdump"
journalctl -b | grep -i "early.*kdump"

### early kdump initrd 내부 동작 ###
# dracut의 early-kdump 모듈 (/usr/lib/dracut/modules.d/99earlykdump/):
# 1. initrd 내에 vmlinuz + kdump initrd를 별도로 포함
# 2. dracut의 초기 훅(pre-trigger)에서 kexec -p 실행
# 3. 이 시점에서 캡처 커널이 로드되어 패닉에 대응 가능
# 4. 이후 정상 부팅 진행 → kdump.service에서 필요 시 재로드

### early kdump의 제약사항 ###
# - initrd 크기 증가 (vmlinuz + kdump initrd 추가 포함)
#   → 일반 initrd ~50MB → early kdump 포함 시 ~150MB
# - 네트워크 덤프 불가 (initrd 단계에서 네트워크 미설정)
#   → 로컬 디스크 덤프만 가능
# - root 파일시스템 마운트 전이므로 /var/crash 사용 불가
#   → dracut 임시 영역에 저장 후 나중에 복사

fadump (Firmware-Assisted Dump)

fadump는 IBM POWER 아키텍처(ppc64le)에서 지원하는 펌웨어 기반 크래시 덤프 메커니즘입니다. kexec 기반 kdump와 달리 펌웨어가 메모리 보존과 CPU 상태 저장을 담당하므로, 일부 kdump의 한계를 극복합니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
### fadump vs kdump 비교 ###
# 항목           fadump (POWER)              kdump (범용)
# ───────────────────────────────────────────────────────────
# 아키텍처       IBM POWER (ppc64le)          x86, arm64, s390x 등
# 메모리 보존    펌웨어가 보존                kexec가 예약 영역 사용
# CPU 상태       펌웨어가 저장                NMI로 다른 CPU 정지+저장
# 부팅 방식      정상 부팅 (같은 커널 재시작) 캡처 커널 (별도 커널)
# 메모리 예약    최소 (부팅 메모리만)         고정 crashkernel 영역
# 초기화 실패    펌웨어가 처리하므로 안정적   캡처 커널 초기화 실패 가능
# 디바이스 접근  정상 부팅이므로 전체 가용    제한된 드라이버만 로드

### fadump 커널 CONFIG ###
# CONFIG_FA_DUMP=y          # fadump 지원
# CONFIG_PRESERVE_FA_DUMP=y # fadump 메모리 보존 (캡처 커널용)

### fadump 설정 (POWER 시스템) ###

# 1. 커널 파라미터
# fadump=on [fadump_reserve_mem=512M]
# crashkernel= 대신 fadump= 사용

# 2. /etc/kdump.conf에서 fadump 사용 (RHEL)
# RHEL은 kdump 인프라를 재사용하되 백엔드만 fadump로 전환
# /etc/sysconfig/kdump:
# KDUMP_FADUMP=yes

# 3. 등록 상태 확인
cat /sys/kernel/fadump_registered
# 1 = fadump 등록됨 (크래시 대응 준비 완료)

cat /sys/kernel/fadump_enabled
# 1 = fadump 활성화됨

# 4. fadump 수동 등록/해제
echo 1 > /sys/kernel/fadump_registered   # 등록
echo 0 > /sys/kernel/fadump_registered   # 해제

### fadump 동작 흐름 ###
# 1. 커널 패닉 발생
# 2. 펌웨어(OPAL/PHYP)가 제어권 인수
# 3. 펌웨어가 모든 CPU 레지스터 + 메모리 레이아웃 저장
# 4. 펌웨어가 정상 부팅 절차 수행 (같은 커널 이미지로)
# 5. 부팅 시 커널이 fadump 메모리 감지
# 6. /proc/vmcore로 크래시 메모리 노출
# 7. kdump 서비스가 makedumpfile로 vmcore 저장
# 8. 저장 완료 후 fadump 메모리 해제 → 정상 운영

### s390x (IBM Z): VMDUMP / stand-alone dump ###
# IBM Z 메인프레임은 하이퍼바이저(z/VM, LPAR)를 통한
# 별도 덤프 메커니즘을 지원합니다.
# CONFIG_CRASH_DUMP + CONFIG_ZFCPDUMP (zfcp 디바이스 덤프)
# vmur (VM reader) 또는 zgetdump 유틸리티로 덤프 수집

kexec 보안 고려사항

kexec는 임의 커널을 로드·실행할 수 있으므로 보안 관점에서 중요한 공격 표면이 됩니다. Secure Boot, Lockdown, IMA 등 다양한 보안 메커니즘과의 상호작용을 이해해야 합니다.

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* kexec 보안 검사 흐름 (kernel/kexec.c, security/security.c) */

/* 1. LSM 보안 훅 */
int security_kernel_load_data(enum kernel_load_data_id id, bool contents)
{
    /* id = LOADING_KEXEC_IMAGE 또는 LOADING_KEXEC_INITRAMFS */
    /* SELinux, AppArmor 등 LSM이 kexec 허용 여부 결정 */
    return call_int_hook(kernel_load_data, id, contents);
}

/* 2. Lockdown 검사 */
/* CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY 또는 lockdown=integrity */
/* → kexec_load() 시스템 콜 완전 차단 (서명 검증 불가) */
/* → kexec_file_load()만 허용 (커널 내부 서명 검증) */

/* 3. kexec_file_load 서명 검증 */
int kexec_image_verify_sig(struct kimage *image, void *buf,
                           unsigned long buf_len)
{
    /* PE/COFF 서명 검증 (x86 bzImage) */
    /* 또는 모듈 서명 방식 검증 */
    return verify_pefile_signature(buf, buf_len,
                                  VERIFY_USE_SECONDARY_KEYRING,
                                  VERIFYING_KEXEC_PE_SIGNATURE);
}

/* 4. IMA (Integrity Measurement Architecture) 연동 */
/* CONFIG_IMA_APPRAISE_MODSIG, CONFIG_IMA_ARCH_POLICY */
/* IMA 정책에 kexec 이미지 측정/감사 규칙 추가 가능 */
/* ima_policy: "appraise func=KEXEC_KERNEL_CHECK appraise_type=imasig" */

/* 키링(keyring) 계층 구조:
 * .builtin_trusted_keys  → 커널 빌드 시 내장된 키
 * .secondary_trusted_keys → 런타임에 추가 가능한 키
 * .machine (UEFI db 키)  → 펌웨어의 Secure Boot 키
 * .platform (MOK 키)     → Machine Owner Key
 *
 * kexec_file_load는 이 키링들로 커널 이미지 서명 검증
 */
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# Lockdown 모드 확인
cat /sys/kernel/security/lockdown
# [none] integrity confidentiality
# integrity: kexec_load 차단, kexec_file_load는 서명 시 허용
# confidentiality: kexec 완전 차단

# kexec 관련 Lockdown 제한 사항
# lockdown=integrity 일 때:
# - kexec_load (kexec -l 기본) → 차단 (EPERM)
# - kexec_file_load (kexec -l -s) → 서명 검증 후 허용
# - /dev/mem, /dev/kmem → 읽기 전용
# - ACPI 테이블 오버라이드 → 차단

# Secure Boot + kexec 환경에서의 kdump 설정
# 캡처 커널도 서명되어야 함!
# 배포판 커널은 자동으로 서명됨 (Canonical, Red Hat 키)
# 커스텀 커널은 MOK(Machine Owner Key)로 직접 서명 필요:
sbsign --key MOK.key --cert MOK.crt \
    /boot/vmlinuz-custom /boot/vmlinuz-custom.signed

# MOK 등록
mokutil --import MOK.der
# 재부팅 후 MOK Manager에서 등록 확인

# kexec_file_load 서명 검증 실패 디버깅
dmesg | grep -i "kexec.*sig\|PKCS\|verify"
# kexec_file: verification failed: -22 → 서명 없거나 키 미등록

# 시스템 키링 확인
keyctl list %:.builtin_trusted_keys
keyctl list %:.secondary_trusted_keys
keyctl list %:.machine
# kexec 커널 이미지의 서명이 이 키링 중 하나로 검증 가능해야 함

# IMA 정책에서 kexec 감사 확인
cat /sys/kernel/security/ima/policy | grep -i kexec
# appraise func=KEXEC_KERNEL_CHECK appraise_type=imasig

# SELinux에서 kexec 권한 확인
sesearch -A -s unconfined_t -t kernel_t -c system -p module_load
# kexec는 내부적으로 module_load 권한을 사용
☢️

kexec 보안 위험: 서명 검증 없이 kexec를 허용하면, 공격자가 루트 권한을 획득한 후 악의적인 커널을 로드하여 전체 시스템을 장악할 수 있습니다. 이는 Secure Boot의 보안 체인을 완전히 우회하는 것입니다. 프로덕션 환경에서는 반드시 lockdown=integrityCONFIG_KEXEC_SIG_FORCE를 활성화하십시오. kexec_load 시스템 콜은 서명 검증이 불가능하므로 보안 환경에서는 CONFIG_KEXEC=n으로 아예 비활성화하고 CONFIG_KEXEC_FILE만 사용하는 것을 권장합니다.

crash_hotplug (동적 핫플러그 지원)

Linux 6.5부터 도입된 CONFIG_CRASH_HOTPLUG는 CPU나 메모리 핫플러그 이벤트 발생 시 캡처 커널의 elfcorehdr을 자동으로 업데이트합니다. 이전에는 CPU/메모리 추가·제거 후 kdump 서비스를 재시작하여 캡처 커널을 다시 로드해야 했습니다.

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* kernel/crash_core.c - crash_hotplug 핵심 구조 (6.5+) */

/* CPU 핫플러그 콜백 */
static int crash_cpuhp_online(unsigned int cpu)
{
    /* 새 CPU 추가 시:
     * 1. elfcorehdr의 PT_NOTE에 새 CPU의 PRSTATUS 공간 확보
     * 2. 기존 PT_LOAD 세그먼트는 변경 불필요 (메모리 레이아웃 동일)
     * 3. elfcorehdr in-place 업데이트 (캡처 커널 재로드 불필요!)
     */
    crash_handle_hotplug_event(KEXEC_CRASH_HP_ADD_CPU, cpu);
    return 0;
}

/* 메모리 핫플러그 콜백 */
static int crash_memhp_notifier(struct notifier_block *nb,
                                unsigned long action, void *data)
{
    switch (action) {
    case MEM_ONLINE:
        /* 새 메모리 추가:
         * 1. 새 메모리 영역에 대한 PT_LOAD 세그먼트 추가
         * 2. elfcorehdr 업데이트
         * 3. 캡처 커널 재로드 없이 in-place 수정
         */
        crash_handle_hotplug_event(KEXEC_CRASH_HP_ADD_MEMORY, 0);
        break;
    case MEM_OFFLINE:
        /* 메모리 제거: 해당 PT_LOAD 세그먼트 제거 */
        crash_handle_hotplug_event(KEXEC_CRASH_HP_REMOVE_MEMORY, 0);
        break;
    }
    return NOTIFY_OK;
}

/* elfcorehdr 업데이트 핵심 함수 */
void crash_handle_hotplug_event(unsigned int hp_action, unsigned int cpu)
{
    /* kexec_mutex 획득 → elfcorehdr 재생성 → 기존 위치에 덮어쓰기 */
    /* 캡처 커널 이미지 자체는 그대로 유지 */
    /* elfcorehdr만 업데이트하므로 매우 빠름 (ms 단위) */

    mutex_lock(&kexec_mutex);

    /* 새 elfcorehdr 생성 */
    crash_prepare_elf64_headers(…);

    /* 예약 영역 내 elfcorehdr 위치에 덮어쓰기 */
    crash_update_elfcorehdr(image, …);

    mutex_unlock(&kexec_mutex);
}
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# crash_hotplug 지원 여부 확인
grep CONFIG_CRASH_HOTPLUG /boot/config-$(uname -r)
# CONFIG_CRASH_HOTPLUG=y

# sysfs 인터페이스 (6.5+)
cat /sys/kernel/crash_hotplug_cpu     # 1: CPU 핫플러그 자동 대응
cat /sys/kernel/crash_hotplug_memory  # 1: 메모리 핫플러그 자동 대응

# 이전 커널 (6.5 미만)에서의 수동 대응
# CPU/메모리 핫플러그 후 kdump 재로드 필요:
echo 1 > /sys/devices/system/cpu/cpu4/online   # CPU 추가
systemctl restart kdump                         # kdump 재로드 필수!
# 또는:
kdumpctl reload                                 # RHEL 전용

# 6.5+ 커널에서는 자동 처리:
echo 1 > /sys/devices/system/cpu/cpu4/online   # CPU 추가
# → 커널이 자동으로 elfcorehdr 업데이트
# → kdump 재시작 불필요!
dmesg | grep -i "crash hp\|hotplug.*elfcore"
# [  123.456] crash hp: online cpu 4, updating elfcorehdr

# 메모리 핫플러그 시나리오 (가상머신에서 흔함)
# 메모리 추가:
echo online > /sys/devices/system/memory/memory32/state
# 6.5+: elfcorehdr 자동 업데이트 → 새 메모리도 vmcore에 포함
# 6.5-: kdump 재시작 필요, 안 하면 새 메모리가 vmcore에서 누락!

# 핫플러그 환경에서 crashkernel 크기 주의
# 메모리를 대량 추가하면 PT_LOAD 세그먼트 수가 증가
# → elfcorehdr 크기 증가 → 예약 영역 내 공간 부족 가능
# 해결: crashkernel 영역을 넉넉히 설정 (여유분 확보)
💡

crash_hotplug와 가상화: KVM/QEMU, VMware 등 가상 환경에서는 CPU/메모리 핫플러그가 빈번합니다. 클라우드 인스턴스의 동적 리소스 조정(auto-scaling)과 kdump가 함께 동작하려면 6.5+ 커널의 crash_hotplug 기능이 매우 유용합니다. 이전 커널에서는 udev 규칙이나 systemd 서비스로 핫플러그 이벤트 시 kdumpctl reload를 자동 실행하도록 구성하십시오.

Call Trace 심화 분석

커널 Call Trace(백트레이스)는 단순한 "함수 목록"이 아닙니다. 실제로는 dump_stack() 계열 함수나 경고/예외 처리 루틴이 현재 문맥의 스택을 언와인드(unwind)해서 주소를 수집하고, 이를 함수명+오프셋 형태로 포맷팅해 로그에 출력한 결과물입니다. 따라서 깊은 분석을 하려면 단순히 "무슨 함수가 찍혔는가"만 보지 말고, 어떤 경로가 dump를 시작했는가, 어떤 함수가 실제 원인이고 어떤 함수가 dump helper인가, 언와인더가 어느 정도까지 신뢰할 수 있는가를 함께 읽어야 합니다.

특히 같은 Call Trace라도 dump_stack()을 코드에서 직접 호출해 찍은 것인지, WARN_ON()이나 KASAN/UBSAN처럼 진단 프레임워크가 자동으로 찍은 것인지, page fault나 BUG처럼 예외 경로가 찍은 것인지에 따라 최상단 프레임의 의미가 달라집니다. 아래 내용은 바로 그 "덤프 함수 호출 흐름"을 기준으로 Call Trace를 해부합니다.

Call Trace를 덤프하는 대표 함수

커널에서 사람이 보는 Call Trace: 블록은 보통 아래 함수들이 협력해 만들어집니다. 아키텍처별 helper 이름은 조금씩 다르더라도, 공용 진입점 → 스택 walk → 주소 포맷팅 → printk 출력이라는 큰 흐름은 거의 같습니다.

함수/경로역할언제 보이는가읽을 때의 의미
dump_stack()현재 CPU/태스크의 Call Trace를 즉시 찍는 공용 진입점드라이버 임시 디버그, 경고 전후 점검, 수동 instrumentation최상단에 dump_stack_lvl이 보이면 "버그 지점"이 아니라 "지금 찍자고 한 지점"일 수 있습니다.
dump_stack_lvl(log_lvl)로그 레벨을 지정하는 내부 공통 래퍼실제 출력 trace 최상단에 자주 보임이 프레임은 원인 함수라기보다 dump helper 체인의 일부로 보는 편이 맞습니다.
show_stack(task, sp, loglvl)특정 태스크 또는 현재 태스크의 스택을 walk현재 태스크 또는 다른 태스크 backtrace 출력tasksp를 인수로 받으므로 "누구의 스택을 어디서부터 읽을지"를 결정하는 층입니다.
arch_stack_walk()아키텍처별 unwinder가 return address를 차례대로 복원x86 ORC, frame pointer, 기타 아키텍처별 unwinder 공통 개념Call Trace의 품질은 대부분 여기서 결정됩니다. 인라인 생략, tail call, 스택 손상 영향도 이 단계에서 나타납니다.
stack_trace_print()수집된 주소 배열을 문자열로 변환해 출력stack_trace_save* 계열의 후처리 포함여기서는 "주소를 어떤 형식으로 보여 주는가"가 중요하지, 프레임을 새로 찾지는 않습니다.
__warn(), warn_slowpath_fmt()WARN 계열이 경고 메시지와 stack dump를 생성WARN_ON(), WARN(), UBSAN/KCSAN 일부 경로최상단 helper를 걷어내고 첫 번째 서브시스템 함수부터 읽어야 원인과 가깝습니다.
show_regs() + 예외 처리 경로예외 발생 레지스터와 RIP를 먼저 보여 준 뒤 Call Trace를 출력Oops, BUG, page fault, GP fault이 경우 실제 fault site는 보통 RIP가 가장 정확하게 가리키고, Call Trace는 그 caller 사슬을 보완합니다.
stack_trace_save*즉시 출력하지 않고 주소만 저장KASAN alloc/free 추적, livepatch 신뢰도 검사, 지연 출력"Call Trace를 dump하는 함수"와 "주소만 수집하는 함수"를 구분해야 로그를 올바르게 해석할 수 있습니다.
/* 개념 예시: 공용 dump 함수와 자동 dump 경로의 관계 */
void dump_stack(void)
{
    dump_stack_lvl(KERN_DEFAULT);
}

void dump_stack_lvl(const char *log_lvl)
{
    dump_stack_print_info(log_lvl);   /* CPU, PID, Comm, taint 등 */
    show_stack(NULL, NULL, log_lvl);    /* current task 기준 stack walk */
}

void show_stack(struct task_struct *task,
                unsigned long *sp,
                const char *log_lvl)
{
    /* 아키텍처 unwinder가 caller 체인을 복원 */
    arch_stack_walk(consume_entry, &cookie, task, regs);

    /* 복원된 주소를 함수명+오프셋으로 출력 */
    stack_trace_print(entries, nr_entries, 0);
}

/* WARN 계열: 수동 dump가 아니라 경고 루틴이 stack dump를 트리거 */
/* WARN_ON(cond) → __WARN() → warn_slowpath_fmt() → __warn(...) → dump helper */

/* Oops/page fault 계열: 예외 레지스터가 먼저, caller 체인은 그 다음 */
/* 예외 진입 → show_regs(regs) → show_stack(current, regs 기반 시작점, ...) */
Call Trace dump 함수 호출 흐름 명시적 dump 드라이버 코드가 dump_stack() 직접 호출 자동 경고 dump WARN_ON(), UBSAN, KASAN 리포트 예외/Oops dump page fault, BUG, GP fault, die 경로 공통 dump 진입점 dump_stack_lvl() / show_stack() 보조 정보 출력 show_regs(), RIP, EFLAGS 헤더 출력 dump_stack_print_info() 언와인더 arch_stack_walk()가 caller 체인 복원 문자열 포맷팅 stack_trace_print() → 함수명+오프셋 최종 출력 printk 링 버퍼, 콘솔, dmesg

dump 함수가 실제 호출 흐름을 만드는 법

Call Trace가 만들어지는 과정은 "스택 메모리를 위에서 아래로 쭉 출력"하는 것이 아닙니다. 커널은 현재 프레임 또는 지정된 레지스터/스택 포인터를 시작점으로 삼아, 각 프레임의 return address를 찾아 caller 체인을 차례대로 복원합니다. 이때 출력 순서와 실제 시간 순서를 구분해서 읽어야 합니다.

단계대표 함수커널이 하는 일분석자가 봐야 할 포인트
1. dump 시작dump_stack(), __warn(), 예외 처리기왜 Call Trace를 찍을지 결정"이 trace가 수동 dump인가, 자동 경고인가, 예외인가?"를 먼저 구분합니다.
2. 시작점 선택show_stack(task, sp, ...)현재 태스크인지, 다른 태스크인지, 어떤 SP/regs에서 시작할지 결정foreign task dump라면 현재 CPU 상태와 다른 스택일 수 있습니다.
3. 프레임 walkarch_stack_walk()현재 프레임에서 caller 프레임으로 이동이 단계가 실패하거나 추측을 섞으면 ? 프레임, 누락 프레임이 생깁니다.
4. 주소 저장/필터링stack_trace_save*, IRQ stack filter필요하면 주소를 배열에 저장하거나 별도 stack을 필터링KASAN alloc/free trace처럼 나중에 별도로 출력될 수도 있습니다.
5. 문자열 변환stack_trace_print()주소를 심볼+오프셋으로 포맷팅여기서는 보이는 형식만 바뀌고, 새로운 frame이 발견되지는 않습니다.
6. 로그 출력printk 경로링 버퍼와 콘솔에 기록콘솔에 안 보여도 dmesg, pstore, vmcore에 남아 있을 수 있습니다.
읽는 방향을 분리해서 보세요: 출력 텍스트는 보통 위에서 아래로 "현재 프레임 → caller" 순서입니다. 하지만 실제 역사적 호출 순서를 복원하려면 아래에서 위로 "엔트리 함수 → 문제 함수"로 다시 읽어야 합니다. 즉, Call Trace는 한 번은 위에서 아래로 구조를 읽고, 다시 한 번 아래에서 위로 시간을 읽어야 합니다.
# 수동 dump_stack() 예시
Call Trace:
 dump_stack_lvl+0x34/0x48
 my_debug_checkpoint+0x52/0x90 [my_module]
 my_probe+0x1c0/0x310 [my_module]
 really_probe+0x1a8/0x3f0
 __driver_probe_device+0x78/0x170

# 위에서 아래로 읽으면:
# - dump_stack_lvl       → 지금 stack dump를 찍는 helper
# - my_debug_checkpoint  → 개발자가 dump_stack()을 호출한 지점
# - my_probe             → 그 호출자

# 아래에서 위로 읽으면 실제 호출 역사:
# __driver_probe_device → really_probe → my_probe → my_debug_checkpoint → dump_stack_lvl

수동 dump와 자동 dump의 최상단 프레임 차이

많은 오해는 "Call Trace 최상단이 항상 실제 버그 함수"라고 생각하는 데서 시작합니다. 그렇지 않습니다. 수동 dump_stack()은 helper가 먼저 보이고, WARN()은 경고 helper와 리포트 helper가 먼저 보이며, KASAN/UBSAN은 sanitizer helper가 앞단에 보입니다. 반대로 Oops/page fault는 RIP가 실제 fault instruction을 가장 정확하게 알려 줍니다.

트리거trace 상단에 흔히 보이는 프레임실제 분석 시작점왜 그렇게 읽어야 하는가
직접 dump_stack() 호출dump_stack_lvl, 호출한 함수dump_stack()을 부른 바로 아래 함수이 경우 helper는 "버그"가 아니라 "여기서 찍자"는 의사결정 흔적입니다.
WARN/UBSAN/KASANdump_stack_lvl, __warn, report_bug, sanitizer helperhelper 체인 아래의 첫 번째 도메인 함수경고 프레임워크가 먼저 개입하므로 가장 위 helper만 보고 원인을 단정하면 안 됩니다.
Oops/page fault/BUGfault 함수 또는 예외 진입 프레임RIP가 가리키는 함수와 그 caller이 경우는 RIP가 실제 trap instruction을 짚어 주므로 우선순위가 더 높습니다.
NMI/lockupwatchdog, nmi_panic, IRQ/NMI entry helper컨텍스트 마커 아래 첫 번째 잠금/루프 함수비동기 선점이라 일반 함수 호출과 같은 의미로 읽으면 안 됩니다.
최상단 프레임은 항상 원인 함수가 아니다 1) 수동 dump_stack() dump_stack_lvl my_debug_checkpoint my_probe really_probe __driver_probe_device 원인 추적 시작점: helper 바로 아래 이 경우는 "여기서 찍어 본다"는 체크포인트 2) WARN / UBSAN / KASAN dump_stack_lvl __warn / report_bug / print_report my_locked_function process_one_work worker_thread helper 층을 걷어내고 첫 서브시스템 함수부터 읽음 sanitizer가 찍은 trace는 특히 이 구분이 중요 3) Oops / Page Fault RIP: my_faulting_fn+0x42 my_faulting_fn my_submit vfs_write syscall entry 이 경우 우선순위 1번은 RIP Call Trace는 caller 사슬 보강 자료

언와인더가 놓치거나 왜곡하는 프레임

dump_stack()은 즉시 사람이 읽을 trace를 만들고, arch_stack_walk()는 그 원재료를 공급합니다. 하지만 이 결과는 어디까지나 best-effort backtrace입니다. 인라인 함수, tail call, trampoline, 손상된 스택, 비동기 컨텍스트 중첩이 개입하면 Call Trace는 완전한 역사 기록이 아니라 복원 가능한 범위의 근사치가 됩니다.

현상로그에 보이는 모습왜 생기는가대응
인라인 함수 생략중간 helper가 trace에 안 보임별도 스택 프레임이 생기지 않기 때문faddr2line, objdump -dS로 해당 오프셋의 인라인 call site를 확인합니다.
tail call 압축caller 한 단계가 생략된 듯 보임함수 epilogue 없이 다음 함수로 점프trace만으로 caller 관계를 단정하지 말고 어셈블리를 함께 봅니다.
wrapper/trampoline 개입syscall/IRQ entry helper가 앞에 보임실제 비즈니스 함수보다 진입 wrapper가 먼저 쌓임helper 층을 걷어내고 첫 도메인 함수부터 읽습니다.
? 접두사 프레임불확실한 frame 표시언와인더가 확실하지 않은 주소를 추정RIP, 레지스터, 컨텍스트 마커와 교차 검증합니다.
스택 손상 / overflow중간에서 trace가 끊기거나 엉뚱한 함수가 섞임저장된 return address 자체가 손상guard page, KASAN, stack depth, large local array 여부를 확인합니다.
신뢰도 요구가 높은 작업일반 dump와 별개 API 사용livepatch 등은 불완전 trace를 허용하지 않음stack_trace_save_tsk_reliable()처럼 reliable API를 별도로 사용합니다.
언와인더가 보는 정상 체인과 왜곡 요인 정상 복원 current frame caller #1 caller #2 entry / worker / syscall return address가 온전하면 caller 체인이 그대로 복원됩니다. 압축 / 생략 current frame inline helper (프레임 없음) caller #1 tail-call로 압축된 caller 코드는 실행됐어도 trace에는 개별 프레임이 남지 않을 수 있습니다. 손상 / 불확실 current frame ? guessed frame corrupted return address trace terminated early 이 경우 RIP, Code, 레지스터가 trace보다 더 신뢰할 수 있습니다.

언와인드 결과가 로그로 보이기까지

수집된 stack frame 주소는 곧바로 화면에 그려지는 것이 아니라, 결국 printk 경로를 타고 로그 버퍼로 들어갑니다. 그래서 시리얼 콘솔에 일부만 보였더라도 dmesg, /dev/kmsg, pstore/ramoops, vmcore 안에는 더 많은 부분이 살아남아 있을 수 있습니다. 반대로 콘솔 잠금이 꼬였거나 재부팅이 너무 빨랐다면 화면에 안 보인다고 해서 dump가 전혀 생성되지 않은 것은 아닙니다.

단계무엇이 전달되는가현장 의미
stack_trace_print()주소 배열을 사람이 읽는 문자열로 변환여기서 보이는 형식이 fn+0xoff/0xsize 형태입니다.
printk 링 버퍼Call Trace 텍스트가 로그 버퍼에 적재콘솔 출력과 별도로 dmesg 조회, crash dump 추출이 가능합니다.
콘솔 / pstore / vmcore환경에 따라 화면, 영구 저장소, 메모리 덤프에 복제분석할 때는 "화면에 본 것"보다 "최종적으로 어디에 남았는가"가 더 중요합니다.

덤프 함수 관점에서 Call Trace를 읽는 실전 순서

  1. trace를 찍은 원인부터 구분
    dump_stack() 직접 호출인지, WARN 계열인지, 예외/Oops인지 먼저 분류합니다.
  2. helper 프레임과 원인 프레임을 분리
    dump_stack_lvl, __warn, sanitizer helper, entry wrapper는 보조 프레임으로 보고 첫 번째 도메인 함수부터 원인 추적을 시작합니다.
  3. Oops라면 RIP를 최우선으로 해석
    예외 경로에서는 Call Trace보다 RIP가 실제 fault instruction을 더 정확하게 가리킵니다.
  4. 출력 순서와 시간 순서를 따로 읽기
    위에서 아래로는 "현재 프레임 구조", 아래에서 위로는 "실제 호출 역사"를 복원합니다.
  5. trace를 맹신하지 말고 교차 검증
    faddr2line, decode_stacktrace.sh, objdump -dS, 레지스터 값, Code: 바이트를 같이 봅니다.
  6. trace가 불완전하면 신뢰도 높은 단서로 되돌아가기
    ? 프레임, 스택 손상, NMI/IRQ 중첩이 보이면 RIP, 접근 주소, poison 패턴, 컨텍스트 마커를 우선합니다.

Call Trace 유형별 해석

위 dump 경로를 이해하면 이제 <TASK>, <IRQ>, <NMI> 같은 컨텍스트 마커를 더 정확하게 읽을 수 있습니다. 아래 표는 "어떤 종류의 스택이 찍혔는가"를 분류하는 기준입니다.

Trace 헤더의미심각도조치
<TASK>프로세스 컨텍스트 스택가변해당 프로세스의 실행 경로 분석
<IRQ>인터럽트 컨텍스트 스택높음인터럽트 핸들러 내 버그
<NMI>NMI 컨텍스트 (watchdog 등)매우 높음hardlockup, 성능 카운터 오버플로
<SOFTIRQ>소프트IRQ 컨텍스트높음네트워크/블록 처리 경로 분석
[exception RIP: ...]예외 발생 지점높음RIP 주소의 명령어 확인
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# Call Trace 전체 소스 위치 변환
./scripts/decode_stacktrace.sh vmlinux /path/to/modules < oops.txt

# 주요 분석 포인트:
# 1. RIP (Instruction Pointer): 오류 발생 정확한 위치
RIP: 0010:my_function+0x28/0x60 [my_module]
# → my_function 시작으로부터 0x28 바이트 오프셋
# → 함수 전체 크기: 0x60 바이트

# 2. 오프셋으로 소스 라인 찾기
./scripts/faddr2line vmlinux 'my_function+0x28/0x60'
# 또는 모듈:
./scripts/faddr2line my_module.ko 'my_function+0x28/0x60'

# 3. objdump으로 어셈블리와 소스 매핑
objdump -dSl vmlinux | grep -A 20 "my_function+0x2"

# 4. 레지스터 값 분석
# RAX에 작은 값 (0x0~0xfff): NULL 포인터 역참조
# RAX에 0xdead000000000100: slab poison (use-after-free)
# RAX에 0x6b6b6b6b6b6b6b6b: slab free poison
# RAX에 0xa5a5a5a5a5a5a5a5: slab init poison
# RAX에 0x5a5a5a5a5a5a5a5a: red zone

# 5. Tainted 플래그 상세
# G: 모든 모듈이 GPL 호환
# P: proprietary 모듈 로드됨
# O: Out-of-tree 모듈 로드됨
# E: 서명 안 된 모듈 로드됨
# W: 이전에 WARN 발생
# C: 스테이징 드라이버 로드됨
# I: 플랫폼 펌웨어 버그 워크어라운드 적용
# D: 이전에 Oops 또는 BUG 발생
# T: 빌드 시간 또는 부팅 시간에 커널 테인트

Call Trace 패턴별 원인 분석

아래 패턴은 dump_stack_lvl, __warn, sanitizer helper처럼 "trace를 찍어 준 함수"를 먼저 걷어낸 뒤 읽어야 의미가 정확해집니다. 특히 KASAN/UBSAN/WARN 리포트는 최상단 몇 프레임이 진단 프레임워크일 수 있으므로, 첫 번째 서브시스템 함수가 어디서 시작되는지 먼저 표시해 두는 습관이 중요합니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# 패턴 1: NULL Pointer Dereference
BUG: kernel NULL pointer dereference, address: 0000000000000008
# → 구조체 멤버 접근 시 기본 포인터가 NULL
# → offset 0x008은 구조체의 두 번째 8바이트 멤버
# 대처: pahole 도구로 구조체 레이아웃 확인
pahole -C task_struct vmlinux | grep "0x008"

# 패턴 2: Use After Free
BUG: KASAN: slab-use-after-free in my_function+0x28
Read of size 8 at addr ffff88810a3b4c00 by task test/1234
# → 이미 해제된 slab 오브젝트에 접근
# → KASAN이 alloc/free 콜스택 모두 출력

# 패턴 3: Stack Overflow
BUG: stack guard page was hit at 00000000deadbeef
kernel stack overflow (double-fault)
# → 커널 스택(보통 16KB) 초과
# 원인: 깊은 재귀, 큰 스택 변수 (VLA, 큰 배열)
# 확인: CONFIG_FRAME_WARN=1024 (1KB 이상 스택 프레임 경고)

# 패턴 4: GPF (General Protection Fault)
general protection fault, probably for non-canonical address 0xdead000000000100
# → 비정상 주소 접근 (slab poison 패턴)
# → use-after-free 또는 uninitialized pointer

# 패턴 5: RCU stall
rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:
# → CPU가 오래동안 RCU grace period를 완료하지 못함
# 원인: 긴 루프에서 cond_resched() 미호출, 인터럽트 비활성화

Oops 메시지 완전 해부

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 ① 예외 헤더 • 접근한 주소, 접근 유형(read/write/exec) • error_code: bit0=present, bit1=write, bit2=user, bit4=insn RIP: 0010:my_function+0x42/0x100 [my_module] ② RIP CS:함수명+오프셋/함수크기 [모듈명] RSP: 0018:ffffc90001234e50 EFLAGS: 00000246 ③ RSP/EFLAGS SS:스택포인터 | IF=1(인터럽트), ZF, CF 등 RAX: 0000000000000000 RBX: ffff888123456000 RCX: 0000000000000000 RDX: 0000000000000001 RSI: ffff888123456040 RDI: 0000000000001234 ④ 범용 레지스터 • RDI=첫번째 인수 (여기서 0x1234=문제 주소) • poison/NULL 값 발견 시 오류 유형 즉시 판별 Modules linked in: my_module(OE) nvidia(POE) ext4 ⑤ 로드된 모듈 O=Out-of-tree, E=Unsigned, P=Proprietary CPU: 2 PID: 1234 Comm: my_app Tainted: G W OE 6.1.0 ⑥ 실행 컨텍스트 CPU번호, PID, 프로세스명, Tainted, 커널버전 Call Trace: <TASK> my_function+0x42/0x100 [my_module] caller+0x30/0x80 </TASK> ⑦ Call Trace (함수 호출 스택) • 상단 = 현재 프레임, 아래로 = 호출자 • Oops에서는 RIP가 실제 fault site를 가장 직접 가리킴 • <TASK> / <IRQ>는 컨텍스트 경계, [모듈명]은 모듈 소속 • ?접두사/도우미 프레임은 별도 해석 필요 Code: 48 8b 07 48 85 c0 74 12 <48> 8b 40 08 48 85 c0 74 05 e8 12 34 56 78 ⑧ Code 바이트 • < > 안의 바이트 = RIP이 가리키는 명령어 • objdump/ndisasm으로 디스어셈블 가능 ---[ end trace 0000000000000000 ]--- ⑨ 종료 마커 trace ID (6.1+ 항상 0, 이전에는 랜덤) 분석 우선순위: ② RIP (크래시 위치) → ① 헤더 (예외 유형) → ④ 레지스터 (인수 값) → ⑦ Call Trace (호출 경로) → ⑧ Code (명령어) RIP의 함수명+오프셋만으로도 대부분의 크래시 원인을 좁힐 수 있습니다. faddr2line으로 소스 라인을 확인하세요.

Code 바이트 디스어셈블

Oops의 Code: 줄은 RIP 주변의 기계어 바이트를 16진수로 표시합니다. < >로 감싼 바이트가 RIP이 가리키는 명령어의 시작입니다. 디버그 심볼이 없을 때 이 바이트를 디스어셈블하면 크래시 명령어를 정확히 알 수 있습니다.

# Code 바이트 디스어셈블 방법

# 예시 Code 줄:
# Code: 48 8b 07 48 85 c0 74 12 <48> 8b 40 08 48 85 c0 74 05 e8 12 34 56 78

# 방법 1: scripts/decodecode (커널 소스 내장)
echo "Code: 48 8b 07 48 85 c0 74 12 <48> 8b 40 08 48 85 c0 74 05 e8 12 34 56 78" \
    | scripts/decodecode
# 출력:
# All code
# ========
#    0: 48 8b 07          mov    (%rdi),%rax
#    3: 48 85 c0          test   %rax,%rax
#    6: 74 12             je     0x1a
#    8:*48 8b 40 08       mov    0x8(%rax),%rax    <-- trapping instruction
#    c: 48 85 c0          test   %rax,%rax
#    f: 74 05             je     0x16
#   11: e8 12 34 56 78    call   0x78563428
# → 오프셋 8의 "mov 0x8(%rax),%rax"에서 크래시
# → rax가 유효하지 않은 주소를 가리킴

# 방법 2: ndisasm (nasm 패키지)
echo -ne '\x48\x8b\x40\x08\x48\x85\xc0\x74\x05' | ndisasm -b64 -
# 00000000  488B4008          mov rax,[rax+0x8]
# 00000004  4885C0            test rax,rax
# 00000007  7405              jz 0xe

# 방법 3: objdump
echo -ne '\x48\x8b\x40\x08' > /tmp/code.bin
objdump -D -b binary -m i386:x86-64 /tmp/code.bin
# 0: 48 8b 40 08    mov    0x8(%rax),%rax

# Code 바이트 해석 핵심:
# - < > 안의 바이트 = 크래시 발생 명령어
# - 이전 바이트들(왼쪽) = 크래시 전 실행된 코드
# - 이후 바이트들(오른쪽) = 실행되지 않은 코드
# - 명령어가 메모리 접근이면 → 접근한 레지스터 값을 ④에서 확인

x86_64 커널 주소 공간 레이아웃

크래시 주소를 보고 어떤 영역인지 즉시 판별하면 분석 시간을 크게 단축할 수 있습니다. x86_64 리눅스 커널의 가상 주소 공간 레이아웃을 이해해야 합니다.

x86_64 커널 가상 주소 공간 (5-level PT, 128TB) 높은 주소 커널 텍스트 (.text, .rodata, .data) ffffffff80000000 ~ ffffffffa0000000 RIP이 이 범위 → 커널 코어 코드 _stext ~ _etext (vmlinux 심볼로 확인) 모듈 영역 ffffffffa0000000 ~ fffffffffeffffff RIP이 이 범위 → 로드된 모듈 코드 [module_name] 표시. KASLR 시 변동 vsyscall (레거시) ffffffffff600000 fixmap 영역 (APIC, IO 매핑 등) vmemmap (struct page 배열) ffffea0000000000 ~ (가변) struct page 접근 관련 크래시 SPARSEMEM_VMEMMAP 모델 vmalloc / ioremap 영역 ffffc90000000000 ~ (가변, ~32TB) vmalloc, vmap, ioremap 할당 커널 스택(VMAP_STACK)도 이 영역 Direct Mapping (전체 물리 메모리) ffff888000000000 ~ (page_offset_base) 물리 메모리 크기만큼 (예: 64GB → 64GB 매핑) kmalloc, 슬랩, 페이지 캐시 등 대부분의 커널 데이터 포인터가 이 범위 virt_to_phys(v) = v - page_offset_base 비정규(non-canonical) 주소 구멍 유저 공간 0000000000000000 ~ 00007fffffffffff (128TB) SMAP/SMEP 위반 시 page fault 발생 낮은 주소 크래시 주소 빠른 판별 가이드 • 0x0000~0x0FFF: NULL 포인터 역참조 (가장 흔함) • 0xdead...: LIST_POISON (list_del 후 접근) • 0x6b6b...: SLAB_POISON (해제 후 접근) • ffff8880...: Direct Map (일반 커널 데이터) • ffffc900...: vmalloc/커널스택/ioremap • ffffffff8...: 커널 텍스트/모듈 코드
주소 범위 (x86_64)영역크래시 시 의미주요 원인
0x0000~0x0FFFNULL 영역NULL 포인터 역참조초기화 누락, 경쟁 조건
0x0001000~0x00007FFF...유저 공간SMAP/SMEP 위반copy_from_user 미사용
0xdead000000000100LIST_POISON1list_del 후 next 접근이중 삭제, use-after-free
0xdead000000000122LIST_POISON2list_del 후 prev 접근이중 삭제, use-after-free
0xffff888000000000~Direct Map일반 커널 데이터 접근 오류해제된 오브젝트, 범위 초과
0xffffc90000000000~vmallocvmalloc/ioremap 영역 오류MMIO 접근 실패, 스택 오버플로우
0xffffea0000000000~vmemmapstruct page 접근 오류잘못된 PFN, 메모리 핫플러그 이슈
0xffffffff80000000~커널 텍스트코드 영역 쓰기 시도ROP/스택 손상 (W^X 위반)
0xffffffffa0000000~모듈모듈 코드/데이터 오류모듈 버그, 언로드된 모듈

decode_stacktrace.sh / faddr2line 실전 사용법

# 커널 소스 트리의 스택 트레이스 변환 도구

### 1. decode_stacktrace.sh ###
# Call Trace를 소스 파일:라인번호로 자동 변환

# 기본 사용법:
dmesg | scripts/decode_stacktrace.sh /usr/lib/debug/boot/vmlinux-$(uname -r)

# 모듈 경로 지정:
dmesg | scripts/decode_stacktrace.sh \
    /usr/lib/debug/boot/vmlinux-$(uname -r) \
    /usr/lib/debug/lib/modules/$(uname -r)/

# 파일에서 변환:
scripts/decode_stacktrace.sh vmlinux < oops_log.txt > decoded.txt

# 출력 예시 (변환 전):
# my_function+0x42/0x100 [my_module]
# 출력 예시 (변환 후):
# my_function (drivers/my_module/main.c:156) my_module

### 2. faddr2line ###
# 함수명+오프셋을 소스 파일:라인으로 변환

# 기본 사용법:
scripts/faddr2line vmlinux my_function+0x42/0x100
# my_function+0x42/0x100:
# my_function at drivers/my_module/main.c:156

# 모듈 함수:
scripts/faddr2line drivers/my_module/my_module.ko my_function+0x42/0x100

# 여러 주소 한 번에:
scripts/faddr2line vmlinux \
    schedule+0x5e/0xd0 \
    __schedule+0x2eb/0x8d0 \
    io_schedule+0x42/0x70

### 3. addr2line (binutils) ###
# RIP 절대 주소를 소스 라인으로 변환 (KASLR 미적용 vmlinux 기준)
addr2line -e vmlinux -f ffffffff81234567
# my_function
# /home/src/kernel/drivers/my_module/main.c:156

# KASLR 적용 시: RIP에서 KASLR 오프셋을 빼야 함
# KASLR 오프셋 = dmesg의 "KASLR Offset: 0x..." 또는 VMCOREINFO의 KERNELOFFSET
# 실제 오프셋 = RIP - KASLR_OFFSET
python3 -c "print(hex(0xffffffff83234567 - 0x2000000))"
# → addr2line에 이 값 사용

### 4. objdump (함수 전체 디스어셈블) ###
objdump -d -S --start-address=0xffffffff81234500 \
    --stop-address=0xffffffff81234600 vmlinux
# -S: 소스 코드와 어셈블리 인터리브
# → RIP 주변의 전체 함수 코드를 소스와 함께 확인

### 5. GDB 직접 사용 ###
gdb vmlinux
(gdb) list *my_function+0x42
# → 해당 오프셋의 소스 코드 출력
(gdb) disassemble my_function
# → 함수 전체 디스어셈블
(gdb) info line *0xffffffff81234567
# → 주소에 해당하는 소스 라인
💡

디버그 정보 없이 분석하기: CONFIG_DEBUG_INFO가 없는 커널에서도 Code: 바이트 디스어셈블, 함수명+오프셋, 레지스터 값으로 상당히 많은 정보를 얻을 수 있습니다. 특히 scripts/decodecode는 vmlinux 없이도 Code 바이트만으로 크래시 명령어를 보여줍니다. 배포판 커널의 디버그 심볼은 debuginfod 서비스를 통해 자동 다운로드할 수 있습니다: export DEBUGINFOD_URLS="https://debuginfod.ubuntu.com"

Softlockup / Hardlockup 심화

Lockup은 CPU가 일정 시간 이상 응답하지 않는 상황을 말합니다. watchdog 메커니즘이 이를 탐지하며, 시스템 장애 분석에서 가장 자주 마주치는 커널 메시지 중 하나입니다.

Softlockup vs Hardlockup

항목SoftlockupHardlockup
정의CPU가 일정 시간 커널 모드에서 스케줄링 없이 실행CPU가 일정 시간 인터럽트 처리 없이 실행
탐지 메커니즘hrtimer → watchdog kthread 스케줄링 여부NMI 기반 perf counter (PMU)
기본 타임아웃2 × watchdog_thresh (기본 20초)watchdog_thresh (기본 10초)
원인긴 루프, 높은 우선순위 작업 독점인터럽트 비활성화 상태 지속, 스핀락 데드락
심각도경고 (시스템 계속 동작 가능)심각 (시스템 응답 불가)
기본 동작WARN + 스택 트레이스 출력패닉 (watchdog_thresh 설정에 따라)
CONFIG 옵션SOFTLOCKUP_DETECTORHARDLOCKUP_DETECTOR

Watchdog 내부 메커니즘

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* kernel/watchdog.c — watchdog 감지 구조 */
/*
 * [Softlockup 감지]
 *
 *  Per-CPU hrtimer (주기: watchdog_thresh)
 *     │
 *     ├── hrtimer 콜백에서 watchdog_timer_fn() 실행
 *     │   ├── hrtimer_interrupts 카운터 증가
 *     │   ├── watchdog kthread가 마지막 터치한 타임스탬프 확인
 *     │   └── (현재 시각 - 마지막 터치) > 2 × watchdog_thresh ?
 *     │       └── YES → soft lockup 경고 출력
 *     │
 *     └── Per-CPU "watchdog/N" kthread
 *         └── 스케줄링될 때마다 타임스탬프 갱신 (touch)
 *             → kthread가 스케줄링 못 받으면 타임스탬프가 안 갱신
 *             → hrtimer가 stale 타임스탬프 감지 → softlockup!
 *
 * [Hardlockup 감지]
 *
 *  NMI (Non-Maskable Interrupt) — perf PMU counter 기반
 *     │
 *     ├── watchdog_overflow_callback() 에서
 *     │   hrtimer_interrupts 카운터 변화 확인
 *     │
 *     └── hrtimer도 실행 못 함 (인터럽트 비활성화 상태)
 *         → hrtimer_interrupts 변화 없음
 *         → NMI가 이를 감지 → hardlockup!
 */

static int is_softlockup(unsigned long touch_ts,
                          unsigned long period_ts)
{
    unsigned long now = get_timestamp();

    /* watchdog kthread의 마지막 터치로부터 경과 시간 */
    if (time_after(now, touch_ts + period_ts))
        return now - touch_ts;  /* lockup 지속 시간 반환 */

    return 0;
}
ℹ️

핵심 차이: softlockup은 "kthread가 스케줄링되지 못함"을 감지하고, hardlockup은 "hrtimer조차 실행되지 못함"을 감지합니다. hardlockup은 인터럽트까지 막혀 있으므로 NMI만이 이를 감지할 수 있습니다.

Lockup 관련 커널 파라미터

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# watchdog 타임아웃 (초)
sysctl kernel.watchdog_thresh=10    # soft: 20초, hard: 10초

# softlockup 발생 시 패닉 여부
sysctl kernel.softlockup_panic=0    # 0: 경고만, 1: 패닉
# 또는 부트 파라미터: softlockup_panic=1

# hardlockup 발생 시 패닉 여부
sysctl kernel.hardlockup_panic=0    # 0: 경고만 (불가능한 경우 있음), 1: 패닉
# 또는 부트 파라미터: nmi_watchdog=1 (hardlockup 활성화)

# watchdog 비활성화
sysctl kernel.watchdog=0            # 전체 비활성화
sysctl kernel.nmi_watchdog=0        # NMI watchdog만 비활성화
sysctl kernel.soft_watchdog=0       # soft watchdog만 비활성화

# 모든 CPU에서 backtrace 출력
sysctl kernel.softlockup_all_cpu_backtrace=0

# hung_task 탐지 (D 상태 프로세스)
sysctl kernel.hung_task_timeout_secs=120  # 기본 120초
sysctl kernel.hung_task_panic=0           # 1: hung_task 시 패닉

Softlockup 메시지 상세 해부

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# 실제 softlockup 메시지를 한 줄씩 해석합니다

watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [kworker/3:1:1234]
# ├── "CPU#3"       → lockup이 발생한 CPU 번호
# ├── "stuck for 22s" → 22초 동안 스케줄링 안 됨 (> 2×watchdog_thresh)
# └── "[kworker/3:1:1234]" → lockup 당시 실행 중인 태스크
#      kworker/3:1 = CPU 3의 workqueue worker #1
#      1234 = PID

Modules linked in: my_module(OE) nvidia(POE) ext4 mbcache jbd2
# 로드된 커널 모듈 목록 (괄호 안은 Tainted 플래그)
# 모듈별 플래그: O=Out-of-tree, E=unsigned, P=Proprietary

CPU: 3 PID: 1234 Comm: kworker/3:1 Tainted: G        W    OE 6.1.0
# ├── "Tainted: G   W   OE" → Tainted 플래그 (아래 표 참조)
# └── "6.1.0" → 커널 버전

Hardware name: Dell PowerEdge R740/0WGD1O, BIOS 2.17.1 01/15/2023
# 하드웨어 식별 (벤더, 모델, BIOS 버전)

RIP: 0010:_raw_spin_lock+0x15/0x30
# ├── "0010" → CS(Code Segment) 값 (커널 코드 = 0x0010)
# └── "_raw_spin_lock+0x15/0x30"
#      함수명 + 함수 시작으로부터 오프셋 / 함수 전체 크기
#      → _raw_spin_lock 함수의 0x15 바이트 위치에서 멈춤

RSP: 0018:ffffc90001234e50 EFLAGS: 00000246
# ├── RSP → 스택 포인터 (커널 스택 범위 확인에 유용)
# └── EFLAGS: 0x246 → IF=1(인터럽트 활성), ZF=1
#     0x200 = IF(Interrupt Flag) 비트
#     EFLAGS에서 IF=0이면 local_irq_disable() 상태

RAX: 0000000000000001 RBX: ffff888123456000 RCX: 0000000000000000
RDX: 0000000000000001 RSI: ffff888123456040 RDI: ffff888123456080
# 범용 레지스터 값 — 의심스러운 값 확인:
# 0x0000000000000000 (NULL 포인터?)
# 0xdead000000000100 (poison value → use-after-free)
# 0x6b6b6b6b6b6b6b6b (SLAB_POISON → freed memory)

Call Trace:
 <TASK>
 my_locked_function+0x45/0x120 [my_module]
 process_one_work+0x1e8/0x3c0
 worker_thread+0x50/0x3b0
 kthread+0xf5/0x130
 ret_from_fork+0x1f/0x30
 </TASK>

# Call Trace 컨텍스트 마커 해석:
# <TASK>  ... </TASK>  → 프로세스 컨텍스트 스택
# <IRQ>   ... </IRQ>   → 하드웨어 인터럽트 스택
# <NMI>   ... </NMI>   → NMI 스택
# <SOFTIRQ> ... </SOFTIRQ> → softirq 스택
# 마커가 중첩되면 인터럽트가 프로세스를 선점한 것

Tainted 플래그 전체 목록

Tainted: 필드는 커널의 "오염" 상태를 나타냅니다. 각 문자가 특정 조건을 표시하며, 커뮤니티 버그 리포트에서 중요한 정보입니다:

위치문자의미설명
0G/PGPLP=Proprietary 모듈 로드됨, G=GPL-only
1FForced모듈이 modprobe --force로 로드됨
2SUnsafe SMPSMP unsafe 모듈이 SMP 커널에 로드됨
3RForced unloadrmmod --force 사용됨
4MMCEMachine Check Exception 발생
5BBad page페이지 해제 시 불량 페이지 감지
6UUser request사용자가 tainted 플래그를 직접 설정
7DDie최근 OOPS/BUG 발생
8AACPI overrideACPI 테이블이 사용자에 의해 오버라이드됨
9WWarning이전에 WARN_ON 경고 발생
10CStagingstaging 드라이버 로드됨
11IFirmware bug플랫폼 펌웨어 버그 감지
12OOut-of-tree트리 외부 모듈 로드됨
13EUnsigned서명되지 않은 모듈 로드됨
14LSoft lockup이전에 soft lockup 발생
15KLive patch커널 라이브 패치 적용됨
16TTest테스트 taint (KUNIT 등)
17XAux보조 taint (배포판 정의)
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# tainted 값을 사람이 읽을 수 있는 형태로 변환
$ cat /proc/sys/kernel/tainted
12881

# 비트별 분석 (12881 = 0x3251)
$ python3 -c "
t=12881
flags='P F S R M B U D A W C I O E L K T X'.split()
for i,f in enumerate(flags):
    if t & (1<<i): print(f'  Bit {i:2d} ({1<<i:5d}): {f}')
"
#   Bit  0 (    1): P  → Proprietary 모듈
#   Bit  5 (   32): B  → Bad page 감지
#   Bit  6 (   64): U  → User request
#   Bit  9 (  512): W  → WARN 발생 이력
#   Bit 12 ( 4096): O  → Out-of-tree 모듈
#   Bit 13 ( 8192): E  → Unsigned 모듈

Softlockup 원인별 분석

원인Call Trace 특징EFLAGS 특징진단
spinlock 데드락_raw_spin_lock에서 멈춤IF=1 (인터럽트 활성)다른 CPU에서 같은 lock holder 확인
무한 루프특정 함수 내부에서 반복IF=1perf top -C N으로 hot function 확인
preempt_disable 장기화preempt_count > 0IF=1preempt_count 값 확인, cond_resched 누락
RT 태스크 CPU 독점RT 태스크의 일반 코드 실행IF=1ps -eo cls,rtprio,pid,comm
softirq 폭주<SOFTIRQ> 마커 내부IF=1/proc/softirqs 카운트 비교
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# 사례: spinlock 데드락으로 인한 softlockup

watchdog: BUG: soft lockup - CPU#2 stuck for 23s! [my_app:5678]
RIP: 0010:native_queued_spin_lock_slowpath+0x1c5/0x200
Call Trace:
 <TASK>
 _raw_spin_lock+0x30/0x40
 my_data_update+0x42/0x150 [my_module]
 my_ioctl_handler+0x88/0x200 [my_module]
 vfs_ioctl+0x21/0x40
 __x64_sys_ioctl+0x6a/0xa0
 </TASK>

# 분석 절차:
# 1. native_queued_spin_lock_slowpath → qspinlock slow path 진입
#    → 락을 매우 오래 기다리고 있음
# 2. 누가 이 lock을 잡고 있는지 찾아야 함:

# crash 도구에서 lock owner 확인
crash> struct my_data_struct.lock ffff888123456000
  lock = {
    val = {
      counter = 1     # locked 상태
    },
    locked = 1,
    pending = 0,
    locked_pending = 1,
    tail = 0
  }

# 모든 CPU의 backtrace에서 같은 lock 주소를 찾기
crash> bt -a | grep -B5 "my_data_update"
# → CPU 5에서도 같은 함수, 같은 lock을 잡고 대기 중
# → ABBA 데드락 또는 재귀적 lock 획득

Hardlockup 메시지 상세 해부

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# 실제 hardlockup 메시지

NMI watchdog: Watchdog detected hard LOCKUP on cpu 2
# NMI watchdog이 감지 → hrtimer조차 실행 안 됨
# → 인터럽트가 비활성화된 상태에서 CPU가 멈춤

CPU: 2 PID: 5678 Comm: my_driver Tainted: G           OE 6.1.0
RIP: 0010:_raw_spin_lock_irqsave+0x20/0x40
# irqsave → 인터럽트 비활성화 상태에서 spinlock 대기

RSP: 0000:ffffc90001abcde0 EFLAGS: 00000002
# EFLAGS: 0x02 → IF=0! 인터럽트 완전 비활성화!
# 이것이 softlockup과 hardlockup의 핵심 차이:
#   softlockup: EFLAGS IF=1 (인터럽트 활성, 스케줄링만 못 함)
#   hardlockup: EFLAGS IF=0 (인터럽트 비활성, NMI만 가능)

Call Trace:
 <NMI>                # NMI 컨텍스트에서 덤프됨
 _raw_spin_lock_irqsave+0x20/0x40
 my_irq_handler+0x30/0x80 [my_module]
 __handle_irq_event_percpu+0x44/0x1c0
 handle_irq_event+0x36/0x56
 handle_edge_irq+0x82/0x1a0
 __common_interrupt+0x42/0xa0
 common_interrupt+0x80/0xa0
 </NMI>
 <IRQ>                # NMI 진입 전 IRQ 컨텍스트에 있었음
 asm_common_interrupt+0x22/0x40
 </IRQ>
 <TASK>               # 원래 실행 중이던 프로세스 컨텍스트
 some_kernel_function+0x10/0x30
 ...
 </TASK>

# 해석: 프로세스가 some_kernel_function 실행 중
# → 인터럽트 발생 (common_interrupt)
# → my_irq_handler에서 spin_lock_irqsave 대기
# → 인터럽트 비활성화 상태로 무한 대기
# → hardlockup!

Hardlockup 주요 원인과 진단

원인특징진단
IRQ 핸들러 내 데드락Call Trace에 <IRQ>/<NMI>, spin_lock_irqsavelockdep(CONFIG_PROVE_LOCKING)으로 재현
IRQ 핸들러 무한 루프IRQ 핸들러 내부 함수가 RIP에 반복 출현하드웨어 상태 레지스터 확인, 인터럽트 storm
local_irq_disable() 복원 누락EFLAGS IF=0, 일반 코드 경로에서 발생코드 리뷰, lockdep irqsoff tracer
하드웨어 버스 행업MMIO 접근 함수에서 멈춤 (readl, writel)PCIe AER 로그, lspci -vvv, MCE 확인
펌웨어 SMI 과점불규칙, 모든 CPU 동시 stall 가능perf stat -e msr/tsc/으로 SMI 감지
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# hardlockup 디버깅 CONFIG 옵션
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PERF=y  # PMU 기반 (x86 기본)
CONFIG_DEBUG_SPINLOCK=y
CONFIG_PROVE_LOCKING=y

# hardlockup 시 시리얼 콘솔이 필수
# 부팅 파라미터:
console=ttyS0,115200n8 console=tty0

# NMI로 수동 크래시 유발 (iLO/IPMI BMC에서)
ipmitool chassis power diag  # NMI 전송
# unknown_nmi_panic=1 설정 시 NMI로 kdump 트리거

# irqsoff tracer: 인터럽트 비활성화 구간 추적
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 재현 ...
cat /sys/kernel/debug/tracing/trace
# 가장 긴 irqoff 구간과 함수 경로가 출력됨

Softlockup 디버깅 CONFIG 옵션

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
CONFIG_PROVE_LOCKING=y     # lockdep: 데드락 탐지
CONFIG_DEBUG_LOCK_ALLOC=y  # 락 할당 추적
CONFIG_LOCK_STAT=y         # 락 경합 통계
CONFIG_DEBUG_SPINLOCK=y    # 스핀락 디버깅

# perf로 lockup 원인 CPU 프로파일링
perf top -C 3              # CPU 3에서 가장 많이 실행되는 함수
perf record -C 3 -g sleep 10  # CPU 3 프로파일링

Hung Task 감지기

Hung task 감지기는 TASK_UNINTERRUPTIBLE(D 상태) 상태에서 너무 오래 머무는 프로세스를 탐지합니다. softlockup과 달리 CPU를 점유하지 않지만, I/O 대기나 lock 대기에서 빠져나오지 못하는 상황을 잡아냅니다.

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* kernel/hung_task.c — 감지 메커니즘 */
/*
 * khungtaskd 커널 스레드가 주기적으로 모든 태스크를 순회하며
 * TASK_UNINTERRUPTIBLE 상태에서 hung_task_timeout_secs 이상
 * 머무른 태스크를 찾아 경고를 출력합니다.
 *
 * 체크 주기: hung_task_check_interval_secs (기본 = timeout/5)
 */

static void check_hung_task(struct task_struct *t,
                             unsigned long timeout)
{
    unsigned long switch_count = t->nvcsw + t->nivcsw;

    /* 컨텍스트 스위치 카운터가 변하지 않으면 hung */
    if (switch_count == t->last_switch_count) {
        /* timeout 초과 시 경고 출력 */
        pr_err("INFO: task %s:%d blocked for more than "
               "%ld seconds.\\n",
               t->comm, t->pid, timeout);
        sched_show_task(t);  /* 스택 트레이스 출력 */
        hung_task_show_lock = true;

        if (sysctl_hung_task_panic)
            panic("hung_task: blocked tasks");
    }
    t->last_switch_count = switch_count;
}

Hung Task 메시지 상세 해석

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# 실제 hung_task 경고 메시지

INFO: task my_app:3456 blocked for more than 120 seconds.
      Not tainted 6.1.0 #1
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:my_app         state:D stack:13120 pid: 3456 ppid:  1200 flags:0x00004000
Call Trace:
 <TASK>
 __schedule+0x2eb/0x8d0
 schedule+0x5e/0xd0
 io_schedule+0x42/0x70
 folio_wait_bit_common+0x13a/0x310
 __filemap_get_folio+0x1e0/0x430
 filemap_fault+0x105/0x7e0
 __do_fault+0x32/0x130
 handle_mm_fault+0x6df/0xde0
 do_user_addr_fault+0x1c0/0x650
 exc_page_fault+0x78/0x170
 asm_exc_page_fault+0x22/0x30
 </TASK>

# 필드별 해석:
# "state:D"     → TASK_UNINTERRUPTIBLE (시그널로 깨울 수 없음)
# "stack:13120" → 사용 중인 커널 스택 크기 (바이트)
# "flags:0x00004000" → PF_MEMALLOC 등 태스크 플래그
#
# Call Trace 분석:
#   io_schedule → I/O 완료를 대기 중
#   folio_wait_bit_common → 페이지 캐시 I/O 대기
#   filemap_fault → 파일 매핑 페이지 폴트 처리 중
#   → 디스크 I/O가 완료되지 않아 프로세스가 무한 대기
#
# 원인 추정:
#   1. 디스크/스토리지 장애 (HDD/SSD 응답 없음)
#   2. NFS/CIFS 네트워크 파일시스템 서버 응답 없음
#   3. dm-multipath 경로 전환 지연
#   4. 블록 디바이스 드라이버 버그

Hung Task 원인별 진단

원인Call Trace 특징진단 명령어
디스크 I/O 장애io_schedule, blk_mq_get_tagiostat -x 1, dmesg | grep -i error
NFS 서버 응답 없음rpc_wait_bit_killable, nfs_file_*nfsstat -c, rpcdebug -m nfs
mutex 경합mutex_lock_killable, __mutex_lock.constproplockdep, /proc/lock_stat
메모리 부족 (OOM 대기)mem_cgroup_charge, __alloc_pages_slowpathfree -h, slabtop, dmesg | grep oom
dm/MD 장애dm_*, md_*dmsetup status, cat /proc/mdstat
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# Hung task 관련 sysctl 파라미터
kernel.hung_task_timeout_secs=120    # D 상태 타임아웃 (0=비활성화)
kernel.hung_task_panic=0             # 1이면 hung_task 시 패닉+kdump
kernel.hung_task_check_count=32768   # 한 번에 확인할 최대 태스크 수
kernel.hung_task_check_interval_secs=0 # 0=timeout/5 간격으로 체크
kernel.hung_task_warnings=10         # 최대 경고 출력 횟수 (-1=무제한)

# D 상태 프로세스 실시간 확인
ps aux | awk '$8 ~ /^D/ {print}'
# 또는
watch -n1 "cat /proc/*/status 2>/dev/null | grep -B1 'State.*sleeping'"

# SysRq로 모든 D 상태 태스크 덤프
echo w > /proc/sysrq-trigger
# dmesg에 모든 blocked 태스크의 스택이 출력됨
⚠️

NFS와 hung_task: NFS 마운트에서 서버 장애 시 hung_task 경고가 빈번하게 발생합니다. mount -o soft,timeo=50으로 soft mount를 사용하거나, hung_task_timeout_secs를 늘리는 것을 고려하세요. 프로덕션에서 hung_task_panic=1과 NFS를 함께 사용할 때는 주의가 필요합니다.

Lockup 종합 디버깅 순서

Lockup/Hang 디버깅 순서 1) 증상 분류 (로그 메시지) soft lockup / hard LOCKUP / blocked for more than / rcu_preempt stall / 메시지 없음(freeze) 먼저 dmesg + 시리얼 콘솔로 유형을 좁힘 2) EFLAGS.IF 확인 IF=1 → softlockup 가능성 / IF=0 → hardlockup 가능성 인터럽트 활성 여부로 1차 분기 3) Call Trace 컨텍스트 마커 <TASK>: 프로세스 컨텍스트 / <IRQ>: IRQ 핸들러 / <SOFTIRQ>: softirq <NMI>: NMI 덤프 컨텍스트 어느 실행 경로에서 멈췄는지 위치 결정 4) RIP 함수 패턴 해석 _raw_spin_lock*: 스핀락 홀더 추적 mutex_lock*: lock_stat/대기 체인 확인 io_schedule*: 스토리지/NFS/블록 경로 점검 native_halt/idle: starvation 혹은 wakeup 누락 확인 5) 전체 상태 캡처 + vmcore 확보 `echo l > /proc/sysrq-trigger` (모든 CPU backtrace) `echo t > /proc/sysrq-trigger` (모든 태스크 스택), `echo w` (D 상태) 필요 시 `echo c` 또는 원격 NMI로 panic 유도 후 kdump 수집 `crash` 도구로 vmcore 분석하여 재현/원인 고정

커널 패닉 심화

panic() 함수 내부 동작

panic()은 단순히 "에러 메시지를 찍고 멈추는 함수"가 아닙니다. 실제로는 패닉을 소유할 CPU를 하나 정하고, 다른 CPU를 정지시키고, 콘솔 출력을 강제로 풀고, 덤프를 남길 기회를 확보한 뒤, 재부팅 또는 정지 정책을 결정하는 __noreturn 비상 종료 경로입니다. 따라서 패닉 경로를 읽을 때는 "출력"만 보지 말고, 누가 패닉을 소유하는가, kdump가 어느 시점에 들어가는가, notifier가 어느 순서에 실행되는가, panic_timeout이 마지막에 어떤 종료 정책을 고르는가를 함께 봐야 합니다.

단계대표 함수/상태핵심 목적왜 중요한가
1. 패닉 소유권 확보panic_cpu, atomic_cmpxchg()여러 CPU가 동시에 패닉 경로를 밟아 로그를 망가뜨리는 것을 방지두 CPU가 동시에 콘솔과 kdump 경로를 건드리면 증거가 크게 손상됩니다.
2. 콘솔 긴급 모드 전환console_verbose(), bust_spinlocks(1)락이 꼬인 상태에서도 패닉 메시지를 최대한 콘솔로 밀어냄락 데드락 직후의 패닉에서는 이 단계가 없으면 핵심 메시지가 안 보일 수 있습니다.
3. 패닉 메시지 출력vscnprintf(), pr_emerg()"Kernel panic - not syncing"와 원인 문자열 기록운영 로그에서 사고를 분류하는 첫 단서입니다.
4. 다른 CPU 정지smp_send_stop(), 아키텍처별 stop IPI/NMI메모리와 장치 상태가 더 오염되는 것을 막음패닉 후에도 다른 CPU가 계속 실행되면 vmcore 신뢰도가 급격히 떨어집니다.
5. 덤프/알림 분기__crash_kexec(), panic_notifier_list, kmsg_dump()vmcore 확보와 긴급 하드웨어 덤프의 순서를 조정crash_kexec_post_notifiers 설정에 따라 분석 가능성과 상태 보존도 사이의 균형이 달라집니다.
6. 추가 정보 출력panic_print_sys_info(), panic_print=태스크/메모리/락/ftrace 상태를 더 출력출력은 늘어나지만 너무 많으면 kdump 전에 지연이나 오염을 키울 수 있습니다.
7. 종료 정책 선택panic_timeout, emergency_restart(), panic_blink()즉시 재부팅, 지연 재부팅, 무한 정지 중 하나를 선택운영 정책과 실험실 분석 정책이 갈리는 지점입니다.
/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* kernel/panic.c: panic() 실행 순서 */
void panic(const char *fmt, ...)
{
    /* 1. panic_cpu 확보: 이 CPU만 패닉 경로를 소유 */
    /* 2. console_verbose + bust_spinlocks: 긴급 출력 모드 */
    /* 3. 패닉 메시지 포맷팅 및 pr_emerg 출력 */
    /* 4. smp_send_stop(): 다른 CPU 정지 */
    /* 5. crash_kexec_post_notifiers == 0 이면 즉시 __crash_kexec() */
    /* 6. panic_notifier_list 콜백: 긴급 최소 덤프 */
    /* 7. kmsg_dump(): pstore/ramoops 등에 로그 저장 */
    /* 8. crash_kexec_post_notifiers == 1 이면 이 시점에 __crash_kexec() */
    /* 9. panic_print_sys_info()로 추가 상태 출력 */
    /* 10. panic_timeout에 따라 즉시/지연 재부팅 또는 무한 정지 */
}

/* panic notifier 등록 (모듈에서 패닉 전 정리 작업) */
static int my_panic_handler(struct notifier_block *nb,
                           unsigned long action, void *data)
{
    /* 최소한의 정리 작업만 수행 */
    /* 주의: 이 시점에서 시스템 상태가 불안정 */
    /* 락 획득, 메모리 할당 등 금지 */
    pr_emerg("my_module: panic cleanup\\n");
    return NOTIFY_DONE;
}

static struct notifier_block my_panic_nb = {
    .notifier_call = my_panic_handler,
    .priority = 200,  /* 높을수록 먼저 호출 */
};

/* 등록/해제 */
atomic_notifier_chain_register(&panic_notifier_list, &my_panic_nb);
atomic_notifier_chain_unregister(&panic_notifier_list, &my_panic_nb);
순서 해석 주의: panic()의 전체 구조는 비교적 안정적이지만, __crash_kexec()가 notifier보다 앞에 가는지 뒤에 가는지는 crash_kexec_post_notifiers 같은 설정과 빌드 옵션에 영향을 받습니다. 따라서 "항상 notifier 후 kdump"처럼 고정 순서로 외우기보다, vmcore 보존 우선 경로와 후처리 우선 경로 두 가지를 함께 이해하는 편이 맞습니다.

panic() 단계별 호출 함수 맵

아래 표는 커널 소스에서 panic()을 따라갈 때 어떤 함수를 왜 보는지 정리한 것입니다. 실제 분석에서는 panic() 자체보다, 어느 단계에서 시간이 오래 걸리거나 증거가 사라졌는지를 찾는 것이 더 중요합니다.

관심 함수읽어야 하는 이유관찰 포인트현장 질문
atomic_cmpxchg(&panic_cpu, ...)패닉 소유 CPU를 한 개로 제한동시 패닉인지, 이미 다른 CPU가 패닉 중이었는지"이 로그가 첫 패닉 CPU 로그인가?"
console_verbose()로그 레벨을 최대로 올림낮은 로그 레벨 때문에 핵심 메시지가 가려지지 않는지"마지막 메시지가 안 찍힌 이유가 로그 레벨 때문인가?"
bust_spinlocks(1)콘솔 잠금 교착 상태를 우회락 꼬임 직후에도 콘솔 출력이 가능한지"데드락 상황에서도 panic 문자열이 남았는가?"
smp_send_stop()다른 CPU의 추가 실행을 억제secondary CPU backtrace가 stop 경로를 탔는지"다른 CPU가 계속 메모리를 만져 vmcore를 오염시켰는가?"
atomic_notifier_call_chain()panic notifier 실행벤더 드라이버/BMC/스토리지 긴급 덤프 훅이 있는지"이 시점에 남긴 vendor dump가 있는가?"
kmsg_dump()pstore/ramoops 등 영구 로그 저장재부팅 후에도 패닉 직전 로그가 살아남는지"시리얼이 없어도 증거를 회수할 수 있는가?"
__crash_kexec()kdump 캡처 커널로 점프여기서 성공하면 이후 panic 본문은 더 진행되지 않음"vmcore가 왜 없었나, kexec가 실행되기 전에 멈췄나?"
panic_print_sys_info()추가 상태 출력panic_print 비트마스크 설정"추가 출력이 원인 규명에 도움인가, 지연만 키우는가?"
emergency_restart()최종 강제 재시작watchdog/kdump와 경쟁하지 않는지"자동 복구가 필요한가, 멈춰서 분석해야 하는가?"

panic() 실행 흐름 상세

panic() 함수 실행 흐름 1. 진입 panic_cpu = smp_processor_id() console_verbose() 2. 다른 CPU 정지 smp_send_stop() → NMI로 모든 CPU 중지 3. 패닉 알림 panic_notifier_list → 등록된 핸들러 호출 4. 로그 저장 kmsg_dump(KMSG_DUMP_PANIC) → pstore/ramoops 기록 5. kdump 시도 __crash_kexec() → 캡처 커널로 전환 (kexec_crash_loaded==1) 성공 시: 여기서 캡처 커널 부팅 (이후 단계 실행 안 됨) kdump 성공 캡처 커널 부팅 미설정/실패 6. 패닉 출력 panic_print_sys_info() → panic_print= 비트마스크 7. 재부팅 판단 panic_timeout 확인 >0: N초 후 재부팅 timeout > 0 touch_nmi_watchdog() mdelay(timeout*1000) → emergency_restart() timeout == 0 local_irq_enable() panic_blink() → 무한 루프 (시스템 멈춤) timeout < 0 → 즉시 emergency_restart() 핵심: kdump가 설정되어 있으면 단계 5에서 캡처 커널로 전환되어 이후 단계는 실행되지 않음. 프로덕션에서는 반드시 kdump를 설정하여 vmcore를 확보하고, panic=10으로 자동 재부팅을 활성화하십시오.
위 그림의 해석: 이 그림은 panic()의 큰 단계만 보여 주는 단순화 버전입니다. 실제 커널에서는 crash_kexec_post_notifiers 설정에 따라 __crash_kexec()가 notifier와 kmsg_dump()보다 앞서 실행될 수도 있습니다. 아래의 추가 그림은 바로 그 분기를 세밀하게 풀어낸 것입니다.
/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* panic() 상세 흐름 (kernel/panic.c, 6.x 기준) */
void panic(const char *fmt, ...)
{
    long i, i_next = 0, len;
    int state = 0;
    int old_cpu, this_cpu;

    /* 1. 패닉 CPU 등록 (중복 패닉 방지) */
    this_cpu = raw_smp_processor_id();
    old_cpu = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);
    if (old_cpu != PANIC_CPU_INVALID && old_cpu != this_cpu)
        panic_smp_self_stop();  /* 다른 CPU가 이미 패닉 중 → 자신은 정지 */

    /* 2. 콘솔 최대 로그 레벨 설정 */
    console_verbose();
    bust_spinlocks(1);  /* 스핀락 무시하고 콘솔 출력 */

    /* 3. 패닉 메시지 포맷팅 및 출력 */
    va_start(args, fmt);
    len = vscnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    pr_emerg("Kernel panic - not syncing: %s\\n", buf);

    /* 4. 다른 모든 CPU 정지 */
    smp_send_stop();
    /* x86: NMI IPI 전송 → 각 CPU가 crash_nmi_callback에서 정지 */
    /* ARM64: smp_cross_call → IPI 전송 */

    /* 5. vmcore 보존 우선 경로 */
    if (!crash_kexec_post_notifiers)
        __crash_kexec(NULL);
    /* 성공하면 여기서 캡처 커널로 전환 */

    /* 6. panic notifier 체인 호출 */
    atomic_notifier_call_chain(&panic_notifier_list, 0, buf);
    /* 등록된 모듈의 정리 코드 실행 (최소한의 작업만!) */

    /* 7. kmsg_dump → pstore/ramoops에 로그 저장 */
    kmsg_dump(KMSG_DUMP_PANIC);

    /* 8. notifier 후 kdump를 원하면 여기서 시도 */
    if (crash_kexec_post_notifiers)
        __crash_kexec(NULL);
    /* notifier가 더 중요할 때 선택하지만, 상태 오염 가능성은 커짐 */

    /* 9. kdump 실패 시 대안 */
    panic_print_sys_info(false);
    /* panic_print 비트마스크에 따라 추가 정보 출력 */

    /* 10. 재부팅 또는 무한 루프 */
    if (panic_timeout > 0) {
        pr_emerg("Rebooting in %d seconds..\\n", panic_timeout);
        for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
            touch_nmi_watchdog();
            mdelay(PANIC_TIMER_STEP);
        }
        emergency_restart();
    } else if (panic_timeout < 0) {
        emergency_restart();  /* 즉시 재부팅 */
    }

    /* timeout == 0: 무한 루프 (콘솔에서 분석 시간 확보) */
    local_irq_enable();
    for (i = 0; ; i += PANIC_TIMER_STEP) {
        touch_softlockup_watchdog();
        panic_blink(state ^= 1);  /* 키보드 LED 깜빡임 */
        mdelay(PANIC_TIMER_STEP);
    }
}

재진입 방지와 SMP 정지 경로

패닉 경로를 깊게 읽을 때 가장 먼저 봐야 하는 것은 "누가 패닉을 소유했는가"입니다. 예를 들어 CPU0이 이미 패닉을 시작했는데 CPU1이 같은 원인 또는 2차 손상 때문에 다시 panic()을 호출할 수 있습니다. 이때 모든 CPU가 각자 dump와 reboot를 시도하면 로그도 깨지고, kdump도 실패하고, 장치 상태도 더 오염됩니다. 그래서 커널은 panic_cpu 같은 전역 상태를 이용해 첫 번째 CPU만 본 경로를 계속 진행하게 하고, 나머지 CPU는 자기 자신을 멈추는 보조 경로로 보냅니다.

/* 개념 예시: 재진입 방지 핵심만 정리 */
this_cpu = raw_smp_processor_id();
old_cpu = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);

if (old_cpu == PANIC_CPU_INVALID) {
    /* 내가 첫 번째 패닉 CPU → 전체 흐름 계속 진행 */
} else if (old_cpu != this_cpu) {
    /* 이미 다른 CPU가 패닉 소유권 보유 */
    panic_smp_self_stop();
    /* 여기서 더 이상 notifier/kdump/reboot 정책에 개입하지 않음 */
} else {
    /* 같은 CPU에서 재귀 패닉 → 최소한의 출력만 시도하고 종료 */
}
panic 재진입 제어와 SMP 정지 CPU0: 첫 패닉 진입 atomic_cmpxchg(&panic_cpu, INVALID, 0) 성공 콘솔 출력, smp_send_stop, kdump 분기 진행 이 CPU만 "본 패닉 경로"를 계속 밟음 CPU1: 뒤늦은 패닉 또는 2차 손상 atomic_cmpxchg 실패, old_cpu = 0 panic_smp_self_stop() 추가 출력/덤프를 하지 않고 정지 CPU2: stop IPI/NMI 수신 smp_send_stop() 또는 아키텍처별 정지 콜백 새 I/O, 새 메모리 쓰기, 새 스케줄링 억제 vmcore 오염 최소화 핵심 의도 패닉은 "모든 CPU가 동시에 달려드는 에러 경로"가 아니라 "한 CPU가 증거를 보존하고, 나머지 CPU는 더 망가뜨리지 않게 멈추는 경로"로 설계됩니다. NMI 컨텍스트 예외 NMI 안에서 다시 일반 panic 경로를 타면 stop IPI와 콘솔 flush가 꼬일 수 있습니다. 이 경우는 nmi_panic() 같은 NMI-safe 경로와 함께 봐야 합니다.

NMI 예외 경로: NMI 안에서 발생한 패닉은 일반 stop IPI 경로와 충돌할 수 있으므로, 자세한 배경은 NMI 문서의 nmi_panic() 설명과 함께 보는 편이 좋습니다.

kdump, notifier, kmsg_dump 순서 분기

패닉 경로에서 가장 자주 오해되는 부분이 "kdump와 notifier 중 무엇이 먼저인가"입니다. 답은 항상 동일하지 않다입니다. 일반적으로는 vmcore 보존을 우선하려고 __crash_kexec()를 최대한 앞에 두는 편이 유리하지만, 특정 장치의 긴급 덤프를 notifier에서 남겨야 한다면 notifier를 먼저 실행하고 나중에 kdump로 가는 경로를 선택할 수도 있습니다. 이때 얻는 장점과 잃는 안정성이 동시에 존재합니다.

kdump와 notifier 순서 분기 공통 시작점 panic 메시지 출력 + 다른 CPU 정지 완료 경로 A: vmcore 보존 우선 보통 더 안전한 기본 사고방식 1) __crash_kexec() 먼저 시도 2) 성공 시 캡처 커널로 즉시 점프 3) 실패했을 때만 notifier / kmsg_dump / panic_print 장점: 메모리 상태 오염이 가장 적음 경로 B: notifier 후 kdump crash_kexec_post_notifiers=1 개념 1) panic notifier 먼저 실행 2) kmsg_dump(), 추가 장치 로그 확보 3) 마지막에 __crash_kexec() 시도 장점: 긴급 장치 덤프 기회 증가, 단점: 상태 오염 위험 증가 둘 다 정답일 수 있지만, 운영 목적이 "증거 보존"인지 "추가 장치 덤프"인지 먼저 정해야 합니다.
선택유리한 상황대가
kdump 먼저메모리 손상 원인을 정확히 보존해야 할 때, 일반적인 프로덕션 vmcore 수집panic notifier가 남길 장치 전용 덤프 기회가 줄어듭니다.
notifier 먼저펌웨어 레지스터, HBA 상태, 벤더 디버그 버퍼를 반드시 패닉 직후 읽어야 할 때notifier 수행 중 추가 오염이나 지연으로 vmcore 순도가 낮아질 수 있습니다.

panic notifier에서 허용되는 일과 금지되는 일

panic notifier는 "정리 루틴"처럼 보여도 정상 종료 경로가 아닙니다. 이미 락, 인터럽트, 메모리, 장치 상태가 깨졌을 가능성이 높기 때문에 아주 짧고, 대기하지 않고, 실패해도 추가 피해가 적은 작업만 해야 합니다.

가능하면 해도 되는 일피해야 하는 일이유
읽기 위주의 MMIO 레지스터 덤프mutex 획득, down(), schedule()panic notifier는 atomic 성격의 긴급 경로라 슬립이 치명적입니다.
고정 버퍼에 짧은 로그 복사kmalloc(GFP_KERNEL), 대형 문자열 포맷팅메모리 상태가 이미 손상됐을 수 있어 추가 할당이 위험합니다.
하드웨어 상태 비트 몇 개 캡처복잡한 장치 reset, 긴 폴링 루프reset은 오히려 vmcore와 장치 단서를 지워 버릴 수 있습니다.
사전 예약된 버퍼나 SRAM에 emergency dump재귀적으로 또 다른 panic 유발2차 패닉은 첫 번째 증거를 크게 망가뜨립니다.
notifier 작성 원칙: panic notifier는 "마지막 기회에 최소한만 남긴다"가 원칙입니다. 장황한 로그, 락, 메모리 할당, 장시간 장치 접근은 오히려 panic()의 본래 목적이던 증거 보존을 해칠 수 있습니다.

콘솔 flush와 panic_timeout의 마지막 의미

패닉을 읽을 때 마지막 분기는 생각보다 중요합니다. panic_timeout == 0이면 시스템은 분석을 위해 멈춰 있고, > 0이면 정해진 시간 뒤 자동 재부팅을 시도하며, < 0이면 사실상 즉시 강제 재시작 경로로 갑니다. 따라서 운영 중 "패닉 메시지가 잠깐 보이고 바로 재부팅됐다"면, 원인 분석보다 먼저 kernel.panic 값부터 확인해야 합니다.

panic_timeout동작현장 의미
0무한 대기 + panic_blink() 류의 정지 루프시리얼 콘솔이나 현장 화면에서 직접 증거를 읽기 좋습니다.
> 0N초 대기 후 emergency_restart()프로덕션 자동 복구에 유리하지만, 현장 분석 시간은 짧아집니다.
< 0즉시 강제 재부팅복구 최우선 정책이지만, 시각적 콘솔 증거는 거의 못 봅니다.
실전 해석 순서: 패닉 로그를 읽을 때는 1) 누가 panic()을 호출했는지, 2) kdump가 실제 실행됐는지, 3) notifier가 개입했는지, 4) panic_timeout 때문에 너무 빨리 재부팅된 것은 아닌지 순으로 확인하면 훨씬 빠르게 상황을 좁힐 수 있습니다.

패닉 관련 커널 파라미터 총정리

파라미터기본값설명
panic=N0패닉 후 N초 뒤 재부팅. 0=재부팅 안 함. -1=즉시 재부팅
panic_on_oops=N01이면 Oops 시 즉시 패닉 (kdump 유발)
panic_on_warn=N01이면 WARN 시 패닉 (디버깅 전용)
panic_on_rcu_stall=N01이면 RCU stall 시 패닉
panic_on_io_nmi=N01이면 I/O NMI 시 패닉
panic_on_unrecovered_nmi=N01이면 처리 불가 NMI 시 패닉
panic_print=N0패닉 시 추가 정보 출력 (비트마스크)
softlockup_panic=N01이면 softlockup 시 패닉
hardlockup_panic=N01이면 hardlockup 시 패닉
hung_task_panic=N01이면 hung_task 시 패닉
unknown_nmi_panic=N01이면 알 수 없는 NMI 시 패닉 (IPMI 디버깅)
oops_limit=N10000이 횟수 초과 시 패닉 (커널 6.2+)
warn_limit=N0이 횟수 초과 시 패닉 (0=무제한, 커널 6.2+)
# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# panic_print 비트마스크 값
# Bit 0 (1):  모든 태스크 정보 출력
# Bit 1 (2):  시스템 메모리 정보 출력
# Bit 2 (4):  타이머 정보 출력
# Bit 3 (8):  락/의존성 정보 출력 (lockdep)
# Bit 4 (16): ftrace 버퍼 출력
# Bit 5 (32): 모든 printk 메시지 버퍼 출력
# 예: panic_print=63 → 모든 정보 출력

# 프로덕션 서버 권장 패닉 설정
# /etc/sysctl.d/99-panic.conf
kernel.panic = 10              # 10초 후 재부팅
kernel.panic_on_oops = 1       # Oops 시 패닉 (kdump 수집)
kernel.softlockup_panic = 1    # softlockup 시 패닉
kernel.unknown_nmi_panic = 1   # BMC NMI 수신 시 패닉 (원격 디버깅)
kernel.panic_print = 0         # 패닉 시 추가 출력 비활성화 (kdump 방해 가능)

# 개발 환경 권장 패닉 설정
kernel.panic = 0               # 재부팅 안 함 (분석 시간 확보)
kernel.panic_on_oops = 1       # Oops 시 패닉
kernel.panic_on_warn = 0       # WARN은 경고만 (너무 잦음)
kernel.softlockup_panic = 0    # softlockup 경고만

WARN_ON / BUG_ON 심화 분석

WARN 메시지 분석 패턴

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# WARN_ON 출력 예시
------------[ cut here ]------------
WARNING: CPU: 1 PID: 3456 at net/core/skbuff.c:456 skb_under_panic+0x15/0x20
Modules linked in: my_module(OE) ...
CPU: 1 PID: 3456 Comm: my_app Tainted: G        W    OE 6.1.0
RIP: 0010:skb_under_panic+0x15/0x20
Call Trace:
 skb_push+0x45/0x50
 my_tx_handler+0x89/0x120 [my_module]
 dev_hard_start_xmit+0xd4/0x1f0
 ...
---[ end trace 0000000000000000 ]---

# 분석 절차:
# 1. "WARNING: ... at 파일:라인" → 소스 코드 확인
# 2. Call Trace → 호출 경로 추적
# 3. Tainted 플래그 'W' → 이전 WARN 발생 이력

# WARN 발생 횟수 확인
cat /proc/sys/kernel/tainted
# 비트 연산으로 해석:
python3 -c "t=512; print([(i,n) for i,n in enumerate(['P','F','S','R','M','B','U','D','A','W','C','I','O','E','L','K','T','X']) if t & (1<<i)])"

# WARN rate limiting
# WARN_ON_ONCE: 같은 위치에서 한 번만 경고
# WARN_RATELIMITED: 시간당 최대 횟수 제한
# net_warn_ratelimited: 네트워크 경로 전용

WARN/BUG 디버깅 전략

/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* 올바른 WARN/BUG 사용 가이드라인 */

/* GOOD: 복구 가능한 오류 → WARN_ON_ONCE + 에러 반환 */
if (WARN_ON_ONCE(size > MAX_SIZE))
    return -EINVAL;

/* GOOD: 발생하면 안 되는 상태 확인 → WARN_ON */
WARN_ON(refcount_read(&obj->ref) < 0);

/* BAD: BUG()는 커널을 불안정하게 만듦 */
/* BUG_ON(condition); → 거의 항상 사용하지 말 것 */

/* GOOD: 치명적 상태에서도 에러 반환 선호 */
if (impossible_state) {
    pr_crit("Impossible state detected: %d\\n", state);
    WARN_ON_ONCE(1);
    return -EIO;
}

/* 커널 6.2+: warn_limit / oops_limit 활용 */
/* warn_limit=100: WARN이 100회 초과 시 패닉 */
/* 이를 통해 빈번한 WARN이 시스템을 degradation시키는 것을 방지 */

crash 유틸리티 완전 가이드

crash는 vmcore(메모리 덤프)를 분석하는 핵심 도구입니다. GDB 기반으로 구축되어 커널 구조체 검사, 프로세스 분석, 메모리 추적 등을 수행합니다. 이 섹션에서는 명령어별 상세 사용법과 실전 분석 워크플로우를 다룹니다.

crash 시작과 기본 설정

# crash 유틸리티 설치
# RHEL/CentOS:
yum install crash
# Debian/Ubuntu:
apt install crash
# 소스 빌드 (최신 버전):
git clone https://github.com/crash-utility/crash.git
cd crash && make target=ARM64  # 크로스 아키텍처 빌드

# crash 시작 방법
# 1. vmcore 분석 (가장 일반적)
crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/*/vmcore

# 2. 실행 중인 시스템 분석 (라이브 모드)
crash /usr/lib/debug/boot/vmlinux-$(uname -r)
# /dev/crash 또는 /proc/kcore 사용

# 3. makedumpfile 포맷 (dumpfile)
crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/vmcore.flat

# 4. KASLR 환경 (자동 감지, 수동 지정 가능)
crash --kaslr=0x2a000000 vmlinux vmcore

# 5. 디버그 심볼 패키지 필요
# RHEL: yum install kernel-debuginfo-$(uname -r)
# Ubuntu: apt install linux-image-$(uname -r)-dbgsym
# 또는 debuginfod 사용: export DEBUGINFOD_URLS="https://debuginfod.ubuntu.com"

핵심 명령어 레퍼런스

명령어기능주요 옵션사용 예시
bt백트레이스-a 전체CPU, -f 풀프레임, -l 소스라인, -e 에러프레임bt, bt -a, bt 1234
log커널 로그 (dmesg)-m 메시지 레벨, -t 타임스탬프, -d 사전 정보log, log -t
ps프로세스 목록-k 커널, -u 유저, -G 스레드그룹, -S D상태만ps, ps -S
struct구조체 조회-o 오프셋struct task_struct.comm ffff...
rd메모리 읽기-8/-16/-32/-64 크기, -s 심볼, -a ASCIIrd ffff... 16
dis디스어셈블-l 소스 라인, -r 역방향dis my_function, dis -l ffff...
vm가상메모리 맵-p 물리 매핑, -f 플래그vm 1234
files열린 파일PID 지정files 1234
kmem메모리 정보-i 전체, -s 슬랩, -z 존, -f 프리kmem -i, kmem -s
mod모듈 정보-s 심볼 로드, -S 디렉토리mod, mod -s mymod
dev디바이스-d 디스크, -i I/O 포트dev -d
net네트워크-s 소켓, -a ARPnet, net -s
tasktask_structPID 또는 주소task 1234
foreach반복 실행bt, task, vmforeach bt, foreach RU bt
runq런큐 정보-t 타임스탬프, -m msrunq
irq인터럽트-s 통계, -a 어피니티irq -s
timer타이머timer
swap스왑 정보swap
mount마운트 정보mount
sys시스템 정보-c cmdlinesys
set컨텍스트 변경PID 또는 CPUset 1234, set -c 0
search메모리 검색-k 커널, -u 유저search -k deadbeef
waitq대기큐waitq ffff...
ptov/vtop주소 변환vtop ffff..., ptov 0x1234

실전 분석 예제

# crash 유틸리티 실전 분석 워크플로우

### 1단계: 시스템 상태 파악 ###
crash> sys
      KERNEL: /usr/lib/debug/boot/vmlinux-6.1.0
    DUMPFILE: /var/crash/127.0.0.1-2024-01-15/vmcore  [PARTIAL DUMP]
        CPUS: 8
    NODENAME: production-server-01
     RELEASE: 6.1.0-amd64
     VERSION: #1 SMP PREEMPT_DYNAMIC
     MACHINE: x86_64
      UPTIME: 45 days, 12:34:56
LOAD AVERAGE: 42.5, 38.2, 35.1     ← 매우 높은 부하
       TASKS: 1234
    PANIC: "Kernel panic - not syncing: softlockup: hung tasks"

### 2단계: 패닉 원인 확인 ###
crash> bt
PID: 0      TASK: ffffffff82c14940  CPU: 3   COMMAND: "swapper/3"
 #0 [ffff888001abcde0] machine_kexec at ffffffff81060c2a
 #1 [ffff888001abce38] __crash_kexec at ffffffff81137def
 #2 [ffff888001abcf00] panic at ffffffff81b5f2e3
 #3 [ffff888001abcf80] watchdog_timer_fn at ffffffff8115f69a
 #4 [ffff888001abcfb0] __hrtimer_run_queues at ffffffff81117e40
 ...

# → watchdog_timer_fn에서 softlockup 감지 후 panic

### 3단계: 모든 CPU 상태 확인 ###
crash> bt -a
# 8개 CPU 모두의 백트레이스가 출력됨
# CPU별로 어떤 태스크가 실행 중이었는지 확인

# CPU 5에서 의심스러운 스택 발견:
PID: 5678   TASK: ffff888123456000  CPU: 5   COMMAND: "kworker/5:1"
 #0 [ffff888234567890] _raw_spin_lock at ffffffff81b5e930
 #1 [ffff888234567898] my_data_lock+0x42/0x100 [my_module]
 #2 [ffff888234567900] process_one_work at ffffffff810a8e48
 ...

### 4단계: 특정 프로세스 상세 분석 ###
crash> set 5678
crash> bt -f -l
# -f: 각 프레임의 스택 데이터 표시
# -l: 소스 파일과 라인 번호 표시

# 레지스터 검사
crash> bt -e
# 에러 프레임의 레지스터 상태 표시

### 5단계: 구조체 필드 조사 ###
crash> struct task_struct.comm,pid,state,flags ffff888123456000
  comm = "kworker/5:1"
  pid = 5678
  state = 2  # TASK_UNINTERRUPTIBLE
  flags = 0x4208060

# 모듈의 커스텀 구조체 조사
crash> struct my_data_struct ffff888abcdef000
  lock = {
    val = { counter = 1 },    # locked
    ...
  }
  refcount = { refs = { counter = -1 } }   ← 비정상! 음수 refcount

### 6단계: 메모리 직접 읽기 ###
crash> rd ffff888abcdef000 32
ffff888abcdef000:  0000000000000001 ffff888123456789   ........y.E.#...
ffff888abcdef010:  dead000000000100 6b6b6b6b6b6b6b6b   ........kkkkkkkk
#                  ^                ^
#                  SLAB_POISON      freed memory pattern
# → use-after-free 의심!

### 7단계: 디스어셈블 ###
crash> dis -l my_data_lock+0x42
/home/src/my_module/main.c: 156
0xffffffffa0001042 <my_data_lock+0x42>: mov    (%rbx),%rax
# rbx = ffff888abcdef000 → freed object를 참조

### 8단계: D 상태 프로세스 전수 조사 ###
crash> ps -S
# TASK_UNINTERRUPTIBLE(D) 상태인 프로세스만 표시
   PID  PPID  CPU   TASK            ST  %MEM     VSZ    COMM
  5678  1200   5  ffff888123456000  UN  0.1   123456    kworker/5:1
  6789  1     2  ffff888234567000  UN  0.2   234567    my_app
  7890  1     7  ffff888345678000  UN  0.1   345678    my_service

# 각 D 상태 프로세스의 스택 확인
crash> foreach UN bt
# → 모두 같은 lock을 기다리고 있으면 → 데드락!

### 9단계: 커널 로그 확인 ###
crash> log | tail -50
# 패닉 직전 마지막 50줄의 커널 로그

### 10단계: 메모리 사용량 확인 ###
crash> kmem -i
                 PAGES     TOTAL    PERCENTAGE
    TOTAL MEM  4194304     16 GB         ---
         FREE   102400    400 MB          2%   ← 거의 고갈!
         USED  4091904   15.6 GB         97%
       SHARED   524288      2 GB         12%
      BUFFERS     8192     32 MB          0%
       CACHED  1048576      4 GB         25%
         SLAB   262144      1 GB          6%

# 슬랩 캐시 상세
crash> kmem -s | sort -k5 -nr | head -10
# 가장 많은 메모리를 사용하는 슬랩 캐시 상위 10개

고급 분석 기법

# crash 고급 분석 기법

### 특정 값이 담긴 메모리 검색 ###
crash> search -k dead000000000100
# 커널 메모리 전체에서 poison 값 검색
# use-after-free 오염 범위 파악에 유용

### 가상↔물리 주소 변환 ###
crash> vtop ffff888123456000
VIRTUAL     PHYSICAL
ffff888123456000  123456000

PGD ENTRY    PMD ENTRY    PTE ENTRY
8000000ab4001067 00000001234fe063 800000012345e163

      PTE         PHYSICAL   FLAGS
800000012345e163  12345e000  (PRESENT|RW|ACCESSED|DIRTY|NX)

### 대기큐 분석 (lock contention) ###
crash> waitq ffff888abcdef100
# 특정 wait_queue에서 대기 중인 태스크 목록

### 락 소유자 추적 ###
# mutex의 owner 필드로 락 보유자 확인
crash> struct mutex.owner ffff888abcdef200
  owner = {
    counter = 0xffff888123456001  # 하위 비트 제거 → task_struct 주소
  }
crash> struct task_struct.comm,pid 0xffff888123456000
  comm = "my_worker"
  pid = 4567

### spinlock 소유자 추적 (CONFIG_DEBUG_SPINLOCK) ###
crash> struct raw_spinlock.owner_cpu ffff888abcdef300
  owner_cpu = 5  # CPU 5가 보유 중

### GDB 내장 명령어 활용 ###
crash> gdb print ((struct my_struct *)0xffff888abcdef000)->field
crash> gdb x/20i 0xffffffff81234567
# 20개 명령어를 디스어셈블
crash> gdb p jiffies
# 전역 변수 값 확인

### 타임라인 재구성 ###
crash> log -t | grep -E "^\[.*\]" | tail -100
# 타임스탬프 기반으로 이벤트 순서 재구성

### 네트워크 상태 확인 ###
crash> net
# 네트워크 인터페이스 목록 및 상태
crash> net -s
# 소켓 목록 (TCP/UDP 연결 상태)

### 런큐 분석 (스케줄링 문제) ###
crash> runq
# 각 CPU의 런큐에 있는 태스크 목록
# RT 태스크가 독점하는 경우 식별 가능

### crash 확장(extensions) ###
# crash에 추가 기능을 로드하여 분석 확장
crash> extend /usr/lib64/crash/extensions/trace.so
# trace.so: ftrace 버퍼 분석

crash> extend /usr/lib64/crash/extensions/dminfo.so
# dminfo.so: device-mapper 상태 분석

### 스크립트 자동화 (input 파일) ###
# crash_commands.txt:
# sys
# bt -a
# ps -S
# log
# foreach UN bt
# kmem -i
# quit
crash vmlinux vmcore < crash_commands.txt > crash_output.txt 2>&1
crash 유틸리티 분석 워크플로우 1. 시스템 파악 sys sys -c (cmdline) PANIC 메시지 확인 2. 백트레이스 bt (패닉 CPU) bt -a (전체 CPU) 크래시 함수 특정 3. 커널 로그 log (dmesg) log -t (타임스탬프) 에러 메시지 검색 4. 프로세스 ps -S (D 상태) foreach UN bt 병목 태스크 식별 5. 데이터 조사 struct (구조체) rd (메모리 읽기) 비정상 값 추적 6. 코드 분석 dis -l (디스어셈블) vtop (주소 변환) 근본 원인 확정 상황별 추가 분석 경로 메모리 문제 kmem -i (전체 메모리 통계) kmem -s (슬랩 캐시 상세) vm PID (VMA 맵 확인) search -k PATTERN (poison 검색) OOM, leak, corruption 분석 락/데드락 문제 struct mutex.owner ADDR foreach bt (전체 스택 비교) waitq ADDR (대기큐 조사) runq (런큐 상태) ABBA 데드락, priority inversion I/O / 디바이스 문제 dev -d (블록 디바이스) files PID (열린 파일) mount (마운트 포인트) irq -s (인터럽트 통계) 스토리지 장애, IRQ storm 네트워크 문제 net (인터페이스) net -s (소켓 목록) net -a (ARP 테이블) struct sk_buff ADDR 패킷 드롭, 소켓 누수 분석 결과 해석 핵심 패턴 • poison 값 발견 (0xdead..., 0x6b6b...) → use-after-free • NULL (0x0000...) 접근 → NULL pointer dereference (초기화 누락 또는 경쟁 조건) • 여러 CPU가 같은 lock에서 대기 → 데드락 (ABBA 패턴 또는 재귀적 lock 획득) • D 상태 대량 발생 + io_schedule → 스토리지/NFS 장애 (하드웨어 또는 네트워크 확인) • refcount 음수 또는 overflow → reference counting 버그 (이중 해제 의심)
💡

crash 분석 팁:

  • foreach RU bt: TASK_RUNNING 상태 태스크만 백트레이스 (CPU를 점유 중인 태스크)
  • foreach UN bt: TASK_UNINTERRUPTIBLE 상태 태스크만 (D 상태, 블록된 태스크)
  • bt -f에서 스택 프레임의 함수 인수 값을 읽을 수 있음 (ABI에 따라 레지스터/스택 위치 다름)
  • struct -o로 필드 오프셋 확인 후 rd로 직접 메모리 읽기 가능
  • mod -S /path/to/modules/로 외부 모듈 디버그 심볼을 일괄 로드

메모리 손상 디버깅

커널 크래시의 상당수는 메모리 손상(corruption)에서 비롯됩니다. use-after-free, buffer overflow, 이중 해제, 초기화되지 않은 메모리 사용 등을 탐지하는 커널 내장 도구들을 살펴봅니다.

Poison Value 레퍼런스

커널은 메모리 상태를 추적하기 위해 특정 패턴(poison)을 기록합니다. 크래시에서 이 값을 발견하면 메모리 오류 유형을 즉시 판별할 수 있습니다.

Poison 값의미설정 CONFIG해석
0x6b (POISON_FREE)SLAB 해제됨SLAB_POISON해제된 슬랩 오브젝트 접근 (use-after-free)
0x5a (POISON_INUSE)SLAB 사용 중 (초기화 전)SLAB_POISON할당되었지만 초기화 안 된 메모리 사용
0xa5SLAB redzoneSLAB_RED_ZONE슬랩 오브젝트 경계 침범 (overflow)
0xbbSLAB redzone (끝)SLAB_RED_ZONE오브젝트 뒤쪽 경계 침범
0xcc페이지 해제됨PAGE_POISONING해제된 페이지 접근
0xdead000000000000계열LIST_POISON항상리스트에서 제거된 노드의 next/prev 접근
0xdead000000000100LIST_POISON1 (next)항상list_del() 후 next 접근
0xdead000000000122LIST_POISON2 (prev)항상list_del() 후 prev 접근
0x0badca11debug_locks offDEBUG_LOCK_ALLOClockdep가 비활성화된 후의 오류
0xdeadbeef일반적 디버그 마커다양디버깅용으로 의도적으로 설정한 값
0x00000000ffffffXXERR_PTR 값항상에러 포인터 (-ENOMEM 등)가 역참조됨
0xffffffff00000000+ 계열KASAN shadow 비정상KASANKASAN이 감지한 비정상 접근

KASAN (Kernel Address Sanitizer)

KASAN은 커널 메모리 오류를 실시간으로 탐지하는 가장 강력한 도구입니다. 컴파일 시 활성화하며, 모든 메모리 접근에 shadow memory 검사를 삽입합니다. 오버헤드가 크지만(CPU ~2x, 메모리 ~2-3x) 개발/테스트 환경에서 필수적입니다.

# KASAN 커널 CONFIG
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y       # Generic 모드 (가장 정확, 가장 느림)
# CONFIG_KASAN_SW_TAGS=y     # SW Tag 모드 (ARM64 전용, 빠름)
# CONFIG_KASAN_HW_TAGS=y     # HW Tag 모드 (ARM64 MTE, 가장 빠름)
CONFIG_KASAN_INLINE=y        # 인라인 검사 (아웃라인보다 빠름)
CONFIG_KASAN_STACK=y         # 스택 변수 접근도 검사
CONFIG_KASAN_VMALLOC=y       # vmalloc 영역 검사

# KASAN 부팅 파라미터
kasan=on                     # KASAN 활성화 (기본)
kasan.mode=quarantine        # 해제된 오브젝트를 격리 (기본)
kasan.fault=report           # 오류 발견 시 리포트만 (기본)
# kasan.fault=panic          # 오류 발견 시 즉시 패닉
kasan.stacktrace=on          # 할당/해제 스택트레이스 기록

# KASAN 메시지 예시 해석
==================================================================
BUG: KASAN: slab-use-after-free in my_function+0x42/0x100
Read of size 8 at addr ffff888123456040 by task my_app/1234
# "slab-use-after-free" → 해제된 슬랩 오브젝트 읽기
# "Read of size 8" → 8바이트 읽기 시도
# "addr ffff888123456040" → 접근한 메모리 주소

CPU: 2 PID: 1234 Comm: my_app Tainted: G    B      OE 6.1.0
Call trace:
 my_function+0x42/0x100 [my_module]
 caller_function+0x30/0x80 [my_module]
 ...
# → 오류가 발생한 호출 경로

Allocated by task 5678:
 kmalloc+0x52/0x80
 my_alloc_function+0x28/0x60 [my_module]
 ...
# → 이 오브젝트를 할당한 호출 경로

Freed by task 5678:
 kfree+0x62/0xb0
 my_free_function+0x35/0x50 [my_module]
 ...
# → 이 오브젝트를 해제한 호출 경로

The buggy address belongs to the object at ffff888123456000
 which belongs to the cache kmalloc-256 of size 256
The buggy address is located 64 bytes inside of
 freed 256-byte region [ffff888123456000, ffff888123456100)
# → 오브젝트 캐시 이름, 크기, 오프셋 정보
# → "freed" → 이미 해제된 오브젝트 확인
KASAN 오류 유형의미주요 원인
slab-use-after-free해제된 슬랩 오브젝트 접근dangling pointer, 레퍼런스 카운팅 오류
slab-out-of-bounds슬랩 오브젝트 경계 밖 접근buffer overflow, 잘못된 인덱스
stack-out-of-bounds스택 변수 범위 밖 접근스택 버퍼 오버플로우
global-out-of-bounds전역 변수 범위 밖 접근배열 인덱스 오류
use-after-scope스코프 벗어난 스택 변수 접근함수 반환 후 로컬 변수 참조
double-free이미 해제된 메모리 재해제중복 kfree 호출
invalid-free잘못된 주소 해제비정상 포인터로 kfree
vmalloc-out-of-boundsvmalloc 영역 경계 밖 접근vmalloc 버퍼 오버플로우

KFENCE (Kernel Electric Fence)

KFENCE는 프로덕션 환경에서 사용할 수 있는 저오버헤드 메모리 오류 감지 도구입니다. KASAN과 달리 샘플링 기반으로 동작하여 성능 영향이 거의 없습니다(~1% 미만).

# KFENCE 커널 CONFIG
CONFIG_KFENCE=y
CONFIG_KFENCE_SAMPLE_INTERVAL=100  # 100ms마다 1개 할당을 KFENCE로 보호
CONFIG_KFENCE_NUM_OBJECTS=255      # 동시 보호 가능한 오브젝트 수
CONFIG_KFENCE_DEFERRABLE=y         # idle 시 불필요한 깨움 방지 (전력 절약)

# 부팅 파라미터
kfence.sample_interval=100         # 런타임 조정 가능

# KFENCE 동작 원리:
# 1. 페이지 경계에 오브젝트 배치 (guard page 인접)
# 2. guard page 접근 시 page fault 발생 → 즉시 감지
# 3. 해제 후 오브젝트 페이지를 PROT_NONE으로 → use-after-free 감지
# 4. 샘플링: 모든 할당이 아닌 일부만 보호 → 오버헤드 최소화

# KFENCE 상태 확인
cat /sys/kernel/debug/kfence/stats
# enabled: 1
# total allocs: 123456
# covered allocs: 5678
# pool usage: 200/255

# KFENCE 메시지 예시
==================================================================
BUG: KFENCE: use-after-free read in my_function+0x42/0x100
Use-after-free read at 0xffff888123456000 (in kfence-#123):
 my_function+0x42/0x100 [my_module]
 ...
kfence-#123: 0xffff888123456000-0xffff8881234560ff, size=256,
 cache=kmalloc-256
allocated by task 5678:
 ...
freed by task 5678:
 ...
💡

KASAN vs KFENCE 선택 가이드:

  • KASAN: 개발/테스트 환경. 100% 탐지율, 높은 오버헤드 (CPU 2x, 메모리 3x). 모든 메모리 버그를 즉시 잡아냄.
  • KFENCE: 프로덕션 환경. 샘플링 탐지 (확률적), 거의 무시할 수 있는 오버헤드 (~1%). 장기 실행 시 재현 빈도 높은 버그를 잡아냄.
  • 권장: 개발 시 KASAN으로 집중 테스트, 프로덕션에는 KFENCE 상시 활성화.

SLUB 디버그 옵션

# SLUB 디버그: 런타임에 활성화 가능한 메모리 검사

# 커널 CONFIG
CONFIG_SLUB_DEBUG=y            # SLUB 디버그 인프라 활성화
CONFIG_SLUB_DEBUG_ON=y         # 전체 캐시에 디버그 활성화 (느림!)

# 부팅 파라미터로 선택적 활성화 (프로덕션 조사용)
slub_debug=FZP                 # F:sanity checks, Z:redzone, P:poisoning
slub_debug=FZPU,kmalloc-256    # 특정 캐시에만 적용

# SLUB 디버그 플래그:
# F: sanity checks (일관성 검사)
# Z: redzone (오브젝트 경계 침범 감지)
# P: poisoning (해제 후 poison 패턴 기록)
# U: user tracking (할당/해제 추적 정보 기록)
# T: tracing (할당/해제 추적 출력)
# A: failslab (메모리 할당 실패 주입)

# 런타임에 특정 캐시 디버그 확인
cat /sys/kernel/slab/kmalloc-256/sanity_checks
cat /sys/kernel/slab/kmalloc-256/red_zone
cat /sys/kernel/slab/kmalloc-256/poison
cat /sys/kernel/slab/kmalloc-256/store_user

# SLUB 에러 메시지 예시
=============================================================================
BUG kmalloc-256 (Tainted: G    B      OE): Redzone overwritten
-----------------------------------------------------------------------------
INFO: 0xffff888123456100-0xffff888123456103 @offset=256. First byte 0x41 instead of 0xbb
INFO: Slab 0xffffea0004d15900 objects=16 used=12 fp=0xffff888123456400
INFO: Object 0xffff888123456000 @offset=0 fp=0xffff888123456200
# Redzone overwritten → 오브젝트 경계(256바이트) 뒤에 데이터가 기록됨
# "First byte 0x41 instead of 0xbb" → 0xbb(redzone 마커)가 0x41('A')로 덮임
# → buffer overflow!

Redzone  ffff888123456100: 41 41 41 41                          AAAA
# 4바이트 오버플로우 발생

Object   ffff888123456000: 48 65 6c 6c 6f 20 57 6f 72 6c 64 ...
# 오브젝트 내용 (해석 참고)

# Padding, Redzone, 할당/해제 stacktrace 등도 함께 출력됨
메모리 손상 감지 도구 비교 KASAN 개발/테스트 전용 감지 방식: Shadow Memory 탐지율: 100% (결정적) CPU 오버헤드: ~2x 메모리 오버헤드: ~3x 컴파일 시 활성화 필수 감지 가능: ✓ use-after-free ✓ out-of-bounds (slab/stack/global) ✓ double-free / invalid-free ✓ use-after-scope ✓ vmalloc out-of-bounds KFENCE 프로덕션 사용 가능 감지 방식: Guard Page 탐지율: 샘플링 (확률적) CPU 오버헤드: <1% 메모리 오버헤드: 수 MB CONFIG만으로 활성화 감지 가능: ✓ use-after-free ✓ out-of-bounds (slab) ✓ double-free / invalid-free ✗ stack/global OOB ✗ 결정적 탐지 불가 SLUB Debug 선택적 활성화 감지 방식: Redzone + Poison 탐지율: 할당/해제 시 검사 CPU 오버헤드: 중간 메모리 오버헤드: 소~중 부팅 파라미터로 활성화 감지 가능: ✓ use-after-free (poison) ✓ buffer overflow (redzone) ✓ 할당/해제 추적 (user) ✗ 스택/전역 변수 검사 불가 ✗ 실시간 접근 감시 불가

추가 메모리 디버깅 도구

도구CONFIG용도오버헤드환경
KCSANCONFIG_KCSAN데이터 레이스(동시성 버그) 탐지중간개발/테스트
UBSANCONFIG_UBSAN정의되지 않은 동작 탐지 (정수 오버플로우, 시프트 등)낮음개발/프로덕션
KMSANCONFIG_KMSAN초기화되지 않은 메모리 사용 탐지높음개발/테스트
DEBUG_PAGEALLOCCONFIG_DEBUG_PAGEALLOC해제된 페이지를 즉시 unmap → UAF 즉시 감지높음개발/테스트
DEBUG_KMEMLEAKCONFIG_DEBUG_KMEMLEAK커널 메모리 누수 탐지중간개발/테스트
FAILSLABCONFIG_FAILSLAB메모리 할당 실패 주입 (오류 경로 테스트)낮음테스트
LOCKDEPCONFIG_PROVE_LOCKING데드락 탐지 (락 순서 검증)중간개발/테스트
# 메모리 디버깅 권장 설정 (개발 커널)

# .config에 추가:
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y
CONFIG_KASAN_INLINE=y
CONFIG_KASAN_STACK=y
CONFIG_KASAN_VMALLOC=y
CONFIG_KCSAN=y
CONFIG_UBSAN=y
CONFIG_UBSAN_BOUNDS=y
CONFIG_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_PAGEALLOC=y
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_SLUB_DEBUG=y

# 프로덕션 환경 권장 설정:
CONFIG_KFENCE=y
CONFIG_KFENCE_SAMPLE_INTERVAL=100
CONFIG_UBSAN=y           # 오버헤드 낮으므로 프로덕션도 가능
CONFIG_SLUB_DEBUG=y      # 필요 시 부팅 파라미터로 활성화

# kmemleak 사용 예
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak
# unreferenced object 0xffff888123456000 (size 256):
#   comm "my_app", pid 1234, jiffies 4567890
#   backtrace:
#     kmalloc+0x52/0x80
#     my_init_function+0x42/0x100
# → 할당했지만 참조가 사라진 메모리 = 누수!

pstore / ramoops

패닉 시 콘솔 출력이나 시리얼 로그를 놓칠 수 있습니다. pstore는 패닉 직전의 커널 로그를 영속 저장소에 남기는 공통 계층이고, ramoops는 그중에서도 예약된 RAM 영역을 사용해 로그를 보존하는 백엔드입니다. 따라서 구현을 읽을 때는 단순히 panic()에서 ramoops로 곧바로 들어간다고 이해하면 틀리고, kmsg_dump()를 거쳐 pstore 코어가 백엔드의 write() 콜백을 호출하는 구조로 봐야 합니다.

핵심 한 줄: 패닉 시 실제 호출 순서는 대체로 panic()kmsg_dump(KMSG_DUMP_PANIC)pstore_dump()psinfo->write()ramoops_pstore_write()persistent_ram_write() 입니다. 재부팅 후에는 반대로 pstore_fill_super() 또는 pstore_register()에서 pstore_get_records()가 돌고, 여기서 ramoops_pstore_read()가 예전 레코드를 다시 꺼내 /sys/fs/pstore/ 아래 파일로 노출합니다.
계층대표 함수역할중요한 구현 포인트
패닉 진입panic(), kmsg_dump()패닉 이유를 확정하고 로그 덤퍼 체인을 실행kmsg_dump()는 백엔드를 모르고, 등록된 dumper 목록만 순회합니다.
pstore 코어pstore_register(), pstore_dump()덤퍼 등록, 압축, 레코드 포맷 공통 처리panic/NMI 경로에서는 블로킹을 피하려고 trylock을 사용합니다.
ramoops 백엔드ramoops_probe(), ramoops_pstore_write()예약 RAM을 zone으로 나누고 실제 쓰기 수행record_size, console_size 등은 2의 거듭제곱으로 내림됩니다.
영속 RAM 레이어persistent_ram_new(), persistent_ram_write()zone 헤더, ECC, 오래된 로그 보존 관리새 패닉을 쓰기 전 persistent_ram_zap()으로 기존 내용을 초기화합니다.
재부팅 후 읽기pstore_get_records(), ramoops_pstore_read(), pstore_mkfile()RAM에 남은 레코드를 파일시스템 항목으로 복원압축된 dmesg는 decompress_record()를 거쳐 평문으로 바뀝니다.

부팅 시 등록 경로

ramoops를 이해할 때 가장 중요한 지점은 "패닉 때만 동작하는 코드"가 아니라 부팅 중 미리 연결되는 등록 경로입니다. 먼저 부트 파라미터나 Device Tree에서 메모리 위치와 크기를 읽고, 이를 dmesg, console, ftrace, pmsg용 zone으로 쪼갭니다. 그 다음 ramoopsstruct pstore_info에 자기 read/write 콜백을 채워 pstore_register()에 넘깁니다. 이 시점부터 pstore 코어가 공통 프런트엔드가 되고, 이후 패닉이 발생하면 코어가 백엔드 콜백을 호출하는 방식으로 연결됩니다.

부팅 시 등록과 패닉 시 기록 경로 1. 설정 입력 boot param / Device Tree mem_address, mem_size, record_size 2. ramoops_probe() 크기 검증, 2의 거듭제곱 내림 zone 분할 준비 3. zone 생성 persistent_ram_new() dmesg / console / pmsg / ftrace 4. pstore_register() read/write 콜백 등록 backend 이름: ramoops 5. 덤퍼 연결 kmsg_dump_register() panic 시 pstore_dump 호출 6. 패닉 발생 panic() kmsg_dump(KMSG_DUMP_PANIC) 7. pstore_dump() 로그 tail 추출, 압축, part 구성 psinfo->write(record) 8. ramoops_pstore_write() zone 선택, header 작성, 잘라 쓰기 persistent_ram_zap / write 9. 예약 RAM 보존 dmesg-ramoops-N warm reboot 뒤에도 남음 중요한 해석 포인트 패닉 경로는 직접 ramoops 함수를 호출하지 않고, 먼저 pstore 코어가 레코드를 만들고 나서 백엔드 write 콜백으로 위임합니다. 그래서 로그 누락을 분석할 때는 panic 메시지 출력만 볼 것이 아니라 kmsg_dump 등록 여부, max_reason, zone 크기, 압축 여부까지 같이 봐야 합니다. console/pmsg/ftrace는 dmesg snapshot과 다른 프런트엔드 경로이므로, 같은 패닉에서도 파일이 서로 다른 이유와 시점을 담을 수 있습니다.
함수위치하는 일실무 해석
ramoops_probe()fs/pstore/ram.c파라미터 검증, zone 초기화, pstore_info 구성여기서 실패하면 패닉이 나도 /sys/fs/pstore는 비어 있습니다.
ramoops_init_przs()fs/pstore/ram.cdmesg 레코드용 zone 배열 생성dump_mem_sz / record_size가 실제 보관 가능한 패닉 개수를 좌우합니다.
pstore_register()fs/pstore/platform.c백엔드 등록, 덤퍼/콘솔/pmsg/ftrace 프런트엔드 연결한 번에 하나의 백엔드만 활성화됩니다.
kmsg_dump_register()include/linux/kmsg_dump.h 인터페이스패닉/Oops 시 호출될 dumper 연결pstore_dumper.max_reason가 여기서 적용됩니다.
/* fs/pstore/ram.c + fs/pstore/platform.c 흐름 요약 */
static int ramoops_probe(...)
{
    /* 1. mem_address / mem_size / record_size 파싱 */
    /* 2. 크기를 2의 거듭제곱으로 정리 */
    /* 3. dmesg / console / pmsg / ftrace zone 생성 */
    ramoops_init_przs("dmesg", ...);
    ramoops_init_prz("console", ...);

    cxt->pstore.open  = ramoops_pstore_open;
    cxt->pstore.read  = ramoops_pstore_read;
    cxt->pstore.write = ramoops_pstore_write;

    return pstore_register(&cxt->pstore);
}

int pstore_register(struct pstore_info *psi)
{
    /* old record 조회 */
    pstore_get_records(0);

    /* 이후 panic/Oops 시 pstore_dump()가 호출되도록 연결 */
    if (psi->flags & PSTORE_FLAGS_DMESG) {
        pstore_dumper.max_reason = psi->max_reason;
        kmsg_dump_register(&pstore_dumper);
    }
}

ramoops 메모리 배치와 크기 계산

ramoops는 하나의 큰 예약 메모리를 통째로 쓰지 않고, 용도별로 나눠서 씁니다. 구현에서 dump_mem_sz = total - console_size - ftrace_size - pmsg_size를 먼저 계산하고, 남은 영역을 record_size 단위의 여러 dmesg zone으로 쪼갭니다. 즉 패닉 snapshot 저장 공간지속적인 console/pmsg/ftrace 기록 공간은 내부적으로 서로 다른 zone입니다.

예약 RAM의 내부 배치와 재부팅 후 복원 경로 reserved-memory / ramoops.mem_address dmesg zone 0 dmesg zone 1 console zone pmsg zone ftrace zone 또는 CPU별 ftrace zones 재부팅 후 mount pstore_fill_super() pstore_get_records(0) backend read ramoops_pstore_open() ramoops_pstore_read() header 파싱 + old buffer 복원 파일시스템 노출 decompress_record() pstore_mkfile() /sys/fs/pstore/dmesg-ramoops-0 dmesg는 zone 앞부분의 "====timestamp-type" header를 읽어 압축 여부와 시각을 해석한 뒤 파일로 복원합니다.
필드의미구현상 주의점
mem_size예약 RAM 총 크기너무 작으면 dmesg zone 수가 줄어 이전 패닉이 빨리 덮어써집니다.
record_size각 dmesg 레코드 zone 크기2의 거듭제곱으로 내림되며, 사실상 한 번의 패닉 snapshot 상한선을 결정합니다.
console_size콘솔 로그용 단일 zonepanic snapshot과 별도로 console 프런트엔드가 쓰므로, dmesg와 내용이 다를 수 있습니다.
pmsg_size사용자 공간 메시지 zone사용자 공간이 남긴 부가 힌트를 패닉 후에도 남길 수 있습니다.
ftrace_sizeftrace zone 또는 CPU별 ftrace zonesRAMOOPS_FLAG_FTRACE_PER_CPU가 켜지면 CPU별 zone으로 나뉩니다.
max_reason어떤 이유까지 저장할지KMSG_DUMP_PANIC이면 패닉만, KMSG_DUMP_OOPS면 Oops까지 포함합니다.
mem_type예약 메모리 매핑 속성기본값은 write-combined이며, 일부 구조에서는 원자 연산 때문에 noncached가 부적절할 수 있습니다.
ecc소프트웨어 ECC 보호하드 리셋 뒤 일부 비트가 흔들려도 읽기 시 정정 정보를 덧붙일 수 있습니다.

패닉 시 함수 호출 흐름

앞의 panic() 섹션에서 kmsg_dump() 호출 자체는 이미 보았습니다. 여기서는 그 아래쪽만 떼어 놓고, "왜 어떤 시스템은 pstore가 비어 있고, 어떤 시스템은 로그가 일부만 남는가"를 설명하는 함수 수준 흐름으로 내려가 보겠습니다.

  1. panic() 또는 Oops 경로가 kmsg_dump(reason)를 호출합니다.
    kmsg_dump()는 단순 인라인 래퍼이고, 실제로는 등록된 모든 kmsg_dumper를 순회합니다. pstore는 부팅 시 이미 pstore_dumper를 등록해 두었기 때문에 이 목록에 포함됩니다.
  2. pstore_dump()가 잠금 전략을 먼저 정합니다.
    pstore_cannot_block_path()는 panic/NMI/긴급 재시작 경로에서 블로킹을 피해야 한다고 판단하면 raw_spin_trylock_irqsave()를 쓰고, 일반 경로면 보통의 spin lock을 사용합니다. 즉 이 시점부터 pstore는 "가능하면 남기되, 패닉 경로 자체를 더 망가뜨리지는 말자"는 정책으로 동작합니다.
  3. kmsg_dump_rewind()kmsg_dump_get_buffer()로 로그 ring buffer의 tail을 잘라 옵니다.
    kmsg_bytes만큼의 tail을 가져오되, 사람이 읽기 쉬운 syslog 형식으로 변환해 레코드 버퍼에 실어 나릅니다. 그래서 화면에 보인 몇 줄만 저장되는 것이 아니라, ring buffer에 남아 있는 범위 내에서 tail을 다시 끌어옵니다.
  4. pstore_record_init()가 공통 메타데이터를 채웁니다.
    type, reason, count, part, 타임스탬프, 압축 여부 같은 값이 여기서 struct pstore_record에 들어갑니다.
  5. 압축이 켜져 있으면 pstore 코어가 먼저 deflate를 시도합니다.
    CONFIG_PSTORE_COMPRESScompress=deflate 조합이면 임시 버퍼에 압축한 뒤 백엔드로 넘깁니다. 압축이 되면 나중에 ramoops header의 타입 문자가 C가 되고, 압축 실패 시에는 평문 그대로 저장됩니다.
  6. psinfo->write()가 실제 백엔드로 분기됩니다.
    ramoops가 등록된 시스템이라면 이 포인터는 ramoops_pstore_write()를 가리킵니다. 즉 pstore 코어는 백엔드 종류를 모른 채 공통 레코드만 만들고, 마지막 한 단계에서 실제 저장소로 위임합니다.
  7. ramoops_pstore_write()는 zone을 고른 뒤 새 패닉용 헤더를 씁니다.
    dmesg의 경우 cxt->dprzs[dump_write_cnt]를 고르고, 먼저 persistent_ram_zap()으로 예전 내용을 비웁니다. 이어서 ramoops_write_kmsg_hdr()====timestamp-C/D 헤더를 쓰고, 그 뒤에 실제 dump payload를 붙입니다.
  8. ramoops는 dmesg를 여러 part로 나눠 저장하지 않습니다.
    record->part != 1이면 -ENOSPC를 반환해 추가 part를 거절합니다. 이는 여러 part가 서로 다른 zone에 찢어져 저장되면 복원과 해석이 더 혼란스러워지기 때문입니다. 따라서 실무에서는 kmsg_bytes보다 record_size가 더 작은지 항상 같이 봐야 합니다.
  9. 마지막으로 persistent_ram_write()가 reserved RAM에 기록하고 write index를 회전시킵니다.
    새 dump가 성공하면 dump_write_cnt가 다음 zone으로 이동하므로, 여러 번의 패닉이 발생하면 원형 버퍼처럼 가장 오래된 레코드부터 덮어써집니다.
실무에서 자주 놓치는 점: pstore_dump()가 로그를 다 끌어왔다고 해서 항상 파일로 남는 것은 아닙니다. zone이 너무 작으면 ramoops_pstore_write()에서 잘리고, panic/NMI 경로에서 경쟁 dump가 있으면 trylock 실패로 건너뛸 수도 있으며, max_reason가 낮으면 Oops는 애초에 저장 대상이 아닙니다.
/* kernel/panic.c → fs/pstore/platform.c → fs/pstore/ram.c */
void panic(...)
{
    ...
    kmsg_dump(KMSG_DUMP_PANIC);
}

static void pstore_dump(struct kmsg_dumper *dumper,
                       struct kmsg_dump_detail *detail)
{
    struct pstore_record record;

    kmsg_dump_rewind(&iter);
    oopscount++;

    while (total < remaining) {
        pstore_record_init(&record, psinfo);
        record.type = PSTORE_TYPE_DMESG;
        record.reason = detail->reason;
        record.part = part;

        if (!kmsg_dump_get_buffer(&iter, true, dst, dst_size, &dump_size))
            break;

        /* ramoops 시스템이면 psinfo->write == ramoops_pstore_write */
        psinfo->write(&record);
        part++;
    }
}

static int ramoops_pstore_write(struct pstore_record *record)
{
    struct persistent_ram_zone *prz = cxt->dprzs[cxt->dump_write_cnt];

    if (record->part != 1)
        return -ENOSPC;

    persistent_ram_zap(prz);
    hlen = ramoops_write_kmsg_hdr(prz, record);
    persistent_ram_write(prz, record->buf,
                         min(record->size, prz->buffer_size - hlen));
    cxt->dump_write_cnt = (cxt->dump_write_cnt + 1) % cxt->max_dump_cnt;
    return 0;
}

재부팅 후 읽기 흐름

읽기 경로도 중요합니다. /sys/fs/pstore/ 아래 파일은 "RAM에 들어 있는 바이트 배열을 그대로 보여 주는 창"이 아니라, pstore 파일시스템이 백엔드 레코드를 다시 읽어 파일로 재구성한 결과입니다. 즉 재부팅 후 파일이 보이려면 ramoops 백엔드가 다시 초기화되어야 하고, pstore 파일시스템이 mount되거나 이미 mount된 상태에서 pstore_get_records()가 돌아야 합니다.

순서대표 함수무엇을 하는가관찰 포인트
1pstore_fill_super()파일시스템 superblock 생성 후 pstore_get_records(0) 호출mount는 단순 뷰가 아니라 레코드 스캔의 시작점입니다.
2pstore_get_backend_records()read_mutex를 잡고 backend open/read/close 루프 수행동시 읽기 중복과 레코드 재생성 경쟁을 막습니다.
3ramoops_pstore_open()dmesg/console/pmsg/ftrace read 카운터 초기화한 번의 스캔에서 각 zone을 순서대로 읽기 위해 필요합니다.
4ramoops_pstore_read()다음 유효 zone을 찾고 header/ECC/압축 플래그를 해석헤더가 깨져 있으면 그 레코드는 정리하고 건너뜁니다.
5decompress_record()압축된 dmesg를 다시 평문으로 복원record->compressed가 참일 때만 동작합니다.
6pstore_mkfile()메모리 레코드를 dentry/inode로 변환여기서 우리가 보는 dmesg-ramoops-0 파일이 만들어집니다.
/* fs/pstore/inode.c + fs/pstore/platform.c + fs/pstore/ram.c */
static int pstore_fill_super(...)
{
    ...
    pstore_get_records(0);
}

void pstore_get_backend_records(struct pstore_info *psi, ...)
{
    mutex_lock(&psi->read_mutex);
    psi->open(psi);   /* ramoops_pstore_open */

    for (;;) {
        record->size = psi->read(record);   /* ramoops_pstore_read */
        if (record->size <= 0)
            break;

        decompress_record(record, &zstream);
        pstore_mkfile(root, record);
    }
}

static ssize_t ramoops_pstore_read(struct pstore_record *record)
{
    /* dmesg zone을 먼저 훑고, 없으면 console / pmsg / ftrace 순서 */
    prz = ramoops_get_next_prz(...);
    header_length = ramoops_read_kmsg_hdr(...);
    record->buf = ...;
    return size;
}

이 구조 때문에 재부팅 후 분석에서는 cat /sys/fs/pstore/dmesg-ramoops-0만 보는 것으로 끝내지 말고, console-ramoops-0, pmsg-ramoops-0, 필요하면 ftrace-ramoops-*도 함께 비교하는 것이 좋습니다. dmesg는 "패닉 순간 snapshot"에 가깝고, console은 "계속 흘러가던 콘솔 출력", pmsg는 "사용자 공간이 남긴 마지막 힌트"이기 때문에 서로 보완 관계입니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# pstore/ramoops 설정 (부트 파라미터)
ramoops.mem_address=0x700000000    # RAM 예약 시작 주소
ramoops.mem_size=0x400000          # 4MB 예약
ramoops.console_size=0x100000      # 콘솔 로그 1MB
ramoops.record_size=0x40000        # 각 패닉 레코드 256KB
ramoops.pmsg_size=0x40000          # 사용자 공간 메시지 256KB
ramoops.ftrace_size=0x40000        # ftrace 버퍼 256KB
ramoops.max_reason=0x2             # Oops와 Panic 모두 저장
ramoops.ecc=1                      # ECC 활성화

# 디바이스 트리에서 ramoops 설정 (임베디드)
ramoops {
    compatible = "ramoops";
    memory-region = <&ramoops_mem>;
    console-size = <0x100000>;
    record-size  = <0x40000>;
    pmsg-size    = <0x40000>;
    ftrace-size  = <0x40000>;
    max-reason   = <0x2>;
    ecc-size     = <16>;
};

# pstore 마운트 및 확인
mount -t pstore pstore /sys/fs/pstore/
ls /sys/fs/pstore/
# dmesg-ramoops-0   → 이전 패닉의 dmesg snapshot
# console-ramoops-0 → 지속 콘솔 로그
# pmsg-ramoops-0    → 사용자 공간 메시지

# pstore 내용 읽기
cat /sys/fs/pstore/dmesg-ramoops-0
cat /sys/fs/pstore/console-ramoops-0

# EFI 기반 pstore (UEFI 시스템)
# 자동으로 EFI 변수에 저장됨
efivar -l | grep dump-type
cat /sys/fs/pstore/dmesg-efi-*

SysRq 디버깅 키

Magic SysRq 키는 시스템이 응답하지 않을 때도 커널에 직접 명령을 보낼 수 있는 디버깅 인터페이스입니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# SysRq 활성화
sysctl kernel.sysrq=1         # 전체 활성화
sysctl kernel.sysrq=438       # 특정 기능만 (비트마스크)
# Bit 0 (1):   sysrq 완전 비활성화
# Bit 1 (2):   로그 레벨 변경
# Bit 2 (4):   콘솔 로그 활성화
# Bit 3 (8):   모든 프로세스 정보
# Bit 4 (16):  SAK (Secure Access Key)
# Bit 5 (32):  sync
# Bit 6 (64):  읽기전용 리마운트
# Bit 7 (128): 프로세스 시그널링 (E, I)
# Bit 8 (256): reboot

# 주요 SysRq 키 (echo X > /proc/sysrq-trigger 또는 Alt+SysRq+X)
echo h > /proc/sysrq-trigger  # 도움말
echo t > /proc/sysrq-trigger  # 모든 태스크 스택 덤프 (softlockup 분석)
echo w > /proc/sysrq-trigger  # D 상태 (uninterruptible) 태스크 덤프
echo l > /proc/sysrq-trigger  # 모든 CPU 백트레이스
echo m > /proc/sysrq-trigger  # 메모리 정보 (OOM 분석)
echo p > /proc/sysrq-trigger  # 현재 CPU 레지스터 덤프
echo c > /proc/sysrq-trigger  # 강제 패닉 (kdump 테스트)

# R-E-I-S-U-B: 안전한 재부팅 시퀀스
# R: 키보드 raw 모드 해제
# E: 모든 프로세스에 SIGTERM
# I: 모든 프로세스에 SIGKILL
# S: 파일시스템 sync
# U: 파일시스템 읽기전용 리마운트
# B: 즉시 재부팅

# 각 단계 사이에 잠시 대기
for key in r e i s u b; do
    echo $key > /proc/sysrq-trigger
    sleep 2
done

RCU Stall 디버깅

RCU(Read-Copy-Update) stall은 CPU가 RCU grace period를 완료하지 못할 때 발생합니다. 네트워크, 파일시스템 등 RCU를 광범위하게 사용하는 서브시스템에서 자주 나타납니다.

💡

RCU stall 메시지의 필드별 상세 해석(idle 필드, CPU 플래그, softirq 카운터, kthread starved 등), 실전 사례 분석, 예방 패턴은 RCU 심화 — RCU CPU Stall 경고 심화 페이지에서 다룹니다.

# 실습 예제: 운영 환경에서 재현 가능한 점검 절차
# RCU stall 메시지 예시
rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:
rcu: 	3-...0: (1 GPs behind) idle=02f/1/0x40000002 softirq=1234/1234 fqs=4567
rcu: 	(detected by 0, t=21003 jiffies, g=12345, q=678 ncpus=8)
rcu: rcu_preempt kthread starved for 21003 jiffies!

# 핵심 필드 요약:
# 3-...0          → CPU 3, 플래그 5자리 (온라인/오프라인/틱/dyntick/irq)
# (1 GPs behind)  → 현재 GP보다 1 뒤처짐
# idle=AAA/BBB/CCC → dynticks nesting / NMI nesting / dynticks 카운터
#   CCC 짝수=idle, 홀수=활성
# softirq=처리/발생 → 차이가 크면 softirq 폭주
# fqs=N           → force-quiescent-state 스캔 횟수
# t=21003         → grace period 경과 jiffies
# g=12345         → grace period 번호 (gp_seq)
# kthread starved → RCU kthread가 스케줄링 못 받음
#   state 0x0=RUNNING(CPU 시간 부족), 0x2=UNINTERRUPTIBLE

# RCU stall 주요 원인:
# 1. 인터럽트 비활성화 상태에서 오래 실행
# 2. preempt_disable() 상태에서 긴 루프
# 3. 큰 데이터 구조 순회 중 cond_resched() 누락
# 4. softirq 처리가 너무 오래 걸림
# 5. RT 태스크가 RCU kthread보다 높은 우선순위로 독점
# 6. 가상머신 steal time (모든 CPU 동시 stall)

# RCU stall 관련 커널 파라미터
echo 60 > /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout
echo 1 > /sys/module/rcupdate/parameters/rcu_cpu_stall_suppress  # 억제

# RCU 상태 모니터링
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
cat /sys/kernel/debug/rcu/rcu_preempt/rcuexp
/* 개념 예시: 커널 내부 동작 이해를 위한 코드 */
/* RCU stall 방지 패턴 */

/* BAD: RCU read-side에서 긴 루프 */
rcu_read_lock();
list_for_each_entry_rcu(entry, &large_list, node) {
    do_expensive_work(entry);  /* RCU stall 위험! */
}
rcu_read_unlock();

/* GOOD: cond_resched_rcu()로 주기적 양보 */
rcu_read_lock();
list_for_each_entry_rcu(entry, &large_list, node) {
    do_expensive_work(entry);
    cond_resched_rcu();  /* RCU unlock → resched → lock */
}
rcu_read_unlock();

/* GOOD: 수동 RCU 구간 재시작 */
rcu_read_lock();
list_for_each_entry_rcu(entry, &large_list, node) {
    do_expensive_work(entry);
    if (need_resched()) {
        rcu_read_unlock();
        cond_resched();
        rcu_read_lock();
    }
}
rcu_read_unlock();

아키텍처별 크래시 분석 차이

커널 크래시 분석은 아키텍처에 따라 레지스터 이름, 예외 유형, 스택 구조 등이 다릅니다. x86_64 중심 분석에 익숙하더라도, ARM64나 RISC-V 환경에서는 차이점을 이해해야 합니다.

아키텍처별 레지스터 매핑

용도x86_64ARM64 (AArch64)RISC-V (rv64)s390x
프로그램 카운터RIPPC (elr_el1)epc (sepc)PSW addr
스택 포인터RSPSP (sp_el0/sp_el1)sp (x2)R15
프레임 포인터RBPx29 (FP)s0 (x8)R11
리턴 주소스택에 pushx30 (LR)ra (x1)R14
첫 번째 인수RDIx0a0 (x10)R2
두 번째 인수RSIx1a1 (x11)R3
반환값RAXx0a0 (x10)R2
상태 레지스터EFLAGSPSTATE (SPSR_EL1)sstatusPSW mask
페이지 테이블CR3TTBR0_EL1/TTBR1_EL1satpCR3
커널 스택 크기16KB (4 pages)16KB (VMAP_STACK)16KB16KB

아키텍처별 예외 유형

x86_64 예외ARM64 대응RISC-V 대응설명
Page Fault (#PF)Data Abort (ESR_EL1)Store/Load Page Fault잘못된 메모리 접근
GPF (#GP)Alignment FaultIllegal Instruction비정상 주소/명령
Double Fault (#DF)해당 없음 (별도 스택)해당 없음스택 오버플로우
NMIFIQ / SError해당 없음 (NMI 없음)마스킹 불가 인터럽트
MCE (#MC)SError (GHES/SDEI)해당 없음하드웨어 에러
UD (#UD)Unknown/UndefinedIllegal Instruction정의되지 않은 명령

아키텍처별 Oops 메시지 비교

# x86_64 Oops 메시지
BUG: unable to handle page fault for address: 0000000000001234
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
RIP: 0010:my_function+0x42/0x100 [my_module]
# → RIP 필드에서 즉시 크래시 위치 확인 가능
# → error_code로 접근 유형(read/write/exec) 판별

# ARM64 Oops 메시지
Unable to handle kernel NULL pointer dereference at virtual address 0000000000001234
Mem abort info:
  ESR = 0x96000004
  EC = 0x25: DABT (current EL), IL = 32 bits
  SET = 0, FnV = 0
  EA = 0, S1PTW = 0
  FSC = 0x04: level 0 translation fault
# ESR(Exception Syndrome Register)이 핵심:
#   EC (Exception Class): 0x25 = Data Abort from current EL
#   FSC (Fault Status Code): translation fault level 정보
#   EC=0x20: Instruction Abort from lower EL
#   EC=0x21: Instruction Abort from same EL
#   EC=0x24: Data Abort from lower EL
#   EC=0x25: Data Abort from same EL
Internal error: Oops: 96000004 [#1] SMP
pc : my_function+0x42/0x100 [my_module]
lr : caller_function+0x30/0x80
sp : ffff800012345678
# ARM64는 pc(Program Counter), lr(Link Register=리턴주소) 별도 표시

# RISC-V Oops 메시지
Unable to handle kernel access to user memory without uaccess routines
 at virtual address 0000000000001234
Oops [#1]
CPU: 0 PID: 123 Comm: my_app Not tainted 6.1.0
epc : my_function+0x42/0x100 [my_module]
 ra : caller_function+0x30/0x80
# epc = exception program counter (RIP에 해당)
# ra = return address (LR에 해당)
# RISC-V는 별도의 exception syndrome 없이 scause 레지스터 사용:
#   scause=13: Load page fault
#   scause=15: Store/AMO page fault
#   scause=12: Instruction page fault
#   scause=2:  Illegal instruction

# s390x Oops 메시지
# s390x는 레지스터가 R0-R15 + 부동소수점 레지스터
# PSW(Program Status Word)가 x86의 RIP+EFLAGS에 해당
# Low-address protection, Addressing exception 등 고유 예외 타입
ℹ️

아키텍처별 crash 도구 사용: crash 유틸리티는 x86_64, ARM64, RISC-V, s390x, PPC64 등 주요 아키텍처를 모두 지원합니다. 크로스 아키텍처 분석 시 crash --target=arm64 등으로 타겟을 지정하고, 해당 아키텍처의 vmlinux와 vmcore를 사용합니다. ARM64에서는 KIMAGE_VOFFSET, RISC-V에서는 va_pa_offset이 KASLR 오프셋 역할을 합니다.

아키텍처별 Watchdog 차이

항목x86_64ARM64RISC-V
Hardlockup 감지PMU perf NMISDEI/FIQ 기반
(v6.3+ buddy hardlockup)
미지원
(NMI 없음)
Softlockup 감지hrtimer watchdoghrtimer watchdoghrtimer watchdog
NMI 전송APIC NMI IPI불가 (대안: SDEI)불가
SysRq 강제 크래시NMI + echo cecho c (NMI 없음)echo c (NMI 없음)
원격 디버그 트리거IPMI NMISDEI (ARM APEI)지원 제한적
# ARM64에서 hardlockup 대안 (buddy hardlockup detector)
# ARM64는 NMI가 없어 전통적 hardlockup 감지 불가
# 커널 6.3+: "buddy" CPU가 이웃 CPU의 hardlockup을 감지
CONFIG_HARDLOCKUP_DETECTOR_PERF=n   # ARM64에서 사용 불가
CONFIG_HARDLOCKUP_DETECTOR_BUDDY=y  # buddy 방식 사용 (6.3+)

# SDEI (Software Delegated Exception Interface)
# ARM64 서버에서 NMI 유사 기능 제공 (펌웨어 지원 필요)
# ACPI GHES(Generic Hardware Error Source)를 통한 에러 알림
dmesg | grep -i sdei
# [    0.000000] SDEI: SDEIv1.0 detected

# RISC-V hardlockup 제한
# RISC-V에는 아직 NMI 메커니즘이 없어 hardlockup 감지 불가
# 외부 watchdog(하드웨어 WDT) 또는 리셋으로 대응
# RISC-V ACLINT(Advanced Core Local Interruptor)가 향후 지원 예정

크래시 디버깅 체크리스트

커널 크래시 발생 시 체계적인 대응 절차를 따르면 복구 시간을 단축하고 재발을 방지할 수 있습니다. 아래 플로차트는 즉시 조치부터 근본 원인 분석까지 전체 프로세스를 보여줍니다.

크래시 복구 체크리스트 플로차트 1단계: 즉시 대응 (5분 이내) ☐ 시스템 응답성 확인 (ping, SSH 연결) ☐ 콘솔 메시지 수집 (시리얼/IPMI SOL) ☐ pstore 내용 백업 (/sys/fs/pstore/*) ☐ vmcore 수집 확인 (/var/crash/*.vmcore) ☐ 서비스 영향 범위 파악 및 우회 페일오버, 로드밸런서 제외, 사용자 공지 2단계: 초기 분석 (30분 이내) ☐ 크래시 유형 분류 (Oops/Panic/Lockup) ☐ Call Trace 최상위 함수 확인 ☐ RIP 주소로 크래시 위치 특정 ☐ 최근 변경사항 확인 (커널 업그레이드 등) ☐ 긴급도 평가: 즉시 재부팅 vs 분석 우선 재현 가능성, 영업 영향도 고려 3단계: 심층 분석 (수 시간 ~ 수 일) ☐ crash 유틸리티로 vmcore 전체 분석 ☐ 레지스터/메모리 값 검사 (poison, NULL) ☐ 모든 CPU 백트레이스 확인 (foreach bt) ☐ 락 상태/소유자 확인 (데드락 의심 시) ☐ 관련 커널 로그/트레이스 교차 검증 ftrace, perf, KASAN 출력 결합 ☐ 재현 환경 구성 (QEMU/테스트 머신) ☐ 새니타이저 활성화 재현 (KASAN/lockdep) ☐ Git blame/log로 관련 커밋 조사 ☐ 메일링리스트/버그 트래커 검색 ☐ 근본 원인 가설 수립 및 검증 코드 레벨 동작 추적 4단계: 해결 및 검증 ☐ 패치 작성 및 리뷰 ☐ 유닛 테스트 추가 ☐ 재현 환경에서 수정 검증 ☐ 스트레스 테스트 통과 ☐ 업스트림 제출 또는 백포트 ☐ 스테이징 배포 및 모니터링 ☐ 프로덕션 배포 (카나리/블루그린) ☐ 동일 조건 재발 여부 확인 (1주+) ☐ 포스트모텀 문서 작성 타임라인, 근본원인, 예방책 ☐ 모니터링 개선 조기 경보 시스템 추가 ☐ 팀 공유 및 교육 ✓ 사고 종료 5단계: 장기 예방 (지속) ☐ CI/CD에 재현 테스트 추가 ☐ 프로덕션 새니타이저 샘플링 도입 (KFENCE) ☐ 정기 kdump 테스트 자동화 ☐ 크래시 패턴 대시보드 구축 ☐ 커널 업그레이드 전략 수립 ☐ 리눅스 메일링리스트 구독 및 패치 모니터링 ☐ 유사 사례 데이터베이스 구축 ☐ 온콜 대응 플레이북 업데이트 체크리스트 원칙: 증거 확보 우선 → 영향 최소화 → 근본 원인 제거 → 재발 방지
그림: 크래시 복구 체크리스트 - 즉시 조치부터 장기 예방까지 전체 프로세스
💡

커널 크래시 발생 시 분석 순서:

  1. dmesg / 시리얼 콘솔 로그 확보: 패닉 메시지, Call Trace, 레지스터 값
  2. Oops 유형 확인: NULL deref, GPF, page fault, BUG, soft/hardlockup
  3. RIP 위치 분석: faddr2line 또는 addr2line으로 소스 라인 확인
  4. Call Trace 분석: 호출 경로를 역추적하여 근본 원인 파악
  5. 레지스터 값 검사: 의심스러운 값(poison, NULL, 범위 초과)
  6. vmcore 분석: crash 도구로 전체 시스템 상태 조사
  7. 재현: 스트레스 테스트, CONFIG_PROVE_LOCKING, KASAN 활성화
  8. 패치 & 검증: 수정 후 동일 조건에서 재현 불가 확인
도구용도필요 CONFIG
decode_stacktrace.shCall Trace를 소스 라인으로 변환DEBUG_INFO
faddr2line함수+오프셋 → 소스 위치DEBUG_INFO
crashvmcore 분석DEBUG_INFO, CRASH_DUMP
KASAN메모리 오류 실시간 탐지KASAN
KFENCE저오버헤드 메모리 오류 샘플링KFENCE
lockdep데드락 탐지PROVE_LOCKING
KCSAN동시성 버그 탐지KCSAN
UBSAN정의되지 않은 동작 탐지UBSAN
pstore/ramoops패닉 로그 보존PSTORE, PSTORE_RAM
ftrace함수 호출 추적FTRACE
SysRq응답 불가 시 디버깅MAGIC_SYSRQ

크래시 분석과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.