從 0 開始學 Linux 驅動開發(一)
作者:Hcamael@知道創宇404實驗室
最近在搞IoT的時候,因為沒有設備,模擬跑固件經常會缺/dev/xxx
,所以我就開始想,我能不能自己寫一個驅動,讓固件能跑起來?因此,又給自己挖了一個很大坑,不管最後能不能達到我的初衷,能學到怎麼開發Linux驅動,也算是有很大的收穫了。
前言
我寫的這個系列以實踐為主,不怎麼談理論,理論可以自己去看書,我是通過《Linux Device Drivers》這本書學的驅動開發,Github上有這本書中講解的實例的代碼[1]。
雖然我不想談太多理論,但是關於驅動的基本概念還是要有的。Linux系統分為內核態和用戶態,只有在內核態才能訪問到硬體設備,而驅動可以算是內核態中提供出的API,供用戶態的代碼訪問到硬體設備。
有了基本概念以後,我就產生了一系列的問題,而我就是通過我的這一系列的問題進行學習的驅動開發:
- 一切代碼的學習都是從Hello World開始的,怎麼寫一個Hello World的程序?
- 驅動是如何在/dev下生成設備文件的?
- 驅動怎麼訪問實際的硬體?
- 因為我畢竟是搞安全的,我會在想,怎麼獲取系統驅動的代碼?或者沒有代碼那能逆向驅動嗎?驅動的二進位文件儲存在哪?以後有機會可能還可以試試搞驅動安全。
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
");
return 0;
}
void hello_exit(void)
{
printk(KERN_INFO "Goodbye World
");
}
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_LICENSE
和MODULE_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
從安全的角度考慮,現在的內核都是假設模塊為不可信的,需要使用可信的證書對模塊進行簽名,才能載入模塊
解決方法用兩種:
- 進入BIOS,關閉UEFI的Secure Boot
- 向內核添加一個自簽名證書,然後使用證書對驅動模塊進行簽名,參考[3]
查看結果
在/dev下增加設備文件
同樣先提供一份代碼,然後講解這份實例代碼[4]
#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; /* dont 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
", 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
", 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: /* cant 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
", 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
");
}
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: cant get major %d
", scull_major);
return result;
} else {
printk(KERN_INFO "scull: get major %d success
", 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 -- 驅動分類
驅動分為3類,字元設備、塊設備和網口介面,上面代碼舉例的是字元設備,其他兩種,之後再說。
如上圖所示,brw-rw----
許可權欄,b開頭的表示塊設備(block),c開頭的表示字元設備(char)
知識點2 -- 主次編號
主編號用來區分驅動,一般主編號相同的表示由同一個驅動程序控制。
一個驅動中能創建多個設備,用次編號來區分。
主編號和次編號一起,決定了一個驅動設備。
如上圖所示,
brw-rw---- 1 root disk 8, 0 Dec 17 13:02 sda
brw-rw---- 1 root disk 8, 1 Dec 17 13:02 sda1
設備sda
和sda1
的主編號為8,一個此編號為0一個此編號為1
知識點3 -- 驅動是如何提供API的
在我的概念中,驅動提供的介面是/dev/xxx
,在Linux下Everything is File
,所以對驅動設備的操作其實就是對文件的操作,所以一個驅動就是用來定義,打開/讀/寫/......一個/dev/xxx
將會發生啥,驅動提供的API也就是一系列的文件操作。
有哪些文件操作?都被定義在內核<linux/fs.h>
[5]頭文件中,file_operations
結構體
上面我舉例的代碼中:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.open = scull_open,
.release = scull_release,
};
我聲明了一個該結構體,並賦值,除了owner
,其他成員的值都為函數指針
之後我在scull_setup_cdev
函數中,使用cdev_add
向每個驅動設備,註冊該文件操作結構體
比如我對該驅動設備執行open操作,則會去執行scull_open
函數,相當於hook了系統調用中的open
函數
知識點4 -- 在/dev下生成相應的設備
對上面的代碼進行編譯,得到scull.ko,然後對其進行簽名,最後使用insmod
載入進內核中
查看是否成功載入:
雖然驅動已經載入成功了,但是並不會在/dev目錄下創建設備文件,需要我們手動使用mknod
進行設備鏈接:
總結
在該實例中,並沒有涉及到對實際物理設備的操作,只是簡單的使用kmalloc
在內核空間申請一塊內存。代碼細節上的就不做具體講解了,都可以通過查頭文件或者用Google搜出來。
再這裡分享一個我學習驅動開發的方法,首先看書把基礎概念給弄懂,細節到需要用到的時候再去查。
比如,我不需要知道驅動一共能提供有哪些API(也就是file_operations結構都有啥),我只要知道一個概念,驅動提供的API都是一些文件操作,而文件操作,目前我只需要open, close, read, write
,其他的等有需求,要用到的時候再去查。
參考
- https://github.com/jesstess/ldd4
- https://raw.githubusercontent.com/Hcamael/Linux_Driver_Study/master/hello.c
- https://jin-yang.github.io/post/kernel-modules.html
- https://raw.githubusercontent.com/Hcamael/Linux_Driver_Study/master/scull.c
- https://raw.githubusercontent.com/torvalds/linux/master/include/linux/fs.h
本文由 Seebug Paper 發布,如需轉載請註明來源。
歡迎關注我和專欄,我將定期搬運技術文章~
也歡迎訪問我們:知道創宇雲安全
推薦閱讀: