Linux Kernel Module 개발 입문 - Hello World 부터 시작하기
1. 커널 모듈이란?
Linux 커널은 하나의 거대한 프로그램입니다. 그런데 모든 드라이버를 커널에 정적으로 포함시키면 커널 크기가 엄청나게 커지고, 새 드라이버를 추가할 때마다 커널을 다시 컴파일해야 합니다.
이 문제를 해결하는 것이 Kernel Module(커널 모듈) 입니다.
커널 모듈 = 실행 중인 커널에 동적으로 추가하거나 제거할 수 있는 코드 조각
USB를 꽂으면 드라이버가 자동으로 로드되고, 뽑으면 언로드되는 것이 바로 커널 모듈 덕분입니다.
Linux 커널 (실행 중)
┌─────────────────────────────────────┐
│ Core Kernel │
│ ┌──────────┐ ┌──────────┐ │
│ │ Module A │ │ Module B │ ← 동적 로드/언로드 가능
│ └──────────┘ └──────────┘ │
│ ┌──────────┐ │
│ │ Module C │ ← 우리가 만들 것 │
│ └──────────┘ │
└─────────────────────────────────────┘
커널 모듈로 만들 수 있는 것들:
- 디바이스 드라이버 (GPIO, I2C, SPI, UART 등)
- 파일시스템 (ext4, FAT 등)
- 네트워크 프로토콜
- 시스템 콜 후킹 (보안, 모니터링)
2. 유저스페이스 vs 커널스페이스
커널 모듈을 이해하려면 이 개념이 반드시 필요합니다.
Linux는 메모리를 두 영역으로 엄격하게 분리합니다.
메모리 공간
높은 주소 ┌─────────────────────┐
│ │
│ Kernel Space │ ← 커널, 드라이버, 모듈
│ (보호된 영역) │ 직접 하드웨어 접근 가능
│ │
├────────────────────┤ ← 경계선 (ARM: 0xC0000000)
│ │
│ User Space │ ← 일반 앱 (bash, python 등)
│ (제한된 영역) │ 하드웨어 직접 접근 불가
│ │
낮은 주소 └─────────────────────┘
유저스페이스
- 일반 애플리케이션이 실행되는 영역
- 하드웨어에 직접 접근 불가
- 잘못된 메모리 접근 → 프로세스만 죽음 (시스템은 안전)
- printf(), malloc(), read() 같은 libc 함수 사용 가능
커널스페이스
- 커널과 드라이버가 실행되는 영역
- 하드웨어에 직접 접근 가능
- 잘못된 메모리 접근 → 커널 패닉, 시스템 다운
- libc 사용 불가 → printk(), kmalloc() 같은 커널 API 사용
⚠️ 커널 모듈은 커널스페이스에서 실행됩니다. printf() 대신 printk(), malloc() 대신 kmalloc()을 써야 합니다. 잘못 짜면 시스템이 죽습니다.
유저스페이스에서 하드웨어에 접근하려면?
User App
│
│ 시스템 콜 (read, write, ioctl ...)
▼
Kernel (드라이버)
│
│ 레지스터 접근
▼
Hardware
유저 앱은 시스템 콜을 통해 커널에 요청하고, 커널(드라이버)이 대신 하드웨어를 제어합니다.
3. 커널 모듈 기본 구조
커널 모듈의 최소 구조는 딱 두 함수입니다.
#include <linux/init.h>
#include <linux/module.h>
/* 모듈 로드 시 실행 — insmod 할 때 */
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel!\n");
return 0; /* 0 반환 = 성공, 음수 반환 = 실패 */
}
/* 모듈 언로드 시 실행 — rmmod 할 때 */
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel!\n");
}
module_init(hello_init); /* init 함수 등록 */
module_exit(hello_exit); /* exit 함수 등록 */
/* 모듈 메타데이터 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("OnePaperHoon");
MODULE_DESCRIPTION("Hello Kernel Module");
각 요소 설명
__init / __exit 매크로
static int __init hello_init(void)
__init은 이 함수가 초기화 시에만 쓰인다는 힌트입니다. 모듈 로드 후 해당 함수 코드를 메모리에서 해제해 공간을 절약합니다.
printk() vs printf()
printk(KERN_INFO "Hello, Kernel!\n");
커널에는 printf()가 없습니다. 대신 printk()를 씁니다. 앞의 KERN_INFO는 로그 레벨입니다.
| KERN_EMERG | 시스템 불능 |
| KERN_ERR | 에러 |
| KERN_WARNING | 경고 |
| KERN_INFO | 일반 정보 |
| KERN_DEBUG | 디버그 |
MODULE_LICENSE("GPL")
반드시 선언해야 합니다. GPL이 아닌 라이선스를 선언하면 일부 커널 심볼을 사용할 수 없고, 로드 시 "tainted kernel" 경고가 뜹니다.
4. Makefile 작성법
커널 모듈은 일반 C 프로그램과 빌드 방식이 다릅니다. 커널 빌드 시스템(Kbuild)을 활용합니다.
# Makefile
obj-m += hello.o # 빌드할 모듈 이름 (.o 확장자)
# Raspberry Pi에서 직접 빌드할 경우
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
Makefile 핵심 설명
obj-m
obj-m은 "이 파일을 모듈로 빌드하라"는 Kbuild 문법입니다. obj-y는 커널에 정적으로 포함시키는 것이고, obj-m은 동적 모듈(.ko 파일)로 빌드합니다.
$(KDIR)
현재 실행 중인 커널의 빌드 디렉토리입니다. uname -r로 커널 버전을 가져와 경로를 자동으로 맞춥니다. 크로스 컴파일(다른 머신에서 빌드)할 때는 이 경로를 대상 보드의 커널 소스로 바꿉니다.
-C $(KDIR) M=$(PWD)
커널 빌드 시스템 디렉토리로 이동해서(-C), 현재 디렉토리(M=$(PWD))의 모듈을 빌드하라는 뜻입니다.
5. 빌드 및 실행 — insmod / rmmod / lsmod
빌드
$ make
성공하면 hello.ko 파일이 생성됩니다. .ko가 커널 오브젝트(Kernel Object), 즉 모듈 파일입니다.
$ ls -l
-rw-r--r-- hello.c
-rw-r--r-- Makefile
-rw-r--r-- hello.ko ← 이게 생겼으면 성공
모듈 로드 — insmod
$ sudo insmod hello.ko
insmod는 insert module의 약자입니다. 커널에 모듈을 삽입합니다.
로드 확인 — lsmod
$ lsmod | grep hello
hello 16384 0
lsmod는 현재 로드된 모듈 목록을 보여줍니다. grep으로 필터링해서 확인합니다.
printk 출력 확인 — dmesg
$ dmesg | tail -5
[ 123.456789] Hello, Kernel!
printk() 출력은 터미널에 바로 보이지 않습니다. 커널 로그 버퍼에 저장되고, dmesg 명령으로 확인합니다.
모듈 언로드 — rmmod
$ sudo rmmod hello
$ dmesg | tail -5
[ 123.456789] Hello, Kernel!
[ 145.678901] Goodbye, Kernel!
rmmod는 remove module의 약자입니다.
명령어 정리
| insmod hello.ko | 모듈 로드 |
| rmmod hello | 모듈 언로드 |
| lsmod | 로드된 모듈 목록 |
| modinfo hello.ko | 모듈 정보 출력 |
| dmesg | 커널 로그 확인 |
6. 실습 — Raspberry Pi에서 Hello, Kernel!
환경 준비
# 커널 헤더 설치 (Raspberry Pi OS 기준)
$ sudo apt update
$ sudo apt install kernel-headers-$(uname -r)
# 설치 확인
$ ls /lib/modules/$(uname -r)/build
```
### 파일 구성
```
hello_module/
├── hello.c
└── Makefile
hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "[hello] module loaded\n");
printk(KERN_INFO "[hello] kernel version: %s\n", UTS_RELEASE);
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "[hello] module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("OnePaperHoon");
MODULE_DESCRIPTION("Hello Kernel Module for Raspberry Pi");
MODULE_VERSION("1.0");
빌드 및 실행
# 빌드
$ make
make -C /lib/modules/6.1.21+/build M=/home/pi/hello_module modules
CC [M] /home/pi/hello_module/hello.o
MODPOST /home/pi/hello_module/Module.symvers
CC [M] /home/pi/hello_module/hello.mod.o
LD [M] /home/pi/hello_module/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.1.21+'
# 로드
$ sudo insmod hello.ko
# 확인
$ dmesg | tail -3
[ 1234.567] [hello] module loaded
[ 1234.568] [hello] kernel version: 6.1.21+
# 언로드
$ sudo rmmod hello
$ dmesg | tail -1
[ 1289.012] [hello] module unloaded
7. 자주 하는 실수 모음
❌ 실수 1: printf() 사용
/* 나쁜 예 — 컴파일 에러 */
printf("Hello\n");
/* 좋은 예 */
printk(KERN_INFO "Hello\n");
커널 모듈에서 libc 함수는 사용할 수 없습니다.
❌ 실수 2: init에서 0이 아닌 값 반환
static int __init hello_init(void)
{
/* 초기화 실패 상황 */
return 1; /* 나쁜 예 — 양수 반환 */
return -ENOMEM; /* 좋은 예 — errno 음수값 반환 */
}
init 함수는 실패 시 반드시 음수 errno 값을 반환해야 합니다. 양수를 반환하면 예상치 못한 동작이 생길 수 있습니다.
❌ 실수 3: 커널 버전 불일치
$ sudo insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Invalid module format
모듈은 빌드한 커널 버전과 로드할 커널 버전이 정확히 일치해야 합니다. 커널 업데이트 후에는 반드시 재빌드하세요.
# 현재 커널 버전 확인
$ uname -r
6.1.21+
# 모듈이 빌드된 커널 버전 확인
$ modinfo hello.ko | grep vermagic
vermagic: 6.1.21+ SMP preempt mod_unload modversions ARMv7
8. 마치며
커널 모듈은 Linux 드라이버 개발의 시작점입니다.
커널 모듈은 커널스페이스에서 실행된다 — 실수하면 시스템이 죽는다.
printf 대신 printk, malloc 대신 kmalloc.
빌드한 커널 버전과 로드할 커널 버전이 일치해야 한다.
Hello World 수준이지만, 이 구조를 이해하면 GPIO 제어, I2C 드라이버, character device 구현까지 자연스럽게 이어집니다.