首页 > 代码库 > Linux内核分析第五周作业

Linux内核分析第五周作业

分析system_call中断处理过程

这次的目标是通过gdb来跟踪上周选择的uname系统调用。因为系统调用是通过中断在内核态实现的,gdb无法调试本机的系统调用。所以必须像之前的内核跟踪那样,用gdb远程连接至qemu虚拟机进行跟踪。

1. 首先修改之前的MenuOS,添加一个myuname函数通过API的方式调用uname系统调用,直接复制了上周的代码,注意要在main函数中把这个函数添加到菜单中

技术分享

用make命令编译以后,会生成一个test的可执行文件,可以先在本地运行试一试

技术分享

可以看到命令运行成功。

接下来就制作rootfs并且把MenuOS放在里面。然后启动qemu并指定initrd为刚才制作的rootfs。

技术分享

可以看到uname也成功地运行在qemu中了。

 

2. 下面使用gdb进行远程调试。

首先qemu的启动参数要加上-s来打开远程调试端口。

然后gdb加载Linux内核的符号表,并且连接到qemu的调试端口1234

gdb
(gdb)file linux-3.18.6/vmlinux
(gdb)target remote:1234

在系统调用的入口——system_call函数加断点,然后让qemu继续运行

技术分享

接下来在qemu中执行系统调用,输入uname命令,居然没有断下来 w(?Д?)w 果然和教程中说的一样。

 

因为system_call是汇编写的代码,直接用gdb直接查找“system_call”符号得到的地址不准确,但是也不会差的太远。尝试从刚才的断点0xc174d881前面15个字节开始反汇编20条指令,看看情况

技术分享

果然0xc174d881并不是一个有效的代码地址。于是把断点直接放在0xc174d884上,就是system_call的真正起始地址

技术分享

这次就成功地断了下来

 

然后对照着entry_32.S单步跟踪,看看究竟做了什么 

技术分享

这一大堆的push显然就是SAVE_ALL所做的事情,将当前的寄存器都保存起来。

技术分享

这里把EAX和0x166比较了一下。EAX里面是系统调用号,0x166是十进制的358。看一眼系统调用表,最后一个系统调用号正好是357。因此这里是说如果传入的EAX超过了最大的系统调用号,就进入异常处理程序syscall_badsys

而如果是有效的系统调用号,就call一个地址。因此再看一眼-0x3e8aede0地址附近的内容,根据无符号整数的定义,-0x3e8aede0其实就是0xc1751220

技术分享

这里面似乎都是地址。随便挑第二个地址查看一下

技术分享

正好就是sys_exit的入口地址,而sys_exit就是1号系统调用exit的实现函数。

参考一下源文件

技术分享

-0x3e8aede0就应该是sys_call_table的地址了。至于为什么用奇怪的负数地址,可能与CPU指令有关,也许这样的指令比较短  ╮(╯_╰)╭

因此这里call的是EAX对应的系统调用的入口地址。

uname的实现函数sys_newuname上周已经看过了

技术分享

很简单,uname的内容一直在内核中保存着,每次系统调用只要将内容从内核态复制到用户态即可,并且复制的时候用锁保护了一下。

从sys_newuname返回以后,继续执行system_call

 技术分享

源文件是这样的

技术分享

EAX是系统调用实现函数的返回值(uname用它返回错误码),这里把EAX保存在ESP+0x18的位置。再回顾一下system_call的开头,SAVE_ALL是如何保存现场的

技术分享

之后在system_call中ESP就没有变过,因此ESP+0x18正好是EAX保存的地方。这样就把之前保存的EAX用函数的返回值替换掉了,而等到最后RESTORE_ALL的时候就会自动地把返回值恢复到EAX中

接下来就该进入收尾工作了

技术分享

syscall_exit_work里面有一个进程调度的机会

技术分享

------------------------------------

 技术分享

可惜实验中似乎条件不满足,没有发生调度。而是直接恢复了全部的寄存器

技术分享

最后清空了堆栈,并且用iret指令从中断返回

技术分享

 

整个系统调用的简单流程图如下

技术分享

 

  

3. 总结

系统调用的方法是通过0x80中断进入内核态,然后内核根据EAX中的系统调用号去调用对应的系统调用函数,最后将返回值通过EAX返回给用户态程序。

此外,系统调用是进程调度切换的时机,因为此时控制交给了内核。对于用户程序来说仅仅是调用了系统调用,然后从系统调用返回。在系统调用中发生的进程切换对于用户程序是透明的。

对于一般的中断处理函数,首先要保存现场,最后要恢复现场

 

4. API与中断调用的区别

其实在实验中,系统调用的方式是通过汇编调用0x80中断,类似于上周的uname_asm程序。因为在设置了正确的system_call入口地址以后,用系统调用的API依然无法触发断点。经过跟踪代码,终于发现了原因。

教程中也提到过,从Pentium II开始新加入了一个专用的指令,快速系统调用——SYSENTER。而API都非常优化,自然使用的是新指令。因此中断0x80的IRQ system_call自然就不会运行,所以断点没有生效。

实际上sysenter的入口也在entry_32.S里面,叫做ia32_sysenter_target,而且代码、流程都和system_call很类似,感兴趣的话可以阅读一下。

 

 

 

 

王岩

原创作品转载请注明出处

《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

Linux内核分析第五周作业