Linux Device Driver 기초 #3 Linux Device Driver 추가하기
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으로 할당받은 메모리를 해제.
- copy_from_user(to, from, size)
자료구조
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>