블로그 이전했습니다. https://jeongzero.oopy.io/
linux Character Device Drivers 만들기
본문 바로가기
컴퓨터 관련 과목/운영체제 & 커널

linux Character Device Drivers 만들기

728x90

출처 : https://temp123.tistory.com/16?category=877924

위 블로그의 설명을 토대로 문자 디바이스 드라이버를 간단하게 구현해볼것이다.

1. 디바이스 드라이버


일반적으로 디바이스란 컴퓨터에 물려있는 여러 주변장치들을 뜻한다. 네트워크 어댑터, LCD 디스플레이, 오디오, 터미널, 키보드, 하드디스크, 플로피디스크, 프린터 등이 바로 디바이스에 해당하고, 이러한 디바이스들을 컨트롤하기 위한 디바이스 드라이버가 존재한다.

디바이스 드라이버란 실제 장치 부분을 추상화시켜 사용자 프로그램이 정형화된 인터페이스를 통해 디바이스를 접근할 수 있도록 해주는 프로그램이라고 보면 된다. 리눅스에서는 모든것을 파일로 간주하는데, 이러한 디바이스 드라이버 또한 파일로 관리된다.

/dev/ 아래에 들어있는 파일들이 바로 디바이스 드라이버 인터페이스이고, 하드웨어와는 독립적으로 응용프로그램이 파일 open, read, 같은 함수로 접근할수 있다.

제일 하단에 있는 Real Device가 실제 물리적인 하드웨어이고, 디바이스 드라이버를 통해서 실제 디바이스를 컨트롤하게 된다. 헌데 각 장치별로 제공되는 디바이스 드라이버가 다르다. 개발자가 모든 하드웨어 규격에 맞춰서 개발을 하는건 매우 비효율적인 일일것이다.

따라서 리눅스에서는 VFS라는 파일 시스템 기능을 지원한다. 또한 모든 디바이스 드라이버는 /dev 하의에 파일로 취급되고, 위에서 말한것처럼 open, read, write 등의 연산을 통해 디바이스를 컨트롤 할 수 있다. 또한 이러한 디바이스 파일들은 고유한 번호와 이름을 할당받기 때문에 만약, 디바이스 드라이버를 제작하고 등록하려면 고유한 번호 및 이름을 할당해야 한다.

2. 디바이스 드라이버 종류


디바이스 드라이버는 크게 3가지로 나뉜다.

Device driver 종류설명등록 함수
Char device driverdevice를 파일처럼 접근하여 직접 read/write 수행한다. data 형태는 stream 방식으로 전송register_chrdev()
block device driverdisk와 같은 file system을 기반으로 block 단위로 데이터를 read/writeregister_blkdev()
network device drviernetwork의 물리계층과 frame 단위의 데이터 송수신register_netdev()

Char Device 특징

  • 버퍼 캐쉬를 사용하지 않는다
  • 자료의 순차성을 지님
  • 터미널, 키보드 사운드 카드, 프린터 등..

Block Device 특징

  • 블록 단위의 입출력이 가능한 장치를 뜻함
  • 버퍼 캐쉬에 의한 내부 장치 표현
  • 파일 시스템에 의해 마운트 되어 관리됨
  • 하드 디스크 등..

Network Device 특징

  • 네트워크 스택과 네트워크 하드웨어 사이에 위치해 데이터의 송수신을 담당
  • 이더넷, 네트워크 인터페이스 카드 등 ..

실제로 /dev/ 하위 파일들을 확인해보자.

brw-rw----   1 root     disk      8,   1 Nov 15 10:52 sda1
crw-rw----+  1 root     cdrom    21,   0 Nov 15 10:52 sg0
crw-rw----   1 root     disk     21,   1 Nov 15 10:52 sg1
drwxrwxrwt   2 root     root          40 Nov 15 10:52 shm
crw-------   1 root     root     10, 231 Nov 15 10:52 snapshot
drwxr-xr-x   3 root     root         200 Nov 15 10:52 snd
brw-rw----+  1 root     cdrom    11,   0 Nov 15 10:52 sr0
lrwxrwxrwx   1 root     root          15 Nov 15 10:52 stderr -> /proc/self/fd/2
lrwxrwxrwx   1 root     root          15 Nov 15 10:52 stdin -> /proc/self/fd/0
lrwxrwxrwx   1 root     root          15 Nov 15 10:52 stdout -> /proc/self/fd/1
crw-rw-rw-   1 root     tty       5,   0 Nov 15 20:05 tty
crw--w----   1 root     tty       4,   0 Nov 15 10:52 tty0
crw--w----   1 gdm      tty       4,   1 Nov 15 10:52 tty1
crw--w----   1 root     tty       4,  10 Nov 15 10:52 tty10
crw--w----   1 root     tty       4,  11 Nov 15 10:52 tty11
crw--w----   1 root     tty       4,  12 Nov 15 10:52 tty12
crw--w----   1 root     tty       4,  13 Nov 15 10:52 tty13
crw--w----   1 root     tty       4,  14 Nov 15 10:52 tty14

맨 앞에 b 가 붙어있으면 block device이고, c가 붙어있으면 char device이다. 또한 형광색으로 칠해준 두개의 숫자가 각각 major, minor 번호이다. major 번호는 디바이스 종류를 구분하기 위해 필요하며, minor는 예를 들어 제어하려는 디바이스가 COM1 포트인지 COM3 포트인지 구분하기 위해 필요하다. 즉 major는 디바이스 종류 구분을 위해, minor는 같은 종류중에서도 실제 제어해야 할 디바이스를 구분하기 위함이다.

위 설명을 하나의 그림으로 표현하면 아래와 같다.

3. Char device driver 제작하기


이제 실제로 간단한 Char device driver를 제작해보자. 우선 흐름은 아래 그림과 같다.

소스코드를 작성하고 컴파일을 한뒤 insmod로 커널에 생성한 모듈을 적재한다. 그다음 만들 커널 모듈을 위한 디바이스 파일을 만들어야 하기 때문에 mknod 명령어를 이용한다. 해당 명령어로 디바이스 파일을 만들었으면, 이제 응용 프로그램에서 open, read 등을 이용하여 디바이스 파일에 접근한다.

실제 char device 드라이버 모듈을 제작해보자. 커널 모듈이 초기에 등록될때 init 과정을 거치는데, 이 안에서 char device를 등록하는 로직을 넣어줘야한다. 이는 register_chrdev() 함수를 이용하면 된다.

register_chrdev("major number", "device name", "file_operations 구조체 변수")

char device 등록함수는 위와 같이 3개의 인수를 가진다. 첫번째는 major 번호, 두번째는 디바이스 이름이다. 마지막은 file_operations 구조체이다.

file_operations 구조체는 Char Device, Block Device 드라이버와 일반 응용 프로그램간의 통신을 위해 제공되는 인터페이스라고 보면 된다. read, write, open, release 등의 함수포인터를 사용하여 디바이스 드라이버를 제작하면 된다.(Network device는 file_operations 구조체를 사용하지 않고 "include/linux/netdevice.h" 의 net_device 구조체를 사용함)

file_operations 구조체

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    unsigned long mmap_supported_flags;
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*setfl)(struct file *, unsigned long);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
    int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
    ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64);
} __randomize_layout;

예를 들어 제작한 디바이스 모듈에서 open() 함수를 다음과 같이 제공 할 수 있다. test_open 이라고 함수명을 작성한 뒤, file operations 구조체의 .open 필드에 저장한다. 이렇게 되면 제작한 디바이스 모듈이 커널에 등록되고 init() 함수가 동작할때 init() 내부에 작성한 register_chrdev(.. , test_open , ..) 함수가 호출되면서 char device가 등록된다.

그 후 응용프로그램에서 제작한 디바이스를 open 하게 되면, test_open 함수가 호출된다.

위 과정을 정리하면 위의 그림으로 표현이 된다. 리눅스 커널 기초 글에서 시스템 콜에 대한 설명을 다룬적이 있다. 유저 공간에서 open, close, read, write 같은 함수들은 트랩에 의해 커널에게 system call을 통해 처리가 된다.

sys_.. 함수내부에선 실제 VFS 내부의 file_operations 구조체의 함수 포인터를 참조하고, 거기에 등록된 디바이스 드라이버 함수가 실제로 호출되는 과정이다. 여기선 test_open이 위에서 my_open() 함수 위치일 것이다.

정리하면 다음과 같다. 제작하려는 디바이스 드라이버 내부에서 file_operations 구조체의 함수 포인터들의 포맷을 맞춰서 원하는 함수를 구현하고, 이를 file_operations 구조체의 각 필드에 등록해주면 된다. 실제 테스트용 디바이스 드라이버 코드는 아래와 같다.

test_device.c

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
 
static char *buffer = NULL;
 
int test_open(struct inode *inode, struct file *filp) {
    printk(KERN_ALERT "test_device open function called\n");
    return 0;
}
 
int test_device_release(struct inode *inode, struct file *filp) {
    printk(KERN_ALERT "testdevice release function called\n");
    return 0;
}
 
ssize_t test_device_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos) {
    printk(KERN_ALERT "test_device write function called\n");
    strcpy(buffer, buf);
    return count;
}
 
ssize_t test_device_read(struct file *filp, char *buf, size_t count, loff_t *f_pos) {
    printk(KERN_ALERT "test_device read function called\n");
    copy_to_user(buf, buffer, 1024);
    return count;
}
 
static struct file_operations vd_fops = {
    .read = test_device_read,
    .write = test_device_write,
    .open = test_device_open,
    .release = test_device_release
};
 
int __init test_device_init(void) {
    if(register_chrdev(300, "test_device", &vd_fops) < 0 )
        printk(KERN_ALERT "driver init failed\n");
    else
        printk(KERN_ALERT "driver init successful\n");
    buffer = (char*)kmalloc(1024, GFP_KERNEL);
    if(buffer != NULL) 
        memset(buffer, 0, 1024);
    return 0;
}
 
void __exit test_device_exit(void) {
    unregister_chrdev(250, "test_device");
    printk(KERN_ALERT "driver cleanup successful\n");
    kfree(buffer);
}
 
module_init(test_device_init);
module_exit(test_device_exit);
MODULE_LICENSE("GPL");

test_device_init() 함수 내부에서 register_chrdev() 를 통해 제작한 char device를 등록한다. 디바이스 드라이버 이름은 'test_device' 이고 major 번호는 300을 주었다. 또한 open, read, write, release 와 관련된 함수를 구현하고 file_operations 구조체의 필드에 매핑시켰다.

test_device_init() 함수 내부를 보면 register_chrdev() 로 등록하고, kmalloc을 통해서 1024 바이트 크기의 버퍼를 동적할당받는다. 그리고 초기화를 시킨다. read 후 write를 buffer에 하면 읽은 데이터가 buffer에 저장될 것이다.

이제 Makefile을 만들어서 컴파일을 해보자

Makefile

  1 obj-m += test_device.o
  2 
  3 all:
  4     make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
  5 clean:
  6     make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
╭─wogh8732@ubuntu ~/Desktop/kernel_study/develop_kermod/start_chr_device 
╰─$ make              
make -C /lib/modules/5.4.0-53-generic/build M=/home/wogh8732/Desktop/kernel_study/develop_kermod/start_chr_device modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-53-generic'
  CC [M]  /home/wogh8732/Desktop/kernel_study/develop_kermod/start_chr_device/test_device.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/wogh8732/Desktop/kernel_study/develop_kermod/start_chr_device/test_device.mod.o
  LD [M]  /home/wogh8732/Desktop/kernel_study/develop_kermod/start_chr_device/test_device.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-53-generic'
╭─wogh8732@ubuntu ~/Desktop/kernel_study/develop_kermod/start_chr_device 
╰─$ vim Makefile 
╭─wogh8732@ubuntu ~/Desktop/kernel_study/develop_kermod/start_chr_device 
╰─$ ls
Makefile       Module.symvers  test_device.ko   test_device.mod.c  test_device.o
modules.order  test_device.c   test_device.mod  test_device.mod.o

test_device.ko 커널 모듈이 정상적으로 생성이 되었다. 이제 커널에 올리고 확인해보자.

╭─wogh8732@ubuntu ~/Desktop/kernel_study/develop_kermod/start_chr_device 
╰─$ sudo insmod ./test_device.ko
╭─wogh8732@ubuntu ~/Desktop/kernel_study/develop_kermod/start_chr_device 
╰─$ sudo lsmod | grep 'test'                                                                   130 ↵
test_device            16384  0

잘 올라갔다. 이제 실제 디바이스는 아니지만 제작한 디바이스 드라이버를 이용하려면 /dev/ 하위에 디바이스 파일을 생성해야 한다. 그래야지 응용프로그램에서 이를 이용하여 디바이스를 컨트롤할수 있다.

디파이스 파일을 생성하는 명령어는 mknod 이다

형식 : mknod ([옵션]) [장치명] [타입] ([주번호 부번호])
------------------------------------------------------------
sudo mknod /dev/test_device c 300 0

디바이스 파일을 생성하고 확인해보자

╭─wogh8732@ubuntu ~/Desktop/kernel_study/develop_kermod/start_chr_device 
╰─$ sudo mknod /dev/test_device c 300 0
╭─wogh8732@ubuntu ~/Desktop/kernel_study/develop_kermod/start_chr_device 
╰─$ ls -al /dev/test*
crw-r--r-- 1 root root 300, 0 Nov 15 23:38 /dev/test_device

정상적으로 생성되었다.

이제 여기까지의 과정을 정리하면 다음과 같다.

test_device.ko을 만들었고, /dev 하위에 test_device 파일을 생성했다. 이제 응용프로그램으로 우리가 제작한 디바이스가 정상적으로 동작하는지 확인하면 된다.

test.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
 
int main() {
    int dev;
    char buff[1024];
 
    printf("Device driver test.\n");
 
    dev = open("/dev/test_device", O_RDWR);
    printf("dev = %d\n", dev);
 
    write(dev, "1234", 4);
    read(dev, buff, 4);
    printf("read from device: %s\n", buff);
    close(dev);
 
    exit(EXIT_SUCCESS);
}

/dev/test_device 를 open하여 write, read를 통해 제작한 디바이스 드라이버가 제대로 동작하는지 알수 있다. 해당 코드를 컴파일하여 결과를 확인해보자.

root@ubuntu:/home/wogh8732/Desktop/kernel_study/develop_kermod/start_chr_device# ./ttt
Device driver test.
dev = 3
Killed

??? 머지;; 에러가 난다. dmesg로 확인해보자.

[ 3875.015627] testdevice release function called
[ 4067.683196] test_device open function called
[ 4067.683239] test_device write function called
[ 4067.683249] BUG: unable to handle page fault for address: 000055d5de97d973
[ 4067.683253] #PF: supervisor read access in kernel mode
[ 4067.683255] #PF: error_code(0x0001) - permissions violation

open 까지는 잘 되는데, write할때 에러가 난다. 음... 저 에러를 해결하려고 삽질좀 했으나 결국 에러는 못 해결쓰 ~~ 시간 세이브를 위해 다른 참고 사이트의 코드로 수정하자.

chardev.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <linux/uaccess.h>
 
MODULE_LICENSE("Dual BSD/GPL");
 
#define DRIVER_NAME "chardev"
#define BUFFER_SIZE 256
     
static const unsigned int MINOR_BASE = 0;
static const unsigned int MINOR_NUM  = 2;
static unsigned int chardev_major;
static struct cdev chardev_cdev;
static struct class *chardev_class = NULL;
 
static int     chardev_open(struct inode *, struct file *);
static int     chardev_release(struct inode *, struct file *);
static ssize_t chardev_read(struct file *, char *, size_t, loff_t *);
static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *);
 
struct file_operations chardev_fops = {
    .open    = chardev_open,
    .release = chardev_release,
    .read    = chardev_read,
    .write   = chardev_write,
};
 
struct data {
    unsigned char buffer[BUFFER_SIZE];
};
 
static int chardev_init(void)
{
    int alloc_ret = 0;
    int cdev_err = 0;
    int minor;
    dev_t dev;
 
    printk("The chardev_init() function has been called.");
     
    alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
    if (alloc_ret != 0) {
        printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
        return -1;
    }
    //Get the major number value in dev.
    chardev_major = MAJOR(dev);
    dev = MKDEV(chardev_major, MINOR_BASE);
 
    //initialize a cdev structure
    cdev_init(&chardev_cdev, &chardev_fops);
    chardev_cdev.owner = THIS_MODULE;
 
    //add a char device to the system
    cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM);
    if (cdev_err != 0) {
        printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }
 
    chardev_class = class_create(THIS_MODULE, "chardev");
    if (IS_ERR(chardev_class)) {
        printk(KERN_ERR  "class_create\n");
        cdev_del(&chardev_cdev);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }
 
    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor);
    }
 
    return 0;
}
 
static void chardev_exit(void)
{
    int minor; 
    dev_t dev = MKDEV(chardev_major, MINOR_BASE);
     
    printk("The chardev_exit() function has been called.");
     
    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_destroy(chardev_class, MKDEV(chardev_major, minor));
    }
 
    class_destroy(chardev_class);
    cdev_del(&chardev_cdev);
    unregister_chrdev_region(dev, MINOR_NUM);
}
 
static int chardev_open(struct inode *inode, struct file *file)
{
    char *str = "helloworld";
    int ret;
 
    struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL);
 
    printk("The chardev_open() function has been called.");
     
    if (p == NULL) {
        printk(KERN_ERR  "kmalloc - Null");
        return -ENOMEM;
    }
 
    ret = strlcpy(p->buffer, str, sizeof(p->buffer));
    if(ret > strlen(str)){
        printk(KERN_ERR "strlcpy - too long (%d)",ret);
    }
 
    file->private_data = p;
    return 0;
}
 
static int chardev_release(struct inode *inode, struct file *file)
{
    printk("The chardev_release() function has been called.");
    if (file->private_data) {
        kfree(file->private_data);
        file->private_data = NULL;
    }
    return 0;
}
 
static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct data *p = filp->private_data;
 
    printk("The chardev_write() function has been called.");   
    printk("Before calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
    if (copy_from_user(p->buffer, buf, count) != 0) {
        return -EFAULT;
    }
    printk("After calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
    return count;
}
 
static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct data *p = filp->private_data;
 
    printk("The chardev_read() function has been called.");
     
    if(count > BUFFER_SIZE){
        count = BUFFER_SIZE;
    }
 
    if (copy_to_user(buf, p->buffer, count) != 0) {
        return -EFAULT;
    }
 
    return count;
}
 
module_init(chardev_init);
module_exit(chardev_exit);

원래 사용했던 test_device.c와는 조금 다르게 추가된 부분들이 많다. open() 만 하려면 동일하게 하면 되지만, read, write 를 하기 위해서는 위와같이 디바이스 코드를 작성해야 하는것 같다. 여튼 위 코드를 간단히 분석해보자.

chardev_init()

커널 모듈이 등록될때 chardev_init() 내부에서는 다음의 동작을 수행한다.

  • alloc_chrdev_region() 함수를 호출하여 char device의 번호를 시스템에 등록한다. 이 말인 즉슨 생성하려는 디바이스 id값을 얻고, 이를 시스템에 등록한다는 것 같다. 디바이스 번호는 dev_t 타입 변수에 저장된다.
  • major(), mkdev() 함수를 이용하여 디바이스에 사용할 Major, Minor 번호를 얻는다.
  • cdev_init() 함수를 이용하여 cdev 구조체를 초기화 한다. 여기서 cdev 구조체란, 커널이 내부적으로 char device를 제어하기 위한 구조체이다. 따라서 file_operations 구조체를 통해 등록된 open, read 같은 함수가 호출되기 전에 cdev 구조체를 할당해줘야 한다. 또한 file_opeartions 구조체도 초기화 한다.
    struct cdev { 
    	struct kobject kobj; 
    	struct module *owner; 
    	const struct file_operations *ops; 
    	struct list_head list; dev_t dev; 
    	unsigned int count; 
    } __randomize_layout;
    
  • cdev_init()을 통해 준비된 cdev 구조체를 커널에 등록하고 char device를 시스템에 추가한다
  • class_create() 함수를 이용하여 시스템에 생성할 device class를 생성한다. 요건 정확히 먼지는 모르겠지만, 최종적으로 device_create()를 할때 필요하다
  • device_create() 함수를 이용하여 시스템에 디바이스를 생성한다. 이건 뇌피셜로 mknod 명령어를 이용하여 수동으로 디바이스 파일을 만드는 그런 역할인 것 같다. 위 코드에서는 for문을 돌면서 2개의 디바이스 파일을 만드는 것 같다.

chardev_exit()

해당 모듈이 제거될때 수행되는 로직이다.

  • device_destroy() 함수를 이용하여 device_create() 함수에 의해 생성된 디바이스를 제거한다.
  • class_destroy() 함수를 이용하여 class_create() 함수에 의해 생성된 디바이스 클래스를 제거한다.
  • cdev_del() 함수를 이용하여 cdev_add() 함수에 의해 추가된 char device를 제거한다
  • unregister_chrdev_region() 함수를 이용하여 alloc_chardev_region() 함수에 의해 등록된 device 번호를 반환한다.

chardev_open()

유저 영역에서 해당 디바이스를 open할 때 마다 호출되며 수행되는 로직이다.

  • kmalloc() 함수를 호출하여 커널 힙 영역에(변수 p) data 구조체의 크기만큼 공간을 할당받는다.
  • strlcpy() 함수를 이용하여 str 변수에 저장된 "helloworld" 값을 p→buffer에 복사한다.
  • file 구조체의 private_data 필드에 kmalloc으로 할당받은 주소를 넣는다.

chardev_release()

유저 영역에서 해당 디바이스를 닫을때 수행되는 로직이다.

  • file->private_data를 확인하여 힙 영역을 할당받은게 있다면 kfree() 를 호출하여 힙 영역을 해제한다.

chardev_write()

  • copy_from_user() 함수를 이용하여 유저 영역으로 부터 전달받은 데이터(buf)를 p→buffer에 복사한다

chardev_read()

  • 유저 영역에서 해당 디바이스로부터 데이터를 받을때 호출되며 copy_to_user()함수를 이용하여 커널 영역 p→buffer 에 저장된 값을 buf 변수에 복사한다.

자 이제 커널 모듈을 빌드해보자. 이론대로라면 insmod 하고 났을때 /dev에 디바이스 파일이 생성되어 있을것이다. makefile은 그대로 이용하면 된다.

╭─wogh8732@ubuntu /dev 
╰─$ sudo su                                                                                    130 ↵
root@ubuntu:/dev# ls -al | grep 'chardev'
crw-------   1 root     root    240,   0 Nov 16 06:17 chardev0
crw-------   1 root     root    240,   1 Nov 16 06:17 chardev1

오 생각한대로 2개의 chardev가 만들어졌다. 뒤에 번호는 지들이 알아서 붙인것 같다. 이제 테스트 코드로 제작한 디바이스 드라이버가 정상적으로 동작하는지 확인해보자

test3.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
 
#define TEXT_LEN 12
 
int main()
{
    static char buff[256];
    int fd;
 
    if ((fd = open("/dev/chardev0", O_RDWR)) < 0){
        printf("Cannot open /dev/chardev0. Try again later.\n");
    }
 
    if (write(fd, "jaehoooo", TEXT_LEN) < 0){
        printf("Cannot write there.\n");
    }
 
    if (read(fd, buff, TEXT_LEN) < 0){
        printf("An error occurred in the read.\n");
    }else{
        printf("%s\n", buff);
    }
 
    if (close(fd) != 0){
        printf("Cannot close.\n");
    }
    return 0;
}

위 코드는 /dev/chardev0를 열고, write로 jaehoooo 문자열을 넣는다. 그다음 read로 읽고 buff에 넣는다.

root@ubuntu:/home/wogh8732/Desktop/kernel_study/develop_kermod/start_chr_device# ./test3
jaehoooo

오호 jaehoooo가 출력되었단 뜻은, open, write, read가 성공적으로 되었다는 뜻!! 자세하게 dmesg도 확인해보자.

[10359.354896] The chardev_open() function has been called.
[10359.354902] The chardev_write() function has been called.
[10359.354905] Before calling the copy_from_user() function : 0000000097745bb5, helloworld
[10359.354907] After calling the copy_from_user() function : 0000000097745bb5, jaehoooo
[10359.354909] The chardev_read() function has been called.

오호 정상적으로 open, write가 되었고, open했을때는 p→buffer에 helloworld가 들어갔고, write 후에는 jaehooo가 들어간것을 확인할수 있다.

4. 정리


간단한 char device driver를 제작해보았다. 최근 플젝을 하면서 low level 쪽을 많이 보고 있는데, 이 참에 커널 해킹을 위한 이론 지식좀 쌓고 그동안 해보고 싶었던 커널 해킹 ctf 문제들좀 풀어봐야 겠다.

이번에 정리한 내용을 토대로 CICSN 2017 CTF의 babydriver 커널 문제를 풀어 볼 예정이다.

5. 참고자료


728x90