Linux内核system_call中断处理过程
“平安的祝福 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
menu中添加指令
在相应的test.c中添加getpid和getpid-asm的函数,使Menu实现getpid和getpid-asm的命令。
添加完成后,修改menu目录下的Makefile文件中的 qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img变为qemu-system-i386 -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
然后执行在menu目录下:终端输入make rootfs
运行效果:
调试
按照网址:http://www.cnblogs.com/pingandezhufu/p/4392297.html的方法进行调试menu的time指令:
设置断点:b sys_time,运行的结果:
设置断点sys_call运行的结果:
system_call 调用处理过程
应用程序告诉内核自己需要执行一个系统调用时,通知内核的机制是靠软中断实现的:通过引发一个异常促使系统切换到内核去执行异常处理程序。该异常处理程序实际就是系统调用处理程序。在x86系统上。通过int$0x80指令触发该中断。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序----系统调用处理程序。系统调用处理程序叫system_call(),它与硬件体系结构紧密相关,在x86-64位系统上在entry_64.S文件中,用汇编语言编写。
system_call函数通过将给定的系统调用号(eax的值)与NR_syscalls做比较来检查其有效性。如果它大于或等于NR_syscalls,该函数返回-ENOSYS。否则执行相应的系统调用:call *sys_call_table(,%rax,4)。由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4。
除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入,所以需要把这些参数从用户空间传给内核。最简单的方法就是像传递系统调用参数那样也存放在寄存器里。在x83-32系统上,ebx,ecx,edx,esi,edi按照顺序存放前五个参数。需要六个或者以上的参数,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。给用户空间的返回值也通过寄存器传递。在x86系统上存放在eax寄存器里。
system_call 的处理流程:
通过int $0x80指令发出系统调用,向量128对应于内核入口点。在内核初始化期间start_kernel函数中调用trap_init(),trap_init()函数中hanset_system_gate(SYSCALL_VECTOR,&system_call)完成对应向量128的中断描述符表表项。该调用把下列值存入这个门描述符的相应字段:
Segment Selector 内核代码段__KERNEL_CS的段选择符。Offset 指向system_call()系统调用处理程序的指针。Type置为15.表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断。DPL(描述符特 权级) 置为3,表示允许用户进程调用这个异常处理程序。
systen_call()函数首先把系统调用号和这个异常处理程序可以用到的所有CPU寄存器保存到相应的栈中,不包括由控制单元已自动保存的eflags,cs,eip,ss和esp寄存器。代码例子:
pushl %eax # save orig_eax SAVE_ALL GET_THREAD_INFO(%ebp) # system call tracing in operation testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsys
随后,这个函数GET_THREAD_INFO(%ebp)使ebp寄存器存储thread information struct的结构地址,这是通过获得内核栈指针的值并把它取整到4KB或者8KB的倍数而完成的。
函数原型
#define GET_THREAD_INFO(reg) \ movq %gs:pda_kernelstack,reg ; \ subq $(THREAD_SIZE-PDA_STACKOFFSET),reg #endif
接下来,检查thread_info结构的flags字段的_TIF_SYSCALL_TRACE和_TIF_SYSCALL_AUDIT是否置为1,也就是检查是否有某一个调试程序正在跟踪执行程序对于系统调用的调用。如果是,那么system_call()函数进入syscall_trace_entry两次调用do_syscall_trace()函数:一次正好这个系统调用服务例程执行之前,一次在其之后。这个函数主要是停止current,并允许调试进程收集关于current的信息。
cmpl $(nr_syscalls), %eax jae syscall_badsys
然后这句汇编,对于用户态进程传递来的系统调用号进行有效性检查。如果这个号大于或等于系统调用分派表中的表项数,系统执行syscall_badsys块,把-ENOSYS的值存放在栈中曾保存eax寄存器的单元中,然后跳到resume_userspace。这样当进程恢复在用户态的执行时,会在eax中发现一个负的返回码。
最后调用与eax中包含的系统调用号对应的特定服务例程:call *sys_call_table(,%eax,4),由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,再加上sys_call_table分派表的起始地址,然后从这个地址单元获取指向服务例程的指针,这样内核就找到了要调用的服务例程。
movl %eax,EAX(%esp),当系统调用服务例程结束时,system_call()函数从eax获得它的返回值,并把这个返回值存放在曾保存用户态eax寄存器值得那个栈单元的位置上。
syscall_exit: cli # make sure we don't miss an interrupt # setting need_resched or sigpending # between sampling and the iret movl TI_flags(%ebp), %ecx testw $_TIF_ALLWORK_MASK, %cx # current->work jne syscall_exit_work restore_all: RESTORE_ALL
然 后system_call()函数关闭本地中断并检查当前进程的thread_info结构中的标志,如果所有的标志都没有设置,函数执行 restore_all标记处的代码恢复保存在内核栈中的寄存器的值,并执行iret汇编语言指令以重新开始执行用户态进程。只要有任何一种标志被设置, 那么就执行syscall_exit_work。
syscall_exit_work: testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP), %cl jz work_pending sti # could let do_syscall_trace() call # schedule() instead movl %esp, %eax movl $1, %edx call do_syscall_trace jmp resume_userspace ALIGN
在syscall_exit_work中,检查是否有_TIF_SYSCALL_TRACE或者_TIF_SYSCALL_AUDIT或者_TIF_SINGLESTEP标志被设置,有则执行do_syscall_trace,然后跳转到resume_userspace。否则,如果_TIF_SYSCALL_TRACE或者_TIF_SYSCALL_AUDIT或者_TIF_SINGLESTEP标志没有被设置,函数跳转到work_pending标记处。
work_pending:
testb $_TIF_NEED_RESCHED, %cl #检查是否需要重新调度
jz work_notifysig #不需要重新调度 #需要重新调度
work_resched:
call schedule #调度进程
在work_pending处,检查是否需要调度,需要的话就执行call schedule;否则不需要的话,跳转到work_notifysig处。
在work_notifysig处,用于处理信号,然后restore_all。