如何在 linux 内核中实现 clone(2) 系统调用的另一种变体?
How to implement another variation of clone(2) syscall in linux kernel?
我正在尝试创建另一个版本的 clone(2) 系统调用(在内核 space 中)来创建一个用户进程的克隆,其中一些额外的 parameters.This 系统调用将完全正确与 clone(2) 的工作相同,但当我看到 glibc 的 code 时,我想从 user_space.However 向内核传递一个额外的参数
似乎每个参数的传递顺序都与用户调用 clone()
的顺序不同
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
其中一些是由 glibc 的代码处理的 itself.I 在互联网上搜索以了解 glib 的 clone() 是如何工作的,但找不到更好的文档。
谁能解释一下
- glibc 如何处理 clone()?
- 而且kernel中syscall的所有参数与glibc中的clone不完全相同,那么这些变化是如何处理的?
How glibc handles the clone()?
通过 arch-specific 程序集包装器。对于 i386,请参见 sysdeps/unix/sysv/linux/i386/clone.S in the glibc sources; for x86-64, see sysdeps/unix/sysv/linux/x86-64/clone.S,依此类推。
普通的系统调用包装器是不够的,因为切换堆栈取决于用户空间。除了系统调用之外,上面的汇编文件还提供了关于在用户空间中实际需要做什么的非常有用的注释。
All the parameters of syscall in kernel are not exactly the same as clone in glibc, so how is these variation handled?
映射到系统调用的 C 库函数是包装函数。
例如,考虑 POSIX.1 write()
C 库 low-level I/O 函数和 Linux write()
系统调用.参数基本相同,错误条件也相同,但错误 return 值不同。如果发生错误,C 库函数 returns -1
设置 errno
,而 Linux 系统调用 returns 负错误代码(基本上匹配 errno
值)。
如果您查看例如sysdeps/unix/sysv/linux/x86_64/sysdep.h,您可以看到 x86-64 上 Linux 的基本系统调用包装器归结为
# define INLINE_SYSCALL(name, nr, args...) \
({ \
unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args); \
if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, ))) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); \
resultvar = (unsigned long int) -1; \
} \
(long int) resultvar; })
它只是调用实际的系统调用,然后检查系统调用 return 值是否指示错误;如果是,则将结果更改为 -1
并相应地设置 errno
。它只是 funny-looking,因为它依赖于 GCC 扩展来使其表现为单个语句。
假设您向 Linux 添加了一个新的系统调用,比如
SYSCALL_DEFINE2(splork, unsigned long, arg1, void *, arg2);
并且,无论出于何种原因,您希望将其公开给用户空间
int splork(void *arg2, unsigned long arg1);
没问题!您只需要提供一个最小的 header 文件,
#ifndef _SPLORK_H
#define _SPLORK_H
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <errno.h>
#ifndef __NR_splork
#if defined(__x86_64__)
#define __NR_splork /* syscall number on x86-64 */
#else
#if defined(__i386)
#define __NR_splork /* syscall number on i386 */
#endif
#endif
#ifdef __NR_splork
#ifndef SYS_splork
#define SYS_splork __NR_splork
#endif
int splork(void *arg2, unsigned long arg1)
{
long retval;
retval = syscall(__NR_splork, (long)arg1, (void *)arg2);
if (retval < 0) {
/* Note: For backward compatibility, we might wish to use
*(__errno_location()) = -retval;
here. */
errno = -retval;
return -1;
} else
return (int)retval;
}
#else
#undef SYS_splork
int splork(void *arg2, unsigned long arg1)
{
/* Note: For backward compatibility, we might wish to use
*(__errno_location()) = ENOTSUP;
here. */
errno = ENOTSUP;
return -1;
}
#endif
#endif /* _SPLORK_H */
SYS_splork
和 __NR_splork
是定义新系统调用的系统调用编号的预处理器宏。由于系统调用编号可能(还没有?)包含在官方内核源代码和 header 中,因此上述 header 文件为每个受支持的体系结构明确声明了它。对于不支持的体系结构,splork()
函数将始终 return -1
和 errno == ENOTSUP
.
但是请注意,Linux 系统调用仅限于 6 个参数。如果你的内核函数需要更多,你需要将参数打包成一个结构体,将该结构体的地址传递给内核,并使用copy_from_user()
将值复制到相同的结构体in-kernel.
在所有Linux架构中,指针和long
大小相同(int
可能比指针小),所以我建议您使用long
或 fixed-size 此类结构中的类型以将数据 to/from 传递给内核。
几乎不需要汇编程序就可以使用克隆系统调用。
问题不在于内核作为系统调用的一部分完成的堆栈切换,而可能是 glibc syscall()
包装器。
改用这些 bare-bones 包装器:
long _x64_syscall0(long n) {
long ret;
__asm__ __volatile__("syscall" : "=a"(ret) : "a"(n) : "rcx", "r11", "memory");
return ret;
}
long _x64_syscall5(long n, long a1, long a2, long a3, long a4, long a5) {
long ret;
register long r10 __asm__("r10") = a4;
register long r8 __asm__("r8") = a5;
__asm__ __volatile__("syscall"
: "=a"(ret)
: "a"(n), "D"(a1), "S"(a2), "d"(a3), "r"(r10), "r"(r8)
: "rcx", "r11", "memory");
return ret;
}
克隆用法草图如下所示:
int ret = _x64_syscall5(56 /* clone */ , CLONE_VM | SIGCHLD,
(long long)new_stack, 0, 0, 0);
if (ret == 0) {
// we are the child
ChildFunc();
_x64_syscall0(60 /* exit */ );
} else {
// we are the parent
}
我正在尝试创建另一个版本的 clone(2) 系统调用(在内核 space 中)来创建一个用户进程的克隆,其中一些额外的 parameters.This 系统调用将完全正确与 clone(2) 的工作相同,但当我看到 glibc 的 code 时,我想从 user_space.However 向内核传递一个额外的参数 似乎每个参数的传递顺序都与用户调用 clone()
的顺序不同int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
其中一些是由 glibc 的代码处理的 itself.I 在互联网上搜索以了解 glib 的 clone() 是如何工作的,但找不到更好的文档。 谁能解释一下
- glibc 如何处理 clone()?
- 而且kernel中syscall的所有参数与glibc中的clone不完全相同,那么这些变化是如何处理的?
How glibc handles the clone()?
通过 arch-specific 程序集包装器。对于 i386,请参见 sysdeps/unix/sysv/linux/i386/clone.S in the glibc sources; for x86-64, see sysdeps/unix/sysv/linux/x86-64/clone.S,依此类推。
普通的系统调用包装器是不够的,因为切换堆栈取决于用户空间。除了系统调用之外,上面的汇编文件还提供了关于在用户空间中实际需要做什么的非常有用的注释。
All the parameters of syscall in kernel are not exactly the same as clone in glibc, so how is these variation handled?
映射到系统调用的 C 库函数是包装函数。
例如,考虑 POSIX.1 write()
C 库 low-level I/O 函数和 Linux write()
系统调用.参数基本相同,错误条件也相同,但错误 return 值不同。如果发生错误,C 库函数 returns -1
设置 errno
,而 Linux 系统调用 returns 负错误代码(基本上匹配 errno
值)。
如果您查看例如sysdeps/unix/sysv/linux/x86_64/sysdep.h,您可以看到 x86-64 上 Linux 的基本系统调用包装器归结为
# define INLINE_SYSCALL(name, nr, args...) \
({ \
unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args); \
if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, ))) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); \
resultvar = (unsigned long int) -1; \
} \
(long int) resultvar; })
它只是调用实际的系统调用,然后检查系统调用 return 值是否指示错误;如果是,则将结果更改为 -1
并相应地设置 errno
。它只是 funny-looking,因为它依赖于 GCC 扩展来使其表现为单个语句。
假设您向 Linux 添加了一个新的系统调用,比如
SYSCALL_DEFINE2(splork, unsigned long, arg1, void *, arg2);
并且,无论出于何种原因,您希望将其公开给用户空间
int splork(void *arg2, unsigned long arg1);
没问题!您只需要提供一个最小的 header 文件,
#ifndef _SPLORK_H
#define _SPLORK_H
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <errno.h>
#ifndef __NR_splork
#if defined(__x86_64__)
#define __NR_splork /* syscall number on x86-64 */
#else
#if defined(__i386)
#define __NR_splork /* syscall number on i386 */
#endif
#endif
#ifdef __NR_splork
#ifndef SYS_splork
#define SYS_splork __NR_splork
#endif
int splork(void *arg2, unsigned long arg1)
{
long retval;
retval = syscall(__NR_splork, (long)arg1, (void *)arg2);
if (retval < 0) {
/* Note: For backward compatibility, we might wish to use
*(__errno_location()) = -retval;
here. */
errno = -retval;
return -1;
} else
return (int)retval;
}
#else
#undef SYS_splork
int splork(void *arg2, unsigned long arg1)
{
/* Note: For backward compatibility, we might wish to use
*(__errno_location()) = ENOTSUP;
here. */
errno = ENOTSUP;
return -1;
}
#endif
#endif /* _SPLORK_H */
SYS_splork
和 __NR_splork
是定义新系统调用的系统调用编号的预处理器宏。由于系统调用编号可能(还没有?)包含在官方内核源代码和 header 中,因此上述 header 文件为每个受支持的体系结构明确声明了它。对于不支持的体系结构,splork()
函数将始终 return -1
和 errno == ENOTSUP
.
但是请注意,Linux 系统调用仅限于 6 个参数。如果你的内核函数需要更多,你需要将参数打包成一个结构体,将该结构体的地址传递给内核,并使用copy_from_user()
将值复制到相同的结构体in-kernel.
在所有Linux架构中,指针和long
大小相同(int
可能比指针小),所以我建议您使用long
或 fixed-size 此类结构中的类型以将数据 to/from 传递给内核。
几乎不需要汇编程序就可以使用克隆系统调用。
问题不在于内核作为系统调用的一部分完成的堆栈切换,而可能是 glibc syscall()
包装器。
改用这些 bare-bones 包装器:
long _x64_syscall0(long n) {
long ret;
__asm__ __volatile__("syscall" : "=a"(ret) : "a"(n) : "rcx", "r11", "memory");
return ret;
}
long _x64_syscall5(long n, long a1, long a2, long a3, long a4, long a5) {
long ret;
register long r10 __asm__("r10") = a4;
register long r8 __asm__("r8") = a5;
__asm__ __volatile__("syscall"
: "=a"(ret)
: "a"(n), "D"(a1), "S"(a2), "d"(a3), "r"(r10), "r"(r8)
: "rcx", "r11", "memory");
return ret;
}
克隆用法草图如下所示:
int ret = _x64_syscall5(56 /* clone */ , CLONE_VM | SIGCHLD,
(long long)new_stack, 0, 0, 0);
if (ret == 0) {
// we are the child
ChildFunc();
_x64_syscall0(60 /* exit */ );
} else {
// we are the parent
}