系统调用

对于应用程序而言,操作系统内核的作用体现在一组可以供其调用的函数,称为“系统调用”。系统调用也可以称为“系统服务”。

两种模式切换

1、 内核态到用户态很容易,运行于系统态的CPU可以通过一些只允许在系统态使用的特权指令改变其运行状态。

2、 用户态到内核态,一般而言有三种方式。

     a) 中断

在开中断前提下,只要有外部设备的中断请求到来,CPU就会自动转入系统空间,并从某个预定的地址开始执行指令。中断发生在两条指令执行之间,因而不会使正在执行的指令半途而废。

     b) 异常

异常发生在指令执行过程中,异常发生会终止指令的运行。

     c) 自陷

中断和异常发生的时机都是不可控的,自陷是程序主动的动作。

系统调用从用户态到内核态有三种方式:调用门、自陷和快速系统调用。下面分别描述这三种调用方式。

1、自陷

让CPU主动地进入系统空间,大多数CPU都设置了专门的自陷指令。一执行这样的指令,CPU就转入系统空间并从某个预定的地址开始执行指令。对于CPU而言,这是成功执行一条指令的结果,所以是主动的,尽管该指令是由程序员安排的。对于应用程序而言,自陷指令就相当于一次子程序调用,只是这个子程序在系统空间内。

CPU在用户空间执行“int0x2e”指令,CPU的运行状态从用户态进入内核态;并从“任务状态段”TSS中装入本线程的系统空间堆栈段寄存器SS和堆栈指针ESP,CPU中有一个“任务寄存器”TR,通过TR的指引找到TSS中的SS和ESP;再依次把用户空间的堆栈段寄存器SS、堆栈指针ESP、标志位寄存器EFLAGS、代码段寄存器CS、指令计数器EIP的内容压入系统空间堆栈;然后从“中断向量表”IDT中以0x2e为“中断向量”,即数组下标获取程序入口,开始执行内核中的程序。从系统调用返回则通过“中断返回”指令iret实现上述过程的逆过程。

2、调用门

调用门可以认为是对自陷机制的改进,x86系列的调用门机制几乎无人使用。

3、快速系统调用

从Pentium 2开始,Intel在x86系统CPU中加入了一对指令sysenter和sysexit,用来实现快速系统调用。并为此增加了三个寄存器:SYSENTER_CS_MSR、SYSENTER_EIP_MSR、SYSTEM_ESP_MSR。

执行sysenter指令时,CPU根据这三个寄存器的内容设置跳转目标和堆栈指针。因此,只要预先妥善设置这三个MSR寄存器,就可以使得执行指令sysenter以后CPU的CS寄存器就指向系统空间,EIP就指向内核中快速系统调用的入口,而SS和ESP的组合则指向系统空间堆栈的顶部。MSR寄存器只能通过特权指令rdmsr和wrmsr读写。

以ReadFile函数为例说明系统调用的整个过程:

         Windows应用程序通过Win32 API调用这个界面所定义的库函数,这些库函数基本都是在DLL中定义的,比如kernel32.dll。

1、  ReadFile函数体内会调用NtReadFile函数,NtReadFile函数是Windows的一个系统调用。该函数的实现在ntoskrnl.exe中。但是ReadFile运行在用户空间,而NtReadFile运行在内核空间,用户空间不可能直接调用内核空间的函数。因此,在用户空间中还存在一个也叫NtReadFile的中介函数。代码如下:

__declspec(naked)__stdcall

NtReadFile(intdummy0,int dummy1,int dummy2)

{

           __asm{

                    Mov eax,152             //152即NtReadFile的调用号

                    Mov ecx,KUSER_SHARED_SYSCALL

                    Call [ecx]

                    Ret 9

}

代码中的KUSER_SHARED_SYSCALL是用户空间的一个地址,实际上是0x7FFE0300,这里存储了一个函数指针,指向KiIntSystemCall或KiFastSystemCall之一。系统初始化的时候根据CPU是否支持快速系统调用而使该指针指向具体的函数。

用户空间的NtReadFile向上代表着内核函数NtReadFile,向下则代表着想要调用内核函数NtReadFile的那个函数,在这里指ReadFile。它本身并不提供什么附加的功能,这样的函数成为“stub”。Windows中几乎所有的stub函数都在ntdll.dll中。ntdll.dll是个基本的“系统DLL”,在系统初始化阶段就会被装入,要不然什么进程都运行不了。从此一直驻留内存,在任意进程中载入的地址都相同。其实CPU写入的是系统空间的地址0xFFDF0300,但是系统空间0XFFDF0300的地方与用户空间0X7FFE0300的地方好像一条隧道的两端,两块64KB的虚存空间都映射到同一块物理内存。

       对于中断向量,并非只有外部中断才有中断向量,异常和自陷也有。它们共用一张IDT表,比如int  0x00就是用于除0异常。而int 0x2e用于自陷。Linux中用int 0x80自陷。IDT表总共256项,每一项8个字节,共2K字节。IDT表项的格式如下所示:

        

Offset 31…16

P

DPL

0D110

000

保留5位

 

段寄存器Selector

Offset 15…0

P:表示本IDT项是否有效,占1位;

DPL:特权级别,占2为;00位ring0,11为ring3;

D:1=32位,0=16位,占1位。

两个offset和在一起即程序入口。

整个IDT表就是一个类型为上图的数据的数组。例如,其中的int 0x2a,程序入口为_KiGetTickCount,这是让用户空间的程序获得高精度时钟计数的;Int 0x2e,程序入口为_KiSystemService,即内核中的KiSystemService(),系统调用的内核入口函数,用来实现Windows系统调用。Windows允许在内核态中发起系统调用,例如ReadFile,在内核中有一个ZwReadFile的系统调用。Linux不允许内核系统调用。对于系统调用目标函数的调用,最终是在KiSystemService中实现的,KiSystemService函数中有一条指令“call ebx”,此时ebx中的内容就是系统调用号,通过这条指令完成系统调用。系统调用操作完成后需要返回,返回操作中有个宏操作CHECK_FOR_APC_DELIVER,该宏的作用是检查是否有执行用户空间“异步过程调用”即APC的请求,如果有就“递交”该请求,为在用户空间执行相应的函数做好准备。APC机制与linux的“信号(signal)”机制类似。APC,相当于由内核向用户空间程序发出的中断请求。

CPU通过int 0x2e进入内核态,需要将用户空间的一些寄存器信息压入系统空间堆栈,这些信息是返回用户空间时所必需的。压入后的系统空间堆栈如图所示:


         每个线程都有自己的系统空间堆栈,其SS和ESP的内容保存在一个“任务状态段”即TSS的数据结构里。CPU中有一个称为“任务寄存器”即TR的段寄存器。每当从用户空间到系统空间,CPU就通过TR找到TSS并从中获取当前进程的SS和ESP。然后将用户空间的那几个寄存器压入系统堆栈。可以看出,自陷方式比较复杂,因此引入“快速系统调用”。

         Windows内核要求CPU运行在内核态时,段寄存器FS必须执行KPCR数据结构。PCR,全称ProcessorControl Region,处理器控制区,CPU的每一个核都有一个KPCR数据结构。从一个CPU的KRCR结构出发,可以直接或间接地找到与该CPU有关的许多信息。32位保护模式下,段寄存器汇总并非一个基地址,而是一个16位的Selector,执行“全局段描述表”GDT中的某个64位的表项。

系统调用(以ReadFile为例)过程中,接口的调用顺序如图所示:



GDT:全局段描述表,主要存放操作系统和各任务公用的描述符,如公用的数据和代码段描述符、各任务的TSS描述符和LDT描述符。GDT是一个结构数组,数组成员为KGDTENTRY数据结构,里面描述了段的起点和大小、保护模式、访问权限等信息。CPU中的GDTR寄存器指向内存中的GDT。GDTR为16位,高13位是GDT数组下标,接下来一位为LDT/GDT选择项,接下来两位表示特权级别,00表示ring0,11表示ring3。GDT表的大小为NUM_GDT,定义为28。


参考资料:Windows内核情景分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值