Embedded System/Linux Kernel & Device Drivers

임베디드 리눅스 커널 모듈(LKM) 작성 및 동적 로딩 가이드

임베디드 친구 2025. 3. 16. 10:24
반응형

임베디드 리눅스 기반의 시스템을 개발할 때 기능 추가나 하드웨어 드라이버 변경이 필요할 때마다 커널 전체를 다시 빌드하고 시스템을 재부팅하는 것은 개발 생산성을 크게 떨어뜨리는 요인입니다. 리눅스 커널은 이러한 비효율성을 해결하기 위해 실행 중인 커널에 동적으로 코드를 삽입하거나 제거할 수 있는 커널 모듈(Loadable Kernel Module, LKM) 메커니즘을 제공합니다. 시스템 운영 연속성을 보장하면서 커널 기능을 유연하게 확장할 수 있는 커널 모듈의 개념과 실제 작성, 크로스 컴파일, 타깃 보드 로딩 테스트까지의 전 과정을 정리했습니다.

Generated by Gemini AI.

핵심 요약 3줄

  • 커널 모듈은 시스템 재부팅 없이 구동 중인 리눅스 커널에 동적으로 로드 및 언로드되어 기능을 확장하는 독립 프로그램입니다.
  • 전용 매크로(__init, __exit)와 모듈 정보 라이선스를 포함한 C 소스코드를 작성하고, 커널 소스 경로가 지정된 Makefile을 통해 빌드합니다.
  • 크로스 컴파일로 생성된 .ko 바이너리를 타깃 보드로 이전한 뒤 insmod와 rmmod 명령어로 제어하며 dmesg를 통해 커널 로그를 검증합니다.

1. 리눅스 커널 모듈(LKM)의 개념과 동적 로딩 메커니즘

커널 모듈은 커널 공간(Kernel Space)에서 실행되는 파일입니다. 범용적인 사용자 애플리케이션과 달리 main() 함수가 존재하지 않으며, 커널에 진입할 때와 빠져나갈 때 호출되는 전용 콜백 인터페이스를 가집니다. 일반 애플리케이션과 커널 모듈의 구조적 차이점은 다음과 같습니다.

비교 항목 사용자 공간 애플리케이션 (User Space App) 커널 모듈 (Kernel Module, LKM)
실행 권한 레벨 비특권 모드 (User Mode, CPU Ring 3) 특권 모드 (Kernel Mode, CPU Ring 0)
진입 및 종료점 main() 함수에서 시작하고 종료 module_init() 및 module_exit() 매크로 매핑
메모리 접근 범위 가상 메모리 분리로 할당된 사용자 영역만 접근 커널 주소 공간 및 실제 하드웨어 레지스터 직접 제어
출력 표준 함수 printf()를 통한 표준 출력 (stdout) printk()를 통한 커널 링 버퍼(dmesg) 출력
파일 확장자 확장자가 없거나 ELF 실행 파일 형태 커널 오브젝트 형태의 .ko 확장자

2. 실전 Hello World 커널 모듈 소스코드 작성

동적 로딩 흐름을 파악하기 위해 커널 진입 시점과 해제 시점에 디버깅 로그를 남기는 가장 기본적인 모듈 코드를 작성합니다.

1. hello_world.c 소스코드

C
 
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

// 모듈 메타데이터 정의 (GPL 라이선스를 명시하지 않으면 커널 오염 경고가 발생함)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("소프트웨어 공장");
MODULE_DESCRIPTION("A simple Hello World Kernel Module");
MODULE_VERSION("1.0");

// 모듈 로드 시 호출되는 초기화 함수
static int __init hello_init(void) {
    printk(KERN_INFO "Hello, World!\n");
    return 0; // 0은 성공적인 로드를 의미하며, 음수 리턴 시 로드가 거부됨
}

// 모듈 제거 시 호출되는 종료 함수
static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye, World!\n");
}

// 커널 시스템에 콜백 함수 등록
module_init(hello_init);
module_exit(hello_exit);

3. 크로스 컴파일을 위한 빌드 환경 구축

타깃 보드의 프로세서 아키텍처에 맞는 모듈을 생성하려면 호스트 PC에 툴체인을 배치하고 커널 트리 환경을 동기화해야 합니다.

1. 호스트 패키지 및 커널 소스 준비

64비트 ARM 아키텍처(ARM64) 크로스 컴파일러를 호스트에 빌드하고, 보드 제조사가 배포한 커널 소스를 가져와 모듈 컴파일 준비 단계(modules_prepare)를 실행합니다.

Plaintext
 
# 크로스 컴파일러 툴체인 및 필수 빌드 도구 구성
sudo apt update
sudo apt install -y build-essential gcc-aarch64-linux-gnu make libncurses-dev bison flex libssl-dev

# 커널 저장소 동기화 및 타깃 빌드 트리 최적화
git clone https://github.com/linux-development/kernel.git
cd kernel
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- generic_board_defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules_prepare

2. 전용 Makefile 구성

커널 모듈 빌드는 일반적인 컴파일과 다릅니다. 타깃 보드용으로 컴파일된 커널 빌드 시스템 소스 트리 경로를 참조하여 컴파일러 아키텍처 매핑을 수행해야 합니다.

Makefile
 
obj-m += hello_world.o

# 실제 빌드 환경의 커널 소스 절대 경로를 지정해야 함
KDIR := /home/user/development/kernel

all:
	make -C $(KDIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules

clean:
	make -C $(KDIR) M=$(PWD) clean

Makefile 작성이 완료되면 셸 콘솔에서 단순히 make 명령을 입력하는 것으로 컴파일이 수행되며, 최종 빌드 결과물로 hello_world.ko 바이너리가 추출됩니다.

4. 타깃 시스템 배포 및 동적 제어 테스트

컴파일된 커널 오브젝트(.ko) 파일을 타깃 장비로 이전한 후 커널 제어 명령어를 통해 정상 동작 여부를 검증합니다.

런타임 커널 모듈 제어 명령어 일람

제어 명령어 주요 기능 실무 명령어 사용 예시
insmod 커널 오브젝트(.ko) 파일을 커널 공간에 직접 동적 로드 sudo insmod hello_world.ko
rmmod 가동 중인 커널 모듈을 안전하게 메모리 공간에서 해제 sudo rmmod hello_world
lsmod 현재 커널에 로드되어 있는 전체 모듈 리스트 및 메모리 점유율 출력 lsmod | grep hello
modinfo 소스코드 내부에 선언한 라이선스, 작성자 등의 메타데이터 확인 modinfo hello_world.ko

테스트 가이드라인 및 출력 로그 결과

Plaintext
 
# 1. 호스트 PC에서 네트워크를 통해 타깃 보드로 바이너리 전송
scp hello_world.ko user@target_board:/home/user

# 2. 타깃 보드 터미널 접속 후 모듈 커널 내부 탑재
ssh user@target_board
sudo insmod hello_world.ko

# 3. 커널 링 버퍼 추적을 통한 초기화 메시지 확인
dmesg | tail -n 3
[ 1234.567890] Hello, World!

# 4. 메모리에 상주 중인 모듈 언로드 처리
sudo rmmod hello_world

# 5. 커널 링 버퍼 추적을 통한 종료 메시지 확인
dmesg | tail -n 3
[ 1235.678901] Goodbye, World!

개발을 위한 팁

  • printk 출력 레벨 제어: 모듈 내부에서 로그를 찍을 때 KERN_INFO 외에 오류 상황을 알리는 KERN_ERR, 경고를 표시하는 KERN_WARNING 등 적절한 로그 레벨 매크로를 지정해야 시스템 로그 파일(/var/log/syslog)에서 가시성을 확보하기 좋습니다.
  • 실시간 로그 추적 활용: 모듈을 로드하거나 언로드할 때 매번 dmesg 명령을 치는 것은 번거롭습니다. 다른 터미널 창을 하나 더 열어 dmesg -w 또는 tail -f /var/log/messages 명령을 실행해 두면 커널 메시지가 찍히는 순간을 실시간으로 실감 나게 확인할 수 있습니다.
  • modprobe 도입 검토: 실무 아키텍처에서는 드라이버 간의 상호 의존성이 얽혀 있습니다. insmod는 의존성 해결을 못 하므로, 모듈들을 타깃 시스템의 /lib/modules/$(uname -r)/ 경로에 밀어 넣고 depmod 명령을 수행한 뒤 modprobe 명령을 이용해 로드하면 연관된 의존 모듈까지 한 번에 알아서 끌어와 로드해 줍니다.

흔히 하는 실수

  • 커널 소스 버전 불일치 (Invalid module format): 모듈 컴파일 시 참조한 호스트 PC의 커널 소스 버전과 실제 타깃 보드에 올라가 돌아가고 있는 커널 버전 문자열이 미세하게라도 다르면, insmod 실행 시 exec format error 플래그와 함께 탑재가 거부됩니다. 항상 타깃 보드의 uname -r 출력값과 소스 버전의 정렬 상태를 체크해야 합니다.
  • __init 및 __exit 매크로 오용: 초기화 함수에 붙는 __init 매크로는 부팅 또는 로딩 직후 해당 함수 코드가 점유했던 메모리 영역을 커널이 다시 회수하도록 지시합니다. 따라서 초기화 함수 내부의 코드를 런타임 중에 다른 함수가 다시 호출하도록 아키텍처를 짜면 커널 참조 크래시가 터지게 됩니다.
  • Makefile 내 탭(Tab) 문자를 공백으로 처리: Makefile 스크립트 작성 시 all: 이나 clean: 하위의 실행 명령 구문 시작점을 키보드의 스페이스바로 공백 처리를 하면 문법 에러(missing separator)를 뿜으며 빌드가 정지됩니다. 빌드 규칙 하위 구문은 무조건 탭 키를 눌러 들여쓰기를 해야 합니다.

결론

동적 커널 모듈 기법은 방대하고 복잡한 리눅스 커널 소스 전체를 매번 빌드할 필요 없이 개발자가 타깃으로 삼은 드라이버 레이어만 가볍게 조각내어 검증할 수 있도록 돕는 고마운 기술입니다. 초기화 콜백 매크로 규칙을 준수하고 툴체인 버전 정렬만 어긋나지 않게 관리한다면 하드웨어 드라이버 디버깅과 커스텀 기능 확장을 매우 효율적이고 유연하게 수행할 수 있는 개발 환경을 구축할 수 있습니다.

반응형