分析系统调用的处理过程(systemcall->iret)

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

本片内容接上一篇,是系统调用的续篇,也就是简要分析一下系统调用的处理过程。同样,以一个实验开始。

本实验同样在实验楼环境下完成。下面先介绍一下实验:

1.实验:

将上一个系统调用函数和asm版本的实现整合进入menu的内核中:

int GetPid()
{
    int pid = getpid();
    printf("The Current Progress pid is : %d\n",pid);
    return 0;
}

int GetPidAsm()
{
    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("The Current Progress pid asm is : %d\n",pid);
    return 0;
}
int main()
{
    PrintMenuOS();
    SetPrompt("MenuOS>>");
    MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
    MenuConfig("quit","Quit from MenuOS",Quit);
    MenuConfig("time","Show System Time",Time);
    MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
    MenuConfig("getpid","Show Current Progress id",GetPid);
    MenuConfig("getpid-asm","Show Current Progress asm id",GetPidAsm);
    ExecuteMenu();
}
简单介绍一下上面的一段代码,MenuConfig这个函数是菜单(也就是制作出来的内核)的初始化配置函数,第一个参数是命令,第二个参数是该命令的描述,第三个参数是这个命令相对应的handler,也就是回调函数,是通过一个函数指针进行实现的。ExecuteMenu这个函数是为了启动这个menu引擎,其实是一个循环等待用户输入命令的过程。

在添加了getpid和getpid-asm这两个命令之后,make一下,再重新编译一下内核,make rootfs。再启动一下qemu:

分析系统调用的处理过程(systemcall->iret)_第1张图片

接下来,我们开始调试这个系统调用函数,对应着getpid这个系统调用,在sys_getpid这个内核函数这里打一个断点。continue之后,输入getpid-asm或者getpid这个命令会停在sys_getpid();

分析系统调用的处理过程(systemcall->iret)_第2张图片


可以看到,在kernel/sys.c中有这么一个sys_getpid()内核函数,这个也就是系统调用的服务例程。跟进这个task_tgid_vnr(current)函数,之后finish之后的返回值是1,也就是init进程的进程号。

分析系统调用的处理过程(systemcall->iret)_第3张图片

当然打印出来的current pid 也是1 。

有点遗憾的是我没有找到让gdb在systemcall这个汇编代码处停止的办法,希望小伙伴可以友情提示。下面来进一步分析从systemcall到系统调用结束之后,iret之间的过程。

2.从systemcall到iret过程:

Systemcall内核源码(汇编代码)进行简要分析:

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
首先在内核栈中,压栈eax,也就是用户态传递给内核态的系统调用号,之后SaveAll这个指令就开始保存现场了,这里GET_THREAD_INFO(%ebp)是将这个进程的threadinfo结构体的地址指针放在了ebp寄存器里,下面对于这个结构体的访问也就可以顺理成章的用ebp的地址偏移进行访问就行了。比如下一句的TI_flags这个宏代表的其实就是一个offset,这个testl 指令主要是为了检查是否存在调试程序对这个系统调用正在调试,如果是,则跳转到syscall_trace_entry标签处,这个标签我一会来分析。接着往下看,cmpl $(NR_syscalls),%eax 这个指令主要是为了防止eax传递参数越界了,也就是超过了系统调用符号表中的值,如果出错了就跳到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

如果上面的都没问题的话,我们来到系统调用函数执行的地方了,调用服务例程,系统调用号乘以4加上sys_call_table的基址,内核系统调用的服务例程,也就是getpid的函数地址就是它了,调用call指令,开始执行,又是顺理成章的将eax的值,也就是系统调用的返回值放到esp的某一个偏移量(PT_EAX)处。

在系统调用退出之前,在restoreall之前,会将threadinfo中的许多标志位进行比较,看是否需要在返回用户态之前做点什么事,比如我们看着一段:

在恢复用户态之前的一段代码:

350	TRACE_IRQS_OFF
351	movl TI_flags(%ebp), %ecx
352	andl $_TIF_WORK_MASK, %ecx	# is there any work to be done on
353					# int/exception return?
354	jne work_pending
355	jmp restore_all
这里检查了TIF_WORK_MASK,有其他的int值或者异常要返回?这个时候我们跳到workpending,这个workpending我们贴一下代码看看:

 
  
593work_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
workpending主要是为了检测重调度标志,下面的work_resched标签是如果进程切换的请求被挂起了,辣么,在内核态开始一次进程调度,选择一个进程运行,当前面的进程要恢复的时候在跳转回到resume_userpace处,也即回到用户态的标签处。
在restoreall之后,就将中断的程序被恢复了。
下面是一张简要流程图:

分析系统调用的处理过程(systemcall->iret)_第4张图片


你可能感兴趣的:(linux,内核分析)