==========================================================================
这周的实验在上周实验四的基础上,进一步的操作:
1.将系统调用函数getpid命令加入menuos中
2.通过gdb跟踪sys_getpid系统调用执行的完整过程
步骤:给MenuOS增加getpid和getpid-asm命令
0)更新menu代码到最新版
1)在main函数中增加MenuConfig
2)增加对应的getpid函数和getpid-asm函数
3)make rootfs
进入实验楼环境后,敲入如下命令:
<span style="font-size:14px;">cd LinuxKernel cd menu vi test.c</span>
进入test.c源程序后添加如下代码:
<span style="font-size:14px;">//添加两个函数,别忘了加头文件#include <unistd.h> int main() { pid_t tt; asm volatile ( "movl $0x20, %%eax\n\t" "int $0x80\n\t" "movl %%eax, %0\n\t" :"=m"(tt) ); printf("%u\n", tt); return 0; } int main() { pid_t tt; tt = getpid(); printf("%u\n", tt); return 0; }</span>
<span style="font-size:14px;">//然后在main函数中添加 MenuConfig("time","Show System Time",Time); MenuConfig("time-asm","Show System Time(asm)",TimeAsm);</span>
代码添加完成后make rootfs重新编译,此时系统会自动启动。如下图:
下面用gdb跟踪sys_getpid执行的过程:
1.执行以下命令开启内核的调试功能 qemu -kernel linux-3.18.6/arch/x86/boot/bz/Image -initrd rootfs.img -s -S,此时系统处于停止状态
2.再打开一个命令行窗口输入gdb,在gdb命令提示符下依次输入file linux-3.18.6/vmlinux, target remote:1234命令连接内核并跟踪调试
3.设置断点break sys_getpid, 接着continue开始运行,此时menuOS从stopped状态开始执行。在menu程序的提示符下输入who,程序执行到断点时暂停,此时gdb窗口显示程序断在sys_getpid处
4.接着使用gdb单步执行命令。next:不进入函数体的单步执行;step:进入函数体的单步执行;finish:进入函数体后退回调用函数
分析
中断相关的初始化代码是通过linux-3.18.6/init/main.c文件中的start_kernel函数里的trap_init()初始化的。执行int $0x80指令后内核开始执行system_call入口处开始的代码,位于entry_32.S汇编文件中。
下面分析system_call汇编代码:
<span style="font-size:14px;"># 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 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_co de irq_return: INTERRUPT_RETURN</span>
1.SAVE ALL // 保存调用前寄存器相关的信息
2.call *sys_call_table(,%eax,4) // 执行系统调用对应的处理函数,eax存放系统调用号
// 通过linux-3.18.6/arch/x86/syscalls/syscall_32.tbl找到系统调用号对应处理函数
3.movl %eax,PT_EAX(%esp) // 保存系统调用处理函数返回值到exa
4. testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work
// 这两句检查调用退出前是否有其他工作要处理,如有则跳到syscall_exit_work处继续处理,以下是syscall_exit_work相关代码:
syscall_exit_work:
testl $_TIF_WORK_SYSCALL_EXIT, %ecx // 测试是否退出前还有工作要处理,如有则跳到work_pending
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)
5.下面是work_pending的相关代码,在注释中解释相关内容
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
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
1:
#else
movl %esp, %eax
#endif
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
movb PT_CS(%esp), %bl
andb $SEGMENT_RPL_MASK, %bl
cmpb $USER_RPL, %bl
jb resume_kernel
xorl %edx, %edx
call do_notify_resume
jmp resume_userspace
#ifdef CONFIG_VM86
ALIGN
work_notifysig_v86:
pushl_cfi %ecx # save ti_flags for do_notify_resume
call save_v86_state # %eax contains pt_regs pointer
popl_cfi %ecx
movl %eax, %esp
jmp 1b
#endif
END(work_pending)
6. restore_all:
RESTORE_INT_REGS // 中断返回之前恢复相关寄存器的内容
7. irq_return:
INTERRUPT_RETURN // 这两行代码主要是返回到用户态
总结
1.执行int 0x80指令后系统从用户态进入内核态,跳到system_call()函数处执行相应服务进程。在此过程中内核先保存中断环境,然后执行系统调用函数。
2.system_call()函数通过系统调用号查找系统调用表sys_cal_table来查找具体系统调用服务进程。
3.执行完系统调用后,iret之前,内核会检查是否有新的中断产生、是否需要进程切换、是否学要处理其它进程发送过来的信号等。
4.内核是处理各种系统调用的中断集合,通过中断机制实现进程上下文的切换,通过系统调用管理整个计算机软硬件资源。
5.如没有新的中断,restore保存的中断环境并返回用户态完成一个系统调用过程。