리눅스 커널 모듈이란
리눅스 커널 모듈은, 시스템에 설치된 커널 바이너리와는 다르게 동적으로 기능을 추가할 수 있는 오브젝트 파일이다. 예를 들어 커널에 어떤 기능을 추가하기 위해선 코드를 수정하고, 커널을 컴파일하고 설치한 후에 재부팅을 해야한다. 하지만 모듈로 작성하면 시스템이 실행중인 도중에도 동적으로 기능을 추가하거나 제거할 수 있다. 물론 모듈을 추가하거나 제거하려면 root 권한이 필요하다. 시스템 호출을 구현하거나, 문자/블록 디바이스 드라이버, 파일시스템 구현 등 다양하게 사용될 수 있다.
커널 모듈의 로딩과 언로딩
lsmod로 설치된 모듈을 확인하고, insmod로 모듈을 로드하고, rmmod로 모듈을 언로드할 수 있다. modprobe, depmod 등의 명령도 존재한다. 간단한 차이점을 서술하면 modprobe는 /lib/modules/$(uname -r)/modules.dep에서 모듈과 관련된 의존성을 확인해서 관련된 모듈을 자동으로 로드해주는 반면 insmod는 모듈의 직접적인 경로를 넘겨주어 순서대로 사용자가 로드해야 한다.
lsmod
sudo insmod moduleName.ko
sudo rmmod moduleName
Hello, World!
간단한 Hello, World! 커널 모듈을 작성해보자. 설명은 주석에 적어두었다. (helloworld.c)
#include <linux/module.h> /* necessary for every kernel module */
#include <linux/kernel.h> /* for printk */
/* 모듈을 로드할 때 초기화하는 함수 */
int __init helloworld_init(void)
{
/* "Hello, World!"를 출력한다. 커널에선 printf가 아니라 printk를 사용한다.
* KERN_DEBUG는 로그 레벨을 지정하는 매크로로
* KERN_INFO, KERN_ALERT, KERN_DEBUG 등이 있다.
* printk로 출력한 내용은 dmesg 명령어로 확인할 수 있다.
* 자세한 사항은: https://www.kernel.org/doc/html/latest/core-api/printk-basics.html
*/
printk(KERN_DEBUG "Hello, World!\n");
return 0;
}
/* 모듈을 언로드할 때 실행되는 함수 */
void __exit helloworld_exit(void)
{
printk(KERN_DEBUG "HelloWorld - made by hyeyoo\n");
}
/* 모듈의 init 함수를 등록한다 */
module_init(helloworld_init);
/* 모듈의 exit 함수를 등록한다 */
module_exit(helloworld_exit);
/* 모듈에 대한 정보(라이센스, 저자, 설명)를 나타내는 매크로이다. */
MODULE_AUTHOR("Hyeonggon Yoo <42.hyeyoo@gmail.com>");
MODULE_DESCRIPTION("simple Hello, World! module");
MODULE_LICENSE("GPL");
Makefile
NAME = helloworld
obj-m += ${NAME}.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
rm -f Module.symvers modules.order
rm -f ${NAME}.o ${NAME}.mod ${NAME}.mod.c ${NAME}.mod.o
fclean: clean
rm -f ${NAME}.ko
re: fclean all
빌드 및 모듈 로딩
make
sudo insmod helloworld.ko
dmesg
make와 insmod가 성공적으로 실행되었다면 dmesg 명령어를 입력하면 다음 출력문을 볼 수 있다. 서명된 모듈만 로드하는 옵션이 (CONFIG_MODULE_SIG) 활성화된 경우, 다음과 같은 오류가 나타날 수 있다. "module verification failed: signature and/or required key missing - tainting kernel" - 필자의 컴퓨터에서는 언로드하고 다시 로드하면 잘 불러와지는데, 서명이 필요한 경우 다음 글을 참고하자. "How to sign a kernel module"
EXPORT_SYMBOL과 외부 모듈 참조
우리가 위에서 만든 helloworld.ko 모듈은 독립적인 모듈이다. 이와 다르게 모듈은 서로에게 의존할 수 있다. 예를 들어서 복잡한 모듈을 작성할 때, 라이브러리 역할을 하는 모듈과 이를 참조하는 모듈을 분리해서 작성할 수 있다. 간단하게 adder 모듈을 만들어서 helloworld 모듈에서 adder 모듈을 참조하도록 해보자. 방법은 단순하다. adder 모듈에서 정의한 함수를 EXPORT_SYMBOL로 등록한 후에 helloworld 모듈에서 사용하면 된다. EXPORT_SYMBOL로 등록한 심볼은 cat /proc/kallsysms 명령어로 확인할 수 있다.
Makefile
makefile에는 adder와 helloworld 모두를 추가해준다.
ADDER = adder
NAME = helloworld
obj-m += ${ADDER}.o
obj-m += ${NAME}.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
rm -f Module.symvers modules.order
rm -f ${NAME}.o ${NAME}.mod ${NAME}.mod.c ${NAME}.mod.o
fclean: clean
rm -f ${NAME}.ko
re: fclean all
adder.c
#include <linux/module.h>
#include <linux/kernel.h>
int __init adder_init(void)
{
printk(KERN_DEBUG "loading adder\n");
return 0;
}
void __exit adder_exit(void)
{
printk(KERN_DEBUG "unloading adder\n");
}
int add(int x, int y)
{
return x + y;
}
EXPORT_SYMBOL(add);
module_init(adder_init);
module_exit(adder_exit);
MODULE_AUTHOR("Hyeonggon Yoo <42.hyeyoo@gmail.com>");
MODULE_DESCRIPTION("adder module for helloworld module");
MODULE_LICENSE("GPL");
helloworld.c
#include <linux/module.h>
#include <linux/kernel.h>
extern int add(int x, int y);
int __init helloworld_init(void)
{
printk(KERN_DEBUG "Hello, World! 1 + 1 = %d!\n", add(1, 1));
return 0;
}
void __exit helloworld_exit(void)
{
printk(KERN_DEBUG "HelloWorld - made By hyeyoo\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_AUTHOR("Hyeonggon Yoo <42.hyeyoo@gmail.com>");
MODULE_DESCRIPTION("helloworld module using adder module");
MODULE_LICENSE("GPL");
이제 make를 하고 modinfo로 각각의 모듈을 확인해보자. helloworld 모듈은 depends: adder라고 나와있다. 따라서 모듈을 로드하려면 adder를 먼저 로드하고, 그 다음 helloworld를 로드해야한다. (언로드할 때는 반대로!)
주의사항
커널 모듈은 커널 공간에서 실행된다. 커널 공간은 사용자 공간과 몇가지 차이점이 존재한다. 주된 차이점은 아래와 같다.
메모리 보호 없음
커널 공간에서 실행되므로, 메모리 보호가 되지 않는다. 이는 특히 디바이스 드라이버나 시스템 호출을 작성할 때 문제가 될 수 있다. 예를 들어서 우리가 read 시스템 호출을 등록했고, 사용자가 read(fd, buf, size)를 호출했는데, buf가 다른 프로세스의 메모리 주소를 참조하고 있다고 생각해보자. 이때 커널 모듈은 커널 공간에서 실행되므로 사용자 공간에서 처럼의 권한 체크는 하지 않는다. 따라서 사용자 공간에 접근할 때는 access_ok,copy_to_user, copy_from_user와 권한 체크를 하는 함수를 사용해야 한다.
고정적인 스택
스택의 크기가 고정적이다. 보통 1 ~ 2 페이지가 할당된다. 따라서 커널에서 스택을 무작정 사용하다보면 언젠가 스택이 부족해 문제가 발생할 수 있다.
libc 사용 불가능
libc에서 제공하는 함수를 사용할 수 없다.
모듈 파라미터
글에서 설명한 것 외에도, 모듈을 로드할 때 insmod helloworld key=value 처럼 파라미터를 넘길 수 있다. 이 부분은 관련 링크로 남기고 글을 마무리하겠다.
참고 문서
'Kernel' 카테고리의 다른 글
[Linux Kernel] proc 파일시스템과 seq_file 인터페이스 (1) | 2021.04.18 |
---|---|
[LInux Kernel] 문자 디바이스 드라이버 작성 (5) | 2021.04.17 |
[Linux Kernel] 가상 파일시스템이란 (VFS, Virtual Filesystem Switch) (0) | 2021.04.13 |
리눅스 커널에 커밋 해보자 (0) | 2021.03.14 |
리눅스 커널 입문에 도움이 될만한 것들 (0) | 2020.01.02 |
댓글