实验要求
- 找一个系统调用,系统调用号为学号最后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
函数进行查看,查看结果如下。
可以看到fstat
API函数需要传入两个参数,一个参数是int类型的文件句柄,另一个参数是struct stat类型的指针,其用来保存fstat产生的输出。
其中struct stat结构是linux中用来保存文件的各个属性值的结构体,因此实际上fstat函数的作用就是获取文件句柄所指向的文件的基本信息,并将
基本信息保存到statbuf中。
我们通过编写一个建议的c语言程序来测试fstat的具体功能,测试代码如下。
#include
#include
#include
#include
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
#include
#include
#include
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前,内核会先进行以下操作。
- swapgs指令切换gs寄存器从用户态到内核态(其实就是修改了运行级别,使其可以访问内核)。
- 保存中断上下文中的rsp寄存器的值。
- SWITCH_TO_KERNEL_CR3 切换到内核堆栈空间。
- 构建内核堆栈结构体的基本成员变量,保存通用目的寄存器到内核堆栈空间。
- TRACE_IRQS_OFF 关闭中断追踪
- 将rax系统调用号和rsp内核堆栈地址保存到rdi和rsi寄存器中。
- 执行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代码主要完成以下几个工作。
- 根据系统调用号查找系统调用表得到系统调用函数。
- 执行系统调用函数。
此时查找到系统调用函数后,对执行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
这段汇编代码主要完成了以下工作:
- swapgs_restore_regs_and_return_to_usermode 恢复通用寄存器并准备恢复到用户态。
- 切换到原来的用户态堆栈上。