가장 단순한 예시인 Hello World 모듈로 커널 모듈을 빌드하고 실행해보면서 커널 모듈 작성법을 익혀보자.
hello.c
/* HEADER SECTION BEGIN */
#include<linux/module.h>
/* HEADER SECTION END */
/* CODE SECTION BEGIN */
static int __init hello_init(void)
{
pr_info("Hello world\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("Goodbye world\n");
}
/* CODE SECTION END */
/* REGISTRATION SECTION BEGIN */
module_init(hello_init);
module_exit(hello_exit);
/* REGISTRATION SECTION END */
/* MODULE DESCRIPTION SECTION BEGIN */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ME");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_INFO(board, "BeagleBone Black Rev C");
MODULE_VERSION("1");
/* MODULE DESCRIPTION SECTION END */
1. 커널 모듈 작성법
1.1 Header 파트
헤더 파트에는 커널 모듈 작성에 필요한 커널 헤더 파일을 포함시킨다. 대부분의 커널 헤트는 <kernel-base>/include/linux/ 경로에 위치한다. 커널 모듈 작성시에는 kernel space 코드만 사용해야 하기 때문에 user space library 헤더파일을 포함하지 않도록 주의한다. (ex.C standard library)
#include<linux/module.h>
1.2 Code 파트
- 커널 모듈의 코드 파트에는 반드시 포함되어야 하는 init 함수와 exit 함수가 있으며, 각 함수는 모듈당 하나만 존재해야 한다.
- init 함수와 exit 함수는 함수의 범위를 모듈 내부로 제한하기 위해 static 키워드를 사용한다.
- 각각 __init, __exit 매크로 함수 앞에 붙이는데 정적 모듈의 경우에만 의미가 있다.
static int __init hello_init(void)
{
pr_info("Hello world\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("Goodbye world\n");
}
1.2.1 Init Function
- 모듈이 로드될 때 호출되고 메모리 할당, 장치 초기화 등 주요 설정 작업을 수행한다.
- 모듈의 entry point로 정적 모듈의 경우 부팅 시 호출되고, 동적 모듈의 경우 insmod 명령으로 삽입 시 호출된다.
- static int __init foo(void); 의 형태로 초기화에 성공한 경우 0을, 그렇지 않은 경우 0이 아닌 값을 반환한다.
- __init, __initdata, __initconst 매크로는 코드나 데이터를 .init.text, .init.data, .init.rodata 섹션에 배치하도록 하는 컴파일러 지시어이다. .init 섹션은 최종 커널 이미지에는 포함되어 있지만 부팅 시 한 번만 사용되고 그 후로 사용되지 않으므로 커널은 부팅 시 초기화 함수 호출 후 메모리에서 .init 섹션을 해제하여 메모리를 절약한다.
1.2.2 Exit function
- 모듈이 제거될 때 호출되며 초기화 작업을 원래대로 되돌리는 역할을 수행한다.
- static 모듈의 경우 커널에서 제거될 일이 없으므로, 동적 모듈의 경우에만 rmmod 명령으로 제거 시 호출된다.
- static void __exit foo(void); 의 형태로 반환타입은 void이다.
- __exit 매크로는 코드를 .exit.text 섹션에 배치하도록 하는 컴파일러 지시어이다. 정적 모듈에서는 exit 함수가 필요하지 않기 때문에 커널 빌드 시스템은 __exit이 붙은 함수를 빌드 과정에서 제외하여 최종 커널 이미지에 포함시키지 않는다.
1.3 등록 파트
module_init, module_exit 매크로 함수를 통해 커널 모듈의 init과 exit 함수를 커널에 등록한다.
module_init(hello_init);
module_exit(hello_exit);
1.4 설명 파트
- 모듈에 대한 메타데이터를 포함한다.
- MODULE_LICENSE가 GPL이 아닌 경우 커널이 "tainted" warning을 발생시킨다.
- MODULE_INFO로 key-value 타입의 사용자 정의 데이터를 추가할 수 있고, modinfo 명령으로 확인 가능하다.
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ME");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_INFO(board, "BeagleBone Black Rev C");
MODULE_VERSION("1");
2. 커널 모듈 컴파일 하기
2.1 빌드 방법에 따른 커널 모듈의 구분
커널 모듈은 빌드 방법에 따라 다음과 같이 구분할 수 있다.
- 정적 모듈 : 정적으로 커널 이미지에 포함된 모듈
- 동적 모듈 : 동적으로 로드 가능한 모듈. In-tree 모듈과 Out-of-tree 모듈로 나뉜다.
- In-tree 모듈 : 리눅스 커널 소스 트리 내부에 존재하는 모듈
- Out-of-tree 모듈 : 리눅스 커널 소스 트리 외부에서 작성된 모듈로 로드 시 "tainted" warning이 발생한다.
여기서는 Out-of-tree 모듈 빌드 방법에 대해서 알아본다.
2.2 빌드 전 준비사항
리눅스 커널 모듈을 빌드할 때는 kbuild라고 하는 Makefile 기반의 커널 빌드 시스템을 사용하여 빌드한다. 이 때 로컬 환경에서 개발하는게 아니라 다른 타겟에서 동작할 커널 모듈을 빌드하는 경우 사전 준비가 필요하다.
- 로컬 환경에서 개발하는 경우 : 일반적으로는 모듈을 빌드하는데 필요한 커널 헤더와 kbuild(Makefile) 시스템이 이미 설치되어 있고 이에 대한 심볼릭 링크가 /lib/modules/<커널버전>/build 경로에 존재할 것이다. 해당 빌드 시스템을 사용해 바로 커널 모듈을 제작할 수 있다.
- 크로스 컴파일 환경에서 개발하는 경우 : 타겟의 커널 버전과 동일한 버전의 커널 소스와 크로스 컴파일러를 다운로드 하고 타겟 시스템의 구성(.config)에 맞게 사전 컴파일된 커널 소스 트리를 준비해야 한다.
2.3 커널 모듈 빌드 명령어 (Out-of-tree)
커널 모듈 빌드에 사용되는 make 명령은 다음과 같다. -C 옵션을 사용해서 먼저 커널 소스 트리의 탑 레벨 Makefile을 호출하고 그 다음 모듈 소스가 있는 워킹 디렉토리로 이동해 로컬 Makefile을 호출하는 순서로 진행된다.
참고로 -C 옵션은 지정된 디렉토리로 이동하여 작업을 수행한 뒤 다시 원래 디렉토리로 돌아오는 동작을 구현한다.
make -C <path-to-linux-kernel-tree> M=<path-to-the-module> [target]
ex.
KDIR=/path/to/linux/kernel/tree/
make -C $(KDIR) M=$(PWD) modules
- [target]의 종류
- modules : 기본 타겟으로 모듈 소스 코드를 컴파일하여 커널 모듈(.ko) 파일을 생성한다.
- modules_install : 모듈을 설치(복사)한다. 기본 설치 위치는 /lib/modules/<커널버전>/의 하위 디렉토리이다.
- clean : 디렉토리 내 생성된 모든 파일 제거
- help : 사용할 수 있는 target 목록 표시
2.4 Makefile 작성
Makefile 상단에는 kbuild 시스템이 해당 모듈을 어떻게 컴파일할지 지정하기 위해 다음 문구를 추가한다.
obj-<X> := <module_name>.o
- <X>의 종류
- n : 모듈을 컴파일하지 않음
- y : 모듈을 커널 이미지와 함께 컴파일하고 링크함
- m : 모듈을 동적으로 로드 가능한 커널 모듈로 컴파일 (.ko 파일 생성)
예를 들어 obj-m := hello.o로 설정하면 hello.c 소스 파일이 컴파일되어 main.o 오브젝트 파일이 생성되고 최종적으로 main.ko라는 동적으로 로드 가능한 커널 모듈이 생성된다.
만약 내가 작성한 커널 모듈만 지정하여 빌드하는게 아니라 in-tree 모듈로서 여러 모듈이 한꺼번에 빌드될 때 추가하여 빌드되도록 하는 경우에는 아래와 같이 +=을 사용해 추가할 수 있다.
obj-m += my_module.o
그럼 위에서 작성한 hello.c 모듈 소스파일을 빌드하기 위한 Makefile을 작성해보자.
- 로컬에서 빌드하는 경우 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
help:
make -C $(KDIR) M=$(PWD) help
만약 /lib/modules/$(uname -r)/build 경로가 존재하지 않는 경우에는 아래 명령으로 커널 헤더와 커널 모듈 빌드 환경을 설치할 수 있다.
$ sudo apt-get install linux-headers-$(uname -r)
- 크로스 컴파일로 빌드하는 경우시 Makefile 예시
obj-m := hello.o
ARCH = arm
CROSS_COMPILE = arm-linux-gnueabihf-
KDIR = /home/jo/workspace/ldd/source/linux-5.10.168-ti-rt-r76/
all:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules
clean:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean
help:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) help
참고로 위의 Makefile에서 CROSS_COMPILE 환경변수에 전체 경로를 적지 않고 gcc 앞에 붙을 접두사만 적은 것은 나의 경우 크로스 컴파일러가 위치한 경로가 PATH 환경 변수에 포함되어 있기 때문이다. 그러니 그렇지 않은 경우에는 크로스 컴파일러가 위치한 경로를 PATH 환경변수에 추가하거나 Makefile 내에서 절대경로로 지정해주어야 한다.
$ vim ~/.bashrc
맨 밑줄에 export PATH=/path/to/toolchains/gcc-arm/bin:$PATH 추가
$ source ~/.bashrc
또는
CROSS_COMPILE = /path/to/toolchains/gcc-arm/bin/arm-linux-gnueabihf-
- 하나의 Makefile에서 로컬 모듈 빌드와 외부 타겟 모듈 빌드를 같이 하는 경우 Makefile 예시
아래와 같이 작성하면 make 명령으로는 타겟용 크로스 컴파일러로 모듈을 빌드하고 make host 명령으로는 host용 모듈을 빌드한다.
obj-m := hello.o
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
KERN_DIR=/home/jo/workspace/ldd/source/linux-5.10.168-ti-rt-r76/
HOST_KERN_DIR=/lib/modules/$(shell uname -r)/build/
all:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERN_DIR) M=$(PWD) modules
clean:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERN_DIR) M=$(PWD) clean
help:
make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERN_DIR) M=$(PWD) help
host:
make -C $(HOST_KERN_DIR) M=$(PWD) modules
- 컴파일러 옵션을 추가 전달해야 하는 경우
EXTRA_CFLAGS 변수를 사용할 수 있다. EXTRA_CFLAGS는 커널 빌드 시스템에서 사용자가 추가적인 컴파일러 플래그를 전달하기 위해 특별히 예약된 변수로 명시적으로 타겟 명령에 포함하지 않고 EXTRA_CFLAGS 변수에 += 로 값을 추가하는 것만으로도 적용이 된다.
다음은 현재 디렉토리를 헤더파일을 검색하는 경로로 추가하고 DEBUG 매크로를 정의하는 예시이다.
EXTRA_CFLAGS += -I$(PWD)
EXTRA_CFLAGS += -DDEBUG
- 커널 빌드 시스템(KBUILD)의 자세한 빌드 과정의 정보를 출력하도록 설정하기
타겟 명령 맨 뒤에 V=1 을 추가하거나 명령 실행 전 환경 변수 V를 1로 설정하면 된다.
make -C /path/to/kernel M=$(PWD) modules V=1
또는
export V=1
3. 커널 모듈 로드, 언로드
- 모듈 로드 : sudo insmod module_name.ko
- 모듈 제거 : sudo rmmod module_name
모듈이 잘 로드됐는지 확인하기 위해 dmesg 명령으로 커널 메시지를 확인하거나 현재 리눅스 커널에 올라와 있는 모든 모듈 목록을 표시하는 lsmod 명령으로 확인한다.
참고 : Fastbit Embedded Brain Academy
'임베디드 개발 > 리눅스 디바이스 드라이버' 카테고리의 다른 글
LDD ] printk를 활용한 디버깅 (0) | 2024.10.06 |
---|---|
LDD ] 리눅스 커널 모듈 In-tree building (0) | 2024.10.04 |
modprobe와 insmod의 차이 (0) | 2024.09.30 |
리눅스 커널 모듈 (Linux Kernel Module) (0) | 2024.09.28 |