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

张家骥 + 原创作品转载请注明出处 + 《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
    ...
.endm
ENTRY(system_call)
    SAVE_ALL
syscall_call:
    call *sys_call_table(,%eax,4)
mov %eax,PT_EAX(%esp)  #store the return value
syscall_exit:
    testl $_TIFALLWORK_MASK, %ecx  #current->work
    jne syscall_exit_work
restore_all:
    RESTORE_INT_REGS
irq_return:
    INTERRUPT_RETURN
ENDPROC(system_call)
syscall_exit_work:
    testl $_TIF_WORK_SYSCALL_EXIT, %ecx
    jz work_pending
END(syscall_exit_work)
work_pending:
    testb $_TIF_NEED_RESCHED, %cl
    jz work_notifysig
work_resched:
    call schedule
    jz restore all
work_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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值