리눅스에서 실행 중인 디바이스 드라이버는 2가지 타입 중 하나입니다.
● 모듈 식 디바이스 드라이버
● 빌트인 식 디바이스 드라이버
모듈식 디바이스 드라이버가 설치 될 때 리눅스 내부에서 어떤 방식으로 동작하는지 살펴보겠습니다.
이 중에 모듈 형태의 디바이스 드라이버는 다음과 같은 명령어를 사용하면 리눅스에 설치할 수 있습니다.
가장 간단한 모듈식 디바이스 드라이버 코드
먼저 가장 간단한 형태의 모듈식 디바이스 드라이버의 소스를 봅시다.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk("<1> Hello world!\n");
return 0;
}
static int hello_exit(void)
{
printk("<1> Good Bye \n");
}
module_init(hello_init);
module_exit(hello_exit);
모듈식 디바이스 드라이버를 설치하는 방법
위 소스를 빌드하고 난 후 device_driver.ko 이란 파일이 생성됐다고 가정합시다.
리눅스 터미널을 열고 다음 명령어를 입력하면 device_driver.ko 파일이 설치됩니다.
$ insmod device_driver.ko
정상적으로 모듈식 디바이스 드라이버가 설치되면 hello_init() 함수가 호출될 것입니다.
static int hello_init(void)
{
printk("<1> Hello world!\n");
return 0;
}
module_init(hello_init);
그런데 공식처럼 외우고 있는 것은 바로 다음 문장입니다.
'insmod device_driver.ko' 명령어를 입력하면 hello_init() 함수가 호출된다.
이번 시간에 조금 더 배워 볼 내용은 '리눅스 내부의 어느 함수에서 hello_init() 함수를 호출하는가'입니다.
sys_init_module() 함수의 선언부와 인자 확인하기
먼저 sys_init_module() 함수의 선언부를 보겠습니다.
https://elixir.bootlin.com/linux/v5.4.30/source/kernel/module.c
SYSCALL_DEFINE3(finit_module, int, fd,
const char __user *, uargs, int, flags)
SYSCALL_DEFINE3 매크로로 선언된 함수는 컴파일 과정에서 다음과 같은 형태로 바뀝니다.
https://elixir.bootlin.com/linux/v5.4.30/source/include/linux/syscalls.h
asmlinkage long sys_finit_module(int fd, const char __user *uargs, int flags);
sys_finit_module() 함수에 전달되는 인자는 다음과 같이 정리할 수 있습니다.
■ fd: 파일 디스크립터
■ const char __user *uargs: insmod 명령어를 실행할 때 적용하는 아규먼트
■ int flags: insmod 명령어를 실행할 때 적용되는 플래그
sys_finit_module() 함수 분석
다음은 sys_finit_module() 함수의 구현부입니다.
https://elixir.bootlin.com/linux/v5.4.30/source/kernel/module.c
01 SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
02 {
03struct load_info info = { };
04loff_t size;
05void *hdr;
06int err;
07
08err = may_init_module();
09if (err)
10return err;
11
12pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);
13
14if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS
15 |MODULE_INIT_IGNORE_VERMAGIC))
16return -EINVAL;
17
18err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX,
19 READING_MODULE);
20if (err)
21return err;
22info.hdr = hdr;
23info.len = size;
24
25return load_module(&info, uargs, flags);
26 }
먼저 12번째 줄을 봅시다.
12pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);
디바이스 드라이버 모듈이 설치된다는 동작을 커널 로그로 출력합니다.
pr_debug() 함수의 이름을 printk() 함수로 바꿔도 똑같이 동작합니다.
18~19번째 줄을 분석하겠습니다.
18err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX,
19 READING_MODULE);
모듈의 헤더와 같은 정보를 읽는 동작입니다.
마지막으로 25번째 줄을 보겠습니다.
25return load_module(&info, uargs, flags);
load_module() 함수를 호출합니다.
load_module() 함수 분석
이어서 load_module() 함수를 분석합니다.
01 static int load_module(struct load_info *info, const char __user *uargs,
02 int flags)
03 {
04 struct module *mod;
05 long err = 0;
06 char *after_dashes;
07
08 err = elf_header_check(info);
09 if (err)
10 goto free_copy;
11
12 err = setup_load_info(info, flags);
13 if (err)
14 goto free_copy;
...
15 /* Done! */
16 trace_module_load(mod);
17
18 return do_init_module(mod);
...
19 }
사실 load_module() 함수의 구현부는 수 많은 함수를 호출해 해석하기 어렵습니다.
그런데 사실, 가장 중요한 동작을 요약하면 다음과 같습니다.
■ 설치하는 디바이스 드라이버 모듈 파일인 *.ko 의 헤더 정보를 읽어 *.ko 파일에 유효한 정보가 담겨 있는 지 확인
■ do_init_module() 함수 호출
커널 입장에서 유저가 어떤 상태의 *.ko 파일로 insmod 명령어를 입력해 드라이버 모듈을 설치하는지 예상하기 어렵습니다.
가끔은 깨진 파일이나 *.o 파일을 *.ko 파일로 변환해 insmod 명령어로 설치할 수 있어, 이를 방지하기 위한 코드입니다.
먼저 01~10번째 줄을 보겠습니다.
08 err = elf_header_check(info);
09 if (err)
10 goto free_copy;
elf_header_check() 함수를 호출해 설치하려는 *.ko 파일의 헤더 정보를 체크합니다.
참고로, *.ko 파일을 포함해 컴파일된 *.o 을 ELF 파일이라고 부르니 elf_header_check() 함수를 호출합니다.
이어서 18번째 줄을 보겠습니다.
18 return do_init_module(mod);
do_init_module() 함수를 호출합니다.
do_init_module() 함수 분석
이어서 do_init_module() 함수를 분석합시다.
01 static noinline int do_init_module(struct module *mod)
02 {
03 int ret = 0;
04 struct mod_initfree *freeinit;
...
05 /* Start the module */
06 if (mod->init != NULL)
07 ret = do_one_initcall(mod->init);
08 if (ret < 0) {
09 goto fail_free_freeinit;
10 }
06~07번째 줄을 보겠습니다.
06 if (mod->init != NULL)
07 ret = do_one_initcall(mod->init);
06번째 줄에서 'mod->init'이 NULL이 아닌지 체크하고 07번째 줄에서 mod->init를 인자로 삼아 do_one_initcall() 함수를 호출합니다.
여기서 'mod->init' 인자가 어떤 정보를 저장하는지 의문이 생깁니다.
'mod->init'는 예로 들었던 간단한 디바이스 드라이버 모듈 코드 기준으로 hello_init() 함수의 주소를 저장합니다.
아래는 이번 페이지에 예시로 들었던 hello_init() 함수의 구현부입니다.
static int hello_init(void)
{
printk("<1> Hello world!\n");
return 0;
}
module_init(hello_init);
do_one_initcall() 함수 분석
이어서 do_one_initcall() 함수의 구현부를 보겠습니다.
https://elixir.bootlin.com/linux/v5.4.30/source/init/main.c
01 int __init_or_module do_one_initcall(initcall_t fn)
02 {
03int count = preempt_count();
04char msgbuf[64];
05int ret;
06
07do_trace_initcall_start(fn);
08ret = fn();
09do_trace_initcall_finish(fn, ret);
do_one_initcall() 함수에서 가장 중요한 부분은 08번째 줄인데 module_init 키워드로 지정된 디바이스 드라이버를 초기화하는 함수를 호출합니다.
fn은 아래 디바이스 드라이버 코드에서 hello_init() 함수의 주소를 저장합니다.
static int hello_init(void)
{
printk("<1> Hello world!\n");
return 0;
}
module_init(hello_init);
do_init_module() 함수의 콜 스택 소개
이번에는 모듈 타입 디바이스 드라이버에서 module_init 키워드로 지정된 함수가 호출되는 콜 스택을 소개합니다.
아래 콜 스택을 같이 보겠습니다.
-000|NSR:0xBF03B114(asm)
-001|do_one_initcall_debug(inline)
-001|do_one_initcall(fn = 0xBF03B000)
-002|do_init_module(inline)
-002|load_module(info = 0xDE895F48, ?, ?)
-003|sys_finit_module()
-004|ret_fast_syscall(asm)
이번 포스팅에서 소개한 함수 목록들이 보입니다.
● sys_finit_module()
● load_module()
● do_one_initcall()
sys_finit_module() 함수 분석
참고로 sys_init_module() 함수의 구현부를 봅시다.
https://elixir.bootlin.com/linux/v5.4.30/source/kernel/module.c
01 SYSCALL_DEFINE3(init_module, void __user *, umod,
02 unsigned long, len, const char __user *, uargs)
03 {
04 int err;
05 struct load_info info = { };
06
07 err = may_init_module();
08 if (err)
09 return err;
10
11 pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
12 umod, len, uargs);
13
14 err = copy_module_from_user(umod, len, &info);
15 if (err)
16 return err;
17
18 return load_module(&info, uargs, 0);
19}
먼저 11~12번째 줄을 봅시다.
11 pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
12 umod, len, uargs);
디바이스 드라이버 모듈이 설치된다는 동작을 커널 로그로 출력합니다.
pr_debug() 함수의 이름을 printk() 함수로 바꿔도 똑같이 동작합니다.
14~16번째 줄을 분석하겠습니다.
14 err = copy_module_from_user(umod, len, &info);
15 if (err)
16 return err;
모듈의 정보는 load_info 구조체 타입인 load_info 변수에 저장합니다.
마지막으로 18번째 줄을 보겠습니다.
18 return load_module(&info, uargs, 0);
load_module() 함수를 호출합니다.
정리
이번에는 리눅스 시스템 전체 관점으로 '$ insmod device_driver.ko' 명령어를 실행할 때 흐름을 살펴보겠습니다.
허접하지만 다음 그림을 같이 볼까요?
유저 공간: insmod
시스템 콜 실행
-----------------------------------
커널 공간:
sys_finit_module()
load_module()
do_one_initcall()
[1] 유저 공간: insmod 명령어를 실행한다.
[2] 유저 공간: 시스템 콜을 발생한다.
[3] 커널 공간: 시스템 콜 핸들러인 sys_finit_module() 함수가 호출된다.
[4] 커널 공간: do_one_initcall() 함수에서 module_init 키워드로 지정된 디바이스 드라이버를 초기화하는 함수를 호출한다.
이 정도면 hello_init() 함수가 호출되는 원리를 잊어 먹지 않을 것 같네요.
static int hello_init(void)
{
printk("<1> Hello world!\n");
return 0;
}
module_init(hello_init);
다음 시간에는 hello_init() 함수가 호출되는 원리를 배워야 하는 이유에 대해서 이야기 하겠습니다.
뭔가를 배우려고 할 때 왜 배워야 하는지 알아야 머릿속에 더 오래 남거든요.
'Core BSP 분석 > Linux Device Driver' 카테고리의 다른 글
[리눅스] insmod 명령어로 드라이버 설치 시 커널 내부 동작 디버깅해보기(ftrace) - (v6.6, Aarch64) (0) | 2025.02.15 |
---|---|
[리눅스] 커널 모듈 타입 디바이스 드라이버 설치해보기 (v6.6, Aarch64) (0) | 2025.02.15 |
[리눅스] insmod 명령어로 드라이버 설치 시 커널 내부 동작 디버깅해보기(ftrace) - (Aarch32, v5.4) (0) | 2025.02.14 |