- 들어가며 — 처음으로 커널 안에서 실행되는 내 코드
- 1. 가장 작은 모듈
- 2. 트리 밖에서 빌드하기
- 3. 올리고 내리고 확인하기
- 4. printk와 로그 레벨
- 5. 모듈 파라미터
- 6. 커널 C의 규칙 — 짧은 예고
- 7. GPL 심볼과 오염 플래그
- 8. 다음 단계 — 트리 안의 예제 읽기
- 흔한 함정
- 마치며
- 직접 해보기
- 이전 편과 다음 편
- 참고 자료
들어가며 — 처음으로 커널 안에서 실행되는 내 코드
4편까지는 읽기였습니다. 남이 쓴 코드를 따라가며 커널이 어떻게 생겼는지 봤습니다. 이 글부터는 씁니다.
모듈이 학습 수단으로 좋은 이유가 있습니다. 커널을 다시 빌드하지 않아도 되기 때문입니다. 3편에서 확인했듯 커널 전체 빌드는 시간이 걸리고, 고쳐서 확인하는 주기가 길면 학습이 느려집니다. 모듈은 몇 초 만에 빌드되고 즉시 로드됩니다. 고치고, 빌드하고, 올리고, dmesg를 보는 사이클이 30초입니다.
이 블로그의 eBPF 기초 글과의 관계를 분명히 해 두겠습니다. eBPF도 커널 안에서 코드를 돌리는 방법이고, 요즘은 오히려 더 자주 쓰입니다. 하지만 둘은 성격이 완전히 다릅니다. eBPF 프로그램은 검증기를 통과해야 하고, 그래서 할 수 있는 일이 제한되는 대신 안전합니다. 모듈은 검증기가 없습니다. 올라가는 순간 커널 코드와 동등한 권한을 갖고, 실수하면 시스템이 멈춥니다. 이 글의 후반부가 그 대가를 다룹니다.
이 글의 실습은 반드시 가상 머신 안에서 하시기 바랍니다. 3편 6절에서 만든 QEMU 환경이면 충분합니다.
1. 가장 작은 모듈
파일 하나면 됩니다.
// SPDX-License-Identifier: GPL-2.0
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
pr_info("hello: 모듈이 올라왔습니다\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("hello: 모듈이 내려갑니다\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("가장 작은 커널 모듈 예제");
한 줄씩 뜯어보겠습니다.
module_init과 module_exit. include/linux/module.h에 정의된 매크로입니다. 모듈이 로드될 때 부를 함수와 내려갈 때 부를 함수를 등록합니다. 이름은 자유이고, 매크로에 넘기기만 하면 됩니다.
초기화 함수의 반환값이 0이어야 합니다. 4편 7절에서 본 규약 그대로입니다. 0이면 성공, 음수면 에러입니다. 음수를 반환하면 모듈이 올라가지 않고 insmod가 그 값을 에러로 보여 줍니다.
__init과 __exit 표시. include/linux/init.h를 열어 보면 이 매크로들이 함수를 특별한 섹션에 배치한다는 것을 알 수 있습니다. __init은 .init.text, __exit은 .exit.text입니다. 초기화가 끝나면 __init 코드가 차지하던 메모리를 커널이 회수합니다. 한 번만 쓰는 코드를 계속 들고 있을 이유가 없기 때문입니다.
MODULE_LICENSE는 선택이 아닙니다. 빠뜨리면 빌드 단계에서 걸립니다. 커널의 모듈 후처리 도구인 scripts/mod/modpost.c가 이 경우 오류를 냅니다.
error: missing MODULE_LICENSE() in <모듈 이름>
MODULE_DESCRIPTION도 넣는 습관을 들이세요. 같은 도구가 이쪽은 경고로 처리합니다.
warning: missing MODULE_DESCRIPTION() in <모듈 이름>
pr_info. printk 계열 매크로입니다. 4절에서 자세히 다룹니다.
2. 트리 밖에서 빌드하기
모듈을 커널 소스 트리 안에 넣지 않고 별도 디렉터리에서 빌드하는 것이 표준적인 학습 방식입니다. 커널 공식 문서 Documentation/kbuild/modules.rst가 제시하는 Makefile 예제는 이렇습니다.
ifneq ($(KERNELRELEASE),)
# kbuild part of makefile
obj-m := 8123.o
8123-y := 8123_if.o 8123_pci.o
else
# normal makefile
KDIR ?= /lib/modules/`uname -r`/build
default:
$(MAKE) -C $(KDIR) M=$$PWD
endif
같은 문서가 KDIR에 대해 설명하기를, 커널 소스 디렉터리의 경로이거나 별도 빌드 디렉터리를 썼다면 그 출력 디렉터리의 경로라고 합니다. 표준 위치는 /lib/modules/ 아래의 실행 중인 커널 디렉터리에 있는 build 링크이며, 여기에 외부 모듈을 빌드하는 데 필요한 커널과 빌드 산출물이 들어 있습니다.
우리 예제에 맞춘 최소 형태는 이렇습니다. 파일 이름을 hello.c라고 하면 됩니다.
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
Makefile은 탭으로 들여써야 합니다. 공백을 쓰면 make가 거부합니다. 이 형태를 처음 복사해 쓸 때 가장 자주 나는 오류입니다.
빌드 명령의 구조를 이해해 두면 오류를 읽기 쉬워집니다. 문서가 제시하는 형태는 이렇습니다.
make -C /lib/modules/`uname -r`/build M=$PWD
make -C /lib/modules/`uname -r`/build M=$PWD modules_install
-C로 커널 빌드 디렉터리로 이동해 그쪽 Makefile을 실행하되, M= 변수로 "내 모듈은 여기 있다"고 알려 주는 방식입니다. 즉 빌드 규칙은 커널의 것을 쓰고 소스만 밖에서 가져오는 구조입니다.
빌드가 끝나면 여러 파일이 생기는데, 실제로 쓰는 것은 .ko 하나입니다. 나머지는 중간 산출물입니다.
빌드가 실패한다면 대개 커널 헤더가 없는 경우입니다. /lib/modules/ 아래의 실행 중인 커널 디렉터리에 build 링크가 살아 있는지 확인하세요. 배포판 커널을 쓴다면 헤더 패키지를 설치해야 합니다. Debian 계열은 linux-headers-$(uname -r), RHEL 계열은 kernel-devel입니다. 3편에서 직접 빌드한 커널이라면 그 소스 트리가 그대로 build 링크의 목적지입니다.
3. 올리고 내리고 확인하기
# 로드
sudo insmod ./hello.ko
# 로드된 모듈 확인
lsmod | grep hello
# 모듈 정보 보기 — 라이선스, 설명, 파라미터가 나옵니다
modinfo ./hello.ko
# 로그 확인
sudo dmesg | tail -5
# 내리기
sudo rmmod hello
네 명령의 차이를 정리해 두겠습니다.
| 명령 | 하는 일 |
|---|---|
insmod | 지정한 파일 하나를 그대로 로드합니다. 의존성을 해결하지 않습니다 |
modprobe | 모듈 이름으로 찾아서 의존하는 모듈까지 함께 로드합니다 |
rmmod | 모듈 하나를 내립니다 |
modinfo | 로드하지 않고 모듈 파일의 메타데이터만 읽습니다 |
lsmod | 지금 로드된 모듈 목록입니다. /proc/modules를 읽어 보여 줍니다 |
실습 단계에서는 insmod와 rmmod면 충분합니다. modprobe는 모듈이 시스템에 설치된 뒤에 의미가 있습니다.
rmmod가 거부하는 경우가 있습니다. 참조 횟수가 0이 아니면 내려가지 않습니다. lsmod 출력의 세 번째 열이 그 숫자입니다. 누가 이 모듈을 쓰고 있는지도 그 옆에 나옵니다. 초기화 함수에서 등록한 것을 종료 함수에서 해제하지 않으면 이 숫자가 남아 영원히 내려가지 않는 모듈이 되고, 그러면 재부팅 외에 방법이 없습니다.
모듈을 내리는 기능 자체가 없을 수도 있습니다. CONFIG_MODULE_UNLOAD가 꺼진 커널에서는 rmmod 자체가 동작하지 않습니다. 배포판 커널은 보통 켜 두지만, 3편에서 직접 만든 설정이라면 확인해 볼 만합니다.
4. printk와 로그 레벨
커널에는 printf가 없습니다. 대신 printk가 있고, 실제로는 그 위에 얹힌 pr_ 계열 매크로를 씁니다.
커널 문서 Documentation/core-api/printk-basics.rst가 정리한 대응 관계입니다.
| 로그 레벨 매크로 | 문자열 | 대응 매크로 |
|---|---|---|
KERN_EMERG | "0" | pr_emerg() |
KERN_ALERT | "1" | pr_alert() |
KERN_CRIT | "2" | pr_crit() |
KERN_ERR | "3" | pr_err() |
KERN_WARNING | "4" | pr_warn() |
KERN_NOTICE | "5" | pr_notice() |
KERN_INFO | "6" | pr_info() |
KERN_DEBUG | "7" | pr_debug(), pr_devel() |
KERN_CONT | "c" | pr_cont() |
실무적으로 알아야 할 것은 셋입니다.
레벨이 콘솔 출력 여부를 결정합니다. 커널은 현재 콘솔 로그 레벨보다 급한 메시지만 콘솔에 직접 찍습니다. 나머지는 링 버퍼에만 들어가고 dmesg로 봐야 합니다. 그래서 "분명히 pr_debug를 넣었는데 화면에 안 나온다"는 상황이 생깁니다.
# 현재 설정 확인 — 네 숫자가 나옵니다
cat /proc/sys/kernel/printk
# 콘솔로 모든 레벨을 내보내기
sudo dmesg -n 8
pr_debug는 기본적으로 아무것도 출력하지 않습니다. 동적 디버그 기능이 꺼져 있으면 컴파일 시점에 사라집니다. 실습 중이라면 그냥 pr_info를 쓰는 편이 혼란이 적습니다.
pr_fmt으로 접두사를 붙이세요. 같은 문서가 설명하듯, 이 매크로는 한 파일의 모든 메시지 앞에 공통 문자열을 붙이는 데 씁니다. 헤더를 포함하기 전에 정의해야 합니다.
#define pr_fmt(fmt) "hello: " fmt
#include <linux/init.h>
#include <linux/module.h>
이렇게 해 두면 본문에서는 접두사를 신경 쓰지 않아도 됩니다. dmesg에서 자기 모듈의 로그만 골라내기가 훨씬 쉬워집니다.
로그를 실시간으로 보려면 이렇게 합니다.
sudo dmesg -wH
-w가 새 메시지를 계속 따라가고, -H가 사람이 읽기 좋은 형태로 시간을 표시합니다. 모듈 실습 중에는 이 창을 하나 띄워 두는 것이 편합니다.
5. 모듈 파라미터
로드할 때 값을 넘길 수 있습니다.
#include <linux/moduleparam.h>
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "출력할 인사 횟수");
static char *name = "world";
module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "인사할 대상 이름");
module_param의 세 번째 인자는 sysfs에서의 권한입니다. include/linux/moduleparam.h의 주석이 설명하듯 0이면 sysfs 항목이 아예 생기지 않고, 0444면 누구나 읽을 수 있고, 0644면 root가 쓸 수 있습니다.
같은 헤더가 나열하는 표준 타입은 이렇습니다.
byte, hexint, short, ushort, int, uint, long, ulong
charp: a character pointer
bool: a bool, values 0/1, y/n, Y/N.
invbool: the above, only sense-reversed (N = true).
쓰는 법은 간단합니다.
sudo insmod ./hello.ko count=3 name=kernel
# 권한을 0644로 줬다면 로드 후에도 읽고 쓸 수 있습니다
cat /sys/module/hello/parameters/count
echo 5 | sudo tee /sys/module/hello/parameters/count
쓰기 가능한 파라미터에는 함정이 하나 있습니다. 같은 헤더의 주석이 경고하듯, 쓰기 가능하게 만들었다면 접근 시 잠금이 필요할 수 있습니다. 특히 문자열 포인터 타입은 값이 바뀔 때 이전 문자열이 해제되기 때문에, 모듈 코드가 그 포인터를 들고 있다가 참조하면 이미 해제된 메모리를 읽게 됩니다. 학습 단계에서는 파라미터를 읽기 전용으로 두는 편이 안전합니다.
6. 커널 C의 규칙 — 짧은 예고
여기서부터가 이 글의 경고 부분입니다. 자세한 것은 7편이 다루지만, 모듈을 쓰기 시작하는 순간 부딪히는 것들을 먼저 적어 두겠습니다.
표준 C 라이브러리가 없습니다
커널 문서 Documentation/process/howto.rst가 이 점을 분명히 합니다. 커널은 프리스탠딩 C 환경이고 표준 C 라이브러리에 의존하지 않으므로 C 표준의 일부는 지원되지 않으며, 임의의 long long 나눗셈과 부동소수점은 허용되지 않는다고 명시합니다.
구체적으로는 이렇게 바뀝니다.
| 사용자 공간에서 쓰던 것 | 커널에서 쓰는 것 |
|---|---|
printf | pr_info 계열 |
malloc / free | kmalloc / kfree |
memcpy / strlen | 같은 이름의 커널 자체 구현 |
assert | BUG_ON / WARN_ON |
double, float | 쓸 수 없습니다 |
부동소수점을 쓸 수 없다는 것이 처음에는 이상하게 들립니다. 이유는 성능입니다. 커널이 부동소수점 레지스터를 쓰려면 문맥 전환마다 그 레지스터들을 저장하고 복원해야 하고, 그 비용을 모든 시스템 콜이 나눠 지게 됩니다. 그래서 아예 쓰지 않기로 한 것입니다. 나눗셈이 필요하면 정수 연산으로 바꾸거나, 커널이 제공하는 전용 헬퍼를 씁니다.
스택이 작습니다
x86_64에서 커널 스택 크기는 arch/x86/include/asm/page_64_types.h에 정의돼 있고, 기본 설정에서 페이지 크기의 네 배입니다. 4KB 페이지 기준으로 16KB입니다. 사용자 공간 스레드가 보통 8MB를 갖는 것과 비교하면 500분의 1입니다.
그래서 커널 코드에서는 큰 지역 배열을 잡으면 안 됩니다. 커널 문서 Documentation/process/submit-checklist.rst도 make checkstack으로 스택을 많이 쓰는 함수를 찾아보라고 요구하며, 512바이트를 넘는 함수를 문제로 봅니다.
실패의 대가가 다릅니다
사용자 공간에서 널 포인터를 건드리면 그 프로세스만 죽습니다. 커널에서는 커널 oops가 나고, 운이 나쁘면 시스템 전체가 멈춥니다. 데이터가 디스크에 쓰이기 전이었다면 그대로 사라집니다.
이것이 이 글에서 실습을 가상 머신 안에서 하라고 반복해 말하는 이유입니다.
7. GPL 심볼과 오염 플래그
이 부분은 라이선스 이야기처럼 보이지만 실제로는 쓸 수 있는 API의 범위에 대한 이야기입니다.
커널은 모듈이 링크할 수 있는 심볼을 명시적으로 공개합니다. include/linux/export.h에 두 가지 형태가 있습니다.
#define EXPORT_SYMBOL(sym) _EXPORT_SYMBOL(sym, "")
#define EXPORT_SYMBOL_GPL(sym) _EXPORT_SYMBOL(sym, "GPL")
차이는 MODULE_LICENSE에 무엇을 적었느냐에 따라 갈립니다. include/linux/module.h의 주석이 그 효과를 직접 설명합니다. 라이선스 문자열의 유일한 목적은 독점 표시가 동작하게 만들고, 비자유 모듈이 로드됐을 때 EXPORT_SYMBOL_GPL로 익스포트된 심볼과의 결합을 거부하는 것이라고 되어 있습니다.
즉 MODULE_LICENSE("Proprietary")로 표시한 모듈은 GPL 전용 심볼을 쓸 수 없습니다. 그리고 커널 내부 API의 상당수가 GPL 전용입니다.
같은 주석이 문자열의 의미도 정리해 줍니다. "GPL v2"와 "GPL" 둘 다 GPL v2 라이선스임을 뜻할 뿐이며, only인지 or later인지를 구분하려던 역사적 시도가 실패한 결과라 지금은 구분이 무의미하다고 적혀 있습니다. "Dual MIT/GPL"이나 "Dual MPL/GPL" 같은 이중 라이선스 표기도 있습니다.
오염 플래그
모듈을 로드하면 커널에 흔적이 남습니다. Documentation/admin-guide/tainted-kernels.rst의 표에서 모듈과 관련된 항목만 뽑으면 이렇습니다.
| 비트 | 문자 | 뜻 |
|---|---|---|
| 0 | P | 독점 라이선스 모듈이 로드됨 |
| 1 | F | 모듈이 강제 로드됨 |
| 10 | C | staging 드라이버가 로드됨 |
| 12 | O | 트리 밖에서 빌드된 모듈이 로드됨 |
| 13 | E | 서명되지 않은 모듈이 로드됨 |
확인은 이렇게 합니다.
cat /proc/sys/kernel/tainted
우리가 만든 모듈을 올리면 이 값이 0이 아니게 됩니다. 최소한 트리 밖 모듈 비트가 켜집니다. 이건 잘못된 것이 아니라 정상입니다. 다만 이 상태에서 발생한 커널 버그를 메일링 리스트에 보고하면, 개발자들이 먼저 하는 질문이 "그 모듈 빼고도 재현됩니까"가 됩니다. 오염된 커널의 버그 리포트는 신뢰도가 낮게 취급되기 때문입니다.
서명
3편 7절에서 다룬 CONFIG_MODULE_SIG_FORCE가 켜져 있으면 서명되지 않은 모듈은 아예 로드되지 않습니다. insmod가 거부하면 이 설정을 먼저 확인해 보세요. 실습용 커널에서는 꺼 두는 편이 편합니다.
8. 다음 단계 — 트리 안의 예제 읽기
hello.ko가 올라갔다면 다음은 무엇을 할 것인가. 좋은 답이 커널 트리 안에 있습니다. samples/ 디렉터리입니다.
특히 samples/kobject/kobject-example.c를 권합니다. 이 예제는 sysfs에 디렉터리를 하나 만들고 그 안에 파일 셋을 만들어, 정수를 쓰면 저장하고 읽으면 돌려주는 모듈입니다. 주석이 그렇게 설명합니다.
이 예제가 좋은 이유는 커널이 사용자 공간에 인터페이스를 노출하는 표준 방식을 그대로 보여 주기 때문입니다. 우리가 /sys 아래에서 보는 파일들이 전부 이런 코드로 만들어집니다. 5절에서 다룬 모듈 파라미터도 결국 이 메커니즘 위에 얹혀 있습니다.
samples/ 아래에는 kprobes, ftrace, trace_events, seccomp, livepatch 예제도 있습니다. 6편에서 kprobes를 다룰 때 그쪽 예제를 다시 보게 됩니다.
흔한 함정
Makefile을 공백으로 들여썼습니다. make가 거부합니다. 탭이어야 합니다.
커널 헤더가 없어 빌드가 안 됩니다. 2절 마지막의 헤더 패키지를 확인하세요.
빌드한 모듈이 로드를 거부합니다. 커널 버전이 다른 경우가 가장 흔합니다. 모듈은 빌드한 커널 버전에서만 로드됩니다. modinfo로 모듈이 기대하는 버전을 확인하고 uname -r과 비교하세요.
MODULE_LICENSE를 빼먹었습니다. 빌드 자체가 오류로 멈춥니다.
초기화 함수에서 실패했는데 0을 반환합니다. 모듈은 올라간 것으로 처리되지만 실제로는 아무것도 준비되지 않은 상태가 됩니다. 실패하면 반드시 음수 errno를 반환하고, 그때까지 잡아 둔 자원을 역순으로 해제해야 합니다.
종료 함수에서 해제를 빠뜨렸습니다. 등록한 것을 해제하지 않으면 참조 횟수가 남아 모듈이 내려가지 않거나, 더 나쁘게는 내려간 뒤 커널이 사라진 코드를 호출해 시스템이 멈춥니다. 초기화에서 A, B, C를 준비했다면 종료에서는 C, B, A 순으로 정리하는 습관을 들이세요.
호스트 머신에서 실습합니다. 6절에서 말한 대로입니다. 널 포인터 하나로 작업 중이던 것이 전부 날아갈 수 있습니다.
마치며
모듈은 커널 학습에서 가장 만족스러운 단계입니다. 처음으로 자기가 쓴 코드가 커널 로그에 나타나는 순간이 있고, 그 순간부터 커널이 남의 물건이 아니게 됩니다.
동시에 이 단계는 처음으로 대가가 실재하는 단계이기도 합니다. 앞의 네 편에서는 최악의 경우가 "컴파일 실패"였지만, 여기서부터는 "시스템 정지"입니다. 그래서 이 글이 가상 머신 이야기를 여러 번 반복했습니다.
정리하면 이렇습니다. 모듈에는 초기화 함수와 종료 함수가 있고, 반환값 규약은 커널 전체와 같으며, 라이선스 표시는 필수이고 그 값이 쓸 수 있는 API의 범위를 결정합니다. 로그는 pr_ 계열로 남기고 dmesg로 봅니다. 초기화에서 잡은 것은 종료에서 역순으로 놓습니다.
다음 편은 관측과 디버깅입니다. 지금까지는 로그를 찍어 확인했지만, 커널에는 훨씬 강력한 도구들이 있습니다. 그리고 그 도구들을 다 소개한 다음에도 결국 printk가 가장 많이 쓰인다는 이야기도 함께 하겠습니다.
이 글의 매크로 정의와 문서 인용은 2026년 8월 19일에 메인라인 트리와 공식 문서에서 직접 확인했습니다.
직접 해보기
- 리눅스 터미널 — insmod와 dmesg 흐름을 손에 익히기
- eBPF 플레이그라운드 — 모듈과 eBPF의 차이를 직접 비교해 보기
- 코드 정리 도구 — 커널 코딩 스타일과 비교해 볼 C 코드 정리
이전 편과 다음 편
참고 자료
모든 링크는 2026년 8월 19일에 확인했습니다.
- Building External Modules — 트리 밖 모듈 Makefile과 빌드 명령
- Message logging with printk — 로그 레벨 표와
pr_fmt - HOWTO do Linux kernel development — 프리스탠딩 환경, 부동소수점 금지
- Tainted kernels — 오염 플래그 표
- Kernel patch submission checklist —
make checkstack - Linux kernel licensing rules
현재 단락 (1/170)
[4편](/blog/linux/2026-08-19-kernel-reading-a-syscall-end-to-end)까지는 읽기였습니다. 남이 쓴 코드를 따라가며 커널이 어떻게 생겼...