2016-03-25
张超的《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
我的虚拟环境和代码在https://www.shiyanlou.com/courses/reports/1028332
我们这次主要分为两部分:
1.系统调用system_call的处理过程
2.给MenuOS增加time和time-asm命令
1.系统调用system_call的处理过程
490ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
492 ASM_CLAC
493 pushl_cfi %eax # save orig_eax
494 SAVE_ALL
495 GET_THREAD_INFO(%ebp)
496 # system call tracing in operation / emulation
497 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
498 jnz syscall_trace_entry
499 cmpl $(NR_syscalls), %eax
500 jae syscall_badsys
501syscall_call:
502 call *sys_call_table(,%eax,4)
503syscall_after_call:
504 movl %eax,PT_EAX(%esp) # store the return value
505syscall_exit:
506 LOCKDEP_SYS_EXIT
507 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work
restore_all:
TRACE_IRQS_IRET
517restore_all_notrace:
#ifdef CONFIG_X86_ESPFIX32
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << ) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << ) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
#endif
530restore_nocheck:
RESTORE_REGS # skip orig_eax/error_code
532irq_return:
INTERRUPT_RETURN
.section .fixup,"ax"
535ENTRY(iret_exc)
pushl $ # no error code
pushl $do_iret_error
jmp error_code
.previous
_ASM_EXTABLE(irq_return,iret_exc)
system_call
我们的system_call代码如上所述。
有实际开发经验的人都知道,在操作系统上运行的某个应用程序,如果想完成一些实际有用的功能,必然会用到操作系统提供的接口,这些接口被称为系统调用(System Call)。
由操作系统提供的功能,通常应用程序本身是无法实现的。例如对文件进行操作,应用程序必需通过系统调用才能做到,因为只有操作系统才具有直接管理外围设备的权限。又如进
程或线程间的同步互斥操作,也必需经由操作系统对内核变量进行维护才能完成。
应用程序的进程通常在user模式下运行,当它调用一个系统调用时,进程进入kernel模式,执行的是kernel内部的代码,从而具有执行特权指令的权限,完成特定的功能。换句话说,
系统调用是应用程序主动进入操作系统内核的入口。
由程序员的代码主动发起的中断。有两种用法:(1)用来实现系统调用;(2)通知调试器某个特殊事件。
至此,我们发现了中断与系统调用的关系:系统调用是一种特殊的中断类型。
系统调用的处理例程在IDT表中占有一项。这一项是在trap_init函数中被初始化的,如下:
set_system_gate(SYSCALL_VECTOR,&system_call);
当系统调用发生时,通过中断机制,系统调用例程system_call被调用。system_call由汇编语言和C的代码构成,它的执行过程大概分为4个步骤(注意参数的传入和返回值的传出过
程):
从寄存器中取出系统调用号(system call number)和输入参数,然后将这些寄存器的值压入kernel栈中。这一部分的代码用汇编写成。
根据系统调用号(system call number)查找系统调用分派表(system call dispatch table),找到系统调用服务例程(system call service routine )。汇编语言。
调用查到的系统调用服务例程。这一部分用C语言写成,因为已经将输入参数保存在kernel栈中,所以在C函数的参数表中能够拿到输入参数,使得系统调用服务例程在表面上
看与一个普通的C函数没有区别。
将系统调用服务例程的返回值出栈,重新保存在寄存器中。汇编语言。
上面描述的系统调用例程system_call在kernel空间中执行。在执行前,系统调用号和输入参数已经存入了寄存器,这个存入过程由user空间的代码完成。实际上,每个真正的系统调
用基本上都有一个封装它的库函数,一般是在这个库函数中完成系统调用号和输入参数的保存动作。当系统调用例程system_call执行完毕后,返回值通过寄存器再传回user空间的库
函数。
下面详细地介绍上面所讲的4个步骤。
在第1步之前,user空间的封装函数已经将对应的系统调用号保存在eax寄存器中,将输入参数保存在ebx, ecx, edx, esi,以及edi寄存器中(因此最多传6个参数,包括系统调用号)。
第1步中将输入参数寄存器的值压入kernel栈的操作由汇编代码__SAVE_ALL宏完成。如下:
#define __SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;
第2步中的系统分派表在kernel代码中以变量sys_call_table表示。查找系统调用服务例程的动作就是从sys_call_table里找系统调用号(存在eax寄存器中)指向的那一项,如下:
syscall_call:
call *sys_call_table(,%eax,4)
sys_call_table中的项在sys_call_table.c文件中定义:
syscall_handler_t *sys_call_table[] = {
......
[ __NR_exit ] (syscall_handler_t *) sys_exit,
[ __NR_fork ] (syscall_handler_t *) sys_fork,
[ __NR_read ] = (syscall_handler_t *) sys_read,
[ __NR_write ] = (syscall_handler_t *) sys_write,
......
[ __NR_socketcall ] (syscall_handler_t *) sys_socketcall,
......
};
在这里我们注意到一些常用的系统调用号,如,exit系统调用号为__NR_exit = 1,fork系统调用号为__NR_fork =2,read系统调用号为__NR_read = 3,write系统调用号为
__NR_write =4,所有socket相关的API的系统调用号都是__NR_socketcall= 102。
第3步,执行C函数实现的系统调用例程。该例程最多接受6个参数(包括系统调用号),返回值是一个整型。返回值为非负,表示执行成功;返回值为负,表示执行出错,该错误码的
绝对值会最后存在user空间的errno全局变量中。
第4步,调用syscall_exit_work退出系统调用,并从kernel模式回到user模式。第3步的C函数执行return err的时候,编译后的代码已经将返回值存在了eax寄存器中。
最后,回到user模式的封装函数中,对返回值eax进行检查。如果eax小于0,则将eax的相反数(即绝对值)存到errno全局变量中,同时将eax值置为-1,这时封装函数返回-1;如果
eax大于等于0,则封装函数返回eax的值。
具体分析:
大致过程:SYSterm_call---运行到SAVE—ALL(保护现场)继续运行----table表找对应程序----iret结束返回
分析call对应函数功能(简化)
注意)通过SAVE_ALL宏完成把所有相关寄存器的内容都保存在堆栈中
GET_THREAD_INFO(%ebp):
将当前信息保存在ebp
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%esp) jnz syscall_trace_entry:
判断是否 trace调用
cmpl $(NR_syscalls), %eax jae syscall_badsys:
判断系统调用号是否超出最大值
call *sys_call_table(,%eax,4):
系统调用的数字实际上是一个序列号,表示其在系统的一个数组sys_call_table[]中的位置。
movl %eax,PT_EAX(%esp):
保存系统调用的返回值
DISABLE_INTERRUPTS(CLBR_ANY):
屏蔽其他系统调用
movl TI_flags(%esp), %eax:
寄存器ecx是通用寄存器,在保护模式中,可以作为内存偏移指针(此时,DS作为 寄存器或段选择器),此时为返回到系统调用之前做准备
testl $_TIF_ALLWORK_MASK, %eax jne syscall_exit_work :
退出系统调用之前,检查是否需要处理信号
RESTORE_REGS 4
:x86架构恢复寄存器代码
INTERRUPT_RETURN
即iret: 系统调用是通过软中断指令
INT 0x80 实现的,而这条INT 0x80指令就被封装在C库的函数中。(软中断和我们常说的硬中断不同之处在于,软中断是由指令触发的,而不是由硬件外设引起的。)
INT 0x80 这条指令的执行会让系统跳转到一个预设的内核空间地址,它指向系统调用处理程序,即system_call函数。
实验过程如下:
1.先切换到我们的虚拟机的LinuxKernel目录下
2.qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
3.右键->新建窗口(水平分割)-> gdb
4.file linux-3.18.6/vmlinux
5.target remote:1234
6.b start_kernel
7.c
2.给MenuOS增加time和time-asm命令
0)更新menu代码到最新版
1)在main函数中增加MenuConfig
2)增加对应的Time函数和TimeASM函数
3)make rootfs
具体如下: