디버깅 & 트러블슈팅 (Kernel Debugging)

Linux 커널 디버깅: printk, KGDB, ftrace, perf, 커널 새니타이저, lockdep, kdump, bpftrace 종합 가이드.

커널 디버깅 철학

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

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

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

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

printk 심화

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

로그 레벨

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

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

pr_* 매크로

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

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/printk.h>

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

dev_* 디바이스 매크로

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

#include <linux/device.h>

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

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

    return 0;
}

Dynamic Debug

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

# 특정 파일의 모든 디버그 메시지 활성화
echo 'file drivers/net/ethernet/intel/e1000e/*.c +p' > \
    /sys/kernel/debug/dynamic_debug/control

# 특정 함수의 디버그 메시지 활성화 (함수명 + 라인번호 포함)
echo 'func my_probe +pfl' > /sys/kernel/debug/dynamic_debug/control

# 특정 모듈의 모든 디버그 메시지 활성화
echo 'module e1000e +p' > /sys/kernel/debug/dynamic_debug/control

# 현재 활성화된 디버그 포인트 확인
cat /sys/kernel/debug/dynamic_debug/control | grep '=p'

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

printk 성능 고려사항

printk 성능: printk는 시리얼 콘솔이 연결된 상태에서 매우 느릴 수 있습니다(1초에 수백 줄 수준). 고빈도 경로에서 printk를 사용하면 타이밍이 완전히 변합니다. printk_ratelimit()이나 printk_once()를 활용하고, 성능에 민감한 경로에서는 trace_printk()를 사용하십시오.

/* trace_printk: ftrace 링 버퍼에 기록 (콘솔 출력 없음, 매우 빠름) */
trace_printk("hot path: val=%d\n", val);

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

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

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

printk 내부 아키텍처

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

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

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

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

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

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

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

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

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

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

콘솔 로그 레벨 제어

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

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

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

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

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

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

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

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

커널 전용 포맷 지정자

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

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

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

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

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

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

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

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

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

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

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

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

pr_cont()와 연속 메시지

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

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

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

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

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

/dev/kmsg와 structured logging

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

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

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

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

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

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

earlyprintk와 earlycon

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

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

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

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

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

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

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

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

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

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

OF_EARLYCON_DECLARE(uart8250, "ns16550a",
                    early_serial8250_setup);

netconsole

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

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

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

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

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

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

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

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

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

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

printk와 NMI 안전성

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

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

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

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

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

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

    return NMI_HANDLED;
}

dmesg와 journalctl 활용

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

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

# 전체 커널 로그 출력
dmesg

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

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

# 컬러 출력
dmesg --color=always

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

printk 디버깅 실전 패턴

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

KGDB/KDB

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

커널 CONFIG 옵션

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

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

시리얼 콘솔 설정

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

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

GDB 연결 방법

# 호스트에서 GDB 실행
gdb vmlinux

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

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

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

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

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

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

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

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

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

KDB 명령어

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

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

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

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

ftrace

ftrace(Function Tracer)는 커널 내장 트레이싱 프레임워크입니다. 함수 호출 추적, 이벤트 트레이싱, 레이턴시 분석 등 다양한 용도로 사용할 수 있습니다. 모든 인터페이스는 tracefs(/sys/kernel/debug/tracing 또는 /sys/kernel/tracing)를 통해 접근합니다.

ftrace 아키텍처와 동작 원리

ftrace는 컴파일 시점에 모든 커널 함수 프롤로그에 삽입되는 계측(instrumentation) 코드를 기반으로 동작합니다. gcc -pg 옵션이 각 함수 시작부에 mcount(또는 x86_64의 fentry) 호출을 삽입하며, 커널 부팅 시 이 호출들은 NOP으로 패치됩니다. ftrace가 활성화되면 NOP을 트램폴린(trampoline) 호출로 동적 교체하여 오버헤드 없이 선택적 트레이싱이 가능합니다.

컴파일 시점 gcc -pg → mcount/fentry 부팅 시 mcount → NOP 패치 트레이싱 활성화 NOP → trampoline 호출 (동적 패치) ftrace 내부 구조 ftrace_ops 체인 트램폴린 (코드 패치) 콜백 함수 호출 링 버퍼 기록 per-CPU 링 버퍼: 각 CPU마다 독립적인 lock-free 버퍼 → 캐시 바운싱 없음, 최소 오버헤드 동적 패치: text_poke_bp()로 INT3 기반 안전한 코드 패치 (CONFIG_DYNAMIC_FTRACE) 직접 트램폴린: ftrace_ops 단독 사용 시 직접 호출로 최적화 (FTRACE_OPS_FL_IPMODIFY)

커널 설정 옵션

ftrace 기능별로 필요한 커널 설정이 다릅니다. 핵심 옵션과 용도를 정리합니다.

설정 옵션설명의존성
CONFIG_FTRACEftrace 프레임워크 전체 활성화 (마스터 스위치)-
CONFIG_FUNCTION_TRACERfunction tracer (함수 진입 추적)FTRACE
CONFIG_FUNCTION_GRAPH_TRACERfunction_graph tracer (진입+반환, 실행 시간)FUNCTION_TRACER
CONFIG_DYNAMIC_FTRACENOP 동적 패치 (비활성 시 오버헤드 제로)FUNCTION_TRACER
CONFIG_FTRACE_SYSCALLS시스템 콜 트레이싱FTRACE
CONFIG_TRACER_SNAPSHOT스냅샷 버퍼 지원FTRACE
CONFIG_HIST_TRIGGERS히스토그램 트리거FTRACE
CONFIG_KPROBE_EVENTSkprobe 기반 동적 이벤트KPROBES, FTRACE
CONFIG_UPROBE_EVENTSuprobe 기반 유저 공간 동적 이벤트UPROBES, FTRACE
CONFIG_IRQSOFF_TRACER인터럽트 비활성 구간 레이턴시 측정FTRACE
CONFIG_PREEMPT_TRACER선점 비활성 구간 레이턴시 측정FTRACE
CONFIG_SCHED_TRACERwakeup 레이턴시 트레이서FTRACE
CONFIG_HWLAT_TRACER하드웨어 레이턴시 감지 트레이서FTRACE
CONFIG_OSNOISE_TRACEROS 노이즈 트레이서 (RT 환경)FTRACE
CONFIG_TIMERLAT_TRACER타이머 레이턴시 트레이서 (RT 환경)OSNOISE_TRACER
CONFIG_FTRACE_MCOUNT_RECORD부팅 시 mcount 위치 기록 (동적 패치용)DYNAMIC_FTRACE
CONFIG_FUNCTION_PROFILER함수별 호출 횟수/시간 프로파일링FUNCTION_TRACER
CONFIG_STACK_TRACER최대 스택 사용량 추적FUNCTION_TRACER
CONFIG_BLK_DEV_IO_TRACE블록 I/O 트레이서 (blktrace)FTRACE
# 현재 커널의 ftrace 관련 설정 확인
zcat /proc/config.gz | grep -i ftrace
# 또는
grep -i ftrace /boot/config-$(uname -r)

# 동적 ftrace 활성화 여부 확인
cat /sys/kernel/debug/tracing/README | head -5
# "# tracer: nop" 형태의 출력이 나오면 정상

tracefs 핵심 파일 레퍼런스

tracefs(일반적으로 /sys/kernel/debug/tracing 또는 커널 4.1+에서 /sys/kernel/tracing)의 주요 파일과 역할을 정리합니다.

파일/디렉토리역할읽기/쓰기
available_tracers사용 가능한 트레이서 목록읽기
current_tracer현재 활성 트레이서 설정/확인읽기/쓰기
trace트레이스 출력 (비소비적, 반복 읽기 가능)읽기/쓰기*
trace_pipe트레이스 출력 (소비적, 실시간 스트리밍)읽기
tracing_on트레이싱 활성/비활성 (1/0)읽기/쓰기
buffer_size_kbper-CPU 링 버퍼 크기 (KB)읽기/쓰기
buffer_total_size_kb전체 버퍼 크기 합계읽기
set_ftrace_filterfunction tracer 추적 대상 함수 필터읽기/쓰기
set_ftrace_notracefunction tracer 제외 함수 필터읽기/쓰기
set_ftrace_pid특정 PID만 추적읽기/쓰기
set_ftrace_notrace_pid특정 PID 제외읽기/쓰기
set_graph_functionfunction_graph tracer 대상 함수읽기/쓰기
set_graph_notracefunction_graph tracer 제외 함수읽기/쓰기
max_graph_depthfunction_graph 최대 추적 깊이읽기/쓰기
available_filter_functions필터링 가능한 함수 목록읽기
enabled_functions현재 활성화된 함수 목록읽기
function_profile_enabled함수 프로파일링 활성화읽기/쓰기
trace_stat/함수 프로파일링 통계읽기
trace_clock타임스탬프 클럭 소스 선택읽기/쓰기
trace_options트레이싱 옵션 플래그읽기/쓰기
options/개별 옵션 파일 (trace_options의 파일별 접근)읽기/쓰기
trace_marker유저 공간에서 트레이스 마커 기록쓰기
trace_marker_raw바이너리 트레이스 마커 기록쓰기
events/이벤트 카테고리별 디렉토리다양
kprobe_eventskprobe 동적 이벤트 정의읽기/쓰기
uprobe_eventsuprobe 동적 이벤트 정의읽기/쓰기
instances/트레이싱 인스턴스 관리디렉토리
snapshot현재 버퍼의 스냅샷 생성/읽기읽기/쓰기
stack_trace최대 스택 사용량 기록읽기
stack_max_size관측된 최대 스택 크기읽기/쓰기
saved_cmdlinesPID ↔ 커맨드명 매핑 캐시읽기
saved_cmdlines_size커맨드명 캐시 크기읽기/쓰기

* trace 파일에 빈 문자열을 쓰면 버퍼가 클리어됩니다: echo > trace

💡

trace vs trace_pipe: trace는 버퍼 내용을 비소비적으로 읽어 반복 확인이 가능합니다. trace_pipe는 소비적(consuming)으로 한 번 읽으면 사라지지만, 실시간 스트리밍에 적합하고 버퍼가 가득 차지 않습니다. 디버깅 시에는 trace로 분석하고, 장시간 모니터링에는 trace_pipe를 사용하세요.

function tracer

function tracer는 커널 함수 호출을 기록하는 가장 기본적인 트레이서입니다. 모든 함수 또는 필터링된 함수의 진입 시점을 캡처합니다.

# tracefs 마운트 확인 (커널 4.1+ 권장 경로)
mount -t tracefs tracefs /sys/kernel/tracing
# 기존 호환 경로: /sys/kernel/debug/tracing (debugfs 통해 접근)

# 사용 가능한 tracer 목록
cat /sys/kernel/debug/tracing/available_tracers
# hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

# function tracer 활성화
echo function > /sys/kernel/debug/tracing/current_tracer

# 특정 함수만 추적 (필터)
echo 'schedule*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'do_sys_open' >> /sys/kernel/debug/tracing/set_ftrace_filter

# 특정 함수 제외
echo '*rcu*' > /sys/kernel/debug/tracing/set_ftrace_notrace

# 트레이싱 시작/정지
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 작업 수행 ...
echo 0 > /sys/kernel/debug/tracing/tracing_on

# 결과 확인
cat /sys/kernel/debug/tracing/trace
# 출력 형식: <task>-<pid> [<cpu>] <flags> <timestamp>: <function> <- <caller>
#   bash-1234  [002] .... 12345.678901: schedule <- schedule_timeout

trace 출력 컬럼 상세

#                              _-----=> irqs-off          (d = 인터럽트 비활성)
#                             / _----=> need-resched      (N = TIF_NEED_RESCHED)
#                            | / _---=> hardirq/softirq   (H/h/s = 컨텍스트)
#                            || / _--=> preempt-depth     (0~f, 선점 비활성 깊이)
#                            ||| / _-=> migrate-disable   (숫자, 마이그레이션 비활성 깊이)
#                            |||| /
#     TASK-PID     [CPU#]  |||||  TIMESTAMP  FUNCTION
#        |  |         |   |||||     |           |
      bash-1234    [002]  d..1.  12345.678901: schedule <- schedule_timeout
   kworker/2:1-56  [002]  .N...  12345.679012: worker_thread <- kthread

function_graph tracer

function_graph tracer는 함수의 진입과 반환을 모두 추적하여 호출 트리를 그래프 형태로 표시합니다. 함수별 실행 시간도 함께 표시되어 성능 병목을 시각적으로 파악할 수 있습니다.

echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'vfs_*' > /sys/kernel/debug/tracing/set_graph_function

# 추적 깊이 제한 (깊은 호출 트리 제한)
echo 5 > /sys/kernel/debug/tracing/max_graph_depth

echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /proc/self/status  # 트레이싱 유발
echo 0 > /sys/kernel/debug/tracing/tracing_on

cat /sys/kernel/debug/tracing/trace
# 출력 예시:
#  CPU  DURATION                  FUNCTION CALLS
#   |     |   |                     |   |   |   |
#  2)               |  vfs_read() {
#  2)               |    rw_verify_area() {
#  2)   0.215 us    |      security_file_permission();
#  2)   0.587 us    |    }
#  2)               |    __vfs_read() {
#  2)               |      new_sync_read() {
#  2)   4.123 us    |        seq_read();
#  2)   4.890 us    |      }
#  2)   5.245 us    |    }
#  2)   6.102 us    |  }

function_graph 옵션 제어

# function_graph 전용 옵션 확인
cat /sys/kernel/debug/tracing/options/funcgraph-overhead
cat /sys/kernel/debug/tracing/options/funcgraph-duration
cat /sys/kernel/debug/tracing/options/funcgraph-proc

# 오버헤드 마커 (! = 100us+, + = 10us+, # = 1000us+)
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-overhead

# CPU 번호 표시
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-cpu

# 프로세스 이름 표시 (CPU 전환 시 유용)
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-proc

# 절대 타임스탬프 표시
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-abstime

# 인터럽트에 의한 함수 표시 안 함 (클린 출력)
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-irqs

# 테일 표시 (닫는 중괄호에 함수명 표시)
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-tail
#  2)   6.102 us    |  } /* vfs_read */

고급 필터링

ftrace 필터는 단순 와일드카드 외에도 모듈 지정, 정규표현식 유사 패턴, 인덱스 기반 필터 등 다양한 방법을 지원합니다.

# 와일드카드 패턴 (*, ? 지원)
echo 'vfs_*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo '*_read' >> /sys/kernel/debug/tracing/set_ftrace_filter
echo 'do_?et*' >> /sys/kernel/debug/tracing/set_ftrace_filter

# 모듈별 필터링 (:mod: 문법)
echo '*:mod:ext4' > /sys/kernel/debug/tracing/set_ftrace_filter
# ext4 모듈의 모든 함수 추적

echo 'write*:mod:nfs' > /sys/kernel/debug/tracing/set_ftrace_filter
# nfs 모듈 중 write로 시작하는 함수만

# 필터 추가 (>> 사용, > 은 기존 필터 대체)
echo 'tcp_*' >> /sys/kernel/debug/tracing/set_ftrace_filter
# 기존 필터에 tcp_* 추가

# 필터 제거 (! 접두사)
echo '!tcp_sendmsg' >> /sys/kernel/debug/tracing/set_ftrace_filter
# set_ftrace_filter에서 tcp_sendmsg만 제거

# 전체 필터 초기화 (빈 문자열)
echo > /sys/kernel/debug/tracing/set_ftrace_filter
# 모든 함수 추적 (필터 없음)

# PID 기반 필터링
echo $$ > /sys/kernel/debug/tracing/set_ftrace_pid
# 현재 셸 프로세스만 추적

echo 1234 5678 > /sys/kernel/debug/tracing/set_ftrace_pid
# 여러 PID 동시 지정

# function-fork 옵션: 자식 프로세스도 자동 추적
echo 1 > /sys/kernel/debug/tracing/options/function-fork

# 추적 가능한 함수 검색
cat /sys/kernel/debug/tracing/available_filter_functions | wc -l
# 일반적으로 40,000~80,000개 함수

cat /sys/kernel/debug/tracing/available_filter_functions | grep 'tcp_'

이벤트 트레이싱

커널은 수천 개의 정적 트레이스 포인트를 제공합니다. 이들은 ftrace 이벤트로 노출되며, 함수 트레이서와 독립적으로 사용할 수 있습니다. 이벤트는 계층적으로 events/<subsystem>/<event> 구조로 구성됩니다.

# 사용 가능한 이벤트 카테고리 확인
ls /sys/kernel/debug/tracing/events/
# block  ext4  irq  kmem  net  sched  signal  syscalls  timer  workqueue ...

# 이벤트 수 확인
cat /sys/kernel/debug/tracing/available_events | wc -l

# 개별 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

# 카테고리 전체 활성화
echo 1 > /sys/kernel/debug/tracing/events/sched/enable

# 모든 이벤트 활성화 (주의: 대량 출력)
echo 1 > /sys/kernel/debug/tracing/events/enable

# set_event 파일로 한 번에 설정
echo 'sched:sched_switch sched:sched_wakeup' > /sys/kernel/debug/tracing/set_event

# 이벤트 포맷 확인 (필터 작성에 필수)
cat /sys/kernel/debug/tracing/events/sched/sched_switch/format
# name: sched_switch
# format:
#   field:char prev_comm[16]; offset:8; size:16; signed:0;
#   field:pid_t prev_pid;     offset:24; size:4; signed:1;
#   field:int prev_prio;      offset:28; size:4; signed:1;
#   field:long prev_state;    offset:32; size:8; signed:1;
#   ...

이벤트 필터 문법

이벤트 필터는 C 유사 표현식으로 필드를 조건 검사합니다. 각 이벤트의 format 파일에서 사용 가능한 필드를 확인하세요.

# 단순 비교
echo 'prev_pid == 1234' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/filter

# 논리 연산자 (&&, ||, !)
echo 'prev_comm == "nginx" || next_comm == "nginx"' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/filter

# 비교 연산자 (==, !=, <, >, <=, >=)
echo 'bytes_req > 4096' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/filter

# 비트 연산 (&)
echo 'prev_state & 2' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/filter
# prev_state에 TASK_UNINTERRUPTIBLE 비트 설정된 경우

# 문자열 와일드카드 (~ 연산자)
echo 'prev_comm ~ "kworker*"' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/filter

# 서브시스템 레벨 필터 (하위 모든 이벤트에 적용)
echo 'common_pid == 1234' > \
    /sys/kernel/debug/tracing/events/sched/filter

# 필터 제거
echo 0 > /sys/kernel/debug/tracing/events/sched/sched_switch/filter

# 트레이싱 시작
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/debug/tracing/tracing_on

# 결과 확인
cat /sys/kernel/debug/tracing/trace

트리거 (Trigger)

이벤트 트리거를 사용하면 특정 이벤트가 발생할 때 자동으로 추가 동작을 수행할 수 있습니다. 트레이싱 제어, 스냅샷 생성, 스택 트레이스 캡처 등 다양한 동작을 조건부로 실행합니다.

# --- stacktrace 트리거: 이벤트 발생 시 커널 스택 트레이스 기록 ---
echo 'stacktrace' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
# 모든 kmalloc 호출 시 스택 트레이스 기록

# 조건부 stacktrace (4KB 이상 할당 시만)
echo 'stacktrace if bytes_req > 4096' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

# 횟수 제한 (처음 5회만)
echo 'stacktrace:5' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

# --- snapshot 트리거: 현재 버퍼 스냅샷 생성 ---
echo 'snapshot' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/trigger
# sched_switch 발생 시 스냅샷 → snapshot 파일에서 확인

echo 'snapshot if prev_comm == "critical_task"' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/trigger

# --- traceon / traceoff 트리거: 트레이싱 제어 ---
echo 'traceoff' > \
    /sys/kernel/debug/tracing/events/signal/signal_deliver/trigger
# 시그널 전달 시 트레이싱 중지 (문제 발생 직전 상태 보존)

echo 'traceon' > \
    /sys/kernel/debug/tracing/events/sched/sched_wakeup/trigger

# --- enable_event / disable_event 트리거: 다른 이벤트 제어 ---
echo 'enable_event:kmem:kmalloc' > \
    /sys/kernel/debug/tracing/events/syscalls/sys_enter_mmap/trigger
# mmap 시스템 콜 진입 시 kmalloc 이벤트 자동 활성화

echo 'disable_event:kmem:kmalloc' > \
    /sys/kernel/debug/tracing/events/syscalls/sys_exit_mmap/trigger

# --- 트리거 확인 및 제거 ---
cat /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
# 현재 설정된 트리거 확인

echo '!stacktrace' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
# ! 접두사로 트리거 제거

히스토그램 트리거 (hist trigger)

히스토그램 트리거(CONFIG_HIST_TRIGGERS)는 이벤트 데이터를 집계하여 분포도를 실시간으로 생성합니다. awk나 외부 도구 없이 커널 내에서 직접 통계를 수집할 수 있어 프로덕션 환경에서도 유용합니다.

# 기본 히스토그램: 시스템 콜별 호출 횟수
echo 'hist:key=id:val=hitcount:sort=hitcount.descending' > \
    /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/trigger

# 결과 확인
cat /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/hist
# { id:          0 } hitcount:      12345  ← sys_read
# { id:          1 } hitcount:       8901  ← sys_write
# ...

# 프로세스별 스케줄 레이턴시 분석
echo 'hist:key=next_comm:val=hitcount:sort=hitcount.descending' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/trigger

# 다중 키: 프로세스 + CPU별 분포
echo 'hist:key=next_comm,common_cpu:val=hitcount:sort=hitcount.descending' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/trigger

# kmalloc 할당 크기 분포 (log2 버킷)
echo 'hist:key=bytes_req.log2:val=hitcount:sort=bytes_req' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

# 조건부 히스토그램
echo 'hist:key=bytes_req:val=hitcount:sort=hitcount.descending if call_site != 0' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

# 스택 트레이스를 키로 사용 (할당 경로별 집계)
echo 'hist:key=stacktrace:val=bytes_req:sort=bytes_req.descending' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

# 타임스탬프 차이 계산 (wakeup 레이턴시)
# 1단계: sched_wakeup에서 타임스탬프 저장
echo 'hist:key=pid:val=hitcount:ts0=common_timestamp.usecs' > \
    /sys/kernel/debug/tracing/events/sched/sched_wakeup/trigger

# 2단계: sched_switch에서 레이턴시 계산
echo 'hist:key=next_pid:val=hitcount:wakeup_lat=common_timestamp.usecs-$ts0:sort=wakeup_lat' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/trigger

# 히스토그램 리셋
echo 'hist:key=id:val=hitcount:sort=hitcount.descending:clear' > \
    /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/trigger

# 히스토그램 일시정지/재개
echo 'hist:key=id:val=hitcount:sort=hitcount.descending:pause' > \
    /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/trigger
echo 'hist:key=id:val=hitcount:sort=hitcount.descending:continue' > \
    /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/trigger

# 히스토그램 트리거 제거
echo '!hist:key=id:val=hitcount' > \
    /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/trigger

인스턴스 (Instances)

ftrace 인스턴스를 사용하면 독립적인 트레이싱 세션을 병렬로 실행할 수 있습니다. 각 인스턴스는 독립된 버퍼, 필터, 이벤트 설정을 가지며 서로 간섭하지 않습니다. 예를 들어, 한 인스턴스에서 네트워킹을, 다른 인스턴스에서 스케줄러를 동시에 트레이싱할 수 있습니다.

# 새 인스턴스 생성 (디렉토리 생성)
mkdir /sys/kernel/debug/tracing/instances/net_trace
mkdir /sys/kernel/debug/tracing/instances/sched_trace

# 각 인스턴스는 독립된 tracefs 구조를 가짐
ls /sys/kernel/debug/tracing/instances/net_trace/
# buffer_size_kb  current_tracer  events/  trace  trace_pipe  ...

# 인스턴스 1: 네트워크 이벤트 추적
echo 4096 > /sys/kernel/debug/tracing/instances/net_trace/buffer_size_kb
echo 1 > /sys/kernel/debug/tracing/instances/net_trace/events/net/enable
echo 1 > /sys/kernel/debug/tracing/instances/net_trace/events/tcp/enable
echo 1 > /sys/kernel/debug/tracing/instances/net_trace/tracing_on

# 인스턴스 2: 스케줄러 추적 (동시 실행)
echo 2048 > /sys/kernel/debug/tracing/instances/sched_trace/buffer_size_kb
echo 1 > /sys/kernel/debug/tracing/instances/sched_trace/events/sched/enable
echo 1 > /sys/kernel/debug/tracing/instances/sched_trace/tracing_on

# 각 인스턴스 결과 독립적으로 확인
cat /sys/kernel/debug/tracing/instances/net_trace/trace
cat /sys/kernel/debug/tracing/instances/sched_trace/trace

# 인스턴스 삭제 (먼저 트레이싱 중지 후)
echo 0 > /sys/kernel/debug/tracing/instances/net_trace/tracing_on
echo nop > /sys/kernel/debug/tracing/instances/net_trace/current_tracer
rmdir /sys/kernel/debug/tracing/instances/net_trace
rmdir /sys/kernel/debug/tracing/instances/sched_trace
💡

인스턴스 활용 팁: 메인 트레이싱 버퍼를 건드리지 않고 독립적으로 운영할 수 있어, 프로덕션 서버에서 이미 실행 중인 모니터링에 영향을 주지 않고 추가 디버깅이 가능합니다. trace-cmd-B <instance> 옵션으로 인스턴스를 활용합니다.

레이턴시 트레이서

ftrace에는 실시간(RT) 시스템과 레이턴시 분석을 위한 전용 트레이서들이 포함되어 있습니다. 이들은 인터럽트/선점 비활성 구간이나 프로세스 깨움(wakeup) 지연 등 특정 레이턴시를 측정합니다.

트레이서측정 대상커널 설정
irqsoff인터럽트 비활성 최대 구간CONFIG_IRQSOFF_TRACER
preemptoff선점 비활성 최대 구간CONFIG_PREEMPT_TRACER
preemptirqsoff인터럽트 또는 선점 비활성 최대 구간위 두 가지 모두
wakeup일반 프로세스 wakeup → 실행 최대 레이턴시CONFIG_SCHED_TRACER
wakeup_rtRT 프로세스 wakeup → 실행 최대 레이턴시CONFIG_SCHED_TRACER
wakeup_dlDEADLINE 프로세스 wakeup 레이턴시CONFIG_SCHED_TRACER
hwlat하드웨어 레이턴시 (SMI, NMI 등)CONFIG_HWLAT_TRACER
osnoiseOS 노이즈 (인터럽트, softirq 등)CONFIG_OSNOISE_TRACER
timerlat타이머 인터럽트 → 스레드 실행 레이턴시CONFIG_TIMERLAT_TRACER
# --- irqsoff: 인터럽트 비활성 최대 구간 추적 ---
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 10
echo 0 > /sys/kernel/debug/tracing/tracing_on

# 최대 레이턴시 확인
cat /sys/kernel/debug/tracing/tracing_max_latency
# 42  (마이크로초)

# 해당 구간의 상세 트레이스
cat /sys/kernel/debug/tracing/trace
# irqsoff latency trace v1.1.5 on ...
# latency: 42 us, #4/4, CPU#1 | ...
#    <...>-1234  1d...  12us : spin_lock_irqsave
#    <...>-1234  1d...  54us : spin_unlock_irqrestore

# 레이턴시 임계값 설정 (이전 최대값보다 커야 기록)
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency
# 리셋하여 새로운 측정 시작

echo 100 > /sys/kernel/debug/tracing/tracing_thresh
# 100us 이상일 때만 트레이스 기록

# --- preemptirqsoff: 인터럽트 또는 선점 비활성 구간 ---
echo preemptirqsoff > /sys/kernel/debug/tracing/current_tracer
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency
echo 1 > /sys/kernel/debug/tracing/tracing_on

# --- wakeup_rt: RT 프로세스 스케줄링 레이턴시 ---
echo wakeup_rt > /sys/kernel/debug/tracing/current_tracer
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency
echo 1 > /sys/kernel/debug/tracing/tracing_on
# RT 프로세스 실행 후...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/tracing_max_latency
# wakeup에서 실제 실행까지의 최대 레이턴시

# --- hwlat: 하드웨어 레이턴시 (SMI 감지) ---
echo hwlat > /sys/kernel/debug/tracing/current_tracer
# hwlat_detector 설정
echo 1000000 > /sys/kernel/debug/tracing/tracing_thresh  # 1ms 임계값
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 60
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

kprobe / kretprobe 동적 이벤트

kprobe 이벤트는 커널 함수의 임의 지점에 동적으로 프로브를 설치하여, 컴파일 시점에 트레이스 포인트가 없는 함수도 추적할 수 있습니다. kretprobe는 함수 반환 시점에 프로브를 설치합니다.

# --- kprobe 이벤트 정의 ---
# 형식: p[:[GRP/]EVENT] SYMBOL[+OFFSET] [FETCHARGS]

# do_sys_openat2 진입 시 파일명(첫 번째 인자) 캡처
echo 'p:myprobes/open_entry do_sys_openat2 filename=+0(%si):string' > \
    /sys/kernel/debug/tracing/kprobe_events

# kretprobe: 반환값 캡처
echo 'r:myprobes/open_return do_sys_openat2 ret=$retval:s64' >> \
    /sys/kernel/debug/tracing/kprobe_events

# 정의된 이벤트 확인
cat /sys/kernel/debug/tracing/kprobe_events

# 이벤트 활성화 (events 디렉토리에 자동 생성됨)
echo 1 > /sys/kernel/debug/tracing/events/myprobes/open_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/myprobes/open_return/enable

# 필터 적용 (일반 이벤트와 동일)
echo 'filename ~ "/etc/*"' > \
    /sys/kernel/debug/tracing/events/myprobes/open_entry/filter

# 트레이싱
echo 1 > /sys/kernel/debug/tracing/tracing_on
ls /etc/ > /dev/null  # 트레이싱 유발
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

# --- fetch 인자 문법 ---
# %REG       : 레지스터 값 (x86_64: %di, %si, %dx, %cx, %r8, %r9)
# +OFFSET(%REG) : 레지스터가 가리키는 메모리 + 오프셋
# $retval    : 반환값 (kretprobe에서만)
# $stack     : 스택 포인터
# $stackN    : 스택의 N번째 엔트리
# $comm      : 현재 프로세스명
# @SYMBOL    : 심볼 주소의 값

# 구조체 필드 접근 (BTF 지원 시, 커널 5.8+)
echo 'p:myprobes/tcp_probe tcp_sendmsg sk=+0(%di):x64 size=%dx:u32' >> \
    /sys/kernel/debug/tracing/kprobe_events

# --- kprobe 이벤트 제거 ---
echo '-:myprobes/open_entry' >> /sys/kernel/debug/tracing/kprobe_events
echo '-:myprobes/open_return' >> /sys/kernel/debug/tracing/kprobe_events
# 또는 전체 제거
echo > /sys/kernel/debug/tracing/kprobe_events

uprobe 동적 이벤트 (유저 공간)

# 유저 공간 함수에 프로브 설치
# 형식: p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS]

# libc의 malloc 추적
echo 'p:uprobes/malloc_entry /usr/lib/x86_64-linux-gnu/libc.so.6:malloc size=%di:u64' > \
    /sys/kernel/debug/tracing/uprobe_events

echo 'r:uprobes/malloc_return /usr/lib/x86_64-linux-gnu/libc.so.6:malloc ret=$retval:x64' >> \
    /sys/kernel/debug/tracing/uprobe_events

echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on

trace_marker (유저 공간 마커)

trace_marker 파일에 유저 공간에서 직접 문자열을 기록하여 커널 트레이스와 타임라인을 동기화할 수 있습니다. 애플리케이션-커널 간 상호작용을 분석할 때 매우 유용합니다.

# 셸에서 마커 기록
echo 'my_app: request_start id=42' > /sys/kernel/debug/tracing/trace_marker
# ... 작업 수행 ...
echo 'my_app: request_end id=42' > /sys/kernel/debug/tracing/trace_marker
/* C 프로그램에서 trace_marker 사용 */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

static int trace_fd = -1;

void trace_init(void) {
    trace_fd = open("/sys/kernel/debug/tracing/trace_marker", O_WRONLY);
}

void trace_write(const char *fmt, ...) {
    char buf[256];
    va_list ap;
    va_start(ap, fmt);
    int len = vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    if (trace_fd >= 0)
        write(trace_fd, buf, len);
}

int main(void) {
    trace_init();
    trace_write("app_start: pid=%d", getpid());
    /* ... 작업 수행 ... */
    trace_write("app_end: result=%d", 0);
    return 0;
}

스택 트레이서

스택 트레이서(CONFIG_STACK_TRACER)는 커널 실행 중 관측된 최대 스택 사용량을 추적합니다. 스택 오버플로우가 의심되거나 커널 스택 크기를 최적화할 때 유용합니다.

# 스택 트레이서 활성화
echo 1 > /proc/sys/kernel/stack_tracer_enabled

# 현재까지 관측된 최대 스택 크기
cat /sys/kernel/debug/tracing/stack_max_size
# 3856  (바이트)

# 최대 스택을 사용한 호출 체인
cat /sys/kernel/debug/tracing/stack_trace
#  Depth    Size   Location    (28 entries)
#  -----    ----   --------
#  0)     3856     176   update_curr+0x6f/0x1a0
#  1)     3680     256   enqueue_entity+0x1f3/0x430
#  2)     3424     128   enqueue_task_fair+0x5d/0x1c0
#  ...

# 최대값 리셋
echo 0 > /sys/kernel/debug/tracing/stack_max_size

함수 프로파일링

함수 프로파일러는 각 함수의 호출 횟수, 총 실행 시간, 평균 실행 시간을 수집합니다. function_graph tracer의 오버헤드 없이 통계만 필요할 때 유용합니다.

# 프로파일링 활성화
echo 1 > /sys/kernel/debug/tracing/function_profile_enabled

# 부하 생성 후 통계 확인 (per-CPU)
cat /sys/kernel/debug/tracing/trace_stat/function0
#  Function                  Hit    Time            Avg             s^2
#  --------                  ---    ----            ---             ---
#  schedule                12345    12345.678 us    1.000 us        0.123 us
#  __schedule              12345    11234.567 us    0.910 us        0.098 us
#  mutex_lock              45678    2345.678 us     0.051 us        0.001 us

# 프로파일링 비활성화
echo 0 > /sys/kernel/debug/tracing/function_profile_enabled

trace-cmd 사용법

trace-cmd는 ftrace의 사용자 공간 프론트엔드로, tracefs를 직접 조작하는 것보다 훨씬 편리합니다. 기록(record), 리포트(report), 실시간 스트림(stream), 프로파일링(profile) 등 다양한 서브커맨드를 제공합니다.

# --- 기본 사용법 ---
# 함수 트레이싱 기록
trace-cmd record -p function_graph -g vfs_read -g vfs_write \
    -F cat /proc/self/status

# 기록된 트레이스 보기
trace-cmd report | head -50

# 이벤트 트레이싱
trace-cmd record -e sched_switch -e sched_wakeup sleep 5

# 특정 PID만 추적
trace-cmd record -p function -P 1234

# 실시간 스트림 모드
trace-cmd stream -p function_graph -g tcp_sendmsg

# --- 고급 사용법 ---
# 필터와 트리거 함께 사용
trace-cmd record -e sched_switch \
    -f 'prev_comm == "myapp" || next_comm == "myapp"' \
    -T 'stacktrace' \
    sleep 10

# 여러 이벤트 + 함수 트레이서 동시 사용
trace-cmd record -p function_graph \
    -g do_sys_openat2 \
    -e syscalls:sys_enter_openat \
    -e kmem:kmalloc \
    -- ls /tmp

# 인스턴스 사용 (-B 옵션)
trace-cmd record -B net_events -e net -e tcp \
    -B sched_events -e sched \
    sleep 5
trace-cmd report -B net_events

# 프로파일 모드 (이벤트별 통계 수집)
trace-cmd profile -e sched -e irq sleep 10
# task   runtime   sleep   wakeup  preempt  ...

# 히스토그램 출력
trace-cmd hist -e sched_switch -k next_comm -s hitcount sleep 5

# 리모트 트레이싱 (호스트-타겟 분리)
# 타겟에서:
trace-cmd listen -p 12345
# 호스트에서:
trace-cmd record -N target_ip:12345 -e sched sleep 10

# trace.dat 파일 분석
trace-cmd report --cpu 0              # CPU 0만
trace-cmd report -F 'sched_switch'   # 특정 이벤트만
trace-cmd report --stat              # 통계 요약
trace-cmd dump                       # 원시 형식 출력

트레이싱 옵션과 클럭

# 사용 가능한 클럭 소스 확인
cat /sys/kernel/debug/tracing/trace_clock
# [local] global counter uptime perf mono mono_raw boot x86-tsc

# 클럭 선택 (CPU 간 동기화가 필요하면 global)
echo global > /sys/kernel/debug/tracing/trace_clock

# 주요 클럭 유형:
# local    : per-CPU, 가장 빠름 (기본값), CPU 간 비교 불가
# global   : 전체 CPU 동기화, 약간 느림
# counter  : 단순 증가 카운터 (시간 개념 없음)
# uptime   : jiffies 기반
# perf     : perf와 동일한 클럭
# mono     : ktime_get_mono_fast_ns
# boot     : 부팅 이후 monotonic (suspend 포함)
# x86-tsc  : TSC 직접 사용 (x86 전용, 가장 정밀)

# 버퍼 크기 조정
echo 8192 > /sys/kernel/debug/tracing/buffer_size_kb
# per-CPU 8MB 버퍼 (총 = 8MB × CPU 수)

cat /sys/kernel/debug/tracing/buffer_total_size_kb

# 주요 트레이싱 옵션
echo nooverwrite > /sys/kernel/debug/tracing/trace_options
# 버퍼 가득 찰 때 새 데이터 버림 (기본: overwrite, 오래된 데이터 덮어쓰기)

echo sym-offset > /sys/kernel/debug/tracing/trace_options
# 함수 오프셋 표시 (schedule+0x2e/0x100)

echo sym-addr > /sys/kernel/debug/tracing/trace_options
# 함수 주소 표시

echo stacktrace > /sys/kernel/debug/tracing/trace_options
# 모든 이벤트에 스택 트레이스 추가 (오버헤드 큼)

echo userstacktrace > /sys/kernel/debug/tracing/trace_options
# 유저 공간 스택 트레이스도 추가

echo print-parent > /sys/kernel/debug/tracing/trace_options
# 호출자(parent) 함수 표시

echo latency-format > /sys/kernel/debug/tracing/trace_options
# 레이턴시 트레이서 전용 포맷

# 현재 모든 옵션 상태 확인
cat /sys/kernel/debug/tracing/trace_options

커널 ftrace API

커널 모듈에서 프로그래밍 방식으로 ftrace를 활용할 수 있습니다. ftrace_ops 구조체를 등록하여 함수 호출 시 콜백을 받거나, 함수를 동적으로 교체(livepatch)할 수 있습니다.

#include <linux/ftrace.h>
#include <linux/module.h>
#include <linux/kallsyms.h>

/* 콜백 함수: 추적 대상 함수가 호출될 때마다 실행 */
static void my_ftrace_callback(
    unsigned long ip,           /* 추적 대상 함수 주소 */
    unsigned long parent_ip,    /* 호출자 함수 주소 */
    struct ftrace_ops *ops,
    struct ftrace_regs *fregs)
{
    struct pt_regs *regs = ftrace_get_regs(fregs);

    if (!within_module(parent_ip, THIS_MODULE))
        pr_info("my_func called from %pS\n", (void *)parent_ip);
}

static struct ftrace_ops my_ops = {
    .func    = my_ftrace_callback,
    .flags   = FTRACE_OPS_FL_SAVE_REGS       /* 레지스터 저장 */
             | FTRACE_OPS_FL_RECURSION        /* 재귀 보호 */
             | FTRACE_OPS_FL_IPMODIFY,        /* IP 수정 가능 (livepatch용) */
};

static unsigned long target_func_addr;

static int __init my_ftrace_init(void)
{
    int ret;

    /* 추적 대상 함수 주소 획득 */
    target_func_addr = kallsyms_lookup_name("do_sys_openat2");
    if (!target_func_addr)
        return -ENOENT;

    /* 특정 함수만 필터링 */
    ret = ftrace_set_filter_ip(&my_ops, target_func_addr, 0, 0);
    if (ret)
        return ret;

    /* ftrace 콜백 등록 */
    ret = register_ftrace_function(&my_ops);
    if (ret) {
        ftrace_set_filter_ip(&my_ops, target_func_addr, 1, 0);
        return ret;
    }

    pr_info("ftrace hook installed on %pS\n", (void *)target_func_addr);
    return 0;
}

static void __exit my_ftrace_exit(void)
{
    unregister_ftrace_function(&my_ops);
    ftrace_set_filter_ip(&my_ops, target_func_addr, 1, 0);
    pr_info("ftrace hook removed\n");
}

module_init(my_ftrace_init);
module_exit(my_ftrace_exit);
MODULE_LICENSE("GPL");

ftrace 기반 함수 후킹 (livepatch 방식)

/* ftrace를 사용한 함수 교체 (간소화 예제)
 * 실제 사용 시에는 커널 livepatch 프레임워크 권장 */
#include <linux/ftrace.h>
#include <linux/version.h>

static unsigned long orig_func;
static unsigned long hook_func;

static void fh_ftrace_thunk(
    unsigned long ip,
    unsigned long parent_ip,
    struct ftrace_ops *ops,
    struct ftrace_regs *fregs)
{
    struct pt_regs *regs = ftrace_get_regs(fregs);

    /* 자기 자신(모듈)에서 호출된 경우 무시 (무한 재귀 방지) */
    if (within_module(parent_ip, THIS_MODULE))
        return;

    /* IP를 후킹 함수로 변경 */
    regs->ip = hook_func;
}

static struct ftrace_ops fh_ops = {
    .func    = fh_ftrace_thunk,
    .flags   = FTRACE_OPS_FL_SAVE_REGS
             | FTRACE_OPS_FL_RECURSION
             | FTRACE_OPS_FL_IPMODIFY,
};

커스텀 트레이스 포인트

커널 모듈이나 서브시스템에 정적 트레이스 포인트를 추가하여 ftrace 이벤트로 노출할 수 있습니다. TRACE_EVENT 매크로는 이벤트 정의, 바이너리 포맷, 출력 형식을 한 번에 선언합니다.

/* include/trace/events/mysubsys.h */
#undef TRACE_SYSTEM
#define TRACE_SYSTEM mysubsys

#if !defined(_TRACE_MYSUBSYS_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_MYSUBSYS_H

#include <linux/tracepoint.h>

TRACE_EVENT(mysubsys_do_work,
    /* 프로토타입: 트레이스 포인트 호출 시 인자 */
    TP_PROTO(struct work_struct *work, int result),

    /* 인자 이름 */
    TP_ARGS(work, result),

    /* 링 버퍼에 저장할 필드 정의 */
    TP_STRUCT__entry(
        __field(void *, work)
        __field(int, result)
        __field(u64, timestamp)
        __string(name, work->func ? "named" : "unnamed")
    ),

    /* 인자 → 링 버퍼 필드 복사 (빠르게 실행되어야 함) */
    TP_fast_assign(
        __entry->work = work;
        __entry->result = result;
        __entry->timestamp = ktime_get_ns();
        __assign_str(name, work->func ? "named" : "unnamed");
    ),

    /* 텍스트 출력 포맷 (trace 파일에서 읽을 때 사용) */
    TP_printk("work=%p result=%d ts=%llu name=%s",
        __entry->work, __entry->result,
        __entry->timestamp, __get_str(name))
);

/* TRACE_EVENT_CONDITION: 조건부 트레이스 포인트 */
TRACE_EVENT_CONDITION(mysubsys_error,
    TP_PROTO(int errcode, const char *msg),
    TP_ARGS(errcode, msg),
    TP_CONDITION(errcode < 0),    /* 에러일 때만 기록 */
    TP_STRUCT__entry(
        __field(int, errcode)
        __string(msg, msg)
    ),
    TP_fast_assign(
        __entry->errcode = errcode;
        __assign_str(msg, msg);
    ),
    TP_printk("error=%d msg=%s", __entry->errcode, __get_str(msg))
);

#endif /* _TRACE_MYSUBSYS_H */

#undef TRACE_INCLUDE_PATH
#define TRACE_INCLUDE_PATH .
#define TRACE_INCLUDE_FILE mysubsys
#include <trace/define_trace.h>
/* 소스 파일에서 트레이스 포인트 사용 */
#define CREATE_TRACE_POINTS
#include "trace/events/mysubsys.h"

void do_work(struct work_struct *work)
{
    int result;
    /* ... 작업 수행 ... */
    result = process(work);

    /* 트레이스 포인트 호출 (비활성 시 NOP과 동일한 오버헤드) */
    trace_mysubsys_do_work(work, result);
    trace_mysubsys_error(result, "processing failed");
}

실전 디버깅 시나리오

시나리오 1: 느린 시스템 콜 원인 분석

# 1. function_graph로 특정 시스템 콜의 내부 호출 흐름 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo do_sys_openat2 > /sys/kernel/debug/tracing/set_graph_function
echo 10 > /sys/kernel/debug/tracing/max_graph_depth

# 대상 프로세스 PID만 추적
echo $TARGET_PID > /sys/kernel/debug/tracing/set_ftrace_pid
echo 1 > /sys/kernel/debug/tracing/options/function-fork

echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 문제 재현 ...
echo 0 > /sys/kernel/debug/tracing/tracing_on

# funcgraph-overhead 마커로 느린 함수 빠르게 식별
# ! = 100us+, + = 10us+, # = 1000us+
cat /sys/kernel/debug/tracing/trace | grep '[!+#]'

시나리오 2: 메모리 누수 의심 경로 추적

# kmalloc/kfree 짝 맞추기
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kfree/enable

# stacktrace 트리거로 할당 경로 기록
echo 'stacktrace if bytes_req >= 4096' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

# 히스토그램으로 할당 크기별 + 호출 경로별 집계
echo 'hist:key=call_site.sym,bytes_req:val=hitcount:sort=hitcount.descending' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 30
echo 0 > /sys/kernel/debug/tracing/tracing_on

# 가장 많이 할당하는 경로 확인
cat /sys/kernel/debug/tracing/events/kmem/kmalloc/hist

시나리오 3: 인터럽트 비활성으로 인한 지연 추적

# irqsoff 트레이서로 최대 인터럽트 비활성 구간 찾기
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency

# 50us 이상만 기록
echo 50 > /sys/kernel/debug/tracing/tracing_thresh
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 부하 발생 후 분석
sleep 60
echo 0 > /sys/kernel/debug/tracing/tracing_on

echo "최대 인터럽트 비활성 시간:"
cat /sys/kernel/debug/tracing/tracing_max_latency
echo "상세 트레이스:"
cat /sys/kernel/debug/tracing/trace

시나리오 4: 네트워크 패킷 경로 추적

# trace-cmd로 네트워크 스택 함수 그래프 추적
trace-cmd record -p function_graph \
    -g ip_rcv -g ip_local_deliver -g tcp_v4_rcv \
    -g ip_output -g tcp_sendmsg \
    -e net:net_dev_xmit \
    -e net:netif_receive_skb \
    -O funcgraph-proc \
    -O funcgraph-tail \
    -- curl -s http://localhost/ > /dev/null

trace-cmd report | head -100
⚠️

프로덕션 환경 주의사항: ftrace는 프로덕션 환경에서도 사용할 수 있도록 설계되었지만, 다음 사항에 주의하세요. (1) function tracer를 필터 없이 사용하면 5~10% 성능 저하가 발생할 수 있습니다. 반드시 set_ftrace_filter로 대상을 제한하세요. (2) 버퍼 크기가 크면 메모리를 많이 소모합니다 (buffer_size_kb × CPU 수). (3) stacktrace 옵션을 글로벌로 켜면 심각한 오버헤드가 발생합니다. 트리거로 조건부 사용을 권장합니다. (4) 트레이싱 완료 후 반드시 정리하세요: echo nop > current_tracer, echo 0 > events/enable.

ftrace 원스톱 리셋 스크립트

#!/bin/bash
# ftrace 상태를 초기 상태로 완전히 리셋
TRACEFS=/sys/kernel/debug/tracing

echo nop > $TRACEFS/current_tracer
echo 0 > $TRACEFS/tracing_on
echo 0 > $TRACEFS/events/enable
echo > $TRACEFS/set_ftrace_filter
echo > $TRACEFS/set_ftrace_notrace
echo > $TRACEFS/set_ftrace_pid
echo > $TRACEFS/set_graph_function
echo > $TRACEFS/set_graph_notrace
echo 0 > $TRACEFS/max_graph_depth
echo > $TRACEFS/trace              # 버퍼 클리어
echo 0 > $TRACEFS/tracing_max_latency
echo 0 > $TRACEFS/tracing_thresh
echo 1408 > $TRACEFS/buffer_size_kb  # 기본값 복원
echo local > $TRACEFS/trace_clock     # 기본 클럭
echo > $TRACEFS/kprobe_events        # kprobe 이벤트 제거
echo > $TRACEFS/uprobe_events        # uprobe 이벤트 제거

# 인스턴스 정리
for inst in $TRACEFS/instances/*/; do
    [ -d "$inst" ] || continue
    echo 0 > "${inst}tracing_on"
    echo 0 > "${inst}events/enable"
    echo nop > "${inst}current_tracer"
    rmdir "$inst" 2>/dev/null
done

echo "ftrace reset complete"

perf

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

하드웨어 카운터 활용

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

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

perf stat

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

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

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

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

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

perf record / perf report

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

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

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

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

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

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

Flame Graph 생성

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

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

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

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

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

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

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

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

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

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

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

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

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

💡

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

커널 새니타이저

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

KASAN (Kernel Address Sanitizer)

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

동작 원리: 섀도 메모리

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

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

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

세 가지 모드 비교

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

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

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

커널 설정

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

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

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

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

부트 파라미터

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

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

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

탐지 가능한 버그 유형

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

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

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

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

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

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

KASAN 리포트 읽기

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

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

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

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

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

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

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

KASAN 테스트 및 어노테이션

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

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

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

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

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

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

KMSAN (Kernel Memory Sanitizer)

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

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

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

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

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

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

KCSAN (Kernel Concurrency Sanitizer)

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

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

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

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

커널 설정

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

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

리포트 읽기

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

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

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

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

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

데이터 레이스 수정 방법

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

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

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

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

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

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

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

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

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

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

KCSAN 어노테이션

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

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

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

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

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

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

UBSAN (Undefined Behavior Sanitizer)

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

커널 설정

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

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

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

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

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

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

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

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

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

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

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

UBSAN 리포트 읽기

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

UBSAN 어노테이션

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

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

새니타이저 조합 가이드

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

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

💡

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

lockdep

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

데드락 탐지 원리

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

CONFIG 옵션

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

lock ordering 검증

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

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

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

lockdep 경고 메시지 해석

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

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

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

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

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

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

lockstat (/proc/lock_stat)

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

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

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

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

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

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

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

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

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

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

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

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

IRQ-safe/unsafe Lock 혼용 경고

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

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

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

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

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

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

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

의존성 체인 읽기

# lockdep이 출력하는 의존성 체인 예시 (3개 lock 순환)
the existing dependency chain (in reverse order) is:

-> #2 (&lock_c){+.+.}-{2:2}:
       lock_acquire+0xd1/0x2d0
       mutex_lock+0x2c/0x40
       func_bc+0x30/0x80         # lock_b → lock_c 순서로 획득

-> #1 (&lock_b){+.+.}-{2:2}:
       lock_acquire+0xd1/0x2d0
       mutex_lock+0x2c/0x40
       func_ab+0x30/0x80         # lock_a → lock_b 순서로 획득

-> #0 (&lock_a){+.+.}-{2:2}:
       lock_acquire+0xd1/0x2d0
       mutex_lock+0x2c/0x40
       func_ca+0x30/0x80         # lock_c → lock_a 시도 ← 순환!

# 역순으로 읽으면:
#   func_ab: lock_a → lock_b
#   func_bc: lock_b → lock_c
#   func_ca: lock_c → lock_a (시도) ← 순환 데드락!
#
# 해결: 3개 lock의 전역 순서를 정의하고 일관되게 따름
# 예: lock_a → lock_b → lock_c (항상 이 순서로만 획득)

lockdep_set_class와 nested locking

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

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

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

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

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

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

lockdep 실전 디버깅 명령

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

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

# lock chain 확인
cat /proc/lockdep_chains

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

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

lockdep 내부 아키텍처

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

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

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

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

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

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

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

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

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

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

lockdep_map과 lock_acquire/release API

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

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

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

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

static struct lock_class_key my_lock_key;

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

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

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

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

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

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

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

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

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

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

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

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

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

Wait type 검증 (LD_WAIT_*)

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

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

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

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

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

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

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

Cross-lock 타입 의존성 검증

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

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

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

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

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

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

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

  Possible unsafe locking scenario:

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

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

lockdep 한계와 오버플로우 복구

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

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

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

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

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

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

# 또는
BUG: MAX_LOCKDEP_ENTRIES too low!

# 또는
BUG: MAX_LOCKDEP_CHAINS too low!

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

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

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

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

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

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

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

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

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

lockdep과 커널 모듈

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

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

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

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

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

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

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

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

lockdep procfs 인터페이스 상세

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

locktorture: 잠금 스트레스 테스트

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

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

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

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

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

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

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

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

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

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

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

kdump/crash

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

kexec 시스템 콜 내부 구조

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

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

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

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

Purgatory: 커널 전환 중간 단계

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

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

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

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

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

kexec/kdump 동작 흐름

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

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

kexec/kdump 커널 CONFIG 상세

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

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

kdump 설정 (kexec)

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

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

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

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

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

vmcore 캡처

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

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

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

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

crash 유틸리티 사용법

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

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

crash 주요 명령어

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

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

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

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

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

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

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

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

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

# mod: 로드된 모듈
crash> mod

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

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

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

crash 고급 분석 기법

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

crash에서 GDB 명령 활용

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

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

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

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

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

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

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

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

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

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

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

crash 분석 실전 시나리오

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

crash vs drgn 선택 기준:

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

커널 패닉 분석

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

Oops 메시지 구조 해석

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

스택 트레이스 읽는 방법

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

addr2line, faddr2line 활용

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

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

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

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

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

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

BUG/WARN 매크로

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

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

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

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

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

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

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

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

bpftrace

bpftrace는 eBPF 기반의 고수준 동적 트레이싱 도구입니다. awk와 유사한 간결한 문법으로 커널과 유저 공간의 이벤트를 트레이싱할 수 있습니다. DTrace/SystemTap의 Linux 대안으로, 프로덕션 환경에서도 낮은 오버헤드로 사용할 수 있습니다.

프로브 종류

프로브 타입설명예시
kprobe커널 함수 진입kprobe:vfs_read
kretprobe커널 함수 반환kretprobe:vfs_read
tracepoint정적 트레이스 포인트tracepoint:syscalls:sys_enter_open
uprobe유저 함수 진입uprobe:/bin/bash:readline
usdt유저 정적 DTrace 프로브usdt:/lib/libc.so:malloc
software소프트웨어 이벤트software:page-faults:1
hardware하드웨어 이벤트hardware:cache-misses:1000
profileCPU 타이머 샘플링profile:hz:99
interval주기적 출력interval:s:1

bpftrace 원라이너 예제

# 프로세스별 syscall 횟수
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

# 프로세스별 read() 바이트 수 히스토그램
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ {
    @bytes[comm] = hist(args->ret);
}'

# vfs_read 레이턴시 (마이크로초) 히스토그램
bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
    @usecs = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# 1초마다 블록 I/O 바이트 수 출력
bpftrace -e 'tracepoint:block:block_rq_complete {
    @bytes = sum(args->nr_sector * 512);
}
interval:s:1 { print(@bytes); clear(@bytes); }'

# TCP 연결 추적 (소스/목적지)
bpftrace -e 'kprobe:tcp_connect {
    $sk = (struct sock *)arg0;
    @[ntop($sk->__sk_common.skc_daddr)] = count();
}'

# 새 프로세스 생성 추적
bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
    printf("%-6d %-16s %s\n", pid, comm, str(args->filename));
}'

히스토그램, 스택 트레이스 집계

# 커널 스택 트레이스 집계 (page fault 원인 분석)
bpftrace -e 'software:page-faults:1 {
    @[kstack, comm] = count();
}'

# 블록 I/O 크기 분포 (로그2 히스토그램)
bpftrace -e 'tracepoint:block:block_rq_issue {
    @size = hist(args->bytes);
}'

# 리니어 히스토그램 (0~100, 스텝 10)
bpftrace -e 'kretprobe:vfs_read {
    @ret = lhist(retval, 0, 10000, 100);
}'

# CPU별 실행 시간 분포
bpftrace -e 'profile:hz:99 {
    @[cpu] = count();
}'

성능 모니터링 스크립트

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

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

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

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

END
{
    clear(@qtime);
}

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

/proc /sys 활용

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

/proc/slabinfo

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

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

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

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

/proc/vmstat

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

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

/proc/buddyinfo

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

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

/sys/kernel/debug/ 주요 항목

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

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

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

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

# softirq 통계
cat /proc/softirqs

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

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

실전 팁

재현 기법

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

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

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

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

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

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

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

git bisect 활용

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

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

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

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

# bisect 결과 확인
git bisect log

# bisect 종료
git bisect reset

시리얼 콘솔 설정

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

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

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

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

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

로그 분석 기법

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

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

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

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

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

QEMU 디버깅 환경

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

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

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

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

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

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

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

crashkernel 심화 설정

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

crashkernel 메모리 크기 결정

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

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

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

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

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

crashkernel 설정 시 주의사항:

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

kdump 설정 심화

# RHEL/CentOS: /etc/kdump.conf 주요 설정
path /var/crash                    # 덤프 저장 경로
core_collector makedumpfile -l --message-level 7 -d 31
# -l: lzo 압축 (빠름)
# -d 31: 제로/캐시/유저/프리 페이지 제외
# --message-level 7: 진행 상황 출력

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

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

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

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

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

kdump 운영 시 고려사항

kdump 실패 주요 원인과 대처:

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

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

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

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

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

makedumpfile 심화

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

# makedumpfile 압축 알고리즘 비교
# -l: lzo 압축 (기본 권장, 빠른 압축/해제)
# -c: zlib 압축 (높은 압축률, 느림)
# -p: snappy 압축 (가장 빠름, 낮은 압축률)
# --zstd: zstd 압축 (makedumpfile 1.7.0+, zlib급 압축률 + lzo급 속도)

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

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

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

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

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

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

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

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

kdump 네트워크 덤프 심화

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

### NFS 덤프 설정 ###
# 서버 측: NFS export 설정
# /etc/exports:
/export/crash  10.0.0.0/8(rw,sync,no_root_squash)
exportfs -ra

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

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

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

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

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

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

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

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

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

가상 환경에서의 kdump

### KVM/QEMU 게스트에서 kdump ###
# 게스트 내부에서 일반적인 kdump 설정과 동일
# 추가 고려사항:

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

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

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

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

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

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

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

kdump 트러블슈팅

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

### kdump 실패 원인 진단 ###

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

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

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

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

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

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

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

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

# Debian/Ubuntu:
kdump-config rebuild

kdump 트러블슈팅 핵심 원칙:

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

kdump 운영 자동화

# vmcore 자동 로테이션 (cron)
# /etc/cron.daily/kdump-rotate:
#!/bin/bash
CRASH_DIR="/var/crash"
MAX_DUMPS=5
MAX_AGE_DAYS=30

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

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

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

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

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

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

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

VMCOREINFO 메커니즘

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

/* VMCOREINFO 생성: kernel/crash_core.c */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

vmcore ELF 형식 내부 구조

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

/* vmcore ELF 구조 개요 */

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

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

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

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

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

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

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

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

kexec 빠른 재부팅

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

### kexec 빠른 재부팅 기본 사용법 ###

# 1. 새 커널 이미지 로드 (일반 kexec, -p가 아님)
kexec -l /boot/vmlinuz-6.1.0 \
    --initrd=/boot/initrd.img-6.1.0 \
    --append="$(cat /proc/cmdline)"
# --append: 현재 부팅 파라미터 재사용
# -l: 일반 로드 (재부팅용), -p: 패닉 로드 (kdump용)

# 2. 즉시 재부팅 (kexec로 전환)
systemctl kexec
# 또는 직접:
kexec -e
# 또는:
reboot   # systemd가 kexec 이미지 로드 시 자동으로 kexec 사용

### kexec_file_load (-s 옵션) 사용 ###
kexec -l -s /boot/vmlinuz-6.1.0 \
    --initrd=/boot/initrd.img-6.1.0 \
    --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 재빌드 후 재부팅
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot

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

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

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

fadump (Firmware-Assisted Dump)

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

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

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

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

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

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

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

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

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

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

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

kexec 보안 고려사항

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

/* kexec 보안 검사 흐름 (kernel/kexec.c, security/security.c) */

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

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

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

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

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

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

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

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

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

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

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

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

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

crash_hotplug (동적 핫플러그 지원)

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

/* kernel/crash_core.c - crash_hotplug 핵심 구조 (6.5+) */

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

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

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

    mutex_lock(&kexec_mutex);

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

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

    mutex_unlock(&kexec_mutex);
}
# crash_hotplug 지원 여부 확인
grep CONFIG_CRASH_HOTPLUG /boot/config-$(uname -r)
# CONFIG_CRASH_HOTPLUG=y

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

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

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

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

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

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

Call Trace 심화 분석

커널 Call Trace(백트레이스)는 문제 발생 시점의 함수 호출 스택을 보여줍니다. 정확한 분석을 위해 다양한 유형의 Call Trace를 이해해야 합니다.

Call Trace 유형별 해석

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 패턴별 원인 분석

# 패턴 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() 미호출, 인터럽트 비활성화

Softlockup / Hardlockup 심화

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

Softlockup vs Hardlockup

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

Watchdog 내부 메커니즘

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

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

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

    return 0;
}

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

Lockup 관련 커널 파라미터

# watchdog 타임아웃 (초)
sysctl kernel.watchdog_thresh=10    # soft: 20초, hard: 10초

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

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

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

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

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

Softlockup 메시지 상세 해부

# 실제 softlockup 메시지를 한 줄씩 해석합니다

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

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

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

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

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

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

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

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

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

Tainted 플래그 전체 목록

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

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

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

Softlockup 원인별 분석

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

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

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

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

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

Hardlockup 메시지 상세 해부

# 실제 hardlockup 메시지

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

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

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

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

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

Hardlockup 주요 원인과 진단

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

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

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

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

Softlockup 디버깅 CONFIG 옵션

CONFIG_PROVE_LOCKING=y     # lockdep: 데드락 탐지
CONFIG_DEBUG_LOCK_ALLOC=y  # 락 할당 추적
CONFIG_LOCK_STAT=y         # 락 경합 통계
CONFIG_DEBUG_SPINLOCK=y    # 스핀락 디버깅

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

Hung Task 감지기

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

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

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

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

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

Hung Task 메시지 상세 해석

# 실제 hung_task 경고 메시지

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

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

Hung Task 원인별 진단

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

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

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

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

Lockup 종합 디버깅 순서

/* 시스템 행(hang) 발생 시 디버깅 순서 */

1. dmesg/시리얼 콘솔에서 메시지 유형 확인
   ├── "soft lockup"     → Softlockup (스케줄링 불가)
   ├── "hard LOCKUP"     → Hardlockup (인터럽트 불가)
   ├── "blocked for more than" → Hung task (I/O/lock 대기)
   ├── "rcu_preempt detected stalls" → RCU stall (QS 미보고)
   └── 아무 메시지 없음   → 완전 freeze (NMI 수동 트리거)

2. EFLAGS 확인 (softlockup/hardlockup 구분)
   ├── IF=1 (0x200 비트 설정)  → 인터럽트 활성 → softlockup
   └── IF=0 (0x200 비트 없음)  → 인터럽트 비활성 → hardlockup

3. Call Trace 컨텍스트 마커 확인
   ├── <TASK> 만 있음   → 프로세스 컨텍스트 문제
   ├── <IRQ> 포함       → 인터럽트 핸들러 문제
   ├── <SOFTIRQ> 포함   → softirq 처리 문제
   └── <NMI> 포함       → NMI가 덤프한 시점 정보

4. RIP 함수 분석
   ├── _raw_spin_lock*   → spinlock 대기 → lock holder 추적
   ├── mutex_lock*       → mutex 대기 → /proc/lock_stat 확인
   ├── io_schedule*      → I/O 대기 → 스토리지 장애 확인
   ├── native_halt       → CPU idle → kthread starvation
   └── 특정 모듈 함수    → 해당 모듈 코드 분석

5. 다른 CPU 상태 확인
   $ echo l > /proc/sysrq-trigger  # 모든 CPU backtrace
   $ echo t > /proc/sysrq-trigger  # 모든 태스크 스택
   $ echo w > /proc/sysrq-trigger  # D 상태 태스크만

6. vmcore 수집 (패닉 유발 + kdump)
   $ echo c > /proc/sysrq-trigger  # 강제 패닉 → kdump
   # 또는 ipmitool chassis power diag (원격 NMI)
   # crash 도구로 vmcore 분석

커널 패닉 심화

panic() 함수 내부 동작

/* kernel/panic.c: panic() 실행 순서 */
void panic(const char *fmt, ...)
{
    /* 1. 다른 CPU 정지 (smp_send_stop) */
    /* 2. panic_notifier_list 콜백 호출 */
    /* 3. kmsg_dump() - 로그를 pstore 등에 저장 */
    /* 4. __crash_kexec() - kdump 캡처 커널으로 전환 */
    /* 5. kdump 미설정 시: panic_blink + 무한 루프 */
    /* 6. panic_timeout > 0이면 타임아웃 후 재부팅 */
}

/* 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=N0패닉 후 N초 뒤 재부팅. 0=재부팅 안 함. -1=즉시 재부팅
panic_on_oops=N01이면 Oops 시 즉시 패닉 (kdump 유발)
panic_on_warn=N01이면 WARN 시 패닉 (디버깅 전용)
panic_on_rcu_stall=N01이면 RCU stall 시 패닉
panic_on_io_nmi=N01이면 I/O NMI 시 패닉
panic_on_unrecovered_nmi=N01이면 처리 불가 NMI 시 패닉
panic_print=N0패닉 시 추가 정보 출력 (비트마스크)
softlockup_panic=N01이면 softlockup 시 패닉
hardlockup_panic=N01이면 hardlockup 시 패닉
hung_task_panic=N01이면 hung_task 시 패닉
unknown_nmi_panic=N01이면 알 수 없는 NMI 시 패닉 (IPMI 디버깅)
oops_limit=N10000이 횟수 초과 시 패닉 (커널 6.2+)
warn_limit=N0이 횟수 초과 시 패닉 (0=무제한, 커널 6.2+)
# panic_print 비트마스크 값
# Bit 0 (1):  모든 태스크 정보 출력
# Bit 1 (2):  시스템 메모리 정보 출력
# Bit 2 (4):  타이머 정보 출력
# Bit 3 (8):  락/의존성 정보 출력 (lockdep)
# Bit 4 (16): ftrace 버퍼 출력
# Bit 5 (32): 모든 printk 메시지 버퍼 출력
# 예: panic_print=63 → 모든 정보 출력

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

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

WARN_ON / BUG_ON 심화 분석

WARN 메시지 분석 패턴

# WARN_ON 출력 예시
------------[ cut here ]------------
WARNING: CPU: 1 PID: 3456 at net/core/skbuff.c:456 skb_under_panic+0x15/0x20
Modules linked in: my_module(OE) ...
CPU: 1 PID: 3456 Comm: my_app Tainted: G        W    OE 6.1.0
RIP: 0010:skb_under_panic+0x15/0x20
Call Trace:
 skb_push+0x45/0x50
 my_tx_handler+0x89/0x120 [my_module]
 dev_hard_start_xmit+0xd4/0x1f0
 ...
---[ end trace 0000000000000000 ]---

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

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

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

WARN/BUG 디버깅 전략

/* 올바른 WARN/BUG 사용 가이드라인 */

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

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

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

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

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

pstore / ramoops

패닉 시 콘솔 출력이나 시리얼 로그를 놓칠 수 있습니다. pstore는 패닉 직전의 커널 로그를 비휘발성 저장소(RAM, EFI 변수, NVRAM 등)에 보존합니다.

# 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.ecc=1                      # ECC 활성화

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

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

# pstore 내용 읽기
cat /sys/fs/pstore/dmesg-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();

커널 Livepatch

Livepatch(커널 라이브 패칭)는 시스템 재부팅 없이 실행 중인 커널의 함수를 동적으로 교체하는 기술입니다. 보안 취약점 긴급 수정, 고가용성 서버 운영, 장기 실행 워크로드 보호 등에 활용됩니다. 리눅스 커널 4.0부터 CONFIG_LIVEPATCH로 공식 지원합니다.

Livepatch 아키텍처

Livepatch는 ftrace의 FTRACE_OPS_FL_IPMODIFY 기능을 기반으로 동작합니다. 패치 대상 함수의 진입점에 ftrace 훅을 설치하고, 함수 호출 시 instruction pointer(IP)를 새 함수로 리다이렉트합니다.

원본 함수 old_func() ftrace trampoline klp_ftrace_handler() 패치 함수 new_func() NOP → call IP 변경 Consistency Model (전환 과정) Task A [old universe] Task B [in transition] Task C [new universe] All Tasks [patched] patched_func에 스택 프레임 있음 안전 지점 대기 (voluntary preempt) 전환 완료 new_func 사용 모든 태스크가 new universe klp_transition_patch() → 각 태스크가 안전 지점 도달 시 universe 전환
Consistency Model: Livepatch는 per-task consistency 모델을 사용합니다. 각 태스크는 "old universe"(원본 함수) 또는 "new universe"(패치 함수) 중 하나에 속하며, 태스크가 패치 대상 함수의 스택 프레임을 벗어나는 안전한 지점(voluntary preemption, syscall 경계 등)에 도달했을 때 전환됩니다.

ftrace 기반 명령어 수준 동작

Livepatch의 핵심은 ftrace의 동적 명령어 패칭입니다. gcc -pg 또는 -mfentry 옵션으로 컴파일된 커널 함수는 진입점에 call __fentry__ 명령어가 삽입됩니다. 부팅 시 이 명령어들은 NOP로 교체되어 오버헤드가 제거되고, ftrace 활성화 시 다시 call ftrace_caller로 복원됩니다.

; === x86_64에서의 함수 진입점 변환 과정 ===

; 1. 컴파일 직후 (gcc -mfentry)
original_func:
    call __fentry__          ; 5바이트: E8 xx xx xx xx
    push rbp
    mov  rbp, rsp
    ...

; 2. 부팅 시 ftrace 초기화 (ftrace_init)
original_func:
    nop DWORD ptr [rax+rax]  ; 5바이트 NOP: 0F 1F 44 00 00
    push rbp
    mov  rbp, rsp
    ...

; 3. Livepatch 활성화 시 (ftrace_modify_code → text_poke_bp)
original_func:
    call ftrace_caller       ; 5바이트: E8 xx xx xx xx
    push rbp                 ; ← ftrace_caller가 여기로 리턴
    mov  rbp, rsp
    ...

; ftrace_caller 내부에서 klp_ftrace_handler() 호출
; → handler가 regs->ip를 new_func 주소로 변경
; → ftrace_caller가 RET 대신 변경된 IP로 점프
; → new_func()이 실행됨 (원본 함수의 본문은 실행되지 않음)
/* klp_ftrace_handler (kernel/livepatch/patch.c) 핵심 로직 */
static void klp_ftrace_handler(unsigned long ip,
                               unsigned long parent_ip,
                               struct ftrace_ops *fops,
                               struct ftrace_regs *fregs)
{
    struct klp_ops *ops;
    struct klp_func *func;
    int patch_state;

    ops = container_of(fops, struct klp_ops, fops);

    /*
     * per-task consistency: 현재 태스크의 universe 확인
     * patch_state가 KLP_TRANSITION_PATCHED이면 new_func 사용
     * 아니면 old_func(nop) 사용 → 원본 함수 그대로 실행
     */
    patch_state = klp_get_patch_state(current);

    if (patch_state == KLP_UNDEFINED) {
        /* 전환 중이 아니면 최신 패치 함수 사용 */
        func = list_last_entry(&ops->func_stack,
                               struct klp_func, stack_node);
    } else {
        /* 전환 중: 태스크의 universe에 맞는 함수 선택 */
        func = list_first_or_last_entry(
            &ops->func_stack, struct klp_func, stack_node,
            patch_state == klp_target_state);
    }

    if (func->nop)
        return;  /* NOP 패치: 원본 함수 실행 */

    /* IP를 새 함수 주소로 변경 → 리턴 시 new_func으로 점프 */
    klp_arch_set_pc(fregs, (unsigned long)func->new_func);
}
1. 컴파일 직후 func: call __fentry__ ; E8 xx xx xx xx push rbp mov rbp, rsp boot 2. 부팅 (NOP 교체) func: nop DWORD [..] ; 0F 1F 44 00 00 push rbp mov rbp, rsp klp 3. Livepatch 활성 func: call ftrace_caller ; E8 xx xx xx xx push rbp mov rbp, rsp Livepatch 호출 흐름 (Phase 3 상세) caller() call original_func original_func call ftrace_caller klp_ftrace_handler regs->ip = new_func new_func() 패치된 로직 실행 원본 함수의 push rbp 이후 코드는 실행되지 않음 new_func()이 caller()에게 직접 리턴 text_poke_bp(): INT3 삽입 → IPI 동기화 → 최종 명령어 교체 (원자적)
text_poke_bp(): x86에서 코드 수정은 text_poke_bp()를 통해 수행됩니다. 이 함수는 먼저 대상 위치의 첫 바이트를 INT3(0xCC, breakpoint)로 교체하고, 모든 CPU에 IPI(Inter-Processor Interrupt)를 보내 instruction cache를 동기화합니다. 그 후 나머지 바이트를 수정하고, 마지막으로 첫 바이트를 최종 값으로 교체합니다. 이 과정은 원자적이어서 다른 CPU가 반쯤 수정된 명령어를 실행하는 것을 방지합니다. INT3을 실행한 CPU는 bp_patching_in_progress를 확인하고 올바른 명령어로 에뮬레이션합니다.

커널 CONFIG 옵션

# 필수 옵션
CONFIG_LIVEPATCH=y          # Livepatch 프레임워크 활성화
CONFIG_FTRACE=y              # ftrace 기반 (필수 의존성)
CONFIG_DYNAMIC_FTRACE=y      # 동적 ftrace (필수 의존성)
CONFIG_MODULES=y             # 모듈 지원 (패치를 모듈로 로드)
CONFIG_KALLSYMS_ALL=y        # 전체 심볼 해석 (권장)

# 스택 검증 관련 (안전한 전환에 필수)
CONFIG_HAVE_RELIABLE_STACKTRACE=y  # 신뢰성 있는 스택 트레이스
CONFIG_STACKTRACE=y                # 스택 트레이스 지원
CONFIG_FRAME_POINTER=y             # 또는 ORC unwinder (x86_64)
CONFIG_UNWINDER_ORC=y              # ORC unwinder (더 정확하고 성능 좋음)

# 아키텍처 지원 확인
# x86_64, s390, ppc64le에서 완전 지원
# arm64는 커널 6.x부터 지원

Livepatch API 핵심 구조체

#include <linux/livepatch.h>

/**
 * klp_func - 패치할 개별 함수 정의
 * @old_name:  교체할 원본 함수의 심볼 이름
 * @new_func:  대체할 새 함수의 포인터
 * @old_sympos: 동명 함수가 여러 개일 때 위치 (0=유일, 1=첫 번째, ...)
 * @nop:       true이면 NOP 패치 (atomic replace에서 활용)
 */
struct klp_func {
    const char *old_name;
    void *new_func;
    unsigned long old_sympos;
    bool nop;

    /* 내부 필드 (커널이 관리) */
    unsigned long old_func;        /* 런타임에 해석됨 */
    struct kobject kobj;
    struct list_head node;
    struct list_head stack_node;
    unsigned long old_size, new_size;
    bool patched;
    bool transition;
};

/**
 * klp_object - 패치 대상 오브젝트 (vmlinux 또는 모듈)
 * @name:   대상 모듈 이름 (NULL이면 vmlinux)
 * @funcs:  이 오브젝트에서 패치할 함수 배열 (sentinel 종료)
 */
struct klp_object {
    const char *name;
    struct klp_func *funcs;

    /* 내부 필드 */
    struct kobject kobj;
    struct list_head func_list;
    struct list_head node;
    struct module *mod;
    bool dynamic;
    bool patched;
};

/**
 * klp_patch - 라이브 패치 최상위 구조체
 * @mod:     패치 모듈 자신
 * @objs:    패치 대상 오브젝트 배열 (sentinel 종료)
 * @replace: true이면 이전 패치들을 모두 대체 (atomic replace)
 */
struct klp_patch {
    struct module *mod;
    struct klp_object *objs;
    bool replace;

    /* 내부 필드 */
    struct list_head list;
    struct kobject kobj;
    bool enabled;
    bool forced;
    struct work_struct free_work;
    struct completion finish;
};

기본 Livepatch 모듈 작성

/*
 * livepatch-sample.c - 커널 함수를 런타임에 교체하는 예제
 * cmdline_proc_show()를 패치하여 /proc/cmdline 출력을 변경
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/seq_file.h>

/* 새 함수: 원본과 동일한 시그니처 필수 */
static int livepatch_cmdline_proc_show(struct seq_file *m, void *v)
{
    seq_printf(m, "%s [LIVEPATCHED]\n", saved_command_line);
    return 0;
}

/* 패치할 함수 목록 (sentinel으로 빈 항목 필수) */
static struct klp_func funcs[] = {
    {
        .old_name = "cmdline_proc_show",
        .new_func = livepatch_cmdline_proc_show,
    },
    { }  /* sentinel */
};

/* 패치 대상 오브젝트: NULL name = vmlinux */
static struct klp_object objs[] = {
    {
        .name  = NULL,    /* vmlinux (커널 코어) */
        .funcs = funcs,
    },
    { }  /* sentinel */
};

static struct klp_patch patch = {
    .mod  = THIS_MODULE,
    .objs = objs,
};

/* 모듈 로드 시 패치 활성화 */
static int __init livepatch_init(void)
{
    return klp_enable_patch(&patch);
}

/* 모듈 언로드 시 자동 복원 (커널이 처리) */
static void __exit livepatch_exit(void)
{
    /* klp_unpatch는 모듈 제거 시 커널이 자동 호출 */
}

module_init(livepatch_init);
module_exit(livepatch_exit);

MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");  /* 필수: livepatch 모듈 표시 */
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Livepatch sample: cmdline_proc_show");
MODULE_INFO(livepatch, "Y"): 이 선언이 없으면 커널이 해당 모듈을 livepatch 모듈로 인식하지 못하며, klp_enable_patch() 호출이 -EINVAL로 실패합니다. 반드시 포함해야 합니다.

Livepatch 모듈 Makefile

# Makefile for livepatch module
obj-m += livepatch-sample.o

# 커널 빌드 디렉터리
KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

모듈 함수 패치

vmlinux뿐만 아니라 로드된 커널 모듈의 함수도 패치할 수 있습니다. klp_object.name에 대상 모듈 이름을 지정합니다.

/* ext4 모듈의 함수를 패치하는 예제 */
static int patched_ext4_file_write_iter(
    struct kiocb *iocb, struct iov_iter *from)
{
    /* 원본 함수 호출 전 추가 검증 로직 */
    struct inode *inode = file_inode(iocb->ki_filp);

    if (iov_iter_count(from) > (1ULL << 30)) {
        pr_warn_ratelimited("ext4: unusually large write %zu on ino %lu\n",
                            iov_iter_count(from), inode->i_ino);
    }

    /* klp_shadow 또는 직접 심볼 참조로 원본 호출 가능 */
    return ext4_file_write_iter(iocb, from);
}

static struct klp_func ext4_funcs[] = {
    {
        .old_name = "ext4_file_write_iter",
        .new_func = patched_ext4_file_write_iter,
    },
    { }
};

static struct klp_object objs[] = {
    {
        .name  = "ext4",     /* 대상 모듈 이름 */
        .funcs = ext4_funcs,
    },
    { }
};
모듈 로딩 순서: 패치 대상 모듈(ext4)이 아직 로드되지 않은 상태에서도 livepatch 모듈을 로드할 수 있습니다. 대상 모듈이 나중에 로드되면 커널이 자동으로 패치를 적용합니다. 대상 모듈이 언로드되면 패치도 자동으로 비활성화됩니다.

Shadow Variable (klp_shadow)

패치 함수에서 원본 구조체에 없는 추가 데이터를 저장해야 할 때 klp_shadow_* API를 사용합니다. 이는 원본 객체에 "그림자" 데이터를 연결하는 해시 테이블 기반 메커니즘입니다.

#include <linux/livepatch.h>

#define SV_LEAK_TRACKER  1   /* shadow variable ID (패치별 고유) */

struct leak_info {
    u64 alloc_time;
    unsigned long alloc_site;
    size_t size;
};

/* 생성자: shadow variable 할당 시 호출 */
static int leak_info_ctor(void *obj, void *shadow_data, void *ctor_data)
{
    struct leak_info *info = shadow_data;
    info->alloc_time = ktime_get_ns();
    info->alloc_site = (unsigned long)ctor_data;
    info->size = 0;
    return 0;
}

/* 패치 함수에서 shadow variable 사용 */
static void *patched_kmalloc(size_t size, gfp_t flags)
{
    void *ptr = kmalloc(size, flags);
    if (ptr) {
        struct leak_info *info;

        /* shadow variable 생성 및 연결 */
        info = klp_shadow_get_or_alloc(
            ptr,                        /* 연결할 원본 객체 */
            SV_LEAK_TRACKER,             /* shadow variable ID */
            sizeof(*info),               /* shadow data 크기 */
            GFP_KERNEL,
            leak_info_ctor,              /* 생성자 */
            (void *)_RET_IP_              /* 생성자에 전달할 데이터 */
        );
        if (info)
            info->size = size;
    }
    return ptr;
}

static void patched_kfree(const void *ptr)
{
    if (ptr) {
        struct leak_info *info;

        /* shadow variable 조회 */
        info = klp_shadow_get(ptr, SV_LEAK_TRACKER);
        if (info) {
            pr_debug("free: obj=%p size=%zu alive=%llu ns\n",
                     ptr, info->size,
                     ktime_get_ns() - info->alloc_time);

            /* shadow variable 해제 */
            klp_shadow_free(ptr, SV_LEAK_TRACKER, NULL);
        }
    }
    kfree(ptr);
}
API설명
klp_shadow_get(obj, id)기존 shadow variable 조회 (없으면 NULL)
klp_shadow_alloc(obj, id, size, gfp, ctor, ctor_data)새 shadow variable 생성 (이미 존재하면 NULL)
klp_shadow_get_or_alloc(obj, id, size, gfp, ctor, ctor_data)조회 후 없으면 생성
klp_shadow_free(obj, id, dtor)특정 shadow variable 해제
klp_shadow_free_all(id, dtor)특정 ID의 모든 shadow variable 일괄 해제

Atomic Replace (누적 패치 관리)

여러 livepatch가 순차적으로 적용되면 패치 스택이 누적됩니다. replace 플래그를 사용하면 기존 모든 패치를 단일 패치로 원자적으로 대체할 수 있어 패치 관리가 간소화됩니다.

/* Atomic replace 패치: 기존 모든 livepatch를 대체 */
static struct klp_patch cumulative_patch = {
    .mod     = THIS_MODULE,
    .objs    = objs,
    .replace = true,   /* 핵심: 이전 패치 모두 대체 */
};

/*
 * replace=true의 동작:
 *
 * 1. 이 패치가 활성화되면 기존 모든 livepatch가 비활성화됨
 * 2. 이 패치에 포함되지 않은 함수는 원본으로 복원됨
 * 3. 전환은 per-task consistency model을 따름
 *
 * 사용 시나리오:
 * - 보안 패치 v1 적용 후 v2로 업그레이드
 * - 여러 개별 패치를 하나의 누적 패치로 통합
 * - 패치 완전 제거 (빈 패치에 replace=true)
 */
패치 완전 제거: 모든 livepatch를 제거하고 원본 상태로 돌아가려면, 빈 함수 목록에 replace=true를 설정한 패치를 적용하면 됩니다. 이렇게 하면 기존 모든 패치가 원자적으로 비활성화됩니다.

동명 함수 처리 (old_sympos)

커널에는 static 함수 중 이름이 같은 것들이 다른 파일에 존재할 수 있습니다. old_sympos로 어떤 함수를 패치할지 지정합니다.

/*
 * 예: 커널 내에 cleanup()이라는 static 함수가 3개 존재
 *
 * kallsyms에서 확인:
 * $ grep ' cleanup$' /proc/kallsyms
 * ffffffff81234560 t cleanup    [drivers/foo/bar.c]  ← sympos=1
 * ffffffff81234780 t cleanup    [drivers/baz/qux.c]  ← sympos=2
 * ffffffff81234900 t cleanup    [net/core/sock.c]    ← sympos=3
 */
static struct klp_func funcs[] = {
    {
        .old_name   = "cleanup",
        .new_func   = patched_cleanup,
        .old_sympos = 2,   /* 두 번째 cleanup (drivers/baz/qux.c) */
    },
    { }
};
/*
 * old_sympos 값:
 *   0 = 유일한 심볼 (동명이 있으면 에러)
 *   1 = kallsyms 순서로 첫 번째
 *   N = kallsyms 순서로 N번째
 */

sysfs 인터페이스와 운영

# Livepatch 모듈 로드 (패치 자동 활성화)
insmod livepatch-sample.ko

# sysfs에서 패치 상태 확인
ls /sys/kernel/livepatch/
# livepatch_sample/

# 패치 활성화 상태
cat /sys/kernel/livepatch/livepatch_sample/enabled
# 1

# 전환(transition) 진행 중인지 확인
cat /sys/kernel/livepatch/livepatch_sample/transition
# 0 (완료) / 1 (진행 중)

# 강제 전환 (전환이 멈춘 경우 — 주의: 일관성 보장 안 됨)
echo 1 > /sys/kernel/livepatch/livepatch_sample/force

# 패치 비활성화 (원본 함수로 복원 전환 시작)
echo 0 > /sys/kernel/livepatch/livepatch_sample/enabled

# 비활성화 후 모듈 제거
rmmod livepatch-sample

# 개별 함수 패치 상태 확인
cat /sys/kernel/livepatch/livepatch_sample/vmlinux/cmdline_proc_show/transition
# 전환 상태 모니터링: 아직 old universe에 있는 태스크 확인
for pid in /proc/[0-9]*; do
    patch_state=$(cat "$pid/patch_state" 2>/dev/null)
    if [ "$patch_state" = "-1" ]; then
        comm=$(cat "$pid/comm" 2>/dev/null)
        echo "Blocking: PID=$(basename $pid) COMM=$comm"
    fi
done

# /proc/<pid>/patch_state 값:
#  -1 = old universe (전환 대기 중)
#   0 = undefined (전환 진행 안 됨)
#   1 = new universe (전환 완료)

전환 메커니즘 상세

Livepatch 전환은 각 태스크가 안전한 지점에 도달할 때까지 대기하는 과정입니다. 안전한 지점이란 패치 대상 함수가 해당 태스크의 스택에 존재하지 않는 상태를 의미합니다.

/*
 * 전환 흐름 (kernel/livepatch/transition.c):
 *
 * klp_enable_patch()
 *   → klp_init_patch()      : 패치 구조체 초기화, sysfs 등록
 *   → klp_try_enable_patch() : ftrace 훅 설치
 *   → klp_start_transition() : 전환 시작
 *       → 모든 태스크를 TIF_PATCH_PENDING 플래그 설정
 *       → klp_target_state = KLP_PATCHED
 *
 * 각 태스크 (스케줄러에서):
 *   schedule()
 *     → __schedule()
 *       → klp_update_patch_state(current)
 *         → klp_try_switch_task(current)
 *           → klp_check_stack()    : reliable stacktrace로 스택 검사
 *             → 패치 대상 함수가 스택에 없으면:
 *               → 태스크를 new universe로 전환
 *               → TIF_PATCH_PENDING 해제
 *             → 스택에 있으면:
 *               → 다음 스케줄링까지 대기
 *
 * 모든 태스크 전환 완료:
 *   klp_complete_transition()
 *     → klp_unpatch_replaced_patches() (replace=true일 때)
 *     → klp_clear_object_relocations()
 *     → 전환 완료
 */
전환 지연 원인: 다음과 같은 태스크는 전환을 지연시킬 수 있습니다:
  • TASK_IDLE 상태 CPU: idle 루프의 스택이 간단하여 보통 빠르게 전환
  • 장기 sleep (TASK_UNINTERRUPTIBLE): 패치 대상 함수 내부에서 I/O 대기 중일 때
  • RT 태스크: 선점이 억제된 상태에서 패치 대상 함수 실행 중
  • kthread: cond_resched()를 드물게 호출하는 커널 스레드
이런 경우 /proc/<pid>/patch_state로 블로킹 태스크를 식별하고, 시그널 전송이나 force 전환을 고려합니다.

Livepatch 콜백 (Pre/Post hooks)

패치 적용 전후에 실행되는 콜백을 등록하여 초기화/정리 작업을 수행할 수 있습니다. 커널 5.1+에서 지원합니다.

/* 콜백: 패치 활성화/비활성화 전후에 실행 */
static int pre_patch_callback(struct klp_object *obj)
{
    pr_info("pre-patch: %s\n", obj->name ? obj->name : "vmlinux");

    /* 패치 적용 전 사전 조건 검증 */
    /* 실패 시 음수 반환하면 패치 적용이 중단됨 */
    return 0;
}

static void post_patch_callback(struct klp_object *obj)
{
    pr_info("post-patch: %s\n", obj->name ? obj->name : "vmlinux");
    /* 패치 적용 완료 후 추가 초기화 */
}

static void pre_unpatch_callback(struct klp_object *obj)
{
    pr_info("pre-unpatch: %s\n", obj->name ? obj->name : "vmlinux");
    /* 패치 제거 전 정리 작업 */
}

static void post_unpatch_callback(struct klp_object *obj)
{
    pr_info("post-unpatch: %s\n", obj->name ? obj->name : "vmlinux");

    /* 모든 shadow variable 정리 */
    klp_shadow_free_all(SV_LEAK_TRACKER, NULL);
}

static struct klp_object objs[] = {
    {
        .name  = NULL,
        .funcs = funcs,
        /* 콜백 등록 */
        .callbacks = {
            .pre_patch   = pre_patch_callback,
            .post_patch  = post_patch_callback,
            .pre_unpatch = pre_unpatch_callback,
            .post_unpatch = post_unpatch_callback,
        },
    },
    { }
};
콜백호출 시점실행 컨텍스트
pre_patch함수 교체 직전전환 시작 전 (실패 시 패치 중단 가능)
post_patch모든 전환 완료 직후모든 태스크가 new universe
pre_unpatch함수 복원 직전역전환 시작 전
post_unpatch모든 역전환 완료 직후모든 태스크가 old universe

안전성 고려사항

Livepatch 적용 시 반드시 확인할 사항:
  • 함수 시그니처 일치: 패치 함수와 원본 함수의 인자, 반환 타입, 호출 규약이 정확히 일치해야 합니다. 불일치 시 스택 손상이나 커널 패닉이 발생합니다.
  • 데이터 구조 호환성: 구조체 레이아웃이 변경된 패치는 적용할 수 없습니다. Livepatch는 함수만 교체하며 데이터 구조는 변경하지 않습니다.
  • 인라인 함수: 컴파일러가 인라인한 함수는 패치할 수 없습니다. noinline 속성이 필요합니다.
  • 컴파일러 최적화: 패치 모듈과 원본 커널은 동일한 컴파일러 버전과 최적화 수준으로 빌드해야 합니다.
  • 시맨틱 변경 범위: 호출자가 기대하는 함수의 의미론(side effect, 에러 코드 등)을 유지해야 합니다.
# Livepatch 적용 전후 검증 스크립트

# 1. 커널 Livepatch 지원 확인
if [ ! -d /sys/kernel/livepatch ]; then
    echo "ERROR: CONFIG_LIVEPATCH not enabled"
    exit 1
fi

# 2. 현재 적용된 패치 목록
echo "=== Active Livepatches ==="
for patch in /sys/kernel/livepatch/*; do
    [ -d "$patch" ] || continue
    name=$(basename "$patch")
    enabled=$(cat "$patch/enabled")
    transition=$(cat "$patch/transition")
    echo "  $name: enabled=$enabled transition=$transition"

    # 패치된 함수 목록
    for obj in "$patch"/*/; do
        [ -d "$obj" ] || continue
        obj_name=$(basename "$obj")
        for func in "$obj"/*/; do
            [ -d "$func" ] || continue
            func_name=$(basename "$func")
            echo "    [$obj_name] $func_name"
        done
    done
done

# 3. tainted 상태 확인 (Livepatch 적용 시 K 플래그 설정)
tainted=$(cat /proc/sys/kernel/tainted)
if [ $((tainted & 32768)) -ne 0 ]; then
    echo "Kernel tainted: TAINT_LIVEPATCH (K)"
fi

# 4. 전환 블로킹 태스크 확인
echo "=== Blocking Tasks ==="
blocking=0
for pid in /proc/[0-9]*; do
    state=$(cat "$pid/patch_state" 2>/dev/null)
    if [ "$state" = "-1" ]; then
        comm=$(cat "$pid/comm" 2>/dev/null)
        wchan=$(cat "$pid/wchan" 2>/dev/null)
        echo "  PID=$(basename $pid) COMM=$comm WCHAN=$wchan"
        blocking=$((blocking + 1))
    fi
done
echo "Total blocking: $blocking"

Livepatch 역사: kGraft vs kpatch

항목kpatch (Red Hat)kGraft (SUSE)Livepatch (통합)
도입2014, RHEL2014, SLES4.0 (2015), mainline
일관성 모델stop_machine (전체 정지)per-task lazyper-task + stack checking
전환 방식모든 CPU 동시 교체태스크별 점진 교체태스크별 + 스택 검증
성능 영향전환 시 짧은 정지전환 중 오버헤드최소 오버헤드
안전성높음 (동시 교체)의미론적 일관성 이슈높음 (스택 검증)
상태유저스페이스 도구 유지mainline에 통합공식 커널 프레임워크

배포판별 Livepatch 운영

# === Ubuntu / Canonical Livepatch ===
# Canonical Livepatch 서비스 활성화
sudo snap install canonical-livepatch
sudo canonical-livepatch enable <TOKEN>

# 상태 확인
canonical-livepatch status --verbose

# === RHEL / Red Hat kpatch ===
# kpatch 유틸리티 설치
sudo yum install kpatch kpatch-patch

# 패치 모듈 목록
kpatch list

# 패치 적용
sudo kpatch load kpatch-module.ko

# 자동 적용 등록 (부팅 시)
sudo kpatch install kpatch-module.ko

# === SUSE / kLP (kernel Live Patching) ===
# SUSE Live Patching 패키지
sudo zypper install kernel-livepatch-tools

# 패치 상태 확인
klp status

# === 공통: sysfs로 직접 확인 ===
# 모든 배포판에서 동일하게 사용 가능
ls /sys/kernel/livepatch/
dmesg | grep livepatch

Livepatch 트러블슈팅

증상원인해결 방법
klp_enable_patch(): -ENODEV대상 함수 심볼을 찾을 수 없음/proc/kallsyms에서 심볼 존재 여부 확인, old_sympos 검토
klp_enable_patch(): -EINVALMODULE_INFO(livepatch, "Y") 누락모듈 소스에 해당 매크로 추가
전환이 완료되지 않음태스크가 패치 대상 함수 내 sleep/proc/<pid>/patch_state로 블로킹 태스크 식별, 시그널 전송
전환이 영원히 완료되지 않음kthread가 cond_resched() 미호출force 전환 또는 해당 kthread 종료
패치 후 커널 패닉함수 시그니처 불일치 또는 ABI 차이동일 컴파일러/옵션으로 빌드 확인, 시그니처 검증
TAINT_LIVEPATCH (K)정상 동작 (livepatch 적용 시 자동 설정)제거 필요 없음, 정보 표시 목적
모듈 언로드 실패패치 비활성화 전 rmmod 시도먼저 echo 0 > .../enabled로 비활성화 후 제거
# Livepatch 디버깅: 상세 로그 활성화
echo 'module livepatch +p' > /sys/kernel/debug/dynamic_debug/control
echo 'file kernel/livepatch/* +p' > /sys/kernel/debug/dynamic_debug/control

# ftrace로 livepatch 전환 과정 추적
echo 1 > /sys/kernel/debug/tracing/events/livepatch/enable
cat /sys/kernel/debug/tracing/trace_pipe &

# livepatch 모듈 로드하여 이벤트 확인
insmod livepatch-sample.ko

# 출력 예:
# livepatch: enabling patch 'livepatch_sample'
# livepatch: 'livepatch_sample': starting patching transition
# livepatch: 'livepatch_sample': patching complete

Reliable Stacktrace 메커니즘

Livepatch 전환의 핵심은 reliable stacktrace입니다. 태스크의 스택을 정확히 분석하여 패치 대상 함수가 현재 실행 중인지 판단해야 합니다. 신뢰할 수 없는 스택 트레이스로 잘못 판단하면 — 패치 대상 함수가 스택에 있는데도 전환하면 — 데이터 손상이나 커널 패닉이 발생할 수 있습니다.

/*
 * klp_check_stack() — 태스크 스택에 패치 대상 함수가 있는지 검사
 * (kernel/livepatch/transition.c)
 */
static int klp_check_stack(struct task_struct *task,
                           const char **oldname)
{
    static unsigned long entries[MAX_STACK_ENTRIES];
    struct klp_object *obj;
    struct klp_func *func;
    int ret, nr_entries;

    /* reliable stacktrace 수집 — 핵심 호출 */
    ret = stack_trace_save_tsk_reliable(task, entries,
                                        MAX_STACK_ENTRIES);
    if (ret < 0) {
        /*
         * -EINVAL: 신뢰할 수 없는 스택
         *   - 인터럽트/예외 프레임이 포함된 경우
         *   - 어셈블리 코드에서 프레임 포인터 누락
         *   - generated code (eBPF JIT 등)
         * → 이 태스크는 아직 전환 불가, 다음 기회에 재시도
         */
        return -EINVAL;
    }
    nr_entries = ret;

    /* 스택의 각 리턴 주소를 패치 대상 함수와 비교 */
    klp_for_each_object(klp_transition_patch, obj) {
        klp_for_each_func(obj, func) {
            for (int i = 0; i < nr_entries; i++) {
                if (entries[i] >= func->old_func &&
                    entries[i] < func->old_func + func->old_size) {
                    *oldname = func->old_name;
                    return -EADDRINUSE;
                    /* 패치 대상 함수가 스택에 있음 → 전환 불가 */
                }
            }
        }
    }

    return 0;  /* 스택 클린: 안전하게 전환 가능 */
}
Stack Unwinder아키텍처신뢰성설명
ORC unwinderx86_64높음컴파일 시 objtool이 생성한 ORC 메타데이터(.orc_unwind) 사용, frame pointer 불필요
Frame pointer범용중간CONFIG_FRAME_POINTER=y 필수, 인라인 어셈블리에서 깨질 수 있음
DWARF unwinderarm64 등높음.eh_frame 기반, 커널 6.3+에서 arm64 livepatch 지원
/*
 * ORC unwinder vs Frame Pointer unwinder 비교
 *
 * Frame Pointer 방식:
 *   - 모든 함수에서 push rbp; mov rbp, rsp 프롤로그 필요
 *   - rbp 레지스터를 프레임 포인터로 예약 → 레지스터 하나 손실
 *   - 어셈블리 코드나 -fomit-frame-pointer로 깨질 수 있음
 *   - 결과: 일부 스택에서 RELIABLE 판단 불가 → 전환 지연
 *
 * ORC 방식 (x86_64 권장):
 *   - objtool이 컴파일 시 .orc_unwind, .orc_unwind_ip 섹션 생성
 *   - 각 명령어 위치별 SP/FP 오프셋과 레지스터 정보 기록
 *   - 런타임에 IP 기반으로 unwind 규칙 조회 → 정확한 스택 해석
 *   - frame pointer 불필요 → 레지스터 하나 추가 사용 가능
 *   - 인터럽트 프레임, 어셈블리 코드도 정확히 처리
 */

/* ORC entry 구조 (arch/x86/include/asm/orc_types.h) */
struct orc_entry {
    s16 sp_offset;       /* CFA에서 SP까지의 오프셋 */
    s16 bp_offset;       /* CFA에서 BP 저장 위치 오프셋 */
    unsigned sp_reg:4;   /* CFA 계산에 사용할 레지스터 */
    unsigned bp_reg:4;   /* BP 복원에 사용할 레지스터 */
    unsigned type:3;     /* 프레임 타입 (call, regs, signal 등) */
    unsigned signal:1;   /* 시그널/인터럽트 프레임 여부 */
};

/*
 * stack_trace_save_tsk_reliable()가 신뢰할 수 없는 스택을 감지하는 조건:
 *
 * 1. 인터럽트 또는 예외 프레임이 스택에 존재
 *    → 인터럽트 시점의 코드 위치가 불확실할 수 있음
 * 2. ORC 엔트리가 없는 코드 영역 (JIT, 생성된 코드)
 *    → unwind 불가능
 * 3. kretprobe trampoline이 스택에 존재
 *    → 실제 리턴 주소가 가려져 있음
 * 4. 비표준 프레임 (inline assembly 등)
 *    → objtool이 경고하는 코드 패턴
 */
CONFIG_UNWINDER_ORC 권장: x86_64에서 livepatch를 사용한다면 ORC unwinder(CONFIG_UNWINDER_ORC=y)를 사용해야 합니다. Frame pointer 기반 unwinder는 모든 함수에서 push rbp; mov rbp, rsp 프롤로그가 있어야 하지만, 컴파일러 최적화나 어셈블리 코드에서 이를 생략하는 경우가 있어 신뢰할 수 없는 스택을 생성합니다. ORC는 objtool이 컴파일 시 생성한 별도의 메타데이터 테이블을 사용하므로 이런 문제가 없습니다. objtool은 빌드 시 unreliable한 코드 패턴을 경고하여 문제를 조기에 발견할 수 있습니다.

Livepatch와 보안 기능 상호작용

최신 커널의 보안 강화 기능들은 livepatch와 복잡한 상호작용을 가집니다. 프로덕션 환경에서 livepatch를 운영할 때 이러한 보안 기능과의 호환성을 반드시 고려해야 합니다.

보안 기능Livepatch 영향대응
KASLR커널 주소 랜덤화로 심볼 주소가 부팅마다 변경Livepatch는 kallsyms를 통해 런타임에 심볼을 해석하므로 영향 없음
모듈 서명CONFIG_MODULE_SIG_FORCE=y이면 서명 없는 모듈 로드 불가livepatch 모듈도 유효한 서명 필요, scripts/sign-file 도구 사용
Lockdownkernel_lockdown(LOCKDOWN_INTEGRITY)에서 unsigned 모듈 차단Secure Boot 환경에서 MOK(Machine Owner Key)으로 모듈 서명 필수
CFIClang CFI가 간접 호출 대상을 타입 기반으로 검증패치 함수의 타입이 원본과 정확히 일치해야 CFI 검증 통과
W^XCONFIG_STRICT_KERNEL_RWX로 코드 영역 쓰기 금지ftrace는 text_poke로 임시 매핑 생성하여 수정 후 복원
IBTx86 CET(Control-flow Enforcement Technology)의 간접 분기 추적커널 6.2+에서 livepatch가 IBT 호환, ENDBR64 명령어 자동 처리
FineIBTkCFI + IBT 조합으로 간접 호출 시 해시 검증패치 함수의 프로토타입이 원본과 동일해야 해시가 일치
# === Secure Boot 환경에서 livepatch 모듈 서명 ===

# 1. 커널 빌드 시 생성된 키로 서명 (개발 환경)
/usr/src/linux/scripts/sign-file sha256 \
    /usr/src/linux/certs/signing_key.pem \
    /usr/src/linux/certs/signing_key.x509 \
    livepatch-sample.ko

# 2. 서명 확인
modinfo livepatch-sample.ko | grep sig
# sig_id:          PKCS#7
# signer:          Build time autogenerated kernel key
# sig_hashalgo:    sha256

# 3. Secure Boot + MOK 환경 (배포판)
# MOK 키 생성
openssl req -new -x509 -newkey rsa:2048 \
    -keyout MOK.priv -outform DER -out MOK.der \
    -nodes -days 36500 -subj "/CN=Livepatch Signing Key/"

# MOK 등록 (재부팅 시 UEFI에서 승인 필요)
sudo mokutil --import MOK.der

# MOK 키로 모듈 서명
/usr/src/linux/scripts/sign-file sha256 \
    MOK.priv MOK.der livepatch-sample.ko

# 4. 서명 검증
sudo keyctl list %:.builtin_trusted_keys
sudo keyctl list %:.secondary_trusted_keys
Lockdown과 livepatch: kernel_lockdown(LOCKDOWN_INTEGRITY) 모드에서는 서명되지 않은 모듈 로드가 차단되지만, 유효한 키로 서명된 livepatch 모듈은 정상적으로 로드됩니다. LOCKDOWN_CONFIDENTIALITY 모드에서는 추가로 /dev/mem, kprobes 등이 차단되지만 livepatch 자체는 영향받지 않습니다. Secure Boot가 활성화된 환경에서는 반드시 MOK 또는 커널 빌드 키로 서명해야 합니다.

kpatch-build를 이용한 자동 패치 생성

kpatch-build는 소스 코드 diff에서 livepatch 모듈을 자동 생성하는 도구입니다. klp_func/klp_object/klp_patch 구조체를 수동으로 작성하는 대신, 일반적인 커널 패치(unified diff)만 제공하면 livepatch 모듈이 자동으로 만들어집니다.

# kpatch-build 설치 (소스에서)
git clone https://github.com/dynup/kpatch.git
cd kpatch
make
sudo make install

# 의존성 (Fedora/RHEL)
sudo dnf install gcc kernel-devel elfutils elfutils-devel
sudo dnf debuginfo-install kernel

# 의존성 (Ubuntu/Debian)
sudo apt install build-essential linux-source \
    linux-headers-$(uname -r) elfutils libelf-dev dpkg-dev
# === kpatch-build 사용 워크플로우 ===

# 1. 패치 파일 준비 (일반 커널 패치 형식)
cat > fix-null-deref.patch <<'EOF'
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -1234,6 +1234,9 @@ static int ext4_write_begin(...)
     handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE, needed);
+    if (IS_ERR(handle)) {
+        ret = PTR_ERR(handle);
+        goto out;
+    }
     if (ext4_should_dioread_nolock(inode)) {
EOF

# 2. livepatch 모듈 자동 생성
# kpatch-build는 커널을 패치 전/후로 두 번 빌드하고,
# 변경된 함수를 비교하여 livepatch 모듈을 생성합니다.
kpatch-build -t vmlinux fix-null-deref.patch

# 소스 RPM/DEB 사용 시:
kpatch-build --sourcerpm kernel-5.14.0-362.el9.src.rpm fix-null-deref.patch
kpatch-build --sourcedir /usr/src/linux fix-null-deref.patch

# 3. 생성된 모듈 확인
ls livepatch-fix-null-deref.ko
modinfo livepatch-fix-null-deref.ko

# 4. 패치 적용
sudo kpatch load livepatch-fix-null-deref.ko

# 5. 상태 확인
kpatch list
# Loaded patch modules:
#   livepatch_fix_null_deref [enabled]

# 6. 부팅 시 자동 적용 등록
sudo kpatch install livepatch-fix-null-deref.ko
# → /var/lib/kpatch/$(uname -r)/ 에 복사, systemd 서비스 등록

# 7. 패치 제거
sudo kpatch unload livepatch-fix-null-deref
sudo kpatch uninstall livepatch-fix-null-deref
kpatch-build 내부 동작 흐름 커널 소스 + diff 패치 원본 빌드 original.o 패치 빌드 patched.o create-diff-object 변경 함수 추출 livepatch 모듈 .ko 생성 create-diff-object: ELF 섹션별 비교 → 변경된 함수만 추출 → klp_func/klp_object/klp_patch 자동 생성 의존하는 심볼(전역 변수, 호출 함수)은 klp relocation으로 자동 처리
kpatch-build의 장점: 수동으로 livepatch 모듈을 작성할 때는 klp_func 구조체 정의, 심볼 이름 매칭, old_sympos 결정 등이 필요하지만, kpatch-build는 이 과정을 모두 자동화합니다. 특히 create-diff-object 도구가 변경된 함수만 정확히 추출하고, 해당 함수가 참조하는 외부 심볼에 대한 relocation도 자동으로 처리합니다. 커널 보안 팀에서도 CVE 긴급 패치 시 kpatch-build를 활용합니다.
kpatch-build 제약사항:
  • 데이터 구조 변경 불가: 구조체 레이아웃이 변경되는 패치는 생성할 수 없습니다 (livepatch 자체의 제한)
  • 전체 커널 빌드 필요: 패치 전/후로 커널을 두 번 빌드하므로 시간과 디스크 공간이 필요합니다
  • 인라인 함수: 컴파일러가 인라인한 함수의 변경은 호출자를 모두 패치해야 하므로 패치 범위가 커질 수 있습니다
  • 디버그 심볼: 커널 debuginfo 패키지가 반드시 필요합니다 (vmlinux with DEBUG_INFO)

Livepatch 셀프테스트

커널 소스 트리에는 livepatch 프레임워크의 동작을 검증하는 셀프테스트가 포함되어 있습니다. 커스텀 패치 개발 후 이 셀프테스트를 실행하여 프레임워크 호환성을 확인하고, 테스트 모듈의 구조를 참고하여 자체 패치를 작성할 수 있습니다.

# 셀프테스트 파일 구조
# tools/testing/selftests/livepatch/
#   test-livepatch.sh      — 기본 패치 적용/제거/전환
#   test-callbacks.sh      — pre/post 콜백 동작 검증
#   test-shadow-vars.sh    — shadow variable CRUD 테스트
#   test-state.sh          — 패치 상태 전환 테스트
#   test-ftrace.sh         — ftrace 연동 (동시 사용 시나리오)
#   test-sysfs.sh          — sysfs 인터페이스 검증
#   README                 — 테스트 가이드
#
# 테스트 커널 모듈 (lib/livepatch/):
#   test_klp_livepatch.c         — 기본 패치 모듈
#   test_klp_atomic_replace.c    — atomic replace 테스트
#   test_klp_callbacks_busy.c    — 바쁜 상태에서의 콜백
#   test_klp_callbacks_demo.c    — 콜백 데모
#   test_klp_shadow_vars.c       — shadow variable 테스트
#   test_klp_state.c             — 상태 전환 테스트

# 셀프테스트 실행 (root 권한 필요)
cd tools/testing/selftests/livepatch
sudo make run_tests

# 개별 테스트 실행
sudo ./test-livepatch.sh
sudo ./test-callbacks.sh

# 테스트 결과 예시:
# TAP version 13
# 1..6
# ok 1 basic function patching
# ok 2 multiple livepatches
# ok 3 atomic replace
# ok 4 livepatch transition
# ok 5 force transition
# ok 6 livepatch + module load/unload

# 특정 CONFIG 확인 (테스트 전 필수)
for cfg in LIVEPATCH DYNAMIC_FTRACE TEST_LIVEPATCH; do
    grep "CONFIG_${cfg}" /boot/config-$(uname -r)
done
# CONFIG_LIVEPATCH=y
# CONFIG_DYNAMIC_FTRACE=y
# CONFIG_TEST_LIVEPATCH=m
/*
 * test_klp_livepatch.c — 셀프테스트용 기본 패치 모듈 구조
 * (lib/livepatch/test_klp_livepatch.c 참조)
 *
 * /proc/cmdline 출력을 변경하여 패치 적용 여부를 확인하는 테스트
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/seq_file.h>

static int test_klp_cmdline_proc_show(
    struct seq_file *m, void *v)
{
    seq_printf(m, "%s [test_klp_livepatch]\n",
               saved_command_line);
    return 0;
}

static struct klp_func funcs[] = {
    {
        .old_name = "cmdline_proc_show",
        .new_func = test_klp_cmdline_proc_show,
    },
    { }
};

static struct klp_object objs[] = {
    { .funcs = funcs },
    { }
};

static struct klp_patch patch = {
    .mod  = THIS_MODULE,
    .objs = objs,
};

static int test_klp_livepatch_init(void)
{
    return klp_enable_patch(&patch);
}

static void test_klp_livepatch_exit(void) { }

module_init(test_klp_livepatch_init);
module_exit(test_klp_livepatch_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");

/*
 * 셀프테스트 스크립트(test-livepatch.sh)의 검증 흐름:
 *
 * 1. insmod test_klp_livepatch.ko
 * 2. cat /proc/cmdline → "[test_klp_livepatch]" 포함 확인
 * 3. cat /sys/kernel/livepatch/test_klp_livepatch/enabled → "1"
 * 4. echo 0 > /sys/kernel/livepatch/test_klp_livepatch/enabled
 * 5. cat /proc/cmdline → "[test_klp_livepatch]" 미포함 확인
 * 6. rmmod test_klp_livepatch
 * 7. dmesg에서 에러 메시지 없음 확인
 */

CVE 패치 적용 실전 워크플로우

Livepatch의 가장 일반적인 활용은 보안 취약점(CVE) 긴급 수정입니다. 서버 재부팅 없이 취약점을 즉시 패치할 수 있어, 고가용성 환경에서 핵심 운영 도구입니다. 다음은 CVE 발견부터 livepatch 적용까지의 실전 워크플로우입니다.

/*
 * 실전 예시: use-after-free 취약점 livepatch 수정
 *
 * 원본 (취약):
 *   rcu_read_lock();
 *   obj = rcu_dereference(global_ptr);
 *   rcu_read_unlock();
 *   use(obj);  ← rcu_read_unlock() 이후 obj 접근 → UAF!
 *
 * 수정: RCU 보호 구간을 use() 이후로 확장
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/livepatch.h>
#include <linux/rcupdate.h>

/* 패치된 함수: RCU 보호 범위 수정 */
static int patched_vulnerable_handler(struct request *req)
{
    struct shared_obj *obj;
    int ret;

    rcu_read_lock();
    obj = rcu_dereference(global_ptr);
    if (!obj) {
        rcu_read_unlock();
        return -ENOENT;
    }
    /* 수정: use()를 RCU 보호 구간 안에서 수행 */
    ret = use(obj, req);
    rcu_read_unlock();    /* ← 이동: use() 이후로 */
    return ret;
}

static struct klp_func funcs[] = {
    {
        .old_name = "vulnerable_handler",
        .new_func = patched_vulnerable_handler,
    },
    { }
};

static struct klp_object objs[] = {
    {
        .funcs = funcs,  /* name=NULL → vmlinux */
    },
    { }
};

static struct klp_patch patch = {
    .mod     = THIS_MODULE,
    .objs    = objs,
    .replace = true,   /* 기존 패치 대체 (누적 패치 관리) */
};

static int __init cve_fix_init(void)
{
    int ret = klp_enable_patch(&patch);
    if (ret)
        pr_err("failed to enable CVE patch: %d\n", ret);
    else
        pr_info("CVE-2024-XXXX livepatch applied\n");
    return ret;
}

static void __exit cve_fix_exit(void) { }

module_init(cve_fix_init);
module_exit(cve_fix_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");
MODULE_DESCRIPTION("Livepatch for CVE-2024-XXXX: fix UAF in handler");
# === CVE livepatch 운영 워크플로우 ===

# 1. CVE 분석 및 패치 작성
# - 취약점 원인 파악 (커널 소스 분석)
# - 최소한의 변경으로 수정하는 패치 함수 작성
# - 함수 시그니처 동일 여부 확인

# 2. 빌드 및 서명
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
/usr/src/linux/scripts/sign-file sha256 \
    MOK.priv MOK.der cve-2024-xxxx.ko

# 3. 테스트 환경에서 검증
# 3-1. 패치 로드
sudo insmod cve-2024-xxxx.ko
dmesg | tail -5
# cve_2024_xxxx: CVE-2024-XXXX livepatch applied

# 3-2. 전환 완료 대기
while [ "$(cat /sys/kernel/livepatch/cve_2024_xxxx/transition)" = "1" ]; do
    sleep 1
done
echo "Transition complete"

# 3-3. 취약점 재현 불가 확인
# (PoC 실행 → 더 이상 크래시 발생하지 않음)

# 4. 프로덕션 배포
# 4-1. 패치 모듈 배포
scp cve-2024-xxxx.ko admin@prod-server:/opt/livepatches/

# 4-2. 프로덕션 적용
sudo insmod /opt/livepatches/cve-2024-xxxx.ko

# 4-3. 모니터링
# 전환 상태, 블로킹 태스크, dmesg 에러 확인
cat /sys/kernel/livepatch/cve_2024_xxxx/enabled
cat /sys/kernel/livepatch/cve_2024_xxxx/transition

# 5. 장기 계획
# 다음 정기 유지보수 윈도우에서:
# - 수정된 커널 패키지로 업데이트
# - 재부팅하여 livepatch 의존성 제거
# - livepatch는 긴급 임시 조치, 재부팅이 최종 해결
CVE livepatch 베스트 프랙티스:
  • replace=true 사용: 누적 패치 방식으로 패치 스택 관리를 단순화합니다. 여러 CVE를 하나의 누적 패치로 통합할 수 있습니다.
  • 최소 변경 원칙: 취약점 수정에 필요한 최소한의 로직만 변경합니다. 리팩터링이나 개선은 포함하지 않습니다.
  • 전환 모니터링: 적용 후 /sys/kernel/livepatch/*/transition으로 전환 완료를 확인합니다. 장시간 완료되지 않으면 블로킹 태스크를 식별합니다.
  • 재부팅 계획: Livepatch는 긴급 임시 조치입니다. 정기 유지보수 윈도우에서 수정된 커널로 재부팅하여 완전히 반영합니다.
  • 필수 매크로: MODULE_INFO(livepatch, "Y")MODULE_LICENSE("GPL")은 반드시 포함해야 합니다.

크래시 디버깅 체크리스트

💡

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

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