커널 모듈 (Kernel Modules)

Loadable Kernel Module(LKM)의 작성부터 빌드, 로딩, 파라미터, 의존성 관리, 서명, 디버깅까지 커널 모듈의 모든 것을 다룹니다.

관련 표준: ELF Specification (모듈 로딩), Loadable Kernel Module Programming Guide — 동적 로딩 가능한 커널 모듈 표준입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 커널 아키텍처빌드 시스템 문서를 먼저 읽으세요. 입문 문서는 개발 환경, 코드 위치, 변경 절차를 연결해 보는 것이 핵심이므로 기본 작업 흐름을 먼저 고정해야 합니다.
일상 비유: 이 주제는 현장 작업 시작 전 안전 교육과 비슷합니다. 도구 사용법과 작업 순서를 먼저 익혀야 실수를 줄일 수 있듯이, 커널 개발도 기본 루틴을 먼저 갖추는 것이 중요합니다.

핵심 요약

  • LKM — Loadable Kernel Module. .ko 확장자를 가진 커널 오브젝트 파일입니다.
  • module_init / module_exit — 모듈이 로드/언로드될 때 호출되는 진입점 함수를 등록합니다.
  • insmod / rmmod / modprobe — 모듈을 로드/언로드하는 명령어. modprobe는 의존성을 자동 해결합니다.
  • module_param() — 모듈 로드 시 파라미터를 전달할 수 있게 해 주는 매크로입니다.
  • EXPORT_SYMBOL — 다른 모듈에서 사용할 수 있도록 심볼을 내보내는 매크로입니다.

단계별 이해

  1. 소스 작성module_init()module_exit()을 포함하는 C 파일을 작성합니다.

    최소한의 Hello World 모듈은 10줄 이내로 작성할 수 있습니다.

  2. Makefile 작성obj-m += hello.o로 모듈 대상을 선언하고 Kbuild를 호출합니다.

    커널 소스 트리의 빌드 시스템을 활용하여 .ko 파일을 생성합니다.

  3. 빌드 및 로드make로 빌드 후 sudo insmod hello.ko로 커널에 로드합니다.

    dmesg로 커널 로그를 확인하면 모듈의 init 함수 출력을 볼 수 있습니다.

  4. 언로드sudo rmmod hello로 모듈을 제거합니다.

    exit 함수가 호출되어 모듈이 사용한 리소스를 정리합니다.

직접 해보기: Hello World 모듈 만들기

이 실습에서는 가장 간단한 커널 모듈을 직접 작성하고 빌드한 뒤 커널에 로드해봅니다. 이론으로만 배운 모듈 개념을 실제 코드로 경험하면서 이해를 깊게 할 수 있습니다.

난이도: 초급 ⏱️ 예상 소요 시간: 30분
⚙️ 환경 요구사항
  • OS: Ubuntu 22.04 LTS, Fedora 38+, Debian 12+ (또는 유사 배포판)
  • 커널: Linux 5.10 이상 (커널 헤더 포함)
  • 패키지: build-essential, linux-headers-$(uname -r), make, gcc
  • 권한: sudo 권한 필요 (모듈 로드/언로드)
  • 디스크: 최소 100MB 여유 공간
권장: 가상 머신(VirtualBox, QEMU)이나 컨테이너(Docker)에서 실습하는 것이 안전합니다. 단, Docker는 호스트 커널을 공유하므로 모듈 로드/언로드 실습은 VM에서 수행하는 것을 권장합니다.
실습 흐름
  1. 환경 준비 및 패키지 설치 (5분)
  2. 작업 디렉토리 생성 및 소스 코드 작성 (10분)
  3. Makefile 작성 (5분)
  4. 빌드 및 에러 수정 (5분)
  5. 모듈 로드 및 테스트 (3분)
  6. 로그 확인 및 분석 (2분)
  7. 정리 및 언로드 (2분)

1단계: 환경 준비 (⏱️ 5분)

목표: 커널 모듈 개발에 필요한 헤더와 도구를 설치합니다.

# Ubuntu/Debian 계열
sudo apt update
sudo apt install -y build-essential linux-headers-$(uname -r)

# Fedora/RHEL 계열
sudo dnf install -y gcc make kernel-devel kernel-headers

# 설치 확인
ls /lib/modules/$(uname -r)/build
✅ 예상 출력:
/lib/modules/6.1.0-18-amd64/build
arch  block  certs  crypto  Documentation  drivers  fs  include  init  ...
      
⚠️ 문제 발생 시
  • 오류: "Unable to locate package linux-headers-..."
    해결: uname -r 결과를 확인하고 정확한 버전의 헤더 패키지 설치
  • 오류: "Permission denied"
    해결: sudo를 빠뜨렸는지 확인

2단계: 소스 코드 작성 (⏱️ 10분)

목표: Hello World 커널 모듈 소스 파일을 작성합니다.

# 작업 디렉토리 생성
mkdir -p ~/kernel_lab/hello
cd ~/kernel_lab/hello

# 소스 파일 생성
nano hello.c  # 또는 vi, vim, gedit 등

아래 코드를 hello.c 파일에 작성하세요:

/* hello.c - 간단한 Hello World 커널 모듈 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

/* 1. 모듈 초기화 함수: insmod 시 호출 */
static int __init hello_init(void)
{
    pr_info("Hello, Kernel World!\\n");
    pr_info("Module loaded at %s:%d\\n", __FILE__, __LINE__);
    return 0;  /* 0 = 성공 */
}

/* 2. 모듈 종료 함수: rmmod 시 호출 */
static void __exit hello_exit(void)
{
    pr_info("Goodbye, Kernel World!\\n");
}

/* 3. 진입점 등록 */
module_init(hello_init);
module_exit(hello_exit);

/* 4. 메타데이터 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your@email.com>");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");
코드 상세 설명
  • 2-4행 필수 헤더 파일. init.h는 초기화 매크로, module.h는 모든 모듈에 필수, kernel.h는 커널 로그 함수 제공
  • 7-12행 모듈 초기화 함수. __init 매크로는 초기화 후 메모리에서 해제될 수 있음을 표시. insmod 시 호출되며 성공 시 0 반환
  • 15-18행 모듈 종료 함수. __exit 매크로는 built-in 모듈에서는 사용되지 않는 코드를 표시. rmmod 시 호출됨
  • 21-22행 커널에 모듈의 진입점 등록. module_init은 초기화 함수, module_exit은 종료 함수를 지정
  • 25-28행 모듈 메타데이터 정의. modinfo 명령으로 확인 가능. GPL 라이선스는 일부 커널 심볼 사용에 필수

3단계: Makefile 작성 (⏱️ 5분)

목표: 커널 빌드 시스템과 통합되는 Makefile을 작성합니다.

같은 디렉토리에 Makefile 생성:

# Makefile for hello module
obj-m += hello.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean
체크포인트: 디렉토리에 hello.cMakefile 두 파일이 있어야 합니다. ls 명령으로 확인하세요.

4단계: 빌드 (⏱️ 5분)

목표: 소스 코드를 컴파일하여 .ko 파일 생성.

# 빌드 실행
make

# 빌드 결과 확인
ls -lh hello.ko
✅ 예상 출력:
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-18-amd64'
  CC [M]  /home/user/kernel_lab/hello/hello.o
  MODPOST /home/user/kernel_lab/hello/Module.symvers
  CC [M]  /home/user/kernel_lab/hello/hello.mod.o
  LD [M]  /home/user/kernel_lab/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-18-amd64'

-rw-r--r-- 1 user user 16K Jan 15 10:30 hello.ko
      
⚠️ 문제 발생 시
  • 오류: "No rule to make target"
    해결: Makefile에서 탭(Tab) 대신 공백을 사용했는지 확인. make는 탭 필수!
  • 오류: "fatal error: linux/module.h: No such file"
    해결: 커널 헤더 미설치. 1단계 재확인

5단계: 모듈 로드 및 테스트 (⏱️ 3분)

목표: 모듈을 커널에 로드하고 동작 확인.

# 모듈 로드
sudo insmod hello.ko

# 로드된 모듈 확인
lsmod | grep hello

# 커널 로그 확인
dmesg | tail -5
✅ 예상 출력:
$ lsmod | grep hello
hello                  16384  0

$ dmesg | tail -5
[12345.678] Hello, Kernel World!
[12345.679] Module loaded at /home/user/kernel_lab/hello/hello.c:9
      
체크포인트: lsmod에서 hello 모듈이 보이고, dmesg에 "Hello, Kernel World!" 메시지가 출력되면 성공!

6단계: 모듈 정보 확인 (⏱️ 2분)

목표: 모듈의 메타데이터를 확인합니다.

# 모듈 상세 정보
modinfo hello.ko

# 특정 필드만 확인
modinfo -F license hello.ko
modinfo -F description hello.ko
실험해보기: 소스 코드의 MODULE_DESCRIPTION을 수정하고 다시 빌드해보세요. modinfo 출력이 어떻게 바뀌는지 확인할 수 있습니다.

7단계: 정리 (⏱️ 2분)

목표: 모듈을 언로드하고 시스템을 원래 상태로 되돌립니다.

# 모듈 언로드
sudo rmmod hello

# 언로드 확인
lsmod | grep hello  # 아무것도 출력 안 되면 성공

# 언로드 로그 확인
dmesg | tail -3

# 빌드 파일 정리 (선택)
make clean
✅ 예상 출력:
$ dmesg | tail -3
[12345.678] Hello, Kernel World!
[12345.679] Module loaded at /home/user/kernel_lab/hello/hello.c:9
[12450.123] Goodbye, Kernel World!
      

결과 검증

다음 항목을 모두 확인했다면 실습이 성공적으로 완료된 것입니다:

  • [ ] 모듈이 에러 없이 컴파일됨 (hello.ko 파일 생성)
  • [ ] insmod로 모듈 로드 성공 (에러 메시지 없음)
  • [ ] lsmod | grep hello에서 모듈이 보임
  • [ ] dmesg에 "Hello, Kernel World!" 메시지 출력
  • [ ] modinfo hello.ko로 메타데이터 확인 가능
  • [ ] rmmod로 모듈 언로드 성공
  • [ ] dmesg에 "Goodbye, Kernel World!" 메시지 출력
  • [ ] 언로드 후 lsmod에서 모듈이 사라짐

다음 단계

더 나아가기:
  • 모듈에 파라미터 추가 (module_param() 사용) — 모듈 파라미터 섹션 참조
  • 여러 소스 파일로 분리 (multi-file 모듈)
  • procfs 또는 sysfs 인터페이스 추가 — procfs/sysfs 문서 참조
  • 실제 디바이스 드라이버 작성 — 디바이스 드라이버 문서로 진행

LKM 개요 (Loadable Kernel Module Overview)

LKM 개념과 장점 (Concept & Benefits)

Loadable Kernel Module(LKM)은 커널이 실행 중인 상태에서 동적으로 로드하거나 언로드할 수 있는 커널 코드 조각입니다. 리눅스 커널은 모놀리식(monolithic) 아키텍처를 기반으로 하지만, LKM을 통해 마이크로커널의 유연성을 일부 확보합니다. 디바이스 드라이버, 파일시스템, 네트워크 프로토콜 등 대부분의 커널 기능이 모듈로 제공될 수 있습니다.

LKM의 주요 장점은 다음과 같습니다:

ℹ️

커널 모듈 파일의 확장자는 .ko (Kernel Object)입니다. 이는 일반 사용자 공간의 .so (Shared Object)와 유사한 개념이지만, 커널 공간에서 동작하므로 오류 발생 시 시스템 전체에 영향을 줄 수 있습니다.

모듈 vs 빌트인 (Module vs Built-in)

커널 설정(Kconfig)에서 각 기능은 세 가지 상태 중 하나로 설정됩니다:

설정값 Kconfig 표기 설명
Y [*] 커널 이미지(vmlinux)에 직접 포함 (빌트인)
M [M] 별도 .ko 파일로 빌드 (모듈)
N [ ] 빌드하지 않음 (제외)

빌트인으로 컴파일된 코드는 부팅 시 항상 사용 가능하며 init 과정에서 초기화됩니다. 반면, 모듈은 필요할 때 insmod 또는 modprobe로 로드합니다. 부팅에 필수적인 드라이버(루트 파일시스템 드라이버, 부트 디스크 컨트롤러 등)는 빌트인 또는 initramfs에 포함해야 합니다.

모듈 ELF 내부 구조 (Module ELF Internals)

커널 모듈(.ko) 파일은 일반적인 ELF(Executable and Linkable Format) 오브젝트 파일입니다. 그러나 사용자 공간의 공유 라이브러리(.so)와는 근본적으로 다릅니다. .so 파일은 ET_DYN(동적 공유 오브젝트)인 반면, .ko 파일은 ET_REL(재배치 가능 오브젝트)입니다. 이는 커널 모듈이 링커에 의해 최종 주소가 결정되지 않은 상태이며, 커널이 로딩 시 직접 심볼 해석과 재배치를 수행한다는 것을 의미합니다.

ℹ️

사용자 공간의 .so 파일은 동적 링커(ld-linux.so)가 로드하며 ET_DYN 타입입니다. 반면 .ko 파일은 ET_REL 타입으로, 커널의 모듈 로더(load_module())가 직접 재배치를 수행합니다. 이 차이는 커널 모듈이 위치 독립적(PIC)이 아닌, 완전한 재배치가 필요한 오브젝트임을 의미합니다.

ELF .ko 파일 섹션 구조

.ko 파일은 다양한 ELF 섹션으로 구성되어 있으며, 각 섹션은 커널 모듈 로더에 의해 특정 목적으로 사용됩니다. readelf -S 명령으로 섹션 헤더를 확인할 수 있습니다:

# .ko 파일의 ELF 섹션 헤더 확인
$ readelf -S hello.ko
There are 26 section headers, starting at offset 0x1a48:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.bu[...] NOTE             0000000000000000  00000040
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .text             PROGBITS         0000000000000000  00000070
       0000000000000000  0000000000000000  AX       0     0     1
  [ 3] .init.text        PROGBITS         0000000000000000  00000070
       0000000000000020  0000000000000000  AX       0     0     1
  [ 4] .rela.init.text   RELA             0000000000000000  00001040
       0000000000000030  0000000000000018   I      23     3     8
  [ 5] .exit.text        PROGBITS         0000000000000000  00000090
       0000000000000010  0000000000000000  AX       0     0     1
  [ 6] .rela.exit.text   RELA             0000000000000000  00001070
       0000000000000018  0000000000000018   I      23     5     8
  [ 7] .rodata.str1.1   PROGBITS         0000000000000000  000000a0
       0000000000000030  0000000000000001 AMS       0     0     1
  [ 8] .modinfo          PROGBITS         0000000000000000  000000d0
       00000000000000c0  0000000000000000   A       0     0     1
  [ 9] __versions        PROGBITS         0000000000000000  00000190
       0000000000000100  0000000000000000   A       0     0     8
  [10] .gnu.linkonce.this_module PROGBITS  0000000000000000  00000300
       0000000000000240  0000000000000000  WA       0     0    64
  [11] .rela.gnu.li[...] RELA             0000000000000000  00001088
       0000000000000030  0000000000000018   I      23    10     8
  ...

주요 ELF 섹션

섹션 이름 타입 설명
.text PROGBITS 모듈의 일반 실행 코드. 로딩 후에도 메모리에 유지됩니다.
.init.text PROGBITS __init으로 표시된 초기화 함수 코드. module_init() 실행 후 메모리에서 해제됩니다.
.exit.text PROGBITS __exit으로 표시된 종료 함수 코드. 빌트인 시 컴파일에서 제외될 수 있습니다.
.rodata PROGBITS 읽기 전용 데이터 (문자열 리터럴, const 변수 등).
.data PROGBITS 초기화된 전역/정적 변수.
.bss NOBITS 초기화되지 않은 전역/정적 변수. 파일에 공간을 차지하지 않으며 로딩 시 0으로 초기화됩니다.
.modinfo PROGBITS 모듈 메타데이터 (MODULE_LICENSE, MODULE_AUTHOR 등)가 키=값 형태로 저장됩니다.
.symtab SYMTAB 심볼 테이블. 모듈이 정의하고 참조하는 모든 심볼 정보를 담고 있습니다.
.strtab STRTAB 문자열 테이블. 심볼 이름 등의 문자열을 저장합니다.
__versions PROGBITS CONFIG_MODVERSIONS 활성 시, 참조하는 외부 심볼의 CRC 체크섬 목록입니다.
.gnu.linkonce.this_module PROGBITS struct module 인스턴스를 담고 있습니다. 모듈 이름, init/exit 함수 포인터 등이 포함됩니다.
.rela.* RELA 재배치 엔트리. 커널 로더가 심볼 주소를 확정할 때 사용합니다.

.modinfo 섹션 내용

.modinfo 섹션에는 MODULE_LICENSE(), MODULE_AUTHOR() 등의 매크로로 선언한 메타데이터가 널(null) 종료 문자열 형태로 저장됩니다. objdumpmodinfo로 확인할 수 있습니다:

# .modinfo 섹션 원시 내용 덤프
$ objdump -s -j .modinfo hello.ko
hello.ko:     file format elf64-x86-64

Contents of section .modinfo:
 0000 6c696365 6e73653d 47504c00 61757468  license=GPL.auth
 0010 6f723d59 6f757220 4e616d65 00646573  or=Your Name.des
 0020 63726970 74696f6e 3d412073 696d706c  cription=A simpl
 0030 65204865 6c6c6f20 576f726c 64206b65  e Hello World ke
 0040 726e656c 206d6f64 756c6500 76657273  rnel module.vers
 0050 696f6e3d 312e3000 73726376 65727369  ion=1.0.srcversi
 0060 6f6e3d41 42434445 46313233 34353637  on=ABCDEF1234567

# modinfo 명령으로 구조화된 출력
$ modinfo hello.ko
filename:       /lib/modules/6.1.0/extra/hello.ko
license:        GPL
author:         Your Name
description:    A simple Hello World kernel module
version:        1.0
srcversion:     ABCDEF1234567890ABCDEF1
depends:
retpoline:      Y
name:           hello
vermagic:       6.1.0 SMP preempt mod_unload
💡

vermagic 문자열은 모듈이 빌드된 커널 버전과 설정 옵션을 담고 있습니다. 커널은 로딩 시 이 문자열을 비교하여 호환되지 않는 모듈의 로딩을 거부합니다. modprobe --force로 강제 로드할 수 있지만 시스템 불안정을 초래할 수 있어 권장되지 않습니다.

__versions 섹션과 MODVERSIONS CRC

CONFIG_MODVERSIONS가 활성화된 커널에서는 심볼 이름에 CRC 체크섬이 부가됩니다. 모듈이 참조하는 각 외부 심볼에 대해 빌드 시 계산된 CRC가 __versions 섹션에 저장되며, 로딩 시 커널에 등록된 CRC와 비교합니다. 불일치 시 로딩이 거부됩니다:

# __versions 섹션 확인: 심볼별 CRC 체크섬
$ objdump -s -j __versions hello.ko
hello.ko:     file format elf64-x86-64

Contents of section __versions:
 0000 3fb8e94c 00000000 6d6f6475 6c655f6c  ?..L....module_l
 0010 61796f75 74000000 00000000 00000000  ayout...........
 ...
 0040 a815b42a 00000000 7072696e 746b0000  ...*....printk..

# CRC 불일치 시 dmesg 오류 메시지 예시
$ dmesg | grep disagrees
hello: disagrees about version of symbol printk

.gnu.linkonce.this_module 섹션

이 섹션은 모듈을 대표하는 struct module 인스턴스를 담고 있습니다. 커널 빌드 시스템이 자동으로 생성하는 <모듈이름>.mod.c 파일에서 이 구조체가 정의됩니다:

/* 커널 빌드 시스템이 자동 생성하는 hello.mod.c (간략화) */
#include <linux/module.h>

__visible struct module __this_module
__used __section(".gnu.linkonce.this_module") = {
    .name = KBUILD_MODNAME,     /* "hello" */
    .init = init_module,         /* → hello_init */
#ifdef CONFIG_MODULE_UNLOAD
    .exit = cleanup_module,      /* → hello_exit */
#endif
    .arch = MODULE_ARCH_INIT,
};
⚠️

.gnu.linkonce.this_module 섹션의 struct module은 모듈 로딩의 핵심입니다. 커널은 이 구조체에서 .init.exit 함수 포인터를 읽어 초기화/종료 함수를 호출합니다. module_init()/module_exit() 매크로는 실제로 init_module/cleanup_module 심볼을 생성하며, 빌드 시스템이 이를 struct module에 연결합니다.

ELF .ko 파일 레이아웃 다이어그램

ELF .ko 파일 내부 섹션 레이아웃 ELF Header (ET_REL) .text (일반 실행 코드) .init.text (초기화 코드 — 로드 후 해제) .exit.text (종료 코드) .rodata (읽기 전용 데이터) .data .bss .modinfo (license, author, vermagic ...) __versions (MODVERSIONS CRC 테이블) .gnu.linkonce.this_module (struct module) .symtab .strtab .rela.* (재배치 엔트리들) Section Header Table 로딩 후에도 유지 (core_layout) init 완료 후 해제 (init_layout) modinfo 명령이 읽는 메타데이터 심볼 버전 호환성 검증용 CRC 커널이 이 struct를 기반으로 모듈 관리 ET_REL vs ET_DYN .ko → ET_REL (재배치 가능 오브젝트) .so → ET_DYN (동적 공유 오브젝트) 커널 로더가 직접 재배치 수행 코드 데이터 메타데이터
ELF .ko 파일의 내부 섹션 레이아웃 — ET_REL 타입으로 커널 로더가 직접 재배치를 수행합니다

ELF 분석 명령어

모듈 .ko 파일을 분석할 때 유용한 명령어들입니다:

# ELF 헤더 확인 — Type이 REL (Relocatable)인지 확인
$ readelf -h hello.ko | grep Type
  Type:                              REL (Relocatable file)

# 심볼 테이블 확인 — 모듈이 정의하고 참조하는 심볼 목록
$ readelf -s hello.ko
Symbol table '.symtab' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
    ...
    11: 0000000000000000    32 FUNC    LOCAL  DEFAULT    3 hello_init
    12: 0000000000000000    16 FUNC    LOCAL  DEFAULT    5 hello_exit
    13: 0000000000000000   576 OBJECT  GLOBAL DEFAULT   10 __this_module
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printk

# 재배치 엔트리 확인 — 로딩 시 패치해야 할 위치
$ readelf -r hello.ko
Relocation section '.rela.init.text' at offset 0x1040 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000005  000e00000002 R_X86_64_PC32     0000000000000000 printk - 4

# 특정 섹션의 16진수 덤프
$ objdump -s -j .modinfo hello.ko
$ objdump -s -j __versions hello.ko

# 디스어셈블 — .init.text 섹션의 머신 코드 확인
$ objdump -d -j .init.text hello.ko
ℹ️

readelfobjdumpbinutils 패키지에 포함되어 있습니다. 크로스 컴파일 환경에서는 aarch64-linux-gnu-readelf와 같이 타겟 아키텍처용 도구를 사용해야 합니다.

모듈 메모리 레이아웃 (Module Memory Layout in Kernel)

커널 모듈이 로드되면 커널 가상 주소 공간의 특정 영역에 배치됩니다. 이 영역은 아키텍처마다 다르며, module_alloc() 함수가 모듈용 메모리를 할당합니다. 모듈 코드는 커널 텍스트 근처에 위치해야 하는데, 이는 상대 점프 명령어의 오프셋 범위 제한 때문입니다.

아키텍처별 모듈 영역

아키텍처 모듈 가상 주소 범위 매크로 비고
x86_64 0xffffffffa00000000xfffffffffeffffff MODULES_VADDR, MODULES_END 커널 텍스트(0xffffffff81000000) 근처 약 1.5GB 영역
ARM64 커널 텍스트 근처 128MB 범위 MODULES_VADDR, MODULES_END vmalloc 유사 영역, ADRP 명령어 범위 내
ARM (32-bit) 0x7f000000 부근 MODULES_VADDR, MODULES_END 커널 시작 주소 바로 아래 16MB 영역
RISC-V 커널 텍스트 근처 2GB 범위 MODULES_VADDR, MODULES_END auipc 명령어의 ±2GB 오프셋 범위 내
/* arch/x86/include/asm/pgtable_64_types.h */
#define MODULES_VADDR    (__START_KERNEL_map + KERNEL_IMAGE_SIZE)
#define MODULES_END      (0xffffffffff000000UL)
#define MODULES_LEN      (MODULES_END - MODULES_VADDR)

/* kernel/module/main.c — 모듈 메모리 할당 */
void *module_alloc(unsigned long size)
{
    return __vmalloc_node_range(size, 1,
            MODULES_VADDR, MODULES_END,
            GFP_KERNEL, PAGE_KERNEL_EXEC,
            VM_FLUSH_RESET_PERMS | VM_DEFER_KMEMLEAK,
            NUMA_NO_NODE, __builtin_return_address(0));
}

Core 레이아웃 vs Init 레이아웃

커널은 모듈의 ELF 섹션을 두 가지 영역으로 분류합니다:

/* include/linux/module.h — struct module (핵심 필드, 간략화) */
struct module {
    enum module_state state;          /* COMING, LIVE, GOING */
    char name[MODULE_NAME_LEN];       /* 모듈 이름 */

    /* Core 레이아웃: 모듈 생존 기간 동안 유지 */
    struct module_layout core_layout;
    /* Init 레이아웃: init 완료 후 해제 */
    struct module_layout init_layout;

    /* 초기화/종료 함수 포인터 */
    int  (*init)(void);
    void (*exit)(void);

    struct mod_arch_specific arch;     /* 아키텍처 특화 데이터 */
    atomic_t refcnt;                   /* 참조 카운트 */
    /* ... 더 많은 필드 생략 ... */
};
⚠️

Init 섹션의 메모리가 해제된 후에도 struct module은 core 레이아웃에 유지됩니다. 따라서 __init 함수를 초기화 이후에 호출하면 해제된 메모리에 접근하게 되어 커널 패닉이 발생합니다. __init/__initdata 어노테이션을 정확하게 사용하는 것이 중요합니다.

CONFIG_STRICT_MODULE_RWX: 모듈 메모리 보호

CONFIG_STRICT_MODULE_RWX(구 CONFIG_DEBUG_SET_MODULE_RONX)가 활성화되면, 커널은 모듈 로딩 완료 후 메모리 영역의 권한을 엄격하게 설정합니다:

영역 초기 권한 init 완료 후 권한 설명
.text RWX R-X 코드: 읽기+실행만 허용, 쓰기 불가
.rodata RW- R-- 읽기 전용 데이터: 읽기만 허용
.data, .bss RW- RW- 읽기+쓰기 (실행 불가 — NX)
.init.text RWX 해제됨 Init 완료 후 메모리 반환
# /proc/modules로 로드된 모듈 확인
# 형식: name size refcount deps state address
$ cat /proc/modules
hello 16384 0 - Live 0xffffffffa0000000
ext4 802816 1 - Live 0xffffffffa0100000
jbd2 131072 1 ext4, Live 0xffffffffa0200000

# /sys/module/<name>/sections/에서 섹션 주소 확인 (root 필요)
$ sudo cat /sys/module/hello/sections/.text
0xffffffffa0002000
$ sudo cat /sys/module/hello/sections/.data
0xffffffffa0004000
$ sudo cat /sys/module/hello/sections/.bss
0xffffffffa0005000

# /proc/kallsyms에서 모듈 심볼 주소 확인
$ sudo grep hello /proc/kallsyms
ffffffffa0002000 t hello_init    [hello]
ffffffffa0002020 t hello_exit    [hello]
ffffffffa0003000 d __this_module [hello]
💡

/sys/module/<name>/sections/ 디렉토리는 기본적으로 root만 읽을 수 있습니다. 이는 KASLR(Kernel Address Space Layout Randomization) 보안을 위한 것으로, 모듈의 정확한 주소가 노출되면 공격자가 이를 악용할 수 있기 때문입니다. /proc/kallsymskptr_restrict sysctl 설정에 따라 비root 사용자에게는 주소가 0으로 표시됩니다.

모듈 메모리 레이아웃 다이어그램

커널 가상 주소 공간 — 모듈 메모리 레이아웃 (x86_64) 커널 가상 주소 User Space 0x0 – 0x7fffffffffff Kernel Text 0xffffffff81000000 Module Region MODULES_VADDR ~1.5GB 0xffffffffa0000000 vmalloc area Direct Mapping 0xffffffffffffffff Fixmap / vsyscall 모듈 내부 메모리 레이아웃 struct module (모듈 메타데이터) core_layout (모듈 수명 동안 유지) .text (R-X) — 실행 코드 .rodata (R--) — 읽기 전용 데이터 .data (RW-) — 초기화된 데이터 .bss (RW-) — 미초기화 데이터 .symtab (심볼 테이블 사본) init_layout (init 완료 후 해제) .init.text (RWX→해제) — 초기화 코드 .init.data — 초기화 전용 데이터 ↑ module_init() 완료 후 해제 ↑ 확인 경로 /proc/modules /proc/kallsyms /sys/module/*/ sections/ root 권한 필요 module_alloc() vmalloc 기반 할당
커널 가상 주소 공간에서의 모듈 메모리 레이아웃 — core_layout은 유지되고 init_layout은 초기화 후 해제됩니다
🚫

CONFIG_STRICT_MODULE_RWX가 활성화된 상태에서 모듈 코드 영역에 쓰기를 시도하면 페이지 폴트가 발생합니다. 이는 보안 강화를 위한 것으로, 공격자가 모듈 코드를 변조하는 것을 방지합니다. 모듈에서 자체 수정 코드(self-modifying code)가 필요한 경우, text_poke() 등의 커널 API를 사용해야 합니다.

Hello World 모듈 작성 (Writing Your First Module)

기본 모듈 구조 (Basic Module Skeleton)

가장 간단한 커널 모듈은 초기화 함수와 종료 함수, 그리고 라이선스 선언으로 구성됩니다. 아래는 전형적인 "Hello World" 커널 모듈입니다:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

/* 모듈 초기화 함수: insmod 시 호출 */
static int __init hello_init(void)
{
    pr_info("Hello, Kernel Module!\\n");
    return 0;  /* 0: 성공, 음수: 실패 (errno) */
}

/* 모듈 종료 함수: rmmod 시 호출 */
static void __exit hello_exit(void)
{
    pr_info("Goodbye, Kernel Module!\\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");

코드의 핵심 요소를 살펴보겠습니다:

⚠️

module_init()에서 등록한 함수가 0이 아닌 값을 반환하면 모듈 로딩이 실패합니다. 반드시 적절한 에러 코드(-ENOMEM, -EINVAL 등)를 반환하고, 이미 할당한 리소스는 모두 해제해야 합니다.

필수 매크로 (Essential Macros)

커널 모듈에는 메타데이터를 제공하는 여러 매크로가 있습니다. 이 정보는 .modinfo 섹션에 저장되어 modinfo 명령으로 확인할 수 있습니다:

매크로 설명 예시
MODULE_LICENSE() 라이선스 선언 (필수). GPL 심볼 접근 권한에 영향 "GPL", "GPL v2", "Dual MIT/GPL"
MODULE_AUTHOR() 모듈 작성자 "Name <email>"
MODULE_DESCRIPTION() 모듈 설명 "My driver module"
MODULE_VERSION() 모듈 버전 "1.0.0"
MODULE_ALIAS() 모듈 별칭 (자동 로딩용) "char-major-10-200"
MODULE_DEVICE_TABLE() 지원 하드웨어 ID 테이블 PCI, USB, OF 디바이스 매칭용
⚠️

MODULE_LICENSE("GPL")을 선언하지 않으면 커널은 "Tainted" 상태가 되며, GPL 전용으로 내보내진 심볼(EXPORT_SYMBOL_GPL)에 접근할 수 없습니다. 커널 API에는 EXPORT_SYMBOLEXPORT_SYMBOL_GPL이 혼재하므로, 사용하는 심볼 집합에 따라 실제 제약이 달라집니다.

실제 사용 예제

에러 처리를 포함한 실무 패턴

프로덕션 환경에서는 초기화 중 에러가 발생할 수 있으므로 적절한 에러 처리가 필수입니다:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>    /* kmalloc, kfree */

static char *data = NULL;

static int __init example_init(void)
{
    /* 1. 메모리 할당 */
    data = kmalloc(1024, GFP_KERNEL);
    if (!data) {
        pr_err("Failed to allocate memory\\n");
        return -ENOMEM;  /* 에러 코드 반환 */
    }

    /* 2. 초기화 작업 */
    sprintf(data, "Module loaded successfully");
    pr_info("Example module: %s\\n", data);

    return 0;  /* 성공 */
}

static void __exit example_exit(void)
{
    /* 3. 리소스 정리 (메모리 해제) */
    if (data) {
        kfree(data);
        data = NULL;
    }
    pr_info("Example module unloaded\\n");
}

module_init(example_init);
module_exit(example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your@email.com>");
MODULE_DESCRIPTION("Example with proper error handling");
✅ 실행 결과:
$ sudo insmod example.ko
$ dmesg | tail -2
[12345.678] Example module: Module loaded successfully
$ sudo rmmod example
$ dmesg | tail -1
[12350.123] Example module unloaded
  

흔한 실수: 초기화 에러 처리

❌ 잘못된 방법
static int __init bad_init(void)
{
    char *buf1, *buf2;

    buf1 = kmalloc(100, GFP_KERNEL);
    buf2 = kmalloc(200, GFP_KERNEL);

    if (!buf2) {
        return -ENOMEM;
        /* 문제: buf1 메모리 누수! */
    }

    return 0;
}

문제점: buf2 할당 실패 시 buf1을 해제하지 않아 메모리 누수 발생

✅ 올바른 방법
static int __init good_init(void)
{
    char *buf1, *buf2;

    buf1 = kmalloc(100, GFP_KERNEL);
    if (!buf1)
        return -ENOMEM;

    buf2 = kmalloc(200, GFP_KERNEL);
    if (!buf2) {
        kfree(buf1);  /* 정리! */
        return -ENOMEM;
    }

    return 0;
}

이유: 에러 발생 시 이미 할당한 리소스를 모두 해제해야 메모리 누수 방지

디버깅 팁: CONFIG_DEBUG_KMEMLEAK=y 커널 옵션을 활성화하면 메모리 누수를 자동 탐지할 수 있습니다. /sys/kernel/debug/kmemleak 파일을 확인하세요.

완전한 빌드 가능 예제

실제로 컴파일하고 테스트할 수 있는 완전한 모듈 코드입니다:

빌드 및 테스트:
# 빌드
make

# 로드
sudo insmod hello_complete.ko name="Kernel" count=3

# 로그 확인
dmesg | tail -5

# 언로드
sudo rmmod hello_complete

# 정리
make clean
  
/* hello_complete.c - 완전한 Hello World 모듈 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

/* 모듈 파라미터 */
static char *name = "World";
module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "Name to greet");

static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of greetings");

static int __init hello_init(void)
{
    int i;

    /* 파라미터 유효성 검사 */
    if (count <= 0 || count > 10) {
        pr_err("Invalid count: %d (must be 1-10)\\n", count);
        return -EINVAL;
    }

    pr_info("Hello Complete Module loaded\\n");
    pr_info("Parameters: name=%s, count=%d\\n", name, count);

    for (i = 0; i < count; i++) {
        pr_info("[%d/%d] Hello, %s!\\n", i + 1, count, name);
    }

    return 0;
}

static void __exit hello_exit(void)
{
    pr_info("Goodbye, %s!\\n", name);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("MINZKN <minzkn@minzkn.com>");
MODULE_DESCRIPTION("Complete Hello World with parameters");
MODULE_VERSION("1.0");

모듈 Makefile 작성 (Module Makefile)

Out-of-tree 빌드 (Out-of-tree Build)

커널 소스 트리 외부에서 모듈을 빌드하는 것을 out-of-tree 빌드라고 합니다. 이 방식은 별도의 프로젝트 디렉터리에서 모듈을 개발할 때 사용합니다.

# 최소한의 out-of-tree 모듈 Makefile
obj-m += hello.o

# 현재 실행 중인 커널의 빌드 디렉터리
KDIR := /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

각 요소의 의미는 다음과 같습니다:

빌드 및 로드 과정:

# 모듈 빌드
$ make

# 빌드 결과 확인
$ ls -la hello.ko
-rw-r--r-- 1 user user 6144 Jan 15 10:30 hello.ko

# 모듈 로드
$ sudo insmod hello.ko

# 커널 로그 확인
$ dmesg | tail -1
[12345.678] Hello, Kernel Module!

# 모듈 언로드
$ sudo rmmod hello

# 언로드 확인
$ dmesg | tail -1
[12345.789] Goodbye, Kernel Module!

다중 파일 모듈 (Multi-file Module)

하나의 모듈이 여러 소스 파일로 구성될 수 있습니다. 이 경우 Makefile에서 <모듈명>-objs를 사용합니다:

# 다중 파일 모듈: mydriver.ko = main.o + hw.o + utils.o
obj-m += mydriver.o
mydriver-objs := main.o hw.o utils.o

KDIR := /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

mydriver-objs에 나열된 오브젝트 파일들이 링크되어 하나의 mydriver.ko가 생성됩니다. 주의할 점은 obj-m에 지정된 이름과 동일한 이름의 .c 파일이 있으면 안 된다는 것입니다 (예: mydriver.c가 존재하면 충돌).

모듈 관리 명령어 (Module Management Commands)

insmod / rmmod

insmodrmmod는 모듈을 직접 로드/언로드하는 저수준 명령입니다.

# insmod: 모듈 파일 경로를 직접 지정하여 로드
$ sudo insmod ./hello.ko

# 파라미터 전달
$ sudo insmod ./hello.ko myint=42 mystr="test"

# rmmod: 모듈 이름으로 언로드 (.ko 확장자 불필요)
$ sudo rmmod hello

# 강제 언로드 (위험! 커널 패닉 가능)
$ sudo rmmod -f hello
⚠️

insmod는 모듈 의존성을 자동으로 해결하지 않습니다. 의존하는 모듈이 먼저 로드되어 있지 않으면 Unknown symbol 에러가 발생합니다. 의존성 자동 해결이 필요하면 modprobe를 사용하세요.

modprobe

modprobe/lib/modules/$(uname -r)/ 아래의 모듈을 검색하고 의존성을 자동으로 해결합니다. modules.dep 파일(depmod가 생성)을 참조하여 필요한 모듈을 순서대로 로드합니다.

# 모듈 로드 (의존성 자동 해결)
$ sudo modprobe e1000e

# 모듈 언로드 (의존하는 모듈도 함께 언로드)
$ sudo modprobe -r e1000e

# dry-run: 실제 로드하지 않고 어떤 일이 일어날지 확인
$ modprobe -n -v e1000e

# 의존성 데이터베이스 갱신
$ sudo depmod -a

# 특정 모듈의 의존성 확인
$ modprobe --show-depends ext4

/etc/modprobe.d/ 디렉터리에 설정 파일을 두면 모듈 로드 시 옵션, 별칭, 블랙리스트를 지정할 수 있습니다:

# /etc/modprobe.d/custom.conf

# 모듈 로드 시 기본 파라미터 설정
options snd_hda_intel power_save=1

# 모듈 별칭 지정
alias eth0 e1000e

# 특정 모듈 로드 차단 (블랙리스트)
blacklist nouveau

lsmod / modinfo

lsmod는 현재 로드된 모듈 목록을 표시하고, modinfo는 모듈 파일의 메타데이터를 출력합니다.

# 로드된 모듈 목록 (Module, Size, Used by)
$ lsmod
Module                  Size  Used by
hello                  16384  0
e1000e                286720  0
ext4                  819200  1

# 내부적으로 /proc/modules 파일을 읽음
$ cat /proc/modules

# 모듈 상세 정보 확인
$ modinfo hello.ko
filename:       /home/user/hello.ko
license:        GPL
author:         Your Name
description:    A simple Hello World kernel module
version:        1.0
srcversion:     ABC123DEF456...
depends:
retpoline:      Y
name:           hello
vermagic:       6.1.0 SMP preempt mod_unload

# 특정 필드만 추출
$ modinfo -F depends ext4
mbcache,jbd2

시각적 개요: 모듈 명령어 비교

이 다이어그램의 목적: insmod, modprobe, lsmod의 차이점과 사용 시나리오를 한눈에 파악할 수 있습니다.
모듈 관리 명령어 워크플로우 insmod 저수준 직접 로드 ✗ 의존성 미해결 경로 필요 modprobe 고수준 자동 로드 ✓ 의존성 자동 해결 이름만 필요 Linux Kernel 로드된 모듈 직접 로드 자동 의존성 해결 lsmod 목록 조회 modinfo 정보 조회 실선: 로드/언로드 | 점선: 조회
핵심 포인트:
  1. insmod: 모듈 파일 경로를 직접 지정. 의존성 수동 해결 필요. 개발/디버깅 시 유용
  2. modprobe: 모듈 이름만 지정. modules.dep를 읽어 의존성 자동 해결. 프로덕션 환경 권장
  3. lsmod: /proc/modules를 읽어 현재 로드된 모듈 목록 표시
  4. modinfo: .ko 파일의 메타데이터(라이선스, 파라미터, 의존성) 출력

모듈 파라미터 (Module Parameters)

module_param 매크로 (module_param Macro)

module_param() 매크로를 사용하면 모듈 로드 시 사용자로부터 값을 전달받을 수 있습니다. 전달된 파라미터는 /sys/module/<모듈명>/parameters/에서도 확인하거나 변경할 수 있습니다.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>

/* 기본값이 있는 파라미터 선언 */
static int myint = 42;
static char *mystr = "default";
static bool debug_mode = false;

/* module_param(변수명, 타입, 권한)
 * 타입: int, uint, long, ulong, short, ushort,
 *       charp (char *), bool, invbool
 * 권한: 0 = sysfs에 노출하지 않음
 *       S_IRUGO (0444) = 읽기 전용
 *       S_IRUGO | S_IWUSR (0644) = root 쓰기 가능
 */
module_param(myint, int, 0644);
MODULE_PARM_DESC(myint, "An integer parameter (default=42)");

module_param(mystr, charp, 0444);
MODULE_PARM_DESC(mystr, "A string parameter");

module_param(debug_mode, bool, 0644);
MODULE_PARM_DESC(debug_mode, "Enable debug mode (default=false)");

static int __init param_demo_init(void)
{
    pr_info("myint=%d, mystr=%s, debug=%d\\n", myint, mystr, debug_mode);
    return 0;
}

static void __exit param_demo_exit(void)
{
    pr_info("param_demo unloaded\\n");
}

module_init(param_demo_init);
module_exit(param_demo_exit);
MODULE_LICENSE("GPL");
# 파라미터를 지정하여 로드
$ sudo insmod param_demo.ko myint=100 mystr="hello" debug_mode=1

# sysfs에서 파라미터 확인
$ cat /sys/module/param_demo/parameters/myint
100

# 런타임에 파라미터 변경 (권한이 0644일 때)
$ sudo sh -c 'echo 200 > /sys/module/param_demo/parameters/myint'

module_param_array (배열 파라미터)

module_param_array()를 사용하면 배열 형태의 파라미터를 전달받을 수 있습니다. 쉼표로 구분된 값을 전달하며, 실제로 전달된 요소의 개수를 별도 변수에 저장합니다.

static int ports[4] = { 0, 0, 0, 0 };
static int num_ports = 0;

/* module_param_array(변수명, 타입, &개수변수, 권한) */
module_param_array(ports, int, &num_ports, 0444);
MODULE_PARM_DESC(ports, "An array of I/O port numbers (max 4)");

static int __init array_demo_init(void)
{
    int i;
    pr_info("Received %d ports:\\n", num_ports);
    for (i = 0; i < num_ports; i++)
        pr_info("  port[%d] = 0x%x\\n", i, ports[i]);
    return 0;
}
# 배열 파라미터 전달 (쉼표로 구분)
$ sudo insmod array_demo.ko ports=0x3f8,0x2f8,0x3e8

모듈 라이프사이클 다이어그램 (Module Lifecycle Diagram)

커널 모듈은 로드부터 언로드까지 명확한 상태 전이를 거칩니다. 아래 다이어그램은 모듈의 전체 라이프사이클을 보여줍니다.

Unloaded (미로드) MODULE_STATE_COMING MODULE_STATE_LIVE MODULE_STATE_GOING Freed (해제됨) insmod / modprobe init() 성공 (return 0) rmmod / modprobe -r exit() 완료, 메모리 해제 ELF 파싱, 심볼 해석 재배치, 섹션 매핑 정상 동작 상태 sysfs 노출 exit() 호출 리소스 정리 init() 실패
커널 모듈 라이프사이클 - Unloaded에서 Live, 다시 해제까지의 상태 전이

모듈 로딩 과정 다이어그램 (Module Loading Process)

insmod 또는 modprobe를 실행하면 커널 내부에서 복잡한 로딩 과정이 진행됩니다. 아래 다이어그램은 init_module() 시스템 콜부터 모듈 초기화까지의 내부 흐름을 보여줍니다.

User Space insmod hello.ko (read ELF, call init_module syscall) init_module() / finit_module() syscall 1. load_module(): ELF 헤더 검증, 섹션 파싱 2. 메모리 할당 (module_alloc), 섹션 복사 3. 심볼 해석 (resolve_symbol), 재배치 (apply_relocations) 4. 모듈 서명 검증 (CONFIG_MODULE_SIG 활성 시) 5. sysfs 등록, /proc/modules 엔트리 생성 6. module_init() 콜백 호출
커널 모듈 로딩 내부 과정 - init_module 시스템 콜부터 모듈 초기화 함수 호출까지
💡

finit_module() 시스템 콜은 파일 디스크립터를 받아 커널이 직접 파일을 읽는 경로로 널리 사용됩니다. 이 방식은 사용자 공간에서 전체 .ko 파일을 메모리에 올리는 부담을 줄일 수 있습니다.

참조 카운팅과 모듈 사용 추적 (Reference Counting & Module Usage Tracking)

커널 모듈은 다른 커널 서브시스템이나 사용자 프로세스에 의해 사용될 수 있습니다. 모듈이 사용 중인 상태에서 언로드되면 커널 패닉이 발생하므로, 커널은 참조 카운팅(reference counting) 메커니즘으로 모듈의 사용 여부를 추적합니다. struct modulerefcnt 필드가 이 역할을 담당합니다.

참조 카운트 API

함수 동작 실패 가능 용도
try_module_get(mod) refcnt 증가 (조건부) 예 — MODULE_STATE_GOING이면 실패 모듈 사용 시작 전 호출. 가장 안전한 방법.
module_put(mod) refcnt 감소 아니오 모듈 사용 완료 후 호출. try_module_get과 쌍으로 사용.
__module_get(mod) refcnt 무조건 증가 아니오 — 호출자가 이미 참조를 보유해야 함 이미 참조를 보유한 상태에서 추가 참조 획득. 주의 필요.
module_refcount(mod) 현재 refcnt 값 반환 아니오 디버깅용. 반환 후 값이 변할 수 있으므로 의사 결정에 사용 금지.
/* include/linux/module.h — 참조 카운트 핵심 함수들 */

/* 안전한 참조 획득: 모듈이 언로드 중이면 false 반환 */
static inline bool try_module_get(struct module *module)
{
    if (module) {
        if (likely(module->state == MODULE_STATE_LIVE)) {
            atomic_inc(&module->refcnt);
            return true;
        }
        return false;  /* MODULE_STATE_GOING — 언로드 진행 중 */
    }
    return true;  /* NULL(빌트인)이면 항상 성공 */
}

/* 참조 반환 */
static inline void module_put(struct module *module)
{
    if (module) {
        if (atomic_dec_if_positive(&module->refcnt) == 0)
            wake_up_process(module->waiter);  /* rmmod 대기자 깨움 */
    }
}

/* 무조건 참조 획득: 호출자가 이미 참조를 보유해야 안전 */
static inline void __module_get(struct module *module)
{
    if (module)
        atomic_inc(&module->refcnt);
}
⚠️

__module_get()은 모듈 상태를 검사하지 않으므로, 호출 시점에 모듈이 이미 MODULE_STATE_GOING이면 위험합니다. 항상 try_module_get()을 우선 사용하고, __module_get()은 이미 참조를 보유한 것이 확실한 경우에만 사용하세요.

lsmod와 참조 카운트 확인

lsmod 출력의 세 번째 컬럼이 참조 카운트이며, 네 번째 컬럼은 이 모듈을 사용하는 모듈 목록입니다:

# lsmod 출력에서 참조 카운트 확인
$ lsmod
Module                  Size  Used by
hello                  16384  0          # refcnt=0, 언로드 가능
ext4                  802816  1          # refcnt=1, 마운트된 파일시스템 사용 중
jbd2                  131072  1 ext4     # refcnt=1, ext4가 의존
snd_hda_intel          57344  3          # refcnt=3, 오디오 스트림 3개 사용 중

# /sys/module을 통한 참조 카운트 직접 확인
$ cat /sys/module/hello/refcnt
0
$ cat /sys/module/ext4/refcnt
1

# refcnt > 0일 때 rmmod 시도 → 실패
$ sudo rmmod ext4
rmmod: ERROR: Module ext4 is in use

# refcnt == 0일 때 rmmod → 성공
$ sudo rmmod hello

file_operations와 .owner = THIS_MODULE

캐릭터 디바이스 드라이버에서 가장 흔한 참조 카운트 패턴은 struct file_operations.owner 필드를 THIS_MODULE로 설정하는 것입니다. VFS가 open() 시 자동으로 try_module_get(fops->owner)를 호출하고, close()module_put(fops->owner)를 호출합니다:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static int mydev_open(struct inode *inode, struct file *filp)
{
    pr_info("mydev: opened\n");
    /* VFS가 이미 try_module_get(THIS_MODULE)을 호출함 */
    return 0;
}

static int mydev_release(struct inode *inode, struct file *filp)
{
    pr_info("mydev: closed\n");
    /* VFS가 module_put(THIS_MODULE)을 호출함 */
    return 0;
}

static ssize_t mydev_read(struct file *filp, char __user *buf,
                          size_t count, loff_t *ppos)
{
    /* 읽기 구현 */
    return 0;
}

/* .owner = THIS_MODULE이 핵심! */
static const struct file_operations mydev_fops = {
    .owner   = THIS_MODULE,    /* ← VFS가 자동으로 참조 카운트 관리 */
    .open    = mydev_open,
    .release = mydev_release,
    .read    = mydev_read,
};
💡

.owner = THIS_MODULE을 설정하지 않으면 파일이 열려 있는 동안에도 모듈이 언로드될 수 있어, 이후 파일 연산 시 해제된 코드를 실행하게 됩니다. 이는 커널 패닉의 대표적인 원인 중 하나입니다. 모든 file_operations, proc_ops, sysfs_ops 등에 반드시 .owner = THIS_MODULE을 설정하세요.

수동 참조 카운트 관리 패턴

file_operations.owner가 아닌, 직접 참조 카운트를 관리해야 하는 경우도 있습니다. 예를 들어, 타이머 콜백이나 워크큐에서 모듈 함수를 사용하는 경우입니다:

/* 타이머에서 모듈 함수를 콜백으로 사용하는 예 */
static struct timer_list my_timer;

static void my_timer_callback(struct timer_list *t)
{
    pr_info("Timer fired!\n");
    /* 작업 완료 후 참조 반환 */
    module_put(THIS_MODULE);
}

static int start_timer(void)
{
    /* 타이머 등록 전 참조 획득 */
    if (!try_module_get(THIS_MODULE)) {
        pr_err("Module is being unloaded\n");
        return -ENODEV;
    }

    timer_setup(&my_timer, my_timer_callback, 0);
    mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
    return 0;
}

static void stop_timer(void)
{
    /* del_timer_sync는 콜백 완료를 보장 */
    if (del_timer_sync(&my_timer)) {
        /* 타이머가 아직 발화하지 않았으면 직접 참조 반환 */
        module_put(THIS_MODULE);
    }
}

rmmod와 open()의 경쟁 조건

rmmodopen()이 동시에 발생하면 경쟁 조건이 생길 수 있습니다. 커널은 이를 다음과 같이 처리합니다:

ℹ️

이 메커니즘은 CONFIG_MODULE_UNLOAD가 활성화된 경우에만 동작합니다. 이 옵션이 비활성화되면 모듈을 언로드할 수 없으며, 참조 카운트도 관리되지 않습니다. 대부분의 배포판 커널은 이 옵션을 활성화합니다.

참조 카운팅 흐름 다이어그램

모듈 참조 카운팅 흐름 struct module refcnt (atomic_t) open() 호출 VFS: try_module_get(fops->owner) refcnt++ (증가) close() 호출 VFS: module_put(fops->owner) refcnt-- (감소) rmmod 요청 state → MODULE_STATE_GOING refcnt == 0? Yes exit() 호출 모듈 메모리 해제 No 대기 또는 -EBUSY "Module is in use" .owner = THIS_MODULE → VFS가 open/close 시 자동으로 refcnt 관리
모듈 참조 카운팅 흐름 — open()으로 증가, close()로 감소, rmmod는 refcnt==0일 때만 성공
🚫

try_module_get(THIS_MODULE)으로 자기 자신의 참조를 획득하는 패턴은 모듈이 스스로를 언로드 불가능하게 만드는 것이므로 신중하게 사용해야 합니다. 반드시 대응하는 module_put(THIS_MODULE)이 있어야 하며, 에러 경로에서도 빠짐없이 호출되어야 합니다. 참조 누수(leak)가 발생하면 모듈을 영원히 언로드할 수 없게 됩니다.

모듈 의존성과 심볼 테이블 (Module Dependencies & Symbol Table)

쉬운 설명: 이 섹션의 핵심 개념을 먼저 이해하고 시작하세요.

왜 모듈 간 의존성이 필요한가요?

일상 비유: 스마트폰 앱이 카메라 기능을 사용하려면 먼저 카메라 앱이 설치되어 있어야 하는 것처럼, 커널 모듈도 다른 모듈이 제공하는 기능을 사용할 수 있습니다.

예를 들어, ext4 파일시스템 모듈jbd2(저널링) 모듈의 기능이 필요합니다. 이런 관계를 "의존성(dependency)"이라고 합니다.

핵심 3단계 이해:

  1. 1단계: 공유 (EXPORT_SYMBOL)

    모듈 A가 함수를 만들고 "다른 모듈도 사용해!" 하고 공개합니다.
    EXPORT_SYMBOL(my_function)으로 선언

  2. 2단계: 사용 (extern 선언)

    모듈 B가 "모듈 A의 함수를 쓰고 싶어요" 하고 선언합니다.
    extern int my_function(...);

  3. 3단계: 순서대로 로드 (depmod + modprobe)

    모듈 A를 먼저 로드한 후, 모듈 B를 로드해야 합니다.
    modprobe가 자동으로 순서를 맞춰줌

실제 예시:

모듈 역할 의존성
ext4.ko ext4 파일시스템 jbd2.ko, mbcache.ko
bluetooth.ko 블루투스 스택 rfkill.ko, crypto.ko
hello.ko 우리가 만든 간단한 모듈 없음 (독립 모듈)
주의: 의존하는 모듈을 먼저 언로드하려고 하면 "Module is in use" 에러가 발생합니다. 의존하는 모듈부터 순서대로 언로드해야 합니다.

EXPORT_SYMBOL (상세)

커널 모듈이 다른 모듈에서 사용할 수 있는 함수나 변수를 내보내려면 EXPORT_SYMBOL() 또는 EXPORT_SYMBOL_GPL() 매크로를 사용합니다. 내보내진 심볼은 커널 심볼 테이블에 등록되어 다른 모듈에서 링크할 수 있게 됩니다.

더 깊게 보기: 이 섹션은 모듈 작성 관점의 핵심만 다룹니다. 커널 심볼 문서에서는 __ksymtab, modpost, Module.symvers, kallsyms, GPL/namespace/CRC 제약을 하나의 흐름으로 이어서 설명합니다.
/* 공유 함수를 제공하는 모듈: shared_lib.c */
#include <linux/module.h>

int shared_add(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL(shared_add);  /* 모든 모듈에서 접근 가능 */

int shared_multiply(int a, int b)
{
    return a * b;
}
EXPORT_SYMBOL_GPL(shared_multiply);  /* GPL 모듈만 접근 가능 */

MODULE_LICENSE("GPL");
/* 공유 심볼을 사용하는 모듈: consumer.c */
#include <linux/module.h>

/* 외부 모듈에서 내보낸 심볼 선언 */
extern int shared_add(int a, int b);
extern int shared_multiply(int a, int b);

static int __init consumer_init(void)
{
    pr_info("add(3,4)=%d, mul(3,4)=%d\\n",
            shared_add(3, 4),
            shared_multiply(3, 4));
    return 0;
}

static void __exit consumer_exit(void)
{
    pr_info("consumer unloaded\\n");
}

module_init(consumer_init);
module_exit(consumer_exit);
MODULE_LICENSE("GPL");

현재 커널의 심볼 테이블을 확인하는 방법:

# 모든 내보내기된 심볼 확인
$ cat /proc/kallsyms | grep shared_add

# 커널 심볼 테이블 (빌드 시 생성)
$ cat /boot/System.map-$(uname -r) | head -20

# 모듈별 내보내기 심볼
$ cat /lib/modules/$(uname -r)/modules.symbols | grep e1000

모듈 의존성 관리 (Dependency Management)

depmod 유틸리티는 /lib/modules/$(uname -r)/ 아래의 모든 .ko 파일을 스캔하여 심볼 의존성을 분석하고 modules.dep 파일을 생성합니다.

# 의존성 데이터베이스 재생성
$ sudo depmod -a

# 의존성 파일 내용 확인
$ cat /lib/modules/$(uname -r)/modules.dep | grep ext4
kernel/fs/ext4/ext4.ko: kernel/fs/mbcache.ko kernel/fs/jbd2/jbd2.ko

# 의존성 트리 시각화
$ modprobe --show-depends ext4
insmod /lib/modules/6.1.0/kernel/fs/jbd2/jbd2.ko
insmod /lib/modules/6.1.0/kernel/fs/mbcache.ko
insmod /lib/modules/6.1.0/kernel/fs/ext4/ext4.ko
ℹ️

모듈 간 순환 의존성(circular dependency)은 허용되지 않습니다. 모듈 A가 모듈 B의 심볼을 사용하고 모듈 B가 모듈 A의 심볼을 사용하는 구조는 로딩할 수 없습니다. 이런 경우 공통 심볼을 제3의 모듈로 분리해야 합니다.

모듈 스택킹과 계층 구조 (Module Stacking & Layered Architecture)

리눅스 커널 모듈은 독립적으로 동작하기보다는 계층적 구조(layered architecture)로 조직되는 경우가 많습니다. 하위 계층의 코어 모듈이 API를 export하고, 상위 계층의 위성 모듈(satellite module)이 해당 API를 사용하여 구체적인 구현을 등록하는 패턴을 모듈 스택킹(module stacking)이라 합니다.

이 패턴은 커널의 거의 모든 주요 서브시스템에서 활용됩니다. USB, 네트워크 필터링, 암호화 프레임워크, 파일시스템, 입력 장치 등 수백 개의 드라이버가 공통 프레임워크 위에 구축되어 있습니다.

모듈 스택킹 계층 아키텍처 다이어그램 사용자 공간 (User Space) mount / mkfs iptables / nft lsusb / usb-devices openssl / cryptsetup Layer 3: 구현 모듈 ext4.ko 파일시스템 nft_nat.ko NAT 규칙 nft_chain_*.ko 체인 타입 uas.ko USB Attached SCSI usb-storage.ko 대용량 저장장치 aes_generic.ko AES 알고리즘 ... Layer 2: 프레임워크 모듈 (API export) nf_tables.ko nftables 프레임워크 usbcore.ko USB 코어 프레임워크 crypto_algapi.ko Crypto 프레임워크 scsi_mod.ko SCSI 서브시스템 Layer 1: 커널 코어 인프라 (built-in / core modules) netfilter core VFS / Block Layer device model (kobject) memory / scheduler 하드웨어 (Hardware) ▲ 의존 방향 (depends on) — 상위 모듈이 하위 모듈의 심볼을 사용

스택킹 패턴: 코어 + 위성 모듈

모듈 스택킹의 핵심 패턴은 코어 모듈이 프레임워크 API를 export하고, 위성 모듈이 해당 API에 자신의 구현을 등록하는 것입니다. 아래는 라이브러리 모듈을 만들고 이를 소비하는 모듈의 예시입니다.

/* mylib.c — 라이브러리(코어) 모듈 */
#include <linux/module.h>
#include <linux/export.h>

static int internal_counter = 0;

int mylib_alloc_resource(const char *name, int flags)
{
    internal_counter++;
    pr_info("mylib: alloc '%s' flags=%d (count=%d)\n",
            name, flags, internal_counter);
    return internal_counter;
}
EXPORT_SYMBOL_GPL(mylib_alloc_resource);

void mylib_free_resource(int handle)
{
    internal_counter--;
    pr_info("mylib: free handle=%d (count=%d)\n",
            handle, internal_counter);
}
EXPORT_SYMBOL_GPL(mylib_free_resource);

static int __init mylib_init(void)
{
    pr_info("mylib: core library loaded\n");
    return 0;
}

static void __exit mylib_exit(void)
{
    pr_info("mylib: core library unloaded\n");
}

module_init(mylib_init);
module_exit(mylib_exit);
MODULE_LICENSE("GPL");
/* myclient.c — 소비자(위성) 모듈 */
#include <linux/module.h>

/* mylib에서 export한 함수 선언 */
extern int mylib_alloc_resource(const char *name, int flags);
extern void mylib_free_resource(int handle);

static int my_handle;

static int __init myclient_init(void)
{
    my_handle = mylib_alloc_resource("test-device", 0);
    pr_info("myclient: got handle %d\n", my_handle);
    return 0;
}

static void __exit myclient_exit(void)
{
    mylib_free_resource(my_handle);
    pr_info("myclient: unloaded\n");
}

module_init(myclient_init);
module_exit(myclient_exit);
MODULE_LICENSE("GPL");

modprobe 의존성 해석 순서

modprobemodules.dep 파일을 기반으로 의존성 트리를 분석하여 올바른 순서로 모듈을 로드합니다. 예를 들어 usb-storage를 로드하면:

# modprobe -v usb-storage 실행 시 내부 동작:
insmod /lib/modules/$(uname -r)/kernel/drivers/usb/core/usbcore.ko
insmod /lib/modules/$(uname -r)/kernel/drivers/usb/storage/usb-storage.ko

# 의존성 확인
$ modprobe --show-depends usb-storage
insmod /lib/modules/6.1.0/kernel/drivers/usb/common/usb-common.ko
insmod /lib/modules/6.1.0/kernel/drivers/usb/core/usbcore.ko
insmod /lib/modules/6.1.0/kernel/drivers/scsi/scsi_mod.ko
insmod /lib/modules/6.1.0/kernel/drivers/usb/storage/usb-storage.ko

# modules.dep 파일 내용 예시
$ grep usb-storage /lib/modules/$(uname -r)/modules.dep
kernel/drivers/usb/storage/usb-storage.ko: kernel/drivers/scsi/scsi_mod.ko \
  kernel/drivers/usb/core/usbcore.ko kernel/drivers/usb/common/usb-common.ko

로딩/언로딩 순서의 영향

모듈 스택킹에서 로딩 순서는 의존성의 역순(leaf → root는 불가, root → leaf 순)이며, 언로딩 순서는 그 반대(leaf → root)입니다. 참조 카운트(refcnt)가 0이 아닌 모듈은 언로드할 수 없습니다.

# 참조 카운트 확인
$ lsmod | grep usbcore
usbcore               327680  5  xhci_pci,xhci_hcd,usb_storage,usbhid,uas

# usbcore는 5개 모듈이 참조 중 → 직접 rmmod 불가
$ rmmod usbcore
rmmod: ERROR: Module usbcore is in use by: xhci_pci xhci_hcd usb_storage usbhid uas

# 올바른 언로딩: 의존 모듈을 먼저 제거
$ rmmod uas
$ rmmod usb_storage
$ rmmod usbhid
$ rmmod xhci_pci
$ rmmod xhci_hcd
$ rmmod usbcore         # 이제 refcnt=0 → 제거 가능
💡

modprobe -r usb-storage를 사용하면 해당 모듈과 더 이상 사용되지 않는 의존 모듈을 함께 자동으로 제거합니다. 단, 다른 모듈이 여전히 참조하는 모듈은 제거되지 않습니다.

커널의 주요 모듈 스택킹 사례

서브시스템코어 모듈프레임워크 모듈구현(위성) 모듈 예시
USB usb-common.ko usbcore.ko usb-storage.ko, uas.ko, usbhid.ko, xhci-hcd.ko
네트워크 필터링 netfilter (built-in) nf_tables.ko, nf_conntrack.ko nft_nat.ko, nft_chain_nat.ko, nf_nat.ko
암호화 crypto_algapi (built-in) cryptd.ko, crypto_simd.ko aes_generic.ko, aesni-intel.ko, sha256_generic.ko
SCSI / 블록 block layer (built-in) scsi_mod.ko sd_mod.ko, sr_mod.ko, sg.ko, iscsi_tcp.ko
사운드 soundcore.ko snd.ko, snd-pcm.ko snd-hda-intel.ko, snd-usb-audio.ko
GPU / DRM drm.ko drm_kms_helper.ko i915.ko, amdgpu.ko, nouveau.ko
입력장치 input (built-in) hid.ko, usbhid.ko hid-generic.ko, hid-logitech.ko
Bluetooth bluetooth.ko btusb.ko btintel.ko, btrtl.ko, bnep.ko
⚠️

설계 원칙: 모듈 스택킹 계층은 가능한 얕게 유지해야 합니다. 5단계 이상의 깊은 의존성 체인은 로드 시간 증가, 디버깅 복잡성, 그리고 잠재적 deadlock 위험을 야기합니다. 커널 커뮤니티에서는 일반적으로 2~3단계를 권장합니다.

심볼 네임스페이스 (Symbol Namespaces, Kernel 5.4+)

커널 5.4부터 도입된 심볼 네임스페이스(Symbol Namespaces)는 export된 심볼에 대한 접근을 특정 서브시스템으로 제한하는 메커니즘입니다. 기존의 EXPORT_SYMBOL()/EXPORT_SYMBOL_GPL()은 모든 모듈이 해당 심볼을 사용할 수 있었지만, 네임스페이스를 사용하면 명시적으로 import를 선언한 모듈만 접근할 수 있습니다.

이는 서브시스템 간의 우발적 의존성(accidental cross-subsystem dependencies)을 방지하고, API 경계를 명확히 하여 장기적인 커널 유지보수를 용이하게 합니다.

심볼 네임스페이스 격리 다이어그램 심볼 네임스페이스 접근 제어 NS: USB_STORAGE usb_stor_bulk_transfer() usb_stor_ctrl_transfer() usb_stor_clear_halt() EXPORT_SYMBOL_NS_GPL(..., USB_STORAGE) NS: IIO iio_device_register() iio_buffer_enabled() iio_trigger_notify() EXPORT_SYMBOL_NS_GPL(..., IIO) NS: MCB chameleon_parse_cells() mcb_bus_add_devices() mcb_alloc_dev() EXPORT_SYMBOL_NS_GPL(..., MCB) uas.ko MODULE_IMPORT_NS(USB_STORAGE) random_driver.ko MODULE_IMPORT_NS 없음 bmc150_accel.ko MODULE_IMPORT_NS(IIO) mcb-pci.ko MODULE_IMPORT_NS(MCB) 허용 (MODULE_IMPORT_NS 선언됨) 차단 (import 선언 없음) 네임스페이스에 속한 심볼을 사용하려면 반드시 MODULE_IMPORT_NS()로 명시적 선언 필요 미선언 시: 빌드 경고(W=1) + modpost 경고 / 런타임 시 로딩 실패 가능

네임스페이스 Export와 Import

심볼을 특정 네임스페이스에 export하려면 EXPORT_SYMBOL_NS() 또는 EXPORT_SYMBOL_NS_GPL() 매크로를 사용합니다. 해당 심볼을 사용하는 모듈은 MODULE_IMPORT_NS()를 선언해야 합니다.

/* 코어 모듈: 심볼을 네임스페이스에 export */
#include <linux/module.h>
#include <linux/export.h>

int mysubsys_register_device(struct mysubsys_dev *dev)
{
    /* 디바이스 등록 로직 */
    return 0;
}
EXPORT_SYMBOL_NS_GPL(mysubsys_register_device, MYSUBSYS);

void mysubsys_unregister_device(struct mysubsys_dev *dev)
{
    /* 디바이스 해제 로직 */
}
EXPORT_SYMBOL_NS_GPL(mysubsys_unregister_device, MYSUBSYS);

int mysubsys_send_event(struct mysubsys_dev *dev, int event)
{
    /* 이벤트 전송 */
    return 0;
}
EXPORT_SYMBOL_NS_GPL(mysubsys_send_event, MYSUBSYS);
/* 소비자 모듈: 네임스페이스를 명시적으로 import */
#include <linux/module.h>

/* 이 선언이 없으면 빌드 경고 + 로딩 실패 */
MODULE_IMPORT_NS(MYSUBSYS);

static struct mysubsys_dev my_dev;

static int __init my_driver_init(void)
{
    return mysubsys_register_device(&my_dev);
}

static void __exit my_driver_exit(void)
{
    mysubsys_unregister_device(&my_dev);
}

module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");

네임스페이스 확인 명령어

modinfo 명령으로 모듈이 import하는 네임스페이스를 확인할 수 있습니다. depmodmodprobe는 네임스페이스 요구사항을 자동으로 처리합니다.

# 모듈이 import하는 네임스페이스 확인
$ modinfo -F import_ns uas
USB_STORAGE

# 특정 모듈의 전체 정보에서 네임스페이스 확인
$ modinfo uas | grep import_ns
import_ns:      USB_STORAGE

# 커널 소스에서 네임스페이스 export 검색
$ grep -r "EXPORT_SYMBOL_NS" drivers/usb/storage/ | head -5
drivers/usb/storage/transport.c:EXPORT_SYMBOL_NS_GPL(usb_stor_bulk_transfer_buf, USB_STORAGE);
drivers/usb/storage/transport.c:EXPORT_SYMBOL_NS_GPL(usb_stor_bulk_transfer_sg, USB_STORAGE);
drivers/usb/storage/transport.c:EXPORT_SYMBOL_NS_GPL(usb_stor_bulk_srb, USB_STORAGE);
drivers/usb/storage/transport.c:EXPORT_SYMBOL_NS_GPL(usb_stor_ctrl_transfer, USB_STORAGE);
drivers/usb/storage/transport.c:EXPORT_SYMBOL_NS_GPL(usb_stor_clear_halt, USB_STORAGE);

# depmod 실행 시 네임스페이스 정보도 modules.dep에 반영
$ depmod -a
$ cat /lib/modules/$(uname -r)/modules.dep | grep uas

MODULE_IMPORT_NS 누락 시 동작

MODULE_IMPORT_NS() 선언 없이 네임스페이스에 속한 심볼을 사용하면 다음과 같은 결과가 발생합니다:

단계동작메시지 예시
빌드 시 (modpost) 경고 출력 (W=1 이상) WARNING: module my_driver uses symbol usb_stor_bulk_transfer from namespace USB_STORAGE, but does not import it.
depmod 실행 시 네임스페이스 불일치 경고 depmod: WARNING: missing namespace import...
modprobe 로딩 시 기본적으로 로딩됨 (경고) dmesg: module: my_driver: module uses symbol from namespace USB_STORAGE, but does not import it
엄격 모드 (향후) 로딩 거부 가능 커널 설정에 따라 -EINVAL 반환
⚠️

현재(커널 6.x) 기준으로 네임스페이스 미선언은 경고(warning)만 발생하고 로딩은 허용됩니다. 그러나 향후 커널 버전에서 엄격한 적용(enforcement)이 기본값이 될 수 있으므로, 반드시 MODULE_IMPORT_NS()를 선언하는 것이 권장됩니다.

주요 커널 심볼 네임스페이스

네임스페이스서브시스템주요 심볼 예시사용 모듈 예시
USB_STORAGE USB 대용량 저장장치 usb_stor_bulk_transfer_buf(), usb_stor_ctrl_transfer() uas.ko, usb-storage 관련
IIO 산업용 I/O iio_device_register(), iio_buffer_enabled() 가속도계, 자이로스코프 드라이버
IIO_TRIGGERED_BUFFER IIO 트리거 버퍼 iio_triggered_buffer_setup() IIO 하드웨어 트리거 드라이버
MCB MEN Chameleon Bus chameleon_parse_cells(), mcb_alloc_dev() mcb-pci.ko, mcb-lpc.ko
CEC_CORE HDMI CEC cec_register_adapter(), cec_transmit_msg() HDMI CEC 드라이버
DMA_BUF DMA 버퍼 공유 dma_buf_export(), dma_buf_map_attachment() GPU, V4L2, DRM 드라이버
DMA_VIRT 가상 DMA dma_async_device_register() 가상 DMA 채널 드라이버
FIREWIRE IEEE 1394 fw_core_add_descriptor() FireWire 드라이버

Makefile/Kconfig 레벨 기본 네임스페이스 지정

서브디렉터리 전체에 기본 네임스페이스를 지정하려면 Makefile에서 DEFAULT_SYMBOL_NAMESPACE를 설정할 수 있습니다. 이렇게 하면 해당 디렉터리의 모든 EXPORT_SYMBOL_GPL()이 자동으로 지정된 네임스페이스에 속합니다.

# drivers/iio/Makefile 예시
ccflags-y += -DDEFAULT_SYMBOL_NAMESPACE=IIO

# 또는 Kconfig에서 설정
# 이 경우 해당 디렉터리의 EXPORT_SYMBOL_GPL()이
# 자동으로 EXPORT_SYMBOL_NS_GPL(..., IIO)와 동일하게 동작
# 누락된 네임스페이스 import를 자동으로 추가하는 스크립트
$ scripts/nsdeps

# 빌드 경고에서 누락된 네임스페이스 확인 후 자동 패치
$ make W=1 2>&1 | grep "does not import" | scripts/nsdeps
💡

커널 소스 트리의 scripts/nsdeps 스크립트는 빌드 로그를 분석하여 누락된 MODULE_IMPORT_NS() 선언을 자동으로 소스 파일에 추가해 줍니다. 새로운 네임스페이스가 도입된 서브시스템의 드라이버를 업데이트할 때 매우 유용합니다.

모듈 서명 (Module Signing)

커널은 CONFIG_MODULE_SIG 옵션을 통해 모듈의 암호화 서명을 검증할 수 있습니다. 이는 신뢰할 수 없는 모듈이 커널에 로드되는 것을 방지하여 보안을 강화합니다. UEFI Secure Boot 환경에서는 필수적으로 사용됩니다.

설정 옵션 설명
CONFIG_MODULE_SIG 모듈 서명 인프라 활성화
CONFIG_MODULE_SIG_FORCE 서명되지 않은 모듈 로드 거부
CONFIG_MODULE_SIG_ALL 빌드 시 모든 모듈 자동 서명
CONFIG_MODULE_SIG_SHA256 서명 해시 알고리즘 (SHA-256)
CONFIG_MODULE_SIG_KEY 서명에 사용할 키 파일 경로
# CONFIG_MODULE_SIG_ALL=y 설정 시 make modules_install에서 자동 서명
$ make modules
$ make modules_install

# 수동으로 모듈 서명 (scripts/sign-file 직접 호출)
$ scripts/sign-file sha256 \
    certs/signing_key.pem \
    certs/signing_key.x509 \
    hello.ko

# 서명 확인
$ modinfo hello.ko | grep sig
sig_id:         PKCS#7
signer:         Build time autogenerated kernel key
sig_key:        AB:CD:EF:...
sig_hashalgo:   sha256
⚠️

CONFIG_MODULE_SIG_FORCE가 활성화된 커널에서는 서명되지 않은 모듈을 절대 로드할 수 없습니다. 개발 중에는 이 옵션을 비활성화하거나 커널 빌드 시 생성되는 키로 모듈에 서명해야 합니다. 배포용 커널에서는 보안을 위해 반드시 활성화하세요.

모듈 서명 키는 커널 빌드 시 certs/ 디렉터리에 자동 생성되거나, CONFIG_MODULE_SIG_KEY를 통해 사전 준비된 키를 지정할 수 있습니다. 서명 없이 모듈을 로드하면 커널에 taint 플래그가 설정되며, 이후 커널 버그 리포트 시 지원을 받기 어려울 수 있습니다.

모듈 보안 심화 (Module Security: Lockdown, LSM, and Integrity)

모듈 서명은 보안의 한 축일 뿐입니다. 리눅스 커널은 Lockdown LSM, UEFI Secure Boot 체인, IMA(Integrity Measurement Architecture), 그리고 SELinux/AppArmor 등 다층적 보안 메커니즘을 통해 커널 모듈의 무결성과 신뢰성을 보장합니다.

Kernel Lockdown (CONFIG_SECURITY_LOCKDOWN_LSM)

커널 5.4에서 도입된 Lockdown LSM은 root 사용자라 하더라도 커널의 무결성을 침해할 수 있는 기능을 제한합니다. 두 가지 모드가 있습니다:

모드보호 대상차단되는 동작
integrity 커널 코드/데이터 변조 방지 서명되지 않은 모듈 로드, /dev/mem 쓰기, kexec(서명 안 된 커널), ioperm/iopl, ACPI 테이블 오버라이드
confidentiality integrity + 커널 정보 유출 방지 위의 모든 것 + /proc/kallsyms 읽기, /dev/kmem 접근, kprobes 사용, perf 커널 프로파일링, bpf 읽기 제한
# 현재 lockdown 상태 확인
$ cat /sys/kernel/security/lockdown
none [integrity] confidentiality

# 커널 명령줄로 설정
lockdown=integrity
lockdown=confidentiality

# UEFI Secure Boot 시 자동 활성화 (배포판 의존)
$ dmesg | grep -i lockdown
[    0.000000] Kernel is locked down from EFI Secure Boot mode; see man kernel_lockdown.7
[    0.000000] Lockdown: swapper/0: Hibernation is restricted; see man kernel_lockdown.7

UEFI Secure Boot 체인

UEFI Secure Boot 환경에서는 펌웨어로부터 모듈 로딩까지 연속적인 신뢰 체인이 형성됩니다. 각 단계에서 다음 단계의 서명을 검증하며, 체인이 끊어지면 부팅이 거부되거나 lockdown 모드가 활성화됩니다.

UEFI Secure Boot에서 모듈 로딩까지의 보안 검증 체인 UEFI Secure Boot → 모듈 서명 검증 체인 UEFI 펌웨어 Platform Key (PK) KEK, db, dbx 검증 shim.efi Microsoft 서명 + MOK 관리 검증 GRUB 배포판 키 서명 부트로더 검증 vmlinuz 배포판 키 서명 lockdown 활성화 검증 *.ko 모듈 커널 빌드 키 서명 검증 서명 검증 실패 시 부팅 거부 (펌웨어/shim/GRUB 단계) | Lockdown 활성화 (커널 단계) | 모듈 로드 거부 또는 taint (모듈 단계) 모듈 로드 시 LSM 보안 훅 체인 insmod / modprobe init_module() syscall security_kernel _module_request() security_kernel _read_file() 모듈 로드 완료 또는 -EPERM 거부 SELinux AppArmor Lockdown IMA 각 LSM 구현체가 보안 훅에 정책을 등록 → 하나라도 거부하면 모듈 로드 실패 (-EPERM)

LSM 보안 훅과 모듈 로딩

커널의 LSM(Linux Security Modules) 프레임워크는 모듈 로딩 과정에 여러 보안 훅(hook)을 제공합니다. 각 LSM 구현체(SELinux, AppArmor, Lockdown 등)는 이 훅에 자신의 정책을 등록합니다.

보안 훅호출 시점주요 검사 내용
security_kernel_module_request() 모듈 자동 로드 요청 시 (request_module) SELinux: 프로세스의 module_request 권한 확인, AppArmor: 프로파일 기반 모듈명 필터링
security_kernel_read_file() 모듈 파일을 커널이 읽을 때 IMA: 파일 해시 무결성 검증, Lockdown: 서명 여부 확인
security_kernel_post_read_file() 모듈 파일 읽기 완료 후 IMA: 측정값(measurement) 기록, 서명 검증

SELinux 모듈 로딩 정책

# SELinux 정책에서 모듈 로드 권한 확인
$ sesearch -A -s init_t -c system -p module_load /etc/selinux/targeted/policy/policy.*
allow init_t self:system module_load;

# SELinux가 모듈 로드를 차단한 로그
$ ausearch -m AVC -ts recent | grep module
type=AVC msg=audit(...): avc:  denied  { module_load } for  pid=1234 comm="insmod"
  scontext=unconfined_u:unconfined_r:unconfined_t:s0
  tcontext=unconfined_u:unconfined_r:unconfined_t:s0

# AppArmor 프로파일에서 모듈 제한
# /etc/apparmor.d/usr.sbin.my-daemon
/usr/sbin/my-daemon {
  # 모듈 로드 차단 (deny 규칙)
  deny capability sys_module,
}

IMA (Integrity Measurement Architecture)

IMA는 파일의 무결성을 측정(measure), 평가(appraise), 감사(audit)하는 커널 서브시스템입니다. 모듈 로딩 시 IMA는 모듈 파일의 해시를 계산하여 사전 등록된 기대값과 비교하거나, 디지털 서명을 검증합니다.

# IMA 정책 파일에서 모듈 관련 규칙
$ cat /etc/ima/ima-policy
measure func=MODULE_CHECK
appraise func=MODULE_CHECK appraise_type=imasig

# IMA 측정 로그 확인
$ cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "\.ko"
10 abc123... ima-ng sha256:def456... /lib/modules/6.1.0/kernel/drivers/usb/core/usbcore.ko

# 모듈에 IMA 서명 추가
$ evmctl ima_sign --key /etc/keys/privkey_ima.pem /lib/modules/$(uname -r)/kernel/drivers/my_module.ko

보안 관련 커널 설정 옵션

설정 옵션기본값설명
CONFIG_MODULE_SIG n 모듈 서명 인프라 활성화
CONFIG_MODULE_SIG_FORCE n 서명 없는 모듈 로드 완전 차단
CONFIG_MODULE_SIG_ALL n 빌드 시 모든 모듈 자동 서명
CONFIG_MODULE_SIG_SHA512 n SHA-512 해시 알고리즘 사용
CONFIG_SECURITY_LOCKDOWN_LSM n Lockdown LSM 활성화
CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY n integrity 모드 강제
CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY n confidentiality 모드 강제
CONFIG_IMA n IMA 서브시스템 활성화
CONFIG_IMA_APPRAISE n IMA 무결성 평가 활성화
CONFIG_IMA_APPRAISE_MODSIG n IMA용 모듈 서명 검증
CONFIG_SECURITY_SELINUX 배포판 의존 SELinux LSM 활성화
CONFIG_SECURITY_APPARMOR 배포판 의존 AppArmor LSM 활성화
CONFIG_MODULE_SIG_KEY "certs/signing_key.pem" 모듈 서명에 사용할 키 경로

모듈 서명 상태 확인 명령어

# 모듈 서명 여부 확인
$ modinfo -F sig_hashalgo my_module
sha512

$ modinfo -F signer my_module
Build time autogenerated kernel key

# 서명 키 ID 확인
$ modinfo -F sig_id my_module
PKCS#7

# 모듈 파일에서 서명 직접 확인 (마지막 바이트)
$ hexdump -C /lib/modules/$(uname -r)/kernel/drivers/my_module.ko | tail -3
... 4d 6f 64 75 6c 65 20 73 69 67 6e 61 74 75 72 65  |Module signature|
... 61 70 70 65 6e 64 65 64 7e                        |appended~|

# 커널 taint 상태 확인
$ cat /proc/sys/kernel/tainted
0          # 0 = 정상, 비트 마스크로 표시

# 주요 taint 비트
# Bit 0 (P): non-GPL 모듈 로드됨
# Bit 12 (E): 서명 안 된 모듈 로드됨
# Bit 13 (X): 별도 배포판 라이브 패치 모듈

# 커널 명령줄에서 서명 강제
module.sig_enforce=1    # CONFIG_MODULE_SIG_FORCE와 동일 효과

Secure Boot 환경에서 커스텀 모듈 개발

Secure Boot가 활성화된 시스템에서 직접 개발한 모듈을 테스트하려면 MOK(Machine Owner Key)를 등록하여 자체 서명 체인을 구축해야 합니다.

# 1. 키 쌍 생성
$ openssl req -new -x509 -newkey rsa:2048 \
    -keyout MOK.priv -outform DER -out MOK.der \
    -nodes -days 36500 \
    -subj "/CN=My Module Signing Key/"

# 2. MOK 등록 (재부팅 필요)
$ sudo mokutil --import MOK.der
# 패스워드 입력 → 재부팅 시 shim이 MOK 등록 화면 표시

# 3. 등록된 MOK 확인
$ mokutil --list-enrolled | grep "Subject:"
Subject: CN=My Module Signing Key

# 4. 모듈 서명
$ /usr/src/linux-headers-$(uname -r)/scripts/sign-file \
    sha256 MOK.priv MOK.der my_module.ko

# 5. 서명 확인
$ modinfo my_module.ko | grep -E "sig|signer"
sig_id:         PKCS#7
signer:         My Module Signing Key
sig_hashalgo:   sha256

# 6. 모듈 로드
$ sudo insmod my_module.ko
# lockdown=integrity 상태에서도 정상 로드됨
⚠️

주의: MOK 개인키(MOK.priv)는 반드시 안전하게 보관해야 합니다. 이 키가 유출되면 공격자가 임의의 커널 모듈에 서명하여 Secure Boot 보호를 우회할 수 있습니다. 개발 완료 후에는 키를 삭제하거나 암호화된 저장소에 보관하십시오.

💡

DKMS(Dynamic Kernel Module Support)를 사용하는 경우, /etc/dkms/framework.confsign_tool과 MOK 키 경로를 설정하면 DKMS가 모듈 빌드 시 자동으로 서명합니다. 예: mok_signing_key="/root/MOK.priv", mok_certificate="/root/MOK.der"

디버깅 (Debugging: printk / dmesg)

커널 모듈 디버깅의 가장 기본적이면서도 강력한 도구는 printk()dmesg입니다. 사용자 공간의 printf()와 달리, printk()는 커널 링 버퍼에 메시지를 기록하며 로그 레벨을 지정할 수 있습니다.

printk 로그 레벨 (Log Levels)

printk()는 8단계의 로그 레벨을 지원합니다. 커널은 현재 콘솔 로그 레벨보다 높은(숫자가 낮은) 우선순위의 메시지만 콘솔에 직접 출력합니다.

레벨 매크로 pr_* 래퍼 용도
0 KERN_EMERG pr_emerg() 시스템 사용 불가
1 KERN_ALERT pr_alert() 즉각적인 조치 필요
2 KERN_CRIT pr_crit() 치명적인 상황
3 KERN_ERR pr_err() 에러 상황
4 KERN_WARNING pr_warn() 경고 상황
5 KERN_NOTICE pr_notice() 정상이지만 중요한 상황
6 KERN_INFO pr_info() 일반 정보
7 KERN_DEBUG pr_debug() 디버그 메시지
/* printk 직접 사용 */
printk(KERN_ERR "Error: device not found (id=%d)\\n", dev_id);

/* pr_* 래퍼 사용 (권장) */
pr_err("Error: device not found (id=%d)\\n", dev_id);
pr_info("Module loaded successfully\\n");
pr_debug("Debug: register value = 0x%08x\\n", reg_val);

/* 디바이스 드라이버에서는 dev_* 래퍼 사용 (디바이스 정보 자동 포함) */
dev_err(dev, "failed to allocate buffer\\n");
dev_info(dev, "device initialized\\n");

/* pr_fmt를 정의하면 모든 pr_* 메시지에 접두사 추가 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
💡

pr_debug()CONFIG_DYNAMIC_DEBUG 활성화 시 동적으로 켜고 끌 수 있으며, 비활성화 시 컴파일에서 완전히 제거됩니다. 프로덕션 코드에서도 성능 영향 없이 디버그 메시지를 남겨둘 수 있어 매우 유용합니다.

dmesg 활용 (Using dmesg)

dmesg는 커널 링 버퍼의 내용을 출력하는 유틸리티입니다. 모듈 개발 시 가장 빈번하게 사용됩니다.

# 전체 커널 로그 출력
$ dmesg

# 사람이 읽기 쉬운 타임스탬프 표시
$ dmesg -T

# 실시간 로그 모니터링 (follow 모드)
$ dmesg -w

# 로그 레벨별 필터링 (err 이상만)
$ dmesg -l err,crit,alert,emerg

# 특정 모듈 관련 로그 검색
$ dmesg | grep hello

# 링 버퍼 초기화 (root 권한 필요)
$ sudo dmesg -C

# 현재 콘솔 로그 레벨 확인
$ cat /proc/sys/kernel/printk
4    4    1    7
# current | default | minimum | boot-time-default

# 콘솔 로그 레벨 변경 (모든 메시지 표시)
$ sudo sh -c 'echo 8 > /proc/sys/kernel/printk'

Dynamic Debug

CONFIG_DYNAMIC_DEBUG가 활성화되면, pr_debug()dev_dbg() 호출을 런타임에 개별적으로 활성화/비활성화할 수 있습니다. 이를 통해 특정 파일, 함수, 또는 모듈의 디버그 메시지만 선택적으로 켤 수 있습니다.

# 사용 가능한 동적 디버그 포인트 확인
$ cat /sys/kernel/debug/dynamic_debug/control | head -5

# 특정 모듈의 모든 pr_debug 활성화
$ sudo sh -c 'echo "module hello +p" > /sys/kernel/debug/dynamic_debug/control'

# 특정 파일의 디버그 메시지 활성화
$ sudo sh -c 'echo "file hello.c +p" > /sys/kernel/debug/dynamic_debug/control'

# 특정 함수의 디버그 메시지 활성화 (함수명 + 라인번호 출력)
$ sudo sh -c 'echo "func hello_init +pfl" > /sys/kernel/debug/dynamic_debug/control'

# 플래그 의미: p=출력, f=함수명, l=라인번호, m=모듈명, t=스레드ID

# 비활성화
$ sudo sh -c 'echo "module hello -p" > /sys/kernel/debug/dynamic_debug/control'
ℹ️

모듈 개발 시 실전적인 디버깅 팁:

  • pr_fmt() 매크로를 파일 상단에 정의하여 모든 로그에 모듈명을 자동 삽입하세요.
  • dump_stack()을 호출하면 현재 호출 스택(backtrace)을 커널 로그에 출력합니다.
  • BUG_ON(condition)은 조건이 참이면 커널 패닉을 유발합니다. 디버그 빌드에서만 사용하세요.
  • WARN_ON(condition)은 조건이 참이면 경고 메시지와 스택 트레이스를 출력하지만, 실행은 계속됩니다.
  • 더 고급 디버깅에는 ftrace, kprobes, eBPF를 활용할 수 있습니다.

커널 핵심 유틸리티 매크로

커널 코드 전반에서 광범위하게 사용되는 유틸리티 매크로들은 커널 개발의 기본 어휘입니다. 이들의 목적, 내부 구현, 주의사항을 정확히 이해해야 안전한 커널 코드를 작성할 수 있습니다.

IS_ERR / PTR_ERR / ERR_PTR — 오류 포인터 체계

커널은 포인터 반환 함수에서 NULL 대신 오류 인코딩 포인터를 사용합니다. 유효한 커널 주소가 될 수 없는 상위 영역(-4095~-1)에 에러 코드를 인코딩합니다.

/* include/linux/err.h */
#define MAX_ERRNO 4095
#define IS_ERR_VALUE(x) unlikely((unsigned long)(void *)(x) >= (unsigned long)-MAX_ERRNO)

/* 에러 코드를 포인터로 인코딩 */
static inline void * __must_check ERR_PTR(long error)
{
    return (void *)error;
}

/* 포인터에서 에러 코드 추출 */
static inline long __must_check PTR_ERR(const void *ptr)
{
    return (long)ptr;
}

/* 포인터가 에러인지 검사 */
static inline bool __must_check IS_ERR(const void *ptr)
{
    return IS_ERR_VALUE((unsigned long)ptr);
}

/* IS_ERR_OR_NULL: NULL도 에러로 취급 */
static inline bool IS_ERR_OR_NULL(const void *ptr)
{
    return unlikely(!ptr) || IS_ERR_VALUE((unsigned long)ptr);
}

/* 올바른 사용 패턴 */
struct clk *clk = clk_get(dev, "my_clock");
if (IS_ERR(clk)) {
    dev_err(dev, "clock get failed: %ld\\n", PTR_ERR(clk));
    return PTR_ERR(clk);  /* 에러 코드 전파 */
}

/* 함수에서 에러 반환 */
struct my_obj *my_create(void)
{
    struct my_obj *obj = kzalloc(sizeof(*obj), GFP_KERNEL);
    if (!obj)
        return ERR_PTR(-ENOMEM);
    return obj;
}
☢️

치명적 실수:

  • IS_ERR() 체크 없이 ERR_PTR 값을 역참조하면 page fault 발생 (주소가 0xFFFFFFFFFFFFFFxx)
  • NULL 체크(if (!ptr))로는 ERR_PTR을 잡을 수 없음 — 반드시 IS_ERR() 사용
  • 일부 함수는 NULL을 반환하고 일부는 ERR_PTR을 반환 → 각 API 문서 확인 필수
  • PTR_ERR()은 반드시 IS_ERR()이 true인 경우에만 사용. 정상 포인터에 사용 시 의미 없는 값 반환

likely / unlikely — 분기 예측 힌트

/* include/linux/compiler.h */
#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

/* 목적: 컴파일러에게 분기 확률 정보를 제공하여
 * 1. 예측 가능한 분기 순서 배치 (hot path를 fall-through로)
 * 2. 캐시 지역성 최적화
 * 3. 분기 예측 정확도 향상 */

/* 올바른 사용 예시 */
if (unlikely(ptr == NULL))  /* 오류 경로: 거의 발생 안 함 */
    return -EINVAL;

if (likely(size <= MAX))    /* 정상 경로: 거의 항상 참 */
    fast_path(size);

/* 주의사항:
 * 1. 50/50 확률이면 사용하지 말 것 (역효과 가능)
 * 2. 핫 패스에서만 의미 있음 (콜드 코드에서는 효과 미미)
 * 3. 실제 프로파일링 데이터 없이 추측으로 사용하지 말 것
 * 4. IS_ERR()에는 이미 unlikely가 내장되어 있음 */

copy_to_user / copy_from_user — 사용자 공간 데이터 복사

/* include/linux/uaccess.h */

/* 사용자 → 커널 복사 (반환: 복사 못한 바이트 수, 성공 시 0) */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

/* 커널 → 사용자 복사 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

/* 단일 값 복사 (더 효율적) */
get_user(kernel_var, user_ptr);   /* user → kernel, 반환: 0 or -EFAULT */
put_user(kernel_val, user_ptr);   /* kernel → user, 반환: 0 or -EFAULT */

/* 올바른 사용 패턴 */
static ssize_t my_write(struct file *f, const char __user *buf,
                        size_t count, loff_t *pos)
{
    char kbuf[256];

    if (count > sizeof(kbuf))
        return -EINVAL;

    /* copy_from_user는 내부에서 access_ok() 검사 수행 */
    if (copy_from_user(kbuf, buf, count))
        return -EFAULT;  /* 반환값 ≠ 0이면 실패 */

    /* kbuf 처리 */
    return count;
}

/* strncpy_from_user: 문자열 복사 (NULL 종단 자동) */
long ret = strncpy_from_user(kbuf, ubuf, sizeof(kbuf));
if (ret < 0) return ret;      /* -EFAULT */
if (ret == sizeof(kbuf)) ...;  /* 잘림 (truncated) */

/* strnlen_user: 사용자 문자열 길이 (NULL 포함) */
long len = strnlen_user(ubuf, MAX_LEN);
☢️

보안 주의사항:

  • 절대로 memcpy()로 사용자 공간 데이터를 복사하지 마십시오 — 주소 검증 없이 커널 임의 메모리에 쓸 수 있습니다 (보안 취약점)
  • __user 어노테이션을 항상 올바르게 사용하십시오. sparse 검사 도구가 누락을 탐지합니다
  • SMAP(Supervisor Mode Access Prevention)이 활성화된 시스템에서는 copy_*_user 없이 사용자 메모리 접근 시 즉시 Oops 발생
  • 반환값은 복사하지 못한 바이트 수이므로, if (copy_from_user(...))는 "실패 시"를 의미합니다
  • 크기 검증: 사용자가 제공하는 count를 신뢰하지 말고 상한을 검사하십시오

ARRAY_SIZE / sizeof 패턴

/* include/linux/array_size.h */
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))

/* __must_be_array: 포인터에 대한 실수 방지 (빌드 타임 검사) */
#define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))

/* 사용 예시 */
static const struct pci_device_id my_ids[] = {
    { PCI_DEVICE(0x8086, 0x1234) },
    { PCI_DEVICE(0x8086, 0x5678) },
    { 0, }
};
int count = ARRAY_SIZE(my_ids);  /* 3 (터미네이터 포함) */

/* 주의: 포인터에 사용하면 빌드 에러 */
void foo(int *arr)
{
    /* ARRAY_SIZE(arr); → 빌드 에러! 포인터는 배열이 아님 */
}

/* struct_size: 가변 길이 구조체 크기 (오버플로 안전) */
struct my_buf {
    int count;
    struct item items[];  /* flexible array member */
};
size_t sz = struct_size(buf, items, n);  /* sizeof(*buf) + n * sizeof(buf->items[0]) */

min / max / clamp — 안전한 비교 매크로

/* include/linux/minmax.h */
/* 타입 안전 min/max (같은 타입이 아니면 경고) */
#define min(x, y)    __careful_cmp(min, x, y)
#define max(x, y)    __careful_cmp(max, x, y)

/* 다른 타입 비교 (명시적 캐스트 필요 시) */
#define min_t(type, x, y)  __careful_cmp(min, (type)(x), (type)(y))
#define max_t(type, x, y)  __careful_cmp(max, (type)(x), (type)(y))

/* 범위 제한 */
#define clamp(val, lo, hi)      min(max(val, lo), hi)
#define clamp_t(type, val, lo, hi)  min_t(type, max_t(type, val, lo), hi)
#define clamp_val(val, lo, hi)  clamp_t(typeof(val), val, lo, hi)

/* swap: 두 변수 교환 */
#define swap(a, b) do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0)

/* 사용 예시 */
int val = clamp(input, 0, 100);  /* 0 ≤ val ≤ 100 */
size_t sz = min_t(size_t, user_len, MAX_BUF);

/* 주의: signed/unsigned 혼합 비교 */
int a = -1;
unsigned int b = 1;
/* min(a, b) → 타입 불일치 경고! min_t(int, a, b) 또는 min_t(unsigned, a, b) 사용 */

__init / __exit / __initdata — 섹션 어노테이션

/* include/linux/init.h */
#define __init    __section(".init.text") __cold  __latent_entropy __noinitretpoline
#define __exit    __section(".exit.text") __cold  __exitused
#define __initdata __section(".init.data")
#define __initconst __section(".init.rodata")

/* 목적: 초기화 후 메모리 해제
 * - __init 함수/데이터는 부팅 완료 후 메모리에서 해제됨
 * - dmesg: "Freeing unused kernel memory: XXXX kB"
 * - 모듈: module_init() 실행 후 __init 섹션 해제 */

static int __init my_init(void)  /* 부팅/로드 시에만 호출 */
{
    /* ... */
    return 0;
}
static void __exit my_exit(void)  /* 언로드 시에만 호출 */
{
    /* ... */
}

/* __initdata: 초기화 전용 데이터 */
static int __initdata my_early_param = 42;
static const char __initconst my_msg[] = "init only";
☢️

__init 사용 시 치명적 실수:

  • __init 함수를 초기화 이후에 호출하면 해제된 메모리 실행 → 크래시 또는 보안 취약점
  • __init 함수의 포인터를 전역 콜백에 등록하면 나중에 호출 시 크래시
  • __init 함수에서 __init이 아닌 함수를 호출하는 것은 안전. 반대는 위험
  • 빌트인 드라이버에서만 __exit이 최적화됨 (모듈은 언로드 시 필요하므로 유지)
  • CONFIG_DEBUG_SECTION_MISMATCH=y로 잘못된 섹션 참조 탐지

BUILD_BUG_ON / static_assert — 컴파일 타임 검증

/* include/linux/build_bug.h */
/* 조건이 참이면 빌드 실패 */
BUILD_BUG_ON(sizeof(struct my_data) != 64);
BUILD_BUG_ON(ARRAY_SIZE(my_table) != MY_COUNT);

/* 표현식 내에서 사용 가능한 버전 (0 반환) */
BUILD_BUG_ON_ZERO(condition);
/* 예: int x[10 + BUILD_BUG_ON_ZERO(sizeof(int) != 4)]; */

/* 메시지 포함 (C11 static_assert) */
static_assert(sizeof(long) == 8, "64-bit kernel required");

/* BUILD_BUG_ON_MSG (커널 자체 구현) */
BUILD_BUG_ON_MSG(X > Y, "X must not exceed Y");

/* 활용 사례 */
/* 구조체 크기가 캐시 라인에 맞는지 확인 */
BUILD_BUG_ON(sizeof(struct hot_data) > L1_CACHE_BYTES);
/* 비트필드가 레지스터 크기를 초과하지 않는지 */
BUILD_BUG_ON(MY_FLAGS_MASK >> 32);
/* 열거형 값이 예상 범위인지 */
BUILD_BUG_ON(MY_ENUM_MAX > 255);

pr_* / dev_* — 커널 로깅 매크로 체계

매크로레벨용도예시
pr_emerg0시스템 사용 불가패닉 직전
pr_alert1즉시 조치 필요하드웨어 장애
pr_crit2치명적 상태심각한 오류
pr_err3오류 상태일반 오류
pr_warn4경고 상태비정상이지만 복구 가능
pr_notice5정상이지만 주목할 상태설정 변경 등
pr_info6정보성드라이버 초기화 메시지
pr_debug7디버그개발 중 디버깅 (CONFIG_DYNAMIC_DEBUG)
/* pr_fmt 정의로 모듈 이름 자동 접두사 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

pr_info("device initialized, version %d\\n", ver);
/* 출력: my_module: device initialized, version 1 */

/* dev_* 매크로: 디바이스 정보 자동 포함 */
dev_err(dev, "probe failed: %d\\n", ret);
/* 출력: my_driver 0000:01:00.0: probe failed: -12 */

/* rate limited 버전 (DoS 방지) */
pr_err_ratelimited("packet error\\n");
dev_warn_ratelimited(dev, "timeout\\n");

/* once 버전 (한 번만 출력) */
pr_warn_once("deprecated API used\\n");

/* netdev_*: 네트워크 디바이스 전용 */
netdev_err(netdev, "link down\\n");
/* 출력: eth0: link down */

EXPORT_SYMBOL / EXPORT_SYMBOL_GPL 심화

/* 기본 export (모든 모듈 접근 가능) */
EXPORT_SYMBOL(my_public_function);

/* GPL export (GPL 호환 모듈만 접근 가능) */
EXPORT_SYMBOL_GPL(my_gpl_function);

/* 네임스페이스 export (커널 5.4+) */
EXPORT_SYMBOL_NS(my_func, MY_SUBSYSTEM);
EXPORT_SYMBOL_NS_GPL(my_func, MY_SUBSYSTEM);

/* 사용 측에서 네임스페이스 import */
MODULE_IMPORT_NS(MY_SUBSYSTEM);

/* 심볼 확인 */
/* cat /proc/kallsyms | grep my_func */
/* nm vmlinux | grep my_func */

/* 주의사항:
 * 1. EXPORT_SYMBOL_GPL은 내부 API 표시 — 변경 가능성 높음
 * 2. 불필요한 export는 공격 표면 증가 — 최소한으로 유지
 * 3. 빌트인이면서 모듈에서 참조하는 심볼만 export 필요
 * 4. MODULE_VERSION()으로 ABI 버전 명시 권장 */

커널 에러 코드 (errno) 주요 값

코드의미커널 내 주요 사용처
-ENOMEM-12메모리 부족kmalloc, alloc_pages 실패
-EINVAL-22잘못된 인자유효하지 않은 파라미터
-EFAULT-14잘못된 주소copy_from_user 실패
-EBUSY-16디바이스 사용 중리소스 점유
-ENODEV-19디바이스 없음probe 실패, 하드웨어 미발견
-EIO-5I/O 오류하드웨어 통신 실패
-ENOSPC-28공간 부족디스크 가득 참
-EPERM-1권한 없음CAP_* 미보유
-EAGAIN-11다시 시도비블로킹 I/O, 리소스 일시 부족
-ETIMEDOUT-110타임아웃하드웨어 응답 없음
-EPROBE_DEFER-517프로브 지연의존 리소스 미준비 (재시도)
-ENOTSUPP-524미지원커널 내부 전용 (EOPNOTSUPP과 구분)

자동 로딩 (Auto-loading: udev & MODULE_DEVICE_TABLE)

리눅스는 하드웨어가 감지되면 해당 드라이버 모듈을 자동으로 로드합니다. 이 메커니즘은 udev, MODULE_DEVICE_TABLE, modules.alias의 세 가지 요소가 협력하여 동작합니다.

자동 로딩 흐름 (Auto-load Flow)

하드웨어 감지 (PCI/USB/OF) 커널 uevent MODALIAS=... udev modprobe 호출 modules.alias 별칭 → 모듈 매칭 .ko 로드 modprobe Kernel Kernel User Space User Space Kernel 하드웨어 감지 → uevent → udev → modprobe → 모듈 로드
모듈 자동 로딩 흐름: 하드웨어 감지부터 모듈 로드까지

MODULE_DEVICE_TABLE 매크로

MODULE_DEVICE_TABLE()은 모듈이 지원하는 하드웨어 ID를 커널에 알립니다. depmod가 이 정보를 추출하여 modules.alias 파일에 기록하고, modprobe가 이를 참조하여 적절한 모듈을 로드합니다.

/* PCI 디바이스 테이블 */
static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x8086, 0x1533) },  /* Intel I210 */
    { PCI_DEVICE(0x8086, 0x1539) },  /* Intel I211 */
    { 0, }  /* 터미네이터 (필수) */
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

/* USB 디바이스 테이블 */
static const struct usb_device_id my_usb_ids[] = {
    { USB_DEVICE(0x0bda, 0x8153) },  /* Realtek RTL8153 */
    { USB_DEVICE_AND_INTERFACE_INFO(0x0bda, 0x8152,
        USB_CLASS_VENDOR_SPEC, 1, 0) },
    { }
};
MODULE_DEVICE_TABLE(usb, my_usb_ids);

/* Device Tree (OF) 매칭 테이블 */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-device" },
    { .compatible = "vendor,my-device-v2" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

/* ACPI 매칭 테이블 */
static const struct acpi_device_id my_acpi_ids[] = {
    { "MYDEV001", 0 },
    { }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);
# modules.alias 파일에서 별칭 확인
$ grep i210 /lib/modules/$(uname -r)/modules.alias
alias pci:v00008086d00001533sv*sd*bc*sc*i* igb

# 특정 디바이스에 어떤 모듈이 매칭되는지 확인
$ modprobe --resolve-alias pci:v00008086d00001533sv*sd*bc02sc00i*

# MODALIAS 환경변수 확인 (sysfs)
$ cat /sys/bus/pci/devices/0000:01:00.0/modalias
pci:v00008086d00001533sv...

# udevadm으로 uevent 확인
$ udevadm info --query=property --path=/sys/bus/pci/devices/0000:01:00.0 | grep MODALIAS
💡

MODULE_DEVICE_TABLE().ko 파일의 .modinfo 섹션에 별칭 정보를 저장합니다. depmod가 이를 읽어 modules.alias를 생성하므로, 수동으로 모듈을 설치한 경우에는 depmod -a를 실행해 자동 로딩 정보가 갱신되었는지 확인하는 것이 좋습니다.

Sysfs 모듈 인터페이스 (/sys/module/ 구조)

커널은 로드된 모든 모듈에 대한 런타임 정보를 /sys/module/ 가상 파일시스템을 통해 노출합니다. 이 인터페이스를 활용하면 모듈 파라미터를 실시간으로 조회·변경하거나, 디버깅에 필요한 섹션 주소를 확인할 수 있습니다. 빌트인(built-in) 모듈도 /sys/module/에 디렉터리가 생성되므로, .ko 파일로 로드된 모듈뿐 아니라 커널 이미지에 직접 컴파일된 기능까지 파라미터를 관리할 수 있습니다.

디렉터리 구조 개요

각 모듈 디렉터리 /sys/module/<name>/ 아래에는 다음과 같은 항목들이 존재합니다. 빌트인 모듈의 경우 일부 항목(예: sections/, refcnt)이 없을 수 있습니다.

/sys/module 디렉터리 트리 구조 /sys/module/<name>/ 하위 디렉터리 속성 파일 parameters/ param1 (rw) param2 (ro) sections/ .text .data / .bss holders/ 의존 모듈 심볼릭 링크 notes/ ELF 노트 섹션 refcnt 참조 카운트 initstate live / coming / going taint 모듈별 오염 상태 srcversion 소스 버전 해시 coresize 코어 메모리 크기 initsize init 섹션 크기 version MODULE_VERSION() 값 루트 디렉터리 파일/속성

주요 항목 상세

경로 유형 권한 설명
parameters/ 디렉터리 모듈 파라미터 파일 집합. 각 파일은 module_param()으로 선언된 파라미터에 대응
parameters/<param> 파일 perm에 따름 파라미터 현재 값. 0644이면 읽기/쓰기, 0444이면 읽기 전용
sections/ 디렉터리 root ELF 섹션별 커널 메모리 주소. GDB 디버깅 시 add-symbol-file에 사용
sections/.text 파일 0400 코드 섹션의 로드 주소 (예: 0xffffffffc0800000)
sections/.data 파일 0400 초기화된 데이터 섹션의 로드 주소
sections/.bss 파일 0400 미초기화 데이터 섹션의 로드 주소
holders/ 디렉터리 이 모듈에 의존하는 다른 모듈들의 심볼릭 링크
notes/ 디렉터리 ELF .note 섹션 (Build ID 등)
refcnt 파일 0444 모듈 참조 카운트. 0이어야 rmmod 가능
initstate 파일 0444 live(정상), coming(초기화 중), going(제거 중)
taint 파일 0444 모듈별 taint 문자열 (예: O=out-of-tree, E=unsigned)
srcversion 파일 0444 소스 코드 해시. MODULE_INFO(srcversion, ...)에 의해 생성
coresize 파일 0444 코어 레이아웃 메모리 크기(바이트). init 이후 상주하는 부분
initsize 파일 0444 init 레이아웃 메모리 크기(바이트). 초기화 후 해제됨
version 파일 0444 MODULE_VERSION() 매크로로 설정한 버전 문자열

파라미터 조회 및 변경

모듈 파라미터는 /sys/module/<name>/parameters/ 아래에서 직접 읽고 쓸 수 있습니다. 권한이 쓰기 가능(0644 등)으로 설정된 파라미터만 런타임에 변경할 수 있습니다.

# 현재 로드된 모듈의 파라미터 목록 확인
$ ls /sys/module/e1000e/parameters/
CrcStripping  IntMode  SmartPowerDownEnable  copybreak  debug

# 특정 파라미터 값 읽기
$ cat /sys/module/e1000e/parameters/IntMode
1

# 쓰기 가능한 파라미터 런타임 변경
$ echo 2 | sudo tee /sys/module/e1000e/parameters/IntMode

# 파라미터 권한 확인 (0644 = 읽기/쓰기, 0444 = 읽기 전용)
$ stat -c '%a' /sys/module/e1000e/parameters/IntMode
644

섹션 주소를 이용한 GDB 디버깅

모듈 디버깅 시 GDB에 심볼 파일과 로드 주소를 알려주어야 합니다. /sys/module/<name>/sections/에서 섹션 주소를 읽어 사용합니다.

# 모듈 섹션 주소 확인 (root 권한 필요)
$ sudo cat /sys/module/my_driver/sections/.text
0xffffffffc0800000
$ sudo cat /sys/module/my_driver/sections/.data
0xffffffffc0802000
$ sudo cat /sys/module/my_driver/sections/.bss
0xffffffffc0803000

# GDB에서 심볼 로드
(gdb) add-symbol-file my_driver.ko 0xffffffffc0800000 \
    -s .data 0xffffffffc0802000 \
    -s .bss 0xffffffffc0803000

모듈 상태 모니터링 스크립트

다음 셸 스크립트는 로드된 모든 모듈의 상태를 한눈에 보여줍니다.

#!/bin/bash
# 모듈 sysfs 정보 요약 스크립트

for mod in /sys/module/*/; do
    name=$(basename "$mod")

    # .ko 모듈만 (refcnt 파일 존재 여부로 판별)
    [ -f "$mod/refcnt" ] || continue

    refcnt=$(cat "$mod/refcnt")
    state=$(cat "$mod/initstate" 2>/dev/null || echo "unknown")
    coresize=$(cat "$mod/coresize" 2>/dev/null || echo "?")
    taint=$(cat "$mod/taint" 2>/dev/null || echo "-")

    printf "%-24s  state=%-7s  refcnt=%-3s  core=%s  taint=%s\n" \
        "$name" "$state" "$refcnt" "$coresize" "$taint"
done | sort

udev 규칙과 sysfs 연동

udev 규칙에서 모듈 파라미터를 설정하거나, 모듈 로드 시 특정 동작을 트리거할 수 있습니다. /etc/modprobe.d/의 설정 파일과 결합하면 더욱 유연한 모듈 관리가 가능합니다.

# /etc/udev/rules.d/99-module-params.rules
# 특정 USB 장치 연결 시 모듈 파라미터 변경
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1234", \
    RUN+="/bin/sh -c 'echo 1 > /sys/module/usbcore/parameters/autosuspend'"

# /etc/modprobe.d/custom-params.conf
# 모듈 로드 시 파라미터 기본값 설정
options e1000e IntMode=2 CrcStripping=1
options snd-hda-intel power_save=1
💡

/sys/module/은 빌트인 모듈도 포함합니다. 예를 들어 /sys/module/printk/parameters/time을 쓰면 커널 로그에 타임스탬프를 추가·제거할 수 있습니다. 빌트인 모듈의 파라미터는 부팅 시 커널 명령줄(예: printk.time=1)로도 설정 가능합니다.

DKMS 동적 커널 모듈 지원 (Dynamic Kernel Module Support)

DKMS(Dynamic Kernel Module Support)는 커널 업데이트 시 out-of-tree 모듈을 자동으로 재빌드·재설치하는 프레임워크입니다. NVIDIA 드라이버, VirtualBox 게스트 모듈, ZFS 등 커널 외부에서 관리되는 모듈들이 DKMS를 널리 사용합니다. 커널 버전이 바뀔 때마다 수동으로 빌드하는 번거로움을 해결해 줍니다.

DKMS 워크플로: 커널 업데이트 → 자동 재빌드 → 설치 ① 소스 등록 dkms add /usr/src/mod-ver/ ② 빌드 dkms build make → .ko 생성 ③ 설치 dkms install .ko → modules 디렉터리 ④ depmod 갱신 depmod -a 의존성 DB 업데이트 커널 업데이트 시 자동 프로세스 패키지 매니저 apt upgrade kernel postinst 훅 dkms autoinstall 자동 재빌드 새 커널 헤더로 빌드 완료 재부팅 시 사용 가능 DKMS 디렉터리 구조: /var/lib/dkms/<module>/<version>/ ├── source → /usr/src/<module>-<version>/ ├── <kernel-version>/<arch>/ ├── module/ (빌드된 .ko) └── log/ (빌드 로그)

dkms.conf 파일 형식

DKMS 모듈의 핵심 설정 파일인 dkms.conf/usr/src/<module>-<version>/ 디렉터리에 위치합니다.

# /usr/src/my_driver-1.0/dkms.conf
# DKMS 설정 파일

PACKAGE_NAME="my_driver"
PACKAGE_VERSION="1.0"

# 빌드할 모듈 목록 (0부터 인덱스)
BUILT_MODULE_NAME[0]="my_driver"
BUILT_MODULE_LOCATION[0]="src/"
DEST_MODULE_LOCATION[0]="/updates/dkms/"

# 빌드 명령 (기본: make)
MAKE[0]="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build/src"
CLEAN="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build/src clean"

# 자동 설치 여부
AUTOINSTALL="yes"

# 여러 모듈 빌드 시
# BUILT_MODULE_NAME[1]="my_helper"
# BUILT_MODULE_LOCATION[1]="src/"
# DEST_MODULE_LOCATION[1]="/updates/dkms/"

DKMS 명령어

명령 설명 예시
dkms add 소스 트리를 DKMS에 등록 dkms add -m my_driver -v 1.0
dkms build 특정 커널 버전에 대해 빌드 dkms build -m my_driver -v 1.0 -k 6.1.0-amd64
dkms install 빌드된 모듈을 modules 디렉터리에 설치 dkms install -m my_driver -v 1.0
dkms remove 설치된 모듈을 제거 dkms remove -m my_driver -v 1.0 --all
dkms status 등록된 모든 DKMS 모듈 상태 표시 dkms status
dkms autoinstall 현재 커널에 대해 등록된 모든 모듈 빌드·설치 dkms autoinstall -k $(uname -r)
dkms mkdeb DKMS 모듈을 .deb 패키지로 생성 dkms mkdeb -m my_driver -v 1.0
dkms mkrpm DKMS 모듈을 .rpm 패키지로 생성 dkms mkrpm -m my_driver -v 1.0

DKMS 실전 예제: 커스텀 모듈 패키징

out-of-tree 모듈을 DKMS로 관리하는 전체 과정입니다.

# 1. 소스 디렉터리 생성
$ sudo mkdir -p /usr/src/my_driver-1.0/src

# 2. 소스 파일 복사
$ sudo cp my_driver.c Makefile /usr/src/my_driver-1.0/src/

# 3. dkms.conf 작성
$ sudo tee /usr/src/my_driver-1.0/dkms.conf <<'EOF'
PACKAGE_NAME="my_driver"
PACKAGE_VERSION="1.0"
BUILT_MODULE_NAME[0]="my_driver"
BUILT_MODULE_LOCATION[0]="src/"
DEST_MODULE_LOCATION[0]="/updates/dkms/"
AUTOINSTALL="yes"
EOF

# 4. DKMS에 등록
$ sudo dkms add -m my_driver -v 1.0

# 5. 현재 커널에 대해 빌드 및 설치
$ sudo dkms build -m my_driver -v 1.0
$ sudo dkms install -m my_driver -v 1.0

# 6. 상태 확인
$ dkms status
my_driver, 1.0, 6.1.0-amd64, x86_64: installed

# 7. 모듈 로드 테스트
$ sudo modprobe my_driver
$ lsmod | grep my_driver

DKMS vs 수동 빌드 vs In-tree 비교

항목 DKMS 수동 빌드 In-tree
커널 업데이트 시 자동 재빌드 수동 재빌드 필요 커널과 함께 빌드
설정 복잡도 dkms.conf 필요 Makefile만 필요 Kconfig + Kbuild
패키지 배포 mkdeb/mkrpm 지원 별도 스크립트 커널 패키지에 포함
버전 관리 모듈별 독립 버전 수동 관리 커널 버전에 종속
적합한 용도 서드파티 드라이버 개발·테스트 단계 메인라인 기여
Taint 여부 O (out-of-tree) O (out-of-tree) 없음

DKMS 빌드 실패 트러블슈팅

# 빌드 로그 확인
$ cat /var/lib/dkms/my_driver/1.0/build/make.log

# 커널 헤더 설치 확인
$ ls /lib/modules/$(uname -r)/build/
# 없으면: sudo apt install linux-headers-$(uname -r)

# 수동 빌드 테스트 (DKMS 환경 밖에서)
$ cd /usr/src/my_driver-1.0/src
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# DKMS 재설정
$ sudo dkms remove -m my_driver -v 1.0 --all
$ sudo dkms add -m my_driver -v 1.0
$ sudo dkms build -m my_driver -v 1.0 2>&1 | tee build.log
⚠️

DKMS 빌드가 실패하는 가장 흔한 원인은 커널 헤더 패키지 미설치입니다. Debian/Ubuntu에서는 linux-headers-$(uname -r), Fedora/RHEL에서는 kernel-devel-$(uname -r) 패키지가 필요합니다. dkms autoinstall/etc/dkms/framework.conf에서 글로벌 설정을 참조합니다.

Taint 플래그 (Kernel Taint Flags)

커널 taint 플래그는 커널이 "오염된" 상태를 나타내는 비트 필드입니다. GPL이 아닌 모듈 로드, 강제 모듈 로드/언로드, 하드웨어 오류 등이 발생하면 설정됩니다. taint 상태는 커널 Oops 메시지에 표시되며, 버그 리포트 시 중요한 정보입니다.

문자 비트 의미
P0프로프라이어터리 모듈 로드됨 (non-GPL)
F1모듈이 강제 로드됨 (insmod -f)
S2SMP 커널에서 SMP 비안전 모듈 로드
R3모듈이 강제 언로드됨 (rmmod -f)
M4Machine Check Exception (하드웨어 오류)
B5Bad page 참조 (페이지 해제 오류)
U6사용자 요청에 의한 taint
D7Oops 발생 (커널 경고)
W8WARN 발생
C9스테이징 드라이버 로드됨
E15서명되지 않은 모듈 로드됨
K17커널 라이브 패치 적용됨
# 현재 taint 상태 확인 (숫자)
$ cat /proc/sys/kernel/tainted
0    # 0 = 깨끗한 상태

# taint 문자열 확인 (Oops 메시지에서)
# "Not tainted" 또는 "Tainted: PF" 등으로 표시

# 수동으로 taint 설정 (테스트용)
$ sudo sh -c 'echo 64 > /proc/sys/kernel/tainted'  # 'U' 비트
⚠️

커널이 tainted 상태이면 커널 개발자가 버그 리포트를 무시할 수 있습니다. 프로프라이어터리 드라이버(NVIDIA 등)를 사용 중이라면 P taint가 설정되며, 이 상태에서의 버그는 해당 드라이버 벤더에게 리포트해야 합니다.

Devm 관리 리소스 (Managed Device Resources)

devm_* API는 디바이스에 바인딩된 리소스를 자동으로 관리합니다. 디바이스가 제거되거나 probe가 실패하면 할당된 리소스가 자동으로 역순 해제됩니다. 이를 통해 에러 처리 경로에서의 리소스 누수를 원천 차단할 수 있습니다.

/* ❌ 기존 방식: 수동 해제 필요 (에러 경로마다 goto 체인) */
static int my_probe_old(struct platform_device *pdev)
{
    struct my_dev *priv;
    struct resource *res;
    void __iomem *base;
    int irq, ret;

    priv = kzalloc(sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = ioremap(res->start, resource_size(res));
    if (!base) { ret = -ENOMEM; goto err_free; }

    irq = platform_get_irq(pdev, 0);
    ret = request_irq(irq, my_isr, 0, "my", priv);
    if (ret) goto err_unmap;

    return 0;

err_unmap:
    iounmap(base);
err_free:
    kfree(priv);
    return ret;
}

/* ✅ devm 방식: 자동 해제, goto 불필요 */
static int my_probe_devm(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_dev *priv;
    void __iomem *base;
    int irq, ret;

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = devm_request_irq(dev, irq, my_isr, 0,
                           dev_name(dev), priv);
    if (ret)
        return ret;

    /* 실패 시 devm이 모든 리소스 자동 해제 */
    return 0;
}

주요 devm_* API 목록:

devm API 대응하는 수동 API 용도
devm_kzalloc()kzalloc() / kfree()메모리 할당
devm_ioremap()ioremap() / iounmap()MMIO 매핑
devm_request_irq()request_irq() / free_irq()인터럽트 등록
devm_clk_get()clk_get() / clk_put()클럭 획득
devm_regulator_get()regulator_get() / regulator_put()레귤레이터 획득
devm_gpio_request()gpio_request() / gpio_free()GPIO 요청
devm_pinctrl_get()pinctrl_get() / pinctrl_put()핀 제어
devm_reset_control_get()reset_control_get() / reset_control_put()리셋 제어
ℹ️

devm_* 리소스는 probe의 역순으로 해제됩니다 (LIFO). remove 콜백에서 수동 해제가 필요한 경우(예: DMA 전송 중지, 하드웨어 비활성화)에는 devm_add_action_or_reset()로 커스텀 정리 콜백을 등록할 수 있습니다.

드라이버 등록 축약 매크로 (Driver Registration Shortcuts)

전통적인 module_init() / module_exit() 패턴 대신, 커널은 버스 유형별 축약 매크로를 제공합니다. 이 매크로들은 보일러플레이트 코드를 제거하고 에러 처리를 자동화합니다.

/* ❌ 전통적 방식: 보일러플레이트 코드 */
static int __init my_pci_init(void)
{
    return pci_register_driver(&my_pci_driver);
}
static void __exit my_pci_exit(void)
{
    pci_unregister_driver(&my_pci_driver);
}
module_init(my_pci_init);
module_exit(my_pci_exit);

/* ✅ 축약 매크로: 위의 8줄을 1줄로 */
module_pci_driver(my_pci_driver);

주요 축약 매크로:

매크로 버스 유형 내부 호출
module_pci_driver(drv)PCIpci_register_driver
module_usb_driver(drv)USBusb_register
module_platform_driver(drv)Platformplatform_driver_register
module_i2c_driver(drv)I2Ci2c_add_driver
module_spi_driver(drv)SPIspi_register_driver
module_serio_driver(drv)Serioserio_register_driver
module_hid_driver(drv)HIDhid_register_driver
module_virtio_driver(drv)Virtioregister_virtio_driver
/* 실전 예: Platform 드라이버 완전한 예제 */
static int my_probe(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "probed\\n");
    return 0;
}

static void my_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "removed\\n");
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My Platform Driver");
💡

probe 초기화만 필요하고 remove가 불필요한 경우 builtin_platform_driver()를 사용할 수 있습니다 (빌트인 전용). 또한 module_platform_driver_probe()는 probe 함수를 __init 섹션에 배치하여 메모리를 절약합니다 (단, 모듈에서는 hotplug 제한).

모듈 버전 관리 (MODVERSIONS & ABI)

CONFIG_MODVERSIONS는 내보내진 심볼에 CRC 체크섬을 부착하여, 커널과 모듈 사이의 ABI 호환성을 런타임에 검증합니다. 커널이 업데이트되어 심볼의 프로토타입이 변경되면 CRC가 달라져 로딩이 거부됩니다.

# 모듈의 vermagic 확인
$ modinfo hello.ko | grep vermagic
vermagic:       6.1.0 SMP preempt mod_unload

# CRC 확인 (Module.symvers 파일)
$ head -3 Module.symvers
0x12345678	my_function	my_module	EXPORT_SYMBOL_GPL
0xabcdef01	another_func	my_module	EXPORT_SYMBOL

# vermagic 불일치 시 에러
$ sudo insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Invalid module format
# dmesg: "hello: version magic '6.1.0' should be '6.2.0'"

# 강제 로드 (위험! ABI 불일치로 크래시 가능)
$ sudo insmod -f hello.ko
⚠️

Linux 커널은 안정된 내부 ABI를 보장하지 않습니다. 커널 버전이 바뀌면 내부 API/ABI가 언제든 변경될 수 있으며, out-of-tree 모듈은 반드시 대상 커널 버전에 맞게 재빌드해야 합니다. DKMS(Dynamic Kernel Module Support)를 사용하면 커널 업데이트 시 자동 재빌드를 설정할 수 있습니다.

In-tree 모듈 개발 (Kconfig & Kbuild 통합)

커널 소스 트리 안에 모듈을 추가하면 Kconfig 메뉴 시스템과 Kbuild 빌드 시스템의 전체 기능을 활용할 수 있습니다. 메인라인 커널에 기여하려는 드라이버는 반드시 in-tree 방식으로 개발해야 하며, make menuconfig에서 선택 가능한 빌드 옵션으로 제공됩니다.

Kconfig 항목 작성

Kconfig 파일은 make menuconfig에 표시되는 설정 항목을 정의합니다. tristate로 선언하면 y(빌트인), m(모듈), n(비활성) 세 가지 상태를 가집니다.

# drivers/misc/Kconfig 에 추가

config MY_EXAMPLE_DRIVER
    tristate "My Example Driver support"
    depends on PCI && NET
    select FW_LOADER
    default n
    help
      This driver provides support for the Example hardware device.
      It requires PCI bus and network subsystem.

      To compile this driver as a module, choose M here: the module
      will be called my_example.

      If unsure, say N.

config MY_EXAMPLE_DEBUG
    bool "Enable debug logging for My Example Driver"
    depends on MY_EXAMPLE_DRIVER
    default n
    help
      Enable verbose debug logging. This adds overhead and should
      only be enabled for development.

config MY_EXAMPLE_BUFFER_SIZE
    int "Default buffer size (KB)"
    depends on MY_EXAMPLE_DRIVER
    range 4 1024
    default 64
    help
      Default ring buffer size in kilobytes.

Kconfig 지시자 종류

지시자 설명 예시
bool y/n 두 가지 상태. 모듈 빌드 불가 bool "Enable feature X"
tristate y/m/n 세 가지 상태. 모듈 빌드 지원 tristate "Driver Y support"
int 정수 값 설정 int "Buffer count" default 8
hex 16진수 값 설정 hex "Base address" default 0x1000
string 문자열 값 설정 string "Device name" default "mydev"
depends on 다른 설정에 의존. 미충족 시 메뉴에서 숨김 depends on PCI && NET
select 이 항목 활성화 시 지정 항목도 강제 활성화 select FW_LOADER
imply select와 유사하나 사용자가 해제 가능 imply HWMON
range int/hex의 허용 범위 지정 range 1 256
default 기본값 설정. 조건부 가능 default y if EXPERT
menuconfig 하위 항목을 가진 메뉴 항목 menuconfig MY_SUBSYSTEM

Kbuild Makefile 작성

Kbuild Makefile은 obj-$(CONFIG_*) 패턴을 사용하여 빌드 대상을 지정합니다. 여러 소스 파일로 구성된 모듈은 <module>-y 변수로 오브젝트 파일 목록을 지정합니다.

# drivers/misc/Makefile 에 추가

# CONFIG_MY_EXAMPLE_DRIVER=y → 빌트인, =m → 모듈, 없으면 빌드 안 함
obj-$(CONFIG_MY_EXAMPLE_DRIVER) += my_example.o

# 여러 소스 파일로 구성된 모듈
# my_example.ko = my_example_main.o + my_example_hw.o + my_example_net.o
my_example-y := my_example_main.o my_example_hw.o my_example_net.o

# 조건부 오브젝트 파일 (CONFIG에 따라 추가)
my_example-$(CONFIG_MY_EXAMPLE_DEBUG) += my_example_debug.o
In-tree 모듈 빌드 워크플로: Kconfig → .config → Kbuild → .ko Kconfig tristate/bool/int 메뉴 항목 정의 Kbuild Makefile obj-$(CONFIG_*) 빌드 규칙 정의 make menuconfig 사용자 설정 선택 CONFIG_*=y/m/n → .config 생성 .config CONFIG_*=m 커널 설정 파일 make modules Kbuild가 .config 참조 CONFIG=m인 항목만 빌드 .ko 커널 모듈 바이너리 modules_install /lib/modules/<ver>/ depmod 의존성 DB 생성 defconfig arch/*/configs/ 기본 설정 소스 정의 설정 단계 빌드 단계 출력/설치

완전한 드라이버 추가 예제

drivers/misc/에 새 드라이버를 추가하는 전체 과정입니다.

/* drivers/misc/my_example_main.c */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/pci.h>

#ifdef CONFIG_MY_EXAMPLE_DEBUG
#define MY_DBG(fmt, ...) pr_debug("my_example: " fmt, ##__VA_ARGS__)
#else
#define MY_DBG(fmt, ...) do {} while(0)
#endif

static int buffer_kb = CONFIG_MY_EXAMPLE_BUFFER_SIZE;
module_param(buffer_kb, int, 0644);
MODULE_PARM_DESC(buffer_kb, "Ring buffer size in KB");

static int __init my_example_init(void)
{
    MY_DBG("initializing with buffer_kb=%d\n", buffer_kb);
    pr_info("my_example: loaded (buffer=%dKB)\n", buffer_kb);
    return 0;
}

static void __exit my_example_exit(void)
{
    pr_info("my_example: unloaded\n");
}

module_init(my_example_init);
module_exit(my_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Developer");
MODULE_DESCRIPTION("Example in-tree kernel module");
MODULE_VERSION("1.0");

빌드 및 설정 명령

# menuconfig에서 드라이버 선택
$ make menuconfig
# Device Drivers → Misc devices → My Example Driver support → M

# 또는 커맨드라인에서 직접 설정
$ scripts/config --module MY_EXAMPLE_DRIVER
$ scripts/config --enable MY_EXAMPLE_DEBUG
$ scripts/config --set-val MY_EXAMPLE_BUFFER_SIZE 128

# .config 검증
$ grep MY_EXAMPLE .config
CONFIG_MY_EXAMPLE_DRIVER=m
CONFIG_MY_EXAMPLE_DEBUG=y
CONFIG_MY_EXAMPLE_BUFFER_SIZE=128

# 모듈 빌드
$ make modules M=drivers/misc

# 또는 전체 커널 빌드
$ make -j$(nproc)

# defconfig로 기본 설정 저장
$ make savedefconfig
$ cp defconfig arch/x86/configs/my_defconfig
💡

depends on은 의존 항목이 비활성이면 메뉴에서 숨기지만, select는 강제로 활성화합니다. select를 남용하면 의존성 순환이 발생할 수 있으므로, 가능하면 depends on이나 imply를 우선 사용하세요. implyselect와 동일하게 동작하되, 사용자가 명시적으로 해제할 수 있는 "약한" 의존성입니다.

모듈 압축과 initramfs (Module Compression & initramfs)

커널 5.x 이후 대부분의 배포판은 디스크 공간 절약을 위해 모듈 파일을 압축하여 저장합니다. .ko.gz, .ko.xz, .ko.zst 형식이 사용되며, modprobe는 압축된 모듈을 투명하게 처리합니다. 또한 부팅 초기 단계에서 필요한 모듈은 initramfs에 포함되어야 합니다.

모듈 압축 형식

형식 확장자 커널 설정 압축률 해제 속도 비고
없음 .ko CONFIG_MODULE_COMPRESS_NONE 압축 안 함 (개발 환경)
gzip .ko.gz CONFIG_MODULE_COMPRESS_GZIP 보통 빠름 가장 오래된 지원 형식
XZ .ko.xz CONFIG_MODULE_COMPRESS_XZ 높음 느림 최고 압축률, Fedora 기본
Zstandard .ko.zst CONFIG_MODULE_COMPRESS_ZSTD 높음 매우 빠름 Ubuntu 22.04+ 기본, 권장
# 현재 시스템의 모듈 압축 형식 확인
$ ls /lib/modules/$(uname -r)/kernel/drivers/net/ethernet/intel/e1000e/
e1000e.ko.zst

# 모듈 정보 조회 (압축된 상태에서도 동작)
$ modinfo e1000e
filename:       /lib/modules/6.1.0-amd64/kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko.zst

# 커널 설정 확인
$ zcat /proc/config.gz | grep MODULE_COMPRESS
CONFIG_MODULE_COMPRESS_ZSTD=y

# 수동 압축/해제
$ zstd -d my_module.ko.zst            # 해제
$ zstd -19 my_module.ko                 # 압축 (높은 압축률)
$ xz -d my_module.ko.xz               # XZ 해제
$ gzip -d my_module.ko.gz             # gzip 해제

CONFIG_MODULE_DECOMPRESS

커널 6.2에서 도입된 CONFIG_MODULE_DECOMPRESS커널 내부에서 압축된 모듈을 해제합니다. 이 옵션이 없으면 modprobe(사용자 공간)가 해제를 담당하고, 활성화하면 커널이 직접 해제하여 finit_module() 시스템 콜로 압축된 파일을 바로 전달할 수 있습니다.

# 커널 설정
CONFIG_MODULE_DECOMPRESS=y          # 커널 내 모듈 해제 활성화
CONFIG_MODULE_COMPRESS_ZSTD=y       # Zstd 압축 사용
부팅 시퀀스: initramfs → rootfs 모듈 로딩 과정 부팅 시간 → 부트로더 GRUB / systemd-boot vmlinuz 로드 initramfs 로드 initramfs (tmpfs) 필수 모듈 포함 (비압축) 디스크/FS/암호화 드라이버 루트 파일시스템 마운트 준비 pivot_root 실제 루트 FS로 전환 /lib/modules/<ver>/ 압축된 .ko 사용 가능 런타임 modprobe 자동 로딩 udev + hotplug .ko.zst 투명 해제 initramfs 내부 구조: / ├── bin/ (busybox, modprobe) ├── etc/ (modprobe.d/) ├── lib/ │ └── modules/<ver>/ │ ├── kernel/ (필수 .ko) │ ├── modules.dep │ └── modules.alias ├── init (초기화 스크립트) └── sbin/ (udevd) rootfs: /lib/modules/<ver>/ kernel/ ├── drivers/ (.ko.zst) ├── fs/ (.ko.zst) ├── net/ (.ko.zst) ├── sound/ (.ko.zst) └── crypto/ (.ko.zst) modules.dep.bin modules.alias.bin modules.symbols.bin

initramfs 관리 도구

배포판 생성 도구 갱신 명령 내용 확인
Debian/Ubuntu initramfs-tools update-initramfs -u lsinitramfs /boot/initrd.img-*
Fedora/RHEL dracut dracut -f lsinitrd /boot/initramfs-*
Arch Linux mkinitcpio mkinitcpio -P lsinitcpio /boot/initramfs-*
범용 dracut dracut --force --kver <ver> dracut --list-modules

initramfs에 모듈 추가

# ── Debian/Ubuntu (initramfs-tools) ──

# /etc/initramfs-tools/modules 에 모듈 이름 추가
$ echo "my_driver" | sudo tee -a /etc/initramfs-tools/modules

# initramfs 재생성
$ sudo update-initramfs -u -k $(uname -r)

# 포함 확인
$ lsinitramfs /boot/initrd.img-$(uname -r) | grep my_driver

# ── Fedora/RHEL (dracut) ──

# /etc/dracut.conf.d/my_driver.conf
$ echo 'add_drivers+=" my_driver "' | sudo tee /etc/dracut.conf.d/my_driver.conf

# initramfs 재생성
$ sudo dracut -f /boot/initramfs-$(uname -r).img $(uname -r)

# 포함 확인
$ lsinitrd /boot/initramfs-$(uname -r).img | grep my_driver

# ── Arch Linux (mkinitcpio) ──

# /etc/mkinitcpio.conf 의 MODULES 배열에 추가
# MODULES=(my_driver)
$ sudo mkinitcpio -P

initramfs vs initrd

항목 initramfs (현재) initrd (레거시)
형식 cpio 아카이브 (tmpfs에 풀림) 파일시스템 이미지 (ramdisk)
메모리 사용 필요한 만큼만 (tmpfs) 고정 크기 할당
모듈 접근 일반 파일처럼 접근 블록 장치로 마운트
커널 지원 2.6+ (현재 표준) 2.4 이하 (폐지 예정)
전환 pivot_root / switch_root pivot_root
💡

initramfs에 포함되는 모듈은 보통 압축하지 않습니다 (initramfs 자체가 압축되므로 이중 압축은 비효율적). dracut--no-compress 옵션이나 initramfs-toolsCOMPRESS=cat 설정으로 initramfs 압축을 끌 수도 있지만, 대부분의 경우 기본 설정이 최적입니다. 루트 파티션 드라이버, 파일시스템 드라이버, 디스크 암호화(dm-crypt) 모듈은 반드시 initramfs에 포함해야 부팅이 가능합니다.

커널 라이브 패치 (Kernel Livepatch)

Kernel Livepatch는 재부팅 없이 실행 중인 커널의 함수를 교체하는 기술입니다. 보안 취약점 수정이나 중요한 버그 패치를 다운타임 없이 적용할 수 있어, 고가용성이 필요한 서버 환경에서 핵심적인 역할을 합니다. Linux 4.0에서 kGraft(SUSE)와 kpatch(Red Hat)가 통합되어 livepatch API로 표준화되었습니다.

동작 원리

Livepatch는 ftrace 인프라를 활용합니다. 패치 대상 함수의 진입점에 ftrace 훅을 설치하고, 원래 함수 대신 패치된 새 함수로 제어를 리다이렉션합니다. 데이터 구조체 변경은 불가능하며, 오직 함수 단위 교체만 지원합니다.

커널 Livepatch 메커니즘: ftrace를 이용한 함수 리다이렉션 패치 전: 호출자 caller() 원래 함수 buggy_func() [취약] 패치 후 (livepatch 활성): 호출자 caller() ftrace 훅 fentry → redirect buggy_func() 실행 안 됨 패치 함수 klp_fixed_func() [수정] 리다이렉션! 일관성 모델 (Per-task Consistency): Task A 유저 공간 복귀 시 패치 전환 ✓ Task B 커널 코드 실행 중 전환 대기... Task C 유저 공간 복귀 시 패치 전환 ✓ → 모든 태스크 전환 완료 시 패치 "전환 완료" 상태

핵심 데이터 구조

구조체 헤더 설명
struct klp_patch <linux/livepatch.h> 패치 전체를 나타내는 최상위 구조체. klp_object 배열 포함
struct klp_object <linux/livepatch.h> 패치 대상 오브젝트 (vmlinux 또는 모듈). klp_func 배열 포함
struct klp_func <linux/livepatch.h> 개별 함수 패치 정보. 원래 함수명과 새 함수 포인터
struct klp_state <linux/livepatch.h> 누적 패치 간 상태 전달용 (원자적 교체 지원)

Livepatch 모듈 예제

/*
 * livepatch_example.c — 간단한 livepatch 모듈 예제
 * 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 klp_cmdline_proc_show(struct seq_file *m, void *v)
{
    seq_printf(m, "%s [LIVEPATCHED]\n", saved_command_line);
    return 0;
}

/* 패치 대상 함수 목록 */
static struct klp_func funcs[] = {
    {
        .old_name = "cmdline_proc_show",
        .new_func = klp_cmdline_proc_show,
    }, { }  /* 종료 센티널 */
};

/* 패치 대상 오브젝트 (vmlinux = 커널 본체) */
static struct klp_object objs[] = {
    {
        /* .name = NULL → vmlinux 대상 */
        .funcs = funcs,
    }, { }  /* 종료 센티널 */
};

/* 패치 구조체 */
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_enable_patch에서 등록한 패치는 모듈 언로드 시 자동 해제 */
}

module_init(livepatch_init);
module_exit(livepatch_exit);

MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");
MODULE_AUTHOR("Developer");
MODULE_DESCRIPTION("Example livepatch module");

Livepatch 관련 커널 설정

설정 설명 의존성
CONFIG_LIVEPATCH Livepatch 기능 활성화 DYNAMIC_FTRACE_WITH_REGS, MODULES, KALLSYMS
CONFIG_DYNAMIC_FTRACE_WITH_REGS 레지스터 저장을 포함한 동적 ftrace HAVE_DYNAMIC_FTRACE_WITH_REGS
CONFIG_KALLSYMS 커널 심볼 테이블 (함수 이름 해석)
CONFIG_KALLSYMS_ALL 모든 심볼 포함 (데이터 심볼 포함) KALLSYMS
CONFIG_STACK_VALIDATION objtool을 이용한 스택 프레임 검증 x86에서 권장
CONFIG_HAVE_RELIABLE_STACKTRACE 일관성 모델에 필요한 신뢰성 있는 스택 트레이스 아키텍처 의존

Sysfs 인터페이스와 관리

# livepatch 모듈 로드
$ sudo insmod livepatch_example.ko

# 패치 상태 확인
$ cat /sys/kernel/livepatch/livepatch_example/enabled
1

# 전환 완료 여부 (0 = 완료, 1 = 전환 중)
$ cat /sys/kernel/livepatch/livepatch_example/transition
0

# 패치 비활성화 (함수를 원래대로 복원)
$ echo 0 | sudo tee /sys/kernel/livepatch/livepatch_example/enabled

# 모듈 언로드 (비활성화 후에만 가능)
$ sudo rmmod livepatch_example

# 커널 taint 플래그 확인 (K = livepatch)
$ cat /proc/sys/kernel/tainted
# 비트 29 (값 536870912) = TAINT_LIVEPATCH

# 전환 강제 완료 (주의: 안전하지 않을 수 있음)
$ echo 0 | sudo tee /sys/kernel/livepatch/livepatch_example/force

kpatch-build를 이용한 Livepatch 생성

kpatch-build는 소스 패치(.patch 파일)로부터 livepatch 모듈(.ko)을 자동으로 생성하는 도구입니다. 수동으로 klp_patch 구조체를 작성할 필요 없이, 일반적인 커널 패치를 livepatch 모듈로 변환할 수 있습니다.

# kpatch 설치 (Fedora)
$ sudo dnf install kpatch kpatch-build

# kpatch 설치 (Ubuntu)
$ sudo apt install kpatch kpatch-build

# 소스 패치로부터 livepatch 모듈 생성
$ kpatch-build -t vmlinux security-fix.patch
# → livepatch-security-fix.ko 생성

# kpatch를 이용한 패치 적용
$ sudo kpatch load livepatch-security-fix.ko

# 적용된 패치 목록
$ kpatch list
Loaded patch modules:
livepatch_security_fix [enabled]

# 패치 영구 적용 (재부팅 후에도 유지)
$ sudo kpatch install livepatch-security-fix.ko

# 패치 해제
$ sudo kpatch unload livepatch-security-fix.ko

Livepatch 제한 사항

제한 설명
함수 단위만 교체 데이터 구조체(struct) 레이아웃 변경 불가. 함수 시그니처가 동일해야 함
인라인 함수 불가 컴파일러가 인라인한 함수는 ftrace 훅을 설치할 수 없음
__init 함수 불가 부팅 시 실행 후 메모리에서 해제된 함수는 패치 대상이 될 수 없음
아키텍처 제한 x86_64, arm64, ppc64le, s390x에서만 지원 (FTRACE_WITH_REGS 필요)
전환 시간 장시간 커널 코드에 머무는 태스크가 있으면 전환 완료 지연
일관성 모든 태스크가 유저 공간으로 복귀해야 전환 완료. kthreadklp_update_patch_state() 호출 필요

역사: kpatch, kGraft → 통합 livepatch API

Linux 커널의 라이브 패치 기능은 두 독립 프로젝트에서 시작되었습니다.

프로젝트 개발사 커널 버전 일관성 모델 현재 상태
kpatch Red Hat 3.x (out-of-tree) stop_machine() (전역 중지) kpatch-build 도구로 활용
kGraft SUSE 3.x (out-of-tree) per-task (태스크별 전환) 통합 API로 대체
livepatch 통합 4.0+ (in-tree) per-task + stack checking 현재 표준 API
⚠️

Livepatch 모듈을 로드하면 커널에 taint 플래그 'K'(TAINT_LIVEPATCH, 비트 29)가 설정됩니다. 이는 커널이 런타임에 수정되었음을 나타내며, 커널 크래시 보고 시 중요한 정보입니다. MODULE_INFO(livepatch, "Y")를 소스에 반드시 포함해야 커널이 해당 모듈을 livepatch로 인식합니다. 프로덕션 환경에서는 배포판 공급사(Red Hat, SUSE, Canonical)의 공식 livepatch 서비스를 사용하는 것이 권장됩니다.

흔한 실수와 해결

이 섹션의 목적: 커널 모듈을 작성할 때 초보자가 자주 범하는 실수를 미리 알아두면 디버깅 시간을 크게 줄일 수 있습니다.
❌ 실수 #1: MODULE_LICENSE 누락 심각도: 중간
#include <linux/module.h>

static int __init my_init(void) {
    pr_info("Module loaded\\n");
    return 0;
}
module_init(my_init);
/* MODULE_LICENSE가 없음! */

문제점: MODULE_LICENSE()를 선언하지 않으면:

  • 커널이 "Tainted" 상태가 됨 (dmesg에 경고)
  • GPL 전용 심볼(EXPORT_SYMBOL_GPL)에 접근 불가
  • 대부분의 커널 내부 API 사용 불가

✅ 올바른 방법
module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");  /* 필수! */
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Module description");
자동 탐지: ✅ 컴파일 타임에 경고 출력. 로드 시 dmesg에 "module license 'unspecified' taints kernel" 메시지.
❌ 실수 #2: init 함수에서 리소스 정리 누락 심각도: 높음
static int __init bad_init(void)
{
    struct device *dev1, *dev2;

    dev1 = device_create(...);
    if (IS_ERR(dev1))
        return PTR_ERR(dev1);

    dev2 = device_create(...);
    if (IS_ERR(dev2))
        return PTR_ERR(dev2);  /* dev1 누수! */

    return 0;
}

문제점: 두 번째 리소스 할당 실패 시 첫 번째 리소스를 정리하지 않아 메모리/리소스 누수 발생.

✅ 올바른 방법: goto를 활용한 정리 패턴
static int __init good_init(void)
{
    struct device *dev1, *dev2;
    int ret;

    dev1 = device_create(...);
    if (IS_ERR(dev1)) {
        ret = PTR_ERR(dev1);
        goto err_dev1;
    }

    dev2 = device_create(...);
    if (IS_ERR(dev2)) {
        ret = PTR_ERR(dev2);
        goto err_dev2;
    }

    return 0;

err_dev2:
    device_destroy(dev1);  /* 역순으로 정리 */
err_dev1:
    return ret;
}
디버깅 팁: CONFIG_DEBUG_KMEMLEAK=y로 메모리 누수 자동 탐지. cat /sys/kernel/debug/kmemleak으로 확인.
❌ 실수 #3: 사용 중인 모듈 강제 언로드 심각도: 높음
# 모듈이 다른 모듈에 의존되고 있는 상태
$ lsmod | grep my_module
my_module    16384  3  # Used by 3개!

# 강제 언로드 시도
$ sudo rmmod -f my_module  # 위험!

문제점: 다른 모듈이나 프로세스가 사용 중인 모듈을 강제로 제거하면 커널 패닉 또는 NULL 포인터 역참조 발생.

✅ 올바른 방법
# 의존하는 모듈 확인
$ lsmod | grep my_module
$ cat /sys/module/my_module/refcnt

# 의존 모듈 먼저 언로드
$ sudo modprobe -r dependent_module
$ sudo modprobe -r my_module  # 이제 안전
자동 탐지: ❌ 런타임에만 감지 가능. rmmod가 "Module is in use" 에러 반환.
❌ 실수 #4: printk 대신 printf 사용 심각도: 낮음
#include <stdio.h>  /* 커널에 없음! */

static int __init my_init(void) {
    printf("Hello\\n");  /* 컴파일 에러! */
    return 0;
}

문제점: 커널 공간에는 C 표준 라이브러리가 없음. printf, malloc, sleep 등 사용 불가.

✅ 올바른 방법: 커널 API 사용
#include <linux/kernel.h>

static int __init my_init(void) {
    pr_info("Hello\\n");         /* 커널 로깅 */
    ptr = kmalloc(size, GFP_KERNEL);  /* 메모리 할당 */
    msleep(100);                 /* 슬립 (밀리초) */
    return 0;
}
자동 탐지: ✅ 컴파일 타임 에러. "implicit declaration of function 'printf'"
❌ 실수 #5: 잘못된 헤더 경로 (유저 vs. 커널) 심각도: 낮음
#include <sys/types.h>   /* 유저 공간 헤더! */
#include <unistd.h>      /* 유저 공간 헤더! */
✅ 올바른 방법: 커널 헤더 사용
#include <linux/types.h>   /* 커널 타입 */
#include <linux/kernel.h>  /* 커널 유틸 */
#include <linux/module.h>  /* 모듈 매크로 */

예방 체크리스트

코드 작성 또는 리뷰 시 다음 항목을 확인하세요:

디버깅 도구

도구탐지 가능한 실수사용법
sparse 락 불균형, 컨텍스트 오류, 타입 오류 make C=1
kmemleak 메모리 누수 CONFIG_DEBUG_KMEMLEAK=y
cat /sys/kernel/debug/kmemleak
KASAN 메모리 접근 오류, use-after-free CONFIG_KASAN=y
lockdep 데드락, 락 순서 위반 CONFIG_PROVE_LOCKING=y
modprobe -n 의존성 문제 (dry-run) modprobe -n -v module_name

실전 모듈 배포 전 점검

모듈이 "빌드되고 로드된다"는 것만으로는 충분하지 않습니다. 반복 로드/언로드, 실패 경로, 버전 호환성까지 확인해야 운영 환경에서 안전합니다.

필수 점검 항목

  1. 수명주기: init 성공/실패 경로, exit 정리 경로가 대칭인지 확인
  2. 동시성: ioctl, sysfs, workqueue, timer가 동시에 접근해도 데이터 경합이 없는지 확인
  3. 참조 안정성: 언로드 중 콜백 진입 가능성(timer/work/irq)을 모두 동기화 취소
  4. 호환성: 대상 커널 버전/CONFIG 조합에서 빌드되는지 확인
  5. 로그 품질: 실패 원인을 좁힐 수 있는 최소한의 pr_err 문맥 포함
# 반복 로드/언로드 스트레스
for i in $(seq 1 100); do
  sudo insmod mymod.ko || break
  sudo rmmod mymod || break
done

# 모듈 정보/의존성 확인
modinfo mymod.ko
modprobe -n -v mymod

# 런타임 로그 확인
dmesg | tail -n 200
검증 항목 위험 신호 대응
반복 로드/언로드 간헐 크래시, refcount 경고 종료 경로 동기화(cancel_work_sync, del_timer_sync) 강화
에러 경로 실패 시 누수 발생 레이블 기반 정리 순서 재구성
버전 불일치 invalid module format 커널/헤더/툴체인 일치 여부 재확인

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