当您调用克隆系统调用时,谁设置了 RIP 寄存器?
Who sets the RIP register when you call the clone syscall?
我正在尝试实现最小内核,并且正在尝试实现克隆系统调用。在手册页中,您可以看到这样定义的克隆系统调用:
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
如您所见,它接收一个函数指针。如果您更仔细地阅读手册页,您实际上可以看到内核中实际的系统调用实现没有接收到函数指针:
long clone(unsigned long flags, void *stack,
int *parent_tid, int *child_tid,
unsigned long tls);
所以,我的问题是,线程创建后谁修改了RIP寄存器?是 libc 吗?
我在 glibc 中找到了这段代码:https://elixir.bootlin.com/glibc/latest/source/sysdeps/unix/sysv/linux/x86_64/clone.S 但我不确定函数实际调用的时间点。
补充信息:
查看 clone.S 源代码时,您可以看到它在系统调用后跳转到 thread_start 分支。在克隆系统调用之后的分支上(因此只有子系统执行此操作),它从堆栈中弹出函数地址和参数。谁实际将这些参数和函数地址压入堆栈?我想它必须发生在内核的某个地方,因为在 syscall
指令处它们不在那里。
这是一些 gdb 输出:
就在系统调用之前:
[-------------------------------------code-------------------------------------]
0x7ffff7d8af22 <clone+34>: mov r8,r9
0x7ffff7d8af25 <clone+37>: mov r10,QWORD PTR [rsp+0x8]
0x7ffff7d8af2a <clone+42>: mov eax,0x38
=> 0x7ffff7d8af2f <clone+47>: syscall
0x7ffff7d8af31 <clone+49>: test rax,rax
0x7ffff7d8af34 <clone+52>: jl 0x7ffff7d8af49 <clone+73>
0x7ffff7d8af36 <clone+54>: je 0x7ffff7d8af39 <clone+57>
0x7ffff7d8af38 <clone+56>: ret
Guessed arguments:
arg[0]: 0x3d0f00
arg[1]: 0x7ffff8020b60 --> 0x7ffff7d3fb30 (<do_something>: push rbx)
arg[2]: 0x7fffffffda90 --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda78 --> 0x7ffff7d3f52c (<main+172>: pop rsi)
0008| 0x7fffffffda80 --> 0x7fffffffda94 --> 0x73658b0000000000
0016| 0x7fffffffda88 --> 0x7fffffffda94 --> 0x73658b0000000000
0024| 0x7fffffffda90 --> 0x0
0032| 0x7fffffffda98 --> 0x492e085573658b00
0040| 0x7fffffffdaa0 --> 0x7ffff7d3f0d0 (<_init>: sub rsp,0x8)
0048| 0x7fffffffdaa8 --> 0x7ffff7d40830 (<__libc_csu_init>: push r15)
0056| 0x7fffffffdab0 --> 0x7ffff7d408d0 (<__libc_csu_fini>: push rbp)
[------------------------------------------------------------------------------]
在子线程上的 syscall 指令之后(检查堆栈顶部 - 这不会发生在父线程上):
[-------------------------------------code-------------------------------------]
0x7ffff7d8af25 <clone+37>: mov r10,QWORD PTR [rsp+0x8]
0x7ffff7d8af2a <clone+42>: mov eax,0x38
0x7ffff7d8af2f <clone+47>: syscall
=> 0x7ffff7d8af31 <clone+49>: test rax,rax
0x7ffff7d8af34 <clone+52>: jl 0x7ffff7d8af49 <clone+73>
0x7ffff7d8af36 <clone+54>: je 0x7ffff7d8af39 <clone+57>
0x7ffff7d8af38 <clone+56>: ret
0x7ffff7d8af39 <clone+57>: xor ebp,ebp
[------------------------------------stack-------------------------------------]
0000| 0x7ffff8020b60 --> 0x7ffff7d3fb30 (<do_something>: push rbx)
0008| 0x7ffff8020b68 --> 0x7ffff7dd5add --> 0x4c414d0074736574 ('test')
0016| 0x7ffff8020b70 --> 0x0
0024| 0x7ffff8020b78 --> 0x411
0032| 0x7ffff8020b80 ("Parameters: 0x7ffff7d3fb30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
0040| 0x7ffff8020b88 ("rs: 0x7ffff7d3fb30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
0048| 0x7ffff8020b90 ("fff7d3fb30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
0056| 0x7ffff8020b98 ("30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
[------------------------------------------------------------------------------]
通常它的工作方式是,当计算机启动时,Linux 设置一个 MSR(模型特定寄存器)以使用汇编指令 syscall
。汇编指令syscall
会让RIP寄存器跳转到MSR中指定的地址进入内核模式。如英特尔的 64-ia-32-architectures-software-developer-vol-2b-manual 所述:
SYSCALL invokes an OS system-call handler at privilege level 0.
It does so by loading RIP from the IA32_LSTAR MSR
进入内核模式后,内核将查看传递到常规寄存器(RAX、RBX 等)的参数以确定系统调用的要求。然后内核将调用原型在 linux/syscalls.h (https://elixir.bootlin.com/linux/latest/source/include/linux/syscalls.h#L217) 中的 sys_XXX 函数之一。 sys_clone的定义在kernel/fork.c.
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#endif
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
SYSCALLDEFINE5 宏采用第一个参数并为其添加前缀 sys_。这个函数实际上是 sys_clone 并且它调用 _do_fork.
这意味着确实没有 clone()
函数被 glibc 调用以调用内核。使用 syscall
指令调用内核,它跳转到 MSR 中指定的地址,然后调用 sys_call_table.
中的系统调用之一
x86 内核的入口点在这里:https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/entry/entry_64.S. If you scroll down you'll see the line: call *sys_call_table(, %rax, 8)
. Basically, call one of the functions of the sys_call_table. The implementation of the sys_call_table is here: https://elixir.bootlin.com/linux/latest/source/arch/x86/entry/syscall_64.c#L20。
// SPDX-License-Identifier: GPL-2.0
/* System call table for x86-64. */
#include <linux/linkage.h>
#include <linux/sys.h>
#include <linux/cache.h>
#include <linux/syscalls.h>
#include <asm/unistd.h>
#include <asm/syscall.h>
#define __SYSCALL_X32(nr, sym)
#define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym)
#define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64
#define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/
[0 ... __NR_syscall_max] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};
我建议您阅读以下内容:https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-2.html。在这个网站上说
As you can see, we include the asm/syscalls_64.h header at the end of the array. This header file is generated by the special script at arch/x86/entry/syscalls/syscalltbl.sh and generates our header file from the syscall table (https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/entry/syscalls/syscall_64.tbl).
...
...
So, after this, our sys_call_table takes the following form:
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &sys_ni_syscall,
[0] = sys_read,
[1] = sys_write,
[2] = sys_open,
...
...
...
};
生成 table 后,当您使用 syscall
汇编指令时,将跳转到其中一个条目。对于 clone() 它将调用 sys_clone() 而它本身又调用 _do_fork()。定义如下:
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
它调用 wake_up_new_task() 将任务放入 运行 队列并唤醒它。我很惊讶它甚至立即唤醒了任务。我猜想调度程序会改为执行它,并且会尽快将 运行 赋予高优先级。就其本身而言,内核不必接收函数指针,因为如 clone() 的联机帮助页所述:
The raw clone() system call corresponds more closely to fork(2)
in that execution in the child continues from the point of the
call. As such, the fn and arg arguments of the clone() wrapper
function are omitted.
child 继续在进行系统调用的地方执行。我不完全了解该机制,但最终 child 将在新线程中继续执行。 parent 线程(它创建了新的 child 线程)return 和 child 线程改为跳转到指定的函数。
我认为它适用于以下行(在您提供的 link 上):
testq %rax,%rax
jl SYSCALL_ERROR_LABEL
jz L(thread_start) //Child jumps to thread_start
ret //Parent returns to where it was
因为rax是64位寄存器,所以他们使用'q'版本的GNU语法汇编指令测试。他们测试 rax 是否为零。如果它小于零,则存在错误。如果它为零,则跳转到 thread_start。如果它既不为零也不为负(在parent线程的情况下),继续执行并return。新线程以 rax 为 0 创建。它允许区分 parent 和 child 线程。
编辑
如您提供的 link 所述,
The parameters are passed in register and on the stack from userland:
rdi: fn
rsi: child_stack
rdx: flags
rcx: arg
r8d: TID field in parent
r9d: thread pointer
所以当你的程序执行以下几行时:
/* Insert the argument onto the new stack. */
subq ,%rsi
movq %rcx,8(%rsi)
/* Save the function pointer. It will be popped off in the
child in the ebx frobbing below. */
movq %rdi,0(%rsi)
它将函数指针和参数插入到新堆栈中。然后它调用内核,内核本身不必将任何东西压入堆栈。它只是接收新堆栈作为参数,然后使 child 的线程 RSP 寄存器指向它。我猜想这发生在 copy_process() 函数(从 fork() 调用)中:
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
if (retval)
goto bad_fork_cleanup_io;
它似乎是在 copy_thread_tls() 函数中完成的,它本身调用 copy_thread()。 copy_thread() 的原型在 include/linux/sched.h 中,它是根据体系结构定义的。我不确定它在哪里为 x86 定义。
是的,libc;内核接口就像 fork
:它 return 到同一个地方两次,但具有不同的 return 值。 (子级中的 0
或父级中的 PID/TID)。 The man page 记录了 glibc 包装器与内核的差异,就像其他存在差异的系统调用一样。
libc 包装器将您传递到新线程堆栈中的函数指针和 arg 存储起来 space,新线程可以在其中加载它。 (内核启动它时将其 RSP 设置为传递给 clone()
的 void *stack
arg,因此它无法访问堆栈内存或寄存器中的旧局部变量,并且使用全局变量不会是线程-如果多个线程同时克隆自己是安全的。)
请注意,还有一个 clone3
系统调用接受一个结构参数,它也更像是 clone
的原始内核接口。 (或者至少没有 glibc 包装器。)
我正在尝试实现最小内核,并且正在尝试实现克隆系统调用。在手册页中,您可以看到这样定义的克隆系统调用:
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
如您所见,它接收一个函数指针。如果您更仔细地阅读手册页,您实际上可以看到内核中实际的系统调用实现没有接收到函数指针:
long clone(unsigned long flags, void *stack,
int *parent_tid, int *child_tid,
unsigned long tls);
所以,我的问题是,线程创建后谁修改了RIP寄存器?是 libc 吗?
我在 glibc 中找到了这段代码:https://elixir.bootlin.com/glibc/latest/source/sysdeps/unix/sysv/linux/x86_64/clone.S 但我不确定函数实际调用的时间点。
补充信息:
查看 clone.S 源代码时,您可以看到它在系统调用后跳转到 thread_start 分支。在克隆系统调用之后的分支上(因此只有子系统执行此操作),它从堆栈中弹出函数地址和参数。谁实际将这些参数和函数地址压入堆栈?我想它必须发生在内核的某个地方,因为在 syscall
指令处它们不在那里。
这是一些 gdb 输出:
就在系统调用之前:
[-------------------------------------code-------------------------------------]
0x7ffff7d8af22 <clone+34>: mov r8,r9
0x7ffff7d8af25 <clone+37>: mov r10,QWORD PTR [rsp+0x8]
0x7ffff7d8af2a <clone+42>: mov eax,0x38
=> 0x7ffff7d8af2f <clone+47>: syscall
0x7ffff7d8af31 <clone+49>: test rax,rax
0x7ffff7d8af34 <clone+52>: jl 0x7ffff7d8af49 <clone+73>
0x7ffff7d8af36 <clone+54>: je 0x7ffff7d8af39 <clone+57>
0x7ffff7d8af38 <clone+56>: ret
Guessed arguments:
arg[0]: 0x3d0f00
arg[1]: 0x7ffff8020b60 --> 0x7ffff7d3fb30 (<do_something>: push rbx)
arg[2]: 0x7fffffffda90 --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda78 --> 0x7ffff7d3f52c (<main+172>: pop rsi)
0008| 0x7fffffffda80 --> 0x7fffffffda94 --> 0x73658b0000000000
0016| 0x7fffffffda88 --> 0x7fffffffda94 --> 0x73658b0000000000
0024| 0x7fffffffda90 --> 0x0
0032| 0x7fffffffda98 --> 0x492e085573658b00
0040| 0x7fffffffdaa0 --> 0x7ffff7d3f0d0 (<_init>: sub rsp,0x8)
0048| 0x7fffffffdaa8 --> 0x7ffff7d40830 (<__libc_csu_init>: push r15)
0056| 0x7fffffffdab0 --> 0x7ffff7d408d0 (<__libc_csu_fini>: push rbp)
[------------------------------------------------------------------------------]
在子线程上的 syscall 指令之后(检查堆栈顶部 - 这不会发生在父线程上):
[-------------------------------------code-------------------------------------]
0x7ffff7d8af25 <clone+37>: mov r10,QWORD PTR [rsp+0x8]
0x7ffff7d8af2a <clone+42>: mov eax,0x38
0x7ffff7d8af2f <clone+47>: syscall
=> 0x7ffff7d8af31 <clone+49>: test rax,rax
0x7ffff7d8af34 <clone+52>: jl 0x7ffff7d8af49 <clone+73>
0x7ffff7d8af36 <clone+54>: je 0x7ffff7d8af39 <clone+57>
0x7ffff7d8af38 <clone+56>: ret
0x7ffff7d8af39 <clone+57>: xor ebp,ebp
[------------------------------------stack-------------------------------------]
0000| 0x7ffff8020b60 --> 0x7ffff7d3fb30 (<do_something>: push rbx)
0008| 0x7ffff8020b68 --> 0x7ffff7dd5add --> 0x4c414d0074736574 ('test')
0016| 0x7ffff8020b70 --> 0x0
0024| 0x7ffff8020b78 --> 0x411
0032| 0x7ffff8020b80 ("Parameters: 0x7ffff7d3fb30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
0040| 0x7ffff8020b88 ("rs: 0x7ffff7d3fb30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
0048| 0x7ffff8020b90 ("fff7d3fb30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
0056| 0x7ffff8020b98 ("30 4001536 0x7ffff8020b70 0x7fffffffda90 0x7ffff8000b60 0x7fffffffda94\n")
[------------------------------------------------------------------------------]
通常它的工作方式是,当计算机启动时,Linux 设置一个 MSR(模型特定寄存器)以使用汇编指令 syscall
。汇编指令syscall
会让RIP寄存器跳转到MSR中指定的地址进入内核模式。如英特尔的 64-ia-32-architectures-software-developer-vol-2b-manual 所述:
SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR
进入内核模式后,内核将查看传递到常规寄存器(RAX、RBX 等)的参数以确定系统调用的要求。然后内核将调用原型在 linux/syscalls.h (https://elixir.bootlin.com/linux/latest/source/include/linux/syscalls.h#L217) 中的 sys_XXX 函数之一。 sys_clone的定义在kernel/fork.c.
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#endif
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
SYSCALLDEFINE5 宏采用第一个参数并为其添加前缀 sys_。这个函数实际上是 sys_clone 并且它调用 _do_fork.
这意味着确实没有 clone()
函数被 glibc 调用以调用内核。使用 syscall
指令调用内核,它跳转到 MSR 中指定的地址,然后调用 sys_call_table.
x86 内核的入口点在这里:https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/entry/entry_64.S. If you scroll down you'll see the line: call *sys_call_table(, %rax, 8)
. Basically, call one of the functions of the sys_call_table. The implementation of the sys_call_table is here: https://elixir.bootlin.com/linux/latest/source/arch/x86/entry/syscall_64.c#L20。
// SPDX-License-Identifier: GPL-2.0
/* System call table for x86-64. */
#include <linux/linkage.h>
#include <linux/sys.h>
#include <linux/cache.h>
#include <linux/syscalls.h>
#include <asm/unistd.h>
#include <asm/syscall.h>
#define __SYSCALL_X32(nr, sym)
#define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym)
#define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64
#define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/
[0 ... __NR_syscall_max] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};
我建议您阅读以下内容:https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-2.html。在这个网站上说
As you can see, we include the asm/syscalls_64.h header at the end of the array. This header file is generated by the special script at arch/x86/entry/syscalls/syscalltbl.sh and generates our header file from the syscall table (https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/entry/syscalls/syscall_64.tbl).
...
...
So, after this, our sys_call_table takes the following form:
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { [0 ... __NR_syscall_max] = &sys_ni_syscall, [0] = sys_read, [1] = sys_write, [2] = sys_open, ... ... ... };
生成 table 后,当您使用 syscall
汇编指令时,将跳转到其中一个条目。对于 clone() 它将调用 sys_clone() 而它本身又调用 _do_fork()。定义如下:
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
它调用 wake_up_new_task() 将任务放入 运行 队列并唤醒它。我很惊讶它甚至立即唤醒了任务。我猜想调度程序会改为执行它,并且会尽快将 运行 赋予高优先级。就其本身而言,内核不必接收函数指针,因为如 clone() 的联机帮助页所述:
The raw clone() system call corresponds more closely to fork(2) in that execution in the child continues from the point of the call. As such, the fn and arg arguments of the clone() wrapper function are omitted.
child 继续在进行系统调用的地方执行。我不完全了解该机制,但最终 child 将在新线程中继续执行。 parent 线程(它创建了新的 child 线程)return 和 child 线程改为跳转到指定的函数。
我认为它适用于以下行(在您提供的 link 上):
testq %rax,%rax
jl SYSCALL_ERROR_LABEL
jz L(thread_start) //Child jumps to thread_start
ret //Parent returns to where it was
因为rax是64位寄存器,所以他们使用'q'版本的GNU语法汇编指令测试。他们测试 rax 是否为零。如果它小于零,则存在错误。如果它为零,则跳转到 thread_start。如果它既不为零也不为负(在parent线程的情况下),继续执行并return。新线程以 rax 为 0 创建。它允许区分 parent 和 child 线程。
编辑
如您提供的 link 所述,
The parameters are passed in register and on the stack from userland:
rdi: fn
rsi: child_stack
rdx: flags
rcx: arg
r8d: TID field in parent
r9d: thread pointer
所以当你的程序执行以下几行时:
/* Insert the argument onto the new stack. */
subq ,%rsi
movq %rcx,8(%rsi)
/* Save the function pointer. It will be popped off in the
child in the ebx frobbing below. */
movq %rdi,0(%rsi)
它将函数指针和参数插入到新堆栈中。然后它调用内核,内核本身不必将任何东西压入堆栈。它只是接收新堆栈作为参数,然后使 child 的线程 RSP 寄存器指向它。我猜想这发生在 copy_process() 函数(从 fork() 调用)中:
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
if (retval)
goto bad_fork_cleanup_io;
它似乎是在 copy_thread_tls() 函数中完成的,它本身调用 copy_thread()。 copy_thread() 的原型在 include/linux/sched.h 中,它是根据体系结构定义的。我不确定它在哪里为 x86 定义。
是的,libc;内核接口就像 fork
:它 return 到同一个地方两次,但具有不同的 return 值。 (子级中的 0
或父级中的 PID/TID)。 The man page 记录了 glibc 包装器与内核的差异,就像其他存在差异的系统调用一样。
libc 包装器将您传递到新线程堆栈中的函数指针和 arg 存储起来 space,新线程可以在其中加载它。 (内核启动它时将其 RSP 设置为传递给 clone()
的 void *stack
arg,因此它无法访问堆栈内存或寄存器中的旧局部变量,并且使用全局变量不会是线程-如果多个线程同时克隆自己是安全的。)
请注意,还有一个 clone3
系统调用接受一个结构参数,它也更像是 clone
的原始内核接口。 (或者至少没有 glibc 包装器。)