Linux系统调用system_call

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

Linux系统调用system_call

Linux系统调用system_call

Linux系统调用system_call

2.给MenuOS增加time和time-asm命令

0)更新menu代码到最新版

1)在main函数中增加MenuConfig

2)增加对应的Time函数和TimeASM函数

3)make rootfs

具体如下:

Linux系统调用system_call

Linux系统调用system_call

Linux系统调用system_call

Linux系统调用system_call

Linux系统调用system_call

Linux系统调用system_call

上一篇:基于Simple Image Statistics(简单图像统计,SIS)的图像二值化算法。


下一篇:25个站长必备的SEO优化工具