实验五 扒开系统调用的三层皮下
王康 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1给MenuOS增加time和time-asm命令
这次课程通过内核方式调试系统调用,把上周系统-调用增加到menuos中
自动化编译menu生成rootfs,做了个脚本make rootfs来自动编译生成。
如何实现呢?
test.c:增加了2行MenuConfig,对应两个函数
2使用gdb跟踪系统调用内核函数sys_time
qemenu -kernel -initrd rootfs.img -S -s后gdb,file linux-3.18.6/vmlinux加载符号表,target remote:1234连接到要调试的menuos端口,b start_kernel断点后c来调试,list可以查看。
b sys_time断点c,执行time会发现卡到一半
这里使用宏定义实现的
list可以看到代码
单步执行一直到获得的时间数值return i;
gdb无法处理汇编代码,所以之后设置b system_call,发现其处于entry_32.S.
发现其仍处于sys_time而在sys_call并不能停下,因为sys_call其并不是一个正常函数,是一段比较特殊的汇编代码,gdb不支持
sys_call位置:
发现其有声明的函数原型但并不是一个函数,只是一段汇编代码的起点
3系统调用在内核代码中的工作机制和初始化
一,
从int 0x80触发一个系统调用到执行系统调用处理的函数sys_time,到后边返回用户态整体过程理解(不太好调试)
1,什么时候把0x80和system_call绑定起来的?什么时候初始化中断向量?
2,system_call处理过程以及哪里调用了sys_xyz(xyz和sys_xyz是通过系统调用号匹配起来的),xyz和system_call是通过中断向量匹配起来的。
3 ret_from_sys_call的单步调试过程
初始化:
可以看到代码有:set_system_trap_gate包含系统调用中断向量和system_call代码入口。
之后系统一旦出现int 0x80就会立即调用system_call
二,
系统调用也是一种中断,所以也存在着保存和恢复现场的问题,下图可看到SAVE_ALL;
call sys_call_table %eax是系统调用表,在menuos中即为sys_time;
之后syscall_after_call:先保存返回值,退出之前有jne syscall_exit_work如果没有这个就直接restore_all返回用户态
进入复杂的syscall_exit_work,里边会有进程调度时机。
简化伪代码:
exit时会检查当前任务current->work需不需要syscall_exit_work去处理,不需要直接ireturn
当前进程有一些信号处理或需要调度就需要syscall_exit_work去处理了:
流程是跳转到work_pending,里边有jz work_notifysing处理信号,
和work_resched需要重新调度,就call schedule,调用完成后再跳转到restore_all把他返回系统调用;
三,浏览system_call到iret之间的代码
system_call:
syscall_exit_work:
work_pending:
调用schedule,调用完之后可以跳转restore_all,把后面恢复现场工作结束掉。
4,实验:分析system_call中断处理过程
1,修改test.c文件,加入menuconfig自定义的函数
2,make编译运行发现出错
3,发现是因为调用系统sdout输出后,缓冲池flush没有内容,这时候mov eax会报错,修改后可以运行
4,qemu打断点运行
5,在自定义sys_write处打断点,list可以看到其具体调用函数。单步执行n可以看到具体执行流程
6,因为我调用的是sys_write所以输出menuos的log以及help所有命令也是一下一下的
7,调用printstring命令就能看到具体的系统调用断点了:
5,总结
1. 系统调用”是操作系统提供给用户程序进行调用的一些服务。这些服务是系统预先提供的函数,在这一点上系统调用与普通的用户程序是没有区别的。而区别则在于“系统调用”是由操作系统提供给用户的,这些服务更接近底层或者要求的安全性更高,因此由操作系统来统一实现和管理。
2. 处理器在eax寄存器中拿到系统调用号之后,会到系统调用表中找到该系统调用所对应的入口函数地址,然后执行该函数。
3. 函数的入口地址在在syscall.c(entry_32s)中。
4. 中断处理中读取中断号及参数,然后找到中断服务例程并执行,退出中断后进行堆栈切换,返回用户态,继续执行用户程序。
5,扩展:
系统调用system_call的处理过程
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 函数执行时会首先把这些寄存器压入堆栈。这样对系统调用服务例程可以直接从堆栈照片能够获取参数。
从system_call开始到iret结束之间的过程
从整体过程来看,系统通过 int 0x80 从用户态进入内核态。在这个过程中系统先保存了中断环境,然后执行系统调用函数。system_call() 函数通过系统调用号查找系统调用表 sys_cal_table 来查找到具体的系统调用服务进程。在执行完系统调用后在执行 iret 之前,内核做了一系列检查,用于检查是否有新的中断产生。如果没有新的中断,则通过已保存的系统中断环境返回用户态。这样就完成了一个系统调用过程。
系统调用通过 INT 0x80 进入内核,跳转到 system_call() 函数,然后执行相应服务进程。因为代表了用户进程,所以这个过程并不属于中断上下文,而是属于进程上下文!这点一定要分清楚。
无论是中断返回(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对于系统调用的汇编具体代码是不一样的。
.macro INTERRUPT_RETURN ; 中断返回
iret
.endm
.macro SAVE_ALL ; 保护现场
...
.macro RESTORE_INT_REGS
...
.endm
ENTRY(system_call)
SAVE_ALL
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax, PT_EAX(%esp) ; store the return value
syscall exit:
testl $_TIF_ALLWORK_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)