dan*_*eld 5 c binary operating-system glibc system-calls
我正在尝试实现最小内核,并且正在尝试实现克隆系统调用。在手册页中,您可以看到克隆系统调用的定义如下:
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
Run Code Online (Sandbox Code Playgroud)
如您所见,它接收一个函数指针。如果您更仔细地阅读手册页,您实际上可以看到内核中的实际系统调用实现不接收函数指针:
long clone(unsigned long flags, void *stack,
int *parent_tid, int *child_tid,
unsigned long tls);
Run Code Online (Sandbox Code Playgroud)
那么,我的问题是,创建线程后谁修改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)
[------------------------------------------------------------------------------]
Run Code Online (Sandbox Code Playgroud)
在子线程上的系统调用指令之后(检查堆栈顶部 - 这不会发生在父线程上):
[-------------------------------------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")
[------------------------------------------------------------------------------]
Run Code Online (Sandbox Code Playgroud)
通常它的工作方式是,当计算机启动时,Linux 会设置一个 MSR(模型特定寄存器)来处理汇编指令syscall
。汇编指令syscall
将使RIP寄存器跳转到MSR中指定的地址,进入内核模式。正如英特尔的 64-ia-32-architectures-software-developer-vol-2b-manual 中所述:
SYSCALL 在特权级别 0 调用操作系统系统调用处理程序。它通过从 IA32_LSTAR MSR 加载 RIP 来实现这一点
一旦进入内核模式,内核将查看传递到常规寄存器(RAX、RBX 等)的参数,以确定系统调用所询问的内容。然后内核将调用 sys_XXX 函数之一,其原型位于 linux/syscalls.h ( https://elixir.bootlin.com/linux/latest/source/include/linux/syscalls.h#L217 )。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);
}
Run Code Online (Sandbox Code Playgroud)
SYSCALLDEFINE5 宏采用第一个参数并为其添加 sys_ 前缀。这个函数实际上是sys_clone,它调用_do_fork。
这意味着 glibc 确实没有clone()
调用内核的函数。使用指令调用内核syscall
,它跳转到 MSR 中指定的地址,然后调用 sys_call_table 中的系统调用之一。
x86 内核的入口点位于: https: //github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/entry/entry_64.S。如果向下滚动,您将看到以下行:call *sys_call_table(, %rax, 8)
。基本上,调用 sys_call_table 的函数之一。sys_call_table 的实现在这里: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>
};
Run Code Online (Sandbox Code Playgroud)
我建议您阅读以下内容:https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-2.html。该网站上注明
如您所见,我们在数组末尾包含了 asm/syscalls_64.h 标头。该头文件由 arch/x86/entry/syscalls/syscalltbl.sh 处的特殊脚本生成,并从系统调用表生成我们的头文件(https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86 /entry/syscalls/syscall_64.tbl)。
...
...
因此,在此之后,我们的 sys_call_table 采用以下形式:
Run Code Online (Sandbox Code Playgroud)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, ... ... ... };
生成表后,当您使用syscall
汇编指令时,将跳转到其条目之一。对于clone(),它将调用sys_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;
}
Run Code Online (Sandbox Code Playgroud)
它调用wake_up_new_task(),将任务放入运行队列并唤醒它。我很惊讶它甚至立即唤醒了任务。我猜调度程序会这样做,并且会被赋予高优先级以尽快运行。就其本身而言,内核不必接收函数指针,因为如clone() 的联机帮助页中所述:
原始的clone()系统调用与fork(2)更接近,因为子进程中的执行从调用点继续。因此,clone() 包装函数的 fn 和 arg 参数被省略。
子进程在进行系统调用的地方继续执行。我不太明白其中的机制,但最终孩子将在新线程中继续执行。父线程(创建了新的子线程)返回,子线程跳转到指定的函数。
我认为它适用于以下几行(在您提供的链接上):
testq %rax,%rax
jl SYSCALL_ERROR_LABEL
jz L(thread_start) //Child jumps to thread_start
ret //Parent returns to where it was
Run Code Online (Sandbox Code Playgroud)
由于 rax 是 64 位寄存器,因此他们使用 GNU 语法汇编指令测试的“q”版本。他们测试 rax 是否为零。如果它小于零,则出现错误。如果为零则跳转到thread_start。如果不为零也不负(在父线程的情况下),则继续执行并返回。新线程创建时 rax 为 0。它允许区分父线程和子线程。
编辑
正如您提供的链接中所述,
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
Run Code Online (Sandbox Code Playgroud)
因此,当您的程序执行以下几行时:
/* Insert the argument onto the new stack. */
subq $16,%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)
Run Code Online (Sandbox Code Playgroud)
它将函数指针和参数插入到新堆栈中。然后它调用内核,内核本身不必将任何内容推入堆栈。它只是接收新堆栈作为参数,然后使子线程的 RSP 寄存器指向它。我猜想这发生在 copy_process() 函数(从 fork() 调用)中,大致如下:
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
if (retval)
goto bad_fork_cleanup_io;
Run Code Online (Sandbox Code Playgroud)
这似乎是在 copy_thread_tls() 函数中完成的,该函数本身调用 copy_thread() 。copy_thread() 的原型位于 include/linux/sched.h 中,它是根据体系结构定义的。我不确定 x86 的定义在哪里。