转自:http://tsengyia.blog.chinaunix.net/uid-30156195-id-4938101.html
赵建清+原创作品转载请注明出处+《Linux内核分析》MOOC课程http://mooc.study.163.com/learn/USTC-1000029000
概述
在Linux系统中应用程序发起系统调用后,使用int $0x80或sysenter汇编指令将CPU切换到内核态,然后开始从地址system_call处执行命令。代码分析
IA-32体系结构下system_call的定义位于内核代码arch/x86/kernel/entry_32.S文件中,由文件名即可知道system_call为汇编代码。下面附上linux-3.18.6内核版本中system_call的代码:
- ENTRY(system_call)
- RING0_INT_FRAME # can't unwind into user space anyway
- ASM_CLAC
- pushl_cfi %eax # save orig_eax
- SAVE_ALL
- GET_THREAD_INFO(%ebp)
- # 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_call:
- call *sys_call_table(,%eax,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
- 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
RING0_INT_FRAME的定义也在相同的文件中,用于设置堆栈指针寄存器esp和指令指针寄存器eip,使其指向内核:
.macro RING0_INT_FRAME
CFI_STARTPROC simple
CFI_SIGNAL_FRAME
CFI_DEF_CFA esp, 3*4
/*CFI_OFFSET cs, -2*4;*/
CFI_OFFSET eip, -3*4
.endm
第4行将通用寄存器eax入栈,用于保存系统调用号。
第5行宏SAVE_ALL在栈中保存中断处理程序可能会使用到的所有CPU寄存器,但不包括eflags、cs、eip、ss和esp:
.macro SAVE_ALL
cld
PUSH_GS
pushl_cfi %fs
/*CFI_REL_OFFSET fs, 0;*/
pushl_cfi %es
/*CFI_REL_OFFSET es, 0;*/
pushl_cfi %ds
/*CFI_REL_OFFSET ds, 0;*/
pushl_cfi %eax
CFI_REL_OFFSET eax, 0
pushl_cfi %ebp
CFI_REL_OFFSET ebp, 0
pushl_cfi %edi
CFI_REL_OFFSET edi, 0
pushl_cfi %esi
CFI_REL_OFFSET esi, 0
pushl_cfi %edx
CFI_REL_OFFSET edx, 0
pushl_cfi %ecx
CFI_REL_OFFSET ecx, 0
pushl_cfi %ebx
CFI_REL_OFFSET ebx, 0
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
SET_KERNEL_GS %edx
.endm
第6行ebp用于存放当前进程thread_info结构的地址。
第8行检查是否有某一调试程序正在跟踪执行程序对系统调用的调用,如果需要追踪当前执行程序,则调转到syscall_trace_entry,调试进程会收集必要的信息。
第9行对用户态进程传来的系统调用号进行有效性检查。如果这个系统调用号不在有效范围内,系统调用处理程序就终止,并在eax中设置错误返回码。
第12行调用与eax中所包含的系统调用号对应的特定服务例程,因为分派表中的每个表项占4个字节,因此首先把系统调用号乘以4,再加上sys_call_table分派表的起始地址,内核就找到了要调用的服务例程。
第18行和第21行关闭本地中断并检查当前进程的thread_info结构中的标志。如果所有的标志没有被设置则跳转到restore_all标记处。
第29行到第35行用于恢复保存在内核栈中的寄存器的值。
第44行执行iret汇编指令重新开始执行用户态进程。
.macro RING0_INT_FRAME
CFI_STARTPROC simple
CFI_SIGNAL_FRAME
CFI_DEF_CFA esp, 3*4
/*CFI_OFFSET cs, -2*4;*/
CFI_OFFSET eip, -3*4
.endm
第4行将通用寄存器eax入栈,用于保存系统调用号。
第5行宏SAVE_ALL在栈中保存中断处理程序可能会使用到的所有CPU寄存器,但不包括eflags、cs、eip、ss和esp:
.macro SAVE_ALL
cld
PUSH_GS
pushl_cfi %fs
/*CFI_REL_OFFSET fs, 0;*/
pushl_cfi %es
/*CFI_REL_OFFSET es, 0;*/
pushl_cfi %ds
/*CFI_REL_OFFSET ds, 0;*/
pushl_cfi %eax
CFI_REL_OFFSET eax, 0
pushl_cfi %ebp
CFI_REL_OFFSET ebp, 0
pushl_cfi %edi
CFI_REL_OFFSET edi, 0
pushl_cfi %esi
CFI_REL_OFFSET esi, 0
pushl_cfi %edx
CFI_REL_OFFSET edx, 0
pushl_cfi %ecx
CFI_REL_OFFSET ecx, 0
pushl_cfi %ebx
CFI_REL_OFFSET ebx, 0
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
SET_KERNEL_GS %edx
.endm
第6行ebp用于存放当前进程thread_info结构的地址。
第8行检查是否有某一调试程序正在跟踪执行程序对系统调用的调用,如果需要追踪当前执行程序,则调转到syscall_trace_entry,调试进程会收集必要的信息。
第9行对用户态进程传来的系统调用号进行有效性检查。如果这个系统调用号不在有效范围内,系统调用处理程序就终止,并在eax中设置错误返回码。
第12行调用与eax中所包含的系统调用号对应的特定服务例程,因为分派表中的每个表项占4个字节,因此首先把系统调用号乘以4,再加上sys_call_table分派表的起始地址,内核就找到了要调用的服务例程。
第18行和第21行关闭本地中断并检查当前进程的thread_info结构中的标志。如果所有的标志没有被设置则跳转到restore_all标记处。
第29行到第35行用于恢复保存在内核栈中的寄存器的值。
第44行执行iret汇编指令重新开始执行用户态进程。
总结
system_call是发起系统调用的必经之路,在执行之前CPU处于用户态,执行system_call过程中系统进入内核态,执行系统调用服务例程,system_call
返回后又切换回用户态。