Linux内核分析:实验五–使用GDB跟踪系统调用执行过程
刘畅 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
概述
本次实验使用GDB跟踪一个系统调用,上一次实验分别使用了C语言和嵌入式汇编实现了write系统调用。这次实验把上次实验写的系统调用代码加入MenuOS中,并且跟踪这个系统调用的执行的过程。
回顾一下write系统调用的实现代码
#include
int main()
{
write( 1, "hello world\n", 12 );
return 0;
}
int main()
{
int result;
char * str = "hello world\n";
__asm__(
"movl $4, %%eax\n\t"
"movl $1, %%ebx\n\t"
"movl %1, %%ecx\n\t"
"movl $12, %%edx\n\t"
"int $0x80\n\t"
"movl %%eax, %0"
:"=m"(result)
:"D"(str)
);
return 0;
}
在MenuOS中加入write系统调用
把之前的menu文件夹删掉,并重新克隆老师的menu代码,过程如下:
cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
然后在test.c中把上面两个函数加入进去,如图所示
最后menuOS中输入write调用此系统调用。如图所示
跟踪write系统调用执行过程
像实验三中一样,我们在启动内核的时候,暂时‘冻住’CPU。
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
然后打开GDB设置断点,如下所示:
b sys_write
当在menuOS中输入Write命令时,GDB中就会在sys_write系统调用处暂停,如下图所示:
内核系统调用的源码分析
在32位x86机器下,Linux关于系统调用这部分的代码放在了arch/x86/kernel/entry_32.S中。这部分的代码使用汇编语言写的,如下所示:
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
系统调用的流程
系统调用是0x80号软中断而引发的调用,0x80号中断的处理程序是system_call,也就是上面所列的代码。当检测到系统调用发生时(int 0x80中断),第一步先保存现场,通过一个宏指令SAVE_ALL实现的,这个指令是把寄存器的状态通过压栈的方式保存起来。
然后会调用sys_call_table,通过eax寄存器的值查找系统调用表,找到几号系统调用,然后调用相应的系统调用。
当系统调用完成时,内核会检测一些情况,比如进程的切换或者信号发生什么的。如果发生了这些情况,内核会转向syscall_exit_work中执行,这里面的代码如下所示:
syscall_exit_work:
testl $_TIF_WORK_SYSCALL_EXIT, %ecx
jz 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)
这里面可能会有信号的处理已经进程的调度。
最后一步是恢复现场,通过iret的方式,把之前压在栈中的寄存器的值全部弹出。执行到这一步就表示完成了整个系统调度了。
系统调用的流程图
系统调用的流程大致如下图所示:
总结
本次实验使用GDB跟踪了系统调用执行的整个过程,同时又从源代码级别比较深入的了解到完成系统调用内核所做的工作。系统调用可以为用户空间提供访问硬件资源的统一接口,以至于应用程序不必去关注具体的硬件访问操作。系统调用可以对系统进行保护,保证系统的稳定和安全。系统调用的存在规定了用户进程进入内核的具体方式,用户是不能从任意位置进入内核空间的,这保证了系统的稳定性。
Linux中是以0x80号软中断引发的系统调用的,即0x80号中断的处理程序就是位于kernel/entry_32.S中system_call函数。这个函数使用汇编实现的,它首先是保存现场,然后通过eax查找对应哪一个系统调用,然后查找sys_call_table调用对应的系统调用。等完成调用之后检查是否有信号发生或者需要进程调度,如果有就进入相应的处理程序,如果没有就恢复现场完成了整个系统调用,就像上面那个流程图所画的一样。。