第5章 系统调用的三层机制(下)
1 给MenuOS增加命令
首先进入LinuxKernel目录下,用rm -rf menu强制删除当前的menu目录,然后用git clone重新克隆一个新版本的menu,如下图所示:
新版本的menu中已经将上一章做的两个系统调用添加进去了,在test.c里我们看到添加的两个系统调用,如下图所示:
可以看到在test.c里的main()函数增加了增加了两行代码,一个是MenuConfig("time"),另一个是MenuConfig("time-asm"),从这里看出如果要给MenuOS增加新的命令,只需要使用MenuConfig命令,并增加对应的函数即可。
接下来进入menu目录下,运行make rootfs脚本就可以自动编译并自动生成根文件系统,这时打开了menu镜像,如下图所示:
在MenuOS菜单中输入help命令可以看到新增了 两条命令,输入time命令和time-asm命令可以看到运行结果,如下图所示:
2 使用gdb跟踪系统调用内核函数sys_time
用“qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S”命令先把内核启动一下,可以看到被冻结起来了,代码没有被运行,如下图所示:
再另外打开一个shell窗口,用Ctrl+Shift+O实现水平分割
启动gdb,把内核加载进来,建立连接。这里用到的代码为:
1 file linux-3.18.6/vmlinux
2 target remote:1234
在这个地方可以看到出现了一个问题:在输入target remote:1234命令时,显示连接超时,这是因为我关掉了另一个Shell打开的MenuOS菜单,重新输入qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S命令打开之后,显示的是连接成功。
接下来就可以设置断点了,在这里用break start_kernel设置一个断点,在此之前内核一直是stop状态,如果按“c”则继续执行,系统开始启动,并启动到start_kernel函数的位置停在断点处,如下图所示:
time系统调用是13号系统调用对应的内核处理函数,即sys_time。接下来在这里用break sys_time设置一个断点,按“c”继续执行,启动MenuOS后执行time命令,程序会停到sys_time这个函数的位置,time命令执行到一半将卡在那里,如下图所示:
通过list命令列出sys_time对应的代码如下图所示:
使用s命令单步执行进入get_seconds(),然后用gdb的finish命令把这个函数全部执行完,再单步执行,一直到return i,获得的就是当前系统时间time的数值。
当执行int 0x80时,CPU会自动跳转到system_call函数,所以我们把断点设置到system_call,并继续执行,如下图所示:
可以看到在MenuOS中执行time-asm命令时,还是停在了原先设定的sys_time这个位置,在system_call这个位置并不能停下。sys_call是一段特殊的汇编代码,只能调试系统调用的内核函数和其他内核函数的处理过程,且system_call还有一个函数原型声明,但它并不是一个普通的函数,,只是一段汇编代码的起点,且内部没有严格遵守函数调用堆栈机制,所以gdb不能完成跟踪执行过程的任务。
3 系统调用在内核代码中的处理过程
3.1 中断向量0x80和system_call中断服务程序入口的关系
在用户态中有一个系统调用xyz(),xyz()系统调用库函数里面用了SYSCALL(在这里即为int 0x80)来触发系统调用,其中中断向量0x80对应system_call中断服务程序入口。
在start_kernel函数里调用的trap_init函数中有一段代码如下:
838#ifdef CONFIG_x86_32
839 set_system_trap_gate(SYSCALL_VECTOR, &system_call);
840 set_bit(SYSCALL_VECTOR, used_vectors);
841#endif
这里通过set_system_trap_gate函数绑定了中断向量0x80(这里的SYSCALL_VECTOR是系统调用的中断向量0x80)和system_call中断服务程序入口后,一旦执行int 0x80,CPU就直接跳转到system_call这个位置来执行,即系统调用的工作机制在start_kernel里初始化好之后,CPU一旦执行到Int 0x80指令就会立即跳转到system_call的位置。
3.2 在system_call汇编代码中的系统调用内核处理函数
system_call这一段代码就是系统调用的处理过程,系统调用是一个特殊一点的中断(或称之为软中断),这一段代码中也有保存现场SAVE_ALL和恢复现场restore_all的过程。同时,system_call_table是一个系统调用的表,EAX寄存器传递的系统调用号,使用者在调用它时会根据EAX寄存器来调用对应的系统调用内核处理函数。
简化后的system_call代码为:
ENTRY(system_call) RING0_INT_FRAME ASM_CLAC pushl_cfi %eax #保存系统调用号 SAVE_ALL #保存现场,将用到的所有CPU寄存器保存到栈中 GET_THREAD_INFO(%ebp) #ebp用于存放当前进程thread_info结构的地址 testl $_TIF_WORK_SYSCALL_ENTRY, TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax #检查系统调用号(系统调用号应小于NR_syscalls) jae syscall_badsys #不合法,跳入异常处理 syscall_call: call *sys_call_table(,%eax,4) #通过系统调用号在系统调用表中找到相应的系统调用内核处理函数,比如sys_time movl %eax, PT_EAX(%esp) #保存返回值到栈中 syscall_exit: testl $_TIF_ALLWORK_MASK, %ecx #检查是否有任务需要处理 jne syscall_exit_work #需要,进入syscall_exit_work,这里是最常见的进程调度时机 restore_all: TRACE_IRQS_TRET #恢复现场 irq_return: INTERRUPT_RETURN #iret
从entry(system_call)开始看这段代码,根据系统调用号来查sys_call_table表中的位置,调用系统调用对应的处理函数,在syscall_exit里面判断当前的任务是否需要处理syscall_exit_work,进入syscall_exit_work,这是最常见的进度调度时机点。
sys_call_table(,%eax,4)中每个表项占4个字节,所以先把系统调用号(EAX寄存器)乘以4,再加上sys_call_table分派表的起始地址,即得到系统调用号对应的系统调用内核处理函数的指针。sys_call_table分派表是由一段脚本根据linux-3.18.6/arch/x86/syscalls/syscall_32.tbl来自动生成的。
3.3 整体上理解系统调用的内核处理过程
system_call流程如下图所示:
总结一下:从系统调用处理过程的入口开始,可以看到SAVE_ALL保存现场,然后找到syscall_call和sys_call_table。call *sys_call_table(,%eax,4)就是调用了系统调用的内核处理函数,之后restore_all和最后有一个INTERRUPT_RETURN(iret)用于恢复现场并返回系统调用到用户态结束。在这个过程当中可能会执行syscall_exit_work,里面有work_pending,其中的work_notifysig是处理信号的。work_pending里还有可能调用schedule,这是一个非常关键的部分。
4 总结
在第四章的基础上,这一章进一步深入内核系统调用处理过程:在用户态下的xyz()就是一个API函数,是系统调用对应的API,其中封装了一个系统调用,这个系统调用 SYSCALL(即为int 0x80汇编语句)来触发中断,对应system_call内核代码的起点,即中断向量0x80对应的中断服务程序入口。从system_call内核代码的起点开始,先是SAVE_ALL保存现场,再是检查EAX寄存器中保存的系统调用号的正确性,接着根据系统调用号在sys_call_table这个分派表查找相对应的系统调用内核处理函数sys_xyz()的入口,得到这个函数的返回值保存到栈中,然后在syscall_exit里面判断当前的任务是否需要处理syscall_exit_work,进入sys_exit_work,进行进程调度,调度完之后就会跳转到restore_all,恢复现场返回系统调用到用户态。