最新消息:图 床

從 0 開始學 Linux 驅動開發(一)

COOL IAM 287浏览 0评论

作者:Hcamael@知道創宇404實驗室
英文版本:https://paper.seebug.org/976/

最近在搞IoT的時候,因為沒有設備,模擬跑固件經常會缺/dev/xxx,所以我就開始想,我能不能自己寫一個驅動,讓固件能跑起來?因此,又給自己挖了一個很大坑,不管最後能不能達到我的初衷,能學到怎麼開發Linux驅動,也算是有很大的收穫了。

前言

我寫的這個系列以實踐為主,不怎麼談理論,理論可以自己去看書,我是通過《Linux Device Drivers》這本書學的驅動開發,Github上有這本書中講解的實例的代碼[1]

雖然我不想談太多理論,但是關於驅動的基本概念還是要有的。Linux系統分為內核態和用戶態,只有在內核態才能訪問到硬件設備,而驅動可以算是內核態中提供出的API,供用戶態的代碼訪問到硬件設備。

有了基本概念以後,我就產生了一系列的問題,而我就是通過我的這一系列的問題進行學習的驅動開發:

  1. 一切代碼的學習都是從Hello World開始的,怎麼寫一個Hello World的程序?
  2. 驅動是如何在/dev下生成設備文件的?
  3. 驅動怎麼訪問實際的硬件?
  4. 因為我畢竟是搞安全的,我會在想,怎麼獲取系統驅動的代碼?或者沒有代碼那能逆向驅動嗎?驅動的二進制文件儲存在哪?以後有機會可能還可以試試搞驅動安全。

Everything start from Hello World

提供我的Hello World代碼[2]

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

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamal");

int hello_init(void)
{
    printk(KERN_INFO "Hello World/n");
    return 0;
}

void hello_exit(void)
{
    printk(KERN_INFO "Goodbye World/n");
}

module_init(hello_init);
module_exit(hello_exit);

Linux下的驅動是使用C語言進行開發的,但是和我們平常寫的C語言也有不同,因為我們平常寫的C語言使用的是Libc庫,但是驅動是跑在內核中的程序,內核中卻不存在libc庫,所以要使用內核中的庫函數。

比如printk可以類比為libc中的printf,這是在內核中定義的一個輸出函數,但是我覺得更像Python裡面logger函數,因為printk的輸出結果是打印在內核的日誌中,可以使用dmesg命令進行查看

驅動代碼只有一個入口點和一個出口點,把驅動加載到內核中,會執行module_init函數定義的函數,在上面代碼中就是hello_init函數。當驅動從內核被卸載時,會調用module_exit函數定義的函數,在上面代碼中就是hello_exit函數。

上面的代碼就很清晰了,當加載驅動時,輸出Hello World,當卸載驅動時,輸出Goodbye World

PS:MODULE_LICENSEMODULE_AUTHOR這兩個不是很重要,我又不是專業開發驅動的,所以不用關注這兩個

PSS: printk輸出的結果要加一個換行,要不然不會刷新緩衝區

編譯驅動

驅動需要通過make命令進行編譯,Makefile如下所示:

ifneq ($(KERNELRELEASE),)

    obj-m := hello.o

else

    KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
    PWD := $(shell pwd)

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

endif


clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

一般情況下,內核的源碼都存在與/usr/src/linux-headers-$(shell uname -r)/目錄下

比如:

$ uname -r
4.4.0-135-generic

/usr/src/linux-headers-4.4.0-135/  --> 該內核源碼目錄
/usr/src/linux-headers-4.4.0-135-generic/    --> 該內核編譯好的源碼目錄

而我們需要的是編譯好后的源碼的目錄,也就是/usr/src/linux-headers-4.4.0-135-generic/

驅動代碼的頭文件都需要從該目錄下進行搜索

M=$(PWD)該參數表示,驅動編譯的結果輸出在當前目錄下

最後通過命令obj-m := hello.o,表示把hello.o編譯出hello.ko, 這個ko文件就是內核模塊文件

加載驅動到內核

需要使用到的一些系統命令:

  • lsmod: 查看當前已經被加載的內核模塊
  • insmod: 加載內核模塊,需要root權限
  • rmmod: 移除模塊

比如:

# insmod hello.ko        // 把hello.ko模塊加載到內核中
# rmmod hello            // 把hello模塊從內核中移除

舊版的內核就是使用上面這樣的方法進行內核的加載與移除,但是新版的Linux內核增加了對模塊的驗證,當前實際的情況如下:

# insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Required key not available

從安全的角度考慮,現在的內核都是假設模塊為不可信的,需要使用可信的證書對模塊進行簽名,才能加載模塊

解決方法用兩種:

  1. 進入BIOS,關閉UEFI的Secure Boot
  2. 向內核添加一個自簽名證書,然後使用證書對驅動模塊進行簽名,參考[3]

查看結果

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>   /* printk() */
#include <linux/slab.h>     /* kmalloc() */
#include <linux/fs.h>       /* everything... */
#include <linux/errno.h>    /* error codes */
#include <linux/types.h>    /* size_t */
#include <linux/fcntl.h>    /* O_ACCMODE */
#include <linux/cdev.h>
#include <asm/uaccess.h>    /* copy_*_user */


MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamael");

int scull_major =   0;
int scull_minor =   0;
int scull_nr_devs = 4;
int scull_quantum = 4000;
int scull_qset = 1000;

struct scull_qset {
    void **data;
    struct scull_qset *next;
};

struct scull_dev {
    struct scull_qset *data;  /* Pointer to first quantum set. */
    int quantum;              /* The current quantum size. */
    int qset;                 /* The current array size. */
    unsigned long size;       /* Amount of data stored here. */
    unsigned int access_key;  /* Used by sculluid and scullpriv. */
    struct mutex mutex;       /* Mutual exclusion semaphore. */
    struct cdev cdev;     /* Char device structure. */
};

struct scull_dev *scull_devices;    /* allocated in scull_init_module */

/*
 * Follow the list.
 */
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
    struct scull_qset *qs = dev->data;

        /* Allocate the first qset explicitly if need be. */
    if (! qs) {
        qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
        if (qs == NULL)
            return NULL;
        memset(qs, 0, sizeof(struct scull_qset));
    }

    /* Then follow the list. */
    while (n--) {
        if (!qs->next) {
            qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
            if (qs->next == NULL)
                return NULL;
            memset(qs->next, 0, sizeof(struct scull_qset));
        }
        qs = qs->next;
        continue;
    }
    return qs;
}

/*
 * Data management: read and write.
 */

ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr; /* the first listitem */
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset; /* how many bytes in the listitem */
    int item, s_pos, q_pos, rest;
    ssize_t retval = 0;

    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;
    if (*f_pos >= dev->size)
        goto out;
    if (*f_pos + count > dev->size)
        count = dev->size - *f_pos;

    /* Find listitem, qset index, and offset in the quantum */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum; q_pos = rest % quantum;

    /* follow the list up to the right position (defined elsewhere) */
    dptr = scull_follow(dev, item);

    if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
        goto out; /* don't fill holes */

    /* read only up to the end of this quantum */
    if (count > quantum - q_pos)
        count = quantum - q_pos;

    if (raw_copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;

  out:
    mutex_unlock(&dev->mutex);
    return retval;
}

ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr;
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset;
    int item, s_pos, q_pos, rest;
    ssize_t retval = -ENOMEM; /* Value used in "goto out" statements. */

    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;

    /* Find the list item, qset index, and offset in the quantum. */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum;
    q_pos = rest % quantum;

    /* Follow the list up to the right position. */
    dptr = scull_follow(dev, item);
    if (dptr == NULL)
        goto out;
    if (!dptr->data) {
        dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
        if (!dptr->data)
            goto out;
        memset(dptr->data, 0, qset * sizeof(char *));
    }
    if (!dptr->data[s_pos]) {
        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
        if (!dptr->data[s_pos])
            goto out;
    }
    /* Write only up to the end of this quantum. */
    if (count > quantum - q_pos)
        count = quantum - q_pos;

    if (raw_copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;

        /* Update the size. */
    if (dev->size < *f_pos)
        dev->size = *f_pos;

  out:
    mutex_unlock(&dev->mutex);
    return retval;
}

/* Beginning of the scull device implementation. */

/*
 * Empty out the scull device; must be called with the device
 * mutex held.
 */
int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *next, *dptr;
    int qset = dev->qset;   /* "dev" is not-null */
    int i;

    for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
        if (dptr->data) {
            for (i = 0; i < qset; i++)
                kfree(dptr->data[i]);
            kfree(dptr->data);
            dptr->data = NULL;
        }
        next = dptr->next;
        kfree(dptr);
    }
    dev->size = 0;
    dev->quantum = scull_quantum;
    dev->qset = scull_qset;
    dev->data = NULL;
    return 0;
}

int scull_release(struct inode *inode, struct file *filp)
{
    printk(KERN_DEBUG "process %i (%s) success release minor(%u) file/n", current->pid, current->comm, iminor(inode));
    return 0;
}

/*
 * Open and close
 */

int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev; /* device information */

    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev; /* for other methods */

    /* If the device was opened write-only, trim it to a length of 0. */
    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
        if (mutex_lock_interruptible(&dev->mutex))
            return -ERESTARTSYS;
        scull_trim(dev); /* Ignore errors. */
        mutex_unlock(&dev->mutex);
    }
    printk(KERN_DEBUG "process %i (%s) success open minor(%u) file/n", current->pid, current->comm, iminor(inode));
    return 0;
}

/*
 * The "extended" operations -- only seek.
 */

loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
    struct scull_dev *dev = filp->private_data;
    loff_t newpos;

    switch(whence) {
      case 0: /* SEEK_SET */
        newpos = off;
        break;

      case 1: /* SEEK_CUR */
        newpos = filp->f_pos + off;
        break;

      case 2: /* SEEK_END */
        newpos = dev->size + off;
        break;

      default: /* can't happen */
        return -EINVAL;
    }
    if (newpos < 0)
        return -EINVAL;
    filp->f_pos = newpos;
    return newpos;
}

struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    // .unlocked_ioctl = scull_ioctl,
    .open =     scull_open,
    .release =  scull_release,
};

/*
 * Set up the char_dev structure for this device.
 */
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor + index);

    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &scull_fops;
    err = cdev_add (&dev->cdev, devno, 1);
    /* Fail gracefully if need be. */
    if (err)
        printk(KERN_NOTICE "Error %d adding scull%d", err, index);
    else
        printk(KERN_INFO "scull: %d add success/n", index);
}


void scull_cleanup_module(void)
{
    int i;
    dev_t devno = MKDEV(scull_major, scull_minor);

    /* Get rid of our char dev entries. */
    if (scull_devices) {
        for (i = 0; i < scull_nr_devs; i++) {
            scull_trim(scull_devices + i);
            cdev_del(&scull_devices[i].cdev);
        }
        kfree(scull_devices);
    }

    /* cleanup_module is never called if registering failed. */
    unregister_chrdev_region(devno, scull_nr_devs);
    printk(KERN_INFO "scull: cleanup success/n");
}


int scull_init_module(void)
{
    int result, i;
    dev_t dev = 0;

    /*
     * Get a range of minor numbers to work with, asking for a dynamic major
     * unless directed otherwise at load time.
     */
    if (scull_major) {
        dev = MKDEV(scull_major, scull_minor);
        result = register_chrdev_region(dev, scull_nr_devs, "scull");
    } else {
        result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
        scull_major = MAJOR(dev);
    }
    if (result < 0) {
        printk(KERN_WARNING "scull: can't get major %d/n", scull_major);
        return result;
    } else {
        printk(KERN_INFO "scull: get major %d success/n", scull_major);
    }

        /*
     * Allocate the devices. This must be dynamic as the device number can
     * be specified at load time.
     */
    scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
    if (!scull_devices) {
        result = -ENOMEM;
        goto fail;
    }
    memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

        /* Initialize each device. */
    for (i = 0; i < scull_nr_devs; i++) {
        scull_devices[i].quantum = scull_quantum;
        scull_devices[i].qset = scull_qset;
        mutex_init(&scull_devices[i].mutex);
        scull_setup_cdev(&scull_devices[i], i);
    }

    return 0; /* succeed */

  fail:
    scull_cleanup_module();
    return result;
}

module_init(scull_init_module);
module_exit(scull_cleanup_module);

知識點1 — 驅動分類

转载请注明:IAMCOOL » 從 0 開始學 Linux 驅動開發(一)

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x