我正在linux中实现我自己的系统调用。 它正在调用里面的重命名系统调用。 它使用用户参数(以下是代码)将代码传递给重命名。
这里是基本的代码:
int sys_mycall(const char __user * inputFile) { // // Code to generate my the "fileName" // // old_fs = get_fs(); set_fs(KERNEL_DS); ans = sys_renameat(AT_FDCWD,fileName,AT_FDCWD,inputFile); set_fs(old_fs); return ans; }
我在这里有两个疑问。
我正在使用old_fs = get_fs(); , set_fs(KERNEL_DS); 和set_fs(old_fs); 绕过实际调用sys_rename因为有错误。 我从这个问题得到了答案: 从内核分配用户空间内存 …这是一个正确的工作?
编辑:
如何下载Redhat Linux Enterprise kernel 2.6.18-128.el5的源代码?
哪些驱动程序在linux内核中使用usb鼠标?
linux内核如何检查sticky位是否设置
int sys_myfunc(const char __user * inputFileUser) { char inputFile[255]; int l = 0; while(inputFileUser[l] != ' ') L++; if(l==0) return -10; if(copy_from_user(inputFile,inputFileUser,l+1)< 0 ) return -20; // //GENERATE fileName here // // char fileName[255]; return sys_renameat(AT_FDCWD,inputFile,fileName); }
以下仍然返回-1。 为什么? 我将数据复制到内核空间。
在Ubuntu上的虚拟机上学习Linux内核编程?
从核心土地写文件
哪些Win32用户模式句柄可以在进程间共享?
我们可以修改limits.h吗? 会产生什么影响?
有关结构的基本问题
我想表明如何正确的方式来达到什么样的要求,但是我最初的回答太长了,我决定把解决方案放在一个单独的答案中。 我将把代码分成几部分,并解释每个片段的作用。
请记住,由于我们重用了内核代码,因此本文中的代码和生成的函数必须根据GPLv2许可证进行许可。
SYSCALL_DEFINE1(myfunc,const char __user *,oldname) {
在内核中,堆栈空间是一个稀缺资源。 你不创建本地数组; 你总是使用动态内存管理。 幸运的是,有一些非常有用的函数,如__getname() ,所以它是非常少的附加代码。 重要的是要记住释放你使用的任何内存。
由于这个系统调用基本上是一个rename的变体,我们重用了几乎所有的fs/namei.c:sys_renameat()代码。 首先是局部变量声明。 也有很多, 正如我所说的,堆栈在内核中是稀缺的,在任何系统调用函数中都看不到更多的局部变量:
struct dentry *old_dir,*new_dir; struct dentry *old_dentry,*new_dentry; struct dentry *trap; struct nameidata oldnd,newnd; char *from; char *to = __getname(); int error;
sys_renameat()的第一个改变是在char *to = __getname(); 已经在上面了。 它动态地分配PATH_MAX+1个字节,并且在不再需要之后必须使用__putname()来释放。 这是为文件或目录名称声明临时缓冲区的正确方法。
为了构建新的路径,我们还需要能够直接访问旧的名字( from )。 由于内核用户空间的限制,我们不能直接访问oldname 。 所以,我们创建它的内核拷贝:
from = getname(oldname); if (IS_ERR(from)) { error = PTR_ERR(from); goto exit; }
尽管许多C程序员被教导goto是邪恶的,但是这是例外:错误处理。 我们不需要记住所有的清理工作(而且我们至少需要做__putname(to) ),我们把清理放在函数的末尾,跳到正确的位置, exit最后一个。 error当然包含错误号。
在我们的函数的这一点上,我们可以from[0]到第一个' ' ,或from[PATH_MAX]到(包括),以先到者为准。 这是一个正常的内核数据,并且可以以任何C代码的普通方式进行访问。
您还预留了新名称的存储空间, to[0]直到并包括to[PATH_MAX] 。 请记住确保它也是使用 (in to[PATH_MAX] = ' '或更早的索引)终止的。
在为内容构建内容之后,我们需要执行路径查找。 不像renameat() ,我们不能使用user_path_parent() 。 然而,我们可以看看user_path_parent()做了什么,并且做了相同的工作 – 当然,我们需要适应自己的需要。 原来它只是调用do_path_lookup()进行错误检查。 所以,两个user_path_parent()调用和它们的错误检查可以被替换
error = do_path_lookup(AT_FDCWD,from,LOOKUP_PARENT,&oldnd); if (error) goto exit0; error = do_path_lookup(AT_FDCWD,to,&newnd); if (error) goto exit1;
请注意, exit0是在原始的renameat()找不到的新标签。 我们需要一个新的标签,因为在exit ,我们只需要; 但在exit0 ,我们有exit0 。 exit0之后,我们必须to , from , oldnd ,等等。
接下来,我们可以重用sys_renameat()的大部分内容。 它完成了重命名的所有工作。 为了节省空间,我将会忽略我所做的事情,因为你可以相信如果rename()起作用,它也可以工作。
error = -EXDEV; if (oldnd.path.mnt != newnd.path.mnt) goto exit2; old_dir = oldnd.path.dentry; error = -EBUSY; if (oldnd.last_type != LAST_norM) goto exit2; new_dir = newnd.path.dentry; if (newnd.last_type != LAST_norM) goto exit2; error = mnt_want_write(oldnd.path.mnt); if (error) goto exit2; oldnd.flags &= ~LOOKUP_PARENT; newnd.flags &= ~LOOKUP_PARENT; newnd.flags |= LOOKUP_RENAME_TARGET; trap = lock_rename(new_dir,old_dir); old_dentry = lookup_hash(&oldnd); error = PTR_ERR(old_dentry); if (IS_ERR(old_dentry)) goto exit3; /* source must exist */ error = -ENOENT; if (!old_dentry->d_inode) goto exit4; /* unless the source is a directory trailing slashes give -ENOTDIR */ if (!S_ISDIR(old_dentry->d_inode->i_mode)) { error = -ENOTDIR; if (oldnd.last.name[oldnd.last.len]) goto exit4; if (newnd.last.name[newnd.last.len]) goto exit4; } /* source should not be ancestor of target */ error = -EINVAL; if (old_dentry == trap) goto exit4; new_dentry = lookup_hash(&newnd); error = PTR_ERR(new_dentry); if (IS_ERR(new_dentry)) goto exit4; /* target should not be an ancestor of source */ error = -ENOTEMPTY; if (new_dentry == trap) goto exit5; error = security_path_rename(&oldnd.path,old_dentry,&newnd.path,new_dentry); if (error) goto exit5; error = vfs_rename(old_dir->d_inode,new_dir->d_inode,new_dentry);
在这一点上,所有的工作都已经完成了,只剩下上面代码所释放的锁,内存等等。 如果在这一点上一切都成功了, error == 0 ,并且我们全部清理。 如果我们遇到问题, error包含错误代码,我们已经跳到正确的标签来执行发生错误所需的清理。 如果vfs_rename()失败 – 它执行实际操作 – 我们都清理。
但是,与原始代码相比,我们from第一个( exit ) to ( exit0 )之后,紧接着dentry查找。 所以,我们需要把它们放到正确的位置(靠近最后,因为它们是先完成的,清理过程当然是按照相反的顺序):
exit5: dput(new_dentry); exit4: dput(old_dentry); exit3: unlock_rename(new_dir,old_dir); mnt_drop_write(oldnd.path.mnt); exit2: path_put(&newnd.path); exit1: path_put(&oldnd.path); exit0: putname(from); exit: __putname(to); return error; }
在这里,我们完成了。
当然,从sys_renameat()复制的部分中有很多细节需要考虑 – 就像我在其他答案中所说的那样,不应该像这样复制代码,而是将常见代码重构为辅助函数; 这使得维护更容易。 幸运的是,因为我们保留了来自renameat()所有检查 – 我们在复制了任何renameat()代码之前进行了路径操作 – 我们可以确定所有必要的检查都已完成。 就好像用户自己指定了被操作的路径,并且被称为renameat() 。
如果您在完成一些检查之后要进行修改,情况就会复杂得多。 你将不得不考虑这些检查是什么,你的修改如何影响他们,几乎总是重新做这些检查。
为了提醒任何读者,你不能只在自己的系统调用中创建一个文件名或任何其他字符串,然后调用另一个系统调用,原因是你刚刚创建的字符串驻留在内核用户空间的内核边界,而系统调用期望数据驻留在另一个用户空间。 而在x86上,你可能会意外地从内核一侧穿透边界,这并不意味着你应该这样做:有copy_from_user()和copy_to_user()及其衍生物,如strncpy_from_user() , 必须用于此目的。 这不是一个魔术来调用另一个系统调用,而是提供数据在哪里(在内核或用户空间)的问题。
嗯.. linux-3.6.2/fs/namei.c包含很多类似的情况。 例如, rename系统调用实际上被定义为
SYSCALL_DEFINE2(rename,oldname,newname) { return sys_renameat(AT_FDCWD,newname); }
换句话说,从另一个系统调用中调用一个系统调用是没有问题的。 问题是指针参数是用户空间指针,而你正在试图提供一个内核指针:你的fileName应该在用户空间中分配,而你的是在内核空间中。
正确的解决方案是从两个函数( fs/namei.c yours和sys_renameat()中sys_renameat()出通用代码,然后从两个系统调用中调用函数。 假设你并不sys_renameat它包含在上游 – 如果你是这样的话,那么它就是重构和重新思考时间 – 你可以简单地将sys_renameat的内容复制到你自己的函数中; 这不是那么大 熟悉这种文件系统操作所需的检查和锁定也是非常有用的一点。
为了解释问题和解决方案而编辑:
在一个非常真实的意义上,正常进程分配的内存( 用户空间内存)和内核分配的内存(内核空间)完全被内核用户空间隔离。
你的代码忽略了这个障碍,根本不应该工作。 (它可能在x86上有些作用,因为内核用户空间的障碍很容易从该体系结构的内核侧穿透)。你也可以使用一个256字节的堆栈作为文件名,这是一个不允许的:内核堆栈是资源非常有限,应该谨慎使用。
正常的进程( 用户空间进程 )不能访问任何内核内存。 你可以试试,这是行不通的。 这是障碍存在的原因。 (有些嵌入式系统的硬件根本不支持这样的屏障,但是为了讨论的目的,我们忽略这些嵌入式系统,并且要记住,即使在x86上,屏障很容易从内核一侧穿透,但并不意味着它不在那里,不要成为家伙,因为它似乎为你工作,它是正确的。)
屏障的本质是,在大多数架构上, 内核也存在屏障 。
为了帮助内核程序员,指向跨越屏障进入用户空间的指针被标记为__user 。 这意味着你不能只取消它们,并期望它们工作。 你需要使用copy_from_user()和copy_to_user() 。 这不仅仅是系统调用参数:当你从内核访问用户空间数据时,你需要使用这两个函数。
所有的系统调用在用户空间数据上工作。 你看到的每个指针都是(或者应该是!)标记为__user 。 每个系统调用都会从用户空间访问数据。
你的问题是,你试图提供内核空间数据, inputFile ,一个系统调用。 它不会工作,因为系统调用将总是试图通过屏障,但inputFile是在屏障的同一侧!
将inputFile复制到屏障的另一边确实没有任何理智的方法。 我的意思是,当然有办法可以做,而且不是那么困难,但它不会是理智的。
所以,让我们探索一下我上面所描述的正确的解决方案,哪一个已经被拒绝了。
首先,让我们来看看在当前的(3.6.2)Linux内核中,重renameat系统调用实际上是什么样的(请记住,这个代码是在GPLv2下授权的)。 rename系统调用只需使用sys_renameat(AT_FDCWD,newname)调用它。 我将插入我对代码所做的解释:
SYSCALL_DEFINE4(renameat,int,olddfd,newdfd,newname) { struct dentry *old_dir,newnd; char *from; char *to; int error;
在内核中,堆栈是有限的资源。 你可以使用相当多的变量,但任何本地数组将是一个严重的问题。 上面的局部变量列表几乎是您在典型的系统调用中看到的最大的变量列表。
error = user_path_parent(olddfd,&oldnd,&from); if (error) goto exit;
注意:在这之后,旧的目录和路径必须在使用后通过调用path_put(&oldnd.path); putname(from);来释放path_put(&oldnd.path); putname(from); path_put(&oldnd.path); putname(from); 。
error = user_path_parent(newdfd,newname,&newnd,&to); if (error) goto exit1;
注意:在这之后,新的目录和路径必须在使用后通过调用path_put(&newnd.path); putname(to);来释放path_put(&newnd.path); putname(to); path_put(&newnd.path); putname(to); 。
error = -EXDEV; if (oldnd.path.mnt != newnd.path.mnt) goto exit2;
old_dir = oldnd.path.dentry; error = -EBUSY; if (oldnd.last_type != LAST_norM) goto exit2; new_dir = newnd.path.dentry; if (newnd.last_type != LAST_norM) goto exit2;
并且包含这些目录的挂载必须是可写的。 请注意,如果成功,这将对挂载应用锁,然后必须在系统调用返回之前始终与mnt_drop_write(oldnd.path.mnt)调用进行配对。
error = mnt_want_write(oldnd.path.mnt); if (error) goto exit2;
接下来,nameidata查找标志被更新以反映目录已经是已知的:
oldnd.flags &= ~LOOKUP_PARENT; newnd.flags &= ~LOOKUP_PARENT; newnd.flags |= LOOKUP_RENAME_TARGET;
接下来,这两个目录在重命名期间被锁定。 这必须与相应的解锁呼叫unlock_rename(new_dir,old_dir)配对。
trap = lock_rename(new_dir,old_dir);
接下来,查找实际存在的文件。 如果这是成功的,则必须通过调用dput(old_dentry)来释放dentry:
old_dentry = lookup_hash(&oldnd); error = PTR_ERR(old_dentry); if (IS_ERR(old_dentry)) goto exit3; /* source must exist */ error = -ENOENT; if (!old_dentry->d_inode) goto exit4; /* unless the source is a directory trailing slashes give -ENOTDIR */ if (!S_ISDIR(old_dentry->d_inode->i_mode)) { error = -ENOTDIR; if (oldnd.last.name[oldnd.last.len]) goto exit4; if (newnd.last.name[newnd.last.len]) goto exit4; } /* source should not be ancestor of target */ error = -EINVAL; if (old_dentry == trap) goto exit4;
新文件名的条目也被查找(毕竟它可能存在)。 再次,如果成功的话,这个dentry也必须在之后使用dput(new_dentry)释放:
new_dentry = lookup_hash(&newnd); error = PTR_ERR(new_dentry); if (IS_ERR(new_dentry)) goto exit4; /* target should not be an ancestor of source */ error = -ENOTEMPTY; if (new_dentry == trap) goto exit5;
在这一点上,功能已经确定,一切正常。 接下来,它必须通过调用security_path_rename(struct path *old_dir,struct dentry *old_dentry,struct path *new_dir,struct dentry *new_dentry)来检查操作是否可以继续(相对于访问模式等security_path_rename(struct path *old_dir,struct dentry *new_dentry) 。 (用户空间进程的身份详细信息保持在current 。)
error = security_path_rename(&oldnd.path,new_dentry); if (error) goto exit5;
如果没有人反对重命名,那么实际的重命名可以使用vfs_rename(struct inode *old_dir,struct inode *new_dir,struct dentry *new_dentry) :
error = vfs_rename(old_dir->d_inode,new_dentry);
在这一点上,所有的工作完成(成功,如果error是零),唯一剩下的就是释放各种查找
exit5: dput(new_dentry); exit4: dput(old_dentry); exit3: unlock_rename(new_dir,old_dir); mnt_drop_write(oldnd.path.mnt); exit2: path_put(&newnd.path); putname(to); exit1: path_put(&oldnd.path); putname(from); exit: return error; }
这就是重命名操作。 正如你所看到的,没有显式的copy_from_user()被看到。 getname()调用getname_flags() ,它调用getname_flags() ,它做到了。 如果您忽略所有必要的检查,则归结为
char *result = __getname(); /* Reserve PATH_MAX+1 bytes of kernel memory for one file name */ in len; len = strncpy_from_user(result,old/newname,PATH_MAX); if (len <= 0) { __putname(result); /* An error occurred,abort! */ } if (len >= PATH_MAX) { __putname(result); /* path is too long,abort! */ } /* Finally,add it to the audit context for the current process. */ audit_getname(result);
并且在不再需要之后,
putname(result);
所以,很简单,你的问题没有简单的解决方案。 没有任何单一的函数调用会神奇地让你的系统调用工作。 你将不得不重写它,看看这些东西在fs/namei.c是如何正确完成的。 这并不困难,但是你必须小心谨慎地去做 – 而且最重要的是,接受一个“只是试图用简单的改变就可以完成这个简单的事情”的方法就不会起作用。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。