ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Loadable Kernel Module(LKM)
    Linux/kernel 2023. 8. 3. 02:18

    글의 참고

    - https://en.wikipedia.org/wiki/Loadable_kernel_module

    - https://tldp.org/HOWTO/Module-HOWTO/x73.html

    - https://francescoquaglia.github.io/TEACHING/AOS/AA-2020-2021/SLIDES/linux-modules.pdf

    - https://www.kernel.org/doc/Documentation/kbuild/modules.txt

    - https://cateee.net/lkddb/web-lkddb/MODULES.html


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Overview

    " 리눅스에서 `Loadable kernel module(LKM)`이 나오면, `Base kernel` 이라는 용어가 반드시 따라 나온다. `LKM`은 동작중인 커널에 동적으로 기능을 추가해주는 방법이라고 보면 된다.여기서 `Base kernel`은 LKM들이 추가되지 않은 알맹이만 있는 커널이라고 생각하면 좋다. 예를 들어, 커널에는 많은 디바이스 드라이버들이 존재한다. 근데, 세상에 모든 디바이스 드라이버들이 커널에 포함될 수 있을까? 만약, 그렇다면 현재 커널 사이즈는 1TB가 넘을 것 이다. 빌드 시점에 디바이스 드라이버를 선택해서 커널을 빌드할 수 도 있지만, 동적으로도 커널 모듈을 추가할 수 있다.

     

     

    " 왜 굳이, 동적으로 커널 모듈을 추가할까? 디바이스 드라이버를 테스트하고 싶은데, 매번 커널을 빌드해야 한다고 하면 어떨까? 테스트보다 빌드에만 걸리는 시간이 더 오래걸릴 수 있다.

     

     

    " 그렇다면, 단점은 ? 당연히 단점도 있다. 바로 `단편화` 문제가 발생할 수 있다는 것이다. 대개, `Base kernel`은 부팅 시점에 RAM에 연속적으로 로딩되게 된다. 일반적으로, `Base kernel`의 대부분은 텍스트 영역일 것이다. 당연히 데이터도 있을 수 있다. 그런데, 잘 생각해보자. 스택과 `malloc`과 같은 동적으로 데이터 영역이 할당/해제 되는 영역은 실제 커널 텍스트 영역과는 확실히 분리된 곳에 있다. 그리고, 파일에서 전역 변수를 선언하면, 해제가 가능할까? 불가능하다. 전역  변수에 `malloc` 함수와 같이 동적으로 메모리를 할당해주는 함수를 사용하지 않는 한, 전역 변수도 해제라는 개념이 없다. 즉, `Base kernel`은 해제될 가능성이 없다. 이 말은 `단편화`가 존재하지 않는다는 말과 같다.

     

     

    " LKM은 어떨까? 동적으로 커널에 로딩된다는 것은 메모리에 동적으로 로딩된다는 것이다. 즉, 특정 메모리 영역에 LKM을 할당해야 한다. 연속으로 3개의 LKM이 해제되지 않고 할당되었다고 가정해보자. 3개 모두 연속적으로 메모리를 할당받았다. 그런데 2번째 로딩되었던 LKM이 해제가 됬다. 이제 메모리에 LKM이 1번, 3번만 남은 상황이다. 그런데, 새로운 LKM을 로드하려고 한다. 그래서 이전에 해제되었던 2번째 LKM 영역에 새로운 LKM을 로드하려고 했는데, 공간이 모자라다. 즉, 이전에 해제됬었던 LKM보다 새로운 LKM의 사이즈가 더 큰것이다. 이런 경우, 3번째 LKM 뒤쪽 영역에 메모리를 할당받게 된다. 근데, `외부 단편화`가 생겨버렸다. 즉, LKM은 `외부 단편화`를 야기시킬 수 있다.

     

     

    " 참고로, 리눅스는 커널 모듈에게 안정적인 `API`나 `ABI`를 제공하지 않는다. 예를 들어, `윈도우`, `mac OS`, `FreeBSD`는 커널 모듈에게 안정적인 API 및 ABI를 제공해서 버전에 따른 호환성 문제를 최소화하고 있다. 예를 들어, 6.0 FreeBSD 커너 모듈에서 사용한 API는 재빌드하지 않고도, 6.4 FreeBSD 에서도 사용이 가능하다(7.x 과 같이 `major version`이 바뀌면 재빌드가 필요하다). 그런데, 리눅스는 6.0 에서 동작하던 코드가 6.1 에서 안될 수 도 있다. 리눅스 커널은 이러한 문제를 막기 위해 LKM을 ELF로 빌드하면서, ELF에 `.modinfo`라는 섹션을 생성한다. 그리고, 여기에 `symbol versioning data`를 넣는다. LKM이 커널에 로딩되기전에 `symbol versioning data`를 검사해서 현재 커널과 동일 버전이면 로딩하고 다르면 로딩하지 않는다.

     

     

    " 리눅스 커널은 외부 모듈을 동적으로 삽입해서 빌트인 모듈들과 통신이 가능한 아키텍처를 가지고 있다. 이게 어떻게 가능할까? `심볼 테이블` 덕분에 가능하다. 어셈블리 언어를 사용해본 사람이라면 `심볼`이라는 용어가 어떤 의미인지 알 것이다. 어셈블리언어에서는 모든 것이 `심볼`이다. 그리고, 이 심볼들은 모두 주소로 표현된다. 빌드과정에서 여러 개의 어셈블리 파일들이 하나의 실행 파일로 합쳐지게 된다. 앞에서 `여러 개의 파일이 하나의 파일로 합쳐진다` 라는 말이 핵심이다. 여기에는 아주 중요한 의미가 있다. 하나의 파일로 합치는 이유는 서로 다른 파일들끼리 서로의 심볼을 참조하고 있기 때문이다. 예를 들어, C 파일에서 어셈블리파일의 심볼을 참조할 수 있다. 당연히 반대로도 가능하다. C 파일의 어셈블리파일에 선언되어 있는 심볼의 주소를 알기 때문이다. 어떻게? 링커가 알려준다. 링커는 C 파일과 어셈블리파일을 받아서 하나의 실행 파일을 만든다. 이 때, 각 파일에 존재하는 심볼들의 주소를 재배치한다. 

     

     

    " 이 심볼 테이블(`Module.symvers`)은 커널 빌드과정에서 생성된다. 심볼 테이블에는 커널과 외부 모듈의 `exported symbol` 이 모두 기록되어 있다. 심볼 테이블의 포맷은 아래와 같다.

    <CRC>                  <Symbol>                          <Module>                            <Export Type>             <Namespace>
    0xe1cc2a05   usb_stor_suspend   drivers/usb/storage/usb-storage   EXPORT_SYMBOL_GPL   USB_STORAGE

     

     

    " 심볼 테이블의 목적은 2가지다. 모든 `외부 심볼` 기록한다. 이 심볼 테이블을 통해서 커널이 외부 모듈의 데이터를 사용하고, 외부 모듈이 커널의 데이터를 사용할 수 있다.

    1" It lists all exported symbols from vmlinux and all modules.
    2" It lists the CRC if CONFIG_MODVERSIONS is enabled.

     

     

    " 커널내의 전역 변수 중 `static` 키워드로 선언되지 않은 변수들에 한해서는 접근이 자유롭다. 그런데, 모듈은 얘기가 조금 다르다. 애네들은 동적으로 로딩된다. 그런데, 애네들의 심볼들은 이미 컴파일시점에 심볼 테이블에 삽입된다.

     Within the kernel proper, the normal linking rules apply (ie. unless a symbol is declared to be file scope with the `static` keyword, it can be used anywhere in the kernel). However, for modules, a special exported symbol table is kept which limits the entry points to the kernel proper. Modules can also export symbols.

    EXPORT_SYMBOL() : Defined in `include/linux/export.h`
    This is the classic method of exporting a symbol: dynamically loaded modules will be able to use the symbol as normal.

    - 참고 : https://www.kernel.org/doc/html/latest/kernel-hacking/hacking.html?highlight=export_symbol [ Symbols ] 

     

     

    " 예를 들어, server.c 코드에 작성된 yohda라는 전역 변수를 client.c에서 사용하고 싶다고 하자.

    // server.c ---------------------------------
    ...
    ...
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kdev_t.h>
    #include <linux/fs.h>
    ...
    ...
    
    int yohda;
    EXPORT_SYMBOL(yohda);
    
    ...
    ...
    ---------------------------------------------
    
    
    // client.c ---------------------------------
    
    ...
    ...
    #include <linux/module.h>
    #include <linux/kdev_t.h>
    ...
    ...
    
    extern int yohda;
    
    ...
    ...
    ---------------------------------------------

     

     

    " server.c 파일에서 변수를 선언하고 EXPORT_SYMBOL()로 외부로 노출한다. client.c 파일에서 extern으로 변수를 동일하게 선언하면 사용이 가능하다. EXPORT_SYMBOL()을 사용하여 커널의 심볼 테이블에 등록된 정보는 커널 외부에서 확인이 가능하다. 제대로 외부로 노출되었는지 확인하려면, `cat /proc/kallsyms | grep yohda`를 통해 확인해볼 수 있다.

     

     

    " 최신 커널에서는 `/proc/kallsyms` 파일이 모든 `exported sysbols` 들을 보여주는 심볼 테이블은 아니다. 현재는 `/lib/modules/`uname -r`/build/Module.symvers` 파일에서 모든 `exported sysbols` 을 확인할 수 있다.

    There is another kind of symbol that relates to an LKM: kallsyms symbols. These are not exported symbols; they do not show up in proc/ksyms. They refer to addresses in the kernel that are nobody's business except the module they are in, and are not meant to be referenced by anything except a debugger. Kdb, the kernel debugger that comes with the Linux kernel, is one user of kallsyms symbols.

    - 참고 : https://tldp.org/HOWTO/html_single/Module-HOWTO/ [ 10.6. Debugging Symbols ]

     

     

    - 빌드

    " 일단 외부 모듈을 빌드하기 위해서는 아래의 조건 중 하나를 만족해야 한다.

    • CONFIG_MODULES가 포함된 상태로 빌드된 커널 소스가 있어야 한다.
    • 커널 헤더가 있어야 한다. 그리고 zcat /proc/config.gz 를 통해 현재 동작하는 커널에 CONFIG_MODULES가 포함되어 있는지 혹인한다.

     

     

    " 커널 외부 모듈 빌드시에 가장 많이 볼 수 있는 make 패턴은 아래와 같다.

    KDIR=/lib/modules/$(shell uname -r)/build
    all:	
    	make -C $KDIR M=$PWD modules

     

    " `$KDIR` 변수는 커널 소스 디렉토리 경로를 참조한다. 그래서 이 값은 `/lib/modules/$(shell uname -r)/build` 가 되어야 한다. 왜냐면, 저 build/ 디렉토리는 다시 `/usr/src/linux-headers-$(shell uname -r)` 혹 `/usr/src/${KERNEL_DIRS}`를 심볼릭 링크하기 때문이다.

     

     

    " 여기서 `-C`, `M`을 options라고 하고, `modules`을 targets라고 한다. options과 targets은 make에서 정해놓은 키워드들이다. 참고로, 위의 $PWD는 커널 전역 변수로 상시 존재하는 변수이다. 그래서 Makefile에 선언이 필요없다.

     

     

    - Options

    " C = 커널 소스 디렉토리 경로 및 커널 헤더 경로를 명시해야 한다.

    " M = 빌드될 모듈이 있는 위치. 대부분은 현재 경로를 사용한다.

    pi@raspberrypi:~/workspace/pi/workspace/iic1602/LDD-IIC1602 $ ls
    bcm2835-rpi.dtsi  i2c-iic1602-overlay.dts  iic1602.c     Makefile
    charlcd.h         iic1602-bak.c            iic1602.dtbo  README.md

     

     

    - Targets

    " modules = 외부 모듈 빌드 시 default target이다. 즉, `make -C $KDIR M=$PWD`만 작성해도 기본적으로 modules target이 적용이 된다. 

    " modules_install = `make -C $KDIR M=$PWD modules` 명령어를 통해 빌드된 모듈들이 설치한다. 기본 위치는 /lib/modules/$(shell uname -r)/extra/ 폴더다. 

    " clean = 빌드를 통해 생성된 모든 파일들을 지운다. 

     

     

    - 예시

    " 아래와 같이 파일들이 존재한다고 합시다. 저는 iic1602.c 라는 파일을 모듈 빌드하고 싶습니다. 

    pi@raspberrypi:~/workspace/pi/workspace/iic1602/LDD-IIC1602 $ ls
    bcm2835-rpi.dtsi  i2c-iic1602-overlay.dts  iic1602.c     Makefile
    charlcd.h         iic1602-bak.c            iic1602.dtbo  README.md

     

     

    " 그럼 Makefile은 아래와 같이 작성될 수 있습니다.

    obj-m += iic1602.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
        // make -C /lib/modules/$(shell uname -r)/build M=$(PWD) 위와 동일한 명령어
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

     

     

     

    - 부팅 시 자동 로드

    " LKM은 커널이 부팅 하는 시점에 자동으로 로드하려면, 어떻게 해야 할까? 예전에는 `/etc/modules` 이라는 파일에 작성을 했다. 아래를 보면, 3개의 모듈이 부팅 시점에 로드된다(`w83781d`, `3c509`, `nf_nat_ftp`). 모듈에 전달하는 인자는 이름과 동일한 라인에 명시하라고 되어있다. 그런데, 지금은 이 파일을 사용하지 않는다.

     # /etc/modules: kernel modules to load at boot time.
    #
    # This file contains the names of kernel modules that
    # should be loaded at boot time, one per line. Lines
    # beginning with "#" are ignored.
    
    w83781d
    
    3c509 irq=15
    nf_nat_ftp
Designed by Tistory.