Linux内核分析:分析system_call中断处理过程

来源:互联网 发布:淘宝借呗客服电话 编辑:程序博客网 时间:2024/06/05 11:09

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

第一部分:调试系统调用内核函数实验

实验内容:使用gdb跟踪分析一个系统调用内核函数(您上周选择那一个系统调用),系统调用列表参见
http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl ,
推荐在实验楼Linux虚拟机环境下完成实验。

1.1 实验代码

修改LinuxKernel/menu/test.c文件。
在main函数中添加两句,增加menu系统的两个命令:gettimeofday和gettimeofday-asm。

MenuConfig("gettimeofday","sleep(1) cost time:",myGettimeofday);    MenuConfig("gettimeofday-asm","sleep(1) cost time:",myGettimeofday_asm);

然后在main函数之前添加这两个命令所执行的函数:

int myGettimeofday(){        struct timeval start,end;        double Dstart,Dend,Dtime;        Dstart=Dend=Dtime=0;        gettimeofday(&start,NULL);        sleep(1);        gettimeofday(&end,NULL);        Dstart=((double)start.tv_sec*1000000+(double)start.tv_usec);        Dend=((double)end.tv_sec*1000000+(double)end.tv_usec);        Dtime=Dend-Dstart;        Dtime=Dtime/1000000;      printf("Dtime=%lf\n",Dtime);        return 0;}
int myGettimeofday_asm(){             struct timeval start;        struct timeval end;        double Dstart=0;        double Dend=0;        double Dtime=0;        asm(                "mov $0,%%ecx\n\t"                "mov %0,%%ebx\n\t"                "mov $0x4e,%%eax\n\t"                "int $0x80\n\t"                :                 :"d" (&start)        );       sleep(1);        asm(                 "mov $0,%%ecx\n\t"                "mov %0,%%ebx\n\t"                "mov $0x4e,%%eax\n\t"                "int $0x80\n\t"                :                 :"d" (&end)        );         Dstart=((double)start.tv_sec*1000000+(double)start.tv_usec);        Dend=((double)end.tv_sec*1000000+(double)end.tv_usec);        Dtime=Dend-Dstart;        Dtime=Dtime/1000000;      //  printf("gettimeofday tv_sec is %d,usec is %d \n",start.tv_sec,start.tv_usec);      printf("Dtime=%lf\n",Dtime);        return 0;}

1.2 重新编译生成根文件系统,并启动内核。

这里写图片描述
测试运行这两个命令,发现可以正常执行:
这里写图片描述

1.3 用gdb跟踪调试sys_gettimeofday

将shell工作目录切换到LinuxKernel下,然后使用命令:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
这里写图片描述
另开一个shell,启动gdb,然后输入命令:
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
这里写图片描述
接下来,要在sys_gettimeofday设置断点,
查看/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl,它是第78号系统调用。
这里写图片描述
使用命令:b sys_gettimeofday 设置断点。
这里写图片描述
在menu界面中输入命令:gettimeofday,触发断点。
这里写图片描述
因为执行命令gettimeofday会两次调用sys_gettimeofday,所以按c继续执行后,会再次在刚才设置的断点处停下。
这里写图片描述
执行命令gettimeofday-asm,也是一样的效果,按c继续执行后,命令成功完成。
这里写图片描述

第二部分:分析system_call中断处理过程

2.1 系统调用机制的初始化

/init/main.c start_kernel
trap_init();
…/arch/x86/kernel/trap.c

#ifdef CONFIG_X86_32 set_system_trap_gate(SYSCALL_VECTOR, &system_call); set_bit(SYSCALL_VECTOR, used_vectors);#endif

这两句将0x80与system_call进行绑定,之后调用int 0x80 就会立即跳转到system_call执行。

2.2 简化后便于理解的system_call伪代码

system_call位于arch/x86/kernel/entry_32.S
其中有一个ENTRY(system_call)
这就是system_call。system_call是一个特殊的中断。所以也会有SAVE_ALL保存现场,和RESTORE_ALL恢复现场。
call *sys_call_table(,%eax,4)会调用系统调用号(eax的值)对应的服务程序。
在服务程序返回之后,RESTORE_ALL之前,可能发生进程调度。
在当前进程,可能有一些信号(比如用于进程间通信)需要处理,会在system_exit_work中处理。这部分执行完后,就是调度时机,(此时当前进程(假设为进程A)的现场仍然在自己的内核栈上,中断处理过程已经全部完成,A的内核栈被清理成了中断处理前的样子,即仅留下A的现场,这时发生调度,切换到进程B,一段时间后返回进程A时,可以从A的内核栈中顺利恢复出刚才进程A的现场,)会调用schedule()。

#system call#asm pseudo code#系统调用处理过程的汇编伪代码.macro INTERRUPT_RETURN    iret.endm.marco SAVE_ALL    ....endm.marco RESTORE_INT_REGS    ....endmENTRY(system_call)    SAVE_ALLsyscall_call:    call *sys_call_table(,%eax,4)mov %eax,PT_EAX(%esp)  #store the return valuesyscall_exit:    testl $_TIFALLWORK_MASK, %ecx  #current->work    jne syscall_exit_workrestore_all:    RESTORE_INT_REGSirq_return:    INTERRUPT_RETURNENDPROC(system_call)syscall_exit_work:    testl $_TIF_WORK_SYSCALL_EXIT, %ecx    jz work_pendingEND(syscall_exit_work)work_pending:    testb $_TIF_NEED_RESCHED, %cl    jz work_notifysigwork_resched:    call schedule    jz restore allwork_notifysig:    #deal with pending signals    ...END(work_pending)   

system_call流程图:
这里写图片描述

第三部分:自己对“系统调用处理过程”的理解,进一步推广到一般的中断处理过程。

3.1 基础知识

中断上下文的保存:
中断过程需要保存上下文,但是中断过程没有自己的栈,只能暂时占用被中断进程的内核栈。
IDT:
(Interrupt Descriptor Table,IDT)将每个异常或中断向量分别与它们的处理过程联
系起来。与GDT和LDT表类似,IDT也是由8字节长描述符组成的一个数组。与GDT不同的是,表中第1项可以包含描述符。为了构成IDT表中的一个索引值,处理器把异常或中断的向量号乘以8。因为最多只有256个中断或异常向量,所以IDT无需包含多于256个描述符。IDT中可以含有少于256个描述符,因为只有可能发生的异常或中断才需要描述符。不过IDT中所有空描述符项应该设置其存在位(标志)为0。
(参考:
http://baike.baidu.com/link?url=nsabpIQMxXNi4Osuvr1VNcNmz_yOSNGOfGfe0dSNrvonPMbChpUVcgnbWM2E3Qa_TKmwLEqN67jU2eV93Orpc_#7)
GDT:
GDT,即全局描述符表。[span]在英特尔x86[span]系列处理器的80286[span]起,为了定义的特点使用不同的存储区,在程序执行期间,包括基地址,大小和访问权限,如可执行可写。这些内存区域被称为段(英特尔的术语)。内存中段所在的位置不需要写入特殊标记,段的信息(基地址、界限、属性等)保存通过段描述符表进行。GDT正是最重要的描述符表,进入保护模式,至少要准备GDT。
(参考:
http://baike.baidu.com/link?url=uCLqGZ4h_jg64WAtwu8Hx2HNEFmKou1VJbGcCVXhrMjCzMyKnmNBtIJT6HvEHnR7W9OgXmM-bcGA3yD8bW4Fk4CIvKX2Zb2P_Z7dRifaD2O#4)

3.2 中断过程(系统调用也是一种特殊的中断)

发生中断和异常会产生中断号或异常号(中断/异常向量),CPU根据IDTR寄存器的值找到IDT的基地址,再根据这个中断号,在IDT表中找到对应的项(这就得到了中断/异常处理程序的入口地址)接下来根据GDTR或LDTR寄存器的值得到GDT/LDT的基地址。GDT基地址+IDT表项中的段选择码=程序代码段基地址
程序代码段基地址赋给CS寄存器,IDT表项中的offset字段赋给eip,CS+eip=中断/异常处理程序的入口地址。中断/异常处理完后,相应的处理程序会执行一条iret汇编指令,这条汇编指令让CPU控制单元做如下事情:
1,用保存在栈中的值装载cs、eip和eflags寄存器。如果一个硬件出错码曾被压入栈中,那么弹出这个硬件出错码。
2,检查处理程序的特权级是否等于cs中最低两位的值(这意味着进程在被中断的时候是运行在内核态还是用户态)。若是,iret终止执行;否则,转入3。
3,从栈中装载ss和esp寄存器。这步意味着返回到与旧特权级相关的栈。
4,检查ds、es、fs和gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且特权级比当前特权级高,则清除相应的寄存器。这么做是防止怀有恶意的用户程序利用这些寄存器访问内核空间。

0 0
原创粉丝点击