첫 번째 커널 모듈 만들기
Hello World 모듈부터 캐릭터 디바이스 구현까지 단계별로 따라하며 커널 모듈 개발의 기초를 확립합니다.
핵심 요약
- module_init / module_exit — 모듈 로드/언로드 시 실행될 함수를 등록하는 매크로입니다.
- MODULE_LICENSE — 라이선스를 명시하는 매크로. GPL 라이선스는 모든 커널 심볼 접근을 허용합니다.
- insmod / rmmod — 모듈을 커널에 로드/언로드하는 명령어입니다.
- module_param — 모듈 로드 시 파라미터를 전달받을 수 있게 합니다.
- file_operations — 캐릭터 디바이스의 read, write, ioctl 등의 동작을 정의하는 구조체입니다.
단계별 이해
- Hello World 모듈
가장 간단한 형태의 모듈을 작성하여 빌드부터 로드까지의 전체 흐름을 익힙니다. - 파라미터 추가
모듈에 런타임 설정을 전달하는 방법을 배웁니다. - sysfs 속성 추가
유저 공간에서 모듈 상태를 조회하고 제어하는 인터페이스를 만듭니다. - 캐릭터 디바이스 구현
실제 동작하는 디바이스 드라이버의 뼈대를 완성합니다.
1단계: Hello World 모듈
환경 준비
커널 모듈을 빌드하려면 커널 헤더 파일이 필요합니다. 현재 실행 중인 커널 버전에 맞는 헤더를 설치하세요.
# 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
소스 코드 작성
작업 디렉토리를 만들고 hello.c 파일을 생성합니다.
mkdir -p ~/kernel_tutorial/hello
cd ~/kernel_tutorial/hello
vim hello.c # 또는 nano, gedit 등 선호하는 에디터 사용
다음은 가장 기본적인 커널 모듈 코드입니다.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
pr_info("Hello World! 모듈이 로드되었습니다.\\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("Goodbye! 모듈이 언로드되었습니다.\\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("간단한 Hello World 커널 모듈");
MODULE_VERSION("1.0");
코드 설명
-
1-3행
커널 모듈 개발에 필요한 헤더 파일 포함.
module.h는 모듈 매크로,kernel.h는 커널 로그 함수,init.h는 초기화/종료 섹션 매크로를 제공합니다. -
5-9행
__init매크로는 이 함수가 초기화 후 메모리에서 해제될 수 있음을 표시합니다.module_init()이 이 함수를 모듈 로드 시 호출할 진입점으로 등록합니다. 성공 시 0, 실패 시 음수(errno)를 반환해야 합니다. -
11-14행
__exit매크로는 이 함수가 모듈이 커널에 직접 빌트인될 경우 사용되지 않음을 표시합니다.module_exit()이 이 함수를 언로드 시 호출할 함수로 등록합니다. - 16-17행 모듈의 초기화 및 종료 함수를 커널에 등록하는 매크로입니다. 이 두 매크로는 반드시 존재해야 합니다.
-
19-22행
모듈 메타데이터를 정의합니다.
MODULE_LICENSE("GPL")은 필수이며, 이를 누락하면 "tainted" 상태가 되어 일부 커널 심볼에 접근할 수 없습니다.
Makefile 작성
같은 디렉토리에 Makefile을 생성합니다.
# Makefile
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
코드 설명
-
2행
obj-m은 모듈로 빌드할 대상을 지정합니다.hello.o는hello.c로부터 생성되며, 최종적으로hello.ko파일이 만들어집니다. - 4행 현재 실행 중인 커널의 빌드 시스템 경로를 가리킵니다. 이 디렉토리에는 Kbuild 파일들과 헤더 파일들이 있습니다.
-
8행
-C $(KDIR)는 커널 빌드 시스템으로 이동하고,M=$(PWD)는 외부 모듈 소스 위치를 알려줍니다.modules타겟은 모듈을 빌드합니다.
빌드 및 확인
이제 모듈을 빌드합니다.
make
빌드가 성공하면 다음 파일들이 생성됩니다.
ls -lh
# hello.c
# hello.ko ← 로드할 커널 모듈
# hello.mod.c ← 자동 생성된 모듈 메타데이터
# hello.mod.o
# hello.o
# modules.order
# Module.symvers
hello.ko가 최종 결과물입니다. 이 파일을 커널에 로드하게 됩니다.
모듈 로드 및 테스트
모듈을 커널에 로드하고 커널 로그를 확인합니다.
# 모듈 로드 (root 권한 필요)
sudo insmod hello.ko
# 커널 로그 확인 (마지막 10줄)
dmesg | tail -10
# 로드된 모듈 목록 확인
lsmod | grep hello
# 모듈 정보 확인
modinfo hello.ko
예상 출력:
$ dmesg | tail -10
[ 123.456789] Hello World! 모듈이 로드되었습니다.
$ lsmod | grep hello
hello 16384 0
$ modinfo hello.ko
filename: /home/user/kernel_tutorial/hello/hello.ko
version: 1.0
description: 간단한 Hello World 커널 모듈
author: Your Name
license: GPL
srcversion: ABCDEF123456
depends:
retpoline: Y
name: hello
vermagic: 6.1.0-18-amd64 SMP preempt mod_unload modversions
모듈 언로드
모듈을 제거하고 확인합니다.
# 모듈 언로드
sudo rmmod hello
# 커널 로그 확인
dmesg | tail -5
예상 출력:
$ dmesg | tail -5
[ 123.456789] Hello World! 모듈이 로드되었습니다.
[ 456.789012] Goodbye! 모듈이 언로드되었습니다.
모듈 생명주기 이해
커널 모듈은 소스 코드 작성부터 커널에 적재되어 실행되기까지 명확한 생명주기를 거칩니다. 아래 다이어그램은 이 전체 흐름을 한눈에 보여줍니다.
그림 1. 커널 모듈의 전체 생명주기 흐름
빌드 과정 상세
make 명령을 실행하면 Kbuild 시스템이 내부적으로 여러 단계를 거쳐 최종 .ko 파일을 생성합니다.
그림 2. Kbuild 시스템의 모듈 빌드 세부 과정
모듈 ELF 섹션 구조
.ko 파일은 표준 ELF 형식이지만, 커널 모듈 전용 섹션들을 포함합니다. readelf -S hello.ko 명령으로 직접 확인할 수 있습니다.
그림 3. .ko 파일의 ELF 섹션 레이아웃
__init 매크로가 붙은 함수와 데이터는 ELF의 .init.text / .init.data 섹션에 배치됩니다. 모듈이 성공적으로 초기화된 뒤 커널은 이 섹션의 메모리를 자동으로 해제합니다. 한 번만 실행되는 초기화 코드가 상주 메모리를 낭비하지 않도록 하는 최적화입니다.
__exit 매크로가 붙은 함수는 ELF의 .exit.text 섹션에 배치됩니다. 만약 모듈이 obj-y로 커널에 빌트인(내장)되는 경우, __exit 함수는 아예 최종 바이너리에 포함되지 않습니다. 빌트인 코드는 언로드될 수 없으므로 종료 함수 자체가 불필요하기 때문입니다.
# ELF 섹션 목록 확인
readelf -S hello.ko
# 모듈 정보(modinfo 섹션) 확인
readelf -p .modinfo hello.ko
# 심볼 테이블 확인
nm hello.ko
# __init 함수가 .init.text에 있는지 확인
objdump -t hello.ko | grep init
2단계: 모듈 파라미터 추가
모듈 로드 시 파라미터를 전달하면 코드 수정 없이 동작을 변경할 수 있습니다. 이는 디버깅이나 설정 조정에 매우 유용합니다.
소스 코드 (hello_param.c)
새로운 파일 hello_param.c를 생성합니다.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
static char *name = "World";
static int count = 1;
static bool debug = false;
module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "인사할 대상 이름 (기본값: World)");
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "메시지 반복 횟수 (기본값: 1)");
module_param(debug, bool, 0644);
MODULE_PARM_DESC(debug, "디버그 모드 활성화 (기본값: false)");
static int __init hello_init(void)
{
int i;
if (debug)
pr_debug("디버그 모드로 모듈 로드 시작\\n");
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");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("파라미터를 받는 커널 모듈");
MODULE_VERSION("1.1");
코드 설명
- 6-8행 모듈 파라미터로 사용할 전역 변수를 선언합니다. 기본값을 설정할 수 있습니다.
-
10-11행
module_param(변수명, 타입, 권한)매크로로 파라미터를 등록합니다. 세 번째 인자는/sys/module/모듈명/parameters/아래에 생성될 파일의 권한입니다.0644는 읽기 전용입니다. -
13-17행
MODULE_PARM_DESC는 파라미터 설명을 추가합니다.modinfo명령으로 확인할 수 있습니다. - 23-24행 디버그 파라미터가 true일 때만 디버그 메시지를 출력합니다.
-
26-28행
count파라미터 값만큼 반복하여 메시지를 출력합니다.
Makefile 수정
="macro">obj-m += hello_param.o
KDIR := /lib/modules/="fn">$(shell uname -r)/build
PWD := ="fn">$(shell pwd)
all:
="fn">$(MAKE) -C ="fn">$(KDIR) M=="fn">$(PWD) modules
clean:
="fn">$(MAKE) -C ="fn">$(KDIR) M=="fn">$(PWD) clean
빌드 및 테스트
# 빌드
make
# 파라미터 정보 확인
modinfo hello_param.ko | grep parm
# 기본값으로 로드
sudo insmod hello_param.ko
dmesg | tail -5
# 파라미터를 전달하여 로드
sudo rmmod hello_param
sudo insmod hello_param.ko name="Linux" count=3 debug=1
dmesg | tail -10
# sysfs를 통해 파라미터 확인
cat /sys/module/hello_param/parameters/name
cat /sys/module/hello_param/parameters/count
cat /sys/module/hello_param/parameters/debug
# 정리
sudo rmmod hello_param
예상 출력:
$ modinfo hello_param.ko | grep parm
parm: debug:디버그 모드 활성화 (기본값: false) (bool)
parm: count:메시지 반복 횟수 (기본값: 1) (int)
parm: name:인사할 대상 이름 (기본값: World) (charp)
$ dmesg | tail -5
[ 789.012345] [1/3] Hello, Linux!
[ 789.012347] [2/3] Hello, Linux!
[ 789.012348] [3/3] Hello, Linux!
0666 등으로 설정하면 모든 사용자가 쓰기 가능해집니다.
보안 위험이 있으므로 신중하게 설정하세요. 대부분의 경우 0444(읽기 전용) 또는 0644(root만 쓰기)를 사용합니다.
파라미터 처리 흐름
아래 다이어그램은 module_param() 매크로가 내부적으로 어떻게 동작하고, 파라미터 값이 어디에 반영되는지 보여줍니다.
그림 4. module_param() 매크로의 파라미터 처리 흐름
지원하는 파라미터 타입
| 타입 이름 | C 타입 | 설명 | 예시 |
|---|---|---|---|
bool |
bool |
참/거짓 (0, 1, Y, N) | insmod m.ko debug=1 |
int |
int |
정수 | insmod m.ko count=42 |
uint |
unsigned int |
부호 없는 정수 | insmod m.ko size=1024 |
long |
long |
긴 정수 | insmod m.ko addr=0xFF |
charp |
char * |
문자열 포인터 | insmod m.ko name="hello" |
short |
short |
짧은 정수 | insmod m.ko port=80 |
배열 파라미터
module_param_array() 매크로를 사용하면 배열 형태의 파라미터를 받을 수 있습니다.
static int ports[4] = { 0 };
static int port_count = 0;
/* 네 번째 인자: 실제 전달된 원소 수를 저장할 변수 */
module_param_array(ports, int, &port_count, 0444);
MODULE_PARM_DESC(ports, "포트 번호 배열 (최대 4개)");
# 배열 파라미터 전달 (쉼표로 구분)
sudo insmod mymod.ko ports=80,443,8080,8443
# 확인
cat /sys/module/mymod/parameters/ports
3단계: sysfs 속성 추가
sysfs는 커널 객체를 파일시스템으로 노출하는 메커니즘입니다. 모듈에 sysfs 속성을 추가하면 유저 공간에서 실시간으로 상태를 조회하거나 설정을 변경할 수 있습니다.
소스 코드 (hello_sysfs.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>
static int counter = 0;
static char message[128] = "Initial message";
/* counter 속성의 show 함수 */
static ssize_t counter_show(struct kobject *kobj,
struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "%d\\n", counter);
}
/* counter 속성의 store 함수 */
static ssize_t counter_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
int ret;
ret = kstrtoint(buf, 10, &counter);
if (ret < 0)
return ret;
pr_info("Counter updated to: %d\\n", counter);
return count;
}
/* message 속성의 show 함수 */
static ssize_t message_show(struct kobject *kobj,
struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "%s\\n", message);
}
/* message 속성의 store 함수 */
static ssize_t message_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
snprintf(message, sizeof(message), "%s", buf);
/* 줄바꿈 제거 */
if (message[count - 1] == '\\n')
message[count - 1] = '\\0';
pr_info("Message updated to: %s\\n", message);
return count;
}
/* 속성 정의 */
static struct kobj_attribute counter_attribute =
__ATTR(counter, 0664, counter_show, counter_store);
static struct kobj_attribute message_attribute =
__ATTR(message, 0664, message_show, message_store);
/* 속성 배열 */
static struct attribute *attrs[] = {
&counter_attribute.attr,
&message_attribute.attr,
NULL,
};
/* 속성 그룹 */
static struct attribute_group attr_group = {
.attrs = attrs,
};
static struct kobject *hello_kobj;
static int __init hello_init(void)
{
int ret;
/* kobject 생성 */
hello_kobj = kobject_create_and_add("hello_sysfs", kernel_kobj);
if (!hello_kobj) {
pr_err("Failed to create kobject\\n");
return -ENOMEM;
}
/* 속성 그룹 생성 */
ret = sysfs_create_group(hello_kobj, &attr_group);
if (ret) {
pr_err("Failed to create sysfs group\\n");
kobject_put(hello_kobj);
return ret;
}
pr_info("sysfs 모듈 로드됨. /sys/kernel/hello_sysfs/ 확인\\n");
return 0;
}
static void __exit hello_exit(void)
{
sysfs_remove_group(hello_kobj, &attr_group);
kobject_put(hello_kobj);
pr_info("sysfs 모듈 언로드됨\\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("sysfs 속성을 가진 커널 모듈");
MODULE_VERSION("1.2");
코드 설명
-
12-17행
show함수는 파일을 읽을 때 호출됩니다.sprintf로 버퍼에 값을 쓰고 길이를 반환합니다. -
20-32행
store함수는 파일에 쓸 때 호출됩니다.kstrtoint로 문자열을 정수로 변환합니다. 성공 시 쓴 바이트 수를 반환해야 합니다. -
58-59행
__ATTR매크로로 속성을 정의합니다. 인자는 (이름, 권한, show 함수, store 함수)입니다. - 65-69행 속성들을 배열로 묶어 그룹을 만듭니다. 마지막은 반드시 NULL이어야 합니다.
-
85-88행
kobject_create_and_add는/sys/kernel/아래에 새로운 디렉토리를 만듭니다.kernel_kobj는/sys/kernel을 가리키는 전역 변수입니다. -
91-97행
sysfs_create_group으로 속성 파일들을 한 번에 생성합니다.
빌드 및 테스트
# Makefile 수정
# obj-m += hello_sysfs.o
# 빌드 및 로드
make
sudo insmod hello_sysfs.ko
# sysfs 파일 확인
ls -l /sys/kernel/hello_sysfs/
# counter 읽기
cat /sys/kernel/hello_sysfs/counter
# counter 쓰기
echo 42 | sudo tee /sys/kernel/hello_sysfs/counter
cat /sys/kernel/hello_sysfs/counter
# message 읽기/쓰기
cat /sys/kernel/hello_sysfs/message
echo "Hello from userspace" | sudo tee /sys/kernel/hello_sysfs/message
cat /sys/kernel/hello_sysfs/message
# 커널 로그 확인
dmesg | tail -10
# 정리
sudo rmmod hello_sysfs
예상 출력:
$ ls -l /sys/kernel/hello_sysfs/
total 0
-rw-rw-r-- 1 root root 4096 Feb 16 10:00 counter
-rw-rw-r-- 1 root root 4096 Feb 16 10:00 message
$ cat /sys/kernel/hello_sysfs/counter
0
$ echo 42 | sudo tee /sys/kernel/hello_sysfs/counter
42
$ dmesg | tail -3
[ 901.234567] sysfs 모듈 로드됨. /sys/kernel/hello_sysfs/ 확인
[ 910.345678] Counter updated to: 42
[ 920.456789] Message updated to: Hello from userspace
/sys/class/, /sys/bus/, /sys/devices/ 아래를 탐색해보면 다양한 예제를 확인할 수 있습니다.
sysfs 계층 구조
지금까지 만든 세 모듈이 sysfs에 어떻게 배치되는지 전체 계층을 살펴봅니다. sysfs는 커널 객체 모델(kobject)을 기반으로 하며, 각 모듈이 다른 방식으로 sysfs에 엔트리를 생성합니다.
그림 5. 튜토리얼 모듈들의 sysfs 계층 구조
4단계: 캐릭터 디바이스 구현
캐릭터 디바이스는 바이트 스트림으로 데이터를 읽고 쓸 수 있는 디바이스입니다. 이 단계에서는 /dev/hello 노드를 통해 유저 공간과 통신하는 간단한 캐릭터 디바이스를 만들어봅니다.
소스 코드 (hello_chardev.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "hello_char"
#define CLASS_NAME "hello"
#define BUFFER_SIZE 1024
static int major_number;
static struct class *hello_class = NULL;
static struct device *hello_device = NULL;
static struct cdev hello_cdev;
static char device_buffer[BUFFER_SIZE];
static int buffer_len = 0;
/* open 핸들러 */
static int hello_open(struct inode *inode, struct file *file)
{
pr_info("hello_char: Device opened\\n");
return 0;
}
/* release 핸들러 */
static int hello_release(struct inode *inode, struct file *file)
{
pr_info("hello_char: Device closed\\n");
return 0;
}
/* read 핸들러 */
static ssize_t hello_read(struct file *file, char __user *user_buf,
size_t count, loff_t *offset)
{
size_t to_read;
if (*offset >= buffer_len)
return 0; /* EOF */
to_read = min(count, (size_t)(buffer_len - *offset));
if (copy_to_user(user_buf, device_buffer + *offset, to_read))
return -EFAULT;
*offset += to_read;
pr_info("hello_char: Read %zu bytes\\n", to_read);
return to_read;
}
/* write 핸들러 */
static ssize_t hello_write(struct file *file, const char __user *user_buf,
size_t count, loff_t *offset)
{
size_t to_write;
to_write = min(count, (size_t)(BUFFER_SIZE - buffer_len));
if (to_write == 0)
return -ENOSPC; /* 버퍼 가득 참 */
if (copy_from_user(device_buffer + buffer_len, user_buf, to_write))
return -EFAULT;
buffer_len += to_write;
pr_info("hello_char: Written %zu bytes\\n", to_write);
return to_write;
}
/* file_operations 구조체 */
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
};
static int __init hello_init(void)
{
int ret;
dev_t dev;
/* 1. 메이저 번호 할당 */
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate major number\\n");
return ret;
}
major_number = MAJOR(dev);
pr_info("Registered with major number %d\\n", major_number);
/* 2. 디바이스 클래스 생성 */
hello_class = class_create(CLASS_NAME);
if (IS_ERR(hello_class)) {
unregister_chrdev_region(dev, 1);
pr_err("Failed to create device class\\n");
return PTR_ERR(hello_class);
}
/* 3. 디바이스 파일 생성 (/dev/hello_char) */
hello_device = device_create(hello_class, NULL, dev, NULL, DEVICE_NAME);
if (IS_ERR(hello_device)) {
class_destroy(hello_class);
unregister_chrdev_region(dev, 1);
pr_err("Failed to create device\\n");
return PTR_ERR(hello_device);
}
/* 4. cdev 초기화 및 추가 */
cdev_init(&hello_cdev, &fops);
hello_cdev.owner = THIS_MODULE;
ret = cdev_add(&hello_cdev, dev, 1);
if (ret < 0) {
device_destroy(hello_class, dev);
class_destroy(hello_class);
unregister_chrdev_region(dev, 1);
pr_err("Failed to add cdev\\n");
return ret;
}
pr_info("Device /dev/%s created\\n", DEVICE_NAME);
return 0;
}
static void __exit hello_exit(void)
{
dev_t dev = MKDEV(major_number, 0);
cdev_del(&hello_cdev);
device_destroy(hello_class, dev);
class_destroy(hello_class);
unregister_chrdev_region(dev, 1);
pr_info("Device /dev/%s removed\\n", DEVICE_NAME);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("간단한 캐릭터 디바이스 드라이버");
MODULE_VERSION("1.3");
코드 설명
-
22-26행
open핸들러는 디바이스 파일이 열릴 때 호출됩니다. 여기서는 로그만 남깁니다. -
36-53행
read핸들러는 유저 공간으로 데이터를 전송합니다.copy_to_user를 사용해야 하며, offset을 업데이트해 연속 읽기를 지원합니다. -
56-71행
write핸들러는 유저 공간에서 데이터를 받습니다.copy_from_user를 사용하며, 버퍼가 가득 차면-ENOSPC를 반환합니다. -
74-80행
file_operations구조체에 핸들러 함수들을 등록합니다. 지원하지 않는 연산은 NULL로 두면 커널이 기본 동작을 제공합니다. -
88-93행
alloc_chrdev_region으로 동적으로 메이저 번호를 할당받습니다. 정적 할당보다 안전합니다. -
96-102행
class_create로 디바이스 클래스를 생성합니다. 이는 udev가/dev에 노드를 자동으로 만들 수 있게 합니다. -
105-112행
device_create로 디바이스 파일을 생성합니다. udev가 이를 감지하여/dev/hello_char를 만듭니다. -
115-125행
cdev_init로 cdev 구조체를 초기화하고,cdev_add로 커널에 등록합니다.
빌드 및 테스트
# Makefile 수정
# obj-m += hello_chardev.o
# 빌드 및 로드
make
sudo insmod hello_chardev.ko
# 디바이스 파일 확인
ls -l /dev/hello_char
# 메이저/마이너 번호 확인
cat /proc/devices | grep hello
# 데이터 쓰기
echo "Hello from userspace!" | sudo tee /dev/hello_char
# 데이터 읽기
sudo cat /dev/hello_char
# 추가 데이터 쓰기
echo "Second line" | sudo tee -a /dev/hello_char
# 다시 읽기
sudo cat /dev/hello_char
# 커널 로그 확인
dmesg | tail -15
# 정리
sudo rmmod hello_chardev
예상 출력:
$ ls -l /dev/hello_char
crw------- 1 root root 240, 0 Feb 16 11:00 /dev/hello_char
$ sudo cat /dev/hello_char
Hello from userspace!
$ dmesg | tail -10
[ 1001.123456] Registered with major number 240
[ 1001.123458] Device /dev/hello_char created
[ 1010.234567] hello_char: Device opened
[ 1010.234569] hello_char: Written 22 bytes
[ 1010.234570] hello_char: Device closed
[ 1020.345678] hello_char: Device opened
[ 1020.345680] hello_char: Read 22 bytes
[ 1020.345681] hello_char: Device closed
테스트 프로그램 작성
C 프로그램으로도 테스트할 수 있습니다. test_chardev.c를 작성합니다.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd;
char write_buf[] = "Test from C program\\n";
char read_buf[100];
ssize_t ret;
/* 디바이스 열기 */
fd = open("/dev/hello_char", O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return 1;
}
/* 쓰기 */
ret = write(fd, write_buf, strlen(write_buf));
printf("Written %zd bytes\\n", ret);
/* 파일 포인터를 처음으로 */
lseek(fd, 0, SEEK_SET);
/* 읽기 */
ret = read(fd, read_buf, sizeof(read_buf) - 1);
if (ret >= 0) {
read_buf[ret] = '\\0';
printf("Read %zd bytes: %s", ret, read_buf);
}
/* 닫기 */
close(fd);
return 0;
}
# 컴파일 및 실행
gcc -o test_chardev test_chardev.c
sudo ./test_chardev
예상 출력:
Written 21 bytes
Read 21 bytes: Test from C program
캐릭터 디바이스 전체 아키텍처
아래 다이어그램은 유저 공간의 응용 프로그램이 /dev/hello_char를 통해 커널 모듈과 통신하는 전체 흐름을 보여줍니다.
그림 6. 캐릭터 디바이스 전체 아키텍처 — 유저 공간 → VFS → 모듈 → udev
5단계: procfs 인터페이스 구현
procfs(/proc)는 커널이 유저 공간에 정보를 노출하는 가상 파일시스템입니다. 프로세스 정보(/proc/[pid])뿐 아니라 커널 모듈도 자체 엔트리를 만들어 상태를 보고하거나 설정을 받을 수 있습니다. Linux 5.6부터 proc_ops 구조체가 도입되어 기존 file_operations를 대체했습니다.
그림 7. procfs 데이터 흐름 — 읽기(seq_file)와 쓰기 경로
소스 코드 (hello_proc.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#define PROC_NAME "hello_proc"
#define BUFFER_SIZE 256
static char proc_buffer[BUFFER_SIZE];
static int proc_buffer_len = 0;
static int read_count = 0;
/* seq_file show 콜백 — 읽기 시 호출 */
static int hello_proc_show(struct seq_file *m, void *v)
{
read_count++;
seq_printf(m, "Hello from /proc/%s!\\n", PROC_NAME);
seq_printf(m, "읽기 횟수: %d\\n", read_count);
if (proc_buffer_len > 0)
seq_printf(m, "저장된 메시지: %s\\n", proc_buffer);
else
seq_printf(m, "저장된 메시지: (없음)\\n");
return 0;
}
/* open 핸들러 — single_open으로 seq_file과 연결 */
static int hello_proc_open(struct inode *inode, struct file *file)
{
return single_open(file, hello_proc_show, NULL);
}
/* write 핸들러 — 유저 공간에서 데이터를 받음 */
static ssize_t hello_proc_write(struct file *file,
const char __user *user_buf,
size_t count, loff_t *offset)
{
size_t len = min(count, (size_t)(BUFFER_SIZE - 1));
if (copy_from_user(proc_buffer, user_buf, len))
return -EFAULT;
proc_buffer[len] = '\\0';
proc_buffer_len = len;
if (len > 0 && proc_buffer[len - 1] == '\\n') {
proc_buffer[len - 1] = '\\0';
proc_buffer_len--;
}
pr_info("hello_proc: 메시지 수신 (%zu bytes): %s\\n", len, proc_buffer);
return len;
}
/* Linux 5.6+ 에서는 proc_ops, 이전 버전에서는 file_operations */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
static const struct proc_ops hello_proc_ops = {
.proc_open = hello_proc_open,
.proc_read = seq_read,
.proc_write = hello_proc_write,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
#else
static const struct file_operations hello_proc_ops = {
.owner = THIS_MODULE,
.open = hello_proc_open,
.read = seq_read,
.write = hello_proc_write,
.llseek = seq_lseek,
.release = single_release,
};
#endif
static struct proc_dir_entry *proc_entry;
static int __init hello_init(void)
{
proc_entry = proc_create(PROC_NAME, 0666, NULL, &hello_proc_ops);
if (!proc_entry) {
pr_err("Failed to create /proc/%s\\n", PROC_NAME);
return -ENOMEM;
}
pr_info("hello_proc: /proc/%s 생성 완료\\n", PROC_NAME);
return 0;
}
static void __exit hello_exit(void)
{
proc_remove(proc_entry);
pr_info("hello_proc: /proc/%s 제거 완료\\n", PROC_NAME);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("procfs 인터페이스를 가진 커널 모듈");
MODULE_VERSION("1.4");
코드 설명
-
4-6행
proc_fs.h는 procfs API를,seq_file.h는 순차 파일 인터페이스를 제공합니다.version.h는 커널 버전 감지에 사용됩니다. -
17-28행
seq_file의 show 콜백입니다.seq_printf는 내부적으로 버퍼 관리를 자동 수행하므로sprintf보다 안전합니다. 데이터가 버퍼 크기를 초과하면 자동으로 버퍼를 확장합니다. -
31-34행
single_open은 단일 show 함수만 필요한 간단한 경우에 사용합니다. 복잡한 순차 데이터는seq_open과start/next/stop/show4개 콜백을 사용합니다. -
57-75행
LINUX_VERSION_CODE매크로로 커널 버전을 감지합니다. 5.6 이상에서는proc_ops를 사용하고, 이전 버전에서는file_operations를 사용합니다.proc_ops는.owner필드가 없고, 함수 이름에proc_접두사가 붙습니다. -
80행
proc_create의 세 번째 인자NULL은/proc루트 아래에 직접 만듭니다. 서브디렉토리를 만들려면proc_mkdir로 먼저 디렉토리를 생성합니다.
빌드 및 테스트
# 빌드 및 로드 (Makefile: obj-m += hello_proc.o)
make
sudo insmod hello_proc.ko
# 읽기
cat /proc/hello_proc
# 데이터 쓰기
echo "커널에게 보내는 메시지" | sudo tee /proc/hello_proc
# 다시 읽기 (저장된 메시지 확인)
cat /proc/hello_proc
# 정리
sudo rmmod hello_proc
예상 출력:
$ cat /proc/hello_proc
Hello from /proc/hello_proc!
읽기 횟수: 1
저장된 메시지: (없음)
$ echo "커널에게 보내는 메시지" | sudo tee /proc/hello_proc
$ cat /proc/hello_proc
Hello from /proc/hello_proc!
읽기 횟수: 2
저장된 메시지: 커널에게 보내는 메시지
- sysfs — 단일 값 (정수, 문자열 등), 디바이스 속성, 하드웨어 설정
- procfs — 여러 줄의 통계/상태 정보, 디버깅용 데이터 덤프, 테이블 형식 출력
- debugfs — 개발/디버깅 전용 인터페이스 (프로덕션에서는 비활성화 가능)
6단계: ioctl 구현
ioctl(Input/Output Control)은 read/write로 처리하기 어려운 디바이스 제어 명령을 구현하는 메커니즘입니다. 디바이스 상태 조회, 설정 변경, 하드웨어 리셋 같은 명령을 정의하며, 구조체를 통해 복잡한 데이터를 주고받을 수 있습니다.
그림 8. ioctl 명령 인코딩과 처리 흐름
_IO(type, nr)— 데이터 전달 없는 명령_IOW(type, nr, datatype)— 유저 → 커널 데이터 전달 (Write)_IOR(type, nr, datatype)— 커널 → 유저 데이터 전달 (Read)_IOWR(type, nr, datatype)— 양방향 데이터 전달
헤더 파일 (hello_ioctl.h)
#ifndef HELLO_IOCTL_H
#define HELLO_IOCTL_H
#include <linux/ioctl.h>
#define HELLO_MAGIC 'H'
struct hello_info {
int version_major;
int version_minor;
int current_count;
char name[32];
};
#define HELLO_GET_COUNT _IOR(HELLO_MAGIC, 1, int)
#define HELLO_SET_COUNT _IOW(HELLO_MAGIC, 2, int)
#define HELLO_RESET _IO(HELLO_MAGIC, 3)
#define HELLO_GET_INFO _IOR(HELLO_MAGIC, 4, struct hello_info)
#endif
커널 모듈의 ioctl 핸들러 핵심
static long hello_ioctl(struct file *file,
unsigned int cmd, unsigned long arg)
{
int tmp;
struct hello_info info;
switch (cmd) {
case HELLO_GET_COUNT:
if (copy_to_user((int __user *)arg, &access_count,
sizeof(access_count)))
return -EFAULT;
break;
case HELLO_SET_COUNT:
if (copy_from_user(&tmp, (int __user *)arg, sizeof(tmp)))
return -EFAULT;
if (tmp < 0)
return -EINVAL;
access_count = tmp;
break;
case HELLO_RESET:
access_count = 0;
break;
case HELLO_GET_INFO:
info.version_major = 1;
info.version_minor = 5;
info.current_count = access_count;
strscpy(info.name, "hello_ioctl", sizeof(info.name));
if (copy_to_user((struct hello_info __user *)arg,
&info, sizeof(info)))
return -EFAULT;
break;
default:
return -ENOTTY; /* 지원하지 않는 ioctl */
}
return 0;
}
/* file_operations에 등록 */
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.unlocked_ioctl = hello_ioctl, /* BKL 없는 ioctl (2.6.36+) */
};
코드 설명
-
HELLO_GET_COUNT
_IOR로 정의되어 커널 → 유저 방향.copy_to_user로 커널 변수 값을 유저 공간에 복사합니다. -
HELLO_SET_COUNT
_IOW로 정의되어 유저 → 커널 방향. 입력값 검증(음수 거부)을 반드시 수행해야 합니다. -
HELLO_RESET
_IO로 정의되어 데이터 전달 없이 명령만 수행합니다. -
HELLO_GET_INFO
구조체 전체를 유저 공간으로 전달.
strscpy는strncpy보다 안전한 문자열 복사 함수로, 항상 NULL 종료를 보장합니다. -
default
정의되지 않은 명령에
-ENOTTY를 반환합니다. 이는 "디바이스에서 지원하지 않는 ioctl"이라는 표준 에러 코드입니다.
copy_from_user/copy_to_user를 사용하세요. 유저 공간 포인터를 직접 역참조하면 커널 패닉이 발생할 수 있습니다.
7단계: 타이머와 워크큐 활용
커널 타이머는 지정된 시간 후 콜백을 실행하는 메커니즘이고, 워크큐는 프로세스 컨텍스트에서 지연 작업을 수행하는 메커니즘입니다. 타이머 콜백은 softirq 컨텍스트에서 실행되므로 슬립할 수 없지만, 워크큐는 프로세스 컨텍스트이므로 블로킹 작업이 가능합니다.
- 타이머 콜백 (softirq) —
GFP_ATOMIC만 사용,mutex사용 불가, 최대한 짧게 실행 - 워크큐 (process) —
GFP_KERNEL사용 가능,mutex사용 가능, 슬립 가능
핵심 코드 (hello_timer.c)
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/workqueue.h>
#include <linux/atomic.h>
#define TIMER_INTERVAL_SEC 1
#define WORK_THRESHOLD 5
static struct timer_list hello_timer;
static atomic_t tick_count = ATOMIC_INIT(0);
/* 워크큐 — 프로세스 컨텍스트에서 실행 */
static void hello_work_handler(struct work_struct *work);
static DECLARE_WORK(hello_work, hello_work_handler);
static void hello_work_handler(struct work_struct *work)
{
pr_info("hello_timer: [워크큐] 틱=%d (process context, pid=%d)\\n",
atomic_read(&tick_count), current->pid);
/* 프로세스 컨텍스트: kmalloc(GFP_KERNEL), mutex_lock 가능 */
}
/* 타이머 콜백 — softirq 컨텍스트 */
static void hello_timer_callback(struct timer_list *t)
{
int ticks = atomic_inc_return(&tick_count);
pr_info("hello_timer: [타이머] tick #%d (jiffies=%lu)\\n", ticks, jiffies);
if (ticks % WORK_THRESHOLD == 0)
schedule_work(&hello_work); /* 5틱마다 워크큐 예약 */
mod_timer(&hello_timer, jiffies + TIMER_INTERVAL_SEC * HZ); /* 재등록 */
}
static int __init hello_init(void)
{
timer_setup(&hello_timer, hello_timer_callback, 0);
mod_timer(&hello_timer, jiffies + TIMER_INTERVAL_SEC * HZ);
pr_info("hello_timer: 로드 (타이머 간격: %d초)\\n", TIMER_INTERVAL_SEC);
return 0;
}
static void __exit hello_exit(void)
{
del_timer_sync(&hello_timer); /* 타이머 정지 (콜백 완료 대기) */
cancel_work_sync(&hello_work); /* 워크 완료 대기 */
pr_info("hello_timer: 언로드 (총 %d틱)\\n", atomic_read(&tick_count));
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
코드 설명
-
timer_setup
Linux 4.15에서 도입된 타이머 초기화 API. 이전의
setup_timer/init_timer를 대체합니다. -
atomic_inc_return
softirq 컨텍스트에서 안전한 원자적 증가. 일반
int대신atomic_t를 사용합니다. - schedule_work 디폴트 워크큐에 작업을 예약합니다. 타이머 콜백에서 직접 할 수 없는 슬립 가능 작업을 위임합니다.
- del_timer_sync / cancel_work_sync 모듈 언로드 시 반드시 호출해야 합니다. 실행 중인 콜백이 있다면 완료될 때까지 대기하여 use-after-free를 방지합니다.
mutex_lock(),kmalloc(GFP_KERNEL),msleep()등 슬립 가능한 함수 호출 금지- 긴 작업은
schedule_work()로 워크큐에 넘기고, 타이머 콜백은 빠르게 반환 - 고해상도가 필요하면
hrtimer사용 (나노초 단위 제어 가능)
8단계: 동시성 보호
커널 모듈은 여러 프로세스나 인터럽트 핸들러가 동시에 접근할 수 있으므로 공유 데이터를 반드시 보호해야 합니다.
그림 9. mutex를 사용한 동시성 보호 전후 비교
동기화 기법 비교
| 기법 | 슬립 가능 | 인터럽트 컨텍스트 | 주요 용도 |
|---|---|---|---|
| mutex | 예 (대기 시 슬립) | 사용 불가 | 프로세스 컨텍스트의 긴 크리티컬 섹션 |
| spinlock | 아니오 (busy-wait) | 사용 가능 | 인터럽트 핸들러, 짧은 크리티컬 섹션 |
| semaphore | 예 | 사용 불가 | 카운팅 세마포어 (동시 N개 접근) |
| RCU | 읽기: 예 | 읽기: 사용 가능 | 읽기 빈도 높고 쓰기 드문 데이터 |
| atomic_t | 해당 없음 | 사용 가능 | 단일 정수의 원자적 조작 |
핵심 코드 (hello_safe.c)
/* === 동기화 객체 선언 === */
static DEFINE_MUTEX(buffer_mutex); /* 버퍼 보호 */
static atomic_t open_count = ATOMIC_INIT(0); /* open 카운터 */
static DEFINE_SPINLOCK(stats_lock); /* 통계 보호 */
static unsigned long total_bytes_read = 0;
/* read — mutex로 버퍼 보호 */
static ssize_t hello_read(struct file *file, char __user *user_buf,
size_t count, loff_t *offset)
{
ssize_t to_read;
unsigned long flags;
/* mutex 획득 (시그널로 중단 가능) */
if (mutex_lock_interruptible(&buffer_mutex))
return -ERESTARTSYS;
if (*offset >= buffer_len) {
mutex_unlock(&buffer_mutex);
return 0;
}
to_read = min(count, (size_t)(buffer_len - *offset));
if (copy_to_user(user_buf, device_buffer + *offset, to_read)) {
mutex_unlock(&buffer_mutex);
return -EFAULT;
}
*offset += to_read;
mutex_unlock(&buffer_mutex);
/* 통계 업데이트 — spinlock (인터럽트 안전) */
spin_lock_irqsave(&stats_lock, flags);
total_bytes_read += to_read;
spin_unlock_irqrestore(&stats_lock, flags);
return to_read;
}
코드 설명
-
DEFINE_MUTEX
정적 mutex 선언. 버퍼와
buffer_len에 대한 모든 접근을 보호합니다. -
mutex_lock_interruptible
시그널에 의해 중단 가능한 락 획득. Ctrl+C 시
-ERESTARTSYS반환.mutex_lock은 무조건 대기합니다. -
spin_lock_irqsave
로컬 인터럽트를 비활성화하면서 spinlock 획득.
flags에 이전 인터럽트 상태 저장 후spin_unlock_irqrestore에서 복원합니다.
- spinlock 내에서 슬립 —
spin_lock()상태에서kmalloc(GFP_KERNEL),copy_from_user()등 호출 시 데드락.CONFIG_DEBUG_ATOMIC_SLEEP=y로 감지 - 같은 mutex 재귀 획득 — 같은 스레드에서
mutex_lock()두 번 호출 시 데드락 - 락 순서 불일치 — A가 lock1→lock2, B가 lock2→lock1 순서면 데드락. 일관된 순서 유지
- 에러 경로에서 unlock 누락 —
goto패턴으로 해제 경로 통합
9단계: 다중 소스 파일과 심볼 내보내기
실제 프로젝트에서는 하나의 모듈을 여러 소스 파일로 분리하거나, 여러 모듈 간에 함수를 공유합니다.
그림 10. 모듈 간 심볼 내보내기와 의존성
다중 파일로 하나의 모듈 빌드
# 하나의 모듈을 여러 .c 파일로 빌드
obj-m += mymod.o
mymod-objs := mymod_main.o mymod_ops.o
# 주의: mymod.c 파일이 있으면 안 됨 (이름 충돌)
EXPORT_SYMBOL_GPL 사용
/* module_a.c — 함수를 내보내는 모듈 */
int hello_shared_func(int increment)
{
shared_counter += increment;
pr_info("module_a: counter = %d\\n", shared_counter);
return shared_counter;
}
EXPORT_SYMBOL_GPL(hello_shared_func);
/* module_b.c — 모듈 A의 함수를 사용하는 모듈 */
extern int hello_shared_func(int increment);
static int __init module_b_init(void)
{
int val = hello_shared_func(10);
pr_info("module_b: result = %d\\n", val);
return 0;
}
# 로드 순서: A 먼저 (B가 A에 의존)
sudo insmod module_a.ko
sudo insmod module_b.ko
# 의존성 확인
lsmod | grep module
# module_b 16384 0
# module_a 16384 1 module_b
# 언로드 순서: B 먼저 (A를 먼저 제거하면 실패)
sudo rmmod module_b
sudo rmmod module_a
- 로드: 의존 대상(A)을 먼저, 사용하는 쪽(B)을 나중에. 바뀌면
Unknown symbol에러 - 언로드: 사용하는 쪽(B)을 먼저, 의존 대상(A)을 나중에.
modprobe사용 시 자동 해결
EXPORT_SYMBOL(sym)— 모든 라이선스의 모듈에서 사용 가능EXPORT_SYMBOL_GPL(sym)— GPL 라이선스 모듈만 사용 가능 (커널 커뮤니티 권장)
일반적인 에러와 해결 방법
컴파일 에러
| 에러 메시지 | 원인 | 해결 방법 |
|---|---|---|
fatal error: linux/module.h: No such file or directory |
커널 헤더 미설치 | sudo apt install linux-headers-$(uname -r) |
Makefile:X: *** missing separator |
탭 대신 스페이스 사용 | 들여쓰기를 탭 문자로 변경 |
ERROR: modpost: "symbol" undefined! |
필요한 심볼을 export하지 않음 | MODULE_LICENSE("GPL") 확인, 의존 모듈 확인 |
implicit declaration of function 'xxx' |
헤더 파일 누락 | 함수가 정의된 헤더 파일 include |
로드 에러
| 에러 메시지 | 원인 | 해결 방법 |
|---|---|---|
insmod: ERROR: could not insert module |
다양한 원인 가능 | dmesg로 정확한 에러 확인 |
Invalid module format |
커널 버전 불일치 | 현재 커널에 맞는 헤더로 재빌드 |
Required key not available |
Secure Boot 활성화 | 모듈 서명 또는 Secure Boot 비활성화 |
Unknown symbol in module |
의존 모듈 미로드 | modprobe 사용 또는 의존 모듈 먼저 로드 |
Module already in use |
모듈 사용 중 | 사용 중인 프로세스 종료 후 재시도 |
런타임 에러
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| 커널 패닉 | NULL 포인터 접근, 메모리 침범 | VM에서 재현, CONFIG_DEBUG_KERNEL 활성화 |
| 메모리 누수 | kmalloc 후 kfree 누락 |
kmemleak 활성화, 코드 리뷰 |
| 데드락 | 잘못된 락 순서 | CONFIG_PROVE_LOCKING 활성화 |
Oops 메시지 |
잘못된 메모리 접근 | dmesg의 콜 스택 분석, addr2line 사용 |
디버깅 팁
printk 활용
커널 모듈 디버깅의 기본은 printk입니다. 로그 레벨을 적절히 사용하세요.
/* 로그 레벨별 사용 */
pr_emerg("시스템 사용 불가"); /* 거의 사용 안 함 */
pr_alert("즉각 조치 필요");
pr_crit("치명적 조건");
pr_err("오류 발생: %d", errno); /* 에러 */
pr_warn("경고: 권장하지 않는 동작"); /* 경고 */
pr_notice("정상이지만 주목");
pr_info("정보성 메시지"); /* 일반 정보 */
pr_debug("디버그 정보"); /* 디버그용 */
/* pr_fmt 매크로로 접두사 자동 추가 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
/* 동적 디버그 (CONFIG_DYNAMIC_DEBUG 필요) */
pr_debug("이 메시지는 runtime에 켜고 끌 수 있음\\n");
/* 한 번만 출력 */
pr_info_once("첫 번째 호출 시에만 출력\\n");
/* 속도 제한 (rate limiting) */
printk_ratelimited(KERN_WARNING "반복 메시지 억제\\n");
커널 디버그 옵션
개발 중에는 다음 커널 설정 옵션들을 활성화하는 것이 좋습니다.
# .config 파일 또는 menuconfig에서 설정
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y # 디버그 심볼 포함
CONFIG_DEBUG_INFO_DWARF4=y # DWARF4 포맷 사용
CONFIG_KALLSYMS=y # 커널 심볼 테이블
CONFIG_KALLSYMS_ALL=y
CONFIG_DEBUG_SLAB=y # 메모리 디버깅
CONFIG_DEBUG_KMEMLEAK=y # 메모리 누수 감지
CONFIG_LOCKDEP=y # 데드락 감지
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_ATOMIC_SLEEP=y # atomic 컨텍스트에서 sleep 감지
CONFIG_KASAN=y # AddressSanitizer (메모리 오류 감지)
커널 크래시 분석
모듈이 커널 패닉을 일으킨 경우 다음 단계로 분석합니다.
# 1. dmesg에서 오류 메시지 확인
dmesg | tail -50
# 2. Oops 메시지의 Instruction Pointer(IP) 확인
# 예: RIP: 0010:[<ffffffffc0123456>] my_function+0x23/0x50 [my_module]
# 3. addr2line으로 소스 코드 위치 찾기
addr2line -e my_module.ko 0x123456
# 4. objdump로 어셈블리 확인
objdump -dS my_module.ko | less
# 5. gdb로 디버깅
gdb ./vmlinux
(gdb) list *(my_function+0x23)
KGDB 사용 (선택)
두 시스템 간 시리얼 연결 또는 가상 머신에서 KGDB를 사용할 수 있습니다.
# 타겟 시스템 (디버깅 대상) 커널 파라미터
kgdboc=ttyS0,115200 kgdbwait
# 호스트 시스템에서 gdb 실행
gdb ./vmlinux
(gdb) target remote /dev/ttyS0
(gdb) b my_function
(gdb) c
- trace-cmd / kernelshark — ftrace GUI 도구
- perf — 성능 프로파일링
- strace — 시스템 콜 추적 (유저 공간)
- crash — kdump 덤프 분석
모범 사례
코딩 스타일
- Linux 커널 코딩 스타일 준수 —
scripts/checkpatch.pl로 검사하세요. - 탭 문자 사용 — 들여쓰기는 스페이스 8개가 아닌 탭 1개입니다.
- 함수명은 소문자 —
my_init()처럼 밑줄로 단어를 구분합니다. - 80자 제한 — 한 줄은 80자를 넘지 않도록 합니다 (권장사항).
# 코딩 스타일 검사
scripts/checkpatch.pl --no-tree -f my_module.c
에러 처리
커널 코드에서는 철저한 에러 처리가 필수입니다.
static int __init my_init(void)
{
int ret;
struct resource *res;
res = kmalloc(sizeof(*res), GFP_KERNEL);
if (!res) {
pr_err("Failed to allocate memory\\n");
return -ENOMEM;
}
ret = register_something(res);
if (ret < 0) {
pr_err("Failed to register: %d\\n", ret);
goto err_free;
}
ret = init_hardware();
if (ret < 0) {
pr_err("Failed to init hardware: %d\\n", ret);
goto err_unregister;
}
return 0;
err_unregister:
unregister_something(res);
err_free:
kfree(res);
return ret;
}
메모리 관리
- 할당과 해제 쌍 맞추기 —
kmalloc에는 항상kfree가 있어야 합니다. - 적절한 플래그 사용 —
GFP_KERNEL(일반),GFP_ATOMIC(인터럽트 컨텍스트) - NULL 체크 — 모든 할당 후 NULL 확인은 필수입니다.
- 메모리 누수 검사 —
CONFIG_DEBUG_KMEMLEAK=y활성화
동기화
- 짧은 크리티컬 섹션 — 락을 잡고 있는 시간을 최소화하세요.
- 일관된 락 순서 — 데드락 방지를 위해 항상 같은 순서로 락을 획득하세요.
- 인터럽트 컨텍스트 주의 —
spin_lock_irqsave사용 - lockdep 활성화 —
CONFIG_PROVE_LOCKING=y로 데드락 자동 감지
보안
- 입력 검증 — 유저 공간에서 온 모든 데이터를 검증하세요.
- 버퍼 오버플로 방지 —
strncpy,snprintf등 안전한 함수 사용 - 정수 오버플로 체크 — 크기 계산 시 오버플로 가능성 확인
- 권한 체크 —
capable(CAP_SYS_ADMIN)등으로 권한 확인
/* 나쁜 예 */
copy_from_user(buf, user_buf, count); /* count를 믿으면 안 됨! */
/* 좋은 예 */
if (count > MAX_SIZE)
return -EINVAL;
if (copy_from_user(buf, user_buf, count))
return -EFAULT;
다음 단계
이제 기본적인 커널 모듈 개발을 익혔으니, 다음 주제들을 학습하세요.
추천 학습 주제
- 고급 디바이스 드라이버
- 디바이스 드라이버 모델 — platform driver, PCI driver 등
- 디바이스 드라이버 모델 — bus_type, platform driver, 버스별 드라이버 인터페이스
- Input 서브시스템 — 키보드, 마우스 드라이버
- 동기화와 병렬성
- 메모리 관리
- 인터럽트와 타이밍
- 인터럽트 — IRQ 처리, 핸들러 작성
- Bottom Half — tasklet, softirq
- Workqueue — 비동기 작업 처리
- 타이머 — 지연 실행, 주기적 작업
- 디버깅 심화
실습 과제
다음 과제들을 직접 구현해보면서 실력을 향상시키세요.
- 간단한 procfs 인터페이스 —
/proc/my_module에 읽기/쓰기 가능한 파일 생성 - 타이머 기반 모듈 — 1초마다 카운터를 증가시키고 로그를 남기는 모듈
- 링 버퍼 구현 — 고정 크기 순환 버퍼를 사용하는 캐릭터 디바이스
- ioctl 추가 — 기존 캐릭터 디바이스에 ioctl 명령 추가
- 다중 디바이스 지원 — 하나의 모듈이 여러 개의 디바이스 노드를 생성
참고자료
공식 문서
- Linux Kernel Documentation — 공식 커널 문서
- Driver API — 드라이버 개발 API
- Core API — 핵심 커널 API
- Elixir Cross Referencer — 커널 소스 코드 탐색
추천 서적
- Linux Device Drivers, 3rd Edition (O'Reilly) — 고전이지만 여전히 유용
- Linux Kernel Development, 3rd Edition (Robert Love) — 커널 내부 전반
- Essential Linux Device Drivers (Sreekrishnan Venkateswaran) — 실전 드라이버 예제
- Understanding the Linux Kernel, 3rd Edition (O'Reilly) — 커널 동작 원리
온라인 리소스
- LWN.net — 커널 개발 뉴스 및 분석
- KernelNewbies — 초보자를 위한 위키
- LKML — 리눅스 커널 메일링 리스트
- Greg KH의 유튜브 — 커널 개발 강의
도구 및 유틸리티
modinfo— 모듈 정보 확인lsmod— 로드된 모듈 목록dmesg— 커널 로그 확인addr2line— 주소를 소스 코드 위치로 변환objdump— 바이너리 디스어셈블perf— 성능 분석trace-cmd— ftrace 프론트엔드
튜토리얼 이후 확장 실습 로드맵
첫 모듈 튜토리얼을 끝낸 뒤에는 "기능 추가"보다 "안전한 구조화"에 집중하는 것이 중요합니다. 아래 단계는 입문 코드를 실무형 모듈로 바꾸는 최소 경로입니다.
| 단계 | 목표 | 완료 기준 |
|---|---|---|
| 1단계 | 에러 경로 정리 | 모든 실패 분기에서 누수 없이 복귀 |
| 2단계 | 동시성 보호 | sysfs/ioctl/read-write 경로에 락 정책 적용 |
| 3단계 | 디버깅 가능성 강화 | 로그 레벨 분리, 동적 디버그 지원 |
| 4단계 | 테스트 자동화 | 로드/언로드, 파라미터, 인터페이스 테스트 스크립트화 |
권장 셀프 테스트 루틴
# 1) 빌드
make -j$(nproc)
# 2) 로드/파라미터 테스트
sudo insmod hello_param.ko whom="kernel" repeat=3
dmesg | tail -n 50
sudo rmmod hello_param
# 3) 반복 안정성 테스트
for i in $(seq 1 50); do sudo insmod hello.ko; sudo rmmod hello; done
# 4) 정적 점검
./scripts/checkpatch.pl --file hello_param.c
관련 문서
- 커널 개발 환경 설정 — 개발 환경 구축
- 커널 모듈 — 모듈 개념 이해
- 빌드 시스템 — Kbuild 시스템 이해