张雨梅 原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-10000
1.给menuos添加命令
改写menu下的test.c,在main中添加两行,
MenuConfig("getuid","show system uid",Getuid);
MenuConfig("getuidasm","show system uidasm",Getuidasm);
并在main函数中添加Getuid、Getuidasm函数,其内容与上次实验一样。
运行结果如下图所示:
可以看到该系统支持getuid,getuidasm命令。
2.使用gdb跟踪分析一个系统调用内核函数
在终端输入
qemu -kernel linux3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
gdb
file linux-3.18.6/vmlinux 加载调试相关内核的符号集
target remote:1234
内核函数返回后进入汇编代码处理,gdb无法继续跟踪,因为当进入system call时,系统已经在挂起状态了。
3. 分析系统调用的过程,从system_call开始到iret结束
代码: http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/kernel/entry_32.S#490
490ENTRY(system_call) 491 RING0_INT_FRAME # can't unwind into user space anyway 492 ASM_CLAC 493 pushl_cfi %eax # save orig_eax 494 SAVE_ALL 495 GET_THREAD_INFO(%ebp) 496 # system call tracing in operation / emulation 497 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) 498 jnz syscall_trace_entry 499 cmpl $(NR_syscalls), %eax 500 jae syscall_badsys 501syscall_call: 502 call *sys_call_table(,%eax,4) 503syscall_after_call: 504 movl %eax,PT_EAX(%esp) # store the return value 505syscall_exit: 506 LOCKDEP_SYS_EXIT 507 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt 508 # setting need_resched or sigpending 509 # between sampling and the iret 510 TRACE_IRQS_OFF 511 movl TI_flags(%ebp), %ecx 512 testl $_TIF_ALLWORK_MASK, %ecx # current->work 513 jne syscall_exit_work 514 515restore_all: 516 TRACE_IRQS_IRET 517restore_all_notrace: 518#ifdef CONFIG_X86_ESPFIX32 519 movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS 520 # Warning: PT_OLDSS(%esp) contains the wrong/random values if we 521 # are returning to the kernel. 522 # See comments in process.c:copy_thread() for details. 523 movb PT_OLDSS(%esp), %ah 524 movb PT_CS(%esp), %al 525 andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax 526 cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax 527 CFI_REMEMBER_STATE 528 je ldt_ss # returning to user-space with LDT SS 529#endif 530restore_nocheck: 531 RESTORE_REGS 4 # skip orig_eax/error_code 532irq_return: 533 INTERRUPT_RETURN
SAVE_ALL 保存现场,将寄存器的值压入堆栈当中,压入堆栈的顺序对应着结构体struct pt_regs ,当出栈的时候,就将这些值传递到结构体struct pt_regs里面的成员,从而实现从汇编代码向C程序传递参数。struct pt_regs 位于:linux/arch/x86/include/asm/ptrace.h。
GET_THREAD_INFO(%ebp) 获得当前进程的thread_info结构的地址,获取当前进程的信息,保存在ebp中。
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) jnz syscall_trace_entry
判断是否 trace调用,若需要,进入syscall_trace_entry
cmpl $(NR_syscalls), %eax jae syscall_badsys
判断系统调用号是否超出系统调用表的最大值,如果大于,进入syscall_badsys
call *sys_call_table(,%eax,4) 进入到系统调用表查找到系统调用服务程序的入口函数的地址,把eax保存的系统调用号乘以4再与sys_call_table表的基址相加,即得到想要调用的函数的入口地址。sys_call_table位于:linux/arch/x86/kernel/syscall_table_32.S
movl %eax,PT_EAX(%esp) 保存函数返回值
movl TI_flags(%ebp), %ecx testl $_TIF_ALLWORK_MASK, %ecx jne syscall_exit_work 检测当前任务是否需要处理,如果需要转入syscall_exit_work
restore_all 恢复现场
INTERRUPT_RETURN 中断返回,即iret
下面分析中间几次判断之后可能调转到的函数做了什么工作。
syscall_trace_entry
1 syscall_trace_entry 2 movl $-ENOSYS,PT_EAX(%esp) 3 movl %esp, %eax 4 call syscall_trace_enter 5 /* What it returned is what we'll actually use. */ 6 cmpl $(NR_syscalls), %eax 7 jnae syscall_call 8 jmp syscall_exit 9 END(syscall_trace_entry)
检查系统调用号是否大于系统调用表中的最大值,如果不大于,执行到syscall-call,如果大于,执行到system_exit。
syscall_badsys
1 syscall_badsys: 2 movl $-ENOSYS,%eax 3 jmp syscall_after_call 4 END(syscall_badsys)
enosys表示文件系统不支持调用,对于这个命令的功能,猜测是把文件系统不支持调用的信号传给eax,执行到system_after_call,eax作为返回值输出。
syscall_exit_work
1 syscall_exit_work: 2 testl $_TIF_WORK_SYSCALL_EXIT, %ecx 3 jz work_pending 4 TRACE_IRQS_ON 5 ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call 6 # schedule() instead 7 movl %esp, %eax 8 call syscall_trace_leave 9 jmp resume_userspace 10 END(syscall_exit_work)
$_TIF_WORK_SYSCALL_EXIT与ecx相与为0,则执行work_pending。否则调用trace_leave,然后恢复用户空间。
work_pending
work_pending: 594 testb $_TIF_NEED_RESCHED, %cl 595 jz work_notifysig 596work_resched: 597 call schedule 598 LOCKDEP_SYS_EXIT 599 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt 600 # setting need_resched or sigpending 601 # between sampling and the iret 602 TRACE_IRQS_OFF 603 movl TI_flags(%ebp), %ecx 604 andl $_TIF_WORK_MASK, %ecx # is there any work to be done other 605 # than syscall tracing? 606 jz restore_all 607 testb $_TIF_NEED_RESCHED, %cl 608 jnz work_resched 609 610work_notifysig: # deal with pending signals and 611 # notify-resume requests 612#ifdef CONFIG_VM86 613 testl $X86_EFLAGS_VM, PT_EFLAGS(%esp) 614 movl %esp, %eax 615 jne work_notifysig_v86 # returning to kernel-space or 616 # vm86-space 6171: 618#else 619 movl %esp, %eax 620#endif 621 TRACE_IRQS_ON 622 ENABLE_INTERRUPTS(CLBR_NONE) 623 movb PT_CS(%esp), %bl 624 andb $SEGMENT_RPL_MASK, %bl 625 cmpb $USER_RPL, %bl 626 jb resume_kernel 627 xorl %edx, %edx 628 call do_notify_resume 629 jmp resume_userspace 630 631#ifdef CONFIG_VM86 632 ALIGN 633work_notifysig_v86: 634 pushl_cfi %ecx # save ti_flags for do_notify_resume 635 call save_v86_state # %eax contains pt_regs pointer 636 popl_cfi %ecx 637 movl %eax, %esp 638 jmp 1b 639#endif 640END(work_pending)
判断是否需要重新调度程序,用work_notifysig处理信号,调用schedule函数,然后返回到system_exit_work。
system_call到iret的流程图如下图。
restore_all之后,即执行IRET。
实验总结
上次实验总结了system_call的大致过程是保存现场,根据系统调用号调用对应的系统函数,然后恢复现场。而这只是system_call的简要概括,其实际还有相关配置,不同阶段不同情况的处理,是一个相对复杂的过程。这篇文章介绍了system_call执行的大概轮廓,有些地方也可能不准确,具体语句的作用还要进一步研究。