【一,准备工作】
1,选择一个系统调用
由于学号末两位是32,故选择32号系统调用,查阅系统调用表(./tools/perf/arch/x86/entry/syscalls/syscall_64.tbl)知32号系统调用是dup函数。
2,dup函数简介
dup函数的功能是赋值一个现存的文件描述符,下面给出函数声明
#includeint dup(int oldfd);
dup用来复制参数oldfd所指的文件描述符。当复制成功是,返回最小的尚未被使用过的文件描述符,若有错误则返回-1.错误代码存入errno中返回的新文件描述符和参数oldfd指向同一个文件,这两个描述符共享同一个数据结构,共享所有的锁定,读写指针和各项全现或标志位。
调用dup(oldfd)等效于fcntl(oldfd, F_DUPFD, 0)
3,编写测试程序,用于使用qemu模拟器触发系统调用系统调用
嵌入式汇编方式进行系统调用
4,编写Makefile文件,将待测试程序加入qemu模拟器
# # Makefile for Menu Program # CC_PTHREAD_FLAGS = -lpthread CC_FLAGS = -c CC_OUTPUT_FLAGS = -o CC = gcc RM = rm RM_FLAGS = -f TARGET = test OBJS = linktable.o menu.o test.o all: $(OBJS) $(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) rootfs: gcc -o init linktable.c menu.c test.c -m32 -static -lpthread gcc -o hello hello.c -m32 -static find init hello | cpio -o -Hnewc |gzip -9 > ../rootfs.img qemu-system-x86_64 -kernel ../linux_kernel/arch/x86/boot/bzImage -initrd ../rootfs.img -S -s .c.o: $(CC) $(CC_FLAGS) $< clean: $(RM) $(RM_FLAGS) $(OBJS) $(TARGET) *.bak
【二,基本理论】
int0x80方式的系统调用
通过中断/异常实现,在执行 int 指令时,发生 trap。硬件找到在中断描述符表中的表项,在自动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs ,将 offset 加载到 eip。最后硬件将 ss / sp / eflags / cs / ip / error code 依次压到内核栈。返回时,iret 将先前压栈的 ss / sp / eflags / cs / ip 弹出,恢复用户态调用时的寄存器上下文。
中断描述符表的初始化(idt.c)
调用流程(entry_32.S)
ENTRY(entry_INT80_32) ASM_CLAC pushl %eax /* pt_regs->orig_ax */ TRACE_IRQS_OFF movl %esp, %eax call do_int80_syscall_32 .Lsyscall_32_done: STACKLEAK_ERASE restore_all: TRACE_IRQS_IRET SWITCH_TO_ENTRY_STACK .Lrestore_all_notrace: CHECK_AND_APPLY_ESPFIX .Lrestore_nocheck: /* Switch back to user CR3 */ SWITCH_TO_USER_CR3 scratch_reg=%eax BUG_IF_WRONG_CR3 /* Restore user state */ RESTORE_REGS pop=4 # skip orig_eax/error_code .Lirq_return: INTERRUPT_RETURN
syscall方式的系统调用
sysenter和syscall都借助CPU内部的MSR寄存器来存放,所以查找系统调用处理入口地址(cpu.c)会更快,因此也称为快速系统调用
调用流程
ENTRY(entry_SYSCALL_64) UNWIND_HINT_EMPTY 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 */ TRACE_IRQS_IRETQ /* we're about to change IF */ movq RCX(%rsp), %rcx movq RIP(%rsp), %r11 cmpq %rcx, %r11 /* SYSRET requires RCX == RIP */ jne swapgs_restore_regs_and_return_to_usermode
【三,动手调试】
1,打开qemu,启动gdb
qemu-system-x86_64 -kernel ../linux_kernel/arch/x86/boot/bzImage -initrd ../rootfs.img -S -s
2,设置断点,触发系统调用
3,开始调试
SYSCALL_DEFINEx:就是将系统调用的参数统一变为了使用long型来接收,再强转转为int,也就是系统调用本来传下来的参数类型。
include/asm-arm/unistd.h文件中定义了7个_syscall宏,分别是:
_syscall0(type, name)
_syscall1(type, name,type1,arg1)
_syscall2(type, name,type1,arg1,type2,arg2)
_syscall3(type, name,type1,arg1,type2,arg2,type3,arg3)
_syscall4(type, name,type1,arg1,type2,arg2,type3, arg3,type4,arg4)
_syscall5(type, name,type1,arg1,type2,arg2,type3, arg3,type4,arg4,type5,arg5)
_syscall6(type, name,type1,arg1,type2,arg2,type3, arg3,type4,arg4,type5,arg5,type6,arg6)
这7个宏是用来产生系统调用的函数名的,其中type表示系统调用的返回值类型,name表示该系统调用的名称,typeN、argN分别表示第N个参数的类型和名称,它们的数目和_syscall后面的数字一样大。
do_syscall_32_irqs_on: 现场保存完毕后,关闭中断,将当前栈指针保存到 eax ,调用 do_int80_syscall_32 => do_syscall_32_irqs_on ,该函数在 arch/x86/entry/common.c
中定义,这个函数的参数 regs(struct pt_regs 定义见 arch/x86/include/asm/ptrace.h
)就是先前在 entry_INT80_32 依次被压入栈的寄存器值。这里先取出系统调用号,从系统调用表(ia32_sys_call_table) 中取出对应的处理函数,然后通过先前寄存器中的参数调用之。
当我们在用户态调用dup时,当运行到int &0x80中断指令时,会跳转到entry_INT80_32,这是Linux系统调用的入口,而在64位系统中对应的则是entry_SYSCALL_compat,它是一段汇编代码,entry_SYSCALL_compat通过系统调用号来查询对应的内核处理函数并跳转到相应的内核处理函数执行,执行完毕后再按顺序逐步返回到用户态。上述过程为cpu保存现场。
调用完毕后恢复现场并执行中断返回指令
【四,实验总结】
计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。
一般情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程。在Unix世界,最流行的API是基于POSIX标准的。
操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。
中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。
一般地,系统调用都是通过软件中断实现的,x86系统上的软件中断由int $0x80指令产生,而128号异常处理程序就是系统调用处理程序system_call(),它与硬件体系有关,在entry.S中用汇编写。接下来就来看一下Linux下系统调用具体的实现过程。
系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:
-
它为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。
-
系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他什么危害系统的事情。
-
每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中,系统调用是用户空间访问内核的惟一手段;除异常和中断外,它们是内核惟一的合法入口。