리눅스 커널 모듈과 드라이버 개발의 핵심
LKM, 디바이스 드라이버, 커널 패치 개발의 구조와 Kbuild, 디버깅, 보안 고려사항을 정리한다.
2026-08-14 · 최초 발행 2026-01-19
런타임 확장부터 소스 수정까지
커널 기능을 넓히거나 하드웨어를 지원하려면 사용자 공간과 다른 수준의 프로그래밍이 필요하다. LKM(Loadable Kernel Module)은 커널을 다시 컴파일하지 않은 채 동적으로 기능을 더하거나 뺄 수 있게 한다. 디바이스 드라이버는 하드웨어와 커널의 접점을 제공하고, 커널 패치는 소스 코드 자체를 바꿔 기능이나 결함을 다룬다.
이 영역에서는 시스템 내부 구조를 이해하는 것만으로는 부족하다. 메모리 관리, 동기화, 오류 처리가 정확하지 않으면 작은 변경도 시스템 전체에 영향을 줄 수 있다.
LKM이 커널을 확장하는 방식
LKM은 커널 이미지를 재빌드하지 않고 런타임에 기능을 추가하거나 제거하는 메커니즘이다. 모놀리식 커널의 유연성을 높이고, 커널 크기를 줄여 메모리를 절약하며, 재부팅 없이 기능을 넣고 뺄 수 있다. 벤더가 드라이버를 독립적으로 배포하는 데에도 활용된다.
모듈은 파일을 로드한 뒤 초기화 함수를 실행하고 커널에 기능을 등록한다. 제거 시에는 종료 함수에서 등록을 해제한 뒤 메모리에서 사라진다.
기본 모듈은 초기화 함수와 종료 함수를 등록하는 형태로 구성한다.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple kernel module");
MODULE_VERSION("1.0");
static int __init hello_init(void) {
printk(KERN_INFO "Hello, Kernel!\n");
return 0; // 0: 성공, 음수: 실패
}
static void __exit hello_exit(void) {
printk(KERN_INFO "Goodbye, Kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
의존성이 있는 모듈은 modprobe가 의존 모듈을 해결하고 로드한 다음 대상 모듈의 심볼 테이블을 연결하는 흐름을 따른다.
하드웨어를 커널 인터페이스로 만드는 드라이버
디바이스 드라이버는 하드웨어 장치와 커널 사이의 추상화 계층을 제공하는 특수 모듈이다. 장치의 입출력 모델과 연결 방식에 따라 구현 방식이 달라진다.
| 유형 | 특징 | 예시 |
|---|---|---|
| 문자 디바이스 | 바이트 스트림 I/O | 시리얼 포트, 키보드 |
| 블록 디바이스 | 블록 단위 I/O, 캐싱 | 하드디스크, SSD |
| 네트워크 디바이스 | 패킷 전송/수신 | 이더넷 카드, WiFi |
| USB 드라이버 | USB 프로토콜 처리 | USB 저장장치 |
| PCI 드라이버 | PCI 버스 장치 | 그래픽 카드 |
문자 디바이스는 VFS를 통해 열기, 쓰기, 닫기 요청을 받고, 드라이버가 하드웨어 초기화와 데이터 전송, 장치 정리를 처리한다.
파일 연산과 문자 디바이스 등록, 클래스 및 /dev 항목 생성은 다음과 같이 연결한다.
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = device_open,
.release = device_release,
.read = device_read,
.write = device_write,
.unlocked_ioctl = device_ioctl,
};
static int __init device_init(void) {
// 메이저 번호 할당
major = register_chrdev(0, DEVICE_NAME, &fops);
// 디바이스 클래스 생성
device_class = class_create(THIS_MODULE, CLASS_NAME);
// /dev 파일 생성
device_device = device_create(device_class, NULL,
MKDEV(major, 0), NULL, DEVICE_NAME);
return 0;
}
인터럽트가 발생하면 커널은 ISR(Interrupt Service Routine)을 호출한다. Top Half는 즉시 처리해야 하는 최소 작업을 맡고, Tasklet이나 Work Queue 같은 Bottom Half는 지연 가능한 작업을 처리한다.
커널 소스를 바꿀 때의 작업과 제출 흐름
커널 패치는 소스 코드를 직접 바꿔 기능을 추가하거나 버그를 수정하는 방식이다. 변경 후에는 빌드와 테스트를 반복하고, 문제가 있으면 디버깅을 거쳐 코드 수정 단계로 되돌아간다. 정상 동작을 확인한 뒤 커밋과 패치 파일을 만들고 메인테이너에게 제출한다.
커널 소스 트리는 관심 영역별로 나뉜다.
| 디렉토리 | 내용 |
|---|---|
| arch/ | 아키텍처별 코드 (x86, arm 등) |
| drivers/ | 디바이스 드라이버 |
| fs/ | 파일 시스템 |
| kernel/ | 핵심 커널 코드 |
| mm/ | 메모리 관리 |
| net/ | 네트워크 스택 |
| include/ | 헤더 파일 |
변경 내용을 확인하고 서명된 커밋을 만든 뒤 패치를 생성·검증·전송하는 명령은 다음과 같다.
# 변경사항 확인
git diff
# 커밋 생성
git commit -s -m "subsystem: brief description
Detailed explanation of the change.
Signed-off-by: Your Name <your@email.com>"
# 패치 파일 생성
git format-patch -1
# 패치 검증
./scripts/checkpatch.pl 0001-*.patch
# 메일로 제출
git send-email --to=maintainer@kernel.org 0001-*.patch
커널 코딩 스타일에서는 탭 크기 8자와 탭 들여쓰기를 사용한다. 한 줄은 최대 80자를 기준으로 하되 예외를 허용한다. 함수는 한 화면에 들어가도록 작성하며, 오류 처리에서는 goto 사용이 허용된다. 전처리기 매크로보다 inline 함수를 권장한다.
로그와 추적으로 문제를 좁히기
커널 로깅은 printk의 로그 레벨로 상황을 남기고 dmesg, journalctl로 확인한다.
// 로그 레벨별 출력
printk(KERN_EMERG "시스템 사용 불가"); // 0
printk(KERN_ALERT "즉시 조치 필요"); // 1
printk(KERN_CRIT "치명적 상황"); // 2
printk(KERN_ERR "오류 발생"); // 3
printk(KERN_WARNING "경고"); // 4
printk(KERN_NOTICE "정상이나 주목 필요"); // 5
printk(KERN_INFO "정보성 메시지"); // 6
printk(KERN_DEBUG "디버그 메시지"); // 7
// 로그 확인
dmesg | tail -20
journalctl -k -f
KGDB, Ftrace, Kprobes, Crash Dump는 원격 디버깅, 함수 호출 추적, 런타임 프로브, 커널 패닉 분석을 위한 도구다.
메모리와 동시성 문제를 점검할 때는 KASAN(Kernel Address Sanitizer)으로 메모리 오용을 탐지하고, UBSAN(Undefined Behavior Sanitizer)으로 정의되지 않은 동작을 탐지한다. LOCKDEP은 데드락 가능성 분석에, KMEMLEAK은 메모리 누수 탐지에 사용한다.
Kbuild에서 모듈을 빌드하는 방법
모듈의 오브젝트 구성과 빌드 규칙은 Makefile에 둔다.
# 모듈 이름
obj-m += mymodule.o
# 여러 파일로 구성된 모듈
mydriver-objs := main.o helper.o
obj-m += mydriver.o
# 빌드
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Kconfig 항목을 추가하면 커널 구성 과정에 드라이버 선택지를 통합할 수 있다.
config MY_DRIVER
tristate "My Custom Driver"
depends on PCI
help
This is my custom PCI driver.
If you don't know what this is, say N.
다른 아키텍처나 특정 커널 소스 트리를 대상으로 빌드할 때는 대상 환경을 명시한다.
# ARM 타겟용 빌드
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- modules
# 특정 커널 트리 대상
make -C /path/to/kernel/source M=$(PWD) modules
커널 공간에서 지켜야 할 보안 경계
사용자 공간 포인터는 검증하고, 커널 메모리로 복사할 때는 안전한 복사 함수를 사용해야 한다. 권한이 필요한 동작에는 capability 검사도 필요하다.
// 사용자 공간 포인터 검증
if (!access_ok(user_ptr, size))
return -EFAULT;
// 사용자 공간으로부터 안전한 복사
if (copy_from_user(&kernel_data, user_ptr, size))
return -EFAULT;
// 권한 확인
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
공유 자원에서는 임계구역의 성격에 맞는 동기화 도구를 선택한다. Spinlock은 짧은 임계구역, Mutex는 슬립 가능한 작업, Read-Write Lock은 다중 읽기, RCU는 읽기 최적화와 관련된다.
메모리 안전성 측면에서는 커널 공간과 사용자 공간 포인터를 섞지 않아야 한다. 버퍼 오버플로우를 막기 위해 strcpy 대신 strscpy를 사용하고, 해제 후 포인터를 NULL로 설정해 Use-after-free를 방지하며, 정수 오버플로우도 검사한다.