0️⃣ 들어가며

드라이버의 개요 두 번째 편이다.
모듈은 드라이버를 담아 주는 개념이라고 생각하면 된다.
이번 글에서는 커널과 모듈의 빌드부터 시작해서
모듈의 틀을 만들어 보는 내용까지가 들어 있다.


1️⃣ 학습 내용

1.7 커널 빌드 및 설정 프로세스

✅ 빌드 프로세스의 비교

  • Application / Kernel 빌드 과정의 비교

    단계Application BuildKernel Build
    FetchFTP, GitHub 등에서 소스 다운로드linux-6.1.21.tar.gz 다운로드 (kernel.org)
    Unpacktar -xvf ... 압축 해제tar -xvf ... 압축 해제
    Patch필요 시 패치 파일 적용벤더(Vendor) 패치 적용 등
    Configure./configure (환경 자동 감지)make menuconfig (수동/상세 설정 필수), 타겟 보드에 맞는 드라이버/옵션 선택
    Compilemake (오브젝트 생성)make (커널 이미지 및 모듈 생성)
    Link라이브러리 링크vmlinux 생성 (Static Linking)
    Installmake install (/usr/bin 등 복사)modules_install, headers_install, 이미지 복사

✅ 커널 설정(Kernel Configuration)

  • 커널 설정의 개요

    커널 빌드 전 어떤 기능을 포함할지 결정하는 단계

  • 필수 패키지 설치

    root@host:~/linux-6.1.21# apt install libncurses-dev flex bison
  • 설정 도구 실행

    # make menuconfig 명령어를 사용

    메뉴 파일(Kconfig)를 불러들여 메뉴를 표시하고, 터미널 기반의 GUI 메뉴로 옵션 선택

    root@host:~/linux-6.1.21# export KERNEL=kernel8
    root@host:~/linux-6.1.21# export ARCH=arm64
    root@host:~/linux-6.1.21# make menuconfig
    • 참고 이미지

  • 옵션 선택

    <*> (Built-in) : 커널 이미지(zImage)에 영구적으로 포함

    <M> (Module) : 별도의 모듈 파일(.ko)로 빌드되어 필요 시 로딩

    < > (Excluded) : 빌드에서 제외

  • 설정 파일 관리 (.config)

    menuconfig 에서 설정한 내용은 .config 파일에 저장됨

    설정 파일 재활용을 위해 미리 만들어둔 설정 파일인 _defconfig 를 이용 (위치 : arch/arm64/configs/ )

    .config를 저장하거나 불러와 이전에 지정했던 상태로 세팅 가능

    root@host:~/linux-6.1.21# export ARCH=arm
    root@host:~/linux-6.1.21# make bcm2835_defconfig
      HOSTCC  scripts/kconfig/conf.o
      HOSTLD  scripts/kconfig/conf
    #
    # configuration written to .config
    #
    # 이 명령은 구형 설정이므로 RPi 4 64bit 환경에는 부적합

✅ Kbuild System과 Makefile 동작 원리

  • KConfig (Kernel Configuration)

    커널의 기능과 드라이버 옵션을 정의하는 설정 명세 파일

    # make menuconfig 를 실행했을 때 보여지는 메뉴 구조와 옵션의 타입, 의존성 등을 결정

    사용자가 선택한 값은 .config 파일에 CONFIG_XXX=y 또는 m 형태로 저장

  • Kbuild (Kernel Build System)

    리눅스 커널을 빌드하기 위한 규칙과 스크립트 체계

    .config 에 저장된 설정값(CONFIG_XXX)을 참조하여 Makefile 을 제어함

    • 핵심 규칙

      obj-y : 커널 이미지(zImage)에 포함, Built-in

      obj-m : 모듈 파일(.ko)로 생성

      obj- (공백) : 빌드에서 제외

  • 동작 예시 : DHT11 드라이버

    1. .config 파일 확인

      CONFIG_DHT11=m (모듈로 설정됨)

    2. Makefile 확인:

      makefileobj-$(CONFIG_DHT11) += dht11.o
      # 위 변수가 'm'으로 치환되어 -> obj-m += dht11.o 가 됨
    3. 결과

      obj-m 으로 지정되어 있으므로 dht11.ko 파일 생성

✅ 커널 빌드와 결과물

  • 커널 빌드하기 명령어 # make 또는 # make -jN 를 실행하면 여러 파일들이 생성됨
    1. 커널 이미지

      vmlinux , zImage 또는 uImage 또는 bzImage 를 생성

    2. 커널 모듈

      .ko : 각 드라이버의 컴파일된 모듈 파일들

    3. 디바이스 트리 (Device Tree)

      보드(MCU, RAM, ROM, Devices를 포함한 SoC)의 하드웨어 스펙을 커널에 알려주는 데이터 구조

      • 소스 파일

        .dts : Source, 사람이 읽을 수 있는 소스 파일로 C언어의 .c 와 유사

        .dtsi : Include, 공통 내용을 담은 헤더로 C언의 .h 와 유사

      • 컴파일러

        dtc : Device Tree Compiler 사용

      • 실행 파일

        .dtb : Blob, 컴파일된 바이너리로 커널이 읽는 파일

        .dtbo : Overlay, 런타임에 동적으로 적용하는 오버레이 바이너리

  • 빌드 명령어
    • # make -jN

      멀티코어 상에서 빠른 빌드에 사용되는 명령어

      (예 : 코어가 4개일 때 # make -j4 )

    • # make zImage / # make uImage

      압축된 커널 이미지 혹은 U-Boot용 이미지를 생성할 때 사용하는 명령어

1.8 모듈 빌드와 실행

✅ 모듈 빌드 설정

  • 모듈 빌드 과정

    커널 소스/헤더 준비 → 빌드 환경 설정 → 빌드 → 설치

  • 커널 소스/헤더 준비 (Build Environment)

    1. Host 개발 (Cross Compile)

      PC에서 빌드하여 타겟 보드로 옮기는 방식으로, 전체 커널 소스 필요 (예 : /work/linux )

    2. Native 개발 (On Board)

      라즈베리파이 등 보드에서 직접 빌드하는 방식

      /lib/modules/$(uname -r)/build : 모듈 빌드에 필요한 최소한의 헤더가 링크된 경로

  • 모듈 빌드 환경 설정 (modules_prepare)

    호스트 개발 시 커널 소스를 다운로드 후 환경 설정이 필요함

    모듈을 빌드하기 전에 기본적인 구성과 버전 체크 등을 수행

    명령어 : # make modules_prepare

  • 모듈 빌드하기 (# make modules)

    KDIR 에 있는 커널의 Makefile 규칙을 빌려와서 M 디렉토리에 있는 소스 컴파일

    # -C: 커널 소스 경로로 이동
    # M=: 현재 모듈 소스 경로를 알려줌 (구식 SUBDIRS 대신 사용)
    root@host:~/linux-6.1.21# make -C $(KDIR) M=$(PWD) modules
  • 모듈 설치하기 (# make modules_install)

    설치된 모듈이 필요한 시점에 커널이 모듈을 찾아 로드할 수 있음

    생성한 .ko 모듈 파일을 시스템 표준 경로(/lib/modules/커널버전/ )에 복사

    modules.dep 파일을 갱신하여 modprobe 가 의존성을 파악할 수 있게 함

    INSTALL_MOD_PATH : Cross Compile 시에는 모듈 설치 위치를 지정해 주어야 함

✅ 모듈 로드와 로그 관리

  • modprobe : 스마트한 로더

    /lib/modules/ 경로를 참고하여 모듈과 의존성까지 자동으로 로드 및 제거

  • 전통적인 명령어 : 수동 제어

    # insmod file.ko : 현재 디렉토리의 파일을 커널에 로드, 의존성 해결은 하지 않음

    # lsmod : 현재 커널에 로드된 모듈 목록 확인

    # rmmod <name> : 모듈 언로드(제거)

  • 커널 로깅(Logging)

    printk() 함수를 사용

    User Space의 printf 에 대응되는 커널 함수

    커널 메시지 버퍼에 로그를 기록함

  • 로그 보는 방법

    /var/log/messages : 로그가 저장되어 있는 파일

    • 실시간 로그 확인

      dmesg : 커널 부팅부터 현재까지의 로그 출력

      dmesg -c : 로그를 출력한 뒤 버퍼를 비워 정리, 테스트 반복 시 유용

✅ 모듈과 디바이스 드라이버의 차이

  • 커널 모듈(Kernel Module)

    리눅스 커널에 동적으로 적재되거나 제거할 수 있는 코드 덩어리

    .ko 파일로 만들어져 있으며, 디바이스 드라이버뿐만 아니라 커널의 기능을 확장하는 모든 코드

  • 디바이스 드라이버(Device Driver)

    특정 하드웨어를 제어하기 위해 만들어진 소프트웨어

    하드웨어의 복잡한 동작을 감추고 커널이나 유저에게 표준화된 인터페이스를 제공

  • 모듈과 드라이버의 관계

    1. 모듈로 만든 드라이버

      필요할 때 insmod 로 커널에 적재할 수 있는 드라이버

      개발과 배포가 유연한 편으로, 대부분의 드라이버가 이 방식을 사용함

    2. 모듈이 아닌 드라이버(Built-in)

      make zImage 할 때 커널에 포함되어 나오는 드라이버

      시스템 타이머 등 부팅에 필수적인 장치는 모듈로 만들 수 없음

    3. 드라이버가 아닌 모듈

      방화벽, 파일시스템 등 하드웨어 제어와 상관없는 순수 소프트웨어 기능

✅ 모듈 빌드 예제

  • Makefile

    # Makefile
    
    ifdef ARCH       # Cross 빌드
            KDIR=/work/linux
    else             # Native 빌드
            KDIR=/lib/modules/$(shell uname -r)/build
    endif
    
    all: dev 
    
    # 커널 소스 디렉토리로 이동해서 Makefile을 실행, 빌드할 소스는 PWD에 있음
    dev:
            $(MAKE) -C $(KDIR) M=$(PWD) modules
    
    clean:
            @$(MAKE) -C $(KDIR) M=$(PWD) clean
  • 모듈 빌드 예제 : work_drivers/exercise/03/01

1.9 모듈 만들기

✅ 모듈의 기본 구조와 헤더

  • 모듈 담당 헤더

    <linux/kernel.h> : printk 등 커널의 기본 함수 포함

    <linux/module.h> : 모듈 관련 매크로와 struct module 이 정의된 파일

    • struct module

      커널 내에서 모듈을 관리하기 위한 핵심 구조체

      모듈의 상태, 모듈 리스트, 중복되지 않는 모듈명, 외부 참조 가능한 심볼 변수 테이블, 모듈 초기화/해제 함수 포인터, 모듈 코드와 데이터가 할당된 메모리 주소 등을 포함

  • 모듈의 기본 형태

    • 초기화 함수/해제 함수

      모듈은 항상 초기화와 종료 함수 쌍으로 구성되며, 커널 매크로를 통해 등록됨

      각각 모듈 로드 시와 모듈 제거 시에 실행됨

      초기화 함수 예시 : static int my_init(void);

      초기화 해제 함수 예시 : static int my_exit(void);

    • 등록 매크로 <linux/init.h>

      module_init(my_init) : 커널에게 초기화 함수를 알림

      module_exit(my_exit) : 커널에게 종료 함수를 알림

    • 내부 호출 흐름

      module_init -> __initcall(fn) -> device_initcall(fn)
      module_exit -> __exitcall(fn)
    • Helper 매크로 (커널 3.x 이후)

      module_platform_driver(my_driver) : 플랫폼 드라이버용

      module_i2c_driver(...)module_spi_driver(...)

  • 모듈 라이선스(Module License)

    MODULE_LICENSE() : 모듈에 적용되는 라이선스 정책을 커널에 명시하는 매크로

    라이선스는 무조건 필요하고, 없으면 경고 메시지 발생 및 기능에 제약

    예 : “GPL” 라이센스 명시하려면 MODULE_LICENSE(”GPL”)

✅ 모듈 빌드

  • 인트리 빌드 (In-Tree Build)

    커널 소스 트리 내부(drivers/ 등)에 모듈 소스를 두고 커널과 함께 빌드하는 방식

    1. 커널 소스 준비

      빌드할 전체 커널 소스 필요

    2. 소스 위치

      drivers/char/ 등 적절한 디렉토리에 드라이버 소스 파일(.c) 넣기

    3. Makefile 수정

      해당 디렉토리의 Makefile에 빌드 객체 추가하기

      obj-m += xxx.o : 무조건 모듈로 빌드

      obj-$(CONFIG_XXX_POLE) += xxx.o : Kconfig 설정 옵션에 맞춰서 빌드

    4. Make

      커널 최상위에서 # make 를 실행해 빌드하기

      CONFIG_yzImage에 포함되고, m이면 xxx.ko 파일이 생성됨

  • 아웃오브트리 빌드 (Out-of-Tree)

    커널 소스 트리가 아닌 외부(사용자 홈 디렉토리 등)에서 독립적으로 모듈을 빌드하는 방식

    1. 커널 소스 준비

      빌드된 커널 헤더나 소스 필요

    2. 소스 위치

      임의의 작업 디렉토리에 드라이버 소스 파일 넣기

    3. Makefile 작성

      obj-m := xxx.o                # 단일 소스일 때
      xxx-objs := xxx.o yyy.o   # 여러 소스 파일로 하나의 모듈(xxx.ko)을 만들 때
    4. Make

      # make -C <KernelLocation> M=$(PWD) modules

      해당 디렉토리에서 make 를 통해 .ko 파일 생성

      -C <KernelLocation> : 커널 소스 트리가 있는 위치 지정

      M=$(PWD) : 드라이버 소스가 있는 곳을 현재 작업 디렉토리로 지정, SUBDIRS=$(PWD) 와 같음

  • 모듈 설치

    # make modules_install : 빌드 후 명령을 통해 모듈 설치

    생성된 .ko 파일들을 `/lib/modules/$(uname -r)/ 아래의 적절한 경로로 복사

    모듈 정의 파일인 modules.dep 을 생성/갱신하여 모듈 리스트에 등록

✅ 모듈 출력

  • 모듈의 메시지 출력 : printk

    커널 모듈은 터미널(stdout)에 직접 출력이 불가능하므로 커널 메시지 버퍼에 기록

    <linux/kernel.h>에 정의되어 있으며, 정확한 위치는 /kernel/printk/printk.c

    printf()와 사용법이 유사하지만 실수 출력을 위한 부동 소수점 포맷 %f, %e 를 지원하지 않음

    /var/log/messages 등의 로그 파일에 내용 저장

  • 출력된 로그 확인하기

    # dmesg : Display Message, 실시간/전체 로그 확인 명령어

    # cat /var/log/messages : 파일을 통해 확인, root 권한 필요

  • 로그 레벨(Log Level)

    로그 레벨은 메시지의 중요도를 나타내는 레벨

    숫자가 작을수록 우선 순위 높

    이 레벨에 따라 printk 는 콘솔에 즉시 내용을 출력할지, 버퍼에만 기록할지 결정

    printk의 로그 레벨보다 콘솔의 로그 레벨이 높아야 메시지 확인 가능

    • 로그 레벨 지정 방법

      문자열 앞에 매크로 상수를 붙여 사용

      예 : printk(KERN_WARNING "Warning message\n");

  • 로그 레벨 우선순위

    매크로 상수숫자의미
    KERN_EMERG<0>시스템이 동작 불가능한 상태
    KERN_ALERT<1>즉각적인 조치가 필요한 상태
    KERN_CRIT<2>치명적인(Critical) 오류
    KERN_ERR<3>일반적인 오류 상태
    KERN_WARNING<4>경고 메시지
    KERN_NOTICE<5>정상이지만 중요한 알림
    KERN_INFO<6>일반 정보 메시지
    KERN_DEBUG<7>디버깅용 메시지
  • 콘솔 출력 제어 (/proc/sys/kernel/printk)

    커널은 현재 설정된 콘솔 로그 레벨보다 중요한 메시지만 콘솔 화면에 출력

    cat /proc/sys/kernel/printk
    # 출력 예: 7       4       1       7
    #         (1)     (2)     (3)     (4)
    1. Console Log level

      현재 콘솔 출력 커트라인, 이 값보다 높은 우선순위 메시지(값이 작은 것)만 출력

    2. Default Message Log Level

      printk 에 별도의 로그 레벨을 설정하지 않을 경우 적용되는 기본값

    3. Minimum Console Log Level

      Console Log Level을 이 값 이하로 내릴 수 없음

    4. Default Console Log Level

      Console Log Level의 부팅 시 기본값

    • 콘솔 로그 레벨 변경
      # 레벨을 8로 높여서 모든(0~7) 로그가 보이게 함
      # ssh 터미널(pts)는 콘솔이 아니므로 이 설정과 무관하게 dmesg로 확인해야 함
      # 직접 보려면 라즈베리파이에 모니터 연결 필요
      echo 8 > /proc/sys/kernel/printk
  • 모듈 출력 예제 /work_drivers/exercise/03/02

1.10 모듈 매크로와 매개변수

로깅을 위한 pr_ 매크로

  • pr_ 매크로

    커널 출력을 할 때 printk 에 로그 레벨을 매번 입력하는 번거로움이 존재함

    이런 불편함을 줄이기 위해 Wrapper로 만든 pr_ 매크로 시리즈를 제공

    pr_info("Module loaded successfully\n"); 형태로 사용

  • pr_ 매크로의 장점

    로그 레벨이 매크로 이름에 포함되어 있어 KERN_INFO 등의 상수를 쓰는 것보다 직관적, 가독성 향상

    로그 레벨을 수동으로 입력할 필요가 없어 오류 감소

    현재 리눅스 커널 개발에서 권장되는 표준 로깅 방식

  • pr_ 매크로 종류

    매크로 함수대응되는 printk 레벨용도 및 의미
    pr_emerg()KERN_EMERG시스템이 붕괴 직전인 긴급 상황
    pr_alert()KERN_ALERT즉각적인 조치 필요
    pr_crit()KERN_CRIT치명적인(Critical) 하드웨어/소프트웨어 오류
    pr_err()KERN_ERR드라이버 동작 실패 등 복구 불가능한 에러
    pr_warn()KERN_WARNING잠재적 문제 경고 (동작은 가능)
    pr_notice()KERN_NOTICE정상적이지만 중요한 알림
    pr_info()KERN_INFO일반적인 정보 및 상태 메시지
    pr_debug()KERN_DEBUG개발용 디버깅 로그 (기본적으로 출력 안 됨)

✅ 모듈 등록과 해제

  • 모듈 등록/해제 매크로

    • module_init(func)

      모듈 로드(insmod) 시 실행될 초기화 함수 등록

    • module_exit(func)

      모듈 제거(rmmod) 시 실행될 종료 함수 등록

    • module_platform_driver(driver)

      플랫폼 드라이버를 개발할 때, init / exit 함수 등록 과정을 한 줄로 축약해 주는 편리한 매크로

  • 모듈 라이선스 매크로와 모듈 정보

    modinfo <filename.ko> : 모듈의 정보를 볼 수 있는 명령어

    MODULE_LICENSE("License") : 모듈에 적용되는 라이센스 명기

    MODULE_AUTHOR("Name") : 모듈 제작자의 이름(정보) 명기

    MODULT_DESCRIPTION("Desc") : 모듈 설명을 명기

  • 모듈 라이선스 작성 예제 03/03

✅ 모듈 매개 변수 (Module Parameters)

  • 모듈에 매개변수 전달하기

    모듈을 로드할 때 상황에 맞는 매개변수를 전달받아 드라이버의 동작을 동적으로 변경하는 기능

    하드웨어 설정값(NIC의 I/O 주소, IRQ값)을 하드코딩하지 않고 유연하게 설정할 수 있음

  • 매개변수 전달 방법

    모듈 로드 시 명령어 뒤에 변수명=값 형태로 전달하고, 배열은 쉼표로 구분하여 지정

    # 단일 값 전달
    insmod my_module.ko myint=100 mystring="Hello"
    
    # 배열 값 전달
    insmod my_module.ko myarray=1,2,3
  • 매개변수 전달 코드 구현

    매개변수 전달을 위해서는 <linux/moduleparam.h>가 필요함

    먼저 전역 변수를 선언하고 module_param 매크로로 연결

    module_param(name, type, perm) : 기본 매크로

    module_param_array(name, type, &count_var, perm) : 배열 매크로

    • module_param 전달 항목

      name : 매개변수로 사용할 변수 이름

      type : 변수의 데이터 타입

      &count_var : 실제로 입력된 개수를 저장할 변수의 주소 (없으면 NULL)

      perm : sysfs 접근 권한, 0644등의 일반적인 8진수 형식 또는 S_ifoo 정의를 OR 연산

      (예 : S_IRUGO | S_IWUSR (0444 | 0644) : 모든 사용자는 읽기, 사용자는 쓰기 가능)

    • 지원 데이터 타입

      타입 이름C 언어 타입설명
      byte, short, ushortchar, short, unsigned short작은 정수형
      int, uintint, unsigned int일반 정수형
      long, ulonglong, unsigned long큰 정수형
      charpchar *문자열 포인터 (String)
      bool, invboolbool불리언 (invbool은 값 반전)
      intarrayint *정수 배열
  • 매개변수 전달 예제 03/04

✅ 커널 심볼

  • 커널 심볼 (Kernel Symbols)

    커널 내부에서 다른 모듈이 접근할 수 있도록 공개된 함수나 변수

    여러 드라이버에서 공통된 기능을 라이브러리처럼 재사용하거나 다른 커널 모듈이 특정 모듈의 함수나 변수를 써야할 때 유용

    전역 심볼 테이블(/proc/kallsyms) 파일에서 현재 커널에 등록된 모든 심볼 확인 가능

  • 심볼 네임스페이스 (Symbol Namespace)

    커널 심볼이 많아지면서 발생하는 이름 충돌 문제를 해결

    모듈 간 종속성을 명확하게 구분하여 관리하기 위한 메커니즘

  • 커널 심볼 내보내기

    • EXPORT_SYMBOL(symbol)

      심볼을 리눅스 커널의 전역 심볼 테이블에 등록하여 모든 모듈에서 사용 가능하게 함

      라이선스 제약 없이 접근 가능

    • EXPORT_SYMBOL_GPL(symbol)

      GPL 라이선스 모듈만 접근을 허용하는 매크로

      커널 내부 핵심 함수들은 대부분 이 매크로로 보호 처리됨

    • EXPORT_SYMBOL_NS(symbol, namespace)

      심볼을 특정 네임스페이스로 내보냄

      사용하는 쪽에서 MODULE_IMPORT_NS(namespace) 를 명시해야 접근 가능


2️⃣ 느낀 점

지쳤나요? 네.
드라이버의 세계는 정말 넓고 깊고 어렵구나
아직까지는 모듈의 틀만 만드는 수준이지만
다음 글부터는 좀 더 본격적인 내용이 나오게 될 것...!

0개의 댓글