kernel_write/kernel_read 和 vfs_write/vfs_read 的区别

文章分析了Linux内核中的vfs_write和kernel_write函数,讨论了它们的区别,以及kernel_write如何通过set_fs改变内存地址检查规则。重点讲解了access_ok函数的工作原理,以及USER_DS和KERNEL_DS在内存空间验证中的作用。
摘要由CSDN通过智能技术生成

Linux内核读写文件有两个常用的函数,分别是vfs_write/vfs_read、kernel_write/kernel_read,两个也都内核导出函数(EXPORT_SYMBOL)。两者有什么区别呢?分别在什么场景下使用?

两者都在 kernel/fs/read_write.c 中实现,我们直接打开源码(基于kernel5.4)分析下。

 1、先来看看 vfs_write。

第二个参数 buf 前面有__user修饰符,这就要求 buf 指针应该指向用空空间的内存,如果传递内核空间的指针,函数会返回失败-EFAULT。具体地内存地址检测是在 access_ok() 实现。

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;

	if (unlikely(!access_ok(buf, count)))
		return -EFAULT;

	ret = rw_verify_area(WRITE, file, pos, count);
	if (!ret) {

		file_start_write(file);
		ret = __vfs_write(file, buf, count, pos);
		if (ret > 0) {
			fsnotify_modify(file);
			add_wchar(current, ret);
		}
		inc_syscw(current);
		file_end_write(file);
	}

	return ret;
}

 vfs_write内部会调用__vfs_write,后者会调用具体的文件系统的对应接口。

static ssize_t __vfs_write(struct file *file, const char __user *p,
			   size_t count, loff_t *pos)
{
	if (file->f_op->write)
		return file->f_op->write(file, p, count, pos);
	else if (file->f_op->write_iter)
		return new_sync_write(file, p, count, pos);
	else
		return -EINVAL;
}

2、先看看 kernel_write

可以看到,kernel_write内部也是调用了vfs_write,但在前后分别调用了set_fs()。该函数的作用是改变kernel对内存地址检查的处理方式,参数有两个取值:USER_DS(32位为0x3FFFFFFF),KERNEL_DS(0x00000000),分别代表用户空间和内核空间。默认情况下,access_ok()会检验将要操作的地址范围是否在当前进程的用户地址空间中,要使内核空间的内存指针也能校验通过,就需要使用set_fs(KERNEL_DS)进行设置。

ssize_t kernel_write(struct file *file, const void *buf, size_t count,
			    loff_t *pos)
{
	mm_segment_t old_fs;
	ssize_t res;

	old_fs = get_fs();
	set_fs(KERNEL_DS);
	/* The cast to a user pointer is valid due to the set_fs() */
	res = vfs_write(file, (__force const char __user *)buf, count, pos);
	set_fs(old_fs);

	return res;
}

3、分析下access_ok() 的原理

如上面描述,access_ok()默认情况下会检验将要操作的地址范围是否在当前进程的用户地址空间中,两个参数分别表示要操作的内存地址和大小,返回0表示校验通过。

其真正的实现体为 __range_ok(),后者的核心代码为汇编程序。

#define access_ok(addr, size)	(__range_ok(addr, size) == 0)

#define __range_ok(addr, size) ({ \
	unsigned long flag, roksum; \
	__chk_user_ptr(addr);	\
	__asm__(".syntax unified\n" \
		"adds %1, %2, %3; sbcscc %1, %1, %0; movcc %0, #0" \
		: "=&r" (flag), "=&r" (roksum) \
		: "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
		: "cc"); \
	flag; })

汇编程序主要有3条语句,解析如下:

第一条:adds %1, %2, %3;  表示 rosum = addr + size。这个操作影响进位标志C,当进位了(C=1),则后面两条指令都不执行,返回值 flag 就为初始值 current_thread_info()->addr_limit;当没进位(C=0),则继续执行后面两条指令。

adds %1, %2, %3; //%1、%2、%3分别代表rosum、addr、size

第二条: sbcscc %1, %1, %0;  表示 rosum = rosum - flag,也影响进位标志C。也就是说如果 addr + size) >= (current_thread_info()->addr_limit) ,则C=1,则后面的指令都不执行,返回值 flag 同为初始值;否则 C=0,继续执行后面的指令。

sbcscc %1, %1, %0;

第三条: movcc %0, #0  表示给flag赋值0

movcc %0, #0

综上所述:__range_ok宏其实等价于:
如果(addr + size) >= (current_thread_info()->addr_limit) ,返回非0,校验未通过。
如果(addr + size) < (current_thread_info()->addr_limit),返回0,校验通过。

而set_fs()就是设置current_thread_info()->addr_limit的值为USER_DS(32位为0x3FFFFFF),KERNEL_DS(0x00000000),所以当set_fs(KERNEL_DS),返回值永远是0。

  • 50
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值