커널 모듈 (Kernel Modules)
Loadable Kernel Module(LKM)의 작성부터 빌드, 로딩, 파라미터, 의존성 관리, 서명, 디버깅까지 커널 모듈의 모든 것을 다룹니다.
핵심 요약
- LKM — Loadable Kernel Module.
.ko확장자를 가진 커널 오브젝트 파일입니다. - module_init / module_exit — 모듈이 로드/언로드될 때 호출되는 진입점 함수를 등록합니다.
- insmod / rmmod / modprobe — 모듈을 로드/언로드하는 명령어.
modprobe는 의존성을 자동 해결합니다. - module_param() — 모듈 로드 시 파라미터를 전달할 수 있게 해 주는 매크로입니다.
- EXPORT_SYMBOL — 다른 모듈에서 사용할 수 있도록 심볼을 내보내는 매크로입니다.
단계별 이해
- 소스 작성 —
module_init()과module_exit()을 포함하는 C 파일을 작성합니다.최소한의 Hello World 모듈은 10줄 이내로 작성할 수 있습니다.
- Makefile 작성 —
obj-m += hello.o로 모듈 대상을 선언하고 Kbuild를 호출합니다.커널 소스 트리의 빌드 시스템을 활용하여
.ko파일을 생성합니다. - 빌드 및 로드 —
make로 빌드 후sudo insmod hello.ko로 커널에 로드합니다.dmesg로 커널 로그를 확인하면 모듈의 init 함수 출력을 볼 수 있습니다. - 언로드 —
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 여유 공간
- 환경 준비 및 패키지 설치 (5분)
- 작업 디렉토리 생성 및 소스 코드 작성 (10분)
- Makefile 작성 (5분)
- 빌드 및 에러 수정 (5분)
- 모듈 로드 및 테스트 (3분)
- 로그 확인 및 분석 (2분)
- 정리 및 언로드 (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.c와 Makefile 두 파일이 있어야 합니다.
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의 주요 장점은 다음과 같습니다:
- 동적 로딩 - 필요할 때만 커널에 로드하여 메모리 사용을 최적화합니다.
- 재부팅 불필요 - 커널을 다시 컴파일하거나 재부팅하지 않고도 기능을 추가/제거할 수 있습니다.
- 개발 편의성 - 드라이버 개발 시 빠른 테스트 사이클을 제공합니다.
- 배포 유연성 - 하드웨어에 맞는 모듈만 선택적으로 로드할 수 있습니다.
- 라이선스 분리 - 프로프라이어터리 드라이버를 별도 모듈로 제공할 수 있습니다(단, GPL 심볼 사용 제한).
커널 모듈 파일의 확장자는 .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) 종료 문자열 형태로 저장됩니다. objdump나 modinfo로 확인할 수 있습니다:
# .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 헤더 확인 — 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
readelf와 objdump는 binutils 패키지에 포함되어 있습니다. 크로스 컴파일 환경에서는 aarch64-linux-gnu-readelf와 같이 타겟 아키텍처용 도구를 사용해야 합니다.
모듈 메모리 레이아웃 (Module Memory Layout in Kernel)
커널 모듈이 로드되면 커널 가상 주소 공간의 특정 영역에 배치됩니다.
이 영역은 아키텍처마다 다르며, module_alloc() 함수가 모듈용 메모리를 할당합니다.
모듈 코드는 커널 텍스트 근처에 위치해야 하는데, 이는 상대 점프 명령어의 오프셋 범위 제한 때문입니다.
아키텍처별 모듈 영역
| 아키텍처 | 모듈 가상 주소 범위 | 매크로 | 비고 |
|---|---|---|---|
| x86_64 | 0xffffffffa0000000 – 0xfffffffffeffffff |
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 섹션을 두 가지 영역으로 분류합니다:
- Core 레이아웃 (
core_layout) — 모듈이 로드된 동안 계속 유지되는 섹션들입니다..text,.rodata,.data,.bss등이 포함됩니다. - Init 레이아웃 (
init_layout) — 초기화 시에만 필요한 섹션들입니다..init.text,.init.data,.init.rodata등이 포함되며,module_init()함수 실행 완료 후 메모리에서 해제됩니다.
/* 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/kallsyms도 kptr_restrict sysctl 설정에 따라 비root 사용자에게는 주소가 0으로 표시됩니다.
모듈 메모리 레이아웃 다이어그램
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");
코드의 핵심 요소를 살펴보겠습니다:
<linux/init.h>-__init,__exit매크로를 정의합니다.__init으로 표시된 함수는 초기화 완료 후 메모리에서 해제됩니다.<linux/module.h>-module_init(),module_exit()및 모듈 관련 매크로를 정의합니다.<linux/kernel.h>-pr_info()등 커널 로깅 매크로를 제공합니다.module_init()- 모듈 로드 시 호출될 초기화 함수를 등록합니다.module_exit()- 모듈 언로드 시 호출될 정리 함수를 등록합니다.
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_SYMBOL과 EXPORT_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
각 요소의 의미는 다음과 같습니다:
obj-m += hello.o-hello.c를hello.ko모듈로 빌드하라는 지시입니다.-m은 모듈을 의미합니다.KDIR- 커널 빌드 디렉터리를 가리킵니다. 커널 헤더와 Kbuild 인프라가 여기에 있습니다.-C $(KDIR)- 커널 빌드 디렉터리로 이동하여 커널의 Makefile을 사용합니다.M=$(PWD)- 모듈 소스가 있는 디렉터리를 지정합니다.
빌드 및 로드 과정:
# 모듈 빌드
$ 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
insmod와 rmmod는 모듈을 직접 로드/언로드하는 저수준 명령입니다.
# 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: 모듈 이름만 지정.
modules.dep를 읽어 의존성 자동 해결. 프로덕션 환경 권장 - lsmod:
/proc/modules를 읽어 현재 로드된 모듈 목록 표시 - 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)
커널 모듈은 로드부터 언로드까지 명확한 상태 전이를 거칩니다. 아래 다이어그램은 모듈의 전체 라이프사이클을 보여줍니다.
모듈 로딩 과정 다이어그램 (Module Loading Process)
insmod 또는 modprobe를 실행하면 커널 내부에서 복잡한 로딩 과정이 진행됩니다.
아래 다이어그램은 init_module() 시스템 콜부터 모듈 초기화까지의 내부 흐름을 보여줍니다.
finit_module() 시스템 콜은 파일 디스크립터를 받아 커널이 직접 파일을 읽는 경로로 널리 사용됩니다. 이 방식은 사용자 공간에서 전체 .ko 파일을 메모리에 올리는 부담을 줄일 수 있습니다.
참조 카운팅과 모듈 사용 추적 (Reference Counting & Module Usage Tracking)
커널 모듈은 다른 커널 서브시스템이나 사용자 프로세스에 의해 사용될 수 있습니다.
모듈이 사용 중인 상태에서 언로드되면 커널 패닉이 발생하므로,
커널은 참조 카운팅(reference counting) 메커니즘으로 모듈의 사용 여부를 추적합니다.
struct module의 refcnt 필드가 이 역할을 담당합니다.
참조 카운트 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()의 경쟁 조건
rmmod와 open()이 동시에 발생하면 경쟁 조건이 생길 수 있습니다.
커널은 이를 다음과 같이 처리합니다:
rmmod은 먼저 모듈 상태를MODULE_STATE_GOING으로 변경합니다.- 이후
open()에서try_module_get()이 호출되면 상태 검사에서 실패하여false를 반환합니다. - VFS는
try_module_get()실패 시open()에-ENODEV를 반환합니다. rmmod은refcnt가 0이 될 때까지 대기한 후 모듈을 해제합니다.
이 메커니즘은 CONFIG_MODULE_UNLOAD가 활성화된 경우에만 동작합니다. 이 옵션이 비활성화되면 모듈을 언로드할 수 없으며, 참조 카운트도 관리되지 않습니다. 대부분의 배포판 커널은 이 옵션을 활성화합니다.
참조 카운팅 흐름 다이어그램
try_module_get(THIS_MODULE)으로 자기 자신의 참조를 획득하는 패턴은 모듈이 스스로를 언로드 불가능하게 만드는 것이므로 신중하게 사용해야 합니다. 반드시 대응하는 module_put(THIS_MODULE)이 있어야 하며, 에러 경로에서도 빠짐없이 호출되어야 합니다. 참조 누수(leak)가 발생하면 모듈을 영원히 언로드할 수 없게 됩니다.
모듈 의존성과 심볼 테이블 (Module Dependencies & Symbol Table)
왜 모듈 간 의존성이 필요한가요?
일상 비유: 스마트폰 앱이 카메라 기능을 사용하려면 먼저 카메라 앱이 설치되어 있어야 하는 것처럼, 커널 모듈도 다른 모듈이 제공하는 기능을 사용할 수 있습니다.
예를 들어, ext4 파일시스템 모듈은 jbd2(저널링) 모듈의 기능이 필요합니다. 이런 관계를 "의존성(dependency)"이라고 합니다.
핵심 3단계 이해:
-
1단계: 공유 (EXPORT_SYMBOL)
모듈 A가 함수를 만들고 "다른 모듈도 사용해!" 하고 공개합니다.
→EXPORT_SYMBOL(my_function)으로 선언 -
2단계: 사용 (extern 선언)
모듈 B가 "모듈 A의 함수를 쓰고 싶어요" 하고 선언합니다.
→extern int my_function(...); -
3단계: 순서대로 로드 (depmod + modprobe)
모듈 A를 먼저 로드한 후, 모듈 B를 로드해야 합니다.
→modprobe가 자동으로 순서를 맞춰줌
실제 예시:
| 모듈 | 역할 | 의존성 |
|---|---|---|
ext4.ko |
ext4 파일시스템 | → jbd2.ko, mbcache.ko |
bluetooth.ko |
블루투스 스택 | → rfkill.ko, crypto.ko |
hello.ko |
우리가 만든 간단한 모듈 | 없음 (독립 모듈) |
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, 네트워크 필터링, 암호화 프레임워크, 파일시스템, 입력 장치 등 수백 개의 드라이버가 공통 프레임워크 위에 구축되어 있습니다.
스택킹 패턴: 코어 + 위성 모듈
모듈 스택킹의 핵심 패턴은 코어 모듈이 프레임워크 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 의존성 해석 순서
modprobe는 modules.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 경계를 명확히 하여 장기적인 커널 유지보수를 용이하게 합니다.
네임스페이스 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하는 네임스페이스를 확인할 수 있습니다.
depmod와 modprobe는 네임스페이스 요구사항을 자동으로 처리합니다.
# 모듈이 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 모드가 활성화됩니다.
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.conf에 sign_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_emerg | 0 | 시스템 사용 불가 | 패닉 직전 |
pr_alert | 1 | 즉시 조치 필요 | 하드웨어 장애 |
pr_crit | 2 | 치명적 상태 | 심각한 오류 |
pr_err | 3 | 오류 상태 | 일반 오류 |
pr_warn | 4 | 경고 상태 | 비정상이지만 복구 가능 |
pr_notice | 5 | 정상이지만 주목할 상태 | 설정 변경 등 |
pr_info | 6 | 정보성 | 드라이버 초기화 메시지 |
pr_debug | 7 | 디버그 | 개발 중 디버깅 (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 | -5 | I/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)
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)이 없을 수 있습니다.
주요 항목 상세
| 경로 | 유형 | 권한 | 설명 |
|---|---|---|---|
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.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 메시지에 표시되며, 버그 리포트 시 중요한 정보입니다.
| 문자 | 비트 | 의미 |
|---|---|---|
P | 0 | 프로프라이어터리 모듈 로드됨 (non-GPL) |
F | 1 | 모듈이 강제 로드됨 (insmod -f) |
S | 2 | SMP 커널에서 SMP 비안전 모듈 로드 |
R | 3 | 모듈이 강제 언로드됨 (rmmod -f) |
M | 4 | Machine Check Exception (하드웨어 오류) |
B | 5 | Bad page 참조 (페이지 해제 오류) |
U | 6 | 사용자 요청에 의한 taint |
D | 7 | Oops 발생 (커널 경고) |
W | 8 | WARN 발생 |
C | 9 | 스테이징 드라이버 로드됨 |
E | 15 | 서명되지 않은 모듈 로드됨 |
K | 17 | 커널 라이브 패치 적용됨 |
# 현재 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) | PCI | pci_register_driver |
module_usb_driver(drv) | USB | usb_register |
module_platform_driver(drv) | Platform | platform_driver_register |
module_i2c_driver(drv) | I2C | i2c_add_driver |
module_spi_driver(drv) | SPI | spi_register_driver |
module_serio_driver(drv) | Serio | serio_register_driver |
module_hid_driver(drv) | HID | hid_register_driver |
module_virtio_driver(drv) | Virtio | register_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
완전한 드라이버 추가 예제
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를 우선 사용하세요. imply는 select와 동일하게 동작하되, 사용자가 명시적으로 해제할 수 있는 "약한" 의존성입니다.
모듈 압축과 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 관리 도구
| 배포판 | 생성 도구 | 갱신 명령 | 내용 확인 |
|---|---|---|---|
| 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-tools의 COMPRESS=cat 설정으로 initramfs 압축을 끌 수도 있지만, 대부분의 경우 기본 설정이 최적입니다. 루트 파티션 드라이버, 파일시스템 드라이버, 디스크 암호화(dm-crypt) 모듈은 반드시 initramfs에 포함해야 부팅이 가능합니다.
커널 라이브 패치 (Kernel Livepatch)
Kernel Livepatch는 재부팅 없이 실행 중인 커널의 함수를 교체하는 기술입니다. 보안 취약점 수정이나 중요한 버그 패치를 다운타임 없이 적용할 수 있어, 고가용성이 필요한 서버 환경에서 핵심적인 역할을 합니다. Linux 4.0에서 kGraft(SUSE)와 kpatch(Red Hat)가 통합되어 livepatch API로 표준화되었습니다.
동작 원리
Livepatch는 ftrace 인프라를 활용합니다. 패치 대상 함수의 진입점에 ftrace 훅을 설치하고, 원래 함수 대신 패치된 새 함수로 제어를 리다이렉션합니다. 데이터 구조체 변경은 불가능하며, 오직 함수 단위 교체만 지원합니다.
핵심 데이터 구조
| 구조체 | 헤더 | 설명 |
|---|---|---|
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 필요) |
| 전환 시간 | 장시간 커널 코드에 머무는 태스크가 있으면 전환 완료 지연 |
| 일관성 | 모든 태스크가 유저 공간으로 복귀해야 전환 완료. kthread는 klp_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 서비스를 사용하는 것이 권장됩니다.
흔한 실수와 해결
#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");
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;
}
문제점: 두 번째 리소스 할당 실패 시 첫 번째 리소스를 정리하지 않아 메모리/리소스 누수 발생.
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으로 확인.
# 모듈이 다른 모듈에 의존되고 있는 상태
$ 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" 에러 반환.
#include <stdio.h> /* 커널에 없음! */
static int __init my_init(void) {
printf("Hello\\n"); /* 컴파일 에러! */
return 0;
}
문제점: 커널 공간에는 C 표준 라이브러리가 없음. printf, malloc, sleep 등 사용 불가.
#include <linux/kernel.h>
static int __init my_init(void) {
pr_info("Hello\\n"); /* 커널 로깅 */
ptr = kmalloc(size, GFP_KERNEL); /* 메모리 할당 */
msleep(100); /* 슬립 (밀리초) */
return 0;
}
#include <sys/types.h> /* 유저 공간 헤더! */
#include <unistd.h> /* 유저 공간 헤더! */
#include <linux/types.h> /* 커널 타입 */
#include <linux/kernel.h> /* 커널 유틸 */
#include <linux/module.h> /* 모듈 매크로 */
예방 체크리스트
코드 작성 또는 리뷰 시 다음 항목을 확인하세요:
- [ ]
MODULE_LICENSE("GPL")선언했는가? - [ ] 모든
kmalloc호출에 NULL 체크가 있는가? - [ ] init 함수 에러 경로에서 모든 리소스를 정리하는가?
- [ ] exit 함수에서 init에서 할당한 리소스를 모두 해제하는가?
- [ ] 커널 API만 사용하고 유저 공간 함수(printf, malloc 등)는 없는가?
- [ ] 모듈이 사용 중일 때 언로드를 시도하지 않는가?
- [ ]
printk또는pr_*매크로를 사용하는가?
디버깅 도구
| 도구 | 탐지 가능한 실수 | 사용법 |
|---|---|---|
sparse |
락 불균형, 컨텍스트 오류, 타입 오류 | make C=1 |
kmemleak |
메모리 누수 | CONFIG_DEBUG_KMEMLEAK=ycat /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 |
실전 모듈 배포 전 점검
모듈이 "빌드되고 로드된다"는 것만으로는 충분하지 않습니다. 반복 로드/언로드, 실패 경로, 버전 호환성까지 확인해야 운영 환경에서 안전합니다.
필수 점검 항목
- 수명주기:
init성공/실패 경로,exit정리 경로가 대칭인지 확인 - 동시성: ioctl, sysfs, workqueue, timer가 동시에 접근해도 데이터 경합이 없는지 확인
- 참조 안정성: 언로드 중 콜백 진입 가능성(timer/work/irq)을 모두 동기화 취소
- 호환성: 대상 커널 버전/CONFIG 조합에서 빌드되는지 확인
- 로그 품질: 실패 원인을 좁힐 수 있는 최소한의
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 | 커널/헤더/툴체인 일치 여부 재확인 |
관련 문서
커널 모듈과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.