-- linux爱好者,业余时间热衷于分析linux内核源码 -- 目前主要研究云计算和虚拟化相关的技术,主要包括libvirt/qemu,openstack,opennebula架构和源码分析。 -- 第五届云计算大会演讲嘉宾 微博:@Marshal-Liu
分类: 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处的代码
以上就是两种系统调用的简要流程!