본문 바로가기
임베디드 개발/리눅스 디바이스 드라이버

LDD ] Hello World 커널 모듈

by eteo 2024. 10. 2.

 

 

가장 단순한 예시인 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 기반의 커널 빌드 시스템을 사용하여 빌드한다. 이 때 로컬 환경에서 개발하는게 아니라 다른 타겟에서 동작할 커널 모듈을 빌드하는 경우 사전 준비가 필요하다.

 

  • 로컬 환경에서 개발하는 경우 : /lib/modules/<커널버전>/build 디렉토리에는 모듈을 빌드하는데 필요한 커널 헤더와 kbuild(Makefile)에 대한 심볼릭 링크가 존재한다. 해당 빌드 시스템을 사용해 바로 커널 모듈을 제작할 수 있다.
  • 크로스 컴파일 환경에서 개발하는 경우 : 타겟의 커널 버전과 동일한 버전의 커널 소스를 다운로드 하고 타겟 시스템의 구성(.config)에 맞게 사전 컴파일된 커널 소스 트리를 준비해야 한다.

 

 

2.3 커널 모듈 빌드 명령어 (Out-of-tree)

커널 모듈 빌드에 사용되는 make 명령은 다음과 같다. -C 옵션을 사용해서 먼저 커널 소스 트리의 탑 레벨 Makefile을 호출하고 그 다음 모듈 소스가 있는 워킹 디렉토리로 이동해 로컬 Makefile을 호출하는 순서로 진행된다.

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라는 동적으로 로드 가능한 커널 모듈이 생성된다.

 

 

그럼 위에서 작성한 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

 

 

- 크로스 컴파일로 빌드하는 경우시 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에서 로컬 모듈 빌드와 외부 타겟 모듈 빌드를 같이 하려면 아래와 같이 작성할 수 있다.

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

 

 

 

 

 

 

 


3. 커널 모듈 로드, 언로드

  • 모듈 로드 : sudo insmod module_name.ko
  • 모듈 제거 : sudo rmmod module_name

모듈이 잘 로드됐는지 확인하기 위해 dmesg 명령으로 커널 메시지를 확인하거나 현재 리눅스 커널에 올라와 있는 모든 모듈 목록을 표시하는 lsmod 명령으로 확인한다.

 

 

 

 

 

 

 

 

참고 : Fastbit Embedded Brain Academy