Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3358839
  • 博文数量: 258
  • 博客积分: 9440
  • 博客等级: 少将
  • 技术积分: 6998
  • 用 户 组: 普通用户
  • 注册时间: 2009-05-03 10:28
个人简介

-- linux爱好者,业余时间热衷于分析linux内核源码 -- 目前主要研究云计算和虚拟化相关的技术,主要包括libvirt/qemu,openstack,opennebula架构和源码分析。 -- 第五届云计算大会演讲嘉宾 微博:@Marshal-Liu

文章分类

全部博文(258)

文章存档

2016年(1)

2015年(4)

2014年(16)

2013年(22)

2012年(41)

2011年(59)

2010年(40)

2009年(75)

分类: LINUX

2009-12-20 13:06:16

1 引起系统调用的两种途径:

(1)int $0×80 , 老式linux内核版本中引起系统调用的唯一方式

(2)sysenter汇编指令

2 退出系统调用的两种方式

(1) iret 汇编指令

(2) sysexit 汇编指令

3 两种系统调用的执行过程

(1)向量128的内核入口点  set_system_gate(0×80,&system_call),从函数的名字就可以看出这是一个系统门,这个门的8个字节包括

segment selector  = __KERNEL_CS    Offset = system_call()系统调用处理程序的地址

Type = 15 表示这是个陷阱,陷阱的特点就是不禁用中断    DPL =3 用户态可以访问的一个陷阱

(2)现在执行流程切换到了system_call函数

先把%eax(系统调用号)压入内核栈,这里我们应该清楚,此时内核栈上的压入信息的情况,下图是从用户态到内核态时硬件自动在内核栈上保存的信息

很明显尽皆着就是eax(对于中断来说,这里就是中断号或出错码),然后执行宏SAVE_ALL,把其他的寄存器保存在堆栈上,然后检测当前用户态进程是否处于debug状态,即EFLAGS.TI = 1

这是通过取thread_info中的eflag来实现的。

     如果是,跳到syscall_trace_entry,否则,判断当前系统调用号是否合法,即不大于最大系统调用号即可。如果合法,执行call *sys_call_table(,%eax,4).

sys_call_table[]数组,系统调用分派表数组,参数即是%eax * 4就可以找到相应的系统调用表中的一项,因为每项4个字节。

可见syscall_table_32.S中的系统调用。我们来看看call *sys_call_table(,%eax,4)是怎么寻址的,  *(0+%eax*4+sys_call_table)这是call的内容,这么写出来就很明显了。

%eax*4+sys_call_table指向了系统表中的某一项,然后加*就是取其内容,即系统调用处理函数。

    从系统调用退出时,先把返回值%eax放到内核栈上保存用户态eax的那个地址上,即当恢复到用户态时,eax中就是返回值。然后禁用中断,主要是为了防止中断丢失,但是什么情况下会丢失,没弄清楚?

然后检查EFLAGS中的所有标志有没有被设置,如果没有就restore_call,如果有设置,就跳到syscall_exit_work.详见linux中断和异常分析部分。

  (3)int $0×80要执行几个一致性和安全性检查,因此速度比较慢,后来就曾加了一种快速的从用户态跳到内核态的方法“sysenter”。

sysenter指令使用了三种特殊的寄存器,他们必须装入以下信息:

a. SYSENTER_CS_MSR    内核段的段选择符

b. SYSENTER_EIP_MSR  内核入口点的线性地址

c. SYSENTER_ESP_MSR 内核堆栈指针

当sysenter执行的时候,cpu执行以下操作

cs = SYSENTER_CS_MSR

eip = SYSENTER_EIP_MSR

esp = SYSENTER_ESP_MSR

ss = SYSENTER_CS_MSR

很明显的SYSENTER_CS_MSR SYSENTER_ESP_MSR SYSENTER_CS_MSR保存的是内核的信息,这样赋值之后,就从用户态切换到了内核态。

这几个寄存器是由谁初始化的呢?

在内核初始化期间,系统中每个cpu一旦执行函数enble_sep_cpu()之后,上面三个寄存器就被初始化了,初始化的值分别是:

SYSENTER_CS_MSR = __KERNEL_CS

SYSENTER_EIP_MSR = sysenter_enter()函数的线性地址

SYSENTER_ESP_MSR = 本地TSS的末端的线性地址

    sysenter的执行流程:

a. 标准库中的封装例程把系统调用号装入eax寄存器,并调用__kernel_vsyscall()函数

__kernel_vsyscall:

pushl %ecx

pushl %edx

pushl %ebp

movl %esp,%ebp

sysenter

b. 当执行完sysenter指令,CPU从用户态切换到内核态,内核开始执行sysenter_entry()函数,这个函数由SYSENTER_EIP_MSR. 下面来看看sysenter_entry()函数

  (1)建立内核堆栈         movl    -508(%esp), %esp

开始时,esp指向本地TSS的第一个位置(这是由SYSENTER_ESP_MSR决定的),因为本地TSS的大小为512个字节。因此,sysenter指令把本地TSS中偏移量为4处的内容即esp0的内容赋值给esp,

esp0总是存放当前进程内核堆栈的指针。

(2)sti

(3)pushl $(__USER_DS)

        pushl %ebp

        pushfl

        pushl $(__USER_CS)

        pushl $SYSENTER_RETURN

  (4) movl (%ebp) , %ebp

4 . sysenter 退出系统调用

和int $0×80一样,退出时先判断current->thread_info中的eflags标志,如果有被设置的标志,就执行相应的处理,跳转代resume_userspace或work_pending处,如果都没有设置,就直接快速返回

movl 40(%esp), %edx

movl 52(%esp), %ecx

xorl %ebp, %ebp

sti

sysexit

5 sysexit指令

实现从内核态切换到用户态,当执行这条指令时,cpu执行下面的操作

(1) cs = SYSENTER_CS_MSR + 16

为什么是加16?

从内核态跳到用户态,即cs = __KERNEL_CS  转变为 cs = __USER_CS ,在GDT表里这两项相差了2,我们再考虑段选择符的结构,前13位是索引,那么索引加了2,就相当于是段选择符加了16

(2)eip = edx

  (3) ss = SYSENTER_CS_MSR + 24    ss加载的是用户数据段的段选择符,可以算出为什么加了24

(4)esp = ecx

6 执行完上述指令,cpu就开始执行eip处的代码

以上就是两种系统调用的简要流程!

阅读(2952) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~