測試環境:Ubuntu 14.04+Kernel 4.4.0-31linux
關鍵詞:KERNEL_DS、USER_DS、get_fs()、set_fs()、addr_limit、access_ok。shell
參考代碼:https://elixir.bootlin.com/linux/v4.4/source函數
內核空間和用戶空間交換數據的方式有不少,好比用戶空間發起的系統調用、proc、虛擬文件系統等。測試
內核空間主動發起的有get_user/put_user、信號、netlink等。ui
這裏介紹get_user/put_user的使用以及背後的原理。spa
要讓內核空間主動發起,須要建立一個module,而後插入到內核中。線程
從內核中發起建立kernel_file,並寫入內容。3d
最後從用戶空間進行驗證。指針
首先,編寫module源碼:code
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> static char buf[] ="來自內核的訪問\n"; static char buf1[32]; int __init test_init(void) { struct file *fp; mm_segment_t fs; loff_t pos; printk("test enter\n"); fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644); if (IS_ERR(fp)){ printk("create file error\n"); return -1; } fs =get_fs(); set_fs(KERNEL_DS); pos =0; vfs_write(fp,buf, sizeof(buf), &pos); pos =0; vfs_read(fp,buf1, sizeof(buf), &pos); printk("Write contet=%s\n",buf1); filp_close(fp,NULL); set_fs(fs); return 0; } void __exit test_exit(void) { printk("test exit\n"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL");
編寫Makefile文件:
obj-m :=read_userspace.o #要生成的模塊名 read_userspace-objs:= read_userspace_file.o #生成這個模塊名所須要的目標文件 KDIR := /lib/modules/`uname -r`/build PWD := $(shell pwd) default: make -C $(KDIR) M=$(PWD) modules clean: rm -rf *.o *.cmd *.ko *.mod.c .tmp_versions Module.symvers modules.order
執行make命令,就能夠獲得read_userspace.ko文件。
sudo insmod read_userspace.ko-----------------插入模組
sudo lsmod | grep read_userspace--------------驗證是否插入成功
sudo rmmod read_userspace----------------------移除模組
測試結果以下,能夠看出kernel_file是由root用戶建立的。
能夠看出內容符合預期。
fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644);---------------------建立用戶空間文件,獲取文件句柄。 if (IS_ERR(fp)){ printk("create file error\n"); return -1; } fs =get_fs();----------------------------------------------------------------------------------------獲取當前線程的thread_info->addr_limit。 set_fs(KERNEL_DS);-----------------------------------------------------------------------------------將能訪問的空間thread_info->addr_limit擴大到KERNEL_DS。 pos =0; vfs_write(fp,buf, sizeof(buf), &pos);----------------------------------------------------------------調用vfs_write寫內容 pos =0; vfs_read(fp,buf1, sizeof(buf), &pos);----------------------------------------------------------------調用vfs_read讀取內容 printk("Write contet=%s\n",buf1); filp_close(fp,NULL);---------------------------------------------------------------------------------關閉文件 set_fs(fs);------------------------------------------------------------------------------------------將thread_info->addr_limit切換回原來值
有下面代碼可知KERNEL_DS範圍很大,到0xffffffffffffffff。
而USER_DS範圍較小,到0x7ffffffff000。
由Linux內存分佈圖可知,KERNEL_DS意味着能夠訪問整個內存全部空間,USER_DS只能訪問用戶空間內存。
經過set_fs能夠改變thread_info->addr_limit的大小。
/* * For historical reasons, the following macros are grossly misnamed: */ #define KERNEL_DS ((mm_segment_t) { ~0UL }) /* cf. access_ok() */ #define USER_DS ((mm_segment_t) { TASK_SIZE-1 }) /* cf. access_ok() */ #define VERIFY_READ 0 #define VERIFY_WRITE 1 #define get_ds() (KERNEL_DS) #define get_fs() (current_thread_info()->addr_limit) #define set_fs(x) (current_thread_info()->addr_limit = (x)) #define TASK_SIZE DEFAULT_TASK_SIZE
將代碼修改一下,不進行addr_limit擴大,看看結果如何。
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> static char buf[] ="來自內核的訪問\n"; static char buf1[32]; int __init test_init(void) { struct file *fp; mm_segment_t fs; loff_t pos; int ret; printk("KERNEL_DS=0x%llx USER_DS=0x%llx get_fs()=0x%llx\n", KERNEL_DS, USER_DS, get_fs()); fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644); if (IS_ERR(fp)){ printk("create file error\n"); return -1; } fs =get_fs(); //set_fs(KERNEL_DS); pos =0; printk("fp=%p, buf=%p get_fs()=0x%llx\n", fp, buf, get_fs()); ret = vfs_write(fp,buf, sizeof(buf), &pos); printk("ret=%d\n", ret); pos =0; printk("fp=%p, buf1=%p\n", fp, buf1); ret = vfs_read(fp,buf1, sizeof(buf), &pos); printk("ret=%d Write contet=%s\n", ret, buf1); filp_close(fp,NULL); //set_fs(fs); return 0; } void __exit test_exit(void) { printk("test exit\n"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL");
執行結果以下,能夠看出fp、buf、buf1都位於內核空間。而當前空間的get_fs()爲0x7ffffffff000,這些地址都超出當前空間。
因此vfs_read和vfs_write返回值都是-14,即「Bad address」。
[49001.240705] KERNEL_DS=0xffffffffffffffff USER_DS=0x7ffffffff000 get_fs()=0x7ffffffff000 [49001.240713] fp=ffff8800cae06900, buf=ffffffffc0305000 get_fs()=0x7ffffffff000 [49001.240714] ret=-14 [49001.240715] fp=ffff8800cae06900, buf1=ffffffffc03053c0 [49001.240716] ret=-14 Write contet= [49013.464812] test exit
簡單看一下vfs_write和vfs_read,二者都調用access_ok對地址合法性進行檢查,嚴禁addr大於當前get_fs()。
此處buf和buf1都不知足條件,因此返回-EFAULT。
#define __access_ok(addr, size, segment) \ ({ \ __chk_user_ptr(addr); \ (likely((unsigned long) (addr) <= (segment).seg) \ && ((segment).seg == KERNEL_DS.seg \ || likely(REGION_OFFSET((unsigned long) (addr)) < RGN_MAP_LIMIT))); \ }) #define access_ok(type, addr, size) __access_ok((addr), (size), get_fs()) ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { ... if (unlikely(!access_ok(VERIFY_READ, buf, count))) return -EFAULT; ... } ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { ... if (unlikely(!access_ok(VERIFY_WRITE, buf, count))) return -EFAULT; ... }
將測試代碼紅色部分打開,擴大addr_limit空間。
能夠看出當前thread_info->addr_limit變成了0xffffffffffffffff。
因此vfs_write和vfs_read的access_ok檢查得以經過,程序獲得正確執行。
[48937.547119] KERNEL_DS=0xffffffffffffffff USER_DS=0x7ffffffff000 get_fs()=0x7ffffffff000 [48937.547138] fp=ffff8800c8300c00, buf=ffffffffc02f3000 get_fs()=0xffffffffffffffff [48937.547155] ret=23 [48937.547158] fp=ffff8800c8300c00, buf1=ffffffffc02f33c0 [48937.547164] ret=23 Write contet=\xffffffe6\xffffff9d\xffffffa5\xffffff9d\xffffffa5\xffffffe8\xffffff87\xffffffaa\xffffff87\xffffffaa\xffffffe5\xffffff86\xffffff85\xffffff86\xffffff85\xffffffe6\xffffffa0\xffffffb8\xffffffa0\xffffffb8\xffffffe7\xffffff9a\xffffff84\xffffff9a\xffffff84\xffffffe8\xffffffae\xffffffbf\xffffffae\xffffffbf\xffffffe9\xffffff97\xffffffae\xffffff97\xffffffae [48937.547164] [48940.600703] test exit
只有使用上面的方法,才能在內核中使用open,write等的系統調用。
其實這樣作的主要緣由是open,write的參數在用戶空間,在這些系統調用的實現裏須要對參數進行檢查,就是檢查它的參數指針地址是否是用戶空間的。
系統調用原本是提供給用戶空間的程序訪問的,因此,對傳遞給它的參數(好比上面的buf、buf1),它默認會認爲來自用戶空間。
在vfs_write()函數中,爲了保護內核空間,通常會用get_fs()獲得的值來和USER_DS進行比較,從而防止用戶空間程序「蓄意」破壞內核空間。
爲了解決這個問題, set_fs(KERNEL_DS)將其能訪問的空間限制擴大到KERNEL_DS,這樣就能夠在內核順利使用系統調用了!
內核使用系統調用參數確定是內核空間,爲了避免讓這些系統調用檢查參數因此必須設置 set_fs(KERNEL_DS)才能使用該系統調用。
vfs_write的流程可調用access_ok,而access_ok會判斷訪問的buf是否在0~addr_limit之間,如何是就ok;不然-EFAULT,這顯然是爲用戶準備的檢查。
addr_limit通常設爲USER_DS,在內核空間,buf確定>USER_DS,必須修改addr_limit,這就是set_fs的由來。