Linux内核源码解析 | system call part 2

29
四月
2021

Linux内核源码解析 | system call part 2

摘要

  • 回顾: 应用程序如何调用 system call
  • 初始化 system calls table

2. 回顾

  • 应用程序必须首先将 对应的 system call 在 系统调用表中对应的编号 存入rax寄存器,然后以正确的顺序 将正确的system call参数值填充通用寄存器 rdi ,rsi,rdx ,rcx ,r8 中,并使用syscall指令进行实际的系统调用
  • syscall指令,系统 将跳入 存储在 MSR_LSTAR Model specific register(Long system target address register)中的地址,在该地址, 存有一个 system call 全局处理函数 entry_SYSCALL_64,
  • entry_SYSCALL_64 负责根据rax 寄存器的值 调用内核中对应的 system call hanler function.

3.初始化 system calls table

3.1 为什莫要 初始化 system calls table? 这个表是干什么的?

当在执行 entry_SYSCALL_64 时, entry_SYSCALL_64 需要根据rax中对应的 system call 的编号,调用对应的 system call handler function。但是Linux内核如何搜索该system call所对应系统调用处理程序的地址呢? Linux内核包含一个特殊的表,称为system calls table。系统调用表由sys_call_table数组表示,entry_SYSCALL_64 将通过该数组,找到 该system call 所对应的处理函数的地址,并调用该处理函数完成 system call。

3.2 如何初始化 system calls table

system calls table 由Linux内核中的sys_call_table数组表示,该数组在arch / x86 / entry / syscall_64.c源代码文件中定义。让我们看一下它的实现

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
    #include <asm/syscalls_64.h>
};
  • sys_call_table是长度为__NR_syscall_max + 1的数组,其中__NR_syscall_max宏表示给定体系结构的最大系统调用数。在 x86_64 体系结构下 共有 547个 系统调用(#define __NR_syscall_max 547),与之对应,在 系统调用表中也有相同数量的 system call

3.2.1 the sys_call_table array 的 类型 是什么?

  • sys_call_ptr_t 表示指向system call table的指针。它被定义为用于不返回任何内容且不接受参数的函数指针:
typedef void (*sys_call_ptr_t)(void);

3.2.2 初始化 sys_call_table array

  • 首先 数组中所有的元素,即 指向system call handler function的指针 指向了 sys_ni_syscall. sys_ni_syscall函数表示未实现的系统调用。 这跟 初始化 int 变量类似,仅仅是为了使该变量在内存空间中占有一个“位置”。 sys_ni_syscall 仅仅是一个返回 -errno 的函数:
asmlinkage long sys_ni_syscall(void)
{
    return -ENOSYS;   //ENOSY tell us  Function not implemented (POSIX.1)
}
  • ... : 该符号 借助 GCC compiler extension : Designated Initializers 使我们以非固定顺序初始化元素 ===》 提高效率
  • asm / syscalls_64.h: 该头文件由arch / x86 / entry / syscalls / syscalltbl.sh中的特殊脚本生成,特殊脚本从syscall表中生成我们的头文件。asm / syscalls_64.h包含以下宏的定义:
__SYSCALL_COMMON(0, sys_read, sys_read)
__SYSCALL_COMMON(1, sys_write, sys_write)
__SYSCALL_COMMON(2, sys_open, sys_open)
__SYSCALL_COMMON(3, sys_close, sys_close)
__SYSCALL_COMMON(5, sys_newfstat, sys_newfstat)
...
...
...
  • __SYSCALL_COMMON宏在同一源代码文件中定义,并扩展为__SYSCALL_64宏,该宏定义最后扩展为相应的 system call handler function:
#define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
#define __SYSCALL_64(nr, sym, compat) [nr] = sym,
`
+ 所以,最后我们的`sys_call_table` 将 拥有以下形式:
```c
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,
    ...
    ...
    ...
};

3.3 总结

经过初始化后,system call table中所有指向 未实现的系统调用的元素 都将包含sys_ni_syscall函数的地址,该地址仅返回-ENOSYS,如我们在上面看到的,其他元素将指向对应的 system call handler function的名字。

这样 entry_SYSCALL_64 函数就知道各个对应的 system call handler function 的位置了

4. 初始化 the system call entry

4.1 为何要 初始化 the system call entry,什么是 the system call entry

还记得在上一篇文章中说过, 当 userspace 的程序 触发 syscall指令 后 ,CPU会跳转到存储在 MSR_LSTAR Model specific register(Long system target address register)中的地址,即 entry_SYSCALL_64 function处。内核负责提供一个自定义的函数来处理所有系统调用,并在系统启动时将此处理函数的地址写入MSR_LSTAR寄存器. 该自定义函数负责判定system call的类型,以及呼叫特定的 system call handler function.

这意味着我们需要在操作系统系统初始化( Linux kernel initialization process) 时将entry_SYSCALL_64 function的地址 放入IA32_LSTAR model specific register中. 这一过程 即叫做 初始化 the system call entry

4.2 如何 初始化 the system call entry

Linux内核在初始化过程中调用trap_init函数。此函数在arch / x86 / kernel / setup.c源代码文件中定义,并执行 一系列有关 系统中断的初始化。此外 ,此函数还调用了定义在 arch / x86 / kernel / cpu / common.c中 的 cpu_init函数,除了初始化每个cpu状态外,该函数还调用了 同一源代码文件中的 syscall_init函数。该函数执行初始化 the system call entry 的具体操作:

void syscall_init(void)
{
	wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
	wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

#ifdef CONFIG_IA32_EMULATION
	wrmsrl(MSR_CSTAR, (unsigned long)entry_SYSCALL_compat);
	/*
	 * This only works on Intel CPUs.
	 * On AMD CPUs these MSRs are 32-bit, CPU truncates MSR_IA32_SYSENTER_EIP.
	 * This does not cause SYSENTER to jump to the wrong location, because
	 * AMD doesn't allow SYSENTER in long mode (either 32- or 64-bit).
	 */
	wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
	wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
	wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
#else
	wrmsrl(MSR_CSTAR, (unsigned long)ignore_sysret);
	wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
	wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
	wrmsrl_safe(MSR_IA32_SYSENTER_EIP, 0ULL);
#endif

	/* Flags to clear on syscall */
	wrmsrl(MSR_SYSCALL_MASK,
	       X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
	       X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
}

4.2.1 代码解释:

  1. 首先,它填充两个model specific registers:
wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);
wrmsrl(MSR_LSTAR, entry_SYSCALL_64);
  • 第一个model specific register: MSR_STAR。 其内包含
    • user code segment 的63:48位。这些位将被加载到CS和SS段寄存器,并 用于·sysret instruction·。 该指令提供了从系统调用完成后 从kernel space 返回到user space 特定位置的功能。
    • 47:32 bits from the kernel code that will be used as the base selector for CS and SS segment registers when user space applications execute a system call.
  • 在第二行代码中,我们的将代表 the system call entryentry_SYSCALL_64符号填充到MSR_LSTAR寄存器。该函数负责 根据 system call 的编号 ,确定system call 的类型,并负责 在呼叫 system call handler之前的准备工作, 最后呼叫 system call handler,并返回 system call handler的结果给 user space program,
  1. 之后, 我们需要设置以下特定于 model specific registers:
+ MSR_CSTAR - target rip for the compatibility mode callers;
+ MSR_IA32_SYSENTER_CS - target cs for the sysenter instruction;
+ MSR_IA32_SYSENTER_ESP - target esp for the sysenter instruction;
+ MSR_IA32_SYSENTER_EIP - target eip for the sysenter instruction.
  • 这些特定于模型的寄存器的值取决于CONFIG_IA32_EMULATION内核配置选项
  • 第一种情况,如果启用了此内核配置选项,则它允许旧的32位程序在64位内核下运行,在这种情况下 ,我们将向 这些 model specific registers中放入 the entry point for the system calls the compatibility mode:
wrmsrl(MSR_CSTAR, entry_SYSCALL_compat);
wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS); // compatibility mode  with the kernel code segment
wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL); // put zero to the stack pointer
wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat); //write the address of the entry_SYSENTER_compat symbol to the instruction pointer
  • 第二种情况,如果CONFIG_IA32_EMULATION内核配置选项被禁用,我们将ignore_sysret符号写入MSR_CSTAR, 然后 我们将MSR_IA32_SYSENTER_ESPMSR_IA32_SYSENTER_EIP填充为零,并将invalid segment of the Global Descriptor Table放入MSR_IA32_SYSENTER_CS model specific register
wrmsrl(MSR_CSTAR, ignore_sysret);
// ignore_sysret is defined in the arch/x86/entry/entry_64.S assembly file and just returns -ENOSYS error code
ENTRY(ignore_sysret)
    mov    $-ENOSYS, %eax
    sysret
END(ignore_sysret)

wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
wrmsrl_safe(MSR_IA32_SYSENTER_EIP, 0ULL);
  1. syscall_init函数的末尾,我们通过将flag set 写入到MSR_SYSCALL_MASK寄存器中来屏蔽标志寄存器中的某些标志位
wrmsrl(MSR_SYSCALL_MASK,
       X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
       X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);

这是syscall_init函数的结尾,这意味着system call entry准备就绪,可以接受从userspace 的请求了。

5.调用 system call handler 前的准备工作,即entry_SYSCALL_64 函数 如何工作?

entry_SYSCALL_64在arch / x86 / entry / entry_64.S 中定义,并从以下宏开始:

SWAPGS_UNSAFE_STACK

该宏在arch / x86 / include / asm / irqflags.h头文件中定义,并扩展为swapgs指令

In the end we just call the USERGS_SYSRET64 macro that expands to the call of the swapgs instruction which exchanges again the user GS and kernel GS???

TAG

网友评论

共有访客发表了评论
请登录后再发布评论,和谐社会,请文明发言,谢谢合作! 立即登录 注册会员