Linux内核分析(五):系统调用深入分析

来源:互联网 发布:ubuntu进不了图形界面 编辑:程序博客网 时间:2024/05/19 06:50

何天杨+ 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

这周的实验在上周实验四的基础上,进一步的操作:
1.将系统调用函数getppid命令加入menuos中
2.通过gdb跟踪sys_getppid系统调用执行的完整过程

步骤:给MenuOS增加getpid和getpid-asm命令
0)更新menu代码到最新版
1)在main函数中增加MenuConfig
2)增加对应的getpid函数和getpid-asm函数
3)make rootfs

一、实验内容

  1. 通过内核的方式使用系统调用

需要使用的命令

rm menu -rf //强制删除当前menugit clone http://github.com/mengning/menu.git //重新克隆新版本的menucd menulsmake rootfs //rootfs是事先写好的一个脚本,自动编译自动生成根文件系统,同时自动启动MenuOS

2.打开menu中的 test.c文件,添加Gitpid和Gitpidasm代码

int Getpid(int argc , char * argv[]){   int pid;   pid=getpid();   printf("pid=%d\n",pid);   return 0;}int Getpidasm(int argc , char *argv[]){   int pid;   asm volatile(       "mov $0,%%ebx\n\t"       "mov $0x14,%%eax\n\t"       "int $0x80\n\t"       "mov %%eax, %0\n\t"   :"=m"(pid)   );   printf("pid = %d\n",pid);   return 0;}

在main函数中添加

MenuConfig(“getpid","Show pid",Getpid);MenuConfig("getpid-asm","Show pid(asm)",Getpidasm);

代码添加完成后make rootfs重新编译,此时系统会自动启动。如下图:
这里写图片描述

3.使用gdb跟踪分析这该系统调用内核函数
  系统中已经成功添加了该函数调用功能,然后对该程序进行调试分析,使用gdb跟踪分析这该系统调用内核函数。
  需要的命令为

qemu -kernel  linux.3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S              //调试。file linux-3.18.6/vmlinux     //加载调试内核符号表。

b 设置断点 n 单步执行
这里写图片描述

二、系统调用过程分析 

  1. 系统调用在内核代码中的工作机制和初始化
    int 0x80——>system call:通过中断向量匹配
    system call——>sys_xyz():通过系统调用号匹配

中断相关的初始化代码是通过linux-3.18.6/init/main.c文件中的start_kernel函数里的trap_init()初始化的。执行int $0x80指令后内核开始执行system_call入口处开始的代码,位于entry_32.S汇编文件中。
下面是system_call汇编代码

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)     // 保存系统调用处理函数返回值到exa4.testl $_TIF_ALLWORK_MASK, %ecx    # current->work5.jne syscall_exit_work  // 这两句检查调用退出前是否有其他工作要处理,如有则跳到syscall_exit_work处继续处理,以下是syscall_exit_work相关

2 以下是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_userspaceEND(syscall_exit_work)

3.下面是work_pending的相关代码,在注释中解释相关内容

work_pending:    testb $_TIF_NEED_RESCHED, %cl    #是否有要继续调度的相关信号    jz work_notifysig                #跳转到处理信号相关的代码处work_resched:    call schedule             #时间调度,进程调度的时机在这里处理    LOCKDEP_SYS_EXIT    DISABLE_INTERRUPTS(CLBR_ANY)  #确认没有忽略中断                     # 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_reschedwork_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-space1:#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    ALIGNwork_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#endifEND(work_pending)

4.restore_all

restore_all:       RESTORE_INT_REGS     // 中断返回之前恢复相关寄存器的内容

5.irq_return

 irq_return:      INTERRUPT_RETURN     # 这两行代码主要是返回到用户态

无论是中断返回(ret_from_intr) ,还是系统调用返回,都使用了 work_pending 和resume_userspace。对于宏SAVE_ALL来说,这条语句会把将寄存器的值压入堆栈当中,压入堆栈的顺序对应struct pt_regs ,出栈时,这些值传递到struct pt_regs的成员,实现从汇编代码向C程序传递参数。struct pt_regs可以在arch/x86/include/asm/ptrace.h中查看。用户态到内核态需要int 0x80进行中断,只有生成了中断向量后才可以切换状态。中断处理让CPU停止当前工作转为执行系统内核中预设的一些任务,因此必须要对当前CPU执行的任务进行执行现场的保护工作,并对一些其他工作进行检查,完成调用后,再进行检查,才能执行iret返回。系统内部调用涉及CPU架构等内容,不同的CPU对于系统调用的汇编具体代码是不一样的。

三、总结
1.执行int 0x80指令后系统从用户态进入内核态,跳到system_call()函数处执行相应服务进程。在此过程中内核先保存中断环境,然后执行系统调用函数。

2.system_call()函数通过系统调用号查找系统调用表sys_cal_table来查找具体系统调用服务进程。
 syscall_call 函数到系统调用服务例程通过系统调用号联系起来:在上面执行软中断 0x80 时,系统调用号会被放入eax寄存器(参数的传递),system_call 函数读取eax寄存器获取参数(当前系统调用的调用号),将其乘以4生成偏移地址。然后以中断向量表(sys_call_table)为基址,以系统调用号所确定的为偏移地址相加得到最后的物理地址:基址+偏移地址 => 系统调用服务例程的地址。其中 sys_call_table 基址在文件 arch/x86/kernel/syscall_table_32.S 中定义,同时表中每一项例程的地址占用4个字节,所以上面乘以4。
 由于系统调用例程在定义时时用 asmlinkage 标记了的,所以编译器仅从堆栈中获取该函数的参数。在进入system_call函数前,用户应用会把参数存放到寄存器中,system_call 函数执行时会首先把这些寄存器压入堆栈。这样对系统调用服务例程可以直接从堆栈照片能够获取参数。
 
3.执行完系统调用后,iret之前,内核会检查是否有新的中断产生、是否需要进程切换、是否学要处理其它进程发送过来的信号等。如果没有新的中断,则通过已保存的系统中断环境返回用户态。这样就完成了一个系统调用过程。系统调用通过 INT 0x80 进入内核,跳转到 system_call() 函数,然后执行相应服务进程。因为代表了用户进程,所以这个过程并不属于中断上下文,而是属于进程上下文。

4.内核是处理各种系统调用的中断集合,通过中断机制实现进程上下文的切换,通过系统调用管理整个计算机软硬件资源。

5.如没有新的中断,restore保存的中断环境并返回用户态完成一个系统调用过程。

这里写图片描述

0 0
原创粉丝点击