深入理解系统调用

实验要求

  • 找一个系统调用,系统调用号为学号最后2位相同的系统调用
  • 通过汇编指令触发该系统调用
  • 通过gdb跟踪该系统调用的内核处理过程
  • 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化

系统调用原理: https://www.cntofu.com/book/104/SysCall/syscall-1.md

实验内容

查找系统调用

查看linux内核arch/x86/entry/syscalls/syscall_64.tbl下的64位x86系统对应的系统调用表,并找到05所对应的系统
调用号。

深入理解系统调用

根据上图可以看到,05所对应的系统调用为__x64_sys_newfstat,其对应的API函数为fstat

编写汇编代码触发系统调用

我们首先根据man手册来对fstat函数进行查看,查看结果如下。

深入理解系统调用

可以看到fstatAPI函数需要传入两个参数,一个参数是int类型的文件句柄,另一个参数是struct stat类型的指针,其用来保存fstat产生的输出。
其中struct stat结构是linux中用来保存文件的各个属性值的结构体,因此实际上fstat函数的作用就是获取文件句柄所指向的文件的基本信息,并将
基本信息保存到statbuf中。

我们通过编写一个建议的c语言程序来测试fstat的具体功能,测试代码如下。

#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int agrc, char *agrv[])
{
   struct stat buf;
   int fd;
   fd = open("./test.txt", O_RDONLY);
   fstat(fd, &buf);
   printf("test.txt file size %ld\n ", buf.st_size);
   return 0;
}

我们再当前路径下创建一个test.txt,并编译运行该程序,可以得到如下结果。

深入理解系统调用

在了解了fstat系统调用函数的主要功能后,我们便可以通过编写汇编代码来触发系统调用。

#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int agrc, char *agrv[])
{
   struct stat buf;
   int fd;
   fd = open("./test.txt", O_RDONLY);
   int ret;

   asm volatile(
                "movq %2, %%rsi\n\t"
                "movq %1, %%rdi\n\t"
                "movl $0x05, %%eax\n\t"
                "syscall\n\t"
                "movq %%rax,%0\n\t"
                :"=m"(ret)
                :"m"(fd), "p"(&buf)  //由于这里第二个参数传递的是struct stat*指针类型,因此限定符应为p
                );

   printf("test.txt file size %ld\n ", buf.st_size);
   return 0;
}

编译并运行后结果如下所示。

深入理解系统调用

gdb跟踪分析系统调用过程

执行下列命令,重新打包根文件系统,开启虚拟机gdb调试。

$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
$ qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

开启新的terminal

$ cd linux-5.4.34
$ gdb vmlinux
$ target remote:1234
$ c

在对应的系统调用入口处打好断点后,执行fstat_test应用程序,并在gdb调试中使用bt查看当前堆栈。

深入理解系统调用

根据上图可以看出,系统调用的进入点为entry_SYSCALL_64,我们找到系统调用的入口并查看相应汇编代码。

ENTRY(entry_SYSCALL_64)
	UNWIND_HINT_EMPTY
	/*
	 * Interrupts are off on entry.
	 * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
	 * it is too small to ever cause noticeable irq latency.
	 */

	swapgs
	/* tss.sp2 is scratch space. */
	movq	%rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
	SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
	movq	PER_CPU_VAR(cpu_current_top_of_stack), %rsp

	/* Construct struct pt_regs on stack */
	pushq	$__USER_DS				/* pt_regs->ss */
	pushq	PER_CPU_VAR(cpu_tss_rw + TSS_sp2)	/* pt_regs->sp */
	pushq	%r11					/* pt_regs->flags */
	pushq	$__USER_CS				/* pt_regs->cs */
	pushq	%rcx					/* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
	pushq	%rax					/* pt_regs->orig_ax */

	PUSH_AND_CLEAR_REGS rax=$-ENOSYS

	TRACE_IRQS_OFF

	/* IRQs are off. */
	movq	%rax, %rdi
	movq	%rsp, %rsi
	call	do_syscall_64		/* returns with IRQs disabled */

可以看出,再执行中断函数do_syscall_64前,内核会先进行以下操作。

  1. swapgs指令切换gs寄存器从用户态到内核态(其实就是修改了运行级别,使其可以访问内核)。
  2. 保存中断上下文中的rsp寄存器的值。
  3. SWITCH_TO_KERNEL_CR3 切换到内核堆栈空间。
  4. 构建内核堆栈结构体的基本成员变量,保存通用目的寄存器到内核堆栈空间。
  5. TRACE_IRQS_OFF 关闭中断追踪
  6. 将rax系统调用号和rsp内核堆栈地址保存到rdi和rsi寄存器中。
  7. 执行do_syscall_64

我们继续查看do_syscall_64部分代码。

#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
	struct thread_info *ti;

	enter_from_user_mode();
	local_irq_enable();
	ti = current_thread_info();
	if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
		nr = syscall_trace_enter(regs);

	if (likely(nr < NR_syscalls)) {
		nr = array_index_nospec(nr, NR_syscalls);
		regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
	} else if (likely((nr & __X32_SYSCALL_BIT) &&
			  (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
		nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
					X32_NR_syscalls);
		regs->ax = x32_sys_call_table[nr](regs);
#endif
	}

	syscall_return_slowpath(regs);
}
#endif

do_syscall_64代码主要完成以下几个工作。

  1. 根据系统调用号查找系统调用表得到系统调用函数。
  2. 执行系统调用函数。

此时查找到系统调用函数后,对执行newfstat对应的系统调用函数。

SYSCALL_DEFINE2(newfstat, unsigned int, fd, struct stat __user *, statbuf)
{
	struct kstat stat;
	int error = vfs_fstat(fd, &stat);

	if (!error)
		error = cp_new_stat(&stat, statbuf);

	return error;
}

可以看到,在newfstat系统调用中,会执行vfs_fstat,实际上vfs_fstat函数会调用vfs_statx_fd函数,该函数才是最终获取文件属性的执行函数。

*
 * vfs_statx_fd - Get the enhanced basic attributes by file descriptor
 * @fd: The file descriptor referring to the file of interest
 * @stat: The result structure to fill in.
 * @request_mask: STATX_xxx flags indicating what the caller wants
 * @query_flags: Query mode (KSTAT_QUERY_FLAGS)
 *
 * This function is a wrapper around vfs_getattr().  The main difference is
 * that it uses a file descriptor to determine the file location.
 *
 * 0 will be returned on success, and a -ve error code if unsuccessful.
 */
int vfs_statx_fd(unsigned int fd, struct kstat *stat,
		 u32 request_mask, unsigned int query_flags)
{
	struct fd f;
	int error = -EBADF;

	if (query_flags & ~KSTAT_QUERY_FLAGS)
		return -EINVAL;

	f = fdget_raw(fd);
	if (f.file) {
		error = vfs_getattr(&f.file->f_path, stat,
				    request_mask, query_flags);
		fdput(f);
	}
	return error;
}

在执行vfs_statx_fd完毕后,返回fstat的系统调用函数,继续执行cp_new_stat函数,该函数的作用就是将从vfs_statx_fd中生成的
信息拷贝到用户在API传入的参数指针所指向的内存空间中。至此系统调用函数已经执行完成。

返回do_syscall_64函数,继续执行syscall_return_slowpath函数。

__visible inline void syscall_return_slowpath(struct pt_regs *regs)
{
	struct thread_info *ti = current_thread_info();
	u32 cached_flags = READ_ONCE(ti->flags);

	CT_WARN_ON(ct_state() != CONTEXT_KERNEL);

	if (IS_ENABLED(CONFIG_PROVE_LOCKING) &&
	    WARN(irqs_disabled(), "syscall %ld left IRQs disabled", regs->orig_ax))
		local_irq_enable();

	rseq_syscall(regs);

	/*
	 * First do one-time work.  If these work items are enabled, we
	 * want to run them exactly once per syscall exit with IRQs on.
	 */
	if (unlikely(cached_flags & SYSCALL_EXIT_WORK_FLAGS))
		syscall_slow_exit_work(regs, cached_flags);

	local_irq_disable();
	prepare_exit_to_usermode(regs);
}

该函数的目的主要是清空一些cpu内核线程状态,关闭中断,为返回用户模式做准备。
执行完该函数后,正式完成了整个系统调用过程,开始由内核态向用户态返回。

        movq	RCX(%rsp), %rcx
	movq	RIP(%rsp), %r11

	cmpq	%rcx, %r11	/* SYSRET requires RCX == RIP */
	jne	swapgs_restore_regs_and_return_to_usermode

	/*
	 * On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
	 * in kernel space.  This essentially lets the user take over
	 * the kernel, since userspace controls RSP.
	 *
	 * If width of "canonical tail" ever becomes variable, this will need
	 * to be updated to remain correct on both old and new CPUs.
	 *
	 * Change top bits to match most significant bit (47th or 56th bit
	 * depending on paging mode) in the address.
	 */
#ifdef CONFIG_X86_5LEVEL
	ALTERNATIVE "shl $(64 - 48), %rcx; sar $(64 - 48), %rcx", \
		"shl $(64 - 57), %rcx; sar $(64 - 57), %rcx", X86_FEATURE_LA57
#else
	shl	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
	sar	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
#endif

	/* If this changed %rcx, it was not canonical */
	cmpq	%rcx, %r11
	jne	swapgs_restore_regs_and_return_to_usermode

	cmpq	$__USER_CS, CS(%rsp)		/* CS must match SYSRET */
	jne	swapgs_restore_regs_and_return_to_usermode

	movq	R11(%rsp), %r11
	cmpq	%r11, EFLAGS(%rsp)		/* R11 == RFLAGS */
	jne	swapgs_restore_regs_and_return_to_usermode

	/*
	 * SYSCALL clears RF when it saves RFLAGS in R11 and SYSRET cannot
	 * restore RF properly. If the slowpath sets it for whatever reason, we
	 * need to restore it correctly.
	 *
	 * SYSRET can restore TF, but unlike IRET, restoring TF results in a
	 * trap from userspace immediately after SYSRET.  This would cause an
	 * infinite loop whenever #DB happens with register state that satisfies
	 * the opportunistic SYSRET conditions.  For example, single-stepping
	 * this user code:
	 *
	 *           movq	$stuck_here, %rcx
	 *           pushfq
	 *           popq %r11
	 *   stuck_here:
	 *
	 * would never get past 'stuck_here'.
	 */
	testq	$(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
	jnz	swapgs_restore_regs_and_return_to_usermode

	/* nothing to check for RSP */

	cmpq	$__USER_DS, SS(%rsp)		/* SS must match SYSRET */
	jne	swapgs_restore_regs_and_return_to_usermode

	/*
	 * We win! This label is here just for ease of understanding
	 * perf profiles. Nothing jumps here.
	 */
syscall_return_via_sysret:
	/* rcx and r11 are already restored (see code above) */
	UNWIND_HINT_EMPTY
	POP_REGS pop_rdi=0 skip_r11rcx=1

	/*
	 * Now all regs are restored except RSP and RDI.
	 * Save old stack pointer and switch to trampoline stack.
	 */
	movq	%rsp, %rdi
	movq	PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp

	pushq	RSP-RDI(%rdi)	/* RSP */
	pushq	(%rdi)		/* RDI */

	/*
	 * We are on the trampoline stack.  All regs except RDI are live.
	 * We can do future final exit work right here.
	 */
	STACKLEAK_ERASE_NOCLOBBER

	SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

	popq	%rdi
	popq	%rsp
	USERGS_SYSRET64

这段汇编代码主要完成了以下工作:

  1. swapgs_restore_regs_and_return_to_usermode 恢复通用寄存器并准备恢复到用户态。
  2. 切换到原来的用户态堆栈上。

深入理解系统调用

上一篇:深入理解libaio 接口


下一篇:Linux系统初始化基础原理笔记