커널 모듈 (Kernel Modules)
Loadable Kernel Module(LKM)의 작성부터 빌드, 로딩, 파라미터, 의존성 관리, 서명, 디버깅까지 커널 모듈의 모든 것을 다룹니다.
obj-m으로 모듈을 빌드하는 Kbuild 규칙을 이해하면 이 문서가 훨씬 쉬워집니다.
insmod는 "앱 설치", rmmod는 "앱 삭제"에 해당합니다.
핵심 요약
- 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 함수가 호출되어 모듈이 사용한 리소스를 정리합니다.
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에 포함해야 합니다.
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는 GPL 전용이므로, 실질적으로 MODULE_LICENSE("GPL")은 필수입니다.
모듈 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
모듈 파라미터 (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() 시스템 콜부터 모듈 초기화까지의 내부 흐름을 보여줍니다.
커널 6.x 이후, finit_module() 시스템 콜이 선호됩니다. 이 호출은 파일 디스크립터를 받아 커널이 직접 파일을 읽으므로, 사용자 공간에서 전체 .ko 파일을 메모리에 올릴 필요가 없습니다.
모듈 의존성과 심볼 테이블 (Module Dependencies & Symbol Table)
EXPORT_SYMBOL
커널 모듈이 다른 모듈에서 사용할 수 있는 함수나 변수를 내보내려면 EXPORT_SYMBOL() 또는 EXPORT_SYMBOL_GPL() 매크로를 사용합니다.
내보내진 심볼은 커널 심볼 테이블에 등록되어 다른 모듈에서 링크할 수 있게 됩니다.
/* 공유 함수를 제공하는 모듈: 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;
}
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 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 |
서명에 사용할 키 파일 경로 |
# 커널 빌드 시 모듈 서명 (자동)
$ make modules
$ make modules_sign
# 수동으로 모듈 서명
$ 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 플래그가 설정되며, 이후 커널 버그 리포트 시 지원을 받기 어려울 수 있습니다.
디버깅 (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를 실행해야 자동 로딩이 동작합니다.
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)를 사용하면 커널 업데이트 시 자동 재빌드를 설정할 수 있습니다.