カーネルドライバ開発

投稿者: | 2023年4月23日

準備(方法①:カーネルソースから準備)

カーネルソースの取得

takayuki@ubuntu:~$ sudo apt update
takayuki@ubuntu:~$ sudo apt install linux-source

/usr/src以下にカーネルソースのbz2ファイルができるので、適当なディレクトリに回答する。

takayuki@ubuntu:~/repos$ tar jxvf /usr/src/linux-source-5.4.0.tar.bz2

カーネルモジュールの前に、ビルドに必要な.configを作成。他にmake prepare, make scriptsを実行しておく。

takayuki@ubuntu:~/repos/linux-source-5.4.0$ sudo apt-get update && sudo apt install -y build-essential bc bison flex
 libelf-dev libssl-dev libncurses5-dev
takayuki@ubuntu:~/repos/linux-source-5.4.0$ make oldconfig
takayuki@ubuntu:~/repos/linux-source-5.4.0$ sudo make prepare
takayuki@ubuntu:~/repos/linux-source-5.4.0$ sudo make scripts

カーネルモジュールのビルド

VBoxでつかっているe1000ドライバをコンパイルしてみて、ちゃんとコンパイルできるか確認する。

takayuki@ubuntu:~/repos/linux-source-5.4.0/drivers/net/ethernet/intel/e1000$ sudo make -C /home/takayuki/repos/linux-source-5.4.0 M=`pwd`
make: Entering directory '/home/takayuki/repos/linux-source-5.4.0'

(..)

  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/takayuki/repos/linux-source-5.4.0/drivers/net/ethernet/intel/e1000/e1000.mod.o
  LD [M]  /home/takayuki/repos/linux-source-5.4.0/drivers/net/ethernet/intel/e1000/e1000.ko
make: Leaving directory '/home/takayuki/repos/linux-source-5.4.0'

準備(方法②linux-headerをインストール)

sudo apt-get update
sudo apt-get install linux-headers-$(uname -r) build-essential

Hello worldモジュール作成

コード

ChatGPTでコード生成。Makefileも作成しておく。

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye world\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your name");
MODULE_DESCRIPTION("Hello world module");
  • insmodで呼び出されるhello_initとrmmodで呼び出されるhello_exitを、それぞれmodule_initとmodule_exitでエントリポイントとして登録
  • エントリポイントは他にopen, ioctl, closeが登録できる
  • ドライバ内でプロセスからのioctlで始まるプロセスコンテキストと、割込みからはじまる割込みコンテキストがある。プロセスコンテキストは処理時間が長くてもいいが、割込みコンテキストは処理時間を短く、スリープもしないようにしないといけない。メモリコピーもできない。
obj-m := hello.o

KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

ビルド

takayuki@ubuntu:~/repos/hello$ make -C /home/takayuki/repos/linux-source-5.4.0 M=$(pwd) 
make: Entering directory '/home/takayuki/repos/linux-source-5.4.0'
  AR      /home/takayuki/repos/hello/built-in.a
  CC [M]  /home/takayuki/repos/hello/hello.o

  WARNING: Symbol version dump ./Module.symvers
           is missing; modules will have no dependencies and modversions.

  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/takayuki/repos/hello/hello.mod.o
  LD [M]  /home/takayuki/repos/hello/hello.ko
make: Leaving directory '/home/takayuki/repos/linux-source-5.4.0'

実行

takayuki@ubuntu:~/repos/hello$ sudo insmod ./hello.ko
takayuki@ubuntu:~/repos/hello$ dmesg|tail -1
[ 7376.257904] Hello world
takayuki@ubuntu:~/repos/hello$ sudo rmmod ./hello.ko

ioctlでソケット操作

Ubuntu Manpage: socket – Linux のソケットインターフェース

ソケットFDを指定して、ソケットのオプションを設定、取得できる。
カーネル内実装にはSIOCGSTAMPやSIOCSPGRPがある。
  
error = ioctl(ip_socket, ioctl_type, &value_result);

ソケットのioctlはsocket.cで、socketファイルにstruct file_operationsにsock_ioctlが設定されている。
ファイルに設定できるstruct file_operationsのため、自前のioctlをソケットに追加することはできない見込み。

ioctlでソケットオプションを取得するモジュール作成

ソケットファイルディスクリプタにioctlを設定するかわりに、ioctl用のデバイスドライバを作成し、自前のioctl関数を登録する。

処理対象のソケットは引数で指定する。


#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/net.h>
#include <net/sock.h>
#include <asm/current.h>
#include <asm/uaccess.h>

#include "sock_ioctl.h"

static int sock_ioctl_devs = 2;
static int sock_ioctl_major = 0;
module_param(sock_ioctl_major, uint, 0);
static struct cdev sock_ioctl_cdev;
static struct class *sock_ioctl_class = NULL;
int minor;

#define DRIVER_NAME "sock_ioctl"
#define MINOR_BASE 0
#define MINOR_NUM 2 

static long sock_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct sock_values sock_values;
    struct socket *sock;
    sockptr_t optval;
    int val = 0;
    int intval = 0;
    int err = 0;

    if (copy_from_user(&sock_values, (void __user *)arg, sizeof(struct sock_values))) {
        printk(KERN_ERR "Can't copy arg from user land\n");
        return -EFAULT;
    }

    sock = sockfd_lookup(sock_values.sockfd, &err);
    if (!sock)return err;

    switch (cmd) {
    case SOCK_SET_CMD:
        val = sock_values.val;
        printk(KERN_ERR "val=%d\n",val);
        optval = KERNEL_SOCKPTR(&val);

        
        if(sock_setsockopt(sock, 0, sock_values.optname,
                      optval, sizeof(val)) < 0){
            printk(KERN_ERR "Failed to set sockopt\n");
        }
        break;
    case SOCK_GET_CMD:
        //TODO: 実装する。
        break;
    default:
        printk(KERN_ERR "Invalid ioctl command\n");
        return -ENOTTY;
    }

    return 0;
}

static int sock_ioctl_open(struct inode* inode, struct file* file){
    printk("%s: major %d minor %d (pid %d)", __func__, imajor(inode), iminor(inode), current->pid);

    inode->i_private = inode;
    file->private_data = file;

    printk("    i_private=%p private_data=%p\n", inode->i_private, file->private_data);

    return 0;
}

static int sock_ioctl_close(struct inode* inode, struct file* file){
    printk("%s: major %d minor %d (pid %d)", __func__, imajor(inode), iminor(inode), current->pid);
    printk("    i_private=%p private_data=%p\n", inode->i_private, file->private_data);

    return 0;
}

static struct file_operations sock_ioctl_fops = {
    .open = sock_ioctl_open,
    .release = sock_ioctl_close,
    .unlocked_ioctl = sock_ioctl,
};

static int __init sock_ioctl_init(void)
{
    dev_t dev = MKDEV(sock_ioctl_major, 0);
    int major;

	//alloc_chrdev_regionで空いているメジャー番号を取得
    if(alloc_chrdev_region(&dev, 0, sock_ioctl_devs, DRIVER_NAME)){
	pr_err("Cannot allocate major number\n");
return -1;
}
    
    sock_ioctl_major = major = MAJOR(dev);
    
    //cdev_initでcdevを初期化
    cdev_init(&sock_ioctl_cdev, &sock_ioctl_fops);
    sock_ioctl_cdev.owner = THIS_MODULE;
    
    //cdevをカーネルに登録。/proc/devices以下にに現れるようになる。mknodでデバイスファイルができる。
    if(cdev_add(&sock_ioctl_cdev, MKDEV(sock_ioctl_major, 0), sock_ioctl_devs)){
		pr_err("Cannot add the device to the system\n");
      cdev_del(&sock_ioctl_cdev);
        unregister_chrdev_region(dev, sock_ioctl_devs);
return -1;
    }

	//デバイスのクラス登録をする(/sys/class/sock_ioctl/ を作る)
    sock_ioctl_class = class_create(THIS_MODULE, "sock_ioctl");
    if (IS_ERR(sock_ioctl_class)) {
        printk(KERN_ERR  "class_create\n");
        cdev_del(&sock_ioctl_cdev);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }
    
    //sys/class/sock_ioctl/sock_ioctl* を作る
    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_create(sock_ioctl_class, NULL, MKDEV(sock_ioctl_major, minor), NULL, "sock_ioctl%d", minor);
    }

    printk(KERN_ALERT "%s driver(major %d) installed.\n", DRIVER_NAME, major);
return 0;
};

static void __exit sock_ioctl_exit(void)
{
    dev_t dev = MKDEV(sock_ioctl_major, 0);

    // /sys/class/sock_ioctl/sock_ioctl* を削除する */
    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_destroy(sock_ioctl_class, MKDEV(sock_ioctl_major, minor));
    }

    // このデバイスのクラス登録を取り除く(/sys/class/sock_ioctl/を削除する)
    class_destroy(sock_ioctl_class);

    // このデバイスドライバ(cdev)をカーネルから取り除く
    cdev_del(&sock_ioctl_cdev);
    unregister_chrdev_region(dev, sock_ioctl_devs);
    printk(KERN_ALERT "%s driver removed.\n", DRIVER_NAME);
}

module_init(sock_ioctl_init);
module_exit(sock_ioctl_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Takayuki Fukatani");
MODULE_DESCRIPTION("Sample module for ioctl");
#ifndef SOCK_DRIVER_H_
#define SOCK_DRIVER_H_

#include <linux/ioctl.h>
#include <linux/socket.h>

struct sock_values {
    int sockfd;
    int optname;
    int val;
};

/*** ioctl用コマンド(request, 第2引数)の定義 ***/
#define SOCK_SET_IOC_TYPE 'M'

#define SOCK_SET_CMD _IOW(SOCK_SET_IOC_TYPE, 1, struct sock_values)

/* デバドラから値を取得するコマンド。パラメータはsock_values型 */
#define SOCK_GET_CMD _IOR(SOCK_IOC_TYPE, 2, struct sock_values)

#endif /* SOCK_DRIVER_H_ */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "sock_ioctl.h"

#define PORT 22

int main()
{
    int fd = 0, sock = 0;
    struct sock_values val;
    struct sockaddr_in server_addr;

    // ソケットの作成
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket");
        exit(1);
    }

    // サーバの情報を設定
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    // サーバに接続
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        exit(1);
    }

    printf("Connection established\n");
    val.sockfd = sock;
    val.optname = SO_REUSEADDR;
    val.val = 1;

    if ((fd = open("/dev/sock_ioctl0", O_RDWR)) < 0) perror("open");
    if (ioctl(fd, SOCK_SET_CMD, &val) < 0) perror("ioctl_set");

    // ソケットオプションを取得
    int get_optval;
    socklen_t optlen = sizeof(get_optval);
    if (getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &get_optval, &optlen) < 0) {
        perror("getsockopt");
        exit(1);
    }

    printf("SO_REUSEADDR: %d\n", get_optval);

    if (close(sock) != 0) perror("close");
    return 0;
}

参考文献

組み込みLinuxデバイスドライバの作り方 (6) – Qiita

デバイスドライバ作成 インラインアセンブラ編 – マイペースなプログラミング日記 (hatenablog.com)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です