本篇文章通过将上篇文章中使用库函数API和C代码中嵌入汇编代码两种方式设计的系统调用添加到系统menu中,来说明在Linux系统中,系统调用的实现的时机以及具体运行,以及一般的中断处理过程。
相关知识
首先关于这篇文章会介绍一些用到的知识。
一、将系统调用号与相应的服务例程关联起来,内核利用了一个系统调用分派表(dispatch table)。这个表存放在sys_call_table数组中,有NR_syscalls个表项:第n个表项包含系统调用号为n的服务例程的地址。
二、因为这篇文章和上篇文章是上下文关系,所以其他相关知识请参考上篇文章:《Linux操作系统分析》之使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用。
分析过程
首先我们将自己写的系统调用fetpid和getpid-asm放进Menu系统的中。需要进行的操作有:
0)更新menuOS到最新版本
1)在test.c中的main函数中添加MenuConfig
2)添加对应的getpid函数和getpid-asm函数
3)make rootfs
如果在上面的操作中出现了什么问题,可以去https://github.com/mengning查询代码等。
第零步
rm menu -rf 强制删除原menu文件 git clone http://github.com/mengning/menu.git 从github中克隆
第一二步结果如下图:
对其进行编译运行,即第三步
运行结果如上图。
设置断点进行跟踪:
我们发现断点进入到entry_32.s中。但是在这里没有停止,而是直接运行结束了。
在进入entry_32.s之前,运行的过程如下:
1)main.c中start_kernel函数:trap_init()
2)set_system_trap_gate(SYSCALL_VECTOR,&system_call)
SYSCALL_VECTOR:系统调用的中断向量
&system_call:汇编代码入口
3)一执行int 0x80,系统直接跳转到system_call。
然后执行到了entry_32.s,我们将entry_32.s文件打开,看一下里面的内容。
# system call handler stub ENTRY(system_call) #系统调用处理入口(内核态) RING0_INT_FRAME # can't unwind into user space anyway ASM_CLAC pushl_cfi %eax # save orig_eax #保存eax,也就是调用号 SAVE_ALL # 保存所有会被覆盖的寄存器信息 GET_THREAD_INFO(%ebp) # 当前进程的thread_info结构的地址,获取当前进程的信息。</span> # system call tracing in operation / emulation testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) # 检测是否由系统跟踪 jnz syscall_trace_entry # 有系统跟踪则先去执行 cmpl $(NR_syscalls), %eax # 比较输入的系统调用号 是否大于等于 最大的系统调用号 jae syscall_badsys # 大于或等于则无效,跳转到syscall_badsys,小于则跳转到相应系统调用号所对应的服务例程当中。 syscall_call: call *sys_call_table(,%eax,4) # 在系统调用表中的调用相应的服务例程,eax为调用号。sys_call_table表的表项占4字节。 syscall_after_call: movl %eax,PT_EAX(%esp) # store the return value # 保存返回值 syscall_exit: LOCKDEP_SYS_EXIT # 用于调试,只有开启调试后才会检测系统调用深度 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt # setting need_resched or sigpending # between sampling and the iret TRACE_IRQS_OFF # 关闭中断跟踪 movl TI_flags(%ebp), %ecx # 检测是否还有其他任务 testl $_TIF_ALLWORK_MASK, %ecx # current->work jne syscall_exit_work<span style="white-space:pre"> </span>#如果有其他的任务,则跳转到syscall_exit_work。没有就执行restore_all等恢复现场的动作。 syscall_exit_work: testl $_TIF_WORK_SYSCALL_EXIT, %ecx jz work_pending # 测试是否退出前还有工作要处理,如果有的话跳转到work_pending TRACE_IRQS_ON # 开启系统中断跟踪 ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call # 允许中断 # schedule() instead movl %esp, %eax call syscall_trace_leave jmp resume_userspace # 恢复用户空间 END(syscall_exit_work) work_pending: testb $_TIF_NEED_RESCHED, %cl # 是否有需要继续调度的相关信号 jz work_notifysig # 跳转到处理信号相关的代码处 work_resched: call schedule # 时间调度, 进程调度的时机在这里处理 LOCKDEP_SYS_EXIT DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt # setting need_resched or sigpending # between sampling and the iret TRACE_IRQS_OFF<span style="white-space:pre"> </span>#系统中断跟踪关闭 movl TI_flags(%ebp), %ecx andl $_TIF_WORK_MASK, %ecx # is there any work to be done other 是否还有其他工作要处理 # than syscall tracing? jz restore_all #如果没有的话就恢复中断上下文,也就是恢复进入之前保存的寄存器相关内容 testb $_TIF_NEED_RESCHED, %cl jnz work_resched work_notifysig: # deal with pending signals and # notify-resume requests #ifdef CONFIG_VM86 testl $X86_EFLAGS_VM, PT_EFLAGS(%esp) movl %esp, %eax jne work_notifysig_v86 # returning to kernel-space or # vm86-space restore_all: TRACE_IRQS_IRET # 恢复中断跟踪 restore_all_notrace: #ifdef CONFIG_X86_ESPFIX32 movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS # Warning: PT_OLDSS(%esp) contains the wrong/random values if we # are returning to the kernel. # See comments in process.c:copy_thread() for details. movb PT_OLDSS(%esp), %ah movb PT_CS(%esp), %al andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax CFI_REMEMBER_STATE je ldt_ss # returning to user-space with LDT SS #endif restore_nocheck: RESTORE_REGS 4 # skip orig_eax/error_code irq_return: INTERRUPT_RETURN在上面代码中我们知道无论是中断返回(ret_from_intr) ,还是系统调用返回,都使用了 work_pending 和resume_userspace。
对于宏SAVE_ALL来说,会把将寄存器的值压入堆栈当中,压入顺序对应struct pt_regs ,出栈时亦然。struct pt_regs可以在ptrace.h中查看。
系统调用的流程图如下:
总结:
1)系统调用的初始化的顺序是:start_kernel()->trap_init()->set_system_trap_gates(SYSCALL_VECTOR,&system_call);
2)用户态到内核态通过0x80进行中断,在内核初始化期间调用trap_init(),用函数set_system_trap_gates(),建立了对应于向量128的中断描述符表表项,从而进入相应的中断服务。
3)system_call()函数首先将系统调用号或中断处理程序需要用到的所有的CPU寄存器保存到相应的栈中。然后进行服务的处理。当系统调用服务例程结束时,system_call()函数从eax获得它的返回值。然后进行一系列的检查,最后恢复用户态进程的执行。
如果想看详细的讲解,请大家参考:深入理解linux内核的第十章系统调用。
备注:
杨峻鹏 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000