Linux内核访问用户空间文件:get_fs()/set_fs()的使用
测试环境:Ubuntu 14.04+Kernel 4.4.0-31
关键词:KERNEL_DS、USER_DS、get_fs()、set_fs()、addr_limit、access_ok。
参考代码:https://elixir.bootlin.com/linux/v4.4/source
内核空间和用户空间交换数据的方式有很多,比如用户空间发起的系统调用、proc、虚拟文件系统等。
内核空间主动发起的有get_user/put_user、信号、netlink等。
这里介绍get_user/put_user的使用以及背后的原理。
1. 构造测试环境:Ubuntu下创建module
要让内核空间主动发起,需要创建一个module,然后插入到内核中。
从内核中发起创建kernel_file,并写入内容。
最后从用户空间进行验证。
1.1 测试源码
首先,编写module源码:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h> static char buf[] ="来自内核的访问\n";
static char buf1[]; 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,);
if (IS_ERR(fp)){
printk("create file error\n");
return -;
}
fs =get_fs();
set_fs(KERNEL_DS);
pos =;
vfs_write(fp,buf, sizeof(buf), &pos);
pos =;
vfs_read(fp,buf1, sizeof(buf), &pos);
printk("Write contet=%s\n",buf1);
filp_close(fp,NULL);
set_fs(fs);
return ;
}
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
1.2 编译
执行make命令,就可以得到read_userspace.ko文件。
1.3 测试
sudo insmod read_userspace.ko-----------------插入模组
sudo lsmod | grep read_userspace--------------验证是否插入成功
sudo rmmod read_userspace----------------------移除模组
测试结果如下,可以看出kernel_file是由root用户创建的。
可以看出内容符合预期。
3. 代码分析
fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,);---------------------创建用户空间文件,获取文件句柄。
if (IS_ERR(fp)){
printk("create file error\n");
return -;
}
fs =get_fs();----------------------------------------------------------------------------------------获取当前线程的thread_info->addr_limit。
set_fs(KERNEL_DS);-----------------------------------------------------------------------------------将能访问的空间thread_info->addr_limit扩大到KERNEL_DS。
pos =;
vfs_write(fp,buf, sizeof(buf), &pos);----------------------------------------------------------------调用vfs_write写内容
pos =;
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切换回原来值
4. 原理
4.1 set_fs和get_fs
有下面代码可知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
4.2 vfs_write和vfs_read对addr_limit的检查
将代码修改一下,不进行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[]; 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,);
if (IS_ERR(fp)){
printk("create file error\n");
return -;
}
fs =get_fs();
//set_fs(KERNEL_DS);
pos =;
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 =;
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 ;
}
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=
[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
5. 小结
只有使用上面的方法,才能在内核中使用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的由来。
Linux内核访问用户空间文件:get_fs()/set_fs()的使用的更多相关文章
- linux内存(三)内核与用户空间交互
来自网址http://www.kerneltravel.net/jiaoliu/005.htm 用户程序和内核的信息交换是双向的,也就是说既可以主动从用户空间向内核空间发送信息,也可以从内核空间向用户 ...
- Linux启动时间优化-内核和用户空间启动优化实践
关键词:initcall.bootgraph.py.bootchartd.pybootchart等. 启动时间的优化,分为两大部分,分别是内核部分和用户空间两大部分. 从内核timestamp 0.0 ...
- Linux环境下用户空间与内核空间数据的交换方式
在linux环境开发过程中,经常会需要在用户空间和内核空间之间进行数据交换. 介绍了 Linux 系统下用户空间与内核空间数据交换的几种方式 第一节:使用procfs实现内核交互简明教程(1) 第二节 ...
- Linux内存管理--用户空间和内核空间【转】
本文转载自:http://blog.csdn.net/yusiguyuan/article/details/12045255 关于虚拟内存有三点需要注意: 4G的进程地址空间被人为的分为两个部分--用 ...
- linux内存管理--用户空间和内核空间
关于虚拟内存有三点需要注意: 4G的进程地址空间被人为的分为两个部分--用户空间与内核空间.用户空间从0到3G(0xc0000000),内核空间占据3G到4G.用户进程通常情况下只能访问用户空间的虚拟 ...
- linux 内核与用户空间通信之netlink使用方法
转自:http://blog.csdn.net/haomcu/article/details/7371835 Linux中的进程间通信机制源自于Unix平台上的进程通信机制.Unix的两大分支AT&a ...
- Linux内核访问外设I/O--动态映射(ioremap)和静态映射(map_desc) (转载)
[转](转)Linux内核访问外设I/O资源的方式-静态映射(map_desc)方式 Linux内核访问外设I/O资源的方式 Author: Dongas Date: 08-08-02 我们知道默认外 ...
- Linux内核态用户态相关知识 & 相互通信
http://www.cnblogs.com/bakari/p/5520860.html 内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境. 系统调用是操作系统的最小功能 ...
- linux 内核与用户空间通信机制netlink初探
1.Linux进程概述 Linux中的进程间通信机制源自于Unix平台上的进程通信机制.Unix的两大分支AT&T Unix和BSD Unix在进程通信实现机制上各有所不同,前者形成了运行 ...
随机推荐
- Python 利用字典实现类似 java switch case 功能
def add(): print('add') def sub(): print('sub') def exit(): print('exit') choice = { '1' : add, '2' ...
- 洛谷P2178 [NOI2015]品酒大会(后缀自动机 线段树)
题意 题目链接 Sol 说一个后缀自动机+线段树的无脑做法 首先建出SAM,然后对parent树进行dp,维护最大次大值,最小次小值 显然一个串能更新答案的区间是\([len_{fa_{x}} + 1 ...
- 【读书笔记】iOS-属性中的内存管理参数
一,assign 代表设置时候直接赋值,而不是复制或者保留它. 二,retain. 会在赋值的时候把新值保留.此属性只能用于Object-C对象类型. 三,copy 在赋值时,将新值复制一份,复制工作 ...
- docker 安装 gitlab
基于Docker部署GitLab环境搭建 建议虚拟机内存2G以上 1.下载镜像文件 docker pull beginor/gitlab-ce:11.0.1-ce.0 注意:一定要配置阿里云的加速镜像 ...
- python自动化开发-8
进程与线程 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程. 线程是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位. ...
- NoHttp封装--03 cookie
NoHttp请求自动维持Cookie: 1.支持Session.Cookie.临时Cookie的位置. 2.支持App重启.关机开机后继续持久化维持. 3.提供了接口,允许开发者监听Coo ...
- Linux下修改IP、DNS、路由命令行设置
本文最后修改时间:20180313 一.快速修改,重启后设置就没了 ifconfig eth0 192.168.1.22 netmask 255.255.255.0 up route add defa ...
- 盐城 - 开设IT公司的好地方
盐城:位于江苏省北部,这一好像只能算三线的城市,不久前当选为“国家中心城市”.在全国仅有的50城市中名列34.它可成为发展IT产业的好地方. (1)人才济济.从这里走出去的高级人才数不胜数,留在这里的 ...
- [20170615]执行dbms_sqldiag.dump_trace看执行计划.txt
[20170615]执行dbms_sqldiag.dump_trace看执行计划.txt --//上午在想查看10053执行计划时使用包时出现如下提示: SCOTT@book> @ &r ...
- 测试中Android与IOS分别关注的点
主要从本身系统的不同点.系统造成的不同点.和注意的测试点做总结 1.自身不同点 研发商:Adroid是google公司做的手机系统,IOS是苹果公司做的手机系统 开源程度:Android是开源的,IO ...