System Programming/Linux Device Driver

Linux Device Driver 기초 #3 Linux Device Driver 추가하기

uzguns 2024. 10. 13. 22:15

Kernel Module 만들기

리눅스 커널 모듈은 커널 코드를 보다 쉽게 로드할 수 있게 함으로써 리눅스 커널 개발에 유용한 인터페이스를 제공하고,

동적 로드와 특정한 기능만 선택적으로 로드할 수 있어 리눅스 커널이 차지하는 메모리의 양을 줄일 수 있는 효과가 있다.

  • 동적 로드:
    • 리눅스 커널 모듈은 시스템이 부팅될 때 커널 이미지에 포함되지 않고 필요 시점에만 메모리에 로드된다.
    • 예를 들어 특정 하드웨어 장치가 연결될 때만 해당 장치를 지원하는 모듈을 메모리에 로드하고 사용이 끝나면 unload하여 메모리에 제거할 수 있다.
  • 선택적 기능:
    • 모든 커널 기능을 한꺼번에 커널 이미지에 포함시키는 대신 커널 모듈을 통해 특정 기능만 선택적으로 사용할 수 있어 부팅 시간과 메모리를 절약할 수 있다.
    • 예를 들어 특정 네트워크 드라이버나 파일 시스템 자원이 필요할 때만 해당 모듈을 로드하면 된다. 
    • 소스 코드를 별도로 구성해서 빌드할 수 있다. (단, 동일한 버전의 커널 소스 코드는 여전히 필요)

 

Loadable Kernel Module

kernel의 이미지에는 포함되지 않으면서도 커널의 기능을 확장할 수 있는 바이너리로 커널을 재부팅하지 않아도 커널 기능을 확장할 수 있다. 커널 주소 공간에서 실행 되므로 루트 권한이 있어야 커널 모듈 로드가 가능하다. 

 

커널 모듈을 로드하기 위해선 아래 절차를 따른다.

 

1. 커널 모듈 작성

커널 모듈의 소스코드는 일반적으로 아래와 같은 구조를 가지고 있다. 

#include <linux/module.h>

static int __init sungch_module_init(void)
{
    printk(KERN_DEBUG "%s\n", __func__);
    return ret;
}

static void __exit sungch_module_exit(void)
{
    printk(KERN_DEBUG "%s\n", __func__);
}

module_init(sungch_module_init);
module_exit(sungch_module_exit);

MODULE_AUTHOR("jc3wrld999@gmail.com");
MODULE_DESCRIPTION("sungch driver");
MODULE_LICENSE("GPL v2");

 

2. 새로운 커널 설정 추가

menu와 endmenu 사이에 여러가지 config를 넣을 수 있다. 

menu "<메뉴 제목>"
    
    config <설정 이름>
        tristate "<간략한 설명>"
            # Y: 커널에 포함
            # N: 커널에서 제외
            # M: 모듈로 빌드
        default <기본값>
        depends on <설정 이름>
        help
            <상세한 설명>

    config <다른 설정 이름>
        bool "<간략한 설명>"
            # Y: 커널에 포함
            # N: 커널에서 제외
        default <기본값>
        depends on <다른 설정 이름>
        help
            <상세한 설명>
            
endmenu

 

아래 예시와 같이 작성하면 된다. menuconfig를 통해 새로 추가한 설정을 확인하고 활성화 가능하다. 

menu "Sungch Example Driver"

config SUNGCH_EXAMPLE
    tristate "Sungch Example Driver"
    help
        This is an example driver using sungch class.

endmenu

 

3.  작성한 Kconfig 파일을 main Kconfig 에 연결한다. 

source "drivers/sungch/Kconfig"

 

4. Makefile Kernel Module 추가

obj-$(CONFIG_SUNGCH_EXAMPLE) += sungch.o
sungch-objs += main.o

main Makefile에도 추가

obj-y 					+= sungch/

 

make menuconfig 하면 추가한 device driver를 확인할 수 있다.

~/linux# ARCH=arm64 make menuconfig

 

M을 눌러 로드하고 저장하고 나온다.

 

5. 커널 모듈 빌드

ARCH=arm64 CROSS_COMPILE=<툴체인 절대경로>/bin/aarch64-none-linux-gnu- make -j<코어 개수>
예시
ARCH=arm64 CROSS_COMPILE=/backup/linux_device_driver/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu- make

 

이제 커널을 빌드하면 추가한 코드가 커널 모듈 바이너리로 빌드가 된다. 전체 커널이 빌드되는 것이 아니라 추가한 모듈과 관련된 코드만 빌드가 된다. 

  CC [M]  drivers/sungch/main.o
  LD [M]  drivers/sungch/sungch.o
  ...
  CC [M]  drivers/sungch/sungch.mod.o
  LD [M]  drivers/sungch/sungch.ko

빌드로그를 확인해보면 먼저 main.o가 gcc에 의해 빌드가 되고 obj에 명시된 목적 파일을 모아서 sungch.o를 만든다. 

이 sungch.o가 모듈 관련된 목적파일 sungch.mod.o와 링크가 되서 링커가 sungch.ko를 만든다. 그럼 커널 모듈 빌드가 완료한 것이다. 이제 이 완료된 커널 모듈을 qemu로 부팅해서 확인할 수 있다.

 

Usage Kernel Module

1. 빌드한 모듈을 rootfs 의 /usr/lib/modules 에 복사하기

sudo mount -o loop <빌드 루트 디렉토리>/output/images/rootfs.ext4 /mnt
sudo mkdir /mnt/usr/lib/modules
sudo cp <커널 디렉토리>/drivers/sungch/sungch.ko /mnt/usr/lib/modules/.
sync
# qemu가 사용할 수 있도록 umount
sudo umount /mnt

예시
$ losetup -f /backup/linux_device_driver/buildroot/output/images/rootfs.ext4
$ losetup -a

/dev/loop14: [2051]:7637340 (/backup/linux_device_driver/buildroot/output/images/rootfs.ext2)

$ mount /dev/loop14 /mnt
$ mkdir /mnt/usr/lib/modules
$ cp drivers/sungch/sungch.ko /mnt/usr/lib/modules/. 
$ sync
$ umount /mnt
  • losetup -f로 자동으로 사용 가능한 루프 장치를 할당.
  • losetup -a로 해당 루프 장치가 무엇인지 확인.
  • mount /dev/loopX /mnt (X는 할당된 루프 장치 번호)를 통해 마운트.

 

2. qemu 실행하기

qemu-system-aarch64 \
    -kernel <리눅스 디렉토리>/arch/arm64/boot/Image \
    -drive format=raw,file=<빌드루트 디렉토리>/output/images/rootfs.ext4,if=virtio \
    -append "root=/dev/vda console=ttyAMA0 nokaslr" \
    -nographic -M virt -cpu cortex-a72 \
    -m 2G -smp 2
    
 예시
 qemu-system-aarch64 \
    -kernel /root/linux/arch/arm64/boot/Image \
    -drive format=raw,file=/backup/linux_device_driver/buildroot/output/images/rootfs.ext4,if=virtio \
    -append "root=/dev/vda console=ttyAMA0 nokaslr" \
    -nographic \
    -M virt \
    -cpu cortex-a72 \
    -m 2G \
    -smp 2

 

3. 모듈 로드 / 언로드 하기

  • insmod /usr/lib/modules/sungch.ko : kernel 에 모듈을 로드하기
  • ismod: 커널에 로드된
  • rmmod <커널 모듈 이름> : 커널에 모듈 제거하기
# dmesg -c
# insmod /usr/lib/modules/sungch.ko
# dmesg
[   97.536465] sungch_module_init
# rmmod /usr/lib/modules/sungch.ko
# dmesg
[   97.536465] sungch_module_init
[  115.619425] sungch_module_exit

Kernel 기본 API 익히기

커널모듈 안에 코드를 작성할 때 필요한 API와 자료구조에 대해 알아보자.

 

인터페이스

  • 메모리 관련 함수
    • copy_from_user(to, from, size)
      • 사용자 공간에서 커널 공간으로 메모리를 복사할 때 사용.
    • copy_to_user(to, from, size)
      • 커널 공간에서 사용자 공간으로 메모리를 복사할 때 사용.
      • 설명:
        커널에서 직접적으로 사용자 공간을 접근하지 못하도록 막혀 있음.
        이유: 커널의 데이터가 사용자 공간으로 유출되는 보안상 취약점이 많이 발생했기 때문.
        반환 값: 복사에 실패한 바이트 수를 반환.
    • kmalloc(size, type), kzalloc(size, type)
      • 커널 공간에서 메모리를 동적으로 할당.
      • kzalloc은 kmalloc과 동일한 메모리 할당 기능을 제공하나, 할당된 메모리를 0으로 초기화.
      • type 파라미터:
        GFP_KERNEL: 메모리 확보가 가능할 때까지 대기.
        GFP_ATOMIC: 메모리 할당이 실패할 수 있으며, 할당 실패 시 NULL 반환.
        GFP_DMA: 하드웨어에서 사용할 수 있는 메모리(물리적으로 연속된 메모리).
    • kfree(pointer)
      • kmalloc 또는 kzalloc으로 할당받은 메모리를 해제.

 

자료구조

double linked list

커널에서 가장 많이 사용하는 자료구조는 double linked list이다.

linux/list.h에 관련 함수와 구조체가 정의되어 있다.
list_head 구조체를 사용하여 원하는 구조체를 노드로 쉽게 사용 가능하다.

#include <linux/module.h>

struct buffer {
    char buffer[256];
    struct list_head list;
};

LIST_HEAD(buffer_list);

...

struct buffer *a = kmalloc(sizeof(struct buffer), GFP_KERNEL);
list_add(&a->list, &buffer_list);

struct buffer *b = kmalloc(sizeof(struct buffer), GFP_KERNEL);
list_add(&b->list, &buffer_list);

struct buffer라는 구조체에 list_head를 포함시켜 링크드 리스트의 노드로 사용한다.

LIST_HEAD(buffer_list)는 buffer_list라는 리스트를 선언하며, list_add 함수를 사용해 동적으로 할당된 struct buffer 노드를 리스트에 추가할 수 있다.

 

  • 1. LIST_HEAD(name) : 링크드 리스트 선언
  • 2. list_add(new, head) : 맨 앞에 새로운 노드 추가(맨 뒤에 추가하는 함수는 list_add_tail(new, head))
  • 3. list_del(target) : 해당 노드를 리스트에서 제거
  • 4. list_empty(head) : 링크드 리스트가 비어있는지 확인
  • 5. list_for_each(node, head, member) { ... } : 링크드 리스트를 하나씩 순회하면서 동작
// 리스트에서 노드 삭제 및 메모리 해제
struct buffer *node, *tmp;
list_for_each_entry_safe(node, tmp, &buffer_list, list) {
    list_del(&node->list);
    kfree(node);
}

// 리스트 순회하며 데이터 출력
struct buffer *node;
list_for_each_entry(node, &buffer_list, list) {
    printk("%s\n", node->buffer);
}

 

 

Hash Table

Hash Table은 linked list만큼 자주 사용하진 않지만 데이터 검색 시 걸리는 시간 복잡도를 줄여주는 용도로 사용되는 자료구조이다. 

  • 1. DEFINE_HASHTABLE(name) : 해시테이블 선언
  • 2. hash_add(table, new, key) : 새로운 노드 추가
  • 3. hash_del(target) : 해당 노드를 해시테이블에서 제거
  • 4. hash_for_each_possible(table, node, member, key) { ... }
    • key에 해당하는 해시테이블 순회
    • node에 각 노드가 들어오며 미리 선언되어 있어야 함
    • member는 구조체 내에서 hlist_node 타입의 변수 이름을 의미
    • 다른 key 값이더라도 해시 값이 같은 상황(충돌)이 발생하므로, key가 일치하는지 검사 필요
  • 5. hash_for_each(table, bkt, node, hash) : 모든 노드를 순회
    • node에 각 노드가 들어오며 미리 선언되어 있어야 함
    • member는 구조체 내에서 hlist_node 타입의 변수 이름을 의미
    • hash_for_each_*_safe(*) : 순회할 때 해시테이블을 수정해야 한다면 사용

Device driver 개발하기

Device driver 종류

1. 문자 디바이스 드라이버

-> 대부분의 디바이스 드라이버가 이에 해당한다.

-> 파일 오퍼레이션만 구현하면 되기 때문에 구현하기 쉽다.

 

2. 블록 디바이스 드라이버

-> 대용량의 데이터를 저장하는 디바이스에 대해 사용

 

3. 네트워크 디바이스 드라이버

-> 외부와 통신하며 특히 소켓을 사용하여 통신하는 디바이스에 대해 사용

 

4. 버스 디바이스 드라이버

-> USB 나 PCI 등 여러 다른 디바이스가 꽂히는 포트 디바이스에 대해 사용

-> 디바이스를 탐색하고 인식하는 것이 주역할

 

Device Node

device driver 를 다루기 위한 특수한 파일로 모든 디바이스 노드는 고유의 타입, 주번호, 부번호를 가진다.

  • type: 문자형(c), 블록형(b)
    • 네트워크나 버스 디바이스 드라이버는 노드로 노출되지 않음
  • major: 디바이스 드라이버를 구분하기 위한 번호(0 ~ 511)
    • 디바이스 드라이버 마다 고유하며 커널이 자동으로 할당하기도 한다.
  • minor: 디바이스를 구분하기 위한 번호 (0 ~ 1048576)
    • 디바이스마다 고유하며 디바이스 드라이버가 할당을 관리함
  • 동일한 디바이스 드라이버를 사용하는 여러 디바이스가 존재할 수 있음
    • ex: USB 마우스를 여러개 꽂았을 경우 - 디바이스는 여러개, 디바이스 드라이버는 1개

아래 명령어로 디바이스 노드 파일을 생성한다. 일반적으로 디바이스 노드는 /dev 에만 생성하여 관리한다. 

mknod <file name> <type> <major> <minor>

 

References