크래시 분석 심화
운영 중 발생하는 커널 장애를 근거 기반으로 분석하는 절차를 정리합니다. panic/oops 로그와 call trace 프레임 해석, `crashkernel`/kdump 설정 및 vmcore 수집, `crash` 도구로 태스크·메모리·락 상태 확인, softlockup/hardlockup/RCU stall 구분, WARN/BUG의 위험도 평가, pstore/ramoops/SysRq를 통한 증거 확보, 재현 시나리오 구성과 패치 검증까지 실무형 체크리스트로 상세히 다룹니다.
핵심 요약
- 증거 우선 — 패닉 직후 로그/콘솔/vmcore를 먼저 확보해야 합니다.
- 분류 우선 — Oops, softlockup, hardlockup, hung task를 먼저 구분해야 분석 경로가 정해집니다.
- 재현성 확보 — 동일 워크로드/커널/설정에서 재현 가능한지 확인해야 합니다.
- 운영 안전성 — 프로덕션에서는 패닉 정책과 자동 재부팅 정책을 분리해 설계해야 합니다.
- 사후 검증 — 패치 후 동일 시나리오에서 재발 여부를 반드시 확인해야 합니다.
단계별 이해
- 증거 수집
시리얼 콘솔, pstore, vmcore 확보 상태를 먼저 점검합니다. - 1차 분류
메시지 패턴으로 lockup/oops/panic 유형을 분리합니다. - 심층 분석
crash, Call Trace, taint 플래그로 원인 가설을 좁힙니다. - 재현/검증
수정 후 같은 조건에서 재현 불가를 확인하고 운영 설정을 재점검합니다.
개념 예시는 내부 동작 이해 목적이며, 실습 예제는 실제 점검/복구 절차 재현 목적입니다.
크래시 유형 분류 가이드
커널 크래시는 증상에 따라 여러 유형으로 분류됩니다. 정확한 분류는 올바른 분석 경로와 해결 전략을 결정하는 첫 번째 단계입니다.
| 유형 | 심각도 | 시스템 영향 | 주요 키워드 | 우선 조치 |
|---|---|---|---|---|
| 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-:256M | RAM 512M~2G→64MB 예약, 2G 이상→256MB 예약 |
| 오프셋 지정 | crashkernel=256M@16M | 물리주소 16MB부터 시작 (레거시 시스템) |
| high/low 분리 | crashkernel=256M,high crashkernel=72M,low | 4GB 이상/이하 영역 분리 예약 (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-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와 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 파싱 → 세그먼트 목록 구성
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=integrity와 CONFIG_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 출력 | task와 sp를 인수로 받으므로 "누구의 스택을 어디서부터 읽을지"를 결정하는 층입니다. |
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 기반 시작점, ...) */
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. 프레임 walk | arch_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에 남아 있을 수 있습니다. |
# 수동 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/KASAN | dump_stack_lvl, __warn, report_bug, sanitizer helper | helper 체인 아래의 첫 번째 도메인 함수 | 경고 프레임워크가 먼저 개입하므로 가장 위 helper만 보고 원인을 단정하면 안 됩니다. |
| Oops/page fault/BUG | fault 함수 또는 예외 진입 프레임 | RIP가 가리키는 함수와 그 caller | 이 경우는 RIP가 실제 trap instruction을 짚어 주므로 우선순위가 더 높습니다. |
| NMI/lockup | watchdog, nmi_panic, IRQ/NMI entry helper | 컨텍스트 마커 아래 첫 번째 잠금/루프 함수 | 비동기 선점이라 일반 함수 호출과 같은 의미로 읽으면 안 됩니다. |
언와인더가 놓치거나 왜곡하는 프레임
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를 별도로 사용합니다. |
언와인드 결과가 로그로 보이기까지
수집된 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를 읽는 실전 순서
- trace를 찍은 원인부터 구분
dump_stack()직접 호출인지,WARN계열인지, 예외/Oops인지 먼저 분류합니다. - helper 프레임과 원인 프레임을 분리
dump_stack_lvl,__warn, sanitizer helper, entry wrapper는 보조 프레임으로 보고 첫 번째 도메인 함수부터 원인 추적을 시작합니다. - Oops라면 RIP를 최우선으로 해석
예외 경로에서는Call Trace보다RIP가 실제 fault instruction을 더 정확하게 가리킵니다. - 출력 순서와 시간 순서를 따로 읽기
위에서 아래로는 "현재 프레임 구조", 아래에서 위로는 "실제 호출 역사"를 복원합니다. - trace를 맹신하지 말고 교차 검증
faddr2line,decode_stacktrace.sh,objdump -dS, 레지스터 값,Code:바이트를 같이 봅니다. - 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 메시지는 여러 섹션으로 구성됩니다. 각 부분이 전달하는 정보를 정확히 이해하면 크래시 원인을 빠르게 좁힐 수 있습니다.
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) | 영역 | 크래시 시 의미 | 주요 원인 |
|---|---|---|---|
0x0000~0x0FFF | NULL 영역 | NULL 포인터 역참조 | 초기화 누락, 경쟁 조건 |
0x0001000~0x00007FFF... | 유저 공간 | SMAP/SMEP 위반 | copy_from_user 미사용 |
0xdead000000000100 | LIST_POISON1 | list_del 후 next 접근 | 이중 삭제, use-after-free |
0xdead000000000122 | LIST_POISON2 | list_del 후 prev 접근 | 이중 삭제, use-after-free |
0xffff888000000000~ | Direct Map | 일반 커널 데이터 접근 오류 | 해제된 오브젝트, 범위 초과 |
0xffffc90000000000~ | vmalloc | vmalloc/ioremap 영역 오류 | MMIO 접근 실패, 스택 오버플로우 |
0xffffea0000000000~ | vmemmap | struct 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
| 항목 | Softlockup | Hardlockup |
|---|---|---|
| 정의 | CPU가 일정 시간 커널 모드에서 스케줄링 없이 실행 | CPU가 일정 시간 인터럽트 처리 없이 실행 |
| 탐지 메커니즘 | hrtimer → watchdog kthread 스케줄링 여부 | NMI 기반 perf counter (PMU) |
| 기본 타임아웃 | 2 × watchdog_thresh (기본 20초) | watchdog_thresh (기본 10초) |
| 원인 | 긴 루프, 높은 우선순위 작업 독점 | 인터럽트 비활성화 상태 지속, 스핀락 데드락 |
| 심각도 | 경고 (시스템 계속 동작 가능) | 심각 (시스템 응답 불가) |
| 기본 동작 | WARN + 스택 트레이스 출력 | 패닉 (watchdog_thresh 설정에 따라) |
| CONFIG 옵션 | SOFTLOCKUP_DETECTOR | HARDLOCKUP_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: 필드는 커널의 "오염" 상태를 나타냅니다. 각 문자가 특정 조건을 표시하며, 커뮤니티 버그 리포트에서 중요한 정보입니다:
| 위치 | 문자 | 의미 | 설명 |
|---|---|---|---|
| 0 | G/P | GPL | P=Proprietary 모듈 로드됨, G=GPL-only |
| 1 | F | Forced | 모듈이 modprobe --force로 로드됨 |
| 2 | S | Unsafe SMP | SMP unsafe 모듈이 SMP 커널에 로드됨 |
| 3 | R | Forced unload | rmmod --force 사용됨 |
| 4 | M | MCE | Machine Check Exception 발생 |
| 5 | B | Bad page | 페이지 해제 시 불량 페이지 감지 |
| 6 | U | User request | 사용자가 tainted 플래그를 직접 설정 |
| 7 | D | Die | 최근 OOPS/BUG 발생 |
| 8 | A | ACPI override | ACPI 테이블이 사용자에 의해 오버라이드됨 |
| 9 | W | Warning | 이전에 WARN_ON 경고 발생 |
| 10 | C | Staging | staging 드라이버 로드됨 |
| 11 | I | Firmware bug | 플랫폼 펌웨어 버그 감지 |
| 12 | O | Out-of-tree | 트리 외부 모듈 로드됨 |
| 13 | E | Unsigned | 서명되지 않은 모듈 로드됨 |
| 14 | L | Soft lockup | 이전에 soft lockup 발생 |
| 15 | K | Live patch | 커널 라이브 패치 적용됨 |
| 16 | T | Test | 테스트 taint (KUNIT 등) |
| 17 | X | Aux | 보조 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=1 | perf top -C N으로 hot function 확인 |
| preempt_disable 장기화 | preempt_count > 0 | IF=1 | preempt_count 값 확인, cond_resched 누락 |
| RT 태스크 CPU 독점 | RT 태스크의 일반 코드 실행 | IF=1 | ps -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_irqsave | lockdep(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_tag | iostat -x 1, dmesg | grep -i error |
| NFS 서버 응답 없음 | rpc_wait_bit_killable, nfs_file_* | nfsstat -c, rpcdebug -m nfs |
| mutex 경합 | mutex_lock_killable, __mutex_lock.constprop | lockdep, /proc/lock_stat |
| 메모리 부족 (OOM 대기) | mem_cgroup_charge, __alloc_pages_slowpath | free -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 종합 디버깅 순서
커널 패닉 심화
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()의 큰 단계만 보여 주는 단순화 버전입니다.
실제 커널에서는 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에서 재귀 패닉 → 최소한의 출력만 시도하고 종료 */
}
NMI 예외 경로: NMI 안에서 발생한 패닉은 일반 stop IPI 경로와 충돌할 수 있으므로, 자세한 배경은 NMI 문서의 nmi_panic() 설명과 함께 보는 편이 좋습니다.
kdump, notifier, kmsg_dump 순서 분기
패닉 경로에서 가장 자주 오해되는 부분이 "kdump와 notifier 중 무엇이 먼저인가"입니다. 답은 항상 동일하지 않다입니다. 일반적으로는 vmcore 보존을 우선하려고 __crash_kexec()를 최대한 앞에 두는 편이 유리하지만, 특정 장치의 긴급 덤프를 notifier에서 남겨야 한다면 notifier를 먼저 실행하고 나중에 kdump로 가는 경로를 선택할 수도 있습니다. 이때 얻는 장점과 잃는 안정성이 동시에 존재합니다.
| 선택 | 유리한 상황 | 대가 |
|---|---|---|
| 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차 패닉은 첫 번째 증거를 크게 망가뜨립니다. |
panic()의 본래 목적이던 증거 보존을 해칠 수 있습니다.
콘솔 flush와 panic_timeout의 마지막 의미
패닉을 읽을 때 마지막 분기는 생각보다 중요합니다. panic_timeout == 0이면 시스템은 분석을 위해 멈춰 있고, > 0이면 정해진 시간 뒤 자동 재부팅을 시도하며, < 0이면 사실상 즉시 강제 재시작 경로로 갑니다. 따라서 운영 중 "패닉 메시지가 잠깐 보이고 바로 재부팅됐다"면, 원인 분석보다 먼저 kernel.panic 값부터 확인해야 합니다.
panic_timeout 값 | 동작 | 현장 의미 |
|---|---|---|
0 | 무한 대기 + panic_blink() 류의 정지 루프 | 시리얼 콘솔이나 현장 화면에서 직접 증거를 읽기 좋습니다. |
> 0 | N초 대기 후 emergency_restart() | 프로덕션 자동 복구에 유리하지만, 현장 분석 시간은 짧아집니다. |
< 0 | 즉시 강제 재부팅 | 복구 최우선 정책이지만, 시각적 콘솔 증거는 거의 못 봅니다. |
panic()을 호출했는지, 2) kdump가 실제 실행됐는지, 3) notifier가 개입했는지, 4) panic_timeout 때문에 너무 빨리 재부팅된 것은 아닌지 순으로 확인하면 훨씬 빠르게 상황을 좁힐 수 있습니다.
패닉 관련 커널 파라미터 총정리
| 파라미터 | 기본값 | 설명 |
|---|---|---|
panic=N | 0 | 패닉 후 N초 뒤 재부팅. 0=재부팅 안 함. -1=즉시 재부팅 |
panic_on_oops=N | 0 | 1이면 Oops 시 즉시 패닉 (kdump 유발) |
panic_on_warn=N | 0 | 1이면 WARN 시 패닉 (디버깅 전용) |
panic_on_rcu_stall=N | 0 | 1이면 RCU stall 시 패닉 |
panic_on_io_nmi=N | 0 | 1이면 I/O NMI 시 패닉 |
panic_on_unrecovered_nmi=N | 0 | 1이면 처리 불가 NMI 시 패닉 |
panic_print=N | 0 | 패닉 시 추가 정보 출력 (비트마스크) |
softlockup_panic=N | 0 | 1이면 softlockup 시 패닉 |
hardlockup_panic=N | 0 | 1이면 hardlockup 시 패닉 |
hung_task_panic=N | 0 | 1이면 hung_task 시 패닉 |
unknown_nmi_panic=N | 0 | 1이면 알 수 없는 NMI 시 패닉 (IPMI 디버깅) |
oops_limit=N | 10000 | 이 횟수 초과 시 패닉 (커널 6.2+) |
warn_limit=N | 0 | 이 횟수 초과 시 패닉 (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 ASCII | rd 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 ARP | net, net -s |
task | task_struct | PID 또는 주소 | task 1234 |
foreach | 반복 실행 | bt, task, vm 등 | foreach bt, foreach RU bt |
runq | 런큐 정보 | -t 타임스탬프, -m ms | runq |
irq | 인터럽트 | -s 통계, -a 어피니티 | irq -s |
timer | 타이머 | timer | |
swap | 스왑 정보 | swap | |
mount | 마운트 정보 | mount | |
sys | 시스템 정보 | -c cmdline | sys |
set | 컨텍스트 변경 | PID 또는 CPU | set 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 분석 팁:
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 | 할당되었지만 초기화 안 된 메모리 사용 |
0xa5 | SLAB redzone | SLAB_RED_ZONE | 슬랩 오브젝트 경계 침범 (overflow) |
0xbb | SLAB redzone (끝) | SLAB_RED_ZONE | 오브젝트 뒤쪽 경계 침범 |
0xcc | 페이지 해제됨 | PAGE_POISONING | 해제된 페이지 접근 |
0xdead000000000000계열 | LIST_POISON | 항상 | 리스트에서 제거된 노드의 next/prev 접근 |
0xdead000000000100 | LIST_POISON1 (next) | 항상 | list_del() 후 next 접근 |
0xdead000000000122 | LIST_POISON2 (prev) | 항상 | list_del() 후 prev 접근 |
0x0badca11 | debug_locks off | DEBUG_LOCK_ALLOC | lockdep가 비활성화된 후의 오류 |
0xdeadbeef | 일반적 디버그 마커 | 다양 | 디버깅용으로 의도적으로 설정한 값 |
0x00000000ffffffXX | ERR_PTR 값 | 항상 | 에러 포인터 (-ENOMEM 등)가 역참조됨 |
0xffffffff00000000+ 계열 | KASAN shadow 비정상 | KASAN | KASAN이 감지한 비정상 접근 |
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-bounds | vmalloc 영역 경계 밖 접근 | 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 등도 함께 출력됨
추가 메모리 디버깅 도구
| 도구 | CONFIG | 용도 | 오버헤드 | 환경 |
|---|---|---|---|---|
KCSAN | CONFIG_KCSAN | 데이터 레이스(동시성 버그) 탐지 | 중간 | 개발/테스트 |
UBSAN | CONFIG_UBSAN | 정의되지 않은 동작 탐지 (정수 오버플로우, 시프트 등) | 낮음 | 개발/프로덕션 |
KMSAN | CONFIG_KMSAN | 초기화되지 않은 메모리 사용 탐지 | 높음 | 개발/테스트 |
DEBUG_PAGEALLOC | CONFIG_DEBUG_PAGEALLOC | 해제된 페이지를 즉시 unmap → UAF 즉시 감지 | 높음 | 개발/테스트 |
DEBUG_KMEMLEAK | CONFIG_DEBUG_KMEMLEAK | 커널 메모리 누수 탐지 | 중간 | 개발/테스트 |
FAILSLAB | CONFIG_FAILSLAB | 메모리 할당 실패 주입 (오류 경로 테스트) | 낮음 | 테스트 |
LOCKDEP | CONFIG_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으로 쪼갭니다. 그 다음 ramoops는 struct pstore_info에 자기 read/write 콜백을 채워 pstore_register()에 넘깁니다. 이 시점부터 pstore 코어가 공통 프런트엔드가 되고, 이후 패닉이 발생하면 코어가 백엔드 콜백을 호출하는 방식으로 연결됩니다.
| 함수 | 위치 | 하는 일 | 실무 해석 |
|---|---|---|---|
ramoops_probe() | fs/pstore/ram.c | 파라미터 검증, zone 초기화, pstore_info 구성 | 여기서 실패하면 패닉이 나도 /sys/fs/pstore는 비어 있습니다. |
ramoops_init_przs() | fs/pstore/ram.c | dmesg 레코드용 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입니다.
| 필드 | 의미 | 구현상 주의점 |
|---|---|---|
mem_size | 예약 RAM 총 크기 | 너무 작으면 dmesg zone 수가 줄어 이전 패닉이 빨리 덮어써집니다. |
record_size | 각 dmesg 레코드 zone 크기 | 2의 거듭제곱으로 내림되며, 사실상 한 번의 패닉 snapshot 상한선을 결정합니다. |
console_size | 콘솔 로그용 단일 zone | panic snapshot과 별도로 console 프런트엔드가 쓰므로, dmesg와 내용이 다를 수 있습니다. |
pmsg_size | 사용자 공간 메시지 zone | 사용자 공간이 남긴 부가 힌트를 패닉 후에도 남길 수 있습니다. |
ftrace_size | ftrace zone 또는 CPU별 ftrace zones | RAMOOPS_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가 비어 있고, 어떤 시스템은 로그가 일부만 남는가"를 설명하는 함수 수준 흐름으로 내려가 보겠습니다.
panic()또는 Oops 경로가kmsg_dump(reason)를 호출합니다.kmsg_dump()는 단순 인라인 래퍼이고, 실제로는 등록된 모든kmsg_dumper를 순회합니다. pstore는 부팅 시 이미pstore_dumper를 등록해 두었기 때문에 이 목록에 포함됩니다.pstore_dump()가 잠금 전략을 먼저 정합니다.pstore_cannot_block_path()는 panic/NMI/긴급 재시작 경로에서 블로킹을 피해야 한다고 판단하면raw_spin_trylock_irqsave()를 쓰고, 일반 경로면 보통의 spin lock을 사용합니다. 즉 이 시점부터 pstore는 "가능하면 남기되, 패닉 경로 자체를 더 망가뜨리지는 말자"는 정책으로 동작합니다.kmsg_dump_rewind()와kmsg_dump_get_buffer()로 로그 ring buffer의 tail을 잘라 옵니다.kmsg_bytes만큼의 tail을 가져오되, 사람이 읽기 쉬운 syslog 형식으로 변환해 레코드 버퍼에 실어 나릅니다. 그래서 화면에 보인 몇 줄만 저장되는 것이 아니라, ring buffer에 남아 있는 범위 내에서 tail을 다시 끌어옵니다.pstore_record_init()가 공통 메타데이터를 채웁니다.type,reason,count,part, 타임스탬프, 압축 여부 같은 값이 여기서struct pstore_record에 들어갑니다.- 압축이 켜져 있으면 pstore 코어가 먼저 deflate를 시도합니다.
CONFIG_PSTORE_COMPRESS와compress=deflate조합이면 임시 버퍼에 압축한 뒤 백엔드로 넘깁니다. 압축이 되면 나중에 ramoops header의 타입 문자가C가 되고, 압축 실패 시에는 평문 그대로 저장됩니다. psinfo->write()가 실제 백엔드로 분기됩니다.
ramoops가 등록된 시스템이라면 이 포인터는ramoops_pstore_write()를 가리킵니다. 즉 pstore 코어는 백엔드 종류를 모른 채 공통 레코드만 만들고, 마지막 한 단계에서 실제 저장소로 위임합니다.ramoops_pstore_write()는 zone을 고른 뒤 새 패닉용 헤더를 씁니다.
dmesg의 경우cxt->dprzs[dump_write_cnt]를 고르고, 먼저persistent_ram_zap()으로 예전 내용을 비웁니다. 이어서ramoops_write_kmsg_hdr()가====timestamp-C/D헤더를 쓰고, 그 뒤에 실제 dump payload를 붙입니다.- ramoops는 dmesg를 여러 part로 나눠 저장하지 않습니다.
record->part != 1이면-ENOSPC를 반환해 추가 part를 거절합니다. 이는 여러 part가 서로 다른 zone에 찢어져 저장되면 복원과 해석이 더 혼란스러워지기 때문입니다. 따라서 실무에서는kmsg_bytes보다record_size가 더 작은지 항상 같이 봐야 합니다. - 마지막으로
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()가 돌아야 합니다.
| 순서 | 대표 함수 | 무엇을 하는가 | 관찰 포인트 |
|---|---|---|---|
| 1 | pstore_fill_super() | 파일시스템 superblock 생성 후 pstore_get_records(0) 호출 | mount는 단순 뷰가 아니라 레코드 스캔의 시작점입니다. |
| 2 | pstore_get_backend_records() | read_mutex를 잡고 backend open/read/close 루프 수행 | 동시 읽기 중복과 레코드 재생성 경쟁을 막습니다. |
| 3 | ramoops_pstore_open() | dmesg/console/pmsg/ftrace read 카운터 초기화 | 한 번의 스캔에서 각 zone을 순서대로 읽기 위해 필요합니다. |
| 4 | ramoops_pstore_read() | 다음 유효 zone을 찾고 header/ECC/압축 플래그를 해석 | 헤더가 깨져 있으면 그 레코드는 정리하고 건너뜁니다. |
| 5 | decompress_record() | 압축된 dmesg를 다시 평문으로 복원 | record->compressed가 참일 때만 동작합니다. |
| 6 | pstore_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_64 | ARM64 (AArch64) | RISC-V (rv64) | s390x |
|---|---|---|---|---|
| 프로그램 카운터 | RIP | PC (elr_el1) | epc (sepc) | PSW addr |
| 스택 포인터 | RSP | SP (sp_el0/sp_el1) | sp (x2) | R15 |
| 프레임 포인터 | RBP | x29 (FP) | s0 (x8) | R11 |
| 리턴 주소 | 스택에 push | x30 (LR) | ra (x1) | R14 |
| 첫 번째 인수 | RDI | x0 | a0 (x10) | R2 |
| 두 번째 인수 | RSI | x1 | a1 (x11) | R3 |
| 반환값 | RAX | x0 | a0 (x10) | R2 |
| 상태 레지스터 | EFLAGS | PSTATE (SPSR_EL1) | sstatus | PSW mask |
| 페이지 테이블 | CR3 | TTBR0_EL1/TTBR1_EL1 | satp | CR3 |
| 커널 스택 크기 | 16KB (4 pages) | 16KB (VMAP_STACK) | 16KB | 16KB |
아키텍처별 예외 유형
| x86_64 예외 | ARM64 대응 | RISC-V 대응 | 설명 |
|---|---|---|---|
Page Fault (#PF) | Data Abort (ESR_EL1) | Store/Load Page Fault | 잘못된 메모리 접근 |
GPF (#GP) | Alignment Fault | Illegal Instruction | 비정상 주소/명령 |
Double Fault (#DF) | 해당 없음 (별도 스택) | 해당 없음 | 스택 오버플로우 |
NMI | FIQ / SError | 해당 없음 (NMI 없음) | 마스킹 불가 인터럽트 |
MCE (#MC) | SError (GHES/SDEI) | 해당 없음 | 하드웨어 에러 |
UD (#UD) | Unknown/Undefined | Illegal 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_64 | ARM64 | RISC-V |
|---|---|---|---|
| Hardlockup 감지 | PMU perf NMI | SDEI/FIQ 기반 (v6.3+ buddy hardlockup) | 미지원 (NMI 없음) |
| Softlockup 감지 | hrtimer watchdog | hrtimer watchdog | hrtimer watchdog |
| NMI 전송 | APIC NMI IPI | 불가 (대안: SDEI) | 불가 |
| SysRq 강제 크래시 | NMI + echo c | echo c (NMI 없음) | echo c (NMI 없음) |
| 원격 디버그 트리거 | IPMI NMI | SDEI (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)가 향후 지원 예정
크래시 디버깅 체크리스트
커널 크래시 발생 시 체계적인 대응 절차를 따르면 복구 시간을 단축하고 재발을 방지할 수 있습니다. 아래 플로차트는 즉시 조치부터 근본 원인 분석까지 전체 프로세스를 보여줍니다.
커널 크래시 발생 시 분석 순서:
- dmesg / 시리얼 콘솔 로그 확보: 패닉 메시지, Call Trace, 레지스터 값
- Oops 유형 확인: NULL deref, GPF, page fault, BUG, soft/hardlockup
- RIP 위치 분석:
faddr2line또는addr2line으로 소스 라인 확인 - Call Trace 분석: 호출 경로를 역추적하여 근본 원인 파악
- 레지스터 값 검사: 의심스러운 값(poison, NULL, 범위 초과)
- vmcore 분석:
crash도구로 전체 시스템 상태 조사 - 재현: 스트레스 테스트, CONFIG_PROVE_LOCKING, KASAN 활성화
- 패치 & 검증: 수정 후 동일 조건에서 재현 불가 확인
| 도구 | 용도 | 필요 CONFIG |
|---|---|---|
decode_stacktrace.sh | Call Trace를 소스 라인으로 변환 | DEBUG_INFO |
faddr2line | 함수+오프셋 → 소스 위치 | DEBUG_INFO |
crash | vmcore 분석 | 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 |
관련 문서
크래시 분석과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.