본문 바로가기

Core BSP 분석/Linux Device Driver

[리눅스] 드라이버: module_init 키워드로 지정한 함수가 호출되는 원리 - sys_finit_module()

리눅스에서 실행 중인 디바이스 드라이버는 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() 함수가 호출되는 원리를 배워야 하는 이유에 대해서 이야기 하겠습니다.
뭔가를 배우려고 할 때 왜 배워야 하는지 알아야 머릿속에 더 오래 남거든요.