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

foo*_*oty 3 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;

}
Run Code Online (Sandbox Code Playgroud)

我在这里有两个疑问。

  1. 我正在使用old_fs = get_fs();,set_fs(KERNEL_DS);set_fs(old_fs);绕过实际调用,sys_rename因为存在错误。我从这个问题得到了答案:从内核分配用户空间内存......这是一个正确的解决方法吗?
  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);

}
Run Code Online (Sandbox Code Playgroud)

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

Nom*_*mal 5

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

请记住,由于我们重用内核代码,因此本文中的代码和生成的函数必须在 GPLv2 许可下获得许可。

首先,我们首先声明一个单参数系统调用。

SYSCALL_DEFINE1(myfunc, const char __user *, oldname)
{
Run Code Online (Sandbox Code Playgroud)

在内核中,堆栈空间是一种稀缺资源。您不创建本地数组;你总是使用动态内存管理。幸运的是,有一些非常有用的函数,比如__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;
Run Code Online (Sandbox Code Playgroud)

对 的第一个更改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;
    }
Run Code Online (Sandbox Code Playgroud)

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

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

您还为新名称保留了内存,to[0]最多包括to[PATH_MAX]。请记住确保它也使用\0(into[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;
Run Code Online (Sandbox Code Playgroud)

请注意,这exit0是原始 中未找到的新标签renameat()。我们需要一个新标签,因为在exit,我们只有to; 但是在exit0,我们同时拥有tofrom。之后exit0,我们有tofromoldnd,等等。

接下来,我们可以重用大部分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);
Run Code Online (Sandbox Code Playgroud)

至此,所有的工作都完成了,只剩下释放上面代码占用的锁、内存等了。如果此时一切都成功error == 0,那么我们将进行所有清理工作。如果我们有问题,error包含错误代码,我们会跳转到正确的标签以进行必要的清理,直到发生错误。如果vfs_rename()失败——它执行实际操作——,我们会做所有清理工作。

然而,相对于原来的代码,我们抓住的from第一个(exit),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;
}
Run Code Online (Sandbox Code Playgroud)

到这里我们就完成了。

当然,上面我们复制的部分有很多细节需要考虑sys_renameat()——就像我在另一个答案中所说的,你不应该像这样复制代码,而是将公共代码重构为辅助函数;这使得维护更容易。幸运的是,因为我们保留了所有检查renameat()——我们renameat()在复制任何代码之前进行路径操作——我们可以确保所有必要的检查都已完成。就好像用户自己指定了操纵路径并调用了renameat().

如果你已经做了一些检查之后再进行修改,情况会复杂得多。您必须考虑这些检查是什么,您的修改对它们有何影响,并且几乎总是重新进行这些检查。

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