如何从linux 3.5.4中的自定义系统调用中调用系统调用

时间:2012-10-14 00:19:28

标签: c linux filesystems kernel system-calls

我在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;

}

我有两个疑问。

  1. 我正在使用old_fs = get_fs();set_fs(KERNEL_DS);set_fs(old_fs);来破解对sys_rename的实际调用,因为出现了错误。我从这个问题得到了答案:allocate user-space memory from kernel ......这是一个正确的解决方法吗?
  2. 如何从系统调用中调用系统调用
  3. 修改

    int sys_myfunc(const char __user * inputFileUser)   {
    
    
        char inputFile[255];
        int l = 0;
        while(inputFileUser[l] != '\0') 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, AT_FDCWD, fileName);
    
    }
    

    以下仍然返回-1。为什么?我将数据复制到内核空间。

2 个答案:

答案 0 :(得分:3)

我想准确地说明如何正确地实现足够的想法,但我原来的答案变得太长了,我决定将解决方案放在一个单独的答案中。我将代码分成几部分,并解释每个片段的作用。

请记住,由于我们重用了内核代码,因此本文中的代码和生成的函数必须根据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()释放。这是为文件或目录名声明临时缓冲区的正确方法。

要构建新路径(to),我们还需要能够直接访问旧名称(from)。由于内核 - 用户空间障碍,我们不能直接访问oldname。因此,我们创建了一个内核副本:

    from = getname(oldname);
    if (IS_ERR(from)) {
        error = PTR_ERR(from);
        goto exit;
    }

尽管许多C程序员都被教导goto是邪恶的,但这是例外:错误处理。我们不需要记住我们需要做的所有清理(而且我们已经需要至少做__putname(to)),我们将清理放在函数的末尾,并跳到正确的点,{{1}成为最后一个。当然,exit会保留错误编号。

在我们的功能的这一点上,我们可以访问error直至第一个from[0],或最多(包括)'\0',以先到者为准。它是一个普通的内核端数据,可以用任何C代码中的普通方式访问。

您还将新名称的内存保留为from[PATH_MAX],直至并包括to[0]。请务必确保使用to[PATH_MAX](在\0或更早的索引中)终止它。

构建to[PATH_MAX] = '\0'的内容后,我们需要进行路径查找。与to不同,我们无法使用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, LOOKUP_PARENT, &newnd); if (error) goto exit1; 是原始exit0中找不到的新标签。我们需要一个新标签,因为在renameat(),我们只有exit;但在to,我们同时拥有exit0to。在from之后,我们有exit0tofrom,依此类推。

接下来,我们可以重复使用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, old_dentry, new_dir->d_inode, new_dentry); ,我们会进行全部清理。如果我们遇到问题,error == 0包含错误代码,我们跳转到正确的标签,对发生错误的点进行必要的清理。如果error失败 - 它执行实际操作 - 我们都会进行清理。

但是,与原始代码相比,我们在{({1}})之后首先获得vfs_rename()fromexit,然后是dentry查找。因此,我们需要将它们释放到正确的位置(接近最后,因为它们是先完成的。当然,清理会以相反的顺序进行):

to

我们已经完成了。

当然,我们从exit0复制的部分中有很多细节需要考虑 - 就像我在另一个答案中所说的那样,你不应该像这样复制代码,而是重构共同的代码到辅助函数;这使维护更容易。幸运的是,因为我们保留了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()

如果您在完成一些检查后进行修改,情况就会复杂得多。您必须考虑这些检查是什么,您的修改如何影响它们,并且几乎总是重新进行这些检查。

为了提醒任何读者,您不能在自己的系统调用中创建文件名或任何其他字符串然后调用另一个系统调用的原因是您刚刚创建的字符串驻留在内核用户空间边界的内核端,而系统调用期望数据驻留在另一个用户空间端。虽然在x86上你可能会意外地从内核方面刺穿边界,但这并不意味着你应该这样做:renameat()renameat()及其衍生物如copy_from_user() 必须用于此目的。这不是一个必须做魔术来调用另一个系统调用的问题,而是关于提供数据的位置(内核或用户空间)。

答案 1 :(得分:1)

Hm .. linux-3.6.2/fs/namei.c包含许多类似的情况。例如,rename系统调用实际上定义为

SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname)
{
    return sys_renameat(AT_FDCWD, oldname, AT_FDCWD, newname);
}

换句话说,从另一个系统调用调用系统调用没有问题。问题是指针参数是用户空间指针,而您正在尝试提供内核指针:您的fileName应该在用户空间中分配,但你的是在内核空间。

正确的解决方案是从两个函数(你的sys_renameat()中的fs/namei.c)中分解公共代码,然后从两个系统调用中调用该函数。假设你并没有试图把它包括在上游 - 如果你是,那么它是重构和重新思考时间 - 你可以轻而易举地将sys_renameat的内容复制到你自己的函数中;它不是那么大。它也是熟悉文件系统操作所需的必要检查和锁定的有用点。


编写以解释问题和解决方案:

在一个非常真实的意义上,由正常进程(用户空间内存)分配的内存和内核分配的内存(内核空间)完全由内核用户空间分隔屏障。

你的代码忽略了这个障碍,根本不应该工作。 (它可能在x86上有所作为,因为内核 - 用户空间屏障很容易从该体系结构的内核端穿出。)你还使用256字节的堆栈作为文件名,这是一个禁忌:内核堆栈是一个资源非常有限,应该谨慎使用。

正常进程(用户空间进程)无法访问任何内核内存。你可以试试,它不会起作用。这就是障碍存在的原因。 (某些嵌入式系统的硬件根本不支持这样的障碍,但为了讨论的目的,请忽略它们。请记住,即使在x86上,屏障很容易从内核端穿出,它并不意味着它不在那里。不要成为一个鸡巴并假设因为它似乎对你有用,它在某种程度上是正确的。)

屏障的本质是这样的,在大多数体系结构中,内核也存在障碍

为了帮助内核程序员,将指向屏障的指针指向用户空间,标记为__user。这意味着你不能只是取消引用它们并期望它们起作用;您需要使用copy_from_user()copy_to_user()。它不仅仅是系统调用参数:当您从内核访问用户空间数据时,您需要使用这两个函数。

所有系统调用都处理用户空间数据。您看到的每个指针都是(或应该!)标记为__user。每个系统调用都会完成从用户空间访问数据的所有必要工作。

您的问题是您正在尝试向系统调用提供内核空间数据inputFile。它不起作用,因为系统调用总是试图通过屏障,但inputFile位于屏障的同一侧!

inputFile复制到障碍的另一边真的没有理智的方法。我的意思是,当然有一些方法可以做到,它甚至不是那么困难,但它不会是理智的。

所以,让我们探讨一下上面描述的正确解决方案,以及哪一个已经拒绝了一次。

首先,让我们看看renameat系统调用在当前(3.6.2)Linux内核中的实际情况(请记住,此代码是在GPLv2下获得许可的)。 rename系统调用只是使用sys_renameat(AT_FDCWD, oldname, AT_FDCWD, newname)调用它。我将插入我对代码所做内容的解释:

SYSCALL_DEFINE4(renameat, int, olddfd, const char __user *, oldname,
                int, newdfd, const char __user *, newname)
{
        struct dentry *old_dir, *new_dir;
        struct dentry *old_dentry, *new_dentry;
        struct dentry *trap;
        struct nameidata oldnd, newnd;
        char *from;
        char *to;
        int error;

在内核中,堆栈是一种有限的资源。您可以使用相当多的变量,但任何本地阵列都是一个严重的问题。上面的局部变量列表几乎是您在典型系统调用中看到的最大值。

对于重命名调用,该函数必须首先找到包含文件名的父目录:

        error = user_path_parent(olddfd, oldname, &oldnd, &from);
        if (error)
                goto exit;

注意:在此之后,必须在使用后通过调用path_put(&oldnd.path); putname(from);来释放旧目录和路径。

        error = user_path_parent(newdfd, newname, &newnd, &to);
        if (error)
                goto exit1;

注意:在此之后,必须在使用后通过调用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;

还会查找新文件名的条目(毕竟它可能存在)。同样,如果成功,此后还必须使用dput(new_dentry)释放此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)来检查操作是否可以继续(关于访问模式等)。 (用户空间进程的身份详细信息保留在current。)

        error = security_path_rename(&oldnd.path, old_dentry,
                                     &newnd.path, new_dentry);
        if (error)
                goto exit5;

如果对重命名没有异议,则可以使用vfs_rename(struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry)完成实际的重命名:

        error = vfs_rename(old_dir->d_inode, old_dentry,
                           new_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()可见。 user_path_parent()调用getname()调用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

所以,脚踏实地,没有简单的解决方案来解决你的问题。没有单一的函数调用可以神奇地使您的系统调用工作。您将不得不重写它,看看在{{1}}中如何正确完成这些工作。这并不困难,但你必须小心谨慎地做到这一点 - 并且最重要的是接受一种方法,即只是试图让这个简单的事情以最小的变化发挥作用&#34;不适用于此。