C 언어 완전 가이드 & 커널 C 관용어
리눅스 커널 코드베이스가 실제로 사용하는 C 관용어를 표준 문법 설명을 넘어 실전 규약으로 정리합니다. GNU 확장(`__attribute__`, `typeof`, statement expression), 타입 안정 매크로(`container_of`, `BUILD_BUG_ON`), 메모리 모델 보조 매크로(`READ_ONCE`, `WRITE_ONCE`, 배리어), 오류 포인터/정리 경로 패턴, sparse 어노테이션과 lockless 코드 주석 규칙까지 유지보수와 리뷰 관점에서 상세히 다룹니다.
- 기본 C 프로그래밍 경험 (포인터, 구조체, 매크로)
- GCC 컴파일러 사용 경험
- Linux 커널 소스 탐색 경험 (선택)
C 언어는 외교 문서의 언어와 같습니다. ISO 표준(C89/C99/C11/C23)은 모든 나라가 합의한 공식 문법이고, GNU C 확장은 "리눅스 왕국"이 추가로 사용하는 방언입니다. 커널은 이 방언을 적극 활용해 성능과 안전성을 극대화합니다 —
__attribute__((section(".init")))처럼 메모리 배치까지 컴파일러에게 지시합니다.
핵심 요약
- 커널은 C11 표준 + GNU 확장(
-std=gnu11)을 사용합니다 — VLA 금지, RTTI 금지.
__attribute__는 GCC/Clang 전용 메타데이터로 30+개 속성이 커널 전체에 쓰입니다.
typeof/__auto_type는 타입 안전한 min()/max() 매크로 구현의 핵심입니다.
- Statement expressions
({ ... })는 container_of 같은 복잡한 매크로를 부작용 없이 작성하게 합니다.
container_of(ptr, type, member)는 멤버 포인터로 상위 구조체를 역추적하는 커널의 핵심 관용어입니다.
likely()/unlikely()는 __builtin_expect로 분기 예측 힌트를 CPU에 전달합니다.
- sparse 어노테이션(
__user/__iomem/__rcu)은 포인터의 "주소 공간"을 정적 분석 도구에 알립니다.
BUILD_BUG_ON(cond)/static_assert는 컴파일 타임 조건 검사로 런타임 오버헤드가 없습니다.
- GNU 인라인 어셈블리(
asm volatile)는 출력/입력 제약 조건(=r, r, m)과 clobber 목록("memory", "cc")으로 레지스터/메모리 사용을 컴파일러에게 정확히 알립니다.
__builtin_add_overflow(a, b, &res)와 커널 래퍼 check_add_overflow()는 정수 오버플로우를 안전하게 처리합니다 — size_add()/array_size()도 같은 계열입니다.
ERR_PTR/IS_ERR/PTR_ERR: 포인터 마지막 페이지(-4095~-1)를 에러 코드로 활용합니다 — NULL 대신 에러 인코딩 포인터 반환이 커널 관용어입니다.
scoped_guard(mutex, &lock) (Linux 6.4+): __attribute__((cleanup)) 기반 자동 락 해제 — 블록 종료 시 자동 mutex_unlock()이 보장됩니다.
module_init(fn): device_initcall 레벨 6에 등록 — initcall 0~7 순서 체계에서 대부분의 드라이버는 레벨 6에 위치합니다.
-std=gnu11)을 사용합니다 — VLA 금지, RTTI 금지.__attribute__는 GCC/Clang 전용 메타데이터로 30+개 속성이 커널 전체에 쓰입니다.typeof/__auto_type는 타입 안전한 min()/max() 매크로 구현의 핵심입니다.({ ... })는 container_of 같은 복잡한 매크로를 부작용 없이 작성하게 합니다.container_of(ptr, type, member)는 멤버 포인터로 상위 구조체를 역추적하는 커널의 핵심 관용어입니다.likely()/unlikely()는 __builtin_expect로 분기 예측 힌트를 CPU에 전달합니다.__user/__iomem/__rcu)은 포인터의 "주소 공간"을 정적 분석 도구에 알립니다.BUILD_BUG_ON(cond)/static_assert는 컴파일 타임 조건 검사로 런타임 오버헤드가 없습니다.asm volatile)는 출력/입력 제약 조건(=r, r, m)과 clobber 목록("memory", "cc")으로 레지스터/메모리 사용을 컴파일러에게 정확히 알립니다.__builtin_add_overflow(a, b, &res)와 커널 래퍼 check_add_overflow()는 정수 오버플로우를 안전하게 처리합니다 — size_add()/array_size()도 같은 계열입니다.ERR_PTR/IS_ERR/PTR_ERR: 포인터 마지막 페이지(-4095~-1)를 에러 코드로 활용합니다 — NULL 대신 에러 인코딩 포인터 반환이 커널 관용어입니다.scoped_guard(mutex, &lock) (Linux 6.4+): __attribute__((cleanup)) 기반 자동 락 해제 — 블록 종료 시 자동 mutex_unlock()이 보장됩니다.module_init(fn): device_initcall 레벨 6에 등록 — initcall 0~7 순서 체계에서 대부분의 드라이버는 레벨 6에 위치합니다.단계별 이해
- C 표준 역사 파악 — C89→C99→C11→C23 변천사와 커널이
gnu11을 선택한 이유를 이해합니다. __attribute__익히기 —noinline,always_inline,packed,aligned,section속성을 커널 소스에서 찾아봅니다.typeof와 statement expressions —include/linux/minmax.h에서min()구현을 분석합니다.container_of분해 —include/linux/container_of.h에서offsetof와 포인터 산술을 추적합니다.- 분기 힌트 실습 —
likely()/unlikely()가 있는 코드를-O2로 컴파일,objdump로 분기 레이아웃을 비교합니다. - sparse 어노테이션 실습 —
make C=2로 커널 빌드 시 sparse가__user오용을 어떻게 잡는지 확인합니다. - 커널 타입 시스템 —
include/linux/types.h에서u8/u32/u64,__be32/__le32를 찾고 엔디안 변환 API와 연결합니다. - 전처리기 패턴 마스터 —
IS_ENABLED(),do { ... } while(0), x-macro를 직접 작성하며 패턴을 내재화합니다. - 인라인 어셈블리 기초 —
asm volatile("" ::: "memory")로barrier()가 어떻게 구현되는지 분석하고, x86cpuid제약 조건 예제를 직접 작성합니다. - 오버플로우 안전 연산 실습 —
__builtin_add_overflow와check_add_overflow()를 사용해 커널 메모리 할당 크기 계산을 안전하게 구현합니다. - 에러 처리 패턴 실습 —
IS_ERR/PTR_ERR/goto cleanup체인을 직접 작성하고,devm_*변환 연습을 합니다. - asm goto 실습 —
net/core/filter.c의static_branch_unlikely와include/linux/jump_label.h에서 JUMP_LABEL 구현을 확인합니다.
개요 — C 언어 철학과 커널의 선택
C 언어는 1972년 Dennis Ritchie가 Unix 운영체제를 재작성하기 위해 만들었습니다. "프로그래머를 신뢰한다"는 철학 아래 하드웨어에 가까운 제어권을 제공하면서도 이식성을 보장합니다. 리눅스 커널은 이 언어의 가장 규모 있는 C 프로젝트 중 하나로, 약 3천만 줄의 C 코드로 이루어져 있습니다.
커널이 C를 고집하는 이유
- 예측 가능한 메모리 레이아웃 — 구조체 패딩을 직접 제어, 하드웨어 레지스터 매핑
- 인라인 어셈블리 통합 —
asm volatile으로 아키텍처 특화 명령 직접 사용 - 컴파일러 최적화 제어 —
__attribute__로 인라인/배치/정렬 결정 - 런타임 없음 — C++ 예외, RTTI, 가상 테이블 등 런타임 오버헤드 없음
C89/C90 — ANSI/ISO 최초 표준화
1989년 ANSI, 1990년 ISO에서 C를 공식 표준화했습니다. K&R C의 모호함을 제거하고 이식성을 보장했습니다.
주요 도입 기능
| 기능 | 설명 | 커널 활용 |
|---|---|---|
void * | 범용 포인터 타입 | kmalloc() 반환 타입 |
| 함수 프로토타입 | 인수 타입 선언 필수화 | 모든 내부 API |
const / volatile | 읽기전용 / 최적화 금지 | HW 레지스터: volatile u32 __iomem *reg |
| 표준 헤더 | <stddef.h>, <limits.h> | offsetof(), NULL 정의 |
열거형 enum | 정수 상수 그룹화 | 오류 코드, 상태 머신 |
/* C89: volatile은 컴파일러 최적화를 막아 실제 읽기/쓰기 보장 */
volatile u32 *reg = (volatile u32 *)MMIO_BASE;
*reg = 0x1; /* 최적화로 제거되지 않음 */
u32 val = *reg; /* 캐시 없이 실제 읽기 */
C99 — inline, stdint.h, restrict
1999년 ISO C99는 C 언어에 현대적 기능을 대거 도입했습니다. 특히 inline, <stdint.h>, restrict, 지정 초기화자(designated initializer)가 커널에 광범위하게 활용됩니다.
주요 도입 기능
| 기능 | 설명 | 커널 활용 |
|---|---|---|
inline | 함수 인라인 힌트 | static inline 헬퍼 함수 |
<stdint.h> | 크기 고정 정수 uint32_t | 커널은 자체 u8/u32 타입 사용 |
_Bool | 논리형 (0/1) | bool: <linux/types.h> 정의 |
| 지정 초기화자 | .field = value | 구조체 / file_operations 초기화 |
// 주석 | 한 줄 주석 | 커널 코드 전반 |
restrict | 포인터 별칭 없음 힌트 | memcpy() 등 최적화 |
| VLA (가변 길이 배열) | 스택 동적 배열 | ⛔ 커널 완전 금지 (스택 오버플로우) |
__func__ | 현재 함수명 문자열 | pr_debug() 내부 |
restrict — 포인터 앨리어싱 힌트
restrict는 포인터가 가리키는 메모리에 다른 포인터로는 접근하지 않는다는 것을 컴파일러에게 약속합니다. 이를 통해 컴파일러는 추가적인 최적화(레지스터 캐싱, 벡터화)를 적용할 수 있습니다.
/* memcpy: src와 dst는 겹치지 않음 → 컴파일러가 벡터화 가능 */
void *memcpy(void * restrict dst,
const void * restrict src, size_t n);
/* restrict 없으면: 컴파일러가 dst와 src 앨리어싱 가정 → 비최적화 */
/* restrict 있으면: 루프마다 src 재읽기 불필요 → 성능 향상 */
복합 리터럴(Compound Literal) — C99 임시 객체
C99 복합 리터럴 (type){ initializer }은 임시 구조체/배열 객체를 표현식 안에서 직접 생성합니다. 함수에 임시 구조체를 전달하거나 초기화에 활용합니다 (자세한 내용은 복합 리터럴 전용 섹션 참조).
/* 복합 리터럴: 함수 호출에 임시 구조체 전달 */
setup_timer(&timer, callback,
(struct timer_args){ .flags = 0, .delay = 100 });
/* __func__: 현재 함수명 문자열 (C99 표준화) */
#define pr_debug_fn(fmt, ...) \
pr_debug("%s: " fmt, __func__, ##__VA_ARGS__)
/* → pr_debug("my_init: starting\n") 형태로 출력 */
/* 지정 초기화자: 순서 무관, 명시적, 나머지는 0 */
static const struct file_operations my_fops = {
.open = my_open,
.read = my_read,
.release = my_release,
/* .write = NULL (명시 안 해도 0으로 초기화) */
};
/* VLA — 커널에서 절대 사용하지 않는다 */
/* int buf[n]; ← CONFIG_VLA 비활성화로 컴파일 오류 */
/* 이유: 스택 검사 불가, 스택 오버플로우 위험 */
코드 설명
- 지정 초기화자C99 지정 초기화자(
.field = value)는 구조체 필드를 이름으로 초기화합니다. 필드 순서와 무관하며, 명시하지 않은 필드는 0/NULL로 초기화됩니다. 커널의file_operations,net_device_ops등 대형 ops 구조체 초기화에 필수적입니다. - VLA 금지가변 길이 배열(Variable Length Array)은 스택에 동적 크기 배열을 할당합니다. 커널은 스택 크기가 제한적(4~8KB)이고 스택 사용량을 컴파일 타임에 검사할 수 없어
CONFIG_VLA=n으로 전면 금지합니다.
C11 — _Atomic, _Generic, _Static_assert
ISO C11(2011)은 멀티스레딩 지원(_Atomic, <threads.h>), 제네릭 선택(_Generic), 컴파일 타임 어서션(_Static_assert)을 도입했습니다.
| 기능 | 설명 | 커널 관련성 |
|---|---|---|
_Atomic | 원자 타입 / 연산 | 커널은 자체 atomic_t 사용 (lock-free) |
_Static_assert | 컴파일 타임 조건 검사 | static_assert() 래퍼로 활용 |
_Generic | 타입 기반 컴파일 타임 선택 | 타입 안전 매크로에 활용 가능 |
_Thread_local | 스레드 로컬 스토리지 | 커널: __percpu 변수 별도 사용 |
_Noreturn | 반환하지 않는 함수 | noreturn / __attribute__((noreturn)) |
| 익명 구조체/공용체 | 이름 없는 내부 struct/union | struct task_struct 내부 여러 곳 |
_Generic — 타입 기반 컴파일 타임 선택
C11 _Generic은 인수 타입에 따라 컴파일 타임에 다른 표현식을 선택합니다. C++의 함수 오버로딩과 유사한 타입 안전 API를 만들 수 있습니다.
/* _Generic으로 타입별 format 문자열 선택 */
#define print_val(x) printf( \
_Generic((x), \
int: "%d\n", \
long: "%ld\n", \
float: "%f\n", \
default: "%p\n"), (x))
/* 커널 응용: u8/u32/u64별 endian 변환 선택 */
#define cpu_to_le(x) _Generic((x), \
u8: (x), \
u16: cpu_to_le16(x), \
u32: cpu_to_le32(x), \
u64: cpu_to_le64(x))
_Atomic vs 커널 atomic_t
| 구분 | C11 _Atomic | 커널 atomic_t |
|---|---|---|
| 표준 | ISO C11 표준 | 리눅스 커널 전용 |
| 선언 | _Atomic int counter; | atomic_t counter = ATOMIC_INIT(0); |
| 연산 | atomic_fetch_add(&counter, 1) | atomic_inc(&counter) |
| 아키텍처 지원 | 컴파일러 의존 | 모든 커널 아키텍처 보장 |
| 커널 채택 여부 | 미채택 (자체 구현 사용) | 전체 커널 사용 |
| refcount 안전 | 없음 | refcount_t (포화 카운터) |
/* C11 _Static_assert: 조건이 거짓이면 컴파일 오류 */
_Static_assert(sizeof(unsigned long) == 8, "64비트 필요");
/* C11 익명 공용체: 멤버를 직접 접근 */
struct my_value {
union {
u32 raw;
struct { u16 lo; u16 hi; }; /* 익명 struct */
};
};
struct my_value v;
v.lo = 0x1234; /* 직접 접근 가능 */
C17/C18 — 수정판과 기능 테스트 매크로
ISO C17(ISO/IEC 9899:2018, 별칭 C18)은 C11의 버그 수정판으로 새 기능 없이 명세 오류를 수정했습니다. 커널 개발에서는 _POSIX_C_SOURCE/_GNU_SOURCE 기능 테스트 매크로가 더 중요합니다.
기능 테스트 매크로
/* _GNU_SOURCE: GNU/Linux 전용 확장 활성화 */
#define _GNU_SOURCE
#include <string.h> /* strchrnul(), memmem() 등 GNU 확장 포함 */
/* 커널 헤더는 자체 __KERNEL__ 가드로 userspace 혼용 방지 */
#ifdef __KERNEL__
/* 커널 전용 코드 */
#else
/* 사용자 공간 코드 */
#endif
C23 — nullptr, constexpr, BitInt, typeof 표준화
ISO C23(ISO/IEC 9899:2024)은 C++의 현대적 기능을 C에 도입했습니다. 커널은 아직 C23을 공식 채택하지 않았지만, GNU C 확장으로 유사 기능을 이미 사용하고 있습니다.
| C23 기능 | 설명 | GNU C 선행 기능 |
|---|---|---|
nullptr | 타입 안전 NULL 포인터 상수 | NULL (기존) |
_BitInt(N) | 임의 비트 폭 정수 | __int128 (부분) |
constexpr | 컴파일 타임 상수 | const + __attribute__((const)) |
typeof(x) | typeof 표준화 | __typeof__(x) (GNU 확장) |
#embed | 이진 파일 포함 | 링커 스크립트 INCBIN |
[[attribute]] | 표준 속성 문법 | __attribute__(()) |
이진 리터럴 0b1010 | 이진수 리터럴 | GCC 이미 지원 |
nullptr vs NULL — 타입 안전 널 포인터
C23의 nullptr은 C++11 nullptr과 동일한 타입 안전 널 포인터 상수입니다. 기존 NULL 매크로는 정수 0 또는 ((void*)0)으로 정의되어 오버로딩/가변 인수에서 문제가 있었습니다.
/* C23: nullptr은 nullptr_t 타입의 상수 */
typedef decltype(nullptr) nullptr_t;
/* NULL 문제점: 정수 0과 포인터 혼동 */
#define NULL ((void *)0) /* 또는 0 */
printf("%p\n", NULL); /* 구현 정의 동작 */
/* nullptr: 모든 포인터 타입으로 암시적 변환 */
int *p = nullptr;
void (*fp)(void) = nullptr;
printf("%p\n", nullptr); /* 정의된 동작 */
/* 커널은 NULL 사용 유지 (gnu11 기준) */
#define NULL ((void *)0)
_BitInt(N) — 임의 비트 폭 정수
C23의 _BitInt(N)은 N비트 정수를 선언합니다. N은 1 이상이어야 하며, 하드웨어 레지스터 매핑, 비트 필드 대체, 프로토콜 구현에 활용합니다.
/* C23: 임의 비트 폭 정수 */
_BitInt(7) flags; /* -64 ~ 63 */
unsigned _BitInt(12) addr; /* 0 ~ 4095 */
/* 레지스터 비트 필드 대체 */
struct hw_reg {
unsigned _BitInt(3) mode;
unsigned _BitInt(5) status;
unsigned _BitInt(24) counter;
}; /* sizeof = 4 (패딩 없음) */
/* 프로토콜 헤더 (12비트 필드) */
struct packet_hdr {
unsigned _BitInt(4) version;
unsigned _BitInt(12) length; /* 0~4095 */
unsigned _BitInt(16) checksum;
};
/* GNU 대안: __int128 (128비트만 지원) */
__int128 big_val = (__int128)1 << 100;
#embed — 이진 파일 포함
C23의 #embed 지시어는 컴파일 타임에 이진 파일을 배열로 포함합니다. 기존에는 링커 스크립트나 외부 도구가 필요했습니다.
/* C23: 이진 파일 직접 포함 */
static const unsigned char firmware[] = {
#embed "firmware.bin"
};
/* 크기 제한 */
static const unsigned char small_data[] = {
#embed "data.bin" limit(1024)
};
/* 커널 기존 방식: 링커 스크립트 + .incbin */
/* .section .rodata
* .incbin "firmware.bin"
*/
[[attribute]] — C23 표준 속성 문법
C23은 C++ 표준 속성 문법 [[attr]]을 도입했습니다. GNU __attribute__((...))보다 이식성이 높습니다.
/* C23 표준 속성 */
[[noreturn]] void panic(const char *msg);
[[deprecated("use new_func()")]] void old_func(void);
[[fallthrough]]; /* switch 의도적 fall-through */
[[maybe_unused]] static int debug_flag = 1;
[[nodiscard]] int get_value(void); /* 반환값 무시 경고 */
/* GNU 속성 (여전히 더 강력) */
__attribute__((noinline, cold)) void error_path(void);
__attribute__((section(".init.text"))) int init_fn(void);
/* 비교 */
/* [[noreturn]] ≈ __attribute__((noreturn)) */
/* [[deprecated]] ≈ __attribute__((deprecated)) */
/* [[fallthrough]] ≈ __attribute__((fallthrough)) */
/* GNU 전용: section, packed, aligned, cold, hot... */
true/false 키워드화 & #elifdef
/* C23: true/false가 키워드 (매크로 아님) */
_Bool b = true; /* true는 1로 정의된 키워드 */
/* 기존 매크로와 호환성 */
#ifdef true
#undef true /* C23에서는 불필요 */
#endif
/* #elifdef / #elifndef (C23) */
#ifdef CONFIG_X86
/* x86 코드 */
#elifdef CONFIG_ARM64
/* ARM64 코드 */
#elifndef CONFIG_NO_ARCH
/* 기타 아키텍처 */
#endif
/* 기존 방식 */
#ifdef CONFIG_X86
#elif defined(CONFIG_ARM64)
#elif !defined(CONFIG_NO_ARCH)
#endif
GNU C — 커널이 -std=gnu11을 선택한 이유
리눅스 커널은 -std=gnu11로 컴파일됩니다. -std=c11(순수 ISO)이 아닌 GNU 확장을 활성화하는 이유는 다음과 같습니다.
| 이유 | 필요한 GNU 확장 | 사용 예 |
|---|---|---|
| 인라인 어셈블리 | asm volatile(...) 확장 문법 | 아키텍처 코드, barrier |
| 타입 안전 매크로 | typeof(x), statement expressions | min(), max(), container_of() |
| 컴파일러 힌트 | __attribute__(()) | noinline, always_inline, packed |
| 분기 예측 | __builtin_expect() | likely(), unlikely() |
| 계산된 goto | &&label, goto *ptr | 해석기 dispatch table |
| 제로 길이 배열 | struct { int data[0]; } | 가변 길이 구조체 끝 표시 |
/* -std=gnu11 vs -std=c11 차이 확인 */
#ifdef __GNUC__
/* GNU C 컴파일러 (GCC 또는 Clang) */
#endif
/* 제로 길이 배열: 가변 크기 구조체 (C99 flexible array와 유사) */
struct sk_buff {
u32 len;
u8 cb[48];
u8 data[0]; /* GNU: 0-length, C99: [] */
};
__attribute__ — GCC/Clang 속성 완전 참조
__attribute__((...))는 GCC와 Clang이 제공하는 메타데이터 시스템입니다. 함수, 변수, 타입에 컴파일러 동작을 지시합니다.
함수 속성 완전 참조
| 속성 | 커널 매크로 | 의미 |
|---|---|---|
noinline | noinline | 인라인 전개 금지 — 디버그/스택 추적용 |
always_inline | __always_inline | 항상 인라인 전개 — 성능 임계 경로 |
noreturn | __noreturn | 반환하지 않는 함수 (panic(), BUG()) |
pure | 직접 사용 | 부작용 없음, 반환값만 있음 (메모이제이션 가능) |
const | 직접 사용 | 인수에만 의존, 전역 미참조 (pure보다 강) |
cold | __cold | 드물게 호출 — 오류 경로, 별도 섹션 배치 |
hot | __hot | 자주 호출 — 핫 경로 최적화 |
flatten | 직접 사용 | 호출하는 모든 함수를 인라인 |
format(printf,m,n) | __printf(m,n) | printf 형식 문자열 검사 |
visibility("hidden") | 직접 사용 | ELF 심볼 가시성 제어 |
weak | __weak | 약한 심볼 — 재정의 가능 |
constructor(priority) | 직접 사용 | main() 이전 실행 |
section("name") | __section(x) | 링커 섹션 지정 (.init.text 등) |
used | __used | 미참조라도 심볼 유지 |
unused | __maybe_unused | 미사용 경고 억제 |
변수/타입 속성
| 속성 | 커널 매크로 | 의미 |
|---|---|---|
aligned(N) | __aligned(N) | N바이트 정렬 (캐시라인, SIMD 요구사항) |
packed | __packed | 패딩 없이 최소 크기 배치 (프로토콜 헤더) |
may_alias | 직접 사용 | 엄격한 앨리어싱 규칙 예외 (__be32 변환) |
cleanup(fn) | __free(fn) | 스코프 이탈 시 자동 해제 (scope guard) → RAII 패턴 상세 |
alloc_size(N) | 직접 사용 | 할당 함수의 크기 인수 지정 — 버그 탐지 |
malloc | 직접 사용 | 메모리 할당 함수 — 별칭 분석 최적화 |
returns_nonnull | 직접 사용 | NULL 반환 안 함 — 호출자 NULL 체크 제거 |
error("msg") | 직접 사용 | 호출 시 컴파일 오류 — deprecated API 완전 차단 |
warning("msg") | 직접 사용 | 호출 시 컴파일 경고 — 레거시 API 사용 알림 |
target("isa") | 직접 사용 | 함수별 ISA 지정 — target("avx2") |
target_clones(...) | 직접 사용 | 다중 ISA 버전 자동 생성 — 멀티버저닝 |
ifunc("resolver") | 직접 사용 | 런타임 함수 선택 — CPU 특화 구현 |
noipa | 직접 사용 | IPA 최적화 완전 금지 — 디버깅/LTO |
no_stack_protector | 직접 사용 | 스택 카나리 비활성화 — 부트/인터럽트 코드 |
no_sanitize_address | __no_sanitize_address | KASAN 검사 제외 |
no_sanitize_undefined | 직접 사용 | UBSAN 검사 제외 |
no_sanitize("thread") | 직접 사용 | TSAN 검사 제외 |
no_kcsan | __no_kcsan | KCSAN 데이터 레이스 검사 제외 |
캐시라인 정렬 & 구조체 패딩 최적화
현대 CPU의 캐시 계층 구조에서 데이터 정렬은 성능에 결정적 영향을 미칩니다. False sharing 방지와 캐시 효율을 위한 정렬 기법을 다룹니다.
/* 캐시라인 정렬: 64바이트 (일반적) */
#define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES)))
/* False sharing 방지: Per-CPU 데이터 */
struct my_percpu_data {
u64 counter ____cacheline_aligned; /* 다른 캐시라인 */
u64 events ____cacheline_aligned;
};
/* 구조체 패딩 최적화: 큰 필드 → 작은 필드 순서 */
struct bad_layout { /* sizeof = 24 */
char a; /* 1 + 7 패딩 */
u64 b; /* 8 */
char c; /* 1 + 7 패딩 */
};
struct good_layout { /* sizeof = 16 */
u64 b; /* 8 */
char a; /* 1 */
char c; /* 1 + 6 패딩 */
};
/* __aligned 최대값: 컴파일러/아키텍처 제한 있음 */
/* x86-64: 64바이트까지 권장 (캐시라인) */
/* 페이지 정렬: __aligned(PAGE_SIZE) */
| 정렬 대상 | 값 | 용도 |
|---|---|---|
SMP_CACHE_BYTES | 64 (일반적) | L1 캐시라인 정렬 |
PAGE_SIZE | 4096 (일반적) | 페이지 경계 정렬 |
__alignof__(type) | 타입 기본 정렬 | 표준 매크로 alignof |
__alignof_max__ | 최대 정렬 | 모든 타입 정렬 중 최대 |
cleanup() — 스코프 가드 (RAII 패턴)
GCC/Clang의 __attribute__((cleanup(fn)))는 변수가 스코프를 이탈할 때 자동으로 fn을 호출합니다. 커널은 이를 __free(fn) 매크로로 래핑합니다.
/* cleanup 헬퍼 함수: 포인터 해제 */
static inline void kfree_cleanup(void *p)
{
kfree(*(void **)p);
}
/* __free(kfree): 스코프 이탈 시 kfree 자동 호출 */
void my_function(void)
{
char *__free(kfree) buf = kmalloc(256, GFP_KERNEL);
if (!buf)
return;
/* ... 작업 ... */
/* 함수 종료 시 자동으로 kfree(buf) 호출 — goto 없이 깔끔 */
}
/* 커널 4.20+ __cleanup() 매크로 실제 선언 */
#define __free(free_fn) __attribute__((__cleanup__(free_fn##_cleanup)))
nonnull + access — GCC 9/10 포인터 안전성
/* nonnull: 인수가 NULL이면 UB → 컴파일러가 경고/최적화 허용 */
void __attribute__((__nonnull__(1, 2)))
copy_data(void *dst, const void *src, size_t len);
/* GCC 10+ access: 포인터 인수가 읽기/쓰기 접근 범위 명시 */
/* access(read_only, ptr_arg, size_arg): size_arg 바이트 읽기 */
int __attribute__((__access__(read_only, 1, 2)))
my_read(const void *buf, size_t len);
/* __section: 링커가 함수를 특정 섹션에 배치 */
static int __init my_driver_init(void) /* __section(".init.text") */
{
return 0;
}
/* __packed: Ethernet 헤더 — 패딩 없이 와이어 포맷 그대로 */
struct __packed ethhdr {
u8 h_dest[6];
u8 h_source[6];
__be16 h_proto;
}; /* sizeof = 14 (패딩 없음) */
/* __cold: 오류 경로 — 캐시 친화적 배치에서 분리 */
static void __cold handle_fatal_error(void) { BUG(); }
typeof — 타입 안전 매크로의 핵심
typeof(x)는 GNU C 확장으로 표현식 x의 타입을 컴파일 타임에 추론합니다. C23에서 표준화되었습니다.
/* min() 매크로 커널 구현 (include/linux/minmax.h) */
#define min(x, y) ({ \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void)(&_x == &_y); /* 타입 일치 검사 */ \
_x < _y ? _x : _y; \
})
/* __auto_type: C++11 auto와 유사 (GCC 4.9+) */
#define swap(a, b) ({ \
__auto_type _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
})
/* typeof 활용: 포인터 제거 */
#define DEREF_TYPE(ptr) typeof(*(ptr))
/* 사용 예 */
int a = 5, b = 3;
int result = min(a, b); /* _x, _y: int 타입 — 부작용 없음 */
/* min(a++, b++) → a++가 두 번 평가되지 않음 */
코드 설명
- typeof(x) _x = (x)
x의 타입을 추론해 임시 변수_x에 복사합니다. 이렇게 하면x가a++처럼 부작용 있는 표현식이어도 한 번만 평가됩니다. - (void)(&_x == &_y)포인터 비교를 통해
x와y의 타입이 호환되는지 컴파일러가 확인합니다. 타입이 다르면 경고가 발생합니다. 결과는 버려집니다((void)).
Statement Expressions — 블록을 값으로
GNU C 확장 ({ ... })는 여러 문(statement)을 하나의 표현식처럼 사용할 수 있게 합니다. 마지막 표현식의 값이 전체 블록의 값이 됩니다.
/* 기본 형태 */
int result = ({
int tmp = compute();
tmp * tmp; /* 이 값이 result에 대입 */
});
/* container_of에서의 사용 (include/linux/container_of.h) */
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
static_assert(__same_type(*(ptr), ((type *)0)->member) \
|| __same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); \
})
/* 커널 list.h에서의 사용 패턴 */
struct list_head *pos;
list_for_each(pos, &head) {
struct my_struct *obj = list_entry(pos, struct my_struct, list);
/* obj->data 접근 */
}
Computed Goto — 디스패치 테이블
GNU C 확장 &&label은 레이블의 주소를 void 포인터로 얻습니다. goto *ptr로 해당 레이블로 점프합니다. 해석기(interpreter)의 바이트코드 디스패치에 활용됩니다.
/* Dispatch table 패턴 — if/switch보다 빠른 디스패치 */
static const void *dispatch_table[] = {
[0] = &&op_add,
[1] = &&op_sub,
[2] = &&op_mul,
[3] = &&op_halt,
};
goto *dispatch_table[opcode]; /* 직접 점프 */
op_add:
result = a + b;
goto *dispatch_table[next_opcode()];
op_sub:
result = a - b;
goto *dispatch_table[next_opcode()];
op_halt:
return result;
커널 사용 예: eBPF JIT 컴파일러, BPF 해석기(kernel/bpf/core.c)에서 computed goto로 각 BPF 명령어를 디스패치합니다.
__builtin_* — 컴파일러 내장 함수
__builtin_* 함수는 컴파일러가 직접 구현하는 내장 함수로, 아키텍처 특화 명령어로 최적화됩니다.
| 내장 함수 | 커널 래퍼 | 설명 |
|---|---|---|
__builtin_expect(e, v) | likely()/unlikely() | 분기 예측 힌트 |
__builtin_clz(x) | __fls(x) | 선행 0 비트 수 (Count Leading Zeros) |
__builtin_ctz(x) | __ffs(x) | 후행 0 비트 수 (Count Trailing Zeros) |
__builtin_popcount(x) | hweight32(x) | 1 비트 수 (Population Count) |
__builtin_unreachable() | unreachable() | 도달 불가 코드 표시 → 최적화 |
__builtin_constant_p(x) | 직접 사용 | 컴파일 타임 상수 여부 확인 |
__builtin_bswap32(x) | swab32(x) | 32비트 바이트 순서 뒤집기 |
__builtin_frame_address(n) | task_pt_regs() | 스택 프레임 주소 |
__builtin_types_compatible_p(T1,T2) | __same_type(a,b) | 두 타입 동일 여부 (컴파일 타임) |
__builtin_choose_expr(e,t,f) | 직접 사용 | 컴파일 타임 삼항 연산 |
__builtin_add_overflow(a,b,res) | check_add_overflow() | 덧셈 오버플로우 감지 (1이면 오버플로우) |
__builtin_sub_overflow(a,b,res) | check_sub_overflow() | 뺄셈 오버플로우 감지 |
__builtin_mul_overflow(a,b,res) | check_mul_overflow() | 곱셈 오버플로우 감지 |
__builtin_cpu_supports("avx2") | 직접 사용 | 런타임 CPU 기능 플래그 확인 |
__builtin_expect_with_probability | 직접 사용 | 확률값(0.0~1.0) 기반 분기 힌트 (GCC 9+) |
__builtin_prefetch(addr, rw, locality) | prefetch() | 캐시 프리페치 힌트 — 0=읽기, 1=쓰기, locality 0~3 |
__builtin_parity(x) | 직접 사용 | 비트 패리티 계산 (1 비트 개수 홀수면 1) |
__builtin_parityl(x) | 직접 사용 | long 버전 패리티 |
__builtin_parityll(x) | 직접 사용 | long long 버전 패리티 |
__builtin_ffs(x) | __ffs()와 유사 | 최하위 1비트 위치 (1부터 시작, 0이면 0) |
__builtin_extract_return_addr(addr) | 직접 사용 | 반환 주소에서 실제 명령 주소 추출 |
__builtin_call_with_static_chain(call, chain) | 직접 사용 | 정적 체인으로 중첩 함수 호출 |
__builtin_alloca(size) | ⛔ 커널 금지 | 스택 동적 할당 — 스택 오버플로우 위험 |
__builtin_object_size(ptr, type) | __builtin_object_size | 객체 크기 추정 — FORTIFY_SOURCE용 |
__builtin_popcountll(x) | hweight64(x) | 64비트 1 비트 수 |
__builtin_ctzl(x) | __ffs() | long 버전 후행 0 비트 수 |
__builtin_clzl(x) | __fls() | long 버전 선행 0 비트 수 |
/* __builtin_constant_p: 상수 인수에 따른 최적화 경로 선택 */
#define test_bit(nr, addr) \
(__builtin_constant_p(nr) ? \
constant_test_bit(nr, addr) : \
variable_test_bit(nr, addr))
/* __builtin_clz: 최상위 비트 위치 계산 */
static inline int fls(unsigned int x)
{
return x ? sizeof(x) * 8 - __builtin_clz(x) : 0;
}
/* __builtin_unreachable: switch default에서 컴파일러 최적화 허용 */
switch (state) {
case STATE_A: ...; break;
case STATE_B: ...; break;
default: unreachable(); /* 도달 불가 — 범위 검사 최적화 */
}
GNU 인라인 어셈블리 — asm volatile 완전 가이드
GNU C는 C 코드 안에 어셈블리 명령을 삽입하는 확장 asm volatile을 제공합니다. 커널은 메모리 배리어, 아키텍처 특화 명령(cpuid, rdtsc), 원자 연산 구현에 광범위하게 활용합니다.
기본 문법
asm [volatile] (
"어셈블리 명령 템플릿"
: 출력 제약 조건 (output operands) /* 선택적 */
: 입력 제약 조건 (input operands) /* 선택적 */
: clobber 목록 /* 선택적 */
);
커널 실전 예제 3가지
/* 1. barrier(): 컴파일러 재배치 방지 — "memory" clobber */
#define barrier() __asm__ volatile("" ::: "memory")
/* template: 빈 문자열 (명령 없음)
* outputs/inputs: 없음
* clobbers: "memory" → 컴파일러가 메모리 읽기/쓰기를 이 지점에서 완료 */
/* 2. x86 cpuid: 출력 4개, 입력 1개, clobber "cc" */
static inline void native_cpuid(u32 *eax, u32 *ebx,
u32 *ecx, u32 *edx)
{
asm volatile("cpuid"
: "=a"(*eax), "=b"(*ebx), /* 출력: eax, ebx */
"=c"(*ecx), "=d"(*edx) /* 출력: ecx, edx */
: "0"(*eax), "2"(*ecx) /* 입력: 출력 0,2와 공유 */
);
}
/* 3. x86 rdtsc: TSC 읽기 (부작용 → volatile 필수) */
static inline u64 rdtsc(void)
{
u32 lo, hi;
asm volatile("rdtsc"
: "=a"(lo), "=d"(hi)); /* EDX:EAX에 결과 */
return ((u64)hi << 32) | lo;
}
asm goto — 점프 레이블과 정적 분기 패치
asm goto는 GNU C 확장으로 인라인 어셈블리에서 C 레이블로 점프할 수 있게 합니다. 리눅스 커널의 JUMP_LABEL 시스템은 이를 활용해 조건 분기의 오버헤드를 사실상 0으로 만듭니다 — 대부분의 경우 NOP 명령 1개이며, 런타임에 JMP 명령으로 패치됩니다.
asm goto 기본 문법
asm goto는 인라인 어셈블리의 4번째 콜론(:) 이후에 C 레이블 목록을 선언합니다. 어셈블리에서 그 레이블로 점프하면 C 실행 흐름이 해당 레이블로 분기합니다.
/* 기본 asm goto 문법 */
asm goto(
"testl %[flag], %[flag]\n\t" /* flag 테스트 */
"jnz %l[label_true]\n\t" /* 비제로면 label_true로 점프 */
: /* 출력 없음 */
: [flag] "r"(my_flag) /* 입력 */
: "cc" /* clobber: 플래그 레지스터 */
: label_true /* 4번째 ':' 이후: 점프 가능 레이블 목록 */
);
/* 여기는 flag == 0 경우 (fall-through) */
return 0;
label_true:
return 1;
/* asm volatile vs asm goto 비교 */
/* asm volatile: 부작용 있는 일반 인라인 어셈블리 */
/* asm goto: C 레이블로 분기 가능 (출력 제약 조건 불가) */
JUMP_LABEL 시스템 — DEFINE_STATIC_KEY
커널 JUMP_LABEL 시스템(include/linux/jump_label.h)은 asm goto를 기반으로 조건 분기를 런타임 패치 가능한 NOP/JMP로 변환합니다.
/* 정적 키 선언 — 기본값 false (NOP 상태) */
static DEFINE_STATIC_KEY_FALSE(net_debug_key);
/* 사용: 대부분의 경우 NOP으로 실행 */
void process_packet(struct sk_buff *skb)
{
if (static_branch_unlikely(&net_debug_key)) {
/* 이 블록은 net_debug_key가 활성화될 때만 진입 */
dump_packet(skb);
}
/* 기본 경로: NOP 5개 → 0사이클 오버헤드 */
}
/* 런타임에 활성화: NOP → JMP 패치 (stop_machine 필요) */
static_branch_enable(&net_debug_key); /* → JMP slow_path */
static_branch_disable(&net_debug_key); /* → NOP ×5 */
/* 기본값 true로 선언 (JMP 상태) */
static DEFINE_STATIC_KEY_TRUE(rcu_expedited_key);
if (static_branch_likely(&rcu_expedited_key)) {
/* 기본으로 실행되는 빠른 경로 */
}
static_branch_likely vs static_branch_unlikely
| API | 기본 상태 | NOP/JMP 배치 | 최적화 대상 |
|---|---|---|---|
static_branch_unlikely(&key) | DEFINE_STATIC_KEY_FALSE | 기본 NOP (fall-through) | 거의 비활성화 경로 (디버그, 통계) |
static_branch_likely(&key) | DEFINE_STATIC_KEY_TRUE | 기본 JMP 후 fall-through가 빠른 경로 | 거의 활성화 경로 (기능 플래그) |
static_branch_enable() | — | NOP → JMP 패치 | 런타임 활성화 |
static_branch_disable() | — | JMP → NOP 패치 | 런타임 비활성화 |
static_branch_inc() | — | 참조 카운트 증가 | 공유 기능 플래그 |
static_branch_dec() | — | 참조 카운트 감소 | 마지막 dec 시 NOP 전환 |
x86 ALTERNATIVE 매크로
ALTERNATIVE는 부팅 시 CPU 기능 플래그를 확인하여 코드를 패치합니다. JUMP_LABEL과 달리 런타임 변경이 아닌 부팅 1회 패치입니다.
/* ALTERNATIVE: 부팅 시 CPU 기능 기반 코드 교체 */
/* arch/x86/include/asm/alternative.h */
/* 기본 코드: 구버전 방식 */
/* AVX2 지원 시: 최적화 버전으로 교체 */
ALTERNATIVE(
"call legacy_memcpy",
"call avx2_memcpy",
X86_FEATURE_AVX2
);
/* ALTERNATIVE_2: 두 단계 패치 */
ALTERNATIVE_2(
"call scalar_fn", /* 기본 */
"call sse4_fn", X86_FEATURE_SSE4_2,
"call avx2_fn", X86_FEATURE_AVX2
);
/* HAVE_ALTERNATIVE: 여러 CPU별 최적 경로를 런타임에 선택 */
/* 동작: apply_alternatives() → 부팅 시 .altinstructions 섹션 순회 */
/* → CPU feature 확인 → text_poke()로 코드 교체 */
성능 임팩트 비교
| 패턴 | 런타임 비용 | 패치 비용 | 적합한 사용처 |
|---|---|---|---|
일반 if (flag) | ~1~15 사이클 (예측 미스 시) | 없음 | 빈번히 변경되는 조건 |
static_branch_unlikely (NOP) | ~0 사이클 (NOP 디코딩) | stop_machine 필요 | 거의 꺼져 있는 디버그/통계 |
static_branch_likely (JMP) | ~1 사이클 (예측 가능 JMP) | stop_machine 필요 | 거의 켜져 있는 기능 플래그 |
ALTERNATIVE | ~0 (패치된 코드 직접 실행) | 부팅 1회만 | CPU 기능 선택 (AVX2, CRC32) |
커널 실제 활용 예
/* net/core/filter.c — eBPF JIT 활성화 여부 */
static DEFINE_STATIC_KEY_FALSE(bpf_jit_enable_key);
/* kernel/sched/core.c — 스케줄러 통계 수집 */
static DEFINE_STATIC_KEY_FALSE(sched_schedstats);
void update_sched_clock(void)
{
if (static_branch_unlikely(&sched_schedstats))
update_sched_statistics(); /* 통계 수집: 거의 꺼져 있음 */
}
/* /proc/sys/kernel/sched_schedstats = 1 → static_branch_enable() */
/* arch/x86/lib/copy_user_64.S — ALTERNATIVE 활용 */
/* ERMS(Enhanced REP MOVSB) 지원 CPU에서 rep movsb 사용 */
ALTERNATIVE "jmp copy_user_generic_string",
"jmp copy_user_enhanced_fast_string", X86_FEATURE_ERMS
include/linux/jump_label.h, arch/x86/include/asm/jump_label.h, arch/x86/include/asm/alternative.h에서 구현을 확인하세요.
오버플로우 안전 정수 연산 — __builtin_add_overflow
C의 정수 오버플로우는 미정의 동작(UB)입니다. 커널은 __builtin_add_overflow 계열과 include/linux/overflow.h의 래퍼로 이를 안전하게 처리합니다. 특히 메모리 할당 크기 계산(kmalloc(n * sizeof(T)))에서 필수적입니다.
__builtin_add_overflow 계열
/* GCC/Clang 내장: 오버플로우 시 true 반환, 결과는 *res에 저장 */
bool ret = __builtin_add_overflow(a, b, &result); /* a + b */
bool ret = __builtin_sub_overflow(a, b, &result); /* a - b */
bool ret = __builtin_mul_overflow(a, b, &result); /* a * b */
/* 타입 추론: a, b, *res 타입은 독립적으로 선택 가능 */
u32 a = 0xFFFFFFFF;
u32 result;
if (__builtin_add_overflow(a, 1U, &result))
handle_overflow(); /* result = 0 (래핑), 오버플로우 감지 */
커널 래퍼: include/linux/overflow.h
/* check_add_overflow: 오버플로우 시 true */
if (check_add_overflow(count, extra, &total))
return -EOVERFLOW;
/* size_add / size_mul: 오버플로우 시 SIZE_MAX 반환 → 할당 실패 유도 */
size_t total = size_add(hdr_size, data_len);
void *buf = kmalloc(total, GFP_KERNEL); /* SIZE_MAX면 실패 */
/* array_size(n, size): n * size 오버플로우 안전 버전 */
void *arr = kmalloc_array(n, sizeof(struct foo), GFP_KERNEL);
/* 내부: array_size(n, sizeof(struct foo)) = size_mul(n, sizeof) */
/* struct_size(ptr, member, n): 가변 구조체 크기 계산 */
struct flex_buf {
u32 count;
u8 data[]; /* flexible array member */
};
size_t sz = struct_size(p, data, n); /* sizeof(*p) + n * sizeof(p->data[0]) */
void *p = kmalloc(sz, GFP_KERNEL);
| 함수 | 동작 | 오버플로우 반환 |
|---|---|---|
check_add_overflow(a,b,res) | a + b → *res | true |
check_sub_overflow(a,b,res) | a - b → *res | true |
check_mul_overflow(a,b,res) | a * b → *res | true |
size_add(a, b) | a + b (size_t) | SIZE_MAX |
size_mul(a, b) | a × b (size_t) | SIZE_MAX |
array_size(n, size) | n × size | SIZE_MAX |
struct_size(p, member, n) | sizeof(*p) + n×sizeof(멤버) | SIZE_MAX |
에러 처리 패턴 — ERR_PTR, IS_ERR, goto cleanup
리눅스 커널은 에러 처리를 위한 독특한 관용어를 사용합니다. ERR_PTR/IS_ERR/PTR_ERR 삼각 관계와 goto cleanup 패턴은 모든 드라이버 코드에서 반복되는 핵심 패턴입니다.
ERR_PTR / IS_ERR / PTR_ERR 삼각 관계
커널은 포인터 반환 함수에서 NULL 대신 에러 코드를 인코딩한 포인터를 반환합니다. 64비트 주소 공간의 마지막 페이지(-4095 ~ -1)는 절대 유효한 포인터가 될 수 없으므로 에러 코드 저장에 활용합니다.
/* ERR_PTR: errno를 포인터로 인코딩 */
struct file *open_file(const char *name)
{
struct file *f = kmalloc(sizeof(*f), GFP_KERNEL);
if (!f)
return ERR_PTR(-ENOMEM); /* NULL 대신 에러 포인터 반환 */
if (do_open(f, name) < 0) {
kfree(f);
return ERR_PTR(-ENOENT);
}
return f; /* 성공: 유효 포인터 반환 */
}
/* 호출자: IS_ERR로 검사 후 PTR_ERR로 코드 추출 */
struct file *f = open_file("/dev/null");
if (IS_ERR(f)) {
int err = PTR_ERR(f); /* -ENOMEM 또는 -ENOENT */
pr_err("open failed: %d\n", err);
return err;
}
/* 여기서는 f가 유효 포인터임이 보장됨 */
use(f);
에러 포인터 함수 6종 비교
| 함수/매크로 | 동작 | 사용 시점 |
|---|---|---|
ERR_PTR(errno) | errno → void* 인코딩 | 에러를 포인터로 반환할 때 |
IS_ERR(ptr) | 포인터가 에러인지 검사 | 반환된 포인터 검사 |
PTR_ERR(ptr) | 에러 포인터 → long errno | IS_ERR() 후 코드 추출 |
ERR_CAST(ptr) | 에러 포인터 타입 변환 (errno 유지) | void* → 구체 타입 변환 시 |
PTR_ERR_OR_ZERO(ptr) | IS_ERR? PTR_ERR(ptr) : 0 | int 반환 함수에서 처리 |
IS_ERR_OR_NULL(ptr) | IS_ERR(ptr) || ptr == NULL | NULL도 에러로 처리할 때 |
goto cleanup 패턴 — 역순 해제 원칙
드라이버 probe() 함수에서 여러 자원을 순서대로 획득하고, 실패 시 역순으로 해제하는 패턴입니다. goto 레이블은 역순 해제 순서를 명확히 표현합니다.
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
/* 1단계: 구조체 할당 */
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM; /* 해제할 것 없음 → 직접 반환 */
/* 2단계: PCI 영역 요청 */
ret = pci_request_regions(pdev, "my_driver");
if (ret)
goto err_free; /* dev 해제 후 리턴 */
/* 3단계: MMIO 매핑 */
dev->regs = pci_iomap(pdev, 0, 0);
if (!dev->regs) {
ret = -EIO;
goto err_regions; /* regions + dev 해제 */
}
/* 4단계: IRQ 요청 */
ret = request_irq(pdev->irq, my_irq, 0, "my", dev);
if (ret)
goto err_unmap; /* unmap + regions + dev 해제 */
pci_set_drvdata(pdev, dev);
return 0; /* 성공 */
/* 역순 해제 — 레이블은 획득 역순 */
err_unmap:
pci_iounmap(pdev, dev->regs);
err_regions:
pci_release_regions(pdev);
err_free:
kfree(dev);
return ret;
}
devm_* — goto 없는 자동 해제
Linux 3.1+의 devm_* 관리 자원 함수는 디바이스 해제 시 자동으로 정리됩니다. probe()에서 goto가 필요 없어집니다.
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
/* devm_kzalloc: 디바이스 해제 시 자동 kfree */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/* devm_pci_iomap: 자동 iounmap */
dev->regs = devm_pci_iomap(&pdev->dev, pdev, 0, 0);
if (!dev->regs)
return -EIO; /* goto 없이 직접 반환 가능 */
/* devm_request_irq: 자동 free_irq */
ret = devm_request_irq(&pdev->dev, pdev->irq,
my_irq, 0, "my", dev);
if (ret)
return ret;
pci_set_drvdata(pdev, dev);
return 0;
/* remove()에서 devm 자원 자동 해제 */
}
에러 처리 안티패턴 체크리스트
| 안티패턴 | 문제 | 올바른 방법 |
|---|---|---|
if (ptr == NULL) ERR_PTR 체크 | IS_ERR 에러 포인터 누락 | if (IS_ERR(ptr)) 사용 |
if (!ptr)로 ERR_PTR 체크 | 에러 포인터는 NULL이 아님 — 역참조 위험 | IS_ERR(ptr) |
| goto 레이블 순서 오류 | 자원 이중 해제 또는 누락 | 획득 역순으로 레이블 배치 |
IS_ERR_OR_NULL 남용 | 설계 문제 숨김 (NULL과 에러 혼동) | API 반환 타입 재설계 |
| 에러 코드 무시 | 상위에 0 반환 → 잘못된 성공 판정 | __must_check, PTR_ERR_OR_ZERO |
ERR_PTR(-err) 부호 오류 | 양수 값 인코딩 → IS_ERR 실패 | 항상 음수 errno 전달 |
C99 복합 리터럴 & Flexible Array Member
C99의 두 가지 강력한 기능 — 복합 리터럴과 Flexible Array Member는 커널에서 임시 구조체 전달과 가변 크기 구조체 패턴에 광범위하게 사용됩니다.
복합 리터럴 (Compound Literal)
(type){ initializer-list } 형태로 임시 객체를 생성합니다. C++의 임시 객체와 유사하지만 자동 수명을 가집니다.
/* 기본 형태: (type){ .field = value, ... } */
struct point origin = (struct point){ .x = 0, .y = 0 };
/* 함수 인수로 임시 구조체 직접 전달 */
setup_irq(irq, &(struct irqaction){
.handler = my_irq_handler,
.flags = IRQF_SHARED,
.name = "my_device",
});
/* 배열 복합 리터럴: 임시 배열 */
memcpy(dst, (u8[]){ 0xAA, 0xBB, 0xCC }, 3);
/* 커널 활용: 임시 skb_shared_info */
const struct ethhdr hdr = (struct ethhdr){
.h_proto = htons(ETH_P_IP),
};
Flexible Array Member (C99 표준)
/* C99 표준: 마지막 필드로 빈 배열 선언 */
struct variable_buf {
u32 count;
u32 flags;
char data[]; /* Flexible Array Member (FAM) */
};
/* 크기: sizeof(struct variable_buf) = 8 (data[] 제외) */
/* 할당: sizeof(struct) + n * sizeof(원소) */
struct variable_buf *buf =
kmalloc(struct_size(buf, data, n), GFP_KERNEL);
buf->count = n;
buf->data[0] = 'A'; /* 정상 접근 */
/* GNU 제로 배열 (C99 FAM 이전 방식) */
struct old_buf {
u32 count;
char data[0]; /* GNU 확장 — C99 FAM으로 대체 권장 */
};
/* 차이: FAM은 sizeof 계산에 포함되지 않음 (C99 보장)
* 제로 배열은 GNU 확장 (이식성 낮음) */
| 구분 | C99 FAM data[] | GNU 제로 배열 data[0] |
|---|---|---|
| 표준 | ISO C99 표준 | GNU 확장 |
| sizeof 포함 | 아니오 (보장) | 아니오 (구현 의존) |
| 포인터 산술 | 허용 | 허용 (GNU) |
| 커널 권장 | ✅ 신규 코드 | ⚠ 레거시 코드 |
Type Punning & 엄격한 앨리어싱
C의 strict aliasing 규칙은 컴파일러가 서로 다른 타입의 포인터가 동일한 메모리를 가리키지 않는다고 가정하는 것을 허용합니다. 이 규칙을 위반하면 미정의 동작(UB)입니다. 커널은 이를 피하기 위한 3가지 안전한 방법을 사용합니다.
Strict Aliasing 규칙
/* 위반 예: float의 비트 패턴을 u32로 읽기 — UB! */
float f = 3.14f;
u32 *ip = (u32 *)&f; /* strict aliasing 위반 */
u32 bits = *ip; /* UB: GCC -O2에서 최적화로 제거될 수 있음 */
안전한 Type Punning 3가지 방법
/* 방법 1: memcpy — 가장 안전, 컴파일러가 최적화 */
float f = 3.14f;
u32 bits;
memcpy(&bits, &f, sizeof(bits)); /* GCC -O2: mov 한 번으로 최적화 */
/* 방법 2: union (C99에서 공식 지원) */
union float_bits {
float f;
u32 bits;
};
union float_bits u = { .f = 3.14f };
u32 bits = u.bits; /* C99 표준에서 허용 */
/* 방법 3: __attribute__((may_alias)) — GNU C 확장 */
typedef u32 __attribute__((__may_alias__)) aliased_u32;
float f = 3.14f;
aliased_u32 *p = (aliased_u32 *)&f; /* OK: may_alias 허용 */
u32 bits = *p;
커널에서의 Type Punning
/* __be32 / __le32: sparse 타입 + may_alias 기반 */
/* typedef __u32 __attribute__((__may_alias__)) __be32; */
/* 엔디안 변환: union 방식 */
static inline __be32 __cpu_to_be32p(const u32 *p)
{
return (__be32)__builtin_bswap32(*p);
}
/* 커널은 -fno-strict-aliasing 으로 컴파일
* → 위반 코드가 오랫동안 있었기 때문에 전면 비활성화
* → 신규 코드는 memcpy 방식 권장 */
| 방법 | 표준 | 성능 | 커널 권장 |
|---|---|---|---|
memcpy | ISO C 완전 준수 | 최적화 후 동등 | ✅ 신규 코드 |
union | C99 허용 | 동등 | ✅ 허용 |
may_alias | GNU 확장 | 동등 | ⚠ 기존 타입(__be32) |
| 캐스트 직접 | UB | 가장 빠름(위험) | ❌ 사용 금지 |
커널 핵심 매크로 — container_of, ARRAY_SIZE, BIT
리눅스 커널 고유의 핵심 매크로들을 분석합니다.
container_of 구현 분석
/* include/linux/container_of.h */
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
static_assert(__same_type(*(ptr), \
((type *)0)->member) || \
__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); \
})
/* 사용 예: list_head에서 상위 구조체 복원 */
struct my_dev {
int id;
struct list_head list;
char name[32];
};
struct list_head *pos;
list_for_each(pos, &dev_list) {
struct my_dev *dev = container_of(pos, struct my_dev, list);
pr_info("device: %s\n", dev->name);
}
ARRAY_SIZE, BIT, GENMASK
/* ARRAY_SIZE: 배열 원소 수 (포인터에는 사용 불가) */
#define ARRAY_SIZE(arr) \
(sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))
/* BIT: 비트 위치 → 마스크 값 */
#define BIT(nr) (1UL << (nr))
/* GENMASK: 연속 비트 마스크 생성 (high:low 포함) */
#define GENMASK(h, l) \
((~0UL - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h))))
/* 예시 */
#define REG_ENABLE BIT(0) /* 0x00000001 */
#define REG_MODE GENMASK(3, 1) /* 0x0000000E */
#define REG_STATUS GENMASK(7, 4) /* 0x000000F0 */
u32 reg = readl(base);
u32 mode = (reg & REG_MODE) >> 1;
/* FIELD_GET/FIELD_PREP (include/linux/bitfield.h) */
u32 mode2 = FIELD_GET(REG_MODE, reg); /* 자동 쉬프트 */
writel(FIELD_PREP(REG_MODE, 3), base); /* 값을 마스크 위치로 */
clamp, swap, round_up, DIV_ROUND_UP
/* clamp(val, lo, hi): lo ≤ val ≤ hi 범위 클램프 */
#define clamp(val, lo, hi) min(max(val, lo), hi)
int volume = clamp(raw_vol, 0, 100); /* 0~100 범위 강제 */
u32 freq = clamp_t(u32, f, 1000, 4000000000U); /* 명시적 타입 */
int val = clamp_val(x, 0, 255); /* val 타입으로 추론 */
/* swap(a, b): typeof로 타입 안전 교환 */
#define swap(a, b) \
do { typeof(a) __t = (a); (a) = (b); (b) = __t; } while (0)
int x = 1, y = 2;
swap(x, y); /* x=2, y=1 */
/* round_up(x, y): x를 y의 배수로 올림 (y는 2의 제곱수) */
#define round_up(x, y) (((x) + (y) - 1) & ~((y) - 1))
size_t aligned = round_up(size, PAGE_SIZE); /* 페이지 단위 올림 */
size_t aligned = round_up(len, 64); /* 캐시라인 단위 올림 */
/* round_down(x, y): x를 y의 배수로 내림 */
#define round_down(x, y) ((x) & ~((y) - 1))
/* DIV_ROUND_UP(n, d): n/d 올림 나눗셈 (페이지 수 계산 등) */
#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
u32 pages = DIV_ROUND_UP(size, PAGE_SIZE); /* 1바이트도 1페이지 */
u32 chunks = DIV_ROUND_UP(total, 64); /* 64바이트 청크 수 */
/* DIV_ROUND_CLOSEST: 가장 가까운 정수 나눗셈 */
#define DIV_ROUND_CLOSEST(x, div) \
(((x) + ((div) / 2)) / (div))
/* ALIGN(x, a): x를 a 단위로 올림 정렬 (power-of-2) */
#define ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1))
#define IS_ALIGNED(x, a) (((x) & ((a) - 1)) == 0)
likely / unlikely — 분기 예측 힌트
likely(x)와 unlikely(x)는 __builtin_expect로 구현된 분기 예측 힌트입니다. CPU에게 "이 조건은 거의 항상 참(참 아님)"임을 알려 분기 예측기를 최적화합니다.
/* include/linux/compiler.h */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* 사용 패턴: 오류 경로는 unlikely */
int ret = do_something();
if (unlikely(ret < 0)) {
/* 오류 처리 — 드물게 발생 */
return ret;
}
/* 정상 경로는 likely */
if (likely(skb->len > 0)) {
process_packet(skb);
}
/* GCC 어셈블리 효과:
* likely: 조건 참일 때 fall-through (직선 경로)
* unlikely: 조건 거짓일 때 fall-through
* → 분기 예측기 미스 감소, instruction cache 효율 향상 */
/* __builtin_expect_with_probability (GCC 9+) */
if (__builtin_expect_with_probability(cond, 1, 0.99)) {
/* 99% 확률로 참 */
}
CPU BTB와 분기 예측의 관계
현대 CPU의 Branch Target Buffer(BTB)는 과거 분기 패턴을 기반으로 다음 분기 결과를 예측합니다. likely()/unlikely()는 GCC가 생성하는 어셈블리 코드의 분기 레이아웃을 바꿔서 BTB 미스를 줄입니다.
/* likely() 효과: GCC -O2 어셈블리 비교 */
/* C 코드 */
if (likely(x > 0)) {
normal_path();
} else {
error_path();
}
/* → GCC 생성 어셈블리 (x86):
* cmp rdi, 0
* jle .L_error ← 거의 안 뛰는 분기 (fall-through가 정상)
* call normal_path ← fall-through: BTB 미스 없음
* ...
* .L_error:
* call error_path ← 드문 경로를 멀리 배치 */
/* unlikely() 없으면: 컴파일러가 임의 배치 → BTB 미스 증가 */
| 힌트 | 어셈블리 배치 | 사용 시점 |
|---|---|---|
likely(x) | 참 경로가 fall-through | 99%+ 참인 조건 (패킷 처리 정상 경로) |
unlikely(x) | 거짓 경로가 fall-through | 오류 처리, 초기화 검사, NULL 체크 |
__builtin_expect_with_probability(x,v,p) | 확률 p로 힌트 | 프로파일링 데이터 기반 정밀 힌트 (GCC 9+) |
barrier, READ_ONCE, WRITE_ONCE — 메모리 순서 제어
컴파일러와 CPU 모두 명령어를 재배치합니다. 커널은 이를 제어하기 위해 메모리 배리어와 접근 프리미티브를 제공합니다.
/* barrier(): 컴파일러 재배치 금지 (CPU는 자유) */
#define barrier() __asm__ volatile("" ::: "memory")
/* READ_ONCE: 컴파일러가 반복 읽기를 최적화하지 못하게 */
#define READ_ONCE(x) (*((volatile __typeof__(x) *)&(x)))
/* WRITE_ONCE: 원자적 쓰기 보장 (torn write 방지) */
#define WRITE_ONCE(x, val) \
(*((volatile __typeof__(x) *)&(x)) = (val))
/* 메모리 배리어: SMP 환경 */
smp_mb(); /* Full memory barrier (read+write) */
smp_rmb(); /* Read memory barrier */
smp_wmb(); /* Write memory barrier */
smp_store_release(&x, val); /* store + release barrier */
smp_load_acquire(&x); /* load + acquire barrier */
/* 실전 패턴: 락 없는 플래그 공유 */
static bool shutdown_requested;
/* 스레드 A (생산자) */
WRITE_ONCE(shutdown_requested, true);
/* 스레드 B (소비자) */
while (!READ_ONCE(shutdown_requested))
do_work();
/* data_race(): KCSAN 데이터 레이스 검출기에서 의도적 레이스 표시 */
int approx = data_race(counter); /* 정확도 불필요한 통계 읽기 */
Acquire/Release 의미론
smp_store_release/smp_load_acquire는 LKMM(Linux Kernel Memory Model)의 핵심 원시 연산으로, C11 memory_order_release/memory_order_acquire에 대응합니다.
/* store-release: 이 저장 이전의 모든 쓰기가 완료된 후 저장 */
smp_store_release(&ready, 1);
/* → 다른 코어가 ready==1을 보면, 앞의 모든 데이터도 보임 */
/* load-acquire: 이 읽기 이후의 모든 읽기는 이 값 이후로 */
int r = smp_load_acquire(&ready);
if (r)
use_data(data); /* ready==1이면 data도 최신 */
/* acquire/release 쌍으로 생산자-소비자 패턴 */
/* 생산자 */
WRITE_ONCE(data, 42);
smp_store_release(&flag, 1); /* data가 먼저 보임을 보장 */
/* 소비자 */
while (!smp_load_acquire(&flag))
cpu_relax();
int val = READ_ONCE(data); /* 42를 보장 */
rcu_dereference() 내부 구현
/* rcu_dereference: RCU read-side에서 포인터 역참조 */
/* 내부 구현 (include/linux/rcupdate.h) */
#define rcu_dereference(p) rcu_dereference_check(p, 0)
#define rcu_dereference_check(p, c) \
rcu_dereference_sparse(p, typeof(p))
/* 본질: volatile 읽기 + memory barrier (Alpha 아키텍처 대응) */
#define __rcu_dereference(p) ({ \
typeof(p) _________p1 = READ_ONCE(p); \
smp_read_barrier_depends(); \
_________p1; \
})
/* 올바른 RCU 패턴 */
rcu_read_lock();
struct foo *p = rcu_dereference(global_foo);
if (p)
do_something(p->data); /* p가 유효한 동안 안전 */
rcu_read_unlock();
/* rcu_assign_pointer(global_foo, new_p): 쓰기 쪽 */
함수 포인터 & 콜백 — 타입 안전 패턴
커널은 함수 포인터를 드라이버 등록, 인터럽트 핸들러, 타이머 콜백, VFS 연산 등 광범위하게 활용합니다. 타입 안전한 선언과 속성 적용이 중요합니다.
함수 포인터 타입 안전 선언
/* 기본 함수 포인터 선언 */
void (*handler)(int); /* 가독성 낮음 */
/* typedef로 타입 정의 — 권장 */
typedef void (*irq_handler_t)(int irq, void *dev_id);
typedef int (*cmp_func_t)(const void *, const void *);
irq_handler_t my_handler = my_irq_handler;
cmp_func_t sort_cmp = strcmp;
/* 구조체 멤버로 함수 포인터 */
struct my_driver {
int (*probe)(struct pci_dev *);
void (*remove)(struct pci_dev *);
int (*suspend)(struct device *, pm_message_t);
};
커널 콜백 패턴
/* 1. 인터럽트 핸들러 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/* ... */
return IRQ_HANDLED;
}
request_irq(irq, my_irq_handler, IRQF_SHARED, "mydev", dev);
/* 2. 타이머 콜백 */
static void my_timer_callback(struct timer_list *t)
{
struct my_dev *dev = from_timer(dev, t, timer);
/* ... */
}
timer_setup(&dev->timer, my_timer_callback, 0);
/* 3. VFS file_operations */
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
속성 적용 — __no_sanitize_*
/* KASAN 검사 제외: 부트 코드, 인터럽트 컨텍스트 */
static void __no_sanitize_address early_init(void)
{
/* KASAN이 아직 초기화되지 않음 */
}
/* KCSAN 검사 제외: 의도적 데이터 레이스 */
static void __no_kcsan update_stats(void)
{
/* 정확도 불필요한 통계 — 레이스 허용 */
stats.counter++; /* KCSAN 경고 억제 */
}
/* UBSAN 검사 제외 */
static void __no_sanitize_undefined legacy_code(void)
{
/* 레거시 오버플로우 코드 */
}
| 속성 | 용도 | 대표 사용처 |
|---|---|---|
__no_sanitize_address | KASAN 제외 | 부트 코드, 인터럽트, KASAN 자체 |
__no_kcsan | KCSAN 제외 | 의도적 레이스, 통계 업데이트 |
__no_sanitize_undefined | UBSAN 제외 | 레거시 코드, 특정 최적화 |
__no_sanitize("thread") | TSAN 제외 | 스레드 새니타이저 우회 |
__attribute__((cleanup)) & Linux 6.4+ RAII 패턴
GCC/Clang의 __attribute__((cleanup(fn)))는 변수가 스코프를 이탈할 때 자동으로 fn을 호출하는 C 언어의 RAII(Resource Acquisition Is Initialization) 구현입니다. Linux 6.4부터 DEFINE_CLASS/scoped_guard 매크로 시스템으로 대폭 확장되었습니다.
__free() / DEFINE_FREE() 매크로 계층
include/linux/cleanup.h는 __attribute__((cleanup)) 위에 사용하기 쉬운 매크로 계층을 제공합니다.
/* __free(fn): 스코프 이탈 시 fn 자동 호출 */
/* include/linux/cleanup.h */
#define __free(free_fn) __attribute__((__cleanup__(free_fn##_p)))
/* DEFINE_FREE: 해제 함수 등록 */
#define DEFINE_FREE(_name, _type, _free) \
static __always_inline void __free_##_name(_type *p) { _free; }
/* 미리 정의된 __free 대상 (include/linux/cleanup.h) */
DEFINE_FREE(kfree, void *, kfree(*p))
DEFINE_FREE(kvfree, void *, kvfree(*p))
DEFINE_FREE(kfree_sensitive, void *, kfree_sensitive(*p))
DEFINE_FREE(fput, struct file *, if (*p) fput(*p))
/* 사용: 어떤 return 경로에서도 자동 kfree */
void my_function(void)
{
char *__free(kfree) buf = kmalloc(256, GFP_KERNEL);
if (!buf)
return; /* buf == NULL이면 kfree(NULL) → 안전 */
if (some_error())
return; /* 자동 kfree(buf) */
use(buf);
} /* 자동 kfree(buf) — goto 없이 깔끔 */
DEFINE_CLASS() — 뮤텍스/스핀락 자동 RAII
DEFINE_CLASS는 락/언락을 자동화하는 클래스를 정의합니다. CLASS(mutex, lock)(&my_mutex)로 선언하면 스코프 이탈 시 자동 언락됩니다.
/* DEFINE_CLASS 정의 형식 */
#define DEFINE_CLASS(_name, _type, _exit, _init, _init_args...) \
typedef _type class_##_name##_t; \
static __always_inline _type \
class_##_name##_constructor(_init_args) { _init; } \
static __always_inline void \
class_##_name##_destructor(_type *p) { _exit; }
/* 미리 정의된 클래스들 (include/linux/mutex.h 등) */
DEFINE_CLASS(mutex, struct mutex *,
mutex_unlock(*p),
({ mutex_lock(_T); _T; }),
struct mutex *_T)
DEFINE_CLASS(spinlock, unsigned long,
spin_unlock_irqrestore(_L, *p),
({ spin_lock_irqsave(_L, _flags); _flags; }),
spinlock_t *_L, unsigned long _flags)
/* 사용: CLASS() 매크로 */
void my_critical_section(void)
{
CLASS(mutex, lock)(&my_mutex); /* mutex_lock() 자동 호출 */
if (error_condition())
return; /* 자동 mutex_unlock() */
do_work();
} /* 자동 mutex_unlock() */
scoped_guard / guard() — 블록 단위 보호
scoped_guard는 명시적 블록을 락으로 보호합니다. guard()는 함수 전체를 보호하는 전통적 패턴입니다.
/* scoped_guard: 특정 블록만 보호 */
void update_shared_data(void)
{
/* 락이 필요 없는 준비 작업 */
prepare();
scoped_guard(mutex, &shared_lock) {
/* 이 블록만 락 보호 */
shared_data.update();
} /* 자동 언락 */
/* 락 없이 후처리 */
cleanup();
}
/* guard(): 함수 전체를 RCU/스핀락으로 보호 */
void rcu_protected_read(void)
{
guard(rcu)(); /* rcu_read_lock() — 함수 종료 시 rcu_read_unlock() */
struct my_obj *obj = rcu_dereference(global_obj);
if (obj)
process(obj);
/* 자동 rcu_read_unlock() */
}
/* scope_guard(): 임의 함수 자동 실행 (C++의 ScopeExit) */
void complex_init(void)
{
enable_device();
bool success = false;
auto cleanup = scope_guard([&] {
if (!success)
disable_device(); /* 실패 시에만 비활성화 */
});
if (setup_phase1() < 0) return;
if (setup_phase2() < 0) return;
success = true; /* 성공 시 cleanup에서 disable 건너뜀 */
}
Linux 6.4+ DEFINE_GUARD 매크로 테이블
| 매크로 | 락 종류 | 획득/해제 | 사용 예 |
|---|---|---|---|
guard(mutex) | 뮤텍스 | mutex_lock / mutex_unlock | 슬립 가능 컨텍스트 |
guard(spinlock) | 스핀락 | spin_lock / spin_unlock | 인터럽트 컨텍스트 |
guard(spinlock_irq) | 스핀락+IRQ | spin_lock_irq / spin_unlock_irq | IRQ 비활성화 필요 시 |
scoped_guard(rcu) | RCU 읽기 | rcu_read_lock / rcu_read_unlock | RCU 보호 포인터 읽기 |
scoped_guard(rwsem_read) | 읽기 세마포어 | down_read / up_read | 공유 읽기 잠금 |
scoped_guard(rwsem_write) | 쓰기 세마포어 | down_write / up_write | 배타적 쓰기 잠금 |
guard(preempt) | 선점 비활성화 | preempt_disable / preempt_enable | Per-CPU 변수 접근 |
cleanup-attribute vs goto cleanup 비교
| 패턴 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
goto cleanup | 명시적, 디버그 용이, 커널 전통 | 레이블 순서 오류 위험, 가독성↓ | 복잡한 역순 해제 체인 |
devm_* | 누락 불가, probe 코드 간소화 | 해제 시점 제어 불가 | 디바이스 드라이버 probe/remove |
__free() | 단순 포인터 해제에 최적 | 복잡한 조건부 해제 어려움 | 단일 자원 자동 해제 |
scoped_guard | 락 해제 누락 방지, 가독성↑ | Linux 6.4+ 전용 | 임계 구역 보호 |
sparse 어노테이션 — 포인터 주소 공간 분류
sparse는 리눅스 커널 전용 정적 분석 도구입니다. __user, __iomem, __rcu 등의 어노테이션을 인식해 잘못된 포인터 사용을 컴파일 타임에 감지합니다.
주요 sparse 어노테이션
| 어노테이션 | 의미 | 사용 예 |
|---|---|---|
__user | 사용자 공간 포인터 — 직접 역참조 금지 | copy_from_user(kernel_buf, __user buf, len) |
__iomem | I/O 메모리 매핑 포인터 | void __iomem *regs = ioremap(paddr, size) |
__percpu | Per-CPU 변수 포인터 | DEFINE_PER_CPU(int, my_counter) |
__rcu | RCU 보호 포인터 — 적절한 컨텍스트에서만 접근 | struct foo __rcu *ptr |
__must_check | 반환값 무시 금지 | int __must_check request_irq(...) |
__force | 강제 타입 변환 (sparse 경고 억제) | 엔디안 변환: (__force __be32) |
__acquire(x) | 락 획득 표시 (sparse 락 추적) | 락 래퍼 함수 |
__release(x) | 락 해제 표시 | 언락 래퍼 함수 |
/* __user: 사용자 공간 포인터 사용 패턴 */
static ssize_t my_write(struct file *f,
const char __user *buf,
size_t count, loff_t *pos)
{
char kbuf[256];
/* if (!buf) return -EFAULT; sparse가 감지 */
if (copy_from_user(kbuf, buf, min(count, sizeof(kbuf))))
return -EFAULT;
return count;
}
/* make C=2 M=drivers/mydrv/ 로 sparse 실행 */
__bitwise — 타입 안전 정수 (gfp_t, __be32 기반)
sparse의 __bitwise는 정수 타입에 "비트 공간 레이블"을 붙여 다른 레이블의 정수와 직접 혼용하면 경고를 발생시킵니다. gfp_t와 엔디안 타입에 활용됩니다.
/* __bitwise: sparse 전용 타입 레이블 (컴파일 런타임 영향 없음) */
#ifdef __CHECKER__ /* sparse가 정의하는 매크로 */
#define __bitwise __attribute__((bitwise))
#else
#define __bitwise
#endif
/* gfp_t: GFP_KERNEL과 GFP_ATOMIC 혼용 감지 */
typedef unsigned int __bitwise gfp_t;
#define GFP_KERNEL ((gfp_t)0x00000CC0u)
#define GFP_ATOMIC ((gfp_t)0x00000020u)
/* 잘못된 사용: sparse 경고 발생 */
gfp_t flags = GFP_KERNEL | 0x100; /* warning: plain integer */
gfp_t flags = (__force gfp_t)0x100; /* OK: __force로 억제 */
/* __be32/__le32: 엔디안 혼용 감지 */
__be32 net_val = cpu_to_be32(42);
u32 host_val = net_val; /* sparse 경고: 엔디안 혼용 */
u32 host_val = be32_to_cpu(net_val); /* 정상 */
sparse 실행 방법
# C=1: 변경된 파일만 sparse 검사
make C=1 M=drivers/net/ethernet/intel/igb/
# C=2: 모든 파일 sparse 검사 (느림)
make C=2 M=drivers/my_driver/
# 특정 파일만 검사
make C=2 drivers/char/mem.o
# CF: sparse 추가 플래그
make C=1 CF="-D__CHECK_ENDIAN__" drivers/net/
# sparse 경고 예시 출력:
# drivers/char/mem.c:123:5: warning: incorrect type in assignment
# expected unsigned int [usertype] [bitwise] gfp_t
# got unsigned int
커널 타입 시스템 — u8, __be32, gfp_t
커널은 include/linux/types.h에서 자체 정수 타입 시스템을 정의합니다. 크기 고정, 엔디안 안전, 컨텍스트 강제를 목적으로 합니다.
크기 고정 정수 타입
| 커널 타입 | 표준 동등 | 용도 |
|---|---|---|
u8 / s8 | uint8_t / int8_t | 바이트 필드, 레지스터 |
u16 / s16 | uint16_t / int16_t | 포트 번호, 16비트 오프셋 |
u32 / s32 | uint32_t / int32_t | PID, 타임스탬프, 레지스터 값 |
u64 / s64 | uint64_t / int64_t | 주소, 파일 크기, 나노초 시각 |
ulong | unsigned long | 아키텍처 워드 크기 |
엔디안 안전 타입 (sparse + __force)
/* Big-endian / Little-endian 타입 (sparse가 혼용 오류 검출) */
__be16 be_port = htons(80); /* Network byte order */
__le32 le_val = cpu_to_le32(0x12345678);
/* 변환 API (include/uapi/linux/byteorder/) */
u32 host = be32_to_cpu(be_val); /* BE → 호스트 */
u32 host = le32_to_cpu(le_val); /* LE → 호스트 */
__be32 net = cpu_to_be32(host); /* 호스트 → BE */
컨텍스트 강제 타입
| 타입 | 정의 | 설명 |
|---|---|---|
gfp_t | typedef unsigned int __bitwise gfp_t | 메모리 할당 플래그 (GFP_KERNEL, GFP_ATOMIC) |
phys_addr_t | typedef u64 phys_addr_t | 물리 주소 (32비트: u32) |
dma_addr_t | typedef u64 dma_addr_t | DMA 버스 주소 |
resource_size_t | typedef phys_addr_t resource_size_t | I/O 리소스 크기 |
sector_t | typedef u64 sector_t | 블록 디바이스 섹터 번호 |
pid_t | typedef int __kernel_pid_t | 프로세스 ID |
atomic_t | struct { int counter; } | 원자적 정수 (직접 접근 금지) |
/* gfp_t: 잘못된 플래그 혼용을 sparse가 감지 */
void *ptr = kmalloc(size, GFP_KERNEL); /* 슬립 가능 컨텍스트 */
void *ptr = kmalloc(size, GFP_ATOMIC); /* 인터럽트 컨텍스트 */
/* phys_addr_t vs dma_addr_t: IOMMU가 있으면 다를 수 있음 */
phys_addr_t paddr = virt_to_phys(kaddr);
dma_addr_t daddr = dma_map_single(dev, kaddr, size, DMA_TO_DEVICE);
refcount_t — 포화 참조 카운터
일반 atomic_t는 0에서 dec 시 언더플로우가 발생할 수 있어 use-after-free 취약점으로 이어집니다. refcount_t는 포화 카운터로 0에서 감소를 차단합니다.
/* refcount_t: include/linux/refcount.h */
struct my_obj {
refcount_t refs; /* atomic_t 대신 */
/* ... */
};
refcount_set(&obj->refs, 1); /* 초기화 */
refcount_inc(&obj->refs); /* +1 (0에서 증가 금지) */
refcount_dec_and_test(&obj->refs) /* -1, 0이면 true 반환 */
if (refcount_dec_and_test(&obj->refs))
kfree(obj); /* 마지막 참조자가 해제 */
/* kref: 고수준 래퍼 (refcount_t 기반) */
struct my_obj {
struct kref kref;
/* ... */
};
kref_init(&obj->kref); /* refs = 1 */
kref_get(&obj->kref); /* refs++ */
kref_put(&obj->kref, release_fn); /* refs--, 0이면 release_fn() */
ktime_t — 고해상도 시각
/* ktime_t: s64 나노초 단위 (include/linux/ktime.h) */
ktime_t start = ktime_get(); /* 현재 시각 (나노초) */
/* ... 작업 ... */
ktime_t end = ktime_get();
s64 elapsed_ns = ktime_to_ns(ktime_sub(end, start));
/* 단위 변환 */
s64 us = ktime_to_us(ktime); /* 마이크로초 */
s64 ms = ktime_to_ms(ktime); /* 밀리초 */
/* timespec64로 변환 */
struct timespec64 ts;
ktime_get_real_ts64(&ts); /* 실제 시각(CLOCK_REALTIME) */
/* atomic64_t: 64비트 원자 연산 */
atomic64_t counter = ATOMIC64_INIT(0);
atomic64_inc(&counter);
atomic64_add(100, &counter);
s64 val = atomic64_read(&counter);
typedef 사용 지침은 커널 코딩 스타일을 참조하세요.
컴파일 타임 검사 — BUILD_BUG_ON, static_assert
런타임 오버헤드 없이 불변식을 강제하는 컴파일 타임 어서션입니다.
/* BUILD_BUG_ON: 조건이 참이면 컴파일 오류 */
BUILD_BUG_ON(sizeof(struct task_struct) > 4096);
BUILD_BUG_ON(THREAD_SIZE < 4096);
/* BUILD_BUG_ON 내부 구현 */
#define BUILD_BUG_ON(condition) \
BUILD_BUG_ON_MSG(condition, "BUILD_BUG_ON failed: " #condition)
/* BUILD_BUG_ON_MSG */
#define BUILD_BUG_ON_MSG(cond, msg) \
compiletime_assert(!(cond), msg)
/* compiletime_assert: 조건 거짓이면 0-사이즈 배열 오류 트릭 */
#define compiletime_assert(condition, msg) \
_compiletime_assert(condition, msg, \
__compiletime_assert_, __COUNTER__)
/* C11 static_assert (매크로 래퍼) */
static_assert(sizeof(int) == 4, "int must be 4 bytes");
/* BUILD_BUG_ON_ZERO: 컴파일 타임 0 반환 (표현식 내 사용) */
/* __must_be_array: 포인터이면 컴파일 오류 */
#define __must_be_array(a) \
BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))
/* 실전: 구조체 크기/오프셋 검사 */
BUILD_BUG_ON(offsetof(struct pt_regs, ip) != 16 * 8);
코드 설명
- BUILD_BUG_ON조건이 참이면 컴파일 오류를 발생시킵니다. 런타임 비용이 전혀 없으며, 구조체 크기, 타입 크기, 오프셋 등의 불변식을 강제하는 데 사용합니다. 커널 전반에서 "이 값은 항상 N이어야 한다"는 가정을 문서화하고 강제합니다.
- __must_be_array
ARRAY_SIZE매크로 내부에서 사용됩니다. 배열이 아닌 포인터에ARRAY_SIZE를 적용하면 컴파일 오류가 발생하도록 합니다. 포인터와 배열의sizeof가 다름을 이용합니다.
커널 C 보안 코딩 — FORTIFY_SOURCE, randstruct, structleak
리눅스 커널은 컴파일 타임과 런타임 보안 강화 기법을 다층으로 적용합니다. FORTIFY_SOURCE, GCC 보안 플러그인, 스택 보호 메커니즘의 동작 원리를 이해하면 더 안전한 커널 코드를 작성할 수 있습니다.
FORTIFY_SOURCE — 경계 검사 강화
-D_FORTIFY_SOURCE=2와 커널의 CONFIG_FORTIFY_SOURCE=y는 __builtin_object_size를 활용해 memcpy, strcpy 등의 경계를 컴파일 타임 또는 런타임에 검사합니다.
/* FORTIFY_SOURCE 동작 원리 (include/linux/fortify-string.h) */
/* 원래 memcpy → FORTIFY 버전으로 치환 */
#define memcpy(p, q, s) __builtin_memcpy_chk(p, q, s, \
__builtin_object_size(p, 0))
/* __builtin_object_size(p, 0): p가 가리키는 객체 크기 반환 */
/* 컴파일 타임: 크기 알면 → BUILD_BUG_ON으로 컴파일 오류 */
/* 런타임: 크기 모르면 → __memcpy_chk()에서 크기 비교 후 panic */
/* 예: 컴파일 타임 탐지 */
char buf[8];
memcpy(buf, src, 16); /* ← 컴파일 오류: 8바이트에 16 복사 */
/* 커널 FORTIFY: strscpy 권장 (strcpy 대신) */
/* strscpy(dst, src, size): 최대 size-1 복사, 항상 NUL 종료 */
strscpy(dev->name, src_name, sizeof(dev->name));
/* strlcpy/strcpy는 FORTIFY_SOURCE에서 경고/오류 발생 가능 */
/* 커널 6.x: strlcpy deprecated → strscpy 사용 권장 */
스택 보안 레이어
| 보안 기법 | GCC 옵션 | Kconfig | 동작 |
|---|---|---|---|
| 스택 카나리 | -fstack-protector-strong | CONFIG_STACKPROTECTOR_STRONG | 함수 진입/복귀 시 카나리 값 검사 |
| 레지스터 초기화 | -fzero-call-used-regs=used-gpr | CONFIG_ZERO_CALL_USED_REGS | 함수 반환 전 사용 레지스터 0으로 초기화 |
| 스택 변수 초기화 | GCC plugin structleak | CONFIG_GCC_PLUGIN_STRUCTLEAK | 구조체 변수 자동 0 초기화 |
| 스택 클래쉬 방지 | -fstack-clash-protection | CONFIG_SHADOW_CALL_STACK | 스택 프로브로 클래쉬 탐지 |
| 리턴 어드레스 보호 | ShadowCallStack (ARM64) | CONFIG_SHADOW_CALL_STACK | 별도 스택에 리턴 주소 저장 |
| CFI (제어 흐름 무결성) | Clang CFI | CONFIG_CFI_CLANG | 함수 포인터 타입 검사 |
GCC 보안 플러그인 — structleak & randstruct
/* structleak: 미초기화 구조체 변수 자동 0 채움 */
/* CONFIG_GCC_PLUGIN_STRUCTLEAK=y 활성화 시 */
/* 컴파일러가 자동으로 memset(&var, 0, sizeof(var)) 추가 */
struct my_sensitive {
u32 key;
u8 secret[16];
};
void process(void)
{
struct my_sensitive s; /* STRUCTLEAK: 자동 0초기화 삽입 */
do_work(&s);
/* 초기화 전 secret이 스택 잔여 데이터를 노출하지 않음 */
}
/* randstruct: 구조체 필드 무작위 배치 */
/* CONFIG_GCC_PLUGIN_RANDSTRUCT=y */
/* 빌드마다 다른 필드 순서 → 오프셋 기반 익스플로잇 방지 */
struct cred { /* 커널 자격 증명 구조체 */
uid_t uid;
gid_t gid;
u32 securebits;
kernel_cap_t cap_effective;
/* 빌드마다 필드 순서 다름 — 주소 예측 불가 */
} __randomize_layout; /* 명시적 무작위화 마킹 */
/* __no_randomize_layout: 특정 구조체 제외 */
struct pt_regs { /* ABI 고정 필요 → 무작위화 금지 */
unsigned long r15;
unsigned long r14;
/* ... */
} __no_randomize_layout;
커널 C 보안 코딩 체크리스트
| 위험 패턴 | 안전한 대안 | 이유 |
|---|---|---|
strcpy(dst, src) | strscpy(dst, src, sizeof(dst)) | 경계 검사 없음 → 오버플로우 |
sprintf(buf, fmt, ...) | snprintf(buf, sizeof(buf), fmt, ...) | 버퍼 오버플로우 위험 |
kmalloc(size, ...) | kzalloc(size, ...) | 미초기화 메모리 노출 방지 |
memcpy(dst, __user ptr, n) | copy_from_user(dst, ptr, n) | 사용자 포인터 직접 역참조 금지 |
*(int*)arbitrary_addr | KASLR + SMEP/SMAP 하에서 접근 불가 | 임의 주소 접근 → panic |
n * sizeof(T) kmalloc | kmalloc_array(n, sizeof(T), ...) | 정수 오버플로우 → 과소 할당 |
| 고정 크기 스택 버퍼 | 동적 할당 또는 kvmalloc | 스택 오버플로우 위험 |
kfree(ptr); ptr→field | ptr = NULL 후 접근 금지 | Use-after-free → KASAN 탐지 |
OPTIMIZER_HIDE_VAR — 보안 변수 최적화 방지
/* 문제: 컴파일러 최적화로 보안 변수 제거 가능 */
u8 key[32];
get_key(key);
crypto_sign(msg, key);
memset(key, 0, sizeof(key)); /* ← 컴파일러가 제거 가능 (이후 미사용) */
/* 해결: OPTIMIZER_HIDE_VAR 또는 memzero_explicit */
memzero_explicit(key, sizeof(key)); /* 최적화 방지 barrier() 포함 */
/* OPTIMIZER_HIDE_VAR: 변수를 컴파일러 최적화에서 숨김 */
#define OPTIMIZER_HIDE_VAR(var) \
asm ("" : "=r"(var) : "0"(var))
u32 secret = get_secret();
OPTIMIZER_HIDE_VAR(secret); /* 컴파일러가 값을 추적하지 못하게 */
use(secret);
/* kfree_sensitive: 해제 전 0 채움 (CONFIG_INIT_ON_FREE_DEFAULT_ON과 별개) */
kfree_sensitive(key_buffer); /* memzero_explicit + kfree */
LTO (Link-Time Optimization) — 커널과 인터랙션
LTO는 링크 타임에 전체 프로그램 최적화를 수행합니다. 커널에서는 제한적으로 사용되며, noinline과 같은 속성의 중요성이 커집니다.
LTO 개요
# Full LTO: 전체 프로그램을 IR로 링크 후 최적화
gcc -flto -O2 -c a.c b.c
gcc -flto -O2 a.o b.o -o program
# Thin LTO: 병렬 처리 + 증분 빌드 (빠름)
gcc -flto=thin -O2 -c a.c b.c
gcc -flto=thin -O2 a.o b.o -o program
커널과 LTO
/* 커널 LTO 지원 (CONFIG_LTO=y) */
/* 장점: IPA 인라이닝, 데드 코드 제거, 전역 최적화 */
/* 단점: 빌드 시간 증가, 메모리 사용량 급증 */
/* LTO 하에서 noinline 중요성 */
/* LTO는 IPA(Inter-Procedural Analysis)로 모든 함수 인라인 후보 */
/* 디버깅/스택 추적용 함수는 noinline 필수 */
void __noinline debug_dump_stack(void) /* 인라인 금지 */
{
dump_stack();
}
/* __used: LTO가 미사용 함수 제거 방지 */
static void __used __init early_init(void)
{
/* 링커가 제거하지 않음 */
}
/* __visible: LTO가 심볼 가시성 유지 */
void __visible public_api(void);
| 속성/옵션 | LTO 효과 | 커널 사용 |
|---|---|---|
noinline | IPA 인라이닝 방지 | 디버그 함수, 스택 추적 |
__used | 미사용 함수 제거 방지 | 링커 테이블, init/cleanup |
__visible | 심볼 가시성 유지 | 외부 API, 어셈블리 호출 |
-flto | Full LTO 활성화 | CONFIG_LTO |
-flto=thin | Thin LTO (병렬) | CONFIG_LTO_THIN |
LTO 관련 커널 Kconfig
# arch/Kconfig
config LTO
bool "Link-time optimization (LTO)"
help
전체 프로그램 최적화 활성화
config LTO_THIN
bool "Thin LTO"
depends on LTO
help
병렬 LTO — 빌드 시간 단축
모듈 초기화 패턴 — __init, initcall, module_param
리눅스 커널 모듈 시스템은 정교한 초기화 체계를 제공합니다. __init/__exit 링커 섹션, 8단계 initcall 레벨, module_param 파라미터 시스템을 이해하면 드라이버와 서브시스템 초기화 흐름을 완전히 파악할 수 있습니다.
__init / __exit / __initdata — 링커 섹션
__init은 함수를 .init.text 섹션에, __initdata는 변수를 .init.data에 배치합니다. 초기화 완료 후 free_initmem()으로 이 메모리를 해제합니다.
/* __init: .init.text 섹션에 배치 */
static int __init my_driver_init(void)
{
pr_info("my_driver: initializing\n");
return platform_driver_register(&my_platform_driver);
}
/* __exit: .exit.text 섹션 — 모듈 언로드 시만 실행 */
static void __exit my_driver_exit(void)
{
platform_driver_unregister(&my_platform_driver);
}
/* module_init/exit: initcall 레벨 6(device_initcall)에 등록 */
module_init(my_driver_init);
module_exit(my_driver_exit);
/* __initdata: 초기화 데이터 — 부팅 후 해제됨 */
static int __initdata init_count = 0;
/* free_initmem(): 부팅 완료 후 .init 섹션 전체 해제 */
/* "Freeing unused kernel memory: 1234K" 메시지의 원인 */
/* __ref: __init 함수를 초기화 외 컨텍스트에서 호출 시 경고 억제 */
void __ref cpu_up(unsigned int cpu) /* 핫플러그 CPU 추가 */
{
__cpu_up(cpu); /* __init 함수 호출 가능 (핫플러그 경우) */
}
initcall 레벨 8단계 전체 참조
| 레벨 | 매크로 | 섹션 | 대표 서브시스템 |
|---|---|---|---|
| 0 | pure_initcall(fn) | .initcall0.init | CPU 기능, KASLR 설정 |
| 1 | core_initcall(fn) | .initcall1.init | kallsyms, workqueue, rcu_init |
| 1s | core_initcall_sync(fn) | .initcall1s.init | 동기화 barrier |
| 2 | postcore_initcall(fn) | .initcall2.init | 버스 추상화, 클럭 프레임워크 |
| 3 | arch_initcall(fn) | .initcall3.init | ACPI, IOMMU, IRQ 서브시스템 |
| 4 | subsys_initcall(fn) | .initcall4.init | PCI, USB, 블록 레이어, 네트워킹 |
| 5 | fs_initcall(fn) | .initcall5.init | VFS, ext4, xfs, btrfs, procfs |
| 6 | device_initcall(fn) | .initcall6.init | module_init() 기본 레벨 |
| 7 | late_initcall(fn) | .initcall7.init | watchdog, 배터리, 최후 진단 |
module_param — 모듈 파라미터 완전 참조
/* 기본 형식: module_param(이름, 타입, 권한) */
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug verbosity level (0=off, 1=info, 2=verbose)");
static bool enable_feature = true;
module_param(enable_feature, bool, 0444); /* 0444: 읽기 전용 */
static char *device_name = "default";
module_param(device_name, charp, 0000); /* 0000: sysfs에 미노출 */
/* module_param_array: 배열 파라미터 */
static int irq_list[8] = { -1 };
static int irq_count = 0;
module_param_array(irq_list, int, &irq_count, 0444);
MODULE_PARM_DESC(irq_list, "IRQ numbers (comma-separated, up to 8)");
/* insmod mydrv.ko irq_list=3,7,11 → irq_list[0..2] = {3,7,11}, irq_count = 3 */
/* 지원 파라미터 타입 */
/* byte, short, ushort, int, uint, long, ulong */
/* charp (char*), bool, invbool (반전 bool) */
/* hexint (0x 접두사 허용 정수) */
/* sysfs 경로: /sys/module/<모듈명>/parameters/<파라미터명> */
/* echo 2 > /sys/module/mydrv/parameters/debug_level */
early_param / __setup — 커맨드라인 파싱
/* __setup: 커맨드라인 파라미터 등록 (메모리 초기화 후) */
static int __init parse_console(char *str)
{
add_preferred_console("ttyS", simple_strtoul(str, &str, 10), str);
return 1; /* 1: 파라미터 소비 완료 */
}
__setup("console=", parse_console);
/* 커맨드라인: console=ttyS0,115200 */
/* early_param: 메모리 초기화 이전에 파싱 (더 이른 시점) */
static int __init parse_earlyprintk(char *str)
{
setup_early_printk(str);
return 0;
}
early_param("earlyprintk", parse_earlyprintk);
/* 커맨드라인: earlyprintk=serial,ttyS0,115200 */
/* 파싱 타이밍 비교 */
/* early_param: start_kernel() → parse_early_param() → 메모리 초기화 전 */
/* __setup: start_kernel() → parse_args() → 메모리 초기화 후 */
/* module_param: insmod/modprobe 시 또는 부팅 시 (builtin 모듈) */
MODULE_* 메타데이터 매크로
| 매크로 | 용도 | 예시 |
|---|---|---|
MODULE_LICENSE("GPL v2") | 라이선스 선언 (non-GPL 심볼 접근 제한) | "GPL", "GPL v2", "Dual MIT/GPL" |
MODULE_AUTHOR("Name") | 작성자 정보 | /sys/module/*/srcversion |
MODULE_DESCRIPTION("...") | 모듈 설명 | modinfo mydrv.ko |
MODULE_VERSION("1.0.0") | 버전 문자열 | modinfo 출력 |
MODULE_ALIAS("pci:...") | 별칭 → udev 자동 로드 | pci:v00008086d... |
MODULE_DEVICE_TABLE(pci, tbl) | PCI ID 테이블 등록 | udev 핫플러그 지원 |
MODULE_FIRMWARE("fw.bin") | 필요 펌웨어 선언 | modinfo firmware 필드 |
MODULE_SOFTDEP("pre: crc32") | 소프트 의존성 (로드 순서) | modprobe 의존성 해결 |
전처리기 패턴 — IS_ENABLED, do-while(0), x-macro
커널에서 자주 사용하는 C 전처리기 관용 패턴들입니다.
do { ... } while (0) 패턴
/* 문제: 멀티-문 매크로가 if-else에서 깨짐 */
#define BAD_MACRO(x) stmt1; stmt2 /* ← 위험 */
if (cond)
BAD_MACRO(x); /* stmt2는 항상 실행됨! */
else
other();
/* 해결: do-while(0) 래핑 — 단일 문으로 처리 */
#define GOOD_MACRO(x) do { stmt1; stmt2; } while (0)
if (cond)
GOOD_MACRO(x); /* 올바르게 동작 */
else
other();
/* 커널 예시: pr_err, list_add_tail, spin_lock */
#define spin_unlock_irqrestore(lock, flags) do { \
raw_spin_unlock_irqrestore(&(lock)->rlock, flags); \
} while (0)
stringify / token-paste
/* # stringify: 인수를 문자열 리터럴로 변환 */
#define STRINGIFY(x) #x
#define STR(x) STRINGIFY(x) /* 매크로 전개 후 stringify */
/* ## token-paste: 두 토큰 연결 */
#define DEFINE_MUTEX(name) struct mutex name##_mutex = __MUTEX_INITIALIZER(name##_mutex)
/* 커널 예: __UNIQUE_ID (충돌 없는 고유 식별자) */
#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)
IS_ENABLED 패턴
/* IS_ENABLED: Kconfig 심볼이 y 또는 m이면 1 반환 */
#if IS_ENABLED(CONFIG_IPV6)
/* IPv6 코드 */
#endif
/* IS_BUILTIN vs IS_MODULE 구분 */
#if IS_BUILTIN(CONFIG_NET)
/* =y 빌트인 전용 코드 */
#elif IS_MODULE(CONFIG_NET)
/* =m 모듈 전용 코드 */
#endif
/* IS_ENABLED 내부 구현 (include/linux/kconfig.h) */
/* CONFIG_FOO=y → #define CONFIG_FOO 1 */
/* CONFIG_FOO=m → #define CONFIG_FOO_MODULE 1 */
#define IS_ENABLED(option) \
(IS_BUILTIN(option) || IS_MODULE(option))
가변 인수 매크로와 pr_debug
/* pr_debug: 조건부 디버그 출력 (C99 가변 인수) */
#ifdef DEBUG
#define pr_debug(fmt, ...) printk(KERN_DEBUG fmt, ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) no_printk(KERN_DEBUG fmt, ##__VA_ARGS__)
#endif
/* ##__VA_ARGS__: 인수 없을 때 앞 쉼표 제거 (GNU 확장) */
__VA_OPT__ — C23 가변 인수 쉼표 처리
C23의 __VA_OPT__는 가변 인수 매크로에서 인수가 있을 때만 내용을 확장합니다. GNU 확장 ##__VA_ARGS__의 표준화 버전입니다.
/* C23: __VA_OPT__ — 가변 인수 있을 때만 확장 */
#define log_debug(fmt, ...) \
printf("[DEBUG] " fmt __VA_OPT__(,) __VA_ARGS__)
log_debug("started\n"); /* printf("[DEBUG] started\n") */
log_debug("count=%d\n", 42); /* printf("[DEBUG] count=%d\n", 42) */
/* GNU 확장 (C23 이전): ##__VA_ARGS__ */
#define pr_info(fmt, ...) \
printk(KERN_INFO fmt, ##__VA_ARGS__)
/* 활용: 선택적 추가 인수 */
#define ASSERT_MSG(cond, ...) \
((cond) ? (void)0 : panic("assertion failed" __VA_OPT__(": ") __VA_ARGS__))
__COUNTER__ — 고유 식별자 생성
__COUNTER__는 매크로가 확장될 때마다 0부터 증가하는 정수로 대체됩니다. 고유 변수명, 레이블, 식별자 생성에 활용합니다.
/* __COUNTER__: 매 확장마다 증가 */
#define UNIQUE_VAR(prefix) prefix##_##__COUNTER__
int UNIQUE_VAR(tmp) = 1; /* int tmp_0 = 1 */
int UNIQUE_VAR(tmp) = 2; /* int tmp_1 = 2 */
/* 커널: __UNIQUE_ID */
#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)
/* SCOPE_GUARD 패턴 */
#define SCOPE_GUARD(name, fn) \
auto void __PASTE(__scope_guard_, __COUNTER__)(void *p) { fn(p); }
/* C의 auto는 다른 의미 — 실제 구현은 cleanup 속성 사용 */
매크로 디버깅 기법
/* 매크로 확장 결과 확인: gcc -E */
/* $ gcc -E test.c -o test.i */
/* 매크로 정의 확인 */
#pragma message("BUILD_BUG_ON defined as: " STRINGIFY(BUILD_BUG_ON))
/* 매크로 인수 개수 확인 */
#define NUM_ARGS(...) (sizeof((int[]){__VA_ARGS__})/sizeof(int))
int n = NUM_ARGS(a, b, c); /* n = 3 */
/* 매크로 재귀 확장 방지 */
#define FOO FOO /* 무한 재귀 방지: 자기 자신으로 확장 금지 */
X-Macro 패턴
/* X-Macro: 목록을 여러 용도로 재사용 */
#define IRQ_TYPES \
X(IRQ_TYPE_NONE, 0, "none") \
X(IRQ_TYPE_EDGE_RISING, 1, "rising") \
X(IRQ_TYPE_EDGE_FALLING, 2, "falling") \
X(IRQ_TYPE_LEVEL_HIGH, 4, "high") \
X(IRQ_TYPE_LEVEL_LOW, 8, "low")
/* 열거형 생성 */
enum irq_type {
#define X(name, val, str) name = val,
IRQ_TYPES
#undef X
};
/* 이름 배열 생성 */
static const char *irq_type_names[] = {
#define X(name, val, str) [val] = str,
IRQ_TYPES
#undef X
};
- GCC 완전 가이드 — 컴파일러 옵션, 최적화, GCC 플러그인 시스템
- 인라인 어셈블리 —
asm volatile문법, 제약 조건, 아키텍처별 패턴 - 메모리 배리어 — SMP 메모리 모델, acquire/release, LKMM 완전 참조
- 커널 코딩 스타일 — 네이밍 규칙, 들여쓰기, typedef 가이드라인