当前位置:文档之家› (1)-系统调用篇

(1)-系统调用篇

Windows的地址空间分用户模式与内核模式,低2GB的部分叫用户模式,高2G的部分叫内核模式,位于用户空间的代码不能访问内核空间,位于内核空间的代码却可以访问用户空间一个线程的运行状态分内核态与用户态,当指令位于用户空间时,就表示当前处于内核态,当指令位于内核空间时,就处于内核态.一个线程由用户态进入内核态的途径有3种典型的方式:1、主动通过int 2e(软中断自陷方式)或sysenter指令(快速系统调用方式)调用系统服务函数,主动进入内核2、发生异常,被迫进入内核3、发生硬件中断,被迫进入内核现在讨论第一种进入内核的方式:(又分为两种方式)1、通过老式的int 2e指令方式调用系统服务(因为老式cpu没提供sysenter指令)如ReadFile函数调用系统服务函数NtReadFileKernel32.ReadFile() //点号前面表示该函数的所在模块{//所有Win32 API通过NTDLL中的系统服务存根函数调用系统服务进入内核NTDLL.NtReadFile();}NTDLL.NtReadFile(){Mov eax,152 //我们要调用的系统服务函数号,也即SSDT表中的索引,记录在eax中If(cpu不支持sysenter指令){Lea edx,[esp+4] //用户空间中的参数区基地址,记录在edx中Int 2e //通过该自陷指令方式进入KiSystemService,‘调用’对应的系统服务}Else{Lea edx,[esp +4] //用户空间中的参数区基地址,记录在edx中Sysenter //通过sysenter方式进入KiFastCallEntry,‘调用’对应的系统服务}Ret 36 //不管是从int 2e方式还是sysenter方式,系统调用都会返回到此条指令处}Int 2e的内部实现原理:该指令是一条自陷指令,执行该条指令后,cpu会自动将当前线程的当前栈切换为本线程的内核栈(栈分用户栈、内核栈),保存中断现场,也即那5个寄存器。

然后从该cpu的中断描述符表(简称IDT)中找到这个2e中断号对应的函数(也即中断服务例程,简称ISR),jmp 到对应的isr处继续执行,此时这个ISR 本身就处于内核空间了,当前线程就进入内核空间了Int 2e指令可以把它理解为intel提供的一个内部函数,它内部所做的工作如下Int 2e{Cli //cpu一中断,立马自动关中断Mov esp, TSS.内核栈地址 //切换为内核栈,TSS中记录了当前线程的内核栈地址Push SSPush espPush eflagsPush csPush eip //这5项工作保存了中断现场【标志、ip、esp】Jmp IDT[中断号] //跳转到对应本中断号的isr}IDT的整体布局:【异常->空白->5系->硬】(推荐采用7字口诀的方式重点记忆)异常:前20个表项存放着各个异常的描述符(IDT表不仅可以放中断描述符,还放置了所有异常的异常处理描述符,0x00-0x13)保留:0x14-0x1F,忽略这块号段空白:接下来存放一组空闲的保留项(0x20-0x29),供系统和程序员自己分配注册使用5系:然后是系统自己注册的5个预定义的软中断向量(软中断指手动的INT指令)(0x2A-0x2E 5个系统预注册的中断向量,0x2A:KiGetTickCount, 0x2B:KiCallbaclReturn0x2C:KiRaiseAssertion, 0x2D:KiDebugService, 0x2E:KiSystemService)硬:最后的表项供驱动程序注册硬件中断使用和自定义注册其他软中断使用(0x30-0xFF)下面是中断号的具体的分配情况:0x00-0x13固定分配给异常:0x00: Divide error(故障)0x01: Debug (故障或陷阱)0x02: 保留未用(为非屏蔽中断保留的,NMI)0x03: breakpoint(陷阱)0x04: Overflow(陷阱)0x05: Bounds check(故障)0x06: Invalid Opcode(故障)0x07: Device not available(故障)0x08: Double fault(异常中止)0x09: Coprocessor segment overrun(异常中止)0x0A: Invalid TSS(故障)0x0B: Segment not present(故障)0x0C: Stack segment(故障)0x0D: General protection(故障)0x0E: Page fault(故障)0x0F: Intel保留0x10: Floating point error(故障)0x11: Alignment check(故障)0x12: Machine check(异常中止)0x13: SIMD floating point(故障)0x14-0x1f:Intel保留给他公司将来自己使用(OS和用户都不要试图去使用这个号段,不安全)----------------------以下的号段可用于自由分配给OS、硬件、用户使用----------------------- linux等其他系统是怎么划分这块号段的,不管,我们只看Windows的情况0x20-0x29:Windows没占用,因此这块号段我们也可以自由使用0x2A-0x2E:Windows自己本身使用的5个中断号0x30-0xFF:Windows决定把这块剩余的号段让给硬件和用户使用参见《寒江独钓》一书P93页注册键盘中断时,搜索空闲未用表项是从0x20开始,到0x29结束的,就知道为什么寒江独钓是在这段范围内搜索空白表项了(其实我们也完全可以从0x14开始搜索)Windows系统中,0x30-0xFF这块号段让给了硬件和用户自己使用。

事实上,这块号段的开头部分默认都是让给硬件IRQ使用的,也即是分配给硬件IRQ的。

IRQ N默认映射到中断号0x30+N,如IRQ0用于系统时钟,系统时钟中断号默认对应就是0x30。

当然程序员也可以修改APIC(可编程中断控制器)将IRQ映射到自定义的中断号。

IRQ对外部设备分配,但IRQ0,IRQ2,IRQ13必须如下分配:IRQ0 ---->间隔定时设备IRQ2 ---->8259A芯片IRQ13 ---->外部数学协处理器其余的IRQ可以任意分配给外部设备。

虽然一个IRQ只对应一个中断号,但是由于IRQ数量有限,而设备种类成千上万,因此多个设备可以使用同一个IRQ,进而,多个设备可以分配同一个中断号。

因此,一个中断号可以共享给多个设备同时使用。

明白了IDT,就可以看到0x2e号中断的isr为KiSystemService,顾名思义,这个中断号专用于提供系统服务。

在正式分析KiSystemService,前,先看下几个辅助函数SaveTrap() //这个函数用来保存寄存器现场和其他状态信息{Push 0 //LastErrorPush ebpPush ebxPush esiPush ediPush fs //此时的fs若是从用户空间自陷进来的就指着TEB,反之指着kpcrPush kpcr.ExceptionListPush kthread.PreviousModeSub esp,0x48 //腾给调式寄存器保存用-----------至此,上面的这些语句连同int 2e中的语句在栈上构造了一个trap帧----------------- Mov CurTrapFrame,esp //当前Trap帧的地址Mov CurTrapFrame.edx, kthread.TrapFrame //将上次的trap帧地址记录到edx成员中Mov kthread.TrapFrame, CurTrapFrame, //修改本线程当前trap帧的地址Mov kthread.PreviousMode,GetMode(进入内核前的CS) //根据CS自动确定上次模式Mov kpcr.ExceptionList,-1 //表示刚进入内核时,尚未安装sehMov fs,kpcr //一进入内核就让fs改指向当前cpu的描述符kpcr,不再指向TEBIf(当前线程处于调试状态)保存DR0-DR7到trap帧中}FindTableCall() //这个函数用来查表,拷贝参数,调用系统服务{Mov edi,eax //系统函数号,低12位为索引,第13为表示是哪张系统服务表中的索引Mov eax, edi.低12位 //eax=真正的服务号If(edi.第13位=1) //if这是shadow SSDT中的系统函数号{If(当前线程.服务描述符表!=shadow)当前线程.服务描述符表=shadow //换用另外一张描述符表}服务表描述符=当前线程.服务描述符表[edi.第13位]Mod edi=服务表描述符.base //这个系统服务表的地址Mov ebx,[edi+eax*4] //查表获得这个函数的地址Mov ecx=服务表描述符.Number[eax] //查表获得的这个系统函数的参数大小Mov esi,edx //esi=用户空间中的参数地址Mov edi,esp //esp已经为内核栈的栈顶地址Rep movsb //将所有参数从用户空间复制到内核空间,相当于N个连续push压参Call ebx //调用对应的系统服务函数}KiSystemService()//int 2e的isr,内核服务函数总入口,注意这个函数可以嵌套、递归!!!{SaveTrap();Sti //开中断---------------上面保存完寄存器等现场后,开始查SSDT表调用系统服务------------------ FindTableCall();---------------------------------调用完系统服务函数后------------------------------ Move esp,kthread.TrapFrame; //将栈顶回到trap帧结构体处Cli //关中断If(上次模式==UserMode){Call KiDeliverApc //遍历执行本线程的内核APC和用户APC队列中的所有APC函数清理Trap帧,恢复寄存器现场Iret //返回用户空间}Else{返回到原call处后面的那条指令处}}上面所说的trap帧(TrapFrame)是指一个结构体,用来保存系统调用、中断、异常发生时的寄存器现场,方便以后回到用户空间/回到中断处时,恢复那些寄存器的值,继续执行Trap帧中除了保存了所有寄存器现场外,还附带保存了一些其他信息,如seh链表的地址等必须说一下trap帧的结构体布局定义:typedef struct _KTRAP_FRAME //Trap现场帧{------------------这些是KiSystemService保存的---------------------------ULONG DbgEbp;ULONG DbgEip;ULONG DbgArgMark;ULONG DbgArgPointer;ULONG TempSegCs;ULONG TempEsp;ULONG Dr0;ULONG Dr1;ULONG Dr2;ULONG Dr3;ULONG Dr6;ULONG Dr7;ULONG SegGs;ULONG SegEs;ULONG SegDs;ULONG Edx;//xy 这个位置不是用来保存edx的,而是用来保存上个Trap帧,因为Trap帧是可以嵌套的 ULONG Ecx; //中断和异常引起的自陷要保存eax,系统调用则不需保存ecxULONG Eax;//中断和异常引起的自陷要保存eax,系统调用则不需保存eaxULONG PreviousPreviousMode;struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh链表的开头地址ULONG SegFs;ULONG Edi;ULONG Esi;ULONG Ebx;ULONG Ebp;---------------------------------------------------------------------------------------- ULONG ErrCode;//发生的不是中断,而是异常时,cpu还会自动在栈中压入对应的具体异常码在这儿-----------下面5个寄存器是由int 2e内部本身保存的或KiFastCallEntry模拟保存的现场--------- ULONG Eip;ULONG SegCs;ULONG EFlags;ULONG HardwareEsp;ULONG HardwareSegSs;---------------以下用于用于保存V86模式的4个寄存器也是cpu自动压入的-------------------ULONG V86Es;ULONG V86Ds;ULONG V86Fs;ULONG V86Gs;} KTRAP_FRAME, *PKTRAP_FRAME;KPCR与KPRCB结构,都是用来描述处理器的,前者叫处理器描述符,后者叫处理器控制块Struct KPCR{KPCR_TIB Tib;//类似于TEB.TIB,内部第一个字段都是ExceptionListKPCR* self;//自身结构体的地址,方便直接寻址KPRCB* kprcb;//处理器控制块的地址、KIRQL irql;//当前cpu的irqlUSHORT* IDT;//本cpu的IDT地址,一有中断/异常就去这个表找isr、eprUSHORT* GDT;//全局描述符表地址KTSS* TSS;//记录了本cpu上当前运行线程的状态信息,重要字段有内核栈地址,IO权限位图……}Struct KPRCB{KTHREAD* CurrentThread;//本cpu上当前运行的线程KTHREAD* NextThread;//本cpu上将抢占当前线程的下个线程(抢占式调度核心)BYTE CpuID;//不多说ULONG KernelTime,UserTime;//本cpu的累计运行时间统计信息……}系统中有两张“系统服务表”,即SSDT和shadow SSDT。

相关主题