【问题标题】:How to call a system call from a custom system call in linux 3.5.4如何从 linux 3.5.4 中的自定义系统调用调用系统调用
【发布时间】:2012-10-04 09:36:11
【问题描述】:

我正在 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. 如何从系统调用调用其他系统调用

编辑:

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。为什么?我将数据复制到内核空间。

【问题讨论】:

    标签: c linux filesystems kernel system-calls


    【解决方案1】:

    我想准确地展示实现 footy 想要的正确方法,但我最初的答案太长了,我决定将解决方案放在单独的答案中。我将代码分成几部分,并解释每个片段的作用。

    请记住,由于我们重用内核代码,因此本文中的代码和生成的函数必须在 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)),我们将清理工作放在函数的末尾,然后跳到正确的位置,exit 是最后一个一。 error 当然是错误号。

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

    您还为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;但在exit0,我们同时拥有to 和from。在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, old_dentry,
                       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(),必须使用以此目的。调用另一个系统调用不是必须做魔术的问题,而是提供的数据在哪里(内核或用户空间)。

    【讨论】:

    • 感谢您的精彩回答。我将在今晚实施(并在此处更新)。这个答案值得高度赞赏。再次感谢。也许我不知道这一点,因为互联网上没有很多我可以轻松参考的材料。我认为这个问题应该得到一个更好的标题,以便像我一样被困的人轻松进行 SEO。
    • 我还有一个问题,如果你有空,我认为值得一看:stackoverflow.com/questions/12886593/… ...如果你有空,请通过它。我不知道为什么它被否决了。
    • 我特地给你开了赏金,给了。这个答案值得你努力!谢谢
    【解决方案2】:

    嗯..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);
    }
    

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

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


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

    在非常真实的意义上,普通进程分配的内存(userspace内存)和内核分配的内存(kernelspace)完全被kernel-userspace分开障碍。

    您的代码忽略了这个障碍,根本不应该工作。 (它可能在 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(&amp;oldnd.path); putname(from);释放。

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

    注意:在此之后,新的目录和路径必须在使用后通过调用path_put(&amp;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) 释放:

            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 中正确完成的。这并不难,但你必须小心谨慎地去做——而且最重要的是接受“只是试图让这个简单的事情以最小的改变工作”的方法不适合这个。

    【讨论】:

    • 对不起,您能否改写第二段:P。我是菜鸟
    • @footy:内核和普通进程之间有一个非常真实的界限。您正试图忽略该边界。在内核中,您必须使用copy_from_user() 从用户空间获取数据,并使用copy_to_user() 将数据存储到用户空间,以通过边界。这就是为什么将指针标记为__user:以提醒您。内核代码调用其他内核代码也没有问题:您的问题是您在边界的内核一侧有fileName,而您正在调用的函数期望它在另一侧,即用户空间一侧。
    • 所以我所要做的就是copy_from_user()我的数据?删除 hack :) PS:感谢您的精彩解释!
    • @footy:您的代码没有修复,全都错了。你需要彻底重写。我已经在这个答案中准确解释了原因和方式,并在新答案中提供了一个示例。
    猜你喜欢
    • 1970-01-01
    • 2014-06-08
    • 2012-06-30
    • 1970-01-01
    • 2014-12-25
    • 1970-01-01
    • 1970-01-01
    • 2012-12-30
    • 2016-05-12
    相关资源
    最近更新 更多